Completed
Pull Request — master (#77)
by
unknown
02:47
created

Router::currentURL()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.6393

Importance

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