Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.74% covered (success)
95.74%
90 / 94
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
FormatChecker
95.74% covered (success)
95.74%
90 / 94
71.43% covered (warning)
71.43%
5 / 7
25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
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
 checkConstraint
94.29% covered (success)
94.29%
33 / 35
0.00% covered (danger)
0.00%
0 / 1
5.00
 formatMessage
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 runRegexCheck
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 runRegexCheckUsingShellbox
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 runRegexCheckUsingSparql
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 checkConstraintParameters
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
3.02
1<?php
2
3namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Checker;
4
5use DataValues\MonolingualTextValue;
6use DataValues\MultilingualTextValue;
7use DataValues\StringValue;
8use MediaWiki\Config\Config;
9use MediaWiki\Shell\ShellboxClientFactory;
10use Shellbox\ShellboxError;
11use Wikibase\DataModel\Entity\ItemId;
12use Wikibase\DataModel\Entity\PropertyId;
13use Wikibase\DataModel\Snak\PropertyValueSnak;
14use WikibaseQuality\ConstraintReport\Constraint;
15use WikibaseQuality\ConstraintReport\ConstraintCheck\ConstraintChecker;
16use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context;
17use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterException;
18use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\ConstraintParameterParser;
19use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\DummySparqlHelper;
20use WikibaseQuality\ConstraintReport\ConstraintCheck\Helper\SparqlHelper;
21use WikibaseQuality\ConstraintReport\ConstraintCheck\Message\ViolationMessage;
22use WikibaseQuality\ConstraintReport\ConstraintCheck\Result\CheckResult;
23use WikibaseQuality\ConstraintReport\Role;
24
25/**
26 * @author BP2014N1
27 * @license GPL-2.0-or-later
28 */
29class FormatChecker implements ConstraintChecker {
30
31    /**
32     * @var ConstraintParameterParser
33     */
34    private $constraintParameterParser;
35
36    /**
37     * @var SparqlHelper
38     */
39    private $sparqlHelper;
40
41    /**
42     * @var Config
43     */
44    private $config;
45
46    /**
47     * @var ShellboxClientFactory
48     */
49    private $shellboxClientFactory;
50
51    /**
52     * @param ConstraintParameterParser $constraintParameterParser
53     * @param Config $config
54     * @param SparqlHelper $sparqlHelper
55     * @param ShellboxClientFactory $shellboxClientFactory
56     */
57    public function __construct(
58        ConstraintParameterParser $constraintParameterParser,
59        Config $config,
60        SparqlHelper $sparqlHelper,
61        ShellboxClientFactory $shellboxClientFactory
62    ) {
63        $this->constraintParameterParser = $constraintParameterParser;
64        $this->config = $config;
65        $this->sparqlHelper = $sparqlHelper;
66        $this->shellboxClientFactory = $shellboxClientFactory;
67    }
68
69    /**
70     * @codeCoverageIgnore This method is purely declarative.
71     */
72    public function getSupportedContextTypes() {
73        return self::ALL_CONTEXT_TYPES_SUPPORTED;
74    }
75
76    /**
77     * @codeCoverageIgnore This method is purely declarative.
78     */
79    public function getDefaultContextTypes() {
80        return Context::ALL_CONTEXT_TYPES;
81    }
82
83    /** @codeCoverageIgnore This method is purely declarative. */
84    public function getSupportedEntityTypes() {
85        return self::ALL_ENTITY_TYPES_SUPPORTED;
86    }
87
88    /**
89     * Checks 'Format' constraint.
90     *
91     * @param Context $context
92     * @param Constraint $constraint
93     *
94     * @throws ConstraintParameterException
95     * @return CheckResult
96     */
97    public function checkConstraint( Context $context, Constraint $constraint ) {
98        $constraintParameters = $constraint->getConstraintParameters();
99        $constraintTypeItemId = $constraint->getConstraintTypeItemId();
100
101        $format = $this->constraintParameterParser->parseFormatParameter(
102            $constraintParameters,
103            $constraintTypeItemId
104        );
105
106        $syntaxClarifications = $this->constraintParameterParser->parseSyntaxClarificationParameter(
107            $constraintParameters
108        );
109
110        $snak = $context->getSnak();
111
112        if ( !$snak instanceof PropertyValueSnak ) {
113            // nothing to check
114            return new CheckResult( $context, $constraint, CheckResult::STATUS_COMPLIANCE );
115        }
116
117        $dataValue = $snak->getDataValue();
118
119        /*
120         * error handling:
121         *   type of $dataValue for properties with 'Format' constraint has to be 'string' or 'monolingualtext'
122         */
123        switch ( $dataValue->getType() ) {
124            case 'string':
125                $text = $dataValue->getValue();
126                break;
127            case 'monolingualtext':
128                /** @var MonolingualTextValue $dataValue */
129                '@phan-var MonolingualTextValue $dataValue';
130                $text = $dataValue->getText();
131                break;
132            default:
133                $message = ( new ViolationMessage( 'wbqc-violation-message-value-needed-of-types-2' ) )
134                    ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM )
135                    ->withDataValueType( 'string' )
136                    ->withDataValueType( 'monolingualtext' );
137                return new CheckResult( $context, $constraint, CheckResult::STATUS_VIOLATION, $message );
138        }
139        $status = $this->runRegexCheck( $text, $format );
140        $message = $this->formatMessage(
141            $status,
142            $text,
143            $format,
144            $context->getSnak()->getPropertyId(),
145            $syntaxClarifications,
146            $constraintTypeItemId
147        );
148        return new CheckResult( $context, $constraint, $status, $message );
149    }
150
151    private function formatMessage(
152        string $status,
153        string $text,
154        string $format,
155        PropertyId $propertyId,
156        MultilingualTextValue $syntaxClarifications,
157        string $constraintTypeItemId
158    ): ?ViolationMessage {
159        $message = null;
160        if ( $status === CheckResult::STATUS_VIOLATION ) {
161            $message = ( new ViolationMessage( 'wbqc-violation-message-format-clarification' ) )
162                ->withEntityId( $propertyId, Role::CONSTRAINT_PROPERTY )
163                ->withDataValue( new StringValue( $text ), Role::OBJECT )
164                ->withInlineCode( $format, Role::CONSTRAINT_PARAMETER_VALUE )
165                ->withMultilingualText( $syntaxClarifications, Role::CONSTRAINT_PARAMETER_VALUE );
166        } elseif ( $status === CheckResult::STATUS_TODO ) {
167            $message = ( new ViolationMessage( 'wbqc-violation-message-security-reason' ) )
168                ->withEntityId( new ItemId( $constraintTypeItemId ), Role::CONSTRAINT_TYPE_ITEM );
169        }
170
171        return $message;
172    }
173
174    private function runRegexCheck( string $text, string $format ): string {
175        if ( !$this->config->get( 'WBQualityConstraintsCheckFormatConstraint' ) ) {
176            return CheckResult::STATUS_TODO;
177        }
178        if (
179            $this->config->get( 'WBQualityConstraintsFormatCheckerShellboxRatio' ) > (float)wfRandom()
180        ) {
181            return $this->runRegexCheckUsingShellbox( $text, $format );
182        }
183
184        return $this->runRegexCheckUsingSparql( $text, $format );
185    }
186
187    private function runRegexCheckUsingShellbox( string $text, string $format ): string {
188        if ( !$this->shellboxClientFactory->isEnabled( 'constraint-regex-checker' ) ) {
189            return CheckResult::STATUS_TODO;
190        }
191        try {
192            $pattern = '/^(?:' . str_replace( '/', '\/', $format ) . ')$/u';
193            $shellboxResponse = $this->shellboxClientFactory->getClient( [
194                'timeout' => $this->config->get( 'WBQualityConstraintsSparqlMaxMillis' ) / 1000,
195                'service' => 'constraint-regex-checker',
196            ] )->call(
197                'constraint-regex-checker',
198                'preg_match',
199                [ $pattern, $text ]
200            );
201        } catch ( ShellboxError $exception ) {
202            throw new ConstraintParameterException(
203                ( new ViolationMessage( 'wbqc-violation-message-parameter-regex' ) )
204                    ->withInlineCode( $pattern, Role::CONSTRAINT_PARAMETER_VALUE )
205            );
206        }
207
208        if ( $shellboxResponse ) {
209            return CheckResult::STATUS_COMPLIANCE;
210        } else {
211            return CheckResult::STATUS_VIOLATION;
212        }
213    }
214
215    private function runRegexCheckUsingSparql( string $text, string $format ): string {
216        if ( $this->sparqlHelper instanceof DummySparqlHelper ) {
217            return CheckResult::STATUS_TODO;
218        }
219
220        if ( $this->sparqlHelper->matchesRegularExpression( $text, $format ) ) {
221            return CheckResult::STATUS_COMPLIANCE;
222        } else {
223            return CheckResult::STATUS_VIOLATION;
224        }
225    }
226
227    public function checkConstraintParameters( Constraint $constraint ) {
228        $constraintParameters = $constraint->getConstraintParameters();
229        $constraintTypeItemId = $constraint->getConstraintTypeItemId();
230        $exceptions = [];
231        try {
232            $this->constraintParameterParser->parseFormatParameter(
233                $constraintParameters,
234                $constraintTypeItemId
235            );
236        } catch ( ConstraintParameterException $e ) {
237            $exceptions[] = $e;
238        }
239        try {
240            $this->constraintParameterParser->parseSyntaxClarificationParameter(
241                $constraintParameters
242            );
243        } catch ( ConstraintParameterException $e ) {
244            $exceptions[] = $e;
245        }
246        return $exceptions;
247    }
248
249}