Completed
Push — master ( 58c9b2...61e000 )
by
unknown
26s
created

Router::parseURI()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 23
ccs 16
cts 16
cp 1
rs 8.7972
cc 4
eloc 15
nc 6
nop 1
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
     * Reference to instantiated controller object.
15
     *
16
     * @var object
17
     */
18
    protected static $instance = false;
19
20
    /**
21
     * System configuration
22
     *
23
     * @var object
24
     */
25
    private $configuration;
26
27
    /**
28
     * The active module
29
     *
30
     * @var    string
31
     * @access public
32
     */
33
    public $module;
34
35
    /**
36
     * The active controller
37
     *
38
     * @var    string
39
     * @access public
40
     */
41
    public $controller;
42
43
    /**
44
     * The active method
45
     *
46
     * @var    string
47
     * @access public
48
     */
49
    public $method;
50
51
    /**
52
     * The base URL
53
     *
54
     * @var    string
55
     * @access public
56
     */
57
    public $baseURL;
58
59
    /**
60
     * Default module
61
     *
62
     * @var    string
63
     * @access public
64
     */
65
    public $defaultModule;
66
67
    /**
68
     * Default controller
69
     *
70
     * @var    string
71
     * @access public
72
     */
73
    public $defaultController;
74
75
    /**
76
     * Default method
77
     *
78
     * @var    string
79
     * @access public
80
     */
81
    public $defaultMethod;
82
83
    /**
84
     * Default uri
85
     *
86
     * @var    string
87
     * @access public
88
     */
89
    public $uri;
90
    /**
91
     * Load up some basic configuration settings.
92
     */
93 28
    public function __construct()
94
    {
95 28
        self::$instance = $this;
96
97 28
        $app = App::getInstance();
98 28
        $moduleConfig = $app->getConfiguration('modules');
99
100 28
        $this->defaultModule = $moduleConfig->defaultModule;
101 28
        $defaultModule = $this->defaultModule;
102 28
        $this->defaultController = $moduleConfig->$defaultModule->defaultController;
103 28
        $this->defaultMethod = $moduleConfig->$defaultModule->defaultMethod;
104
105 28
        $normalizedURI = $this->normalizeURI();
106
107
        //check routes
108
109
110 28
        $this->uri = $this->uri($normalizedURI);
111 28
        $this->baseURL = $this->baseURL();
112 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...
113
114
        //@TODO: routing
115 28
        $uriChunks = $this->parseURI($this->uri);
116
117 18
        $app = App::getInstance();
118
119 18
        $app->setConfiguration(
120 18
            'router',
121
            (object)[
122 18
            'module' => $uriChunks[0],
123 18
            'controller' => $uriChunks[1],
124 18
            'method' => $uriChunks[2],
125 18
            'params' => array_slice($uriChunks, 3),
126 18
            'baseURL' => $this->baseURL,
127 18
            'currentURL' => $this->currentURL
128 18
            ]
129 18
        );
130 18
    }
131
132
133 28
    private function isURIClean($uri, $uriChunks)
134
    {
135 28
        if (!preg_match("/^[a-z0-9:_\/\.\[\]-]+$/i", $uri)
136 28
            || array_filter(
137 19
                $uriChunks,
138
                function ($uriChunk) {
139 19
                    if (strpos($uriChunk, '__') !== false) {
140 1
                        return true;
141
                    }
142 19
                }
143 19
            )
144 28
        ) {
145 10
            return false;
146
        } else {
147 18
            return true;
148
        }
149
    }
150
151
    //@TODO add Security class.
152 22
    private function normalize($data)
153
    {
154 22
        if (is_numeric($data)) {
155 12
            if (is_int($data) || ctype_digit(trim($data, '-'))) {
156 3
                $data = (int)$data;
157 12
            } elseif ($data === (string)(float)$data) {
158
                //@TODO: this needs work.. 9E26 converts to float
159 4
                $data = (float)$data;
160 4
            }
161 12
        }
162 22
        return $data;
163
    }
164
165
    /**
166
     * Parse and explode URI segments into chunks
167
     *
168
     * @access private
169
     *
170
     * @param string $uri
171
     *
172
     * @return array chunks of uri
173
     * @throws RouteException on disallowed characters
174
     */
175 28
    private function parseURI($uri)
