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