Completed
Push — master ( 2ed767...e6cf52 )
by
unknown
03:05
created

Router::normalize()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5

Importance

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