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
32.20
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
19.09
 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            if ( $name !== null && isset( $groupRefs[$name] ) && $groupRefs[$name]->extends === null ) {
66                // T242141: A top-level <ref> can't be changed into a sub-reference
67                return StatusValue::newFatal( 'cite_error_references_duplicate_key', $name );
68            } elseif ( isset( $groupRefs[$extends] ) && $groupRefs[$extends]->extends !== null ) {
69                // A sub-reference can not be extended a second time (no nesting)
70                return StatusValue::newFatal( 'cite_error_ref_nested_extends', $extends,
71                    $groupRefs[$extends]->extends ?? '' );
72            }
73        }
74
75        if ( $follow && ( $name || $extends ) ) {
76            return StatusValue::newFatal( 'cite_error_ref_follow_conflicts' );
77        }
78
79        if ( $dir !== null && $dir !== 'rtl' && $dir !== 'ltr' ) {
80            return StatusValue::newFatal( 'cite_error_ref_invalid_dir', $dir );
81        }
82
83        return $this->inReferencesGroup === null ?
84            $this->validateRefOutsideOfReferenceList( $text, $name ) :
85            $this->validateRefInReferenceList( $text, $group, $name );
86    }
87
88    private function validateRefOutsideOfReferenceList(
89        ?string $text,
90        ?string $name
91    ): StatusValue {
92        if ( !$name ) {
93            if ( $text === null ) {
94                // Completely empty ref like <ref /> is forbidden.
95                return StatusValue::newFatal( 'cite_error_ref_no_key' );
96            } elseif ( trim( $text ) === '' ) {
97                // Must have content or reuse another ref by name.
98                return StatusValue::newFatal( 'cite_error_ref_no_input' );
99            }
100        }
101
102        if ( $text !== null && preg_match(
103                '/<ref(erences)?\b[^>]*+>/i',
104                preg_replace( '#<(\w++)[^>]*+>.*?</\1\s*>|<!--.*?-->#s', '', $text )
105            ) ) {
106            // (bug T8199) This most likely implies that someone left off the
107            // closing </ref> tag, which will cause the entire article to be
108            // eaten up until the next <ref>.  So we bail out early instead.
109            // The fancy regex above first tries chopping out anything that
110            // looks like a comment or SGML tag, which is a crude way to avoid
111            // false alarms for <nowiki>, <pre>, etc.
112            //
113            // Possible improvement: print the warning, followed by the contents
114            // of the <ref> tag.  This way no part of the article will be eaten
115            // even temporarily.
116            return StatusValue::newFatal( 'cite_error_included_ref' );
117        }
118
119        return StatusValue::newGood();
120    }
121
122    private function validateRefInReferenceList(
123        ?string $text,
124        string $group,
125        ?string $name
126    ): StatusValue {
127        if ( $group !== $this->inReferencesGroup ) {
128            // <ref> and <references> have conflicting group attributes.
129            return StatusValue::newFatal( 'cite_error_references_group_mismatch',
130                Sanitizer::safeEncodeAttribute( $group ) );
131        }
132
133        if ( !$name ) {
134            // <ref> calls inside <references> must be named
135            return StatusValue::newFatal( 'cite_error_references_no_key' );
136        }
137
138        if ( $text === null || trim( $text ) === '' ) {
139            // <ref> called in <references> has no content.
140            return StatusValue::newFatal(
141                'cite_error_empty_references_define',
142                Sanitizer::safeEncodeAttribute( $name ),
143                Sanitizer::safeEncodeAttribute( $group )
144            );
145        }
146
147        // Section previews are exempt from some rules.
148        if ( !$this->isSectionPreview ) {
149            $groupRefs = $this->referenceStack->getGroupRefs( $group );
150
151            if ( !isset( $groupRefs[$name] ) ) {
152                // No such named ref exists in this group.
153                return StatusValue::newFatal( 'cite_error_references_missing_key',
154                    Sanitizer::safeEncodeAttribute( $name ) );
155            }
156        }
157
158        return StatusValue::newGood();
159    }
160
161}