Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.23% covered (success)
94.23%
49 / 52
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Validator
94.23% covered (success)
94.23%
49 / 52
75.00% covered (warning)
75.00%
3 / 4
30.17
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
 validateRef
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
16.86
 validateRefOutsideOfReferenceList
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 validateRefInReferenceList
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3namespace Cite;
4
5use MediaWiki\Parser\Sanitizer;
6use StatusValue;
7
8/**
9 * Context-aware, detailed validation of the arguments and content of a <ref> tag.
10 *
11 * @license GPL-2.0-or-later
12 */
13class Validator {
14
15    private ReferenceStack $referenceStack;
16    private ?string $inReferencesGroup;
17    private bool $isSectionPreview;
18    private bool $isExtendsEnabled;
19
20    /**
21     * @param ReferenceStack $referenceStack
22     * @param string|null $inReferencesGroup Group name of the <references> context to consider
23     *  during validation. Null if we are currently not in a <references> context.
24     * @param bool $isSectionPreview Validation is relaxed when previewing parts of a page
25     * @param bool $isExtendsEnabled Temporary feature flag
26     */
27    public function __construct(
28        ReferenceStack $referenceStack,
29        ?string $inReferencesGroup = null,
30        bool $isSectionPreview = false,
31        bool $isExtendsEnabled = false
32    ) {
33        $this->referenceStack = $referenceStack;
34        $this->inReferencesGroup = $inReferencesGroup;
35        $this->isSectionPreview = $isSectionPreview;
36        $this->isExtendsEnabled = $isExtendsEnabled;
37    }
38
39    public function validateRef(
40        ?string $text,
41        string $group,
42        ?string $name,
43        ?string $extends,
44        ?string $follow,
45        ?string $dir
46    ): StatusValue {
47        if ( ctype_digit( (string)$name )
48            || ctype_digit( (string)$extends )
49            || ctype_digit( (string)$follow )
50        ) {
51            // Numeric names mess up the resulting id's, potentially producing
52            // duplicate id's in the XHTML.  The Right Thing To Do
53            // would be to mangle them, but it's not really high-priority
54            // (and would produce weird id's anyway).
55            return StatusValue::newFatal( 'cite_error_ref_numeric_key' );
56        }
57
58        if ( $extends ) {
59            // Temporary feature flag until mainstreamed, see T236255
60            if ( !$this->isExtendsEnabled ) {
61                return StatusValue::newFatal( 'cite_error_ref_too_many_keys' );
62            }
63
64            $groupRefs = $this->referenceStack->getGroupRefs( $group );
65            // @phan-suppress-next-line PhanTypeMismatchDimFetchNullable false positive
66            if ( isset( $groupRefs[$name] ) && !isset( $groupRefs[$name]->extends ) ) {
67                // T242141: A top-level <ref> can't be changed into a sub-reference
68                return StatusValue::newFatal( 'cite_error_references_duplicate_key', $name );
69            } elseif ( isset( $groupRefs[$extends]->extends ) ) {
70                // A sub-reference can not be extended a second time (no nesting)
71                return StatusValue::newFatal( 'cite_error_ref_nested_extends', $extends,
72                    $groupRefs[$extends]->extends );
73            }
74        }
75
76        if ( $follow && ( $name || $extends ) ) {
77            return StatusValue::newFatal( 'cite_error_ref_follow_conflicts' );
78        }
79
80        if ( $dir !== null && $dir !== 'rtl' && $dir !== 'ltr' ) {
81            return StatusValue::newFatal( 'cite_error_ref_invalid_dir', $dir );
82        }
83
84        return $this->inReferencesGroup === null ?
85            $this->validateRefOutsideOfReferenceList( $text, $name ) :
86            $this->validateRefInReferenceList( $text, $group, $name );
87    }
88
89    private function validateRefOutsideOfReferenceList(
90        ?string $text,
91        ?string $name
92    ): StatusValue {
93        if ( !$name ) {
94            if ( $text === null ) {
95                // Completely empty ref like <ref /> is forbidden.
96                return StatusValue::newFatal( 'cite_error_ref_no_key' );
97            } elseif ( trim( $text ) === '' ) {
98                // Must have content or reuse another ref by name.
99                return StatusValue::newFatal( 'cite_error_ref_no_input' );
100            }
101        }
102
103        if ( $text !== null && preg_match(
104                '/<ref(erences)?\b[^>]*+>/i',
105                preg_replace( '#<(\w++)[^>]*+>.*?</\1\s*>|<!--.*?-->#s', '', $text )
106            ) ) {
107            // (bug T8199) This most likely implies that someone left off the
108            // closing </ref> tag, which will cause the entire article to be
109            // eaten up until the next <ref>.  So we bail out early instead.
110            // The fancy regex above first tries chopping out anything that
111            // looks like a comment or SGML tag, which is a crude way to avoid
112            // false alarms for <nowiki>, <pre>, etc.
113            //
114            // Possible improvement: print the warning, followed by the contents
115            // of the <ref> tag.  This way no part of the article will be eaten
116            // even temporarily.
117            return StatusValue::newFatal( 'cite_error_included_ref' );
118        }
119
120        return StatusValue::newGood();
121    }
122
123    private function validateRefInReferenceList(
124        ?string $text,
125        string $group,
126        ?string $name
127    ): StatusValue {
128        if ( $group !== $this->inReferencesGroup ) {
129            // <ref> and <references> have conflicting group attributes.
130            return StatusValue::newFatal( 'cite_error_references_group_mismatch',
131                Sanitizer::safeEncodeAttribute( $group ) );
132        }
133
134        if ( !$name ) {
135            // <ref> calls inside <references> must be named
136            return StatusValue::newFatal( 'cite_error_references_no_key' );
137        }
138
139        if ( $text === null || trim( $text ) === '' ) {
140            // <ref> called in <references> has no content.
141            return StatusValue::newFatal(
142                'cite_error_empty_references_define',
143                Sanitizer::safeEncodeAttribute( $name ),
144                Sanitizer::safeEncodeAttribute( $group )
145            );
146        }
147
148        // Section previews are exempt from some rules.
149        if ( !$this->isSectionPreview ) {
150            $groupRefs = $this->referenceStack->getGroupRefs( $group );
151
152            if ( !isset( $groupRefs[$name] ) ) {
153                // No such named ref exists in this group.
154                return StatusValue::newFatal( 'cite_error_references_missing_key',
155                    Sanitizer::safeEncodeAttribute( $name ) );
156            }
157        }
158
159        return StatusValue::newGood();
160    }
161
162}