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
![]() |
|||
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 |