Completed
Pull Request — master (#70)
by
unknown
02:33
created

Router::redirect()   C

Complexity

Conditions 12
Paths 35

Size

Total Lines 51
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 7
Bugs 1 Features 0
Metric Value
c 7
b 1
f 0
dl 0
loc 51
ccs 0
cts 34
cp 0
rs 5.6668
cc 12
eloc 35
nc 35
nop 2
crap 156

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
            ]
129
        );
130 18
    }
131
132
133 28
    private function isURIClean($uri, $uriChunks)
134
    {
135 28
        if (!preg_match("/^[a-z0-9:_\/\.\[\]-]+$/i", $uri)
136 19
            || array_filter(
137
                $uriChunks,
138
                function ($uriChunk) {
139 19
                    if (strpos($uriChunk, '__') !== false) {
140 1
                        return true;
141
                    }
142 28
                }
143
            )
144
        ) {
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 11
            if (is_int($data) || ctype_digit(trim($data, '-'))) {
156 3
                $data = (int)$data;
157 8
            } elseif ($data === (string)(float)$data) {
158
                //@TODO: this needs work.. 9E26 converts to float
159 4
                $data = (float)$data;
160
            }
161
        }
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
            } else {
185 28
                $uriChunks[] = trim($fragment);
186
            }
187 28
            $iteration++;
188
        }
189
190 28
        $result = array_merge($uriChunks, $params);
191
192 28
        if ($this->isURIClean($uri, $result) === false) {
193
//            echo "<PRE>\r\n";
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
194
//            print_r($_SERVER);
195
//            print_r($uri);echo "\r\n";
196
//            print_r($result);echo "\r\n";
197
//            die("test");
198 10
            throw new RouteException('Invalid key characters.');
199
        }
200
201 18
        return $result;
202
    }
203
204
    /**
205
     * Normalize the $_SERVER vars for formatting the URI.
206
     *
207
     * @access private
208
     * @return string formatted/u/r/l
209
     */
210 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...
211
    {
212 28
        if (!empty($_SERVER['PATH_INFO'])) {
213
            $normalizedURI = $_SERVER['PATH_INFO'];
214 28
        } elseif (!empty($_SERVER['REQUEST_URI'])) {
215 26
            $normalizedURI = $_SERVER['REQUEST_URI'];
216
        } else {
217 4
            $normalizedURI = false;
218
        }
219
220 28
        if ($normalizedURI === '/') {
221 2
            $normalizedURI = false;
222
        }
223
224 28
        $normalizedURI = ltrim(preg_replace('/\?.*/', '', $normalizedURI), '/');
225
226 28
        if (! empty($this->configuration->routes)) {
227
            $normalizedURI = $this->discoverRoute($normalizedURI);
228
        }
229
230 28
        return $normalizedURI;
231
    }
232
233
    private function discoverRoute($uri)
234
    {
235
        $routes = $this->configuration->routes;
236
237
        foreach ($routes as $route => $reroute) {
238
            $pattern = '/^(?i)' . str_replace('/', '\/', $route) . '$/';
239
240
            if (preg_match($pattern, $uri, $params)) {
241
                array_shift($params);
242
243
                $uri = $reroute;
244
245
                if (! empty($params)) {
246
                    $pat = '/(\$\d+)/';
247
                    $uri = preg_replace_callback(
248
                        $pat,
249
                        function () use (&$params) {
250
                            $first = $params[0];
251
                            array_shift($params);
252
                            return $first;
253
                        },
254
                        $reroute
255
                    );
256
                }
257
            }
258
        }
259
260
        return $uri;
261
    }
262
263
    /**
264
     * Normalize the $_SERVER vars for formatting the URI.
265
     *
266
     * @access public
267
     * @return string formatted/u/r/l
268
     */
269 28
    private function uri($uri)
270
    {
271 28
        if ($uri !== '') {
272 24
            $uriChunks = explode('/', filter_var(trim($uri), FILTER_SANITIZE_URL));
273 24
            $chunks = $this->sortURISegments($uriChunks);
274
        } else {
275 4
            $chunks = $this->sortURISegments();
276
        }
277
278 28
        $uri = ltrim(implode('/', $chunks), '/');
279
280 28
        return $uri;
281
    }
282
283 28
    private function sortURISegments($uriChunks = [])
