Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.33% covered (warning)
81.33%
61 / 75
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikibaseClientSiteLinksForItemHandler
81.33% covered (warning)
81.33%
61 / 75
81.82% covered (warning)
81.82%
9 / 11
33.10
0.00% covered (danger)
0.00%
0 / 1
 newFromGlobalState
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 provideSiteLinks
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 doProvideSiteLinks
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addSiteLink
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommonsSiteLink
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 getLinkedItemSitelink
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getCommonsCategoryName
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getCommonsSitelinkFromMainSnaks
52.63% covered (warning)
52.63%
10 / 19
0.00% covered (danger)
0.00%
0 / 1
14.80
 getItem
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStringValueFromMainSnaks
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
6.00
1<?php
2
3declare( strict_types = 1 );
4
5namespace WikimediaBadges;
6
7use DataValues\StringValue;
8use MediaWiki\MediaWikiServices;
9use OutOfBoundsException;
10use Wikibase\Client\Usage\UsageAccumulator;
11use Wikibase\Client\WikibaseClient;
12use Wikibase\DataModel\Entity\EntityIdValue;
13use Wikibase\DataModel\Entity\Item;
14use Wikibase\DataModel\Entity\ItemId;
15use Wikibase\DataModel\Entity\NumericPropertyId;
16use Wikibase\DataModel\Services\Lookup\EntityLookup;
17use Wikibase\DataModel\Services\Lookup\EntityLookupException;
18use Wikibase\DataModel\SiteLink;
19use Wikibase\DataModel\Snak\PropertyValueSnak;
20use Wikibase\DataModel\Snak\Snak;
21
22/**
23 * Handler for the WikibaseClientSiteLinksForItem hook that changes the link
24 * to Wikimedia Commons with the one to the commons category.
25 *
26 * @since 0.1
27 *
28 * @license GPL-2.0-or-later
29 * @author Marius Hoch < hoo@online.de >
30 */
31class WikibaseClientSiteLinksForItemHandler {
32
33    /** @var EntityLookup */
34    private $entityLookup;
35
36    /** @var string|null */
37    private $topicsMainCategoryProperty;
38
39    /** @var string|null */
40    private $categoryRelatedToListProperty;
41
42    /**
43     * @var string|null
44     */
45    private $commonsCategoryPropertySetting;
46
47    private static function newFromGlobalState(): self {
48        $services = MediaWikiServices::getInstance();
49        $config = $services->getMainConfig();
50
51        return new self(
52            WikibaseClient::getEntityLookup( $services ),
53            $config->get( 'WikimediaBadgesTopicsMainCategoryProperty' ),
54            $config->get( 'WikimediaBadgesCategoryRelatedToListProperty' ),
55            $config->get( 'WikimediaBadgesCommonsCategoryProperty' )
56        );
57    }
58
59    public function __construct(
60        EntityLookup $entityLookup,
61        ?string $topicsMainCategoryProperty,
62        ?string $categoryRelatedToListProperty,
63        ?string $commonsCategoryPropertySetting
64    ) {
65        $this->entityLookup = $entityLookup;
66        $this->topicsMainCategoryProperty = $topicsMainCategoryProperty;
67        $this->categoryRelatedToListProperty = $categoryRelatedToListProperty;
68        $this->commonsCategoryPropertySetting = $commonsCategoryPropertySetting;
69    }
70
71    /**
72     * @param Item $item
73     * @param SiteLink[] &$siteLinks
74     * @param UsageAccumulator $usageAccumulator
75     */
76    public static function provideSiteLinks(
77        Item $item, array &$siteLinks, UsageAccumulator $usageAccumulator
78    ): void {
79        $self = self::newFromGlobalState();
80
81        $self->doProvideSiteLinks( $item, $siteLinks );
82    }
83
84    /**
85     * @param Item $item
86     * @param SiteLink[] &$siteLinks
87     */
88    public function doProvideSiteLinks( Item $item, array &$siteLinks ): void {
89        $sitelink = $this->getCommonsSiteLink( $item );
90        if ( $sitelink !== null ) {
91            $this->addSiteLink( $sitelink, $siteLinks );
92        }
93    }
94
95    /**
96     * @param string $siteLink
97     * @param SiteLink[] &$siteLinks
98     */
99    private function addSiteLink( string $siteLink, array &$siteLinks ): void {
100        $siteLinks['commonswiki'] = new SiteLink( 'commonswiki', $siteLink );
101    }
102
103    private function getCommonsSiteLink( Item $item ): ?string {
104        try {
105            return $item->getSiteLink( 'commonswiki' )->getPageName();
106        } catch ( OutOfBoundsException $e ) {
107            // pass
108        }
109
110        $topicsMainCategorySitelink = $this->getLinkedItemSitelink(
111            $item,
112            $this->topicsMainCategoryProperty
113        );
114        if ( $topicsMainCategorySitelink !== null ) {
115            return $topicsMainCategorySitelink;
116        }
117
118        $categoryRelatedToListSitelink = $this->getLinkedItemSitelink(
119            $item,
120            $this->categoryRelatedToListProperty
121        );
122        if ( $categoryRelatedToListSitelink !== null ) {
123            return $categoryRelatedToListSitelink;
124        }
125
126        $categoryName = $this->getCommonsCategoryName( $item );
127        if ( $categoryName !== null ) {
128            return 'Category:' . $categoryName;
129        }
130
131        return null;
132    }
133
134    private function getLinkedItemSitelink( Item $item, ?string $propertyIdString ): ?string {
135        if ( $propertyIdString === null ) {
136            return null;
137        }
138
139        $propertyId = new NumericPropertyId( $propertyIdString );
140        $statements = $item->getStatements()->getByPropertyId( $propertyId );
141
142        $mainSnaks = $statements->getBestStatements()->getMainSnaks();
143
144        return $this->getCommonsSitelinkFromMainSnaks(
145            $mainSnaks,
146            $item->getId(),
147            $propertyId
148        );
149    }
150
151    private function getCommonsCategoryName( Item $item ): ?string {
152        if ( $this->commonsCategoryPropertySetting === null ) {
153            return null;
154        }
155
156        $propertyId = new NumericPropertyId( $this->commonsCategoryPropertySetting );
157        $statements = $item->getStatements()->getByPropertyId( $propertyId );
158
159        $mainSnaks = $statements->getBestStatements()->getMainSnaks();
160
161        return $this->getStringValueFromMainSnaks(
162            $mainSnaks,
163            $item->getId(),
164            $propertyId
165        );
166    }
167
168    private function getCommonsSitelinkFromMainSnaks(
169        array $mainSnaks,
170        ItemId $itemId,
171        NumericPropertyId $propertyId
172    ): ?string {
173        foreach ( $mainSnaks as $snak ) {
174            if ( !( $snak instanceof PropertyValueSnak ) ) {
175                continue;
176            }
177
178            $dataValue = $snak->getDataValue();
179            if ( !(
180                $dataValue instanceof EntityIdValue &&
181                $dataValue->getEntityId() instanceof ItemId
182            ) ) {
183                wfLogWarning(
184                    $itemId->getSerialization() . ' has a PropertyValueSnak with ' .
185                    $propertyId->getSerialization() . ' that has non-ItemId data.'
186                );
187
188                continue;
189            }
190            $itemId = $dataValue->getEntityId();
191            '@phan-var ItemId $itemId';
192
193            try {
194                $item = $this->getItem( $itemId );
195            } catch ( EntityLookupException $e ) {
196                continue;
197            }
198            if ( $item === null ) {
199                continue;
200            }
201
202            try {
203                return $item->getSiteLink( 'commonswiki' )->getPageName();
204            } catch ( OutOfBoundsException $e ) {
205                continue;
206            }
207        }
208
209        return null;
210    }
211
212    /** @throws EntityLookupException */
213    private function getItem( ItemId $itemId ): ?Item {
214        return $this->entityLookup->getEntity( $itemId );
215    }
216
217    /**
218     * @param Snak[] $mainSnaks
219     * @param ItemId $itemId
220     * @param NumericPropertyId $propertyId
221     *
222     * @return string|null
223     */
224    private function getStringValueFromMainSnaks(
225        array $mainSnaks,
226        ItemId $itemId,
227        NumericPropertyId $propertyId
228    ): ?string {
229        foreach ( $mainSnaks as $snak ) {
230            if ( !( $snak instanceof PropertyValueSnak ) ) {
231                continue;
232            }
233
234            if ( !( $snak->getDataValue() instanceof StringValue ) ) {
235                wfLogWarning(
236                    $itemId->getSerialization() . ' has a PropertyValueSnak with ' .
237                        $propertyId->getSerialization() . ' that has non-StringValue data.'
238                );
239
240                continue;
241            }
242
243            return $snak->getDataValue()->getValue();
244        }
245
246        return null;
247    }
248}