Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.35% covered (warning)
69.35%
86 / 124
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckConstraintParameters
69.35% covered (warning)
69.35%
86 / 124
70.00% covered (warning)
70.00%
7 / 10
45.46
0.00% covered (danger)
0.00%
0 / 1
 newFromGlobalState
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 parsePropertyIds
45.00% covered (danger)
45.00%
9 / 20
0.00% covered (danger)
0.00%
0 / 1
6.66
 parseConstraintIds
39.29% covered (danger)
39.29%
11 / 28
0.00% covered (danger)
0.00%
0 / 1
10.60
 checkPropertyIds
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 checkConstraintIds
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getResultPathForPropertyId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResultPathForConstraintId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConstraintParameterExceptionsToResult
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
4
 getAllowedParams
n/a
0 / 0
n/a
0 / 0
1
 getExamplesMessages
n/a
0 / 0
n/a
0 / 0
1
1<?php
2
3declare( strict_types = 1 );
4
5namespace WikibaseQuality\ConstraintReport\Api;
6
7use ApiBase;
8use ApiMain;
9use ApiResult;
10use IBufferingStatsdDataFactory;
11use InvalidArgumentException;
12use Wikibase\DataModel\Entity\NumericPropertyId;
13use Wikibase\DataModel\Services\Statement\StatementGuidParser;
14use Wikibase\DataModel\Services\Statement\StatementGuidParsingException;
15use Wikibase\Lib\LanguageFallbackChainFactory;
16use Wikibase\Repo\Api\ApiErrorReporter;
17use Wikibase\Repo\Api\ApiHelperFactory;
18use WikibaseQuality\ConstraintReport\ConstraintCheck\DelegatingConstraintChecker;
19use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterException;
20use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessageRendererFactory;
21use Wikimedia\ParamValidator\ParamValidator;
22
23/**
24 * API module that checks whether the parameters of a constraint statement are valid.
25 *
26 * @author Lucas Werkmeister
27 * @license GPL-2.0-or-later
28 */
29class CheckConstraintParameters extends ApiBase {
30
31    public const PARAM_PROPERTY_ID = 'propertyid';
32    public const PARAM_CONSTRAINT_ID = 'constraintid';
33    public const KEY_STATUS = 'status';
34    public const STATUS_OKAY = 'okay';
35    public const STATUS_NOT_OKAY = 'not-okay';
36    public const STATUS_NOT_FOUND = 'not-found';
37    public const KEY_PROBLEMS = 'problems';
38    public const KEY_MESSAGE_HTML = 'message-html';
39
40    private ApiErrorReporter $apiErrorReporter;
41    private LanguageFallbackChainFactory $languageFallbackChainFactory;
42    private DelegatingConstraintChecker $delegatingConstraintChecker;
43    private ViolationMessageRendererFactory $violationMessageRendererFactory;
44    private StatementGuidParser $statementGuidParser;
45    private IBufferingStatsdDataFactory $dataFactory;
46
47    /**
48     * Creates new instance from global state.
49     */
50    public static function newFromGlobalState(
51        ApiMain $main,
52        string $name,
53        IBufferingStatsdDataFactory $dataFactory,
54        ApiHelperFactory $apiHelperFactory,
55        LanguageFallbackChainFactory $languageFallbackChainFactory,
56        StatementGuidParser $statementGuidParser,
57        DelegatingConstraintChecker $delegatingConstraintChecker,
58        ViolationMessageRendererFactory $violationMessageRendererFactory
59    ): self {
60        return new self(
61            $main,
62            $name,
63            $apiHelperFactory,
64            $languageFallbackChainFactory,
65            $delegatingConstraintChecker,
66            $violationMessageRendererFactory,
67            $statementGuidParser,
68            $dataFactory
69        );
70    }
71
72    public function __construct(
73        ApiMain $main,
74        string $name,
75        ApiHelperFactory $apiHelperFactory,
76        LanguageFallbackChainFactory $languageFallbackChainFactory,
77        DelegatingConstraintChecker $delegatingConstraintChecker,
78        ViolationMessageRendererFactory $violationMessageRendererFactory,
79        StatementGuidParser $statementGuidParser,
80        IBufferingStatsdDataFactory $dataFactory
81    ) {
82        parent::__construct( $main, $name );
83
84        $this->apiErrorReporter = $apiHelperFactory->getErrorReporter( $this );
85        $this->languageFallbackChainFactory = $languageFallbackChainFactory;
86        $this->delegatingConstraintChecker = $delegatingConstraintChecker;
87        $this->violationMessageRendererFactory = $violationMessageRendererFactory;
88        $this->statementGuidParser = $statementGuidParser;
89        $this->dataFactory = $dataFactory;
90    }
91
92    public function execute() {
93        $this->dataFactory->increment(
94            'wikibase.quality.constraints.api.checkConstraintParameters.execute'
95        );
96
97        $params = $this->extractRequestParams();
98        $result = $this->getResult();
99
100        $propertyIds = $this->parsePropertyIds( $params[self::PARAM_PROPERTY_ID] );
101        $constraintIds = $this->parseConstraintIds( $params[self::PARAM_CONSTRAINT_ID] );
102
103        $this->checkPropertyIds( $propertyIds, $result );
104        $this->checkConstraintIds( $constraintIds, $result );
105
106        $result->addValue( null, 'success', 1 );
107    }
108
109    /**
110     * @param array|null $propertyIdSerializations
111     * @return NumericPropertyId[]
112     */
113    private function parsePropertyIds( ?array $propertyIdSerializations ): array {
114        if ( $propertyIdSerializations === null ) {
115            return [];
116        } elseif ( $propertyIdSerializations === [] ) {
117            $this->apiErrorReporter->dieError(
118                'If ' . self::PARAM_PROPERTY_ID . ' is specified, it must be nonempty.',
119                'no-data'
120            );
121        }
122
123        return array_map(
124            function ( $propertyIdSerialization ) {
125                try {
126                    return new NumericPropertyId( $propertyIdSerialization );
127                } catch ( InvalidArgumentException $e ) {
128                    $this->apiErrorReporter->dieError(
129                        "Invalid id: $propertyIdSerialization",
130                        'invalid-property-id',
131                        0, // default argument
132                        [ self::PARAM_PROPERTY_ID => $propertyIdSerialization ]
133                    );
134                }
135            },
136            $propertyIdSerializations
137        );
138    }
139
140    /**
141     * @param array|null $constraintIds
142     * @return string[]
143     */
144    private function parseConstraintIds( ?array $constraintIds ): array {
145        if ( $constraintIds === null ) {
146            return [];
147        } elseif ( $constraintIds === [] ) {
148            $this->apiErrorReporter->dieError(
149                'If ' . self::PARAM_CONSTRAINT_ID . ' is specified, it must be nonempty.',
150                'no-data'
151            );
152        }
153
154        return array_map(
155            function ( $constraintId ) {
156                try {
157                    $propertyId = $this->statementGuidParser->parse( $constraintId )->getEntityId();
158                    if ( !$propertyId instanceof NumericPropertyId ) {
159                        $this->apiErrorReporter->dieError(
160                            "Invalid property ID: {$propertyId->getSerialization()}",
161                            'invalid-property-id',
162                            0, // default argument
163                            [ self::PARAM_CONSTRAINT_ID => $constraintId ]
164                        );
165                    }
166                    return $constraintId;
167                } catch ( StatementGuidParsingException $e ) {
168                    $this->apiErrorReporter->dieError(
169                        "Invalid statement GUID: $constraintId",
170                        'invalid-guid',
171                        0, // default argument
172                        [ self::PARAM_CONSTRAINT_ID => $constraintId ]
173                    );
174                }
175            },
176            $constraintIds
177        );
178    }
179
180    /**
181     * @param NumericPropertyId[] $propertyIds
182     * @param ApiResult $result
183     */
184    private function checkPropertyIds( array $propertyIds, ApiResult $result ): void {
185        foreach ( $propertyIds as $propertyId ) {
186            $result->addArrayType( $this->getResultPathForPropertyId( $propertyId ), 'assoc' );
187            $allConstraintExceptions = $this->delegatingConstraintChecker
188                ->checkConstraintParametersOnPropertyId( $propertyId );
189            foreach ( $allConstraintExceptions as $constraintId => $constraintParameterExceptions ) {
190                $this->addConstraintParameterExceptionsToResult(
191                    $constraintId,
192                    $constraintParameterExceptions,
193                    $result
194                );
195            }
196        }
197    }
198
199    /**
200     * @param string[] $constraintIds
201     * @param ApiResult $result
202     */
203    private function checkConstraintIds( array $constraintIds, ApiResult $result ): void {
204        foreach ( $constraintIds as $constraintId ) {
205            if ( $result->getResultData( $this->getResultPathForConstraintId( $constraintId ) ) ) {
206                // already checked as part of checkPropertyIds()
207                continue;
208            }
209            $constraintParameterExceptions = $this->delegatingConstraintChecker
210                ->checkConstraintParametersOnConstraintId( $constraintId );
211            $this->addConstraintParameterExceptionsToResult( $constraintId, $constraintParameterExceptions, $result );
212        }
213    }
214
215    /**
216     * @param NumericPropertyId $propertyId
217     * @return string[]
218     */
219    private function getResultPathForPropertyId( NumericPropertyId $propertyId ): array {
220        return [ $this->getModuleName(), $propertyId->getSerialization() ];
221    }
222
223    /**
224     * @param string $constraintId
225     * @return string[]
226     */
227    private function getResultPathForConstraintId( string $constraintId ): array {
228        $propertyId = $this->statementGuidParser->parse( $constraintId )->getEntityId();
229        '@phan-var NumericPropertyId $propertyId';
230        return array_merge( $this->getResultPathForPropertyId( $propertyId ), [ $constraintId ] );
231    }
232
233    /**
234     * Add the ConstraintParameterExceptions for $constraintId to the API result.
235     *
236     * @param string $constraintId
237     * @param ConstraintParameterException[]|null $constraintParameterExceptions
238     * @param ApiResult $result
239     */
240    private function addConstraintParameterExceptionsToResult(
241        string $constraintId,
242        ?array $constraintParameterExceptions,
243        ApiResult $result
244    ): void {
245        $path = $this->getResultPathForConstraintId( $constraintId );
246        if ( $constraintParameterExceptions === null ) {
247            $result->addValue(
248                $path,
249                self::KEY_STATUS,
250                self::STATUS_NOT_FOUND
251            );
252        } else {
253            $result->addValue(
254                $path,
255                self::KEY_STATUS,
256                $constraintParameterExceptions === [] ? self::STATUS_OKAY : self::STATUS_NOT_OKAY
257            );
258
259            $language = $this->getLanguage();
260            $violationMessageRenderer = $this->violationMessageRendererFactory
261                ->getViolationMessageRenderer(
262                    $language,
263                    $this->languageFallbackChainFactory->newFromLanguage( $language ),
264                    $this
265                );
266            $problems = [];
267            foreach ( $constraintParameterExceptions as $constraintParameterException ) {
268                $problems[] = [
269                    self::KEY_MESSAGE_HTML => $violationMessageRenderer->render(
270                        $constraintParameterException->getViolationMessage() ),
271                ];
272            }
273            $result->addValue(
274                $path,
275                self::KEY_PROBLEMS,
276                $problems
277            );
278        }
279    }
280
281    /**
282     * @return array[]
283     * @codeCoverageIgnore
284     */
285    public function getAllowedParams() {
286        return [
287            self::PARAM_PROPERTY_ID => [
288                ParamValidator::PARAM_TYPE => 'string',
289                ParamValidator::PARAM_ISMULTI => true,
290            ],
291            self::PARAM_CONSTRAINT_ID => [
292                ParamValidator::PARAM_TYPE => 'string',
293                ParamValidator::PARAM_ISMULTI => true,
294            ],
295        ];
296    }
297
298    /**
299     * @return string[]
300     * @codeCoverageIgnore
301     */
302    public function getExamplesMessages() {
303        return [
304            'action=wbcheckconstraintparameters&propertyid=P247'
305                => 'apihelp-wbcheckconstraintparameters-example-propertyid-1',
306            'action=wbcheckconstraintparameters&' .
307            'constraintid=P247$0fe1711e-4c0f-82ce-3af0-830b721d0fba|' .
308            'P225$cdc71e4a-47a0-12c5-dfb3-3f6fc0b6613f'
309                => 'apihelp-wbcheckconstraintparameters-example-constraintid-2',
310        ];
311    }
312
313}