Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.08% covered (success)
96.08%
49 / 51
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
IfNoneMatch
96.08% covered (success)
96.08%
49 / 51
66.67% covered (warning)
66.67%
4 / 6
15
0.00% covered (danger)
0.00%
0 / 1
 parseHeaderList
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 parseETag
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getLastError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseHeader
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 consumeTagList
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 consumeTag
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
3.00
1<?php
2
3namespace MediaWiki\Rest\HeaderParser;
4
5/**
6 * A class to assist with the parsing of If-None-Match, If-Match and ETag headers
7 */
8class IfNoneMatch extends HeaderParserBase {
9    private $results = [];
10
11    /** @var string|null */
12    private $lastError;
13
14    /**
15     * Parse an If-None-Match or If-Match header list as returned by
16     * RequestInterface::getHeader().
17     *
18     * The return value is an array of tag info arrays. Each tag info array is
19     * an associative array with the following keys:
20     *
21     *   - weak: True if the tag is weak, false otherwise
22     *   - contents: The contents of the double-quoted opaque-tag, not
23     *     including the quotes.
24     *   - whole: The whole ETag, including weak indicator and quoted opaque-tag
25     *
26     * In the case of a wildcard header like "If-Match: *", there cannot be any
27     * other tags. The return value is an array with a single tag info array with
28     * 'whole' => '*'.
29     *
30     * If the header was invalid, an empty array will be returned. Further
31     * information about why the parsing failed can be found by calling
32     * getLastError().
33     *
34     * @param string[] $headerList
35     * @return array[]
36     */
37    public function parseHeaderList( $headerList ) {
38        $this->lastError = null;
39        if ( count( $headerList ) === 1 && $headerList[0] === '*' ) {
40            return [ [
41                'weak' => true,
42                'contents' => null,
43                'whole' => '*'
44            ] ];
45        }
46        $this->results = [];
47        try {
48            foreach ( $headerList as $header ) {
49                $this->parseHeader( $header );
50            }
51            return $this->results;
52        } catch ( HeaderParserError $e ) {
53            $this->lastError = $e->getMessage();
54            return [];
55        }
56    }
57
58    /**
59     * Parse an entity-tag such as might be found in an ETag response header.
60     * The result is an associative array in the same format returned by
61     * parseHeaderList().
62     *
63     * If parsing failed, null is returned.
64     *
65     * @param string $eTag
66     * @return array|null
67     */
68    public function parseETag( $eTag ) {
69        $this->setInput( $eTag );
70        $this->results = [];
71
72        try {
73            $this->consumeTag();
74            $this->assertEnd();
75        } catch ( HeaderParserError $e ) {
76            $this->lastError = $e->getMessage();
77            return null;
78        }
79        /* @phan-suppress-next-line PhanTypeInvalidDimOffset */
80        return $this->results[0];
81    }
82
83    /**
84     * Get the last parse error message, or null if there was none
85     *
86     * @return string|null
87     */
88    public function getLastError() {
89        return $this->lastError;
90    }
91
92    /**
93     * Parse a single header
94     *
95     * @param string $header
96     * @throws HeaderParserError
97     */
98    private function parseHeader( $header ) {
99        $this->setInput( $header );
100        $this->consumeTagList();
101        $this->assertEnd();
102    }
103
104    /**
105     * Consume a comma-separated list of entity-tags
106     *
107     * @throws HeaderParserError
108     */
109    private function consumeTagList() {
110        while ( true ) {
111            $this->skipWhitespace();
112            $this->consumeTag();
113            $this->skipWhitespace();
114            if ( $this->pos === $this->inputLength ) {
115                break;
116            } else {
117                $this->consumeString( ',' );
118            }
119        }
120    }
121
122    /**
123     * Consume a single entity-tag and append to the result array
124     *
125     * @throws HeaderParserError
126     */
127    private function consumeTag() {
128        $weak = false;
129        $whole = '';
130        if ( substr( $this->input, $this->pos, 2 ) === 'W/' ) {
131            $weak = true;
132            $whole .= 'W/';
133            $this->pos += 2;
134        }
135        $this->consumeString( '"' );
136        if ( !preg_match( '/[!#-~\x80-\xff]*/A', $this->input, $m, 0, $this->pos ) ) {
137            $this->error( 'unexpected regex failure' );
138        }
139        $contents = $m[0];
140        $this->pos += strlen( $contents );
141        $this->consumeString( '"' );
142        $whole .= '"' . $contents . '"';
143        $this->results[] = [
144            'weak' => $weak,
145            'contents' => $contents,
146            'whole' => $whole
147        ];
148    }
149}