Completed
Push — master ( 7ded72...98326e )
by
unknown
23s
created

Router::removeQueryString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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