Completed
Push — master ( 951284...b5c57e )
by
unknown
06:36 queued 11s
created

repo/includes/Api/GetEntities.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace Wikibase\Repo\Api;
6
7
use ApiBase;
8
use ApiMain;
9
use IBufferingStatsdDataFactory;
10
use Wikibase\DataModel\Entity\EntityId;
11
use Wikibase\DataModel\Entity\EntityIdParser;
12
use Wikibase\DataModel\Entity\EntityIdParsingException;
13
use Wikibase\DataModel\Services\Entity\EntityPrefetcher;
14
use Wikibase\Lib\LanguageFallbackChainFactory;
15
use Wikibase\Lib\Store\DivergingEntityIdException;
16
use Wikibase\Lib\Store\EntityRevision;
17
use Wikibase\Lib\Store\EntityRevisionLookup;
18
use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException;
19
use Wikibase\Lib\StringNormalizer;
20
use Wikibase\Repo\SiteLinkTargetProvider;
21
use Wikibase\Repo\WikibaseRepo;
22
23
/**
24
 * API module to get the data for one or more Wikibase entities.
25
 *
26
 * @license GPL-2.0-or-later
27
 */
28
class GetEntities extends ApiBase {
29
30
	use FederatedPropertyApiValidatorTrait;
31
32
	/**
33
	 * @var StringNormalizer
34
	 */
35
	private $stringNormalizer;
36
37
	/**
38
	 * @var LanguageFallbackChainFactory
39
	 */
40
	private $languageFallbackChainFactory;
41
42
	/**
43
	 * @var SiteLinkTargetProvider
44
	 */
45
	private $siteLinkTargetProvider;
46
47
	/**
48
	 * @var EntityPrefetcher
49
	 */
50
	private $entityPrefetcher;
51
52
	/**
53
	 * @var string[]
54
	 */
55
	private $siteLinkGroups;
56
57
	/**
58
	 * @var ApiErrorReporter
59
	 */
60
	protected $errorReporter;
61
62
	/**
63
	 * @var ResultBuilder
64
	 */
65
	private $resultBuilder;
66
67
	/**
68
	 * @var EntityRevisionLookup
69
	 */
70
	private $entityRevisionLookup;
71
72
	/**
73
	 * @var EntityIdParser
74
	 */
75
	private $idParser;
76
77
	/** @var IBufferingStatsdDataFactory */
78
	private $stats;
79
80
	/**
81
	 * @param ApiMain $mainModule
82
	 * @param string $moduleName
83
	 * @param StringNormalizer $stringNormalizer
84
	 * @param LanguageFallbackChainFactory $languageFallbackChainFactory
85
	 * @param SiteLinkTargetProvider $siteLinkTargetProvider
86
	 * @param EntityPrefetcher $entityPrefetcher
87
	 * @param string[] $siteLinkGroups
88
	 * @param ApiErrorReporter $errorReporter
89
	 * @param ResultBuilder $resultBuilder
90
	 * @param EntityRevisionLookup $entityRevisionLookup
91
	 * @param EntityIdParser $idParser
92
	 * @param IBufferingStatsdDataFactory $stats
93
	 * @param bool $federatedPropertiesEnabled
94
	 *
95
	 * @see ApiBase::__construct
96
	 */
97
	public function __construct(
98
		ApiMain $mainModule,
99
		string $moduleName,
100
		StringNormalizer $stringNormalizer,
101
		LanguageFallbackChainFactory $languageFallbackChainFactory,
102
		SiteLinkTargetProvider $siteLinkTargetProvider,
103
		EntityPrefetcher $entityPrefetcher,
104
		array $siteLinkGroups,
105
		ApiErrorReporter $errorReporter,
106
		ResultBuilder $resultBuilder,
107
		EntityRevisionLookup $entityRevisionLookup,
108
		EntityIdParser $idParser,
109
		IBufferingStatsdDataFactory $stats,
110
		bool $federatedPropertiesEnabled
111
	) {
112
		parent::__construct( $mainModule, $moduleName );
113
114
		$this->stringNormalizer = $stringNormalizer;
115
		$this->languageFallbackChainFactory = $languageFallbackChainFactory;
116
		$this->siteLinkTargetProvider = $siteLinkTargetProvider;
117
		$this->entityPrefetcher = $entityPrefetcher;
118
		$this->siteLinkGroups = $siteLinkGroups;
119
		$this->errorReporter = $errorReporter;
120
		$this->resultBuilder = $resultBuilder;
121
		$this->entityRevisionLookup = $entityRevisionLookup;
122
		$this->idParser = $idParser;
123
		$this->stats = $stats;
124
		$this->federatedPropertiesEnabled = $federatedPropertiesEnabled;
125
	}
126
127
	public static function factory( ApiMain $apiMain, string $moduleName, IBufferingStatsdDataFactory $stats ): self {
128
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
129
		$settings = $wikibaseRepo->getSettings();
130
		$apiHelperFactory = $wikibaseRepo->getApiHelperFactory( $apiMain->getContext() );
131
132
		$siteLinkTargetProvider = new SiteLinkTargetProvider(
133
			$wikibaseRepo->getSiteLookup(),
134
			$settings->getSetting( 'specialSiteLinkGroups' )
135
		);
136
137
		return new self(
138
			$apiMain,
139
			$moduleName,
140
			$wikibaseRepo->getStringNormalizer(),
141
			$wikibaseRepo->getLanguageFallbackChainFactory(),
142
			$siteLinkTargetProvider,
143
			$wikibaseRepo->getStore()->getEntityPrefetcher(),
144
			$settings->getSetting( 'siteLinkGroups' ),
145
			$apiHelperFactory->getErrorReporter( $apiMain ),
146
			$apiHelperFactory->getResultBuilder( $apiMain ),
147
			$wikibaseRepo->getEntityRevisionLookup(),
148
			$wikibaseRepo->getEntityIdParser(),
149
			$stats,
150
			$wikibaseRepo->inFederatedPropertyMode()
151
		);
152
	}
153
154
	/**
155
	 * @inheritDoc
156
	 */
157
	public function execute(): void {
158
		$this->getMain()->setCacheMode( 'public' );
159
160
		$params = $this->extractRequestParams();
161
162
		if ( !isset( $params['ids'] ) && ( empty( $params['sites'] ) || empty( $params['titles'] ) ) ) {
163
			$this->errorReporter->dieWithError(
164
				'wikibase-api-illegal-ids-or-sites-titles-selector',
165
				'param-missing'
166
			);
167
		}
168
169
		$resolveRedirects = $params['redirects'] === 'yes';
170
171
		$entityIds = $this->getEntityIdsFromParams( $params );
172
		foreach ( $entityIds as $entityId ) {
173
			$this->validateAlteringEntityById( $entityId );
174
		}
175
176
		$this->stats->updateCount( 'wikibase.repo.api.getentities.entities', count( $entityIds ) );
177
178
		$entityRevisions = $this->getEntityRevisionsFromEntityIds( $entityIds, $resolveRedirects );
179
180
		foreach ( $entityRevisions as $sourceEntityId => $entityRevision ) {
181
			$this->handleEntity( $sourceEntityId, $entityRevision, $params );
182
		}
183
184
		$this->resultBuilder->markSuccess( 1 );
185
	}
186
187
	/**
188
	 * Get a unique array of EntityIds from api request params
189
	 *
190
	 * @param array $params
191
	 *
192
	 * @return EntityId[]
193
	 */
194
	private function getEntityIdsFromParams( array $params ): array {
195
		$fromIds = $this->getEntityIdsFromIdParam( $params );
196
		$fromSiteTitleCombinations = $this->getEntityIdsFromSiteTitleParams( $params );
197
		$ids = array_merge( $fromIds, $fromSiteTitleCombinations );
198
		return array_unique( $ids );
199
	}
200
201
	/**
202
	 * @param array $params
203
	 *
204
	 * @return EntityId[]
205
	 */
206
	private function getEntityIdsFromIdParam( array $params ): array {
207
		if ( !isset( $params['ids'] ) ) {
208
			return [];
209
		}
210
211
		$ids = [];
212
		foreach ( $params['ids'] as $id ) {
213
			try {
214
				$ids[] = $this->idParser->parse( $id );
215
			} catch ( EntityIdParsingException $e ) {
216
				$this->errorReporter->dieWithError(
217
					[ 'wikibase-api-no-such-entity', $id ],
218
					'no-such-entity',
219
					0,
220
					[ 'id' => $id ]
221
				);
222
			}
223
		}
224
		return $ids;
225
	}
226
227
	/**
228
	 * @param array $params
229
	 * @return EntityId[]
230
	 */
231
	private function getEntityIdsFromSiteTitleParams( array $params ): array {
232
		$ids = [];
233
		if ( !empty( $params['sites'] ) && !empty( $params['titles'] ) ) {
234
			$entityByTitleHelper = $this->getItemByTitleHelper();
235
236
			list( $ids, $missingItems ) = $entityByTitleHelper->getEntityIds(
237
				$params['sites'],
238
				$params['titles'],
239
				$params['normalize']
240
			);
241
242
			$this->addMissingItemsToResult( $missingItems );
243
		}
244
		return $ids;
245
	}
246
247
	private function getItemByTitleHelper(): EntityByTitleHelper {
248
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
249
		$siteLinkStore = $wikibaseRepo->getStore()->getEntityByLinkedTitleLookup();
250
		return new EntityByTitleHelper(
251
			$this,
252
			$this->resultBuilder,
253
			$siteLinkStore,
254
			$wikibaseRepo->getSiteLookup(),
255
			$this->stringNormalizer
256
		);
257
	}
258
259
	/**
260
	 * @param array[] $missingItems Array of arrays, Each internal array has a key 'site' and 'title'
261
	 */
262
	private function addMissingItemsToResult( array $missingItems ): void {
263
		foreach ( $missingItems as $missingItem ) {
264
			$this->resultBuilder->addMissingEntity( null, $missingItem );
265
		}
266
	}
267
268
	/**
269
	 * Returns props based on request parameters
270
	 *
271
	 * @param array $params
272
	 *
273
	 * @return array
274
	 */
275
	private function getPropsFromParams( array $params ): array {
276
		if ( in_array( 'sitelinks/urls', $params['props'] ) ) {
277
			$params['props'][] = 'sitelinks';
278
		}
279
280
		return $params['props'];
281
	}
282
283
	/**
284
	 * @param EntityId[] $entityIds
285
	 * @param bool $resolveRedirects
286
	 *
287
	 * @return EntityRevision[]
288
	 */
289
	private function getEntityRevisionsFromEntityIds( array $entityIds, bool $resolveRedirects = false ): array {
290
		$revisionArray = [];
291
292
		$this->entityPrefetcher->prefetch( $entityIds );
293
294
		foreach ( $entityIds as $entityId ) {
295
			$sourceEntityId = $entityId->getSerialization();
296
			$entityRevision = $this->getEntityRevision( $entityId, $resolveRedirects );
297
298
			$revisionArray[$sourceEntityId] = $entityRevision;
299
		}
300
301
		return $revisionArray;
302
	}
303
304
	private function getEntityRevision( EntityId $entityId, bool $resolveRedirects = false ): ?EntityRevision {
305
		$entityRevision = null;
306
307
		try {
308
			$entityRevision = $this->entityRevisionLookup->getEntityRevision( $entityId );
309
		} catch ( RevisionedUnresolvedRedirectException $ex ) {
310
			if ( $resolveRedirects ) {
311
				$entityId = $ex->getRedirectTargetId();
312
				$entityRevision = $this->getEntityRevision( $entityId, false );
313
			}
314
		} catch ( DivergingEntityIdException $ex ) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
315
			// DivergingEntityIdException is thrown when the repository $entityId is from other
316
			// repository than the entityRevisionLookup was configured to read from.
317
			// Such cases are input errors (e.g. specifying non-existent repository prefix)
318
			// and should be ignored and treated as non-existing entities.
319
		}
320
321
		return $entityRevision;
322
	}
