Completed
Pull Request — master (#83)
by
unknown
11:18
created

Router::isURIClean()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
ccs 8
cts 8
cp 1
rs 9.2
cc 4
eloc 10
nc 2
nop 2
crap 4
1
<?php
2
namespace Zewa;
3
4
use Zewa\Exception\RouteException;
5
6
/**
7
 * Handles everything relating to URL/URI.
8
 *
9
 * @author Zechariah Walden<zech @ zewadesign.com>
10
 */
11
class Router
12
{
13
    /**
14
     * System routes
15
     *
16
     * @var object
17
     */
18
    private $routes;
19
20
    /**
21
     * The active module
22
     *
23
     * @var    string
24
     * @access public
25
     */
26
    public $module;
27
28
    /**
29
     * The active controller
30
     *
31
     * @var    string
32
     * @access public
33
     */
34
    public $controller;
35
36
    /**
37
     * The active method
38
     *
39
     * @var    string
40
     * @access public
41
     */
42
    public $method;
43
44
    /**
45
     * The base URL
46
     *
47
     * @var    string
48
     * @access public
49
     */
50
    public $baseURL;
51
52
    /**
53
     * Default module
54
     *
55
     * @var    string
56
     * @access public
57
     */
58
    public $defaultModule;
59
60
    /**
61
     * Default controller
62
     *
63
     * @var    string
64
     * @access public
65
     */
66
    public $defaultController;
67
68
    /**
69
     * Default method
70
     *
71
     * @var    string
72
     * @access public
73
     */
74
    public $defaultMethod;
75
76
    /**
77
     * Default uri
78
     *
79
     * @var    string
80
     * @access public
81
     */
82
    public $uri;
83
84
    /**
85
     * @var Config
86
     */
87
    public $config;
88
    /**
89
     * Load up some basic configuration settings.
90
     */
91 28
    public function __construct(Config $config)
92
    {
93 28
        $this->modules = $config->get('Modules');
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
94 28
        $this->routes = $config->get('Routes');
95
96 28
        $this->prepare();
97
        //@TODO: routing
98 28
        $uriChunks = $this->parseURI($this->uri);
99
100 18
        $params = array_slice($uriChunks, 3);
101
102
        // clear ending / with no value..
103 18
        if (!empty($params) && $params[0] === '') {
104
            $params = [];
105
        }
106
107 18
        $config->set('Routing', (object)[
108 18
            'module' => $uriChunks[0],
109 18
            'controller' => $uriChunks[1],
110 18
            'method' => $uriChunks[2],
111 18
            'params' => $params,
112 18
            'baseURL' => $this->baseURL,
113 18
            'currentURL' => $this->currentURL
114
        ]);
115 18
        $this->config = $config;
116 18
    }
117
118
    public function getConfig()
119
    {
120
        return $this->config;
121
    }
122
123
    /**
124
     * Set class defaults and normalized url/uri segments
125
     */
126 28
    private function prepare()
127
    {
128 28
        $this->defaultModule = $this->modules['defaultModule'];
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
129 28
        $defaultModule = $this->defaultModule;
130 28
        $this->defaultController = $this->modules[$defaultModule]['defaultController'];
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
131 28
        $this->defaultMethod = $this->modules[$defaultModule]['defaultMethod'];
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
132
133 28
        $normalizedURI = $this->normalizeURI();
134
        //check routes
135 28
        $this->uri = $this->uri($normalizedURI);
136 28
        $this->baseURL = $this->baseURL();
137 28
        $this->currentURL = $this->currentURL();
0 ignored issues
show
Bug introduced by
The property currentURL does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
138 28
    }
139
140
    /**
141
     * Checks if URL contains special characters not permissable/considered dangerous
142
     *
143
     * Safe: a-z, 0-9, :, _, [, ], +
144
     *
145
     * @param $uri
146
     * @param $uriChunks
147
     * @return bool
148
     */
149 28
    private function isURIClean($uri, $uriChunks)
150
    {
151 28
        if (!preg_match("/^[a-z0-9:_\/\.\[\]-]+$/i", $uri)
152 19
            || array_filter(
153
                $uriChunks,
154
                function ($uriChunk) {
155 19
                    if (strpos($uriChunk, '__') !== false) {
156 1
                        return true;
157
                    }
158 28
                }
159
            )
160
        ) {
161 10
            return false;
162
        } else {
163 18
            return true;
164
        }
165
    }
166
167
    //@TODO add Security class.
168 22
    private function normalize($data)
169
    {
170 22
        if (is_numeric($data)) {
171 11
            if (is_int($data) || ctype_digit(trim($data, '-'))) {
172 3
                $data = (int)$data;
173 8
            } elseif ($data === (string)(float)$data) {
174
                //@TODO: this needs work.. 9E26 converts to float
175 4
                $data = (float)$data;
176
            }
177
        }
178 22
        return $data;
179
    }
180
181
    /**
182
     * Parse and explode URI segments into chunks
183
     *
184
     * @access private
185
     *
186
     * @param string $uri
187
     *
188
     * @return array chunks of uri
189
     * @throws RouteException on disallowed characters
190
     */
191 28
    private function parseURI($uri)
192
    {
193 28
        $uriFragments = explode('/', $uri);
194 28
        $uriChunks = [];
195 28
        $params = [];
196 28
        $iteration = 0;
197 28
        foreach ($uriFragments as $location => $fragment) {
198 28
            if ($iteration > 2) {
199 22
                $params[] = $this->normalize(trim($fragment));
200
            } else {
201 28
                $uriChunks[] = trim($fragment);
202
            }
203 28
            $iteration++;
204
        }
205
206 28
        $result = array_merge($uriChunks, $params);
207
208 28
        if ($this->isURIClean($uri, $result) === false) {
209 10
            throw new RouteException('Invalid key characters.');
210
        }
211
212 18
        return $result;
213
    }
214
215
    /**
216
     * Normalize the $_SERVER vars for formatting the URI.
217
     *
218
     * @access private
219
     * @return string formatted/u/r/l
220
     */
221 28
    private function normalizeURI()
0 ignored issues
show
Coding Style introduced by
normalizeURI uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
222
    {
223 28
        if (!empty($_SERVER['PATH_INFO'])) {
224
            $normalizedURI = $_SERVER['PATH_INFO'];
225 28
        } elseif (!empty($_SERVER['REQUEST_URI'])) {
226 25
            $normalizedURI = $_SERVER['REQUEST_URI'];
227
        } else {
228 3
            $normalizedURI = false;
229
        }
230
231 28
        if ($normalizedURI === '/') {
232 1
            $normalizedURI = false;
233
        }
234
235 28
        $normalizedURI = ltrim(preg_replace('/\?.*/', '', $normalizedURI), '/');
236
237 28
        if (! empty($this->routes)) {
238 28
            $normalizedURI = $this->discoverRoute($normalizedURI);
239
        }
240
241 28
        return $normalizedURI;
242
    }
243
244 28
    private function discoverRoute($uri)
245
    {
246 28
        $routes = $this->routes;
247
248 28
        foreach ($routes as $route => $reroute) {
249 28
            $pattern = '/^(?i)' . str_replace('/', '\/', $route) . '$/';
250 28
            if (preg_match($pattern, $uri, $params)) {
251
                array_shift($params);
252
253
                $uri = $reroute;
254
255
                if (! empty($params)) {
256
                    $pat = '/(\$\d+)/';
257
                    $uri = preg_replace_callback(
258
                        $pat,
259 28
                        function () use (&$params) {
260
                            $first = $params[0];
261
                            array_shift($params);
262
                            return $first;
263 28
                        },
264
                        $reroute
265
                    );
266
                }
267
            }
268
        }
269
270 28
        return $uri;
271
    }
272
273
    /**
274
     * Normalize the $_SERVER vars for formatting the URI.
275
     *
276
     * @param $uri
277
     * @access public
278
     * @return string formatted/u/r/l
279
     */
280 28
    private function uri($uri)
281
    {
282
283 28
        if ($uri !== '') {
284 24
            $uriChunks = explode('/', filter_var(trim($uri), FILTER_SANITIZE_URL));
285 24
            $chunks = $this->sortURISegments($uriChunks);
286
        } else {
287 4
            $chunks = $this->sortURISegments();
288
        }
289
290 28
        $uri = ltrim(implode('/', $chunks), '/');
291 28
        return $uri;
292
    }
293
294 28
    private function sortURISegments($uriChunks = [])
295
    {
296 28
        $module = ucfirst(strtolower($this->defaultModule));
297 28
        $controller = ucfirst(strtolower($this->defaultController));
298 28
        $method = ucfirst(strtolower($this->defaultMethod));
299
300 28
        if (!empty($uriChunks)) {
301 24
            $module = ucfirst(strtolower($uriChunks[0]));
302
303 24
            if (!empty($uriChunks[1])) {
304 22
                $controller = ucfirst(strtolower($uriChunks[1]));
305 2
            } elseif (!empty($this->modules->$module->defaultController)) {
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
306
                $controller = $this->modules->$module->defaultController;
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
307
            }
308
309 24
            if (!empty($uriChunks[2])) {
310 22
                $method = ucfirst(strtolower($uriChunks[2]));
311 22
                $class = '\\App\\Modules\\' . $module . '\\Controllers\\' . $controller;
312 22
                $methodExist = method_exists($class, $method);
313
                
314 22
                if ($methodExist === false) {
315 22
                    if (!empty($this->modules->$module->defaultMethod)) {
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
316
                        $method = $this->modules->$module->defaultMethod;
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
317 22
                        array_unshift($uriChunks, null);
318
                    }
319
                }
320 2
            } elseif (!empty($this->modules->$module->defaultMethod)) {
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
321
                $method = $this->modules->$module->defaultMethod;
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
322
            }
323
324 24
            unset($uriChunks[0], $uriChunks[1], $uriChunks[2]);
325
        }
326
327 28
        $return = [$module, $controller, $method];
328 28
        return array_merge($return, array_values($uriChunks));
329
    }
330
331 2
    private function addQueryString($url, $key, $value)
332
    {
333 2
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
334 2
        $url = substr($url, 0, -1);
335 2
        if (strpos($url, '?') === false) {
336 2
            return ($url . '?' . $key . '=' . $value);
337
        } else {
338 1
            return ($url . '&' . $key . '=' . $value);
339
        }
340
    }
341
342 2
    private function removeQueryString($url, $key)
343
    {
344 2
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
345 2
        $url = substr($url, 0, -1);
346 2
        return ($url);
347
    }
348
349
    /**
350
     * Return the currentURL w/ query strings
351
     *
352
     * @access public
353
     * @return string http://tld.com/formatted/u/r/l?q=bingo
354
     */
355 28
    public function currentURL($params = false)
0 ignored issues
show
Coding Style introduced by
currentURL uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
356
    {
357 28
        if (trim($_SERVER['REQUEST_URI']) === '/') {
358 1
            $url = $this->baseURL()
359 1
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
360
        } else {
361 27
            $url = $this->baseURL($this->uri)
362 27
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
363
        }
364
365 28
        if (!empty($params)) {
366 2
            foreach ($params as $key => $param) {
0 ignored issues
show
Bug introduced by
The expression $params of type boolean is not traversable.
Loading history...
367 2
                $url = $this->removeQueryString($url, $key);
368 2
                $url = $this->addQueryString($url, $key, $param);
369
            }
370
        }
371
372 28
        return $url;
373
    }
374
375
    /**
376
     * Return the baseURL
377
     *
378
     * @access public
379
     * @return string http://tld.com
380
     */
381 28
    public function baseURL($path = '')
0 ignored issues
show
Coding Style introduced by
baseURL uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
382
    {
383 28
        if (is_null($this->baseURL)) {
384 28
            $self = $_SERVER['PHP_SELF'];
385 28
            $server = $_SERVER['HTTP_HOST']
386 28
                      . rtrim(str_replace(strstr($self, 'index.php'), '', $self), '/');
387
388 28
            if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
389 27
                || !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
390 28
                && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
391
            ) {
392 1
                $protocol = 'https://';
393
            } else {
394 27
                $protocol = 'http://';
395
            }
396
397 28
            $this->baseURL = $protocol . $server;
398
        }
399
400 28
        $url = $this->baseURL;
401
402 28
        if ($path !== '') {
403 27
            $url .= '/' . $path;
404
        }
405
406 28
        return $url;
407
    }
408
409
    /**
410
     * Set optional status header, and redirect to provided URL
411
     *
412
     * @access public
413
     * @return bool
414
     */
415
    public function redirect($url = '/', $status = null)
416
    {
417
        $url = str_replace(array('\r', '\n', '%0d', '%0a'), '', $url);
418
419
        if (headers_sent()) {
420
            return false;
421
        }
422
423
        // trap session vars before redirect
424
        session_write_close();
425
426
        if (is_null($status)) {
427
            $status = '302';
428
        }
429
430
        // push a status to the browser if necessary
431
        if ((int)$status > 0) {
432
            switch ($status) {
433
                case '301':
434
                    $msg = '301 Moved Permanently';
435
                    break;
436
                case '307':
437
                    $msg = '307 Temporary Redirect';
438
                    break;
439
                // Using these below (except 302) would be an intentional misuse of the 'system'
440
                // Need to dig into the above comment @zech
441
                case '401':
442
                    $msg = '401 Access Denied';
443
                    break;
444
                case '403':
445
                    $msg = '403 Request Forbidden';
446
                    break;
447
                case '404':
448
                    $msg = '404 Not Found';
449
                    break;
450
                case '405':
451
                    $msg = '405 Method Not Allowed';
452
                    break;
453
                case '302':
454
                default:
455
                    $msg = '302 Found';
456
                    break; // temp redirect
457
            }
458
            if (isset($msg)) {
459
                header('HTTP/1.1 ' . $msg);
460
            }
461
        }
462
463
        $url = preg_replace('!^/*!', '', $url);
464
        header("Location: " . $url);
465
    }
466
}
467