Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.91% covered (success)
98.91%
91 / 92
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
98.91% covered (success)
98.91%
91 / 92
90.00% covered (success)
90.00%
9 / 10
58
0.00% covered (danger)
0.00%
0 / 1
 getPasteId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 __construct
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
24
 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%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
16
1<?php declare(strict_types=1);
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 */
11
12namespace PrivateBin;
13
14use PrivateBin\Exception\JsonException;
15use PrivateBin\Model\Paste;
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 document.
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            $key = (string) $key;
88            if (empty($value) && Paste::isValidId($key)) {
89                return $key;
90            }
91        }
92
93        return 'invalid id';
94    }
95
96    /**
97     * Constructor
98     *
99     * @access public
100     */
101    public function __construct()
102    {
103        // decide if we are in JSON API or HTML context
104        $this->_isJsonApi = $this->_detectJsonRequest();
105
106        // parse parameters, depending on request type
107        switch ($_SERVER['REQUEST_METHOD'] ?? 'GET') {
108            case 'DELETE':
109            case 'PUT':
110            case 'POST':
111                // it might be a creation or a deletion, the latter is detected below
112                $this->_operation = 'create';
113                try {
114                    $data          = file_get_contents(self::$_inputStream);
115                    $this->_params = Json::decode($data);
116                } catch (JsonException $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                    'shortenviashlink' => FILTER_SANITIZE_SPECIAL_CHARS,
128                ), false);
129        }
130        if (
131            !array_key_exists('pasteid', $this->_params) &&
132            !array_key_exists('jsonld', $this->_params) &&
133            !array_key_exists('link', $this->_params) &&
134            array_key_exists('QUERY_STRING', $_SERVER) &&
135            !empty($_SERVER['QUERY_STRING'])
136        ) {
137            $this->_params['pasteid'] = $this->getPasteId();
138        }
139
140        // prepare operation, depending on current parameters
141        if (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid'])) {
142            if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken'])) {
143                $this->_operation = 'delete';
144            } elseif ($this->_operation !== 'create') {
145                $this->_operation = 'read';
146            }
147        } elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld'])) {
148            $this->_operation = 'jsonld';
149        } elseif (array_key_exists('link', $this->_params) && !empty($this->_params['link'])) {
150            if (str_contains($this->getRequestUri(), '/shortenviayourls') || array_key_exists('shortenviayourls', $this->_params)) {
151                $this->_operation = 'yourlsproxy';
152            }
153            if (str_contains($this->getRequestUri(), '/shortenviashlink') || array_key_exists('shortenviashlink', $this->_params)) {
154                $this->_operation = 'shlinkproxy';
155            }
156        }
157    }
158
159    /**
160     * Get current operation
161     *
162     * @access public
163     * @return string
164     */
165    public function getOperation()
166    {
167        return $this->_operation;
168    }
169
170    /**
171     * Get data of paste or comment
172     *
173     * @access public
174     * @return array
175     */
176    public function getData()
177    {
178        $data = array(
179            'adata' => $this->getParam('adata'),
180        );
181        $required_keys = array('v', 'ct');
182        $meta          = $this->getParam('meta');
183        if (empty($meta)) {
184            $required_keys[] = 'pasteid';
185            $required_keys[] = 'parentid';
186        } else {
187            $data['meta'] = $meta;
188        }
189        foreach ($required_keys as $key) {
190            $data[$key] = $this->getParam($key, $key === 'v' ? 1 : '');
191        }
192        // forcing a cast to int or float
193        $data['v'] = $data['v'] + 0;
194        return $data;
195    }
196
197    /**
198     * Get a request parameter
199     *
200     * @access public
201     * @param  string $param
202     * @param  string $default
203     * @return string
204     */
205    public function getParam($param, $default = '')
206    {
207        return $this->_params[$param] ?? $default;
208    }
209
210    /**
211     * Get host as requested by the client
212     *
213     * @access public
214     * @return string
215     */
216    public function getHost()
217    {
218        $host = array_key_exists('HTTP_HOST', $_SERVER) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : '';
219        return empty($host) ? 'localhost' : $host;
220    }
221
222    /**
223     * Get request URI
224     *
225     * @access public
226     * @return string
227     */
228    public function getRequestUri()
229    {
230        $uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : '';
231        return empty($uri) ? '/' : $uri;
232    }
233
234    /**
235     * If we are in a JSON API context
236     *
237     * @access public
238     * @return bool
239     */
240    public function isJsonApiCall()
241    {
242        return $this->_isJsonApi;
243    }
244
245    /**
246     * Override the default input stream source, used for unit testing
247     *
248     * @param string $input
249     */
250    public static function setInputStream($input)
251    {
252        self::$_inputStream = $input;
253    }
254
255    /**
256     * Detect the clients supported media type and decide if its a JSON API call or not
257     *
258     * Adapted from: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
259     *
260     * @access private
261     * @return bool
262     */
263    private function _detectJsonRequest()
264    {
265        $acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
266
267        // simple cases
268        if (
269            ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'JSONHttpRequest' ||
270            (
271                str_contains($acceptHeader, self::MIME_JSON) &&
272                !str_contains($acceptHeader, self::MIME_HTML) &&
273                !str_contains($acceptHeader, self::MIME_XHTML)
274            )
275        ) {
276            return true;
277        }
278
279        // advanced case: media type negotiation
280        if (!empty($acceptHeader)) {
281            $mediaTypes = array();
282            foreach (explode(',', trim($acceptHeader)) as $mediaTypeRange) {
283                if (preg_match(
284                    '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#',
285                    trim($mediaTypeRange), $match
286                )) {
287                    if (!isset($match[2])) {
288                        $match[2] = '1.0';
289                    } else {
290                        $match[2] = (string) floatval($match[2]);
291                        if ($match[2] === '0.0') {
292                            continue;
293                        }
294                    }
295                    if (!isset($mediaTypes[$match[2]])) {
296                        $mediaTypes[$match[2]] = array();
297                    }
298                    $mediaTypes[$match[2]][] = strtolower($match[1]);
299                }
300            }
301            krsort($mediaTypes);
302            foreach ($mediaTypes as $acceptedQuality => $acceptedValues) {
303                foreach ($acceptedValues as $acceptedValue) {
304                    if (
305                        str_starts_with($acceptedValue, self::MIME_HTML) ||
306                        str_starts_with($acceptedValue, self::MIME_XHTML)
307                    ) {
308                        return false;
309                    } elseif (str_starts_with($acceptedValue, self::MIME_JSON)) {
310                        return true;
311                    }
312                }
313            }
314        }
315        return false;
316    }
317}