Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.92% |
92 / 93 |
|
90.00% |
9 / 10 |
CRAP | |
0.00% |
0 / 1 |
Request | |
98.92% |
92 / 93 |
|
90.00% |
9 / 10 |
62 | |
0.00% |
0 / 1 |
getPasteId | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
__construct | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
23 | |||
getOperation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getData | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
getParam | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getHost | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
getRequestUri | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
isJsonApiCall | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setInputStream | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
_detectJsonRequest | |
96.97% |
32 / 33 |
|
0.00% |
0 / 1 |
19 |
1 | <?php |
2 | /** |
3 | * PrivateBin |
4 | * |
5 | * a zero-knowledge paste bin |
6 | * |
7 | * @link https://github.com/PrivateBin/PrivateBin |
8 | * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) |
9 | * @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License |
10 | * @version 1.7.2 |
11 | */ |
12 | |
13 | namespace PrivateBin; |
14 | |
15 | use Exception; |
16 | |
17 | /** |
18 | * Request |
19 | * |
20 | * parses request parameters and provides helper functions for routing |
21 | */ |
22 | class Request |
23 | { |
24 | /** |
25 | * MIME type for JSON |
26 | * |
27 | * @const string |
28 | */ |
29 | const MIME_JSON = 'application/json'; |
30 | |
31 | /** |
32 | * MIME type for HTML |
33 | * |
34 | * @const string |
35 | */ |
36 | const MIME_HTML = 'text/html'; |
37 | |
38 | /** |
39 | * MIME type for XHTML |
40 | * |
41 | * @const string |
42 | */ |
43 | const MIME_XHTML = 'application/xhtml+xml'; |
44 | |
45 | /** |
46 | * Input stream to use for PUT parameter parsing |
47 | * |
48 | * @access private |
49 | * @var string |
50 | */ |
51 | private static $_inputStream = 'php://input'; |
52 | |
53 | /** |
54 | * Operation to perform |
55 | * |
56 | * @access private |
57 | * @var string |
58 | */ |
59 | private $_operation = 'view'; |
60 | |
61 | /** |
62 | * Request parameters |
63 | * |
64 | * @access private |
65 | * @var array |
66 | */ |
67 | private $_params = array(); |
68 | |
69 | /** |
70 | * If we are in a JSON API context |
71 | * |
72 | * @access private |
73 | * @var bool |
74 | */ |
75 | private $_isJsonApi = false; |
76 | |
77 | /** |
78 | * Return the paste ID of the current paste. |
79 | * |
80 | * @access private |
81 | * @return string |
82 | */ |
83 | private function getPasteId() |
84 | { |
85 | foreach ($_GET as $key => $value) { |
86 | // only return if value is empty and key is 16 hex chars |
87 | if (($value === '') && strlen($key) === 16 && ctype_xdigit($key)) { |
88 | return $key; |
89 | } |
90 | } |
91 | |
92 | return 'invalid id'; |
93 | } |
94 | |
95 | /** |
96 | * Constructor |
97 | * |
98 | * @access public |
99 | */ |
100 | public function __construct() |
101 | { |
102 | // decide if we are in JSON API or HTML context |
103 | $this->_isJsonApi = $this->_detectJsonRequest(); |
104 | |
105 | // parse parameters, depending on request type |
106 | switch (array_key_exists('REQUEST_METHOD', $_SERVER) ? $_SERVER['REQUEST_METHOD'] : 'GET') { |
107 | case 'DELETE': |
108 | case 'PUT': |
109 | case 'POST': |
110 | // it might be a creation or a deletion, the latter is detected below |
111 | $this->_operation = 'create'; |
112 | try { |
113 | $this->_params = Json::decode( |
114 | file_get_contents(self::$_inputStream) |
115 | ); |
116 | } catch (Exception $e) { |
117 | // ignore error, $this->_params will remain empty |
118 | } |
119 | break; |
120 | default: |
121 | $this->_params = filter_var_array($_GET, array( |
122 | 'deletetoken' => FILTER_SANITIZE_SPECIAL_CHARS, |
123 | 'jsonld' => FILTER_SANITIZE_SPECIAL_CHARS, |
124 | 'link' => FILTER_SANITIZE_URL, |
125 | 'pasteid' => FILTER_SANITIZE_SPECIAL_CHARS, |
126 | 'shortenviayourls' => FILTER_SANITIZE_SPECIAL_CHARS, |
127 | ), false); |
128 | } |
129 | if ( |
130 | !array_key_exists('pasteid', $this->_params) && |
131 | !array_key_exists('jsonld', $this->_params) && |
132 | !array_key_exists('link', $this->_params) && |
133 | array_key_exists('QUERY_STRING', $_SERVER) && |
134 | !empty($_SERVER['QUERY_STRING']) |
135 | ) { |
136 | $this->_params['pasteid'] = $this->getPasteId(); |
137 | } |
138 | |
139 | // prepare operation, depending on current parameters |
140 | if (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) { |
141 | if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken'])) { |
142 | $this->_operation = 'delete'; |
143 | } elseif ($this->_operation != 'create') { |
144 | $this->_operation = 'read'; |
145 | } |
146 | } elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld'])) { |
147 | $this->_operation = 'jsonld'; |
148 | } elseif (array_key_exists('link', $this->_params) && !empty($this->_params['link'])) { |
149 | if (strpos($this->getRequestUri(), '/shortenviayourls') !== false || array_key_exists('shortenviayourls', $this->_params)) { |
150 | $this->_operation = 'yourlsproxy'; |
151 | } |
152 | } |
153 | } |
154 | |
155 | /** |
156 | * Get current operation |
157 | * |
158 | * @access public |
159 | * @return string |
160 | */ |
161 | public function getOperation() |
162 | { |
163 | return $this->_operation; |
164 | } |
165 | |
166 | /** |
167 | * Get data of paste or comment |
168 | * |
169 | * @access public |
170 | * @return array |
171 | */ |
172 | public function getData() |
173 | { |
174 | $data = array( |
175 | 'adata' => $this->getParam('adata'), |
176 | ); |
177 | $required_keys = array('v', 'ct'); |
178 | $meta = $this->getParam('meta'); |
179 | if (empty($meta)) { |
180 | $required_keys[] = 'pasteid'; |
181 | $required_keys[] = 'parentid'; |
182 | } else { |
183 | $data['meta'] = $meta; |
184 | } |
185 | foreach ($required_keys as $key) { |
186 | $data[$key] = $this->getParam($key, $key == 'v' ? 1 : ''); |
187 | } |
188 | // forcing a cast to int or float |
189 | $data['v'] = $data['v'] + 0; |
190 | return $data; |
191 | } |
192 | |
193 | /** |
194 | * Get a request parameter |
195 | * |
196 | * @access public |
197 | * @param string $param |
198 | * @param string $default |
199 | * @return string |
200 | */ |
201 | public function getParam($param, $default = '') |
202 | { |
203 | return array_key_exists($param, $this->_params) ? |
204 | $this->_params[$param] : $default; |
205 | } |
206 | |
207 | /** |
208 | * Get host as requested by the client |
209 | * |
210 | * @access public |
211 | * @return string |
212 | */ |
213 | public function getHost() |
214 | { |
215 | $host = array_key_exists('HTTP_HOST', $_SERVER) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : ''; |
216 | return empty($host) ? 'localhost' : $host; |
217 | } |
218 | |
219 | /** |
220 | * Get request URI |
221 | * |
222 | * @access public |
223 | * @return string |
224 | */ |
225 | public function getRequestUri() |
226 | { |
227 | $uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : ''; |
228 | return empty($uri) ? '/' : $uri; |
229 | } |
230 | |
231 | /** |
232 | * If we are in a JSON API context |
233 | * |
234 | * @access public |
235 | * @return bool |
236 | */ |
237 | public function isJsonApiCall() |
238 | { |
239 | return $this->_isJsonApi; |
240 | } |
241 | |
242 | /** |
243 | * Override the default input stream source, used for unit testing |
244 | * |
245 | * @param string $input |
246 | */ |
247 | public static function setInputStream($input) |
248 | { |
249 | self::$_inputStream = $input; |
250 | } |
251 | |
252 | /** |
253 | * Detect the clients supported media type and decide if its a JSON API call or not |
254 | * |
255 | * Adapted from: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447 |
256 | * |
257 | * @access private |
258 | * @return bool |
259 | */ |
260 | private function _detectJsonRequest() |
261 | { |
262 | $hasAcceptHeader = array_key_exists('HTTP_ACCEPT', $_SERVER); |
263 | $acceptHeader = $hasAcceptHeader ? $_SERVER['HTTP_ACCEPT'] : ''; |
264 | |
265 | // simple cases |
266 | if ( |
267 | (array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) && |
268 | $_SERVER['HTTP_X_REQUESTED_WITH'] == 'JSONHttpRequest') || |
269 | ($hasAcceptHeader && |
270 | strpos($acceptHeader, self::MIME_JSON) !== false && |
271 | strpos($acceptHeader, self::MIME_HTML) === false && |
272 | strpos($acceptHeader, self::MIME_XHTML) === false) |
273 | ) { |
274 | return true; |
275 | } |
276 | |
277 | // advanced case: media type negotiation |
278 | if ($hasAcceptHeader) { |
279 | $mediaTypes = array(); |
280 | foreach (explode(',', trim($acceptHeader)) as $mediaTypeRange) { |
281 | if (preg_match( |
282 | '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#', |
283 | trim($mediaTypeRange), $match |
284 | )) { |
285 | if (!isset($match[2])) { |
286 | $match[2] = '1.0'; |
287 | } else { |
288 | $match[2] = (string) floatval($match[2]); |
289 | if ($match[2] === '0.0') { |
290 | continue; |
291 | } |
292 | } |
293 | if (!isset($mediaTypes[$match[2]])) { |
294 | $mediaTypes[$match[2]] = array(); |
295 | } |
296 | $mediaTypes[$match[2]][] = strtolower($match[1]); |
297 | } |
298 | } |
299 | krsort($mediaTypes); |
300 | foreach ($mediaTypes as $acceptedQuality => $acceptedValues) { |
301 | foreach ($acceptedValues as $acceptedValue) { |
302 | if ( |
303 | strpos($acceptedValue, self::MIME_HTML) === 0 || |
304 | strpos($acceptedValue, self::MIME_XHTML) === 0 |
305 | ) { |
306 | return false; |
307 | } elseif (strpos($acceptedValue, self::MIME_JSON) === 0) { |
308 | return true; |
309 | } |
310 | } |
311 | } |
312 | } |
313 | return false; |
314 | } |
315 | } |