Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.96% covered (success)
98.96%
95 / 96
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Request
98.96% covered (success)
98.96%
95 / 96
90.00% covered (success)
90.00%
9 / 10
63
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
25
 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 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 Exception;
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 (array_key_exists('REQUEST_METHOD', $_SERVER) ? $_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 (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                    '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 array_key_exists($param, $this->_params) ?
208            $this->_params[$param] : $default;
209    }
210
211    /**
212     * Get host as requested by the client
213     *
214     * @access public
215     * @return string
216     */
217    public function getHost()
218    {
219        $host = array_key_exists('HTTP_HOST', $_SERVER) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : '';
220        return empty($host) ? 'localhost' : $host;
221    }
222
223    /**
224     * Get request URI
225     *
226     * @access public
227     * @return string
228     */
229    public function getRequestUri()
230    {
231        $uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : '';
232        return empty($uri) ? '/' : $uri;
233    }
234
235    /**
236     * If we are in a JSON API context
237     *
238     * @access public
239     * @return bool
240     */
241    public function isJsonApiCall()
242    {
243        return $this->_isJsonApi;
244    }
245
246    /**
247     * Override the default input stream source, used for unit testing
248     *
249     * @param string $input
250     */
251    public static function setInputStream($input)
252    {
253        self::$_inputStream = $input;
254    }
255
256    /**
257     * Detect the clients supported media type and decide if its a JSON API call or not
258     *
259     * Adapted from: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
260     *
261     * @access private
262     * @return bool
263     */
264    private function _detectJsonRequest()
265    {
266        $hasAcceptHeader = array_key_exists('HTTP_ACCEPT', $_SERVER);
267        $acceptHeader    = $hasAcceptHeader ? $_SERVER['HTTP_ACCEPT'] : '';
268
269        // simple cases
270        if (
271            (array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) &&
272                $_SERVER['HTTP_X_REQUESTED_WITH'] == 'JSONHttpRequest') ||
273            ($hasAcceptHeader &&
274                str_contains($acceptHeader, self::MIME_JSON) &&
275                !str_contains($acceptHeader, self::MIME_HTML) &&
276                !str_contains($acceptHeader, self::MIME_XHTML))
277        ) {
278            return true;
279        }
280
281        // advanced case: media type negotiation
282        if ($hasAcceptHeader) {
283            $mediaTypes = array();
284            foreach (explode(',', trim($acceptHeader)) as $mediaTypeRange) {
285                if (preg_match(
286                    '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#',
287                    trim($mediaTypeRange), $match
288                )) {
289                    if (!isset($match[2])) {
290                        $match[2] = '1.0';
291                    } else {
292                        $match[2] = (string) floatval($match[2]);
293                        if ($match[2] === '0.0') {
294                            continue;
295                        }
296                    }
297                    if (!isset($mediaTypes[$match[2]])) {
298                        $mediaTypes[$match[2]] = array();
299                    }
300                    $mediaTypes[$match[2]][] = strtolower($match[1]);
301                }
302            }
303            krsort($mediaTypes);
304            foreach ($mediaTypes as $acceptedQuality => $acceptedValues) {
305                foreach ($acceptedValues as $acceptedValue) {
306                    if (
307                        str_starts_with($acceptedValue, self::MIME_HTML) ||
308                        str_starts_with($acceptedValue, self::MIME_XHTML)
309                    ) {
310                        return false;
311                    } elseif (str_starts_with($acceptedValue, self::MIME_JSON)) {
312                        return true;
313                    }
314                }
315            }
316        }
317        return false;
318    }
319}