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