176
    {
177 28
        $uriFragments = explode('/', $uri);
178 28
        $uriChunks = [];
179 28
        $params = [];
180 28
        $iteration = 0;
181 28
        foreach ($uriFragments as $location => $fragment) {
182 28
            if ($iteration > 2) {
183 22
                $params[] = $this->normalize(trim($fragment));
184 22
            } else {
185 28
                $uriChunks[] = trim($fragment);
186
            }
187 28
            $iteration++;
188 28
        }
189
190 28
        $result = array_merge($uriChunks, $params);
191
192 28
        if ($this->isURIClean($uri, $result) === false) {
193 10
            throw new RouteException('Invalid key characters.');
194
        }
195
196 18
        return $result;
197
    }
198
199
    /**
200
     * Normalize the $_SERVER vars for formatting the URI.
201
     *
202
     * @access private
203
     * @return string formatted/u/r/l
204
     */
205 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...
206
    {
207 28
        if (!empty($_SERVER['PATH_INFO'])) {
208
            $normalizedURI = $_SERVER['PATH_INFO'];
209 28
        } elseif (!empty($_SERVER['REQUEST_URI'])) {
210 26
            $normalizedURI = $_SERVER['REQUEST_URI'];
211 26
        } else {
212 4
            $normalizedURI = false;
213
        }
214
215 28
        if ($normalizedURI === '/') {
216 2
            $normalizedURI = false;
217 2
        }
218
219 28
        $normalizedURI = ltrim(preg_replace('/\?.*/', '', $normalizedURI), '/');
220
221 28
        if (! empty($this->configuration->routes)) {
222
            $normalizedURI = $this->discoverRoute($normalizedURI);
223
        }
224
225 28
        return $normalizedURI;
226
    }
227
228
    private function discoverRoute($uri)
229
    {
230
        $routes = $this->configuration->routes;
231
232
        foreach ($routes as $route => $reroute) {
233
            $pattern = '/^(?i)' . str_replace('/', '\/', $route) . '$/';
234
235
            if (preg_match($pattern, $uri, $params)) {
236
                array_shift($params);
237
238
                $uri = $reroute;
239
240
                if (! empty($params)) {
241
                    $pat = '/(\$\d+)/';
242
                    $uri = preg_replace_callback(
243
                        $pat,
244
                        function () use (&$params) {
245
                            $first = $params[0];
246
                            array_shift($params);
247
                            return $first;
248
                        },
249
                        $reroute
250
                    );
251
                }
252
            }
253
        }
254
255
        return $uri;
256
    }
257
258
    /**
259
     * Normalize the $_SERVER vars for formatting the URI.
260
     *
261
     * @access public
262
     * @return string formatted/u/r/l
263
     */
264 28
    private function uri($uri)
265
    {
266 28
        if ($uri !== '') {
267 24
            $uriChunks = explode('/', filter_var(trim($uri), FILTER_SANITIZE_URL));
268 24
            $chunks = $this->sortURISegments($uriChunks);
269 24
        } else {
270 4
            $chunks = $this->sortURISegments();
271
        }
272
273 28
        $uri = ltrim(implode('/', $chunks), '/');
274
275 28
        return $uri;
276
    }
277
278 28
    private function sortURISegments($uriChunks = [])
279
    {
280 28
        $module = ucfirst(strtolower($this->defaultModule));
281 28
        $controller = ucfirst(strtolower($this->defaultController));
282 28
        $method = ucfirst(strtolower($this->defaultMethod));
283
284 28
        if (!empty($uriChunks)) {
285 24
            $module = ucfirst(strtolower($uriChunks[0]));
286
287 24
            if (!empty($uriChunks[1])) {
288 22
                $controller = ucfirst(strtolower($uriChunks[1]));
289 24
            } elseif (!empty($this->configuration->modules->$module->defaultController)) {
290
                $controller = $this->configuration->modules->$module->defaultController;
291
            }
292
293 24
            if (!empty($uriChunks[2])) {
294 22
                $method = ucfirst(strtolower($uriChunks[2]));
295 22
                $class = '\\App\\Modules\\' . $module . '\\Controllers\\' . $controller;
296 22
                $methodExist = method_exists($class, $method);
297
                
298 22 View Code Duplication
                if ($methodExist === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
299 22
                    if (!empty($this->configuration->modules->$module->defaultMethod)) {
300
                        $method = $this->configuration->modules->$module->defaultMethod;
301
                        array_unshift($uriChunks, null);
302
                    }
303 22
                }
304 24 View Code Duplication
            } elseif (!empty($this->configuration->modules->$module->defaultMethod)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
305
                $method = $this->configuration->modules->$module->defaultMethod;
306
            }
307
308 24
            unset($uriChunks[0], $uriChunks[1], $uriChunks[2]);
309 24
        }
310
311 28
        $return = [$module, $controller, $method];
312 28
        return array_merge($return, array_values($uriChunks));
313
    }