323
324
	/**
325
	 * Adds the given EntityRevision to the API result.
326
	 *
327
	 * @param string|null $sourceEntityId
328
	 * @param EntityRevision|null $entityRevision
329
	 * @param array $params
330
	 */
331
	private function handleEntity(
332
		?string $sourceEntityId,
333
		EntityRevision $entityRevision = null,
334
		array $params = []
335
	): void {
336
		if ( $entityRevision === null ) {
337
			$this->resultBuilder->addMissingEntity( $sourceEntityId, [ 'id' => $sourceEntityId ] );
338
		} else {
339
			list( $languageCodeFilter, $fallbackChains ) = $this->getLanguageCodesAndFallback( $params );
340
			$this->resultBuilder->addEntityRevision(
341
				$sourceEntityId,
342
				$entityRevision,
343
				$this->getPropsFromParams( $params ),
344
				$params['sitefilter'],
345
				$languageCodeFilter,
346
				$fallbackChains
347
			);
348
		}
349
	}
350
351
	/**
352
	 * @param array $params
353
	 *
354
	 * @return array
355
	 *     0 => string[] languageCodes that the user wants returned
356
	 *     1 => TermLanguageFallbackChain[] Keys are requested lang codes
357
	 */
358
	private function getLanguageCodesAndFallback( array $params ): array {
359
		$languageCodes = ( is_array( $params['languages'] ) ? $params['languages'] : [] );
360
		$fallbackChains = [];
361
362
		if ( $params['languagefallback'] ) {
363
			$fallbackMode = LanguageFallbackChainFactory::FALLBACK_ALL;
364
			foreach ( $languageCodes as $languageCode ) {
365
				$fallbackChains[$languageCode] = $this->languageFallbackChainFactory
366
					->newFromLanguageCode( $languageCode, $fallbackMode );
367
			}
368
		}
369
370
		return [ array_unique( $languageCodes ), $fallbackChains ];
371
	}
