Completed
Push — master ( c0292c...a7cc68 )
by
unknown
22s
created

Router::addQueryString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
cc 2
eloc 7
nc 2
nop 3
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',
120
            (object)[
121 18
            'module' => $uriChunks[0],
122 18
            'controller' => $uriChunks[1],
123 18
            'method' => $uriChunks[2],
124 18
            'params' => array_slice($uriChunks, 3),
125 18
            'baseURL' => $this->baseURL,
126 18
            'currentURL' => $this->currentURL
127
            ]
128
        );
129 18
    }
130
131
132 28
    private function isURIClean($uri, $uriChunks)
133
    {
134 28
        if (!preg_match("/^[a-z0-9:_\/\.\[\]-]+$/i", $uri)
135 19
            || array_filter(
136
                $uriChunks,
137
                function ($uriChunk) {
138 19
                    if (strpos($uriChunk, '__') !== false) {
139 1
                        return true;
140
                    }
141 28
                }
142
            )
143
        ) {
144 10
            return false;
145
        } else {
146 18
            return true;
147
        }
148
    }
149
150
    //@TODO add Security class.
151 22
    private function normalize($data)
152
    {
153 22
        if (is_numeric($data)) {
154 11
            if (is_int($data) || ctype_digit(trim($data, '-'))) {
155 3
                $data = (int)$data;
156 8
            } elseif ($data === (string)(float)$data) {
157
                //@TODO: this needs work.. 9E26 converts to float
158 4
                $data = (float)$data;
159
            }
160
        }
161 22
        return $data;
162
    }
163
164
    /**
165
     * Parse and explode URI segments into chunks
166
     *
167
     * @access private
168
     *
169
     * @param string $uri
170
     *
171
     * @return array chunks of uri
172
     * @throws Exception on disallowed characters
173
     */
174 28
    private function parseURI($uri)
175
    {
176 28
        $uriFragments = explode('/', $uri);
177 28
        $uriChunks = [];
178 28
        $params = [];
179 28
        $iteration = 0;
180 28
        foreach ($uriFragments as $location => $fragment) {
181 28
            if ($iteration > 2) {
182 22
                $params[] = $this->normalize(trim($fragment));
183
            } else {
184 28
                $uriChunks[] = trim($fragment);
185
            }
186 28
            $iteration++;
187
        }
188
189 28
        $result = array_merge($uriChunks, $params);
190
191 28
        if ($this->isURIClean($uri, $result) === false) {
192 10
            throw new RouteException('Invalid key characters.');
193
        }
194
195 18
        return $result;
196
    }
197
198
    /**
199
     * Normalize the $_SERVER vars for formatting the URI.
200
     *
201
     * @access private
202
     * @return string formatted/u/r/l
203
     */
204 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...
205
    {
206
207 28
        if (!empty($_SERVER['PATH_INFO'])) {
208 1
            $normalizedURI = $_SERVER['PATH_INFO'];
209 27
        } elseif (!empty($_SERVER['REQUEST_URI'])) {
210 26
            $normalizedURI = $_SERVER['REQUEST_URI'];
211
        } else {
212 1
            $normalizedURI = false;
213
        }
214
215 28
        if ($normalizedURI === '/') {
216 1
            $normalizedURI = false;
217
        }
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 26
            $uriChunks = explode('/', filter_var(trim($uri), FILTER_SANITIZE_URL));
268 26
            $chunks = $this->sortURISegments($uriChunks);
269
        } else {
270 2
            $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 26
            $module = ucfirst(strtolower($uriChunks[0]));
286
287 26
            if (!empty($uriChunks[1])) {
288 23
                $controller = ucfirst(strtolower($uriChunks[1]));
289 3
            } elseif (!empty($this->configuration->modules->$module->defaultController)) {
290
                $controller = $this->configuration->modules->$module->defaultController;
291
            }
292
293 26
            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 22
                        array_unshift($uriChunks, null);
302
                    }
303
                }
304 4 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 26
            unset($uriChunks[0], $uriChunks[1], $uriChunks[2]);
309
        }
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 1
            $url = $this->baseURL()
343 1
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
344
        } else {
345 27
            $url = $this->baseURL($this->uri)
346 27
                   . (!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
            }
354
        }
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 28
                && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
375
            ) {
376 1
                $protocol = 'https://';
377
            } else {
378 27
                $protocol = 'http://';
379
            }
380
381 28
            $this->baseURL = $protocol . $server;
382
        }
383
384 28
        $url = $this->baseURL;
385
386 28
        if ($path !== '') {
387 27
            $url .= '/' . $path;
388
        }
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