284
    {
285 28
        $module = ucfirst(strtolower($this->defaultModule));
286 28
        $controller = ucfirst(strtolower($this->defaultController));
287 28
        $method = ucfirst(strtolower($this->defaultMethod));
288
289 28
        if (!empty($uriChunks)) {
290 24
            $module = ucfirst(strtolower($uriChunks[0]));
291
292 24
            if (!empty($uriChunks[1])) {
293 22
                $controller = ucfirst(strtolower($uriChunks[1]));
294 2
            } elseif (!empty($this->configuration->modules->$module->defaultController)) {
295
                $controller = $this->configuration->modules->$module->defaultController;
296
            }
297
298 24
            if (!empty($uriChunks[2])) {
299 22
                $method = ucfirst(strtolower($uriChunks[2]));
300 22
                $class = '\\App\\Modules\\' . $module . '\\Controllers\\' . $controller;
301 22
                $methodExist = method_exists($class, $method);
302
                
303 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...
304 22
                    if (!empty($this->configuration->modules->$module->defaultMethod)) {
305
                        $method = $this->configuration->modules->$module->defaultMethod;
306 22
                        array_unshift($uriChunks, null);
307
                    }
308
                }
309 2 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...
310
                $method = $this->configuration->modules->$module->defaultMethod;
311
            }
312
313 24
            unset($uriChunks[0], $uriChunks[1], $uriChunks[2]);
314
        }
315
316 28
        $return = [$module, $controller, $method];
317 28
        return array_merge($return, array_values($uriChunks));
318
    }
319
320 2
    private function addQueryString($url, $key, $value)
321
    {
322 2
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
323 2
        $url = substr($url, 0, -1);
324 2
        if (strpos($url, '?') === false) {
325 2
            return ($url . '?' . $key . '=' . $value);
326
        } else {
327 1
            return ($url . '&' . $key . '=' . $value);
328
        }
329
    }
330
331 2
    private function removeQueryString($url, $key)
332
    {
333 2
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
334 2
        $url = substr($url, 0, -1);
335 2
        return ($url);
336
    }
337
338
    /**
339
     * Return the currentURL w/ query strings
340
     *
341
     * @access public
342
     * @return string http://tld.com/formatted/u/r/l?q=bingo
343
     */
344 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...
345
    {
346 28
        if (trim($_SERVER['REQUEST_URI']) === '/') {
347 2
            $url = $this->baseURL()
348 2
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
349
        } else {
350 28
            $url = $this->baseURL($this->uri)
351 28
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
352
        }
353
354 28
        if (!empty($params)) {
355 2
            foreach ($params as $key => $param) {
0 ignored issues
show
Bug introduced by
The expression $params of type boolean is not traversable.
Loading history...
356 2
                $url = $this->removeQueryString($url, $key);
357 2
                $url = $this->addQueryString($url, $key, $param);
358
            }
359
        }
360
361 28
        return $url;
362
    }
363
364
    /**
365
     * Return the baseURL
366
     *
367
     * @access public
368
     * @return string http://tld.com
369
     */
370 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...
371
    {
372 28
        if (is_null($this->baseURL)) {
373 28
            $self = $_SERVER['PHP_SELF'];
374 28
            $server = $_SERVER['HTTP_HOST']
375 28
                      . rtrim(str_replace(strstr($self, 'index.php'), '', $self), '/');
376
377 28
            if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
378 27
                || !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
379 28
                && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
380
            ) {
381 1
                $protocol = 'https://';
382
            } else {
383 27
                $protocol = 'http://';
384
            }
385
386 28
            $this->baseURL = $protocol . $server;
387
        }
388
389 28
        $url = $this->baseURL;
390
391 28
        if ($path !== '') {
392 28
            $url .= '/' . $path;
393
        }
394
395 28
        return $url;
396
    }
397
398
    /**
399
     * Set optional status header, and redirect to provided URL
400
     *
401
     * @access public
402
     * @return bool
403
     */
404
    public function redirect($url = '/', $status = null)
405
    {
406
        $url = str_replace(array('\r', '\n', '%0d', '%0a'), '', $url);
407
408
        if (headers_sent()) {
409
            return false;
410
        }
411
412
        // trap session vars before redirect
413
        session_write_close();
414
415
        if (is_null($status)) {
416
            $status = '302';
417
        }
418
419
        // push a status to the browser if necessary
420
        if ((int)$status > 0) {
421
            switch ($status) {
422
                case '301':
423
                    $msg = '301 Moved Permanently';
424
                    break;
425
                case '307':
426
                    $msg = '307 Temporary Redirect';
427
                    break;
428
                // Using these below (except 302) would be an intentional misuse of the 'system'
429
                // Need to dig into the above comment @zech
430
                case '401':
431
                    $msg = '401 Access Denied';
432
                    break;
433
                case '403':
434
                    $msg = '403 Request Forbidden';
435
                    break;
436
                case '404':
437
                    $msg = '404 Not Found';
438
                    break;
439
                case '405':
440
                    $msg = '405 Method Not Allowed';
441
                    break;
442
                case '302':
443
                default:
444
                    $msg = '302 Found';
445
                    break; // temp redirect
446
            }
447
            if (isset($msg)) {
448
                header('HTTP/1.1 ' . $msg);
449
            }
450
        }
451
452
        $url = preg_replace('!^/*!', '', $url);
453
        header("Location: " . $url);
454
    }
455
}
456