Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.91% |
91 / 92 |
|
90.00% |
9 / 10 |
CRAP | |
0.00% |
0 / 1 |
Request | |
98.91% |
91 / 92 |
|
90.00% |
9 / 10 |
58 | |
0.00% |
0 / 1 |
getPasteId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
__construct | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
22 | |||
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% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getRequestUri | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isJsonApiCall | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setInputStream | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
_detectJsonRequest | |
97.06% |
33 / 34 |
|
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.0 |
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 | // RegEx to check for valid paste ID (16 base64 chars) |
86 | $pasteIdRegEx = '/^[a-f0-9]{16}$/'; |
87 | |
88 | foreach ($_GET as $key => $value) { |
89 | // only return if value is empty and key matches RegEx |
90 | if (($value === '') and preg_match($pasteIdRegEx, $key, $match)) { |
91 | return $match[0]; |
92 | } |
93 | } |
94 | |
95 | return 'invalid id'; |
96 | } |
97 | |
98 | /** |
99 | * Constructor |
100 | * |
101 | * @access public |
102 | */ |
103 | public function __construct() |
104 | { |
105 | // decide if we are in JSON API or HTML context |
106 | $this->_isJsonApi = $this->_detectJsonRequest(); |
107 | |
108 | // parse parameters, depending on request type |
109 | switch (array_key_exists('REQUEST_METHOD', $_SERVER) ? $_SERVER['REQUEST_METHOD'] : 'GET') { |
110 | case 'DELETE': |
111 | case 'PUT': |
112 | case 'POST': |
113 | // it might be a creation or a deletion, the latter is detected below |
114 | $this->_operation = 'create'; |
115 | try { |
116 | $this->_params = Json::decode( |
117 | file_get_contents(self::$_inputStream) |
118 | ); |
119 | } catch (Exception $e) { |
120 | // ignore error, $this->_params will remain empty |
121 | } |
122 | break; |
123 | default: |
124 | $this->_params = $_GET; |
125 | } |
126 | if ( |
127 | !array_key_exists('pasteid', $this->_params) && |
128 | !array_key_exists('jsonld', $this->_params) && |
129 | !array_key_exists('link', $this->_params) && |
130 | array_key_exists('QUERY_STRING', $_SERVER) && |
131 | !empty($_SERVER['QUERY_STRING']) |
132 | ) { |
133 | $this->_params['pasteid'] = $this->getPasteId(); |
134 | } |
135 | |
136 | // prepare operation, depending on current parameters |
137 | if (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) { |
138 | if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken'])) { |
139 | $this->_operation = 'delete'; |
140 | } elseif ($this->_operation != 'create') { |
141 | $this->_operation = 'read'; |
142 | } |
143 | } elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld'])) { |
144 | $this->_operation = 'jsonld'; |
145 | } elseif (array_key_exists('link', $this->_params) && !empty($this->_params['link'])) { |
146 | if (strpos($this->getRequestUri(), '/shortenviayourls') !== false) { |
147 | $this->_operation = 'yourlsproxy'; |
148 | } |
149 | } |
150 | } |
151 | |
152 | /** |
153 | * Get current operation |
154 | * |
155 | * @access public |
156 | * @return string |
157 | */ |
158 | public function getOperation() |
159 | { |
160 | return $this->_operation; |
161 | } |
162 | |
163 | /** |
164 | * Get data of paste or comment |
165 | * |
166 | * @access public |
167 | * @return array |
168 | */ |
169 | public function getData() |
170 | { |
171 | $data = array( |
172 | 'adata' => $this->getParam('adata'), |
173 | ); |
174 | $required_keys = array('v', 'ct'); |
175 | $meta = $this->getParam('meta'); |
176 | if (empty($meta)) { |
177 | $required_keys[] = 'pasteid'; |
178 | $required_keys[] = 'parentid'; |
179 | } else { |
180 | $data['meta'] = $meta; |
181 | } |
182 | foreach ($required_keys as $key) { |
183 | $data[$key] = $this->getParam($key, $key == 'v' ? 1 : ''); |
184 | } |
185 | // forcing a cast to int or float |
186 | $data['v'] = $data['v'] + 0; |
187 | return $data; |
188 | } |
189 | |
190 | /** |
191 | * Get a request parameter |
192 | * |
193 | * @access public |
194 | * @param string $param |
195 | * @param string $default |
196 | * @return string |
197 | */ |
198 | public function getParam($param, $default = '') |
199 | { |
200 | return array_key_exists($param, $this->_params) ? |
201 | $this->_params[$param] : $default; |
202 | } |
203 | |
204 | /** |
205 | * Get host as requested by the client |
206 | * |
207 | * @access public |
208 | * @return string |
209 | */ |
210 | public function getHost() |
211 | { |
212 | return array_key_exists('HTTP_HOST', $_SERVER) ? |
213 | htmlspecialchars($_SERVER['HTTP_HOST']) : |
214 | 'localhost'; |
215 | } |
216 | |
217 | /** |
218 | * Get request URI |
219 | * |
220 | * @access public |
221 | * @return string |
222 | */ |
223 | public function getRequestUri() |
224 | { |
225 | return array_key_exists('REQUEST_URI', $_SERVER) ? |
226 | htmlspecialchars( |
227 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) |
228 | ) : '/'; |
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 | $mediaTypes = array(); |
279 | if ($hasAcceptHeader) { |
280 | $mediaTypeRanges = explode(',', trim($acceptHeader)); |
281 | foreach ($mediaTypeRanges as $mediaTypeRange) { |
282 | if (preg_match( |
283 | '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#', |
284 | trim($mediaTypeRange), $match |
285 | )) { |
286 | if (!isset($match[2])) { |
287 | $match[2] = '1.0'; |
288 | } else { |
289 | $match[2] = (string) floatval($match[2]); |
290 | } |
291 | if (!isset($mediaTypes[$match[2]])) { |
292 | $mediaTypes[$match[2]] = array(); |
293 | } |
294 | $mediaTypes[$match[2]][] = strtolower($match[1]); |
295 | } |
296 | } |
297 | krsort($mediaTypes); |
298 | foreach ($mediaTypes as $acceptedQuality => $acceptedValues) { |
299 | if ($acceptedQuality === '0.0') { |
300 | continue; |
301 | } |
302 | foreach ($acceptedValues as $acceptedValue) { |
303 | if ( |
304 | strpos($acceptedValue, self::MIME_HTML) === 0 || |
305 | strpos($acceptedValue, self::MIME_XHTML) === 0 |
306 | ) { |
307 | return false; |
308 | } elseif (strpos($acceptedValue, self::MIME_JSON) === 0) { |
309 | return true; |
310 | } |
311 | } |
312 | } |
313 | } |
314 | return false; |
315 | } |
316 | } |