Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.92% covered (success)
98.92%
92 / 93
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
98.92% covered (success)
98.92%
92 / 93
90.00% covered (success)
90.00%
9 / 10
62
0.00% covered (danger)
0.00%
0 / 1
 getPasteId
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 __construct
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
23
 getOperation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getData
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 getParam
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getHost
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getRequestUri
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 isJsonApiCall
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInputStream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 _detectJsonRequest
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
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
13namespace PrivateBin;
14
15use Exception;
16
17/**
18 * Request
19 *
20 * parses request parameters and provides helper functions for routing
21 */
22class 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}