Completed
Pull Request — master (#66)
by
unknown
03:12
created

Router::uri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 9.4285
cc 2
eloc 8
nc 2
nop 1
crap 2
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 18
        $app->setConfiguration(
119 18
            'router', (object)[
120 18
            'module' => $uriChunks[0],
121 18
            'controller' => $uriChunks[1],
122 18
            'method' => $uriChunks[2],
123 18
            'params' => array_slice($uriChunks, 3),
124 18
            'baseURL' => $this->baseURL,
125 18
            'currentURL' => $this->currentURL
126 18
            ]
127 18
        );
128 18
    }
129
130
131 28
    private function isURIClean($uri, $uriChunks)
132
    {
133 28
        if (!preg_match("/^[a-z0-9:_\/\.\[\]-]+$/i", $uri)
134 28
            || array_filter(
135
                $uriChunks, function ($uriChunk) {
136 19
                    if (strpos($uriChunk, '__') !== false) {
137 1
                        return true;
138
                    }
139 19
                }
140 19
            )
141 28
        ) {
142 10
            return false;
143
        } else {
144 18
            return true;
145
        }
146
    }
147
148
    //@TODO add Security class.
149 22
    private function normalize($data)
150
    {
151 22
        if (is_numeric($data)) {
152 12
            if (is_int($data) || ctype_digit(trim($data, '-'))) {
153 3
                $data = (int)$data;
154 12
            } elseif ($data === (string)(float)$data) {
155
                //@TODO: this needs work.. 9E26 converts to float
156 4
                $data = (float)$data;
157 4
            }
158 12
        }
159 22
        return $data;
160
    }
161
162
    /**
163
     * Parse and explode URI segments into chunks
164
     *
165
     * @access private
166
     *
167
     * @param string $uri
168
     *
169
     * @return array chunks of uri
170
     * @throws Exception on disallowed characters
171
     */
172 28
    private function parseURI($uri)
173
    {
174 28
        $uriFragments = explode('/', $uri);
175 28
        $uriChunks = [];
176 28
        $params = [];
177 28
        $iteration = 0;
178 28
        foreach ($uriFragments as $location => $fragment) {
179 28
            if ($iteration > 2) {
180 22
                $params[] = $this->normalize(trim($fragment));
181 22
            } else {
182 28
                $uriChunks[] = trim($fragment);
183
            }
184 28
            $iteration++;
185 28
        }
186
187 28
        $result = array_merge($uriChunks, $params);
188
189 28
        if ($this->isURIClean($uri, $result) === false) {
190 10
            throw new RouteException('Invalid key characters.');
191
        }
192
193 18
        return $result;
194
    }
195
196
    /**
197
     * Normalize the $_SERVER vars for formatting the URI.
198
     *
199
     * @access private
200
     * @return string formatted/u/r/l
201
     */
202 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...
203
    {
204
205 28
        if (!empty($_SERVER['PATH_INFO'])) {
206 1
            $normalizedURI = $_SERVER['PATH_INFO'];
207 28
        } elseif (!empty($_SERVER['REQUEST_URI'])) {
208 26
            $normalizedURI = $_SERVER['REQUEST_URI'];
209 26
        } else {
210 1
            $normalizedURI = false;
211
        }
212
213 28
        if ($normalizedURI === '/') {
214 1
            $normalizedURI = false;
215 1
        }
216
217 28
        $normalizedURI = ltrim(preg_replace('/\?.*/', '', $normalizedURI), '/');
218
219 28
        if (! empty($this->configuration->routes)) {
220
            $normalizedURI = $this->discoverRoute($normalizedURI);
221
        }
222
        
223 28
        return $normalizedURI;
224
    }
225
226
    private function discoverRoute($uri)
227
    {
228
        $routes = $this->configuration->routes;
229
230
        foreach ($routes as $route => $reroute) {
231
            $pattern = '/^(?i)' . str_replace('/', '\/', $route) . '$/';
232
233
            if (preg_match($pattern, $uri, $params)) {
234
                array_shift($params);
235
236
                $uri = $reroute;
237
238
                if (! empty($params)) {
239
                    $pat = '/(\$\d+)/';
240
                    $uri = preg_replace_callback(
241
                        $pat, function () use (&$params) {
242
                            $first = $params[0];
243
                            array_shift($params);
244
                            return $first;
245
                        }, $reroute
246
                    );
247
                }
248
            }
249
        }
250
251
        return $uri;
252
    }
253
254
    /**
255
     * Normalize the $_SERVER vars for formatting the URI.
256
     *
257
     * @access public
258
     * @return string formatted/u/r/l
259
     */
260 28
    private function uri($uri)
261
    {
262 28
        if ($uri !== '') {
263 26
            $uriChunks = explode('/', filter_var(trim($uri), FILTER_SANITIZE_URL));
264 26
            $chunks = $this->sortURISegments($uriChunks);
265 26
        } else {
266 2
            $chunks = $this->sortURISegments();
267
        }
268
269 28
        $uri = ltrim(implode('/', $chunks), '/');
270
271 28
        return $uri;
272
    }
273
274 28
    private function sortURISegments($uriChunks = [])
