filterEntitySerializationUsingSiteIds()   A
last analyzed

Complexity

Conditions 6
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.2222
c 0
b 0
f 0
cc 6
nc 2
nop 2
1
<?php
2
3
namespace Wikibase\Repo\Api;
4
5
use ApiResult;
6
use Serializers\Serializer;
7
use SiteLookup;
8
use Status;
9
use Wikibase\DataModel\Entity\EntityDocument;
10
use Wikibase\DataModel\Entity\EntityId;
11
use Wikibase\DataModel\Reference;
12
use Wikibase\DataModel\SerializerFactory;
13
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
14
use Wikibase\DataModel\SiteLinkList;
15
use Wikibase\DataModel\Statement\Statement;
16
use Wikibase\DataModel\Statement\StatementList;
17
use Wikibase\DataModel\Term\AliasGroupList;
18
use Wikibase\DataModel\Term\TermList;
19
use Wikibase\Lib\Serialization\CallbackFactory;
20
use Wikibase\Lib\Serialization\SerializationModifier;
21
use Wikibase\Lib\Store\EntityRevision;
22
use Wikibase\Lib\Store\EntityTitleLookup;
23
use Wikibase\Lib\TermLanguageFallbackChain;
24
use Wikibase\Repo\Dumpers\JsonDataTypeInjector;
25
use Wikimedia\Assert\Assert;
26
27
/**
28
 * Builder of MediaWiki ApiResult objects with various convenience functions for adding Wikibase concepts
29
 * and result parts to results in a uniform way.
30
 *
31
 * This class was introduced when Wikibase was reduced from 2 sets of serializers (lib & data-model) to one.
32
 * This class makes various modifications to the 1 standard serialization of Wikibase concepts for public exposure.
33
 * The resulting format can be seen as the public serialization of Wikibase concepts.
34
 *
35
 * Many concepts such as "tag name" relate to concepts explained within ApiResult.
36
 *
37
 * @license GPL-2.0-or-later
38
 * @author Addshore
39
 * @author Daniel Kinzler
40
 */