372
373
	/**
374
	 * @inheritDoc
375
	 */
376
	protected function getAllowedParams(): array {
377
		$sites = $this->siteLinkTargetProvider->getSiteList( $this->siteLinkGroups );
378
379
		return array_merge( parent::getAllowedParams(), [
380
			'ids' => [
381
				self::PARAM_TYPE => 'string',
382
				self::PARAM_ISMULTI => true,
383
			],
384
			'sites' => [
385
				self::PARAM_TYPE => $sites->getGlobalIdentifiers(),
386
				self::PARAM_ISMULTI => true,
387
				self::PARAM_ALLOW_DUPLICATES => true
388
			],
389
			'titles' => [
390
				self::PARAM_TYPE => 'string',
391
				self::PARAM_ISMULTI => true,
392
				self::PARAM_ALLOW_DUPLICATES => true
393
			],
394
			'redirects' => [
395
				self::PARAM_TYPE => [ 'yes', 'no' ],
396
				self::PARAM_DFLT => 'yes',
397
			],
398
			'props' => [
399
				self::PARAM_TYPE => [ 'info', 'sitelinks', 'sitelinks/urls', 'aliases', 'labels',
400
					'descriptions', 'claims', 'datatype' ],
401
				self::PARAM_DFLT => 'info|sitelinks|aliases|labels|descriptions|claims|datatype',
402
				self::PARAM_ISMULTI => true,
403
			],
404
			'languages' => [
405
				self::PARAM_TYPE => WikibaseRepo::getDefaultInstance()->getTermsLanguages()->getLanguages(),
406
				self::PARAM_ISMULTI => true,
407
			],
408
			'languagefallback' => [
409
				self::PARAM_TYPE => 'boolean',
410
				self::PARAM_DFLT => false
411
			],
412
			'normalize' => [
413
				self::PARAM_TYPE => 'boolean',
414
				self::PARAM_DFLT => false
415
			],
416
			'sitefilter' => [
417
				self::PARAM_TYPE => $sites->getGlobalIdentifiers(),
418
				self::PARAM_ISMULTI => true,
419
				self::PARAM_ALLOW_DUPLICATES => true
420
			],
421
		] );
422
	}
