Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.83% covered (success)
95.83%
69 / 72
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommonsLinkChecker
95.83% covered (success)
95.83%
69 / 72
83.33% covered (warning)
83.33%
5 / 6
28
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSupportedContextTypes
n/a
0 / 0
n/a
0 / 0
1
 getDefaultContextTypes
n/a
0 / 0
n/a
0 / 0
1
 getSupportedEntityTypes
n/a
0 / 0
n/a
0 / 0
1
 getCommonsNamespace
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
8.79
 checkConstraint
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
11
 checkConstraintParameters
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 commonsLinkIsWellFormed
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 valueIncludesNamespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare( strict_types = 1 );
4
5namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker;
6
7use MediaWiki\Site\MediaWikiPageNameNormalizer;
8use Wikibase\DataModel\Entity\ItemId;
9use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
10use Wikibase\DataModel\Snak\PropertyValueSnak;
11use WikibaseQuality\ConstraintReport\Constraint;
12use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
13use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
14use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterException;
15use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
16use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
17use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
18use WikibaseQuality\ConstraintReport\Role;
19
20/**
21 * @author BP2014N1
22 * @license GPL-2.0-or-later
23 */
24class CommonsLinkChecker implements ConstraintChecker {
25
26    /**
27     * @var ConstraintParameterParser
28     */
29    private $constraintParameterParser;
30
31    /**
32     * @var MediaWikiPageNameNormalizer
33     */
34    private $pageNameNormalizer;
35
36    /**
37     * @var PropertyDataTypeLookup
38     */
39    private $propertyDatatypeLookup;
40
41    public function __construct(
42        ConstraintParameterParser $constraintParameterParser,
43        MediaWikiPageNameNormalizer $pageNameNormalizer,
44        PropertyDataTypeLookup $propertyDatatypeLookup
45    ) {
46        $this->constraintParameterParser = $constraintParameterParser;
47        $this->pageNameNormalizer = $pageNameNormalizer;
48        $this->propertyDatatypeLookup = $propertyDatatypeLookup;
49    }
50
51    /**
52     * @codeCoverageIgnore This method is purely declarative.
53     */
54    public function getSupportedContextTypes(): array {
55        return self::ALL_CONTEXT_TYPES_SUPPORTED;
56    }
57
58    /**
59     * @codeCoverageIgnore This method is purely declarative.
60     */
61    public function getDefaultContextTypes(): array {
62        return Context::ALL_CONTEXT_TYPES;
63    }
64
65    /** @codeCoverageIgnore This method is purely declarative. */
66    public function getSupportedEntityTypes() {
67        return self::ALL_ENTITY_TYPES_SUPPORTED;
68    }
69
70    /**
71     * Get the number of a namespace on Wikimedia Commons (commonswiki).
72     * All namespaces not known to this function will be looked up by the TitleParser.
73     *
74     * @return array first element is the namespace number (default namespace for TitleParser),
75     * second element is a string to prepend to the title before giving it to the TitleParser
76     */
77    private function getCommonsNamespace( string $namespace ): array {
78        switch ( $namespace ) {
79            case '':
80                return [ NS_MAIN, '' ];
81            // extra namespaces, see operations/mediawiki-config.git,
82            // wmf-config/InitialiseSettings.php, 'wgExtraNamespaces' key, 'commonswiki' subkey
83            case 'Creator':
84                return [ 100, '' ];
85            case 'TimedText':
86                return [ 102, '' ];
87            case 'Sequence':
88                return [ 104, '' ];
89            case 'Institution':
90                return [ 106, '' ];
91            // extension namespace, see mediawiki/extensions/JsonConfig.git,
92            // extension.json, 'namespaces' key, third element
93            case 'Data':
94                return [ 486, '' ];
95            default:
96                return [ NS_MAIN, $namespace . ':' ];
97        }
98    }
99
100    /**
101     * Checks 'Commons link' constraint.
102     *
103     * @throws ConstraintParameterException
104     */
105    public function checkConstraint( Context $context, Constraint $constraint ): CheckResult {
106        $constraintParameters = $constraint->getConstraintParameters();
107        $constraintTypeItemId = $constraint->getConstraintTypeItemId();
108
109        $namespace = $this->constraintParameterParser->parseNamespaceParameter(
110            $constraintParameters,
111            $constraintTypeItemId
112        );
113
114        $snak = $context->getSnak();
115
116        if ( !$snak instanceof PropertyValueSnak ) {
117            // nothing to check
118            return new CheckResult( $context, $constraint, CheckResult::STATUS_COMPLIANCE );
119        }
120
121        $dataValue = $snak->getDataValue();
122
123        /*
124         * error handling:
125         *   type of $dataValue for properties with 'Commons link' constraint has to be 'string'
126         *   parameter $namespace can be null, works for commons galleries
127         */
128        if ( $dataValue->getType() !== 'string' ) {
129            $message = ( new ViolationMessage( 'wbqc-violation-message-value-needed-of-type' ) )
130                ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
131                ->withDataValueType( 'string' );
132            return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION, $message );
133        }
134
135        $commonsLink = $dataValue->getValue();
136        if ( !$this->commonsLinkIsWellFormed( $commonsLink ) ) {
137            return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION,
138                new ViolationMessage( 'wbqc-violation-message-commons-link-not-well-formed' ) );
139        }
140
141        $dataType = $this->propertyDatatypeLookup->getDataTypeIdForProperty( $snak->getPropertyId() );
142        switch ( $dataType ) {
143            case 'geo-shape':
144            case 'tabular-data':
145                if ( strpos( $commonsLink, $namespace . ':' ) !== 0 ) {
146                    return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION,
147                        new ViolationMessage( 'wbqc-violation-message-commons-link-not-well-formed' ) );
148                }
149                $pageName = $commonsLink;
150                break;
151            default:
152                $pageName = $namespace ? $namespace . ':' . $commonsLink : $commonsLink;
153                break;
154        }
155
156        $prefix = $this->getCommonsNamespace( $namespace )[1];
157        $normalizedTitle = $this->pageNameNormalizer->normalizePageName(
158            $pageName,
159            'https://commons.wikimedia.org/w/api.php'
160        );
161        if ( $normalizedTitle === false ) {
162            if ( $this->valueIncludesNamespace( $commonsLink, $namespace ) ) {
163                return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION,
164                    new ViolationMessage( 'wbqc-violation-message-commons-link-not-well-formed' ) );
165            }
166            return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION,
167                new ViolationMessage( 'wbqc-violation-message-commons-link-no-existent' ) );
168        }
169
170        return new CheckResult( $context, $constraint, CheckResult::STATUS_COMPLIANCE, null );
171    }
172
173    public function checkConstraintParameters( Constraint $constraint ): array {
174        $constraintParameters = $constraint->getConstraintParameters();
175        $constraintTypeItemId = $constraint->getConstraintTypeItemId();
176        $exceptions = [];
177        try {
178            $this->constraintParameterParser->parseNamespaceParameter(
179                $constraintParameters,
180                $constraintTypeItemId
181            );
182        } catch ( ConstraintParameterException $e ) {
183            $exceptions[] = $e;
184        }
185        return $exceptions;
186    }
187
188    private function commonsLinkIsWellFormed( string $commonsLink ): bool {
189        $toReplace = [ "_", "%20" ];
190        $compareString = trim( str_replace( $toReplace, '', $commonsLink ) );
191
192        return $commonsLink === $compareString;
193    }
194
195    /**
196     * Checks whether the value of the statement already includes the namespace.
197     * This special case should be reported as “malformed title” instead of “title does not exist”.
198     */
199    private function valueIncludesNamespace( string $value, string $namespace ): bool {
200        return $namespace !== '' &&
201            strncasecmp( $value, $namespace . ':', strlen( $namespace ) + 1 ) === 0;
202    }
203
204}