1 <?php
2 /**
3 * @package Joomla.Libraries
4 * @subpackage Router
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE.txt
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 /**
13 * Mask for the raw routing mode
14 *
15 * @deprecated 4.0
16 */
17 const JROUTER_MODE_RAW = 0;
18
19 /**
20 * Mask for the SEF routing mode
21 *
22 * @deprecated 4.0
23 */
24 const JROUTER_MODE_SEF = 1;
25
26 /**
27 * Class to create and parse routes
28 *
29 * @since 1.5
30 */
31 class JRouter
32 {
33 /**
34 * Mask for the before process stage
35 *
36 * @var string
37 * @since 3.4
38 */
39 const PROCESS_BEFORE = 'preprocess';
40
41 /**
42 * Mask for the during process stage
43 *
44 * @var string
45 * @since 3.4
46 */
47 const PROCESS_DURING = '';
48
49 /**
50 * Mask for the after process stage
51 *
52 * @var string
53 * @since 3.4
54 */
55 const PROCESS_AFTER = 'postprocess';
56
57 /**
58 * The rewrite mode
59 *
60 * @var integer
61 * @since 1.5
62 * @deprecated 4.0
63 */
64 protected $mode = null;
65
66 /**
67 * The rewrite mode
68 *
69 * @var integer
70 * @since 1.5
71 * @deprecated 4.0
72 */
73 protected $_mode = null;
74
75 /**
76 * An array of variables
77 *
78 * @var array
79 * @since 1.5
80 */
81 protected $vars = array();
82
83 /**
84 * An array of variables
85 *
86 * @var array
87 * @since 1.5
88 * @deprecated 4.0 Will convert to $vars
89 */
90 protected $_vars = array();
91
92 /**
93 * An array of rules
94 *
95 * @var array
96 * @since 1.5
97 */
98 protected $rules = array(
99 'buildpreprocess' => array(),
100 'build' => array(),
101 'buildpostprocess' => array(),
102 'parsepreprocess' => array(),
103 'parse' => array(),
104 'parsepostprocess' => array(),
105 );
106
107 /**
108 * An array of rules
109 *
110 * @var array
111 * @since 1.5
112 * @deprecated 4.0 Will convert to $rules
113 */
114 protected $_rules = array(
115 'buildpreprocess' => array(),
116 'build' => array(),
117 'buildpostprocess' => array(),
118 'parsepreprocess' => array(),
119 'parse' => array(),
120 'parsepostprocess' => array(),
121 );
122
123 /**
124 * Caching of processed URIs
125 *
126 * @var array
127 * @since 3.3
128 */
129 protected $cache = array();
130
131 /**
132 * JRouter instances container.
133 *
134 * @var JRouter[]
135 * @since 1.7
136 */
137 protected static $instances = array();
138
139 /**
140 * Class constructor
141 *
142 * @param array $options Array of options
143 *
144 * @since 1.5
145 */
146 public function __construct($options = array())
147 {
148 if (array_key_exists('mode', $options))
149 {
150 $this->_mode = $options['mode'];
151 }
152 else
153 {
154 $this->_mode = JROUTER_MODE_RAW;
155 }
156 }
157
158 /**
159 * Returns the global JRouter object, only creating it if it
160 * doesn't already exist.
161 *
162 * @param string $client The name of the client
163 * @param array $options An associative array of options
164 *
165 * @return JRouter A JRouter object.
166 *
167 * @since 1.5
168 * @throws RuntimeException
169 */
170 public static function getInstance($client, $options = array())
171 {
172 if (empty(self::$instances[$client]))
173 {
174 // Create a JRouter object
175 $classname = 'JRouter' . ucfirst($client);
176
177 if (!class_exists($classname))
178 {
179 // @deprecated 4.0 Everything in this block is deprecated but the warning is only logged after the file_exists
180 // Load the router object
181 $info = JApplicationHelper::getClientInfo($client, true);
182
183 if (is_object($info))
184 {
185 $path = $info->path . '/includes/router.php';
186
187 JLoader::register($classname, $path);
188
189 if (class_exists($classname))
190 {
191 JLog::add('Non-autoloadable JRouter subclasses are deprecated, support will be removed in 4.0.', JLog::WARNING, 'deprecated');
192 }
193 }
194 }
195
196 if (class_exists($classname))
197 {
198 self::$instances[$client] = new $classname($options);
199 }
200 else
201 {
202 throw new RuntimeException(JText::sprintf('JLIB_APPLICATION_ERROR_ROUTER_LOAD', $client), 500);
203 }
204 }
205
206 return self::$instances[$client];
207 }
208
209 /**
210 * Function to convert a route to an internal URI
211 *
212 * @param JUri &$uri The uri.
213 *
214 * @return array
215 *
216 * @since 1.5
217 */
218 public function parse(&$uri)
219 {
220 // Do the preprocess stage of the URL build process
221 $vars = $this->processParseRules($uri, self::PROCESS_BEFORE);
222
223 // Process the parsed variables based on custom defined rules
224 // This is the main parse stage
225 $vars += $this->_processParseRules($uri);
226
227 // Parse RAW URL
228 if ($this->_mode == JROUTER_MODE_RAW)
229 {
230 $vars += $this->_parseRawRoute($uri);
231 }
232
233 // Parse SEF URL
234 if ($this->_mode == JROUTER_MODE_SEF)
235 {
236 $vars += $this->_parseSefRoute($uri);
237 }
238
239 // Do the postprocess stage of the URL build process
240 $vars += $this->processParseRules($uri, self::PROCESS_AFTER);
241
242 return array_merge($this->getVars(), $vars);
243 }
244
245 /**
246 * Function to convert an internal URI to a route
247 *
248 * @param string $url The internal URL or an associative array
249 *
250 * @return JUri The absolute search engine friendly URL object
251 *
252 * @since 1.5
253 */
254 public function build($url)
255 {
256 $key = md5(serialize($url));
257
258 if (isset($this->cache[$key]))
259 {
260 return clone $this->cache[$key];
261 }
262
263 // Create the URI object
264 $uri = $this->createUri($url);
265
266 // Do the preprocess stage of the URL build process
267 $this->processBuildRules($uri, self::PROCESS_BEFORE);
268
269 // Process the uri information based on custom defined rules.
270 // This is the main build stage
271 $this->_processBuildRules($uri);
272
273 // Build RAW URL
274 if ($this->_mode == JROUTER_MODE_RAW)
275 {
276 $this->_buildRawRoute($uri);
277 }
278
279 // Build SEF URL : mysite/route/index.php?var=x
280 if ($this->_mode == JROUTER_MODE_SEF)
281 {
282 $this->_buildSefRoute($uri);
283 }
284
285 // Do the postprocess stage of the URL build process
286 $this->processBuildRules($uri, self::PROCESS_AFTER);
287
288 $this->cache[$key] = clone $uri;
289
290 return $uri;
291 }
292
293 /**
294 * Get the router mode
295 *
296 * @return integer
297 *
298 * @since 1.5
299 * @deprecated 4.0
300 */
301 public function getMode()
302 {
303 return $this->_mode;
304 }
305
306 /**
307 * Set the router mode
308 *
309 * @param integer $mode The routing mode.
310 *
311 * @return void
312 *
313 * @since 1.5
314 * @deprecated 4.0
315 */
316 public function setMode($mode)
317 {
318 $this->_mode = $mode;
319 }
320
321 /**
322 * Set a router variable, creating it if it doesn't exist
323 *
324 * @param string $key The name of the variable
325 * @param mixed $value The value of the variable
326 * @param boolean $create If True, the variable will be created if it doesn't exist yet
327 *
328 * @return void
329 *
330 * @since 1.5
331 */
332 public function setVar($key, $value, $create = true)
333 {
334 if ($create || array_key_exists($key, $this->_vars))
335 {
336 $this->_vars[$key] = $value;
337 }
338 }
339
340 /**
341 * Set the router variable array
342 *
343 * @param array $vars An associative array with variables
344 * @param boolean $merge If True, the array will be merged instead of overwritten
345 *
346 * @return void
347 *
348 * @since 1.5
349 */
350 public function setVars($vars = array(), $merge = true)
351 {
352 if ($merge)
353 {
354 $this->_vars = array_merge($this->_vars, $vars);
355 }
356 else
357 {
358 $this->_vars = $vars;
359 }
360 }
361
362 /**
363 * Get a router variable
364 *
365 * @param string $key The name of the variable
366 *
367 * @return mixed Value of the variable
368 *
369 * @since 1.5
370 */
371 public function getVar($key)
372 {
373 $result = null;
374
375 if (isset($this->_vars[$key]))
376 {
377 $result = $this->_vars[$key];
378 }
379
380 return $result;
381 }
382
383 /**
384 * Get the router variable array
385 *
386 * @return array An associative array of router variables
387 *
388 * @since 1.5
389 */
390 public function getVars()
391 {
392 return $this->_vars;
393 }
394
395 /**
396 * Attach a build rule
397 *
398 * @param callable $callback The function to be called
399 * @param string $stage The stage of the build process that
400 * this should be added to. Possible values:
401 * 'preprocess', '' for the main build process,
402 * 'postprocess'
403 *
404 * @return void
405 *
406 * @since 1.5
407 */
408 public function attachBuildRule($callback, $stage = self::PROCESS_DURING)
409 {
410 if (!array_key_exists('build' . $stage, $this->_rules))
411 {
412 throw new InvalidArgumentException(sprintf('The %s stage is not registered. (%s)', $stage, __METHOD__));
413 }
414
415 $this->_rules['build' . $stage][] = $callback;
416 }
417
418 /**
419 * Attach a parse rule
420 *
421 * @param callable $callback The function to be called.
422 * @param string $stage The stage of the parse process that
423 * this should be added to. Possible values:
424 * 'preprocess', '' for the main parse process,
425 * 'postprocess'
426 *
427 * @return void
428 *
429 * @since 1.5
430 */
431 public function attachParseRule($callback, $stage = self::PROCESS_DURING)
432 {
433 if (!array_key_exists('parse' . $stage, $this->_rules))
434 {
435 throw new InvalidArgumentException(sprintf('The %s stage is not registered. (%s)', $stage, __METHOD__));
436 }
437
438 $this->_rules['parse' . $stage][] = $callback;
439 }
440
441 /**
442 * Function to convert a raw route to an internal URI
443 *
444 * @param JUri &$uri The raw route
445 *
446 * @return boolean
447 *
448 * @since 1.5
449 * @deprecated 4.0 Attach your logic as rule to the main parse stage
450 */
451 protected function _parseRawRoute(&$uri)
452 {
453 return $this->parseRawRoute($uri);
454 }
455
456 /**
457 * Function to convert a raw route to an internal URI
458 *
459 * @param JUri &$uri The raw route
460 *
461 * @return array Array of variables
462 *
463 * @since 3.2
464 * @deprecated 4.0 Attach your logic as rule to the main parse stage
465 */
466 protected function parseRawRoute(&$uri)
467 {
468 return array();
469 }
470
471 /**
472 * Function to convert a sef route to an internal URI
473 *
474 * @param JUri &$uri The sef URI
475 *
476 * @return string Internal URI
477 *
478 * @since 1.5
479 * @deprecated 4.0 Attach your logic as rule to the main parse stage
480 */
481 protected function _parseSefRoute(&$uri)
482 {
483 return $this->parseSefRoute($uri);
484 }
485
486 /**
487 * Function to convert a sef route to an internal URI
488 *
489 * @param JUri &$uri The sef URI
490 *
491 * @return array Array of variables
492 *
493 * @since 3.2
494 * @deprecated 4.0 Attach your logic as rule to the main parse stage
495 */
496 protected function parseSefRoute(&$uri)
497 {
498 return array();
499 }
500
501 /**
502 * Function to build a raw route
503 *
504 * @param JUri &$uri The internal URL
505 *
506 * @return string Raw Route
507 *
508 * @since 1.5
509 * @deprecated 4.0 Attach your logic as rule to the main build stage
510 */
511 protected function _buildRawRoute(&$uri)
512 {
513 return $this->buildRawRoute($uri);
514 }
515
516 /**
517 * Function to build a raw route
518 *
519 * @param JUri &$uri The internal URL
520 *
521 * @return string Raw Route
522 *
523 * @since 3.2
524 * @deprecated 4.0 Attach your logic as rule to the main build stage
525 */
526 protected function buildRawRoute(&$uri)
527 {
528 }
529
530 /**
531 * Function to build a sef route
532 *
533 * @param JUri &$uri The uri
534 *
535 * @return string The SEF route
536 *
537 * @since 1.5
538 * @deprecated 4.0 Attach your logic as rule to the main build stage
539 */
540 protected function _buildSefRoute(&$uri)
541 {
542 return $this->buildSefRoute($uri);
543 }
544
545 /**
546 * Function to build a sef route
547 *
548 * @param JUri &$uri The uri
549 *
550 * @return string The SEF route
551 *
552 * @since 3.2
553 * @deprecated 4.0 Attach your logic as rule to the main build stage
554 */
555 protected function buildSefRoute(&$uri)
556 {
557 }
558
559 /**
560 * Process the parsed router variables based on custom defined rules
561 *
562 * @param JUri &$uri The URI to parse
563 *
564 * @return array The array of processed URI variables
565 *
566 * @since 1.5
567 * @deprecated 4.0 Use processParseRules() instead
568 */
569 protected function _processParseRules(&$uri)
570 {
571 return $this->processParseRules($uri);
572 }
573
574 /**
575 * Process the parsed router variables based on custom defined rules
576 *
577 * @param JUri &$uri The URI to parse
578 * @param string $stage The stage that should be processed.
579 * Possible values: 'preprocess', 'postprocess'
580 * and '' for the main parse stage
581 *
582 * @return array The array of processed URI variables
583 *
584 * @since 3.2
585 */
586 protected function processParseRules(&$uri, $stage = self::PROCESS_DURING)
587 {
588 if (!array_key_exists('parse' . $stage, $this->_rules))
589 {
590 throw new InvalidArgumentException(sprintf('The %s stage is not registered. (%s)', $stage, __METHOD__));
591 }
592
593 $vars = array();
594
595 foreach ($this->_rules['parse' . $stage] as $rule)
596 {
597 $vars += (array) call_user_func_array($rule, array(&$this, &$uri));
598 }
599
600 return $vars;
601 }
602
603 /**
604 * Process the build uri query data based on custom defined rules
605 *
606 * @param JUri &$uri The URI
607 *
608 * @return void
609 *
610 * @since 1.5
611 * @deprecated 4.0 Use processBuildRules() instead
612 */
613 protected function _processBuildRules(&$uri)
614 {
615 $this->processBuildRules($uri);
616 }
617
618 /**
619 * Process the build uri query data based on custom defined rules
620 *
621 * @param JUri &$uri The URI
622 * @param string $stage The stage that should be processed.
623 * Possible values: 'preprocess', 'postprocess'
624 * and '' for the main build stage
625 *
626 * @return void
627 *
628 * @since 3.2
629 */
630 protected function processBuildRules(&$uri, $stage = self::PROCESS_DURING)
631 {
632 if (!array_key_exists('build' . $stage, $this->_rules))
633 {
634 throw new InvalidArgumentException(sprintf('The %s stage is not registered. (%s)', $stage, __METHOD__));
635 }
636
637 foreach ($this->_rules['build' . $stage] as $rule)
638 {
639 $rule($this, $uri);
640 }
641 }
642
643 /**
644 * Create a uri based on a full or partial URL string
645 *
646 * @param string $url The URI
647 *
648 * @return JUri
649 *
650 * @since 1.5
651 * @deprecated 4.0 Use createUri() instead
652 * @codeCoverageIgnore
653 */
654 protected function _createUri($url)
655 {
656 return $this->createUri($url);
657 }
658
659 /**
660 * Create a uri based on a full or partial URL string
661 *
662 * @param string $url The URI or an associative array
663 *
664 * @return JUri
665 *
666 * @since 3.2
667 */
668 protected function createUri($url)
669 {
670 if (!is_array($url) && substr($url, 0, 1) !== '&')
671 {
672 return new JUri($url);
673 }
674
675 $uri = new JUri('index.php');
676
677 if (is_string($url))
678 {
679 $vars = array();
680
681 if (strpos($url, '&') !== false)
682 {
683 $url = str_replace('&', '&', $url);
684 }
685
686 parse_str($url, $vars);
687 }
688 else
689 {
690 $vars = $url;
691 }
692
693 $vars = array_merge($this->getVars(), $vars);
694
695 foreach ($vars as $key => $var)
696 {
697 if ($var == '')
698 {
699 unset($vars[$key]);
700 }
701 }
702
703 $uri->setQuery($vars);
704
705 return $uri;
706 }
707
708 /**
709 * Encode route segments
710 *
711 * @param array $segments An array of route segments
712 *
713 * @return array Array of encoded route segments
714 *
715 * @since 1.5
716 * @deprecated 4.0 This should be performed in the component router instead
717 * @codeCoverageIgnore
718 */
719 protected function _encodeSegments($segments)
720 {
721 return $this->encodeSegments($segments);
722 }
723
724 /**
725 * Encode route segments
726 *
727 * @param array $segments An array of route segments
728 *
729 * @return array Array of encoded route segments
730 *
731 * @since 3.2
732 * @deprecated 4.0 This should be performed in the component router instead
733 */
734 protected function encodeSegments($segments)
735 {
736 $total = count($segments);
737
738 for ($i = 0; $i < $total; $i++)
739 {
740 $segments[$i] = str_replace(':', '-', $segments[$i]);
741 }
742
743 return $segments;
744 }
745
746 /**
747 * Decode route segments
748 *
749 * @param array $segments An array of route segments
750 *
751 * @return array Array of decoded route segments
752 *
753 * @since 1.5
754 * @deprecated 4.0 This should be performed in the component router instead
755 * @codeCoverageIgnore
756 */
757 protected function _decodeSegments($segments)
758 {
759 return $this->decodeSegments($segments);
760 }
761
762 /**
763 * Decode route segments
764 *
765 * @param array $segments An array of route segments
766 *
767 * @return array Array of decoded route segments
768 *
769 * @since 3.2
770 * @deprecated 4.0 This should be performed in the component router instead
771 */
772 protected function decodeSegments($segments)
773 {
774 $total = count($segments);
775
776 for ($i = 0; $i < $total; $i++)
777 {
778 $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1);
779 }
780
781 return $segments;
782 }
783 }
784