314
315 2
    private function addQueryString($url, $key, $value)
316
    {
317 2
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
318 2
        $url = substr($url, 0, -1);
319 2
        if (strpos($url, '?') === false) {
320 2
            return ($url . '?' . $key . '=' . $value);
321
        } else {
322 1
            return ($url . '&' . $key . '=' . $value);
323
        }
324
    }
325
326 2
    private function removeQueryString($url, $key)
327
    {
328 2
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
329 2
        $url = substr($url, 0, -1);
330 2
        return ($url);
331
    }
332
333
    /**
334
     * Return the currentURL w/ query strings
335
     *
336
     * @access public
337
     * @return string http://tld.com/formatted/u/r/l?q=bingo
338
     */
339 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...
340
    {
341 28
        if (trim($_SERVER['REQUEST_URI']) === '/') {
342 2
            $url = $this->baseURL()
343 2
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
344 2
        } else {
345 28
            $url = $this->baseURL($this->uri)
346 28
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
347
        }
348
349 28
        if (!empty($params)) {
350 2
            foreach ($params as $key => $param) {
0 ignored issues
show
Bug introduced by
The expression $params of type boolean is not traversable.
Loading history...
351 2
                $url = $this->removeQueryString($url, $key);
352 2
                $url = $this->addQueryString($url, $key, $param);
353 2
            }
354 2
        }
355
356 28
        return $url;
357
    }
358
359
    /**
360
     * Return the baseURL
361
     *
362
     * @access public
363
     * @return string http://tld.com
364
     */
365 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...
366
    {
367 28
        if (is_null($this->baseURL)) {
368 28
            $self = $_SERVER['PHP_SELF'];
369 28
            $server = $_SERVER['HTTP_HOST']
370 28
                      . rtrim(str_replace(strstr($self, 'index.php'), '', $self), '/');
371
372 28
            if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
373 27
                || !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
374 27
                && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
375 28
            ) {
376 1
                $protocol = 'https://';
377 1
            } else {
378 27
                $protocol = 'http://';
379
            }
380
381 28
            $this->baseURL = $protocol . $server;
382 28
        }
383
384 28
        $url = $this->baseURL;
385
386 28
        if ($path !== '') {
387 28
            $url .= '/' . $path;
388 28
        }
389
390 28
        return $url;
391
    }
392
393
    /**
394
     * Set optional status header, and redirect to provided URL
395
     *
396
     * @access public
397
     * @return bool
398
     */
399
    public function redirect($url = '/', $status = null)
400
    {
401
        $url = str_replace(array('\r', '\n', '%0d', '%0a'), '', $url);
402
403
        if (headers_sent()) {
404
            return false;
405
        }
406
407
        // trap session vars before redirect
408
        session_write_close();
409
410
        if (is_null($status)) {
411
            $status = '302';
412
        }
413
414
        // push a status to the browser if necessary
415
        if ((int)$status > 0) {
416
            switch ($status) {
417
                case '301':
418
                    $msg = '301 Moved Permanently';
419
                    break;
420
                case '307':
421
                    $msg = '307 Temporary Redirect';
422
                    break;
423
                // Using these below (except 302) would be an intentional misuse of the 'system'
424
                // Need to dig into the above comment @zech
425
                case '401':
426
                    $msg = '401 Access Denied';
427
                    break;
428
                case '403':
429
                    $msg = '403 Request Forbidden';
430
                    break;
431
                case '404':
432
                    $msg = '404 Not Found';
433
                    break;
434
                case '405':
435
                    $msg = '405 Method Not Allowed';
436
                    break;
437
                case '302':
438
                default:
439
                    $msg = '302 Found';
440
                    break; // temp redirect
441
            }
442
            if (isset($msg)) {
443
                header('HTTP/1.1 ' . $msg);
444
            }
445
        }
446
447
        $url = preg_replace('!^/*!', '', $url);
448
        header("Location: " . $url);
449
    }
450
}
451