423
424
	/**
425
	 * @inheritDoc
426
	 */
427
	protected function getExamplesMessages(): array {
428
		return [
429
			"action=wbgetentities&ids=Q42"
430
			=> "apihelp-wbgetentities-example-1",
431
			"action=wbgetentities&ids=P17"
432
			=> "apihelp-wbgetentities-example-2",
433
			"action=wbgetentities&ids=Q42|P17"
434
			=> "apihelp-wbgetentities-example-3",
435
			"action=wbgetentities&ids=Q42&languages=en"
436
			=> "apihelp-wbgetentities-example-4",
437
			"action=wbgetentities&ids=Q42&languages=ii&languagefallback="
438
			=> "apihelp-wbgetentities-example-5",
439
			"action=wbgetentities&ids=Q42&props=labels"
440
			=> "apihelp-wbgetentities-example-6",
441
			"action=wbgetentities&ids=P17|P3&props=datatype"
442
			=> "apihelp-wbgetentities-example-7",
443
			"action=wbgetentities&ids=Q42&props=aliases&languages=en"
444
			=> "apihelp-wbgetentities-example-8",
445
			"action=wbgetentities&ids=Q1|Q42&props=descriptions&languages=en|de|fr"
446
			=> "apihelp-wbgetentities-example-9",
447
			'action=wbgetentities&sites=enwiki&titles=Berlin&languages=en'
448
			=> 'apihelp-wbgetentities-example-10',
449
			'action=wbgetentities&sites=enwiki&titles=berlin&normalize='
450
			=> 'apihelp-wbgetentities-example-11',
451
			'action=wbgetentities&ids=Q42&props=sitelinks'
452
			=> 'apihelp-wbgetentities-example-12',
453
			'action=wbgetentities&ids=Q42&sitefilter=enwiki'
454
			=> 'apihelp-wbgetentities-example-13'
455
		];
456
	}
457
458
}
459