41
class ResultBuilder {
42
43
	/**
44
	 * @var ApiResult
45
	 */
46
	private $result;
47
48
	/**
49
	 * @var EntityTitleLookup
50
	 */
51
	private $entityTitleLookup;
52
53
	/**
54
	 * @var SerializerFactory
55
	 */
56
	private $serializerFactory;
57
58
	/**
59
	 * @var Serializer
60
	 */
61
	private $entitySerializer;
62
63
	/**
64
	 * @var SiteLookup
65
	 */
66
	private $siteLookup;
67
68
	/**
69
	 * @var PropertyDataTypeLookup
70
	 */
71
	private $dataTypeLookup;
72
73
	/**
74
	 * @var bool|null when special elements such as '_element' are needed by the formatter.
75
	 */
76
	private $addMetaData;
77
78
	/**
79
	 * @var SerializationModifier
80
	 */
81
	private $modifier;
82
83
	/**
84
	 * @var CallbackFactory
85
	 */
86
	private $callbackFactory;
87
88
	/**
89
	 * @var int
90
	 */
91
	private $missingEntityCounter = -1;
92
93
	/**
94
	 * @var JsonDataTypeInjector
95
	 */
96
	private $dataTypeInjector;
97
98
	/**
99
	 * @param ApiResult $result
100
	 * @param EntityTitleLookup $entityTitleLookup
101
	 * @param SerializerFactory $serializerFactory
102
	 * @param Serializer $entitySerializer
103
	 * @param SiteLookup $siteLookup
104
	 * @param PropertyDataTypeLookup $dataTypeLookup
105
	 * @param bool|null $addMetaData when special elements such as '_element' are needed
106
	 */
107
	public function __construct(
108
		ApiResult $result,
109
		EntityTitleLookup $entityTitleLookup,
110
		SerializerFactory $serializerFactory,
111
		Serializer $entitySerializer,
112
		SiteLookup $siteLookup,
113
		PropertyDataTypeLookup $dataTypeLookup,
114
		$addMetaData = null
115
	) {
116
		$this->result = $result;
117
		$this->entityTitleLookup = $entityTitleLookup;
118
		$this->serializerFactory = $serializerFactory;
119
		$this->entitySerializer = $entitySerializer;
120
		$this->siteLookup = $siteLookup;
121
		$this->dataTypeLookup = $dataTypeLookup;
122
		$this->addMetaData = $addMetaData;
123
124
		$this->modifier = new SerializationModifier();
125
		$this->callbackFactory = new CallbackFactory();
126
127
		$this->dataTypeInjector = new JsonDataTypeInjector(
128
			$this->modifier,
129
			$this->callbackFactory,
130
			$dataTypeLookup
131
		);
132
	}
133
134
	/**
135
	 * Mark the ApiResult as successful.
136
	 *
137
	 * { "success": 1 }
138
	 *
139
	 * @param bool|int|null $success
140
	 */
141
	public function markSuccess( $success = true ) {
142
		$value = (int)$success;
143
144
		Assert::parameter(
145
			$value == 1 || $value == 0,
146
			'$success',
147
			'$success must evaluate to either 1 or 0 when casted to integer'
148
		);
149
150
		$this->result->addValue( null, 'success', $value );
151
	}
152
153
	/**
154
	 * Adds a list of values for the given path and name.
155
	 * This automatically sets the indexed tag name, if appropriate.
156
	 *
157
	 * To set atomic values or records, use setValue() or appendValue().
158
	 *
159
	 * @see ApiResult::addValue
160
	 * @see ApiResult::setIndexedTagName
161
	 * @see ResultBuilder::setValue()
162
	 * @see ResultBuilder::appendValue()
163
	 *
164
	 * @param array|string|null $path
165
	 * @param string $name
166
	 * @param array $values
167
	 * @param string $tag tag name to use for elements of $values if not already present
168
	 */
169
	public function setList( $path, $name, array $values, $tag ) {
170
		$this->checkPathType( $path );
171
		Assert::parameterType( 'string', $name, '$name' );
172
		Assert::parameterType( 'string', $tag, '$tag' );
173
174
		if ( $this->addMetaData ) {
175
			if ( !array_key_exists( ApiResult::META_TYPE, $values ) ) {
176
				ApiResult::setArrayType( $values, 'array' );
177
			}
178
			if ( !array_key_exists( ApiResult::META_INDEXED_TAG_NAME, $values ) ) {
179
				ApiResult::setIndexedTagName( $values, $tag );
180
			}
181
		}
182
183
		$this->result->addValue( $path, $name, $values );
184
	}
185
186
	/**
187
	 * Set an atomic value (or record) for the given path and name.
188
	 * If the value is an array, it should be a record (associative), not a list.
189
	 * For adding lists, use setList().
190
	 *
191
	 * @see ResultBuilder::setList()
192
	 * @see ResultBuilder::appendValue()
193
	 * @see ApiResult::addValue
194
	 *
195
	 * @param array|string|null $path
196
	 * @param string $name
197
	 * @param mixed $value
198
	 */
199
	public function setValue( $path, $name, $value ) {
200
		$this->checkPathType( $path );
201
		Assert::parameterType( 'string', $name, '$name' );
202
		$this->checkValueIsNotList( $value );
203
204
		$this->result->addValue( $path, $name, $value );
205
	}
206
207
	/**
208
	 * Appends a value to the list at the given path.
209
	 * This automatically sets the indexed tag name, if appropriate.
210
	 *
211
	 * If the value is an array, it should be associative, not a list.
212
	 * For adding lists, use setList().
213
	 *
214
	 * @see ResultBuilder::setList()
215
	 * @see ResultBuilder::setValue()
216
	 * @see ApiResult::addValue
217
	 * @see ApiResult::setIndexedTagName_internal
218
	 *
219
	 * @param array|string|null $path
220
	 * @param int|string|null $key the key to use when appending, or null for automatic.
221
	 * May be ignored even if given, based on $this->addMetaData.
222
	 * @param mixed $value
223
	 * @param string $tag tag name to use for $value in indexed mode
224
	 */
225
	public function appendValue( $path, $key, $value, $tag ) {
226
		$this->checkPathType( $path );
227
		$this->checkKeyType( $key );
228
		Assert::parameterType( 'string', $tag, '$tag' );
229
		$this->checkValueIsNotList( $value );
230
231
		$this->result->addValue( $path, $key, $value );
232
		if ( $this->addMetaData ) {
233
			$this->result->addIndexedTagName( $path, $tag );
234
		}
235
	}
236
237
	/**
238
	 * @param array|string|null $path
239
	 */
240
	private function checkPathType( $path ) {
241
		Assert::parameter(
242
			is_string( $path ) || is_array( $path ) || $path === null,
243
			'$path',
244
			'$path must be an array (or null)'
245
		);
246
	}
247
248
	/**
249
	 * @param int|string|null $key the key to use when appending, or null for automatic.
250
	 */
251
	private function checkKeyType( $key ) {
252
		Assert::parameter(
253
			is_string( $key ) || is_int( $key ) || $key === null,
254
			'$key',
255
			'$key must be an array (or null)'
256
		);
257
	}
258
259
	/**
260
	 * @param mixed $value
261
	 */
262
	private function checkValueIsNotList( $value ) {
263
		Assert::parameter(
264
			!( is_array( $value ) && isset( $value[0] ) ),
265
			'$value',
266
			'$value must not be a list'
267
		);
268
	}
269
270
	/**
271
	 * Get serialized entity for the EntityRevision and add it to the result alongside other needed properties.
272
	 *
273
	 *
274
	 * @param string|null $sourceEntityIdSerialization EntityId used to retrieve $entityRevision
275
	 *        Used as the key for the entity in the 'entities' structure and for adding redirect
276
	 *     info Will default to the entity's serialized ID if null. If given this must be the
277
	 *     entity id before any redirects were resolved.
278
	 * @param EntityRevision $entityRevision
279
	 * @param string[]|string $props a list of fields to include, or "all"
280
	 * @param string[]|null $filterSiteIds A list of site IDs to filter by
281
	 * @param string[] $filterLangCodes A list of language codes to filter by
282
	 * @param TermLanguageFallbackChain[] $termFallbackChains with keys of the origional language
283
	 */
284
	public function addEntityRevision(
285
		$sourceEntityIdSerialization,
286
		EntityRevision $entityRevision,
287
		$props = 'all',
288
		array $filterSiteIds = null,
289
		array $filterLangCodes = [],
290
		array $termFallbackChains = []
291
	) {
292
		$entity = $entityRevision->getEntity();
293
		$entityId = $entity->getId();
294
295
		if ( $sourceEntityIdSerialization === null ) {
296
			$sourceEntityIdSerialization = $entityId->getSerialization();
297
		}
298
299
		$record = [];
300
301
		// If there are no props defined only return type and id..
302
		// @phan-suppress-next-line PhanTypeComparisonToArray
303
		if ( $props === [] ) {
304
			$record = $this->addEntityInfoToRecord( $record, $entityId );
0 ignored issues
show
Bug introduced by
It seems like $entityId defined by $entity->getId() on line 293 can be null; however, Wikibase\Repo\Api\Result...addEntityInfoToRecord() 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...
305
		} else {
306
			// @phan-suppress-next-line PhanTypeMismatchArgumentInternal False positive
307
			if ( $props == 'all' || in_array( 'info', $props ) ) {
308
				$record = $this->addPageInfoToRecord( $record, $entityRevision );
309
			}
310
			if ( $sourceEntityIdSerialization !== $entityId->getSerialization() ) {
311
				$record = $this->addEntityRedirectInfoToRecord( $record, $sourceEntityIdSerialization, $entityId );
0 ignored issues
show
Bug introduced by
It seems like $entityId defined by $entity->getId() on line 293 can be null; however, Wikibase\Repo\Api\Result...yRedirectInfoToRecord() 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...
312
			}
313
314
			$entitySerialization = $this->getModifiedEntityArray(
315
				$entity,
316
				$props,
317
				$filterSiteIds,
318
				$filterLangCodes,
319
				$termFallbackChains
320
			);
321
322
			$record = array_merge( $record, $entitySerialization );
323
		}
324
325
		$this->appendValue( [ 'entities' ], $sourceEntityIdSerialization, $record, 'entity' );
326
		if ( $this->addMetaData ) {
327
			$this->result->addArrayType( [ 'entities' ], 'kvp', 'id' );
328
			$this->result->addValue(
329
				[ 'entities' ],
330
				ApiResult::META_KVP_MERGE,
331
				true,
332
				ApiResult::OVERRIDE
333
			);
334
		}
335
	}
336
337
	private function addEntityInfoToRecord( array $record, EntityId $entityId ): array {
338
		$record['id'] = $entityId->getSerialization();
339
		$record['type'] = $entityId->getEntityType();
340
		return $record;
341
	}
342
343
	private function addPageInfoToRecord( array $record, EntityRevision $entityRevision ): array {
344
		$title = $this->entityTitleLookup->getTitleForId( $entityRevision->getEntity()->getId() );
0 ignored issues
show
Bug introduced by
It seems like $entityRevision->getEntity()->getId() can be null; however, getTitleForId() 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...
345
		$record['pageid'] = $title->getArticleID();
346
		$record['ns'] = $title->getNamespace();
347
		$record['title'] = $title->getPrefixedText();
348
		$record['lastrevid'] = $entityRevision->getRevisionId();
349
		$record['modified'] = wfTimestamp( TS_ISO_8601, $entityRevision->getTimestamp() );
350
		return $record;
351
	}
352
353
	private function addEntityRedirectInfoToRecord( array $record, $sourceEntityIdSerialization, EntityId $entityId ): array {
354
		$record['redirects'] = [
355
			'from' => $sourceEntityIdSerialization,
356
			'to' => $entityId->getSerialization()
357
		];
358
		return $record;
359
	}
360
361
	/**
362
	 * Gets the standard serialization of an EntityDocument and modifies it in a standard way.
363
	 *
364
	 * This code was created for Items and Properties and since new entity types have been introduced
365
	 * it may not work in the desired way.
366
	 * @see https://phabricator.wikimedia.org/T249206
367
	 *
368
	 * @see ResultBuilder::addEntityRevision
369
	 *
370
	 * @param EntityDocument $entity
371
	 * @param array|string $props
372
	 * @param string[]|null $filterSiteIds
373
	 * @param string[] $filterLangCodes
374
	 * @param TermLanguageFallbackChain[] $termFallbackChains
375
	 *
376
	 * @return array
377
	 */
378
	private function getModifiedEntityArray(
379
		EntityDocument $entity,
380
		$props,
381
		?array $filterSiteIds,
382
		array $filterLangCodes,
383
		array $termFallbackChains
384
	) {
385
		$serialization = $this->entitySerializer->serialize( $entity );
386
387
		$serialization = $this->filterEntitySerializationUsingProps( $serialization, $props );
388
389
		if ( $props == 'all' || in_array( 'sitelinks/urls', $props ) ) {
390
			$serialization = $this->injectEntitySerializationWithSiteLinkUrls( $serialization );
391
		}
392
		$serialization = $this->sortEntitySerializationSiteLinks( $serialization );
393
		$serialization = $this->dataTypeInjector->injectEntitySerializationWithDataTypes( $serialization );
394
		$serialization = $this->filterEntitySerializationUsingSiteIds( $serialization, $filterSiteIds );
395
		if ( !empty( $termFallbackChains ) ) {
396
			$serialization = $this->addEntitySerializationFallbackInfo( $serialization, $termFallbackChains );
397
		}
398
		$serialization = $this->filterEntitySerializationUsingLangCodes(
399
			$serialization,
400
			$filterLangCodes
401
		);
402
403
		if ( $this->addMetaData ) {
404
			$serialization = $this->getEntitySerializationWithMetaData( $serialization );
405
		}
406
407
		return $serialization;
408
	}
409
410
	/**
411
	 * @param array $serialization
412
	 * @param string|array $props
413
	 *
414
	 * @return array
415
	 */
416
	private function filterEntitySerializationUsingProps( array $serialization, $props ) {
417
		if ( $props !== 'all' ) {
418
			if ( !in_array( 'labels', $props ) ) {
419
				unset( $serialization['labels'] );
420
			}
421
			if ( !in_array( 'descriptions', $props ) ) {
422
				unset( $serialization['descriptions'] );
423
			}
424
			if ( !in_array( 'aliases', $props ) ) {
425
				unset( $serialization['aliases'] );
426
			}
427
			if ( !in_array( 'claims', $props ) ) {
428
				unset( $serialization['claims'] );
429
			}
430
			if ( !in_array( 'sitelinks', $props ) ) {
431
				unset( $serialization['sitelinks'] );
432
			}
433
		}
434
		return $serialization;
435
	}
436
437
	private function injectEntitySerializationWithSiteLinkUrls( array $serialization ) {
438
		if ( isset( $serialization['sitelinks'] ) ) {
439
			$serialization['sitelinks'] = $this->getSiteLinkListArrayWithUrls( $serialization['sitelinks'] );
440
		}
441
		return $serialization;
442
	}
443
444
	private function sortEntitySerializationSiteLinks( array $serialization ) {
445
		if ( isset( $serialization['sitelinks'] ) ) {
446
			ksort( $serialization['sitelinks'] );
447
		}
448
		return $serialization;
449
	}
450
451
	private function filterEntitySerializationUsingSiteIds(
452
		array $serialization,
453
		array $siteIds = null
454
	) {
455
		if ( !empty( $siteIds ) && array_key_exists( 'sitelinks', $serialization ) ) {
456
			foreach ( $serialization['sitelinks'] as $siteId => $siteLink ) {
457
				if ( is_array( $siteLink ) && !in_array( $siteLink['site'], $siteIds ) ) {
458
					unset( $serialization['sitelinks'][$siteId] );
459
				}
460
			}
461
		}
462
		return $serialization;
463
	}
464
465
	/**
466
	 * @param array $serialization
467
	 * @param TermLanguageFallbackChain[] $termFallbackChains
468
	 *
469
	 * @return array
470
	 */
471
	private function addEntitySerializationFallbackInfo(
472
		array $serialization,
473
		array $termFallbackChains
474
	) {
475
		if ( isset( $serialization['labels'] ) ) {
476
			$serialization['labels'] = $this->getTermsSerializationWithFallbackInfo(
477
				$serialization['labels'],
478
				$termFallbackChains
479
			);
480
		}
481
482
		if ( isset( $serialization['descriptions'] ) ) {
483
			$serialization['descriptions'] = $this->getTermsSerializationWithFallbackInfo(
484
				$serialization['descriptions'],
485
				$termFallbackChains
486
			);
487
		}
488
489
		return $serialization;
490
	}
491
492
	/**
493
	 * @param array $serialization
494
	 * @param TermLanguageFallbackChain[] $termFallbackChains
495
	 *
496
	 * @return array
497
	 */
498
	private function getTermsSerializationWithFallbackInfo(
499
		array $serialization,
500
		array $termFallbackChains
501
	) {
502
		$newSerialization = $serialization;
503
		foreach ( $termFallbackChains as $requestedLanguageCode => $fallbackChain ) {
504
			if ( !array_key_exists( $requestedLanguageCode, $serialization ) ) {
505
				$fallbackSerialization = $fallbackChain->extractPreferredValue( $serialization );
506
				if ( $fallbackSerialization !== null ) {
507
					if ( $fallbackSerialization['source'] !== null ) {
508
						$fallbackSerialization['source-language'] = $fallbackSerialization['source'];
509
					}
510
					unset( $fallbackSerialization['source'] );
511
					if ( $requestedLanguageCode !== $fallbackSerialization['language'] ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $requestedLanguageCode (integer) and $fallbackSerialization['language'] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
512
						$fallbackSerialization['for-language'] = $requestedLanguageCode;
513
					}
514
					$newSerialization[$requestedLanguageCode] = $fallbackSerialization;
515
				}
516
			}
517
		}
518
		return $newSerialization;
519
	}
520
521
	/**
522
	 * @param array $serialization
523
	 * @param string[] $langCodes
524
	 *
525
	 * @return array
526
	 */
527
	private function filterEntitySerializationUsingLangCodes(
528
		array $serialization,
529
		array $langCodes
530
	) {
531
		if ( !empty( $langCodes ) ) {
532
			if ( array_key_exists( 'labels', $serialization ) ) {
533
				foreach ( $serialization['labels'] as $langCode => $languageArray ) {
534
					if ( !in_array( $langCode, $langCodes ) ) {
535
						unset( $serialization['labels'][$langCode] );
536
					}
537
				}
538
			}
539
			if ( array_key_exists( 'descriptions', $serialization ) ) {
540
				foreach ( $serialization['descriptions'] as $langCode => $languageArray ) {
541
					if ( !in_array( $langCode, $langCodes ) ) {
542
						unset( $serialization['descriptions'][$langCode] );
543
					}
544
				}
545
			}
546
			if ( array_key_exists( 'aliases', $serialization ) ) {
547
				foreach ( $serialization['aliases'] as $langCode => $languageArray ) {
548
					if ( !in_array( $langCode, $langCodes ) ) {
549
						unset( $serialization['aliases'][$langCode] );
550
					}
551
				}
552
			}
553
		}
554
		return $serialization;
555
	}
556
557
	private function getEntitySerializationWithMetaData( array $serialization ) {
558
		$arrayTypes = [
559
			'aliases' => 'id',
560
			'claims/*/*/references/*/snaks' => 'id',
561
			'claims/*/*/qualifiers' => 'id',
562
			'claims' => 'id',
563
			'descriptions' => 'language',
564
			'labels' => 'language',
565
			'sitelinks' => 'site',
566
		];
567
		foreach ( $arrayTypes as $path => $keyName ) {
568
			$serialization = $this->modifier->modifyUsingCallback(
569
				$serialization,
570
				$path,
571
				$this->callbackFactory->getCallbackToSetArrayType( 'kvp', $keyName )
572
			);
573
		}
574
575
		$kvpMergeArrays = [
576
			'descriptions',
577
			'labels',
578
			'sitelinks',
579
		];
580
		foreach ( $kvpMergeArrays as $path ) {
581
			$serialization = $this->modifier->modifyUsingCallback(
582
				$serialization,
583
				$path,
584
				function( $array ) {
585
					if ( is_array( $array ) ) {
586
						$array[ApiResult::META_KVP_MERGE] = true;
587
					}
588
					return $array;
589
				}
590
			);
591
		}
592
593
		$indexTags = [
594
			'labels' => 'label',
595
			'descriptions' => 'description',
596
			'aliases/*' => 'alias',
597
			'aliases' => 'language',
598
			'sitelinks/*/badges' => 'badge',
599
			'sitelinks' => 'sitelink',
600
			'claims/*/*/qualifiers/*' => 'qualifiers',
601
			'claims/*/*/qualifiers' => 'property',
602
			'claims/*/*/qualifiers-order' => 'property',
603
			'claims/*/*/references/*/snaks/*' => 'snak',
604
			'claims/*/*/references/*/snaks' => 'property',
605
			'claims/*/*/references/*/snaks-order' => 'property',
606
			'claims/*/*/references' => 'reference',
607
			'claims/*' => 'claim',
608
			'claims' => 'property',
609
		];
610
		foreach ( $indexTags as $path => $tag ) {
611
			$serialization = $this->modifier->modifyUsingCallback(
612
				$serialization,
613
				$path,
614
				$this->callbackFactory->getCallbackToIndexTags( $tag )
615
			);
616
		}
617
618
		return $serialization;
619
	}
620
621
	/**
622
	 * Get serialized information for the EntityId and add them to result
623
	 *
624
	 * @param EntityId $entityId
625
	 * @param string|array|null $path
626
	 */
627
	public function addBasicEntityInformation( EntityId $entityId, $path ) {
628
		$this->setValue( $path, 'id', $entityId->getSerialization() );
629
		$this->setValue( $path, 'type', $entityId->getEntityType() );
630
	}
631
632
	/**
633
	 * Get serialized labels and add them to result
634
	 *
635
	 * @param TermList $labels the labels to insert in the result
636
	 * @param array|string $path where the data is located
637
	 */
638
	public function addLabels( TermList $labels, $path ) {
639
		$this->addTermList( $labels, 'labels', 'label', $path );
640
	}
641
642
	/**
643
	 * Adds fake serialization to show a label has been removed
644
	 *
645
	 * @param string $language
646
	 * @param array|string $path where the data is located
647
	 */
648
	public function addRemovedLabel( $language, $path ) {
649
		$this->addRemovedTerm( $language, 'labels', 'label', $path );
650
	}
651
652
	/**
653
	 * Get serialized descriptions and add them to result
654
	 *
655
	 * @param TermList $descriptions the descriptions to insert in the result
656
	 * @param array|string $path where the data is located
657
	 */
658
	public function addDescriptions( TermList $descriptions, $path ) {
659
		$this->addTermList( $descriptions, 'descriptions', 'description', $path );
660
	}
661
662
	/**
663
	 * Adds fake serialization to show a label has been removed
664
	 *
665
	 * @param string $language
666
	 * @param array|string $path where the data is located
667
	 */
668
	public function addRemovedDescription( $language, $path ) {
669
		$this->addRemovedTerm( $language, 'descriptions', 'description', $path );
670
	}
671
672
	/**
673
	 * Get serialized TermList and add it to the result
674
	 *
675
	 * @param TermList $termList
676
	 * @param string $name
677
	 * @param string $tag
678
	 * @param array|string $path where the data is located
679
	 */
680
	private function addTermList( TermList $termList, $name, $tag, $path ) {
681
		$serializer = $this->serializerFactory->newTermListSerializer();
682
		$value = $serializer->serialize( $termList );
683
		if ( $this->addMetaData ) {
684
			ApiResult::setArrayType( $value, 'kvp', 'language' );
685
			$value[ApiResult::META_KVP_MERGE] = true;
686
		}
687
		$this->setList( $path, $name, $value, $tag );
688
	}
689
690
	/**
691
	 * Adds fake serialization to show a term has been removed
692
	 *
693
	 * @param string $language
694
	 * @param string $name
695
	 * @param string $tag
696
	 * @param array|string $path where the data is located
697
	 */
698
	private function addRemovedTerm( $language, $name, $tag, $path ) {
699
		$value = [
700
			$language => [
701
				'language' => $language,
702
				'removed' => '',
703
			]
704
		];
705
		if ( $this->addMetaData ) {
706
			ApiResult::setArrayType( $value, 'kvp', 'language' );
707
			$value[ApiResult::META_KVP_MERGE] = true;
708
		}
709
		$this->setList( $path, $name, $value, $tag );
710
	}
711
712
	/**
713
	 * Get serialized AliasGroupList and add it to result
714
	 *
715
	 * @param AliasGroupList $aliasGroupList the AliasGroupList to set in the result
716
	 * @param array|string $path where the data is located
717
	 */
718
	public function addAliasGroupList( AliasGroupList $aliasGroupList, $path ) {
719
		$serializer = $this->serializerFactory->newAliasGroupListSerializer();
720
		$values = $serializer->serialize( $aliasGroupList );
721
722
		if ( $this->addMetaData ) {
723
			$values = $this->modifier->modifyUsingCallback(
724
				$values,
725
				null,
726
				$this->callbackFactory->getCallbackToSetArrayType( 'kvp', 'id' )
727
			);
728
			$values = $this->modifier->modifyUsingCallback(
729
				$values,
730
				'*',
731
				$this->callbackFactory->getCallbackToIndexTags( 'alias' )
732
			);
733
		}
734
735
		$this->setList( $path, 'aliases', $values, 'language' );
736
		ApiResult::setArrayType( $values, 'kvp', 'id' );
737
	}
738
739
	/**
740
	 * Get serialized sitelinks and add them to result
741
	 *
742
	 * @todo use a SiteLinkListSerializer when created in DataModelSerialization here
743
	 *
744
	 * @param SiteLinkList $siteLinkList the site links to insert in the result
745
	 * @param array|string $path where the data is located
746
	 * @param bool $addUrl
747
	 */
748
	public function addSiteLinkList( SiteLinkList $siteLinkList, $path, $addUrl = false ) {
749
		$serializer = $this->serializerFactory->newSiteLinkSerializer();
750
751
		$values = [];
752
		foreach ( $siteLinkList->toArray() as $siteLink ) {
753
			$values[$siteLink->getSiteId()] = $serializer->serialize( $siteLink );
754
		}
755
756
		if ( $addUrl ) {
757
			$values = $this->getSiteLinkListArrayWithUrls( $values );
758
		}
759
760
		if ( $this->addMetaData ) {
761
			$values = $this->getSiteLinkListArrayWithMetaData( $values );
762
		}
763
764
		$this->setList( $path, 'sitelinks', $values, 'sitelink' );
765
	}
766
767
	private function getSiteLinkListArrayWithUrls( array $array ) {
768
		$siteLookup = $this->siteLookup;
769
		$addUrlCallback = function( $array ) use ( $siteLookup ) {
770
			$site = $siteLookup->getSite( $array['site'] );
771
			if ( $site !== null ) {
772
				$array['url'] = $site->getPageUrl( $array['title'] );
773
			}
774
			return $array;
775
		};
776
		return $this->modifier->modifyUsingCallback( $array, '*', $addUrlCallback );
777
	}
778
779
	private function getSiteLinkListArrayWithMetaData( array $array ) {
780
		$array = $this->modifier->modifyUsingCallback(
781
			$array,
782
			null,
783
			$this->callbackFactory->getCallbackToSetArrayType( 'kvp', 'site' )
784
		);
785
		$array[ApiResult::META_KVP_MERGE] = true;
786
		$array = $this->modifier->modifyUsingCallback(
787
			$array,
788
			'*/badges',
789
			$this->callbackFactory->getCallbackToIndexTags( 'badge' )
790
		);
791
		return $array;
792
	}
793
794
	/**
795
	 * Adds fake serialization to show a sitelink has been removed
796
	 *
797
	 * @param SiteLinkList $siteLinkList
798
	 * @param array|string $path where the data is located
799
	 */
800
	public function addRemovedSiteLinks( SiteLinkList $siteLinkList, $path ) {
801
		$serializer = $this->serializerFactory->newSiteLinkSerializer();
802
		$values = [];
803
		foreach ( $siteLinkList->toArray() as $siteLink ) {
804
			$value = $serializer->serialize( $siteLink );
805
			$value['removed'] = '';
806
			$values[$siteLink->getSiteId()] = $value;
807
		}
808
		if ( $this->addMetaData ) {
809
			$values = $this->modifier->modifyUsingCallback(
810
				$values,
811
				null,
812
				$this->callbackFactory->getCallbackToSetArrayType( 'kvp', 'site' )
813
			);
814
			$values[ApiResult::META_KVP_MERGE] = true;
815
		}
816
		$this->setList( $path, 'sitelinks', $values, 'sitelink' );
817
	}
818
819
	/**
820
	 * Get serialized claims and add them to result
821
	 *
822
	 * @param StatementList $statements the labels to set in the result
823
	 * @param array|string $path where the data is located
824
	 * @param array|string $props a list of fields to include, or "all"
825
	 */
826
	public function addStatements( StatementList $statements, $path, $props = 'all' ) {
827
		$serializer = $this->serializerFactory->newStatementListSerializer();
828
829
		$values = $serializer->serialize( $statements );
830
831
		if ( is_array( $props ) && !in_array( 'references', $props ) ) {
832
			$values = $this->modifier->modifyUsingCallback(
833
				$values,
834
				'*/*',
835
				function ( $array ) {
836
					unset( $array['references'] );
837
					return $array;
838
				}
839
			);
840
		}
841
842
		$values = $this->getArrayWithAlteredClaims( $values, '*/*/' );
843
844
		if ( $this->addMetaData ) {
845
			$values = $this->getClaimsArrayWithMetaData( $values, '*/*/' );
846
			$values = $this->modifier->modifyUsingCallback(
847
				$values,
848
				null,
849
				$this->callbackFactory->getCallbackToSetArrayType( 'kvp', 'id' )
850
			);
851
			$values = $this->modifier->modifyUsingCallback(
852
				$values,
853
				'*',
854
				$this->callbackFactory->getCallbackToIndexTags( 'claim' )
855
			);
856
		}
857
858
		$values = $this->dataTypeInjector->getArrayWithDataTypesInSnakAtPath(
859
			$values,
860
			'*/*/mainsnak'
861
		);
862
863
		if ( $this->addMetaData ) {
864
			ApiResult::setArrayType( $values, 'kvp', 'id' );
865
		}
866
867
		$this->setList( $path, 'claims', $values, 'property' );
868
	}
869
870
	/**
871
	 * Get serialized claim and add it to result
872
	 *
873
	 * @param Statement $statement
874
	 */
875
	public function addStatement( Statement $statement ) {
876
		$serializer = $this->serializerFactory->newStatementSerializer();
877
878
		//TODO: this is currently only used to add a Claim as the top level structure,
879
		//      with a null path and a fixed name. Would be nice to also allow claims
880
		//      to be added to a list, using a path and a id key or index.
881
882
		$value = $serializer->serialize( $statement );
883
884
		$value = $this->getArrayWithAlteredClaims( $value );
885
886
		if ( $this->addMetaData ) {
887
			$value = $this->getClaimsArrayWithMetaData( $value );
888
		}
889
890
		$value = $this->dataTypeInjector->getArrayWithDataTypesInSnakAtPath(
891
			$value,
892
			'mainsnak'
893
		);
894
895
		$this->setValue( null, 'claim', $value );
896
	}
897
898
	/**
899
	 * @param array $array
900
	 * @param string $claimPath to the claim array/arrays with trailing /
901
	 *
902
	 * @return array
903
	 */
904
	private function getArrayWithAlteredClaims(
905
		array $array,
906
		$claimPath = ''
907
	) {
908
		$array = $this->dataTypeInjector->getArrayWithDataTypesInGroupedSnakListAtPath(
909
			$array,
910
			$claimPath . 'references/*/snaks'
911
		);
912
		$array = $this->dataTypeInjector->getArrayWithDataTypesInGroupedSnakListAtPath(
913
			$array,
914
			$claimPath . 'qualifiers'
915
		);
916
917
		$array = $this->dataTypeInjector->getArrayWithDataTypesInSnakAtPath(
918
			$array,
919
			$claimPath . 'mainsnak'
920
		);
921
922
		return $array;
923
	}
924
925
	/**
926
	 * @param array $array
927
	 * @param string $claimPath to the claim array/arrays with trailing /
928
	 *
929
	 * @return array
930
	 */
931
	private function getClaimsArrayWithMetaData( array $array, $claimPath = '' ) {
932
		$metaDataModifications = [
933
			'references/*/snaks/*' => [
934
				$this->callbackFactory->getCallbackToIndexTags( 'snak' ),
935
			],
936
			'references/*/snaks' => [
937
				$this->callbackFactory->getCallbackToSetArrayType( 'kvp', 'id' ),
938
				$this->callbackFactory->getCallbackToIndexTags( 'property' ),
939
			],
940
			'references/*/snaks-order' => [
941
				$this->callbackFactory->getCallbackToIndexTags( 'property' )
942
			],
943
			'references' => [
944
				$this->callbackFactory->getCallbackToIndexTags( 'reference' ),
945
			],
946
			'qualifiers/*' => [
947
				$this->callbackFactory->getCallbackToIndexTags( 'qualifiers' ),
948
			],
949
			'qualifiers' => [
950
				$this->callbackFactory->getCallbackToSetArrayType( 'kvp', 'id' ),
951
				$this->callbackFactory->getCallbackToIndexTags( 'property' ),
952
			],
953
			'qualifiers-order' => [
954
				$this->callbackFactory->getCallbackToIndexTags( 'property' )
955
			],
956
			'mainsnak' => [
957
				$this->callbackFactory->getCallbackToAddDataTypeToSnak( $this->dataTypeLookup ),
958
			],
959
		];
960
961
		foreach ( $metaDataModifications as $path => $callbacks ) {
962
			foreach ( $callbacks as $callback ) {
963
				$array = $this->modifier->modifyUsingCallback( $array, $claimPath . $path, $callback );
964
			}
965
		}
966
967
		return $array;
968
	}
969
970
	/**
971
	 * Get serialized reference and add it to result
972
	 *
973
	 * @param Reference $reference
974
	 */
975
	public function addReference( Reference $reference ) {
976
		$serializer = $this->serializerFactory->newReferenceSerializer();
977
978
		//TODO: this is currently only used to add a Reference as the top level structure,
979
		//      with a null path and a fixed name. Would be nice to also allow references
980
		//      to be added to a list, using a path and a id key or index.
981
982
		$value = $serializer->serialize( $reference );
983
984
		$value = $this->dataTypeInjector->getArrayWithDataTypesInGroupedSnakListAtPath( $value, 'snaks' );
985
986
		if ( $this->addMetaData ) {
987
			$value = $this->getReferenceArrayWithMetaData( $value );
988
		}
989
990
		$this->setValue( null, 'reference', $value );
991
	}
992
993
	private function getReferenceArrayWithMetaData( array $array ) {
994
		$array = $this->modifier->modifyUsingCallback( $array, 'snaks-order', function ( $array ) {
995
			ApiResult::setIndexedTagName( $array, 'property' );
996
			return $array;
997
		} );
998
		$array = $this->modifier->modifyUsingCallback( $array, 'snaks', function ( $array ) {
999
			foreach ( $array as &$snakGroup ) {
1000
				if ( is_array( $snakGroup ) ) {
1001
					ApiResult::setArrayType( $array, 'array' );
1002
					ApiResult::setIndexedTagName( $snakGroup, 'snak' );
1003
				}
1004
			}
1005
			ApiResult::setArrayType( $array, 'kvp', 'id' );
1006
			ApiResult::setIndexedTagName( $array, 'property' );
1007
			return $array;
1008
		} );
1009
		return $array;
1010
	}
1011
1012
	/**
1013
	 * Add an entry for a missing entity...
1014
	 *
1015
	 * @param string|null $key The key under which to place the missing entity in the 'entities'
1016
	 *        structure. If null, defaults to the 'id' field in $missingDetails if that is set;
1017
	 *        otherwise, it defaults to using a unique negative number.
1018
	 * @param array $missingDetails array containing key value pair missing details
1019
	 */
1020
	public function addMissingEntity( $key, array $missingDetails ) {
1021
		if ( $key === null && isset( $missingDetails['id'] ) ) {
1022
			$key = $missingDetails['id'];
1023
		}
1024
1025
		if ( $key === null ) {
1026
			$key = $this->missingEntityCounter;
1027
		}
1028
1029
		$this->appendValue(
1030
			'entities',
1031
			$key,
1032
			array_merge( $missingDetails, [ 'missing' => "" ] ),
1033
			'entity'
1034
		);
1035
1036
		if ( $this->addMetaData ) {
1037
			$this->result->addIndexedTagName( 'entities', 'entity' );
1038
			$this->result->addArrayType( [ 'entities' ], 'kvp', 'id' );
1039
			$this->result->addValue(
1040
				[ 'entities' ],
1041
				ApiResult::META_KVP_MERGE,
1042
				true,
1043
				ApiResult::OVERRIDE
1044
			);
1045
		}
1046
1047
		$this->missingEntityCounter--;
1048
	}
1049
1050
	/**
1051
	 * @param string $from
1052
	 * @param string $to
1053
	 * @param string $name
1054
	 */
1055
	public function addNormalizedTitle( $from, $to, $name = 'n' ) {
1056
		$this->setValue(
1057
			'normalized',
1058
			$name,
1059
			[ 'from' => $from, 'to' => $to ]
1060
		);
1061
	}
1062
1063
	/**
1064
	 * Adds the ID of the new revision from the Status object to the API result structure.
1065
	 * The status value is expected to be structured in the way that EditEntity::attemptSave()
1066
	 * resp WikiPage::doEditContent() do it: as an array, with an EntityRevision object in the
1067
	 *  'revision' field. If $oldRevId is set and the latest edit was null, a 'nochange' flag
1068
	 *  is also added.
1069
	 *
1070
	 * If no revision is found in the Status object, this method does nothing.
1071
	 *
1072
	 * @see ApiResult::addValue()
1073
	 *
1074
	 * @param Status $status The status to get the revision ID from.
1075
	 * @param string|null|array $path Where in the result to put the revision id
1076
	 * @param int|null $oldRevId The id of the latest revision of the entity before
1077
	 *        the last (possibly null) edit
1078
	 */
1079
	public function addRevisionIdFromStatusToResult( Status $status, $path, $oldRevId = null ) {
1080
		$value = $status->getValue();
1081
1082
		if ( isset( $value['revision'] ) ) {
1083
			if ( $value['revision'] instanceof EntityRevision ) {
1084
				// Should always be the case, but sanity check
1085
				$revisionId = $value['revision']->getRevisionId();
1086
			} else {
1087
				$revisionId = 0;
1088
			}
1089
1090
			$this->setValue( $path, 'lastrevid', $revisionId );
1091
1092
			if ( $oldRevId && $oldRevId === $revisionId ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oldRevId of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1093
				// like core's ApiEditPage
1094
				$this->setValue( $path, 'nochange', true );
1095
			}
1096
		}
1097
	}
1098
1099
}
1100