275
    {
276 28
        $module = ucfirst(strtolower($this->defaultModule));
277 28
        $controller = ucfirst(strtolower($this->defaultController));
278 28
        $method = ucfirst(strtolower($this->defaultMethod));
279
280 28
        if (!empty($uriChunks)) {
281 26
            $module = ucfirst(strtolower($uriChunks[0]));
282
283 26
            if (!empty($uriChunks[1])) {
284 23
                $controller = ucfirst(strtolower($uriChunks[1]));
285 26
            } elseif (!empty($this->configuration->modules->$module->defaultController)) {
286
                $controller = $this->configuration->modules->$module->defaultController;
287
            }
288
289 26
            if (!empty($uriChunks[2])) {
290 22
                $method = ucfirst(strtolower($uriChunks[2]));
291 22
                $class = '\\App\\Modules\\' . $module . '\\Controllers\\' . $controller;
292 22
                $methodExist = method_exists($class, $method);
293
                
294 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...
295 22
                    if (!empty($this->configuration->modules->$module->defaultMethod)) {
296
                        $method = $this->configuration->modules->$module->defaultMethod;
297
                        array_unshift($uriChunks, null);
298
                    }
299 22
                }
300 26 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...
301
                $method = $this->configuration->modules->$module->defaultMethod;
302
            }
303
304 26
            unset($uriChunks[0], $uriChunks[1], $uriChunks[2]);
305 26
        }
306
307 28
        $return = [$module, $controller, $method];
308 28
        return array_merge($return, array_values($uriChunks));
309
    }
310
311
    private function addQueryString($url, $key, $value)
312
    {
313
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
314
        $url = substr($url, 0, -1);
315
        if (strpos($url, '?') === false) {
316
            return ($url . '?' . $key . '=' . $value);
317
        } else {
318
            return ($url . '&' . $key . '=' . $value);
319
        }
320
    }
321
322
    private function removeQueryString($url, $key)
323
    {
324
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
325
        $url = substr($url, 0, -1);
326
        return ($url);
327
    }
328
329
    /**
330
     * Return the currentURL w/ query strings
331
     *
332
     * @access public
333
     * @return string http://tld.com/formatted/u/r/l?q=bingo
334
     */
335 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...
336
    {
337 28
        if (trim($_SERVER['REQUEST_URI']) === '/') {
338 1
            $url = $this->baseURL()
339 1
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
340 1
        } else {
341 27
            $url = $this->baseURL($this->uri)
342 27
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
343
        }
344
345 28
        if (!empty($params)) {
346
            foreach ($params as $key => $param) {
0 ignored issues
show
Bug introduced by
The expression $params of type boolean is not traversable.
Loading history...
347
                $url = $this->removeQueryString($url, $key);
348
                $url = $this->addQueryString($url, $key, $param);
349
            }
350
        }
351
352 28
        return $url;
353
    }
354
355
    /**
356
     * Return the baseURL
357
     *
358
     * @access public
359
     * @return string http://tld.com
360
     */
361 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...
362
    {
363 28
        if (is_null($this->baseURL)) {
364 28
            $self = $_SERVER['PHP_SELF'];
365 28
            $server = $_SERVER['HTTP_HOST']
366 28
                      . rtrim(str_replace(strstr($self, 'index.php'), '', $self), '/');
367
368 28
            if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
369 27
                || !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
370 27
                && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
371 28
            ) {
372 1
                $protocol = 'https://';
373 1
            } else {
374 27
                $protocol = 'http://';
375
            }
376
377 28
            $this->baseURL = $protocol . $server;
378 28
        }
379
380 28
        $url = $this->baseURL;
381
382 28
        if ($path !== '') {
383 27
            $url .= '/' . $path;
384 27
        }
385
386 28
        return $url;
387
    }
388
389
    /**
390
     * Set optional status header, and redirect to provided URL
391
     *
392
     * @access public
393
     * @return bool
394
     */
395
    public function redirect($url = '/', $status = null)
396
    {
397
        $url = str_replace(array('\r', '\n', '%0d', '%0a'), '', $url);
398
399
        if (headers_sent()) {
400
            return false;
401
        }
402
403
        // trap session vars before redirect
404
        session_write_close();
405
406
        if (is_null($status)) {
407
            $status = '302';
408
        }
409
410
        // push a status to the browser if necessary
411
        if ((int)$status > 0) {
412
            switch ($status) {
413
            case '301':
414
                $msg = '301 Moved Permanently';
415
                break;
416
            case '307':
417
                $msg = '307 Temporary Redirect';
418
                break;
419
                // Using these below (except 302) would be an intentional misuse of the 'system'
420
            case '401':
421
                $msg = '401 Access Denied';
422
                break;
423
            case '403':
424
                $msg = '403 Request Forbidden';
425
                break;
426
            case '404':
427
                $msg = '404 Not Found';
428
                break;
429
            case '405':
430
                $msg = '405 Method Not Allowed';
431
                break;
432
            case '302':
433
            default:
434
                $msg = '302 Found';
435
                break; // temp redirect
436
            }
437
            if (isset($msg)) {
438
                header('HTTP/1.1 ' . $msg);
439
            }
440
        }
441
        if (preg_match('/^https?/', $url)) {
442
            header("Location: $url");
443
            //@TODO: does this break without exit?
444
            //exit;
445
        }
446
        // strip leading slashies
447
        $url = preg_replace('!^/*!', '', $url);
448
        header("Location: " . $this->baseURL($url));
449
        //@TODO: does this break without exit?
450
        //exit;
451
    }
452
}
453