Completed
Pull Request — master (#20)
by Auke
02:48
created

headers::isAcceptable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 4
Bugs 1 Features 3
Metric Value
c 4
b 1
f 3
dl 0
loc 6
ccs 0
cts 0
cp 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
crap 2
1
<?php
2
3
/*
4
 * This file is part of the Ariadne Component Library.
5
 *
6
 * (c) Muze <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace arc\http;
13
14
/**
15
 * This class contains static methods to help parse HTTP headers.
16
 * @package arc\http
17
 */
18
final class headers
19
{
20
21
    /**
22
     * Parse response headers string from a HTTP request into an array of headers. e.g.
23
     * [ 'Location' => 'http://www.example.com', ... ]
24
     * When multiple headers with the same name are present, all values will form an array, in the order in which
25
     * they are present in the source.
26
     * @param string|string[] $headers The headers string to parse.
27
     * @return array
28
     */
29 6
    public static function parse( $headers ) {
30 6
        if ( !is_array($headers) && !$headers instanceof \ArrayObject ) {
31 4
            $headers = array_filter(
32 4
                array_map( 'trim', explode( "\n", (string) $headers ) )
33 4
            );
34 4
        }
35 6
        $result = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
36 6
        $currentName = '';
37 6
        foreach( $headers as $key => $header ) {
38 6
            if ( !is_array($header) ) {
39 6
                @list($name, $value) = array_map('trim', explode(':', $header, 2) );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
40
            } else {
41
                $name = $header;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
42 6
                $value = null;
43 6
            }
44
            if ( isset( $value ) ) {
45 6
                $result = self::addHeader($result, $name, $value);
46 6
            } else if (is_numeric($key)) {
47
                if ( $currentName ) {
48 2
                    $result = self::addHeader($result, $currentName, $name);
49 2
                } else {
50 2
                    $result[] = $name;
51 2
                }
52 2
                $name = $key;
53 1
            } else {
54
                $result = self::addHeader($result, $key, $name);
55 6
                $name = $key;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
56 5
            }
57 5
            $currentName = ( is_numeric($name) ? $currentName : $name );
58
        }
59
        return $result;
60 6
    }
61 6
62
    /**
63
     * Return an array with values from a Comma seperated header like Cache-Control or Accept
64
     * e.g. 'max-age=300,public,no-store'
65
     * results in
66
     * [ 0 => [ 'max-age' => '300' ], 1 => [ 'public' => 'public' ], 2 => ['no-store' => 'no-store'] ]
67
     * @param string $header
68
     * @return array
69 1
     */
70 1
    public static function parseHeader($header)
71
    {
72
        $header = (strpos($header, ':')!==false) ? explode(':', $header)[1] : $header;
73 1
        $parts   = array_map('trim', explode(',', $header));
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 3 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
74
        $header = [];
75
        foreach ( $parts as $part ) {
76
            $elements = array_map('trim', explode(';', $part));
77
            $result = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
78
            foreach ($elements as $element) {
79
                @list($name, $value)          = array_map('trim', explode( '=', $element));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 10 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
80
                if ( !isset($value) ) {
81
                    $result['value'] = $name;
82
                } else {
83
                    $result[$name] = (isset($value) ? $value : $name);
84 4
                }
85
            }
86 4
            $header[] = $result;
87 4
        }
88 4
        return $header;
89 4
    }
90 4
91 4
    /**
92 4
     * Merge multiple occurances of a comma seperated header
93 4
     * @param array $headers
94
     * @return array
95
     */
96
    public static function mergeHeaders( $headers )
97
    {
98
        $result = [];
99
        if ( is_string($headers) ) {
100
            $result = self::parseHeader( $headers );
101 4
        } else foreach ( $headers as $header ) {
102
            if (is_string($header)) {
103 4
                $header = self::parseHeader($header);
104 4
            }
105 3
            $result = array_merge( $result, $header );
106 4
        }
107 1
        return $result;
108 1
    }
109 1
110 1
    /**
111 1
     * Parse response headers to determine if and how long you may cache the response. Doesn't understand ETags.
112 4
     * @param string|string[] $headers Headers string or array as returned by parse()
113
     * @param bool $private Whether to store a private cache (true) or public cache image (false). Default is public.
114
     * @return int The number of seconds you may cache this result starting from now.
115 4
     */
116
    public static function parseCacheTime( $headers, $private=false )
117 4
    {
118 4
        $result = null;
119 4
        if ( is_string($headers) || ( !isset($headers['Cache-Control']) && !isset($headers['Expires']) ) ) {
120
            $headers = \arc\http\headers::parse( $headers );
0 ignored issues
show
Bug introduced by
It seems like $headers defined by \arc\http\headers::parse($headers) on line 120 can also be of type array<integer|string,str...ull","Expires":"null"}>; however, arc\http\headers::parse() does only seem to accept string|array<integer,string>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
121 4
        }
122 4
        if ( isset( $headers['Cache-Control'] ) ) {
123 4
            $header = self::mergeHeaders( $headers['Cache-Control'] );
124 4
            $result = self::getCacheControlTime( $header, $private );
125 4
        }
126 4
        if ( !isset($result) && isset( $headers['Expires'] ) ) {
127
            $result = strtotime( self::getLastHeader( $headers['Expires'] ) ) - time();
128 4
        }
129 4
        return (int) $result;
130 3
    }
131 3
132 1
    /**
133 1
     * Parses Accept-* header and returns best matching value from the $acceptable list
134 1
     * Takes into account the Q value and wildcards. Does not take into account other parameters
135 1
     * currently ( e.g. text/html;level=1 )
136 2
     * @param array|string $header The Accept-* header (Accept:, Accept-Lang:, Accept-Encoding: etc.)
137 2
     * @param array $acceptable List of acceptable values, in order of preference
138 1
     * @return string
139 1
     */
140 1
    public static function accept( $header, $acceptable )
141 1
    {
142
        if ( is_string($header) ) {
143
            $header = \arc\http\headers::parseHeader( $header );
144 1
        }
145 1
        $ordered = self::orderByQuality($header);
146 1
        foreach( $ordered as $value ) {
147 4
            if ( self::isAcceptable($value['value'], $acceptable) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::isAcceptable($value['value'], $acceptable) of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
148 4
                return $value['value'];
149 2
            }
150 2
        }
151 4
    }
152
153
    private static function addHeader($headers, $name, $value)
154
    {
155
        if ( !isset($headers[ $name]) ) {
156
            // first entry for this header
157
            $headers[ $name ] = $value;
158
        } else if ( is_string($headers[ $name ]) ) {
159
            // second header entry with same name
160 5
            $headers[ $name ] = [
161
                $headers[ $name ],
162 5
                $value
163 5
            ];
164 1
        } else { // third or later header entry with same name
165 1
            $headers[ $name ][] = $value;
166 5
        }
167 4
        return $headers;
168 4
    }
169 4
170 5
    private static function getCacheControlTime( $header, $private )
171 1
    {
172 1
        $result    = null;
173 5
        $dontcache = false;
174
        foreach ( $header as $value ) {
175
            if ( isset($value['value']) ) {
176
                switch($value['value']) {
177
                    case 'private':
178
                        if ( !$private ) {
179
                            $dontcache = true;
180
                        }
181
                    break;
182
                    case 'no-cache':
183
                    case 'no-store':
184
                    case 'must-revalidate':
185
                    case 'proxy-revalidate':
186
                        $dontcache = true;
187
                    break;
188
                }
189
            } else if ( isset($value['max-age']) || isset($value['s-maxage']) ) {
190
                $maxage = (int) (isset($value['max-age']) ? $value['max-age'] : $value['s-maxage']);
191
                if ( isset($result) ) {
192
                    $result = min($result, $maxage);
193
                } else {
194
                    $result = $maxage;
195
                }
196
            }
197
        }
198
        if ( $dontcache ) {
199
            $result = 0;
200
        }
201
        return $result;
202
    }
203
204
    private static function orderByQuality($header)
205
    {
206
        $getQ = function($entry) {
207
            $q = ( isset($entry['q']) ? floatval($entry['q']) : 1);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
208
            $name = $entry['value'];
209
            if ( $name[ strlen($name)-1 ] == '*' || $name[0] == '*' ) {
210
                $q -= 0.0001; // exact matches are preferred over wildcards
211
            }
212
            return $q;
213
        };
214
        usort($header, function($a,$b) use ($getQ) {
215
            return ($getQ($a)>$getQ($b) ? -1 : 1);
216
        });
217
        return $header;
218
    }
219
220
    private static function pregEscape($string) {
221
        $special = ".\\+'?[^]$(){}=!<>|:";
222
        // * and - are not included, since they are allowed in the accept mimetypes
223
        return AddCSlashes($string, $special);
224
   }
225
226
    private static function isAcceptable($name, $acceptable)
227
    {
228
        $name = str_replace('*', '.*', $name);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
229
        $result = preg_grep('|'.self::pregEscape($name).'|', $acceptable);
230
        return current($result);
231
    }
232
233
    /**
234
     * Return the last value sent for a specific header, uses the output of parse().
235
     * @param (mixed) $headers An array with multiple header strings or a single string.
236
     * @return array|mixed
237
     */
238
    private static function getLastHeader($headers) {
239
        if ( is_array($headers) ) {
240
            return end($headers);
241
        }
242
        return $headers;
243
    }
244
245
246
}