Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.91% covered (warning)
70.91%
39 / 55
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
HeaderContainer
70.91% covered (warning)
70.91%
39 / 55
72.73% covered (warning)
72.73%
8 / 11
31.86
0.00% covered (danger)
0.00%
0 / 1
 resetHeaders
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 convertToListAndString
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setHeader
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 addHeader
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 removeHeader
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeader
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 hasHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeaderLine
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getHeaderLines
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRawHeaderLines
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace MediaWiki\Rest;
4
5/**
6 * This is a container for storing headers. The header names are case-insensitive,
7 * but the case is preserved for methods that return headers in bulk. The
8 * header values are a comma-separated list, or equivalently, an array of strings.
9 *
10 * Unlike PSR-7, the container is mutable.
11 */
12class HeaderContainer {
13    /** @var array[] */
14    private $headerLists = [];
15    /** @var string[] */
16    private $headerLines = [];
17    /** @var string[] */
18    private $headerNames = [];
19
20    /**
21     * Erase any existing headers and replace them with the specified
22     * header arrays or values.
23     *
24     * @param array $headers
25     */
26    public function resetHeaders( $headers = [] ) {
27        $this->headerLines = [];
28        $this->headerLists = [];
29        $this->headerNames = [];
30        foreach ( $headers as $name => $value ) {
31            $this->headerNames[ strtolower( $name ) ] = $name;
32            [ $valueParts, $valueLine ] = $this->convertToListAndString( $value );
33            $this->headerLines[$name] = $valueLine;
34            $this->headerLists[$name] = $valueParts;
35        }
36    }
37
38    /**
39     * Take an input header value, which may either be a string or an array,
40     * and convert it to an array of header values and a header line.
41     *
42     * The return value is an array where element 0 has the array of header
43     * values, and element 1 has the header line.
44     *
45     * Theoretically, if the input is a string, this could parse the string
46     * and split it on commas. Doing this is complicated, because some headers
47     * can contain double-quoted strings containing commas. The User-Agent
48     * header allows commas in comments delimited by parentheses. So it is not
49     * just explode(",", $value), we would need to parse a grammar defined by
50     * RFC 7231 appendix D which depends on header name.
51     *
52     * It's unclear how much it would help handlers to have fully spec-aware
53     * HTTP header handling just to split on commas. They would probably be
54     * better served by an HTTP header parsing library which provides the full
55     * parse tree.
56     *
57     * @param string|string[] $value The input header value
58     * @return array
59     */
60    private function convertToListAndString( $value ) {
61        if ( is_array( $value ) ) {
62            return [ array_values( $value ), implode( ', ', $value ) ];
63        } else {
64            return [ [ $value ], $value ];
65        }
66    }
67
68    /**
69     * Set or replace a header
70     *
71     * @param string $name
72     * @param string|string[] $value
73     */
74    public function setHeader( $name, $value ) {
75        [ $valueParts, $valueLine ] = $this->convertToListAndString( $value );
76        $lowerName = strtolower( $name );
77        $origName = $this->headerNames[$lowerName] ?? null;
78        if ( $origName !== null ) {
79            unset( $this->headerLines[$origName] );
80            unset( $this->headerLists[$origName] );
81        }
82        $this->headerNames[$lowerName] = $name;
83        $this->headerLines[$name] = $valueLine;
84        $this->headerLists[$name] = $valueParts;
85    }
86
87    /**
88     * Set a header or append to an existing header
89     *
90     * @param string $name
91     * @param string|string[] $value
92     */
93    public function addHeader( $name, $value ) {
94        [ $valueParts, $valueLine ] = $this->convertToListAndString( $value );
95        $lowerName = strtolower( $name );
96        $origName = $this->headerNames[$lowerName] ?? null;
97        if ( $origName === null ) {
98            $origName = $name;
99            $this->headerNames[$lowerName] = $origName;
100            $this->headerLines[$origName] = $valueLine;
101            $this->headerLists[$origName] = $valueParts;
102        } else {
103            $this->headerLines[$origName] .= ', ' . $valueLine;
104            $this->headerLists[$origName] = array_merge( $this->headerLists[$origName],
105                $valueParts );
106        }
107    }
108
109    /**
110     * Remove a header
111     *
112     * @param string $name
113     */
114    public function removeHeader( $name ) {
115        $lowerName = strtolower( $name );
116        $origName = $this->headerNames[$lowerName] ?? null;
117        if ( $origName !== null ) {
118            unset( $this->headerNames[$lowerName] );
119            unset( $this->headerLines[$origName] );
120            unset( $this->headerLists[$origName] );
121        }
122    }
123
124    /**
125     * Get header arrays indexed by original name
126     *
127     * @return string[][]
128     */
129    public function getHeaders() {
130        return $this->headerLists;
131    }
132
133    /**
134     * Get the header with a particular name, or an empty array if there is no
135     * such header.
136     *
137     * @param string $name
138     * @return string[]
139     */
140    public function getHeader( $name ) {
141        $headerName = $this->headerNames[ strtolower( $name ) ] ?? null;
142        if ( $headerName === null ) {
143            return [];
144        }
145        return $this->headerLists[$headerName];
146    }
147
148    /**
149     * Return true if the header exists, false otherwise
150     * @param string $name
151     * @return bool
152     */
153    public function hasHeader( $name ) {
154        return isset( $this->headerNames[ strtolower( $name ) ] );
155    }
156
157    /**
158     * Get the specified header concatenated into a comma-separated string.
159     * If the header does not exist, an empty string is returned.
160     *
161     * @param string $name
162     * @return string
163     */
164    public function getHeaderLine( $name ) {
165        $headerName = $this->headerNames[ strtolower( $name ) ] ?? null;
166        if ( $headerName === null ) {
167            return '';
168        }
169        return $this->headerLines[$headerName];
170    }
171
172    /**
173     * Get all header lines
174     *
175     * @return string[]
176     */
177    public function getHeaderLines() {
178        return $this->headerLines;
179    }
180
181    /**
182     * Get an array of strings of the form "Name: Value", suitable for passing
183     * directly to header() to set response headers. The PHP manual describes
184     * these strings as "raw HTTP headers", so we adopt that terminology.
185     *
186     * @return string[] Header list (integer indexed)
187     */
188    public function getRawHeaderLines() {
189        $lines = [];
190        foreach ( $this->headerNames as $lowerName => $name ) {
191            if ( $lowerName === 'set-cookie' ) {
192                // As noted by RFC 7230 section 3.2.2, Set-Cookie is the only
193                // header for which multiple values cannot be concatenated into
194                // a single comma-separated line.
195                foreach ( $this->headerLists[$name] as $value ) {
196                    $lines[] = "$name$value";
197                }
198            } else {
199                $lines[] = "$name" . $this->headerLines[$name];
200            }
201        }
202        return $lines;
203    }
204}