]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/urlmapper.php
Merge remote-tracking branch 'upstream/master' into social-master
[quix0rs-gnu-social.git] / lib / urlmapper.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2011, StatusNet, Inc.
5  *
6  * URL mapper
7  *
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @category  URL
24  * @package   StatusNet
25  * @author    Evan Prodromou <evan@status.net>
26  * @copyright 2011 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     // This check helps protect against security problems;
33     // your code file can't be executed directly from the web.
34     exit(1);
35 }
36
37 /**
38  * URL mapper
39  *
40  * Converts a path into a set of parameters, and vice versa
41  *
42  * We used to use URLMapper, so there's a wrapper class at Router, q.v.
43  *
44  * NUM's vagaries are the main reason we have weirdnesses here.
45  *
46  * @category  URL
47  * @package   StatusNet
48  * @author    Evan Prodromou <evan@status.net>
49  * @copyright 2011 StatusNet, Inc.
50  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
51  * @link      http://status.net/
52  */
53
54 class URLMapper
55 {
56     const ACTION = 'action';
57
58     protected $statics = array();
59     protected $variables = array();
60     protected $reverse = array();
61     protected $allpaths = array();
62
63     function connect($path, $args, $paramPatterns=array())
64     {
65         if (!array_key_exists(self::ACTION, $args)) {
66             throw new Exception(sprintf("Can't connect %s; path has no action.", $path));
67         }
68
69         $allpaths[] = $path;
70
71         $action = $args[self::ACTION];
72
73         $paramNames = $this->getParamNames($path);
74
75         if (empty($paramNames)) {
76             $this->statics[$path] = $args;
77             if (array_key_exists($action, $this->reverse)) {
78                 $this->reverse[$args[self::ACTION]][] = array($args, $path);
79             } else {
80                 $this->reverse[$args[self::ACTION]] = array(array($args, $path));
81             }
82         } else {
83
84             // Eff if I understand why some go here and some go there.
85             // Anyways, fixup my preconceptions
86
87             foreach ($paramNames as $name) {
88                 if (!array_key_exists($name, $paramPatterns) &&
89                     array_key_exists($name, $args)) {
90                     $paramPatterns[$name] = $args[$name];
91                     unset($args[$name]);
92                 }
93             }
94
95             $regex = self::makeRegex($path, $paramPatterns);
96
97             $this->variables[] = array($args, $regex, $paramNames);
98
99             $format = $this->makeFormat($path, $paramPatterns);
100
101             if (array_key_exists($action, $this->reverse)) {
102                 $this->reverse[$args[self::ACTION]][] = array($args, $format, $paramNames);
103             } else {
104                 $this->reverse[$args[self::ACTION]] = array(array($args, $format, $paramNames));
105             }
106         }
107     }
108
109     function match($path)
110     {
111         if (array_key_exists($path, $this->statics)) {
112             return $this->statics[$path];
113         }
114
115         foreach ($this->variables as $pattern) {
116             list($args, $regex, $paramNames) = $pattern;
117             if (preg_match($regex, $path, $match)) {
118                 $results = $args;
119                 foreach ($paramNames as $name) {
120                     $results[$name] = $match[$name];
121                 }
122                 return $results;
123             }
124         }
125
126         throw new Exception(sprintf('No match for path "%s"', $path));
127     }
128
129     function generate($args, $qstring, $fragment)
130     {
131         if (!array_key_exists(self::ACTION, $args)) {
132             throw new Exception("Every path needs an action.");
133         }
134
135         $action = $args[self::ACTION];
136
137         if (!array_key_exists($action, $this->reverse)) {
138             throw new Exception(sprintf('No candidate paths for action "%s"', $action));
139         }
140
141         $candidates = $this->reverse[$action];
142
143         foreach ($candidates as $candidate) {
144             if (count($candidate) == 2) { // static
145                 list($tryArgs, $tryPath) = $candidate;
146                 foreach ($tryArgs as $key => $value) {
147                     if (!array_key_exists($key, $args) || $args[$key] != $value) {
148                         // next candidate
149                         continue 2;
150                     }
151                 }
152                 // success
153                 $path = $tryPath;
154             } else {
155                 list($tryArgs, $format, $paramNames) = $candidate;
156
157                 foreach ($tryArgs as $key => $value) {
158                     if (!array_key_exists($key, $args) || $args[$key] != $value) {
159                         // next candidate
160                         continue 2;
161                     }
162                 }
163
164                 // success
165
166                 $toFormat = array();
167
168                 foreach ($paramNames as $name) {
169                     if (!array_key_exists($name, $args)) {
170                         // next candidate
171                         continue 2;
172                     }
173                     $toFormat[] = $args[$name];
174                 }
175
176                 $path = vsprintf($format, $toFormat);
177             }
178
179             if (!empty($qstring)) {
180                 $formatted = http_build_query($qstring);
181                 $path .= '?' . $formatted;
182             }
183
184             return $path;
185         }
186
187         // failure; some reporting twiddles
188
189         unset($args['action']);
190
191         if (empty($args)) {
192             throw new Exception(sprintf('No matches for action "%s"', $action));
193         }
194
195         $argstring = '';
196
197         foreach ($args as $key => $value) {
198             $argstring .= "$key=$value ";
199         }
200
201         throw new Exception(sprintf('No matches for action "%s" with arguments "%s"', $action, $argstring));
202     }
203
204     protected function getParamNames($path)
205     {
206         preg_match_all('/:(?P<name>\w+)/', $path, $match);
207         return $match['name'];
208     }
209
210     static function makeRegex($path, $paramPatterns)
211     {
212         $pr = new PatternReplacer($paramPatterns);
213
214         $regex = preg_replace_callback('/:(\w+)/',
215                                        array($pr, 'toPattern'),
216                                        $path);
217
218         $regex = '#^' . str_replace('#', '\#', $regex) . '$#';
219
220         return $regex;
221     }
222
223     protected function makeFormat($path, $paramPatterns)
224     {
225         $format = preg_replace('/(:\w+)/', '%s', $path);
226
227         return $format;
228     }
229
230     public function getPaths()
231     {
232         return $this->allpaths;
233     }
234 }
235
236 class PatternReplacer
237 {
238     private $patterns;
239
240     function __construct($patterns)
241     {
242         $this->patterns = $patterns;
243     }
244
245     function toPattern($matches)
246     {
247         // trim out the :
248         $name = $matches[1];
249         if (array_key_exists($name, $this->patterns)) {
250             $pattern = $this->patterns[$name];
251         } else {
252             // ???
253             $pattern = '\w+';
254         }
255         return '(?P<'.$name.'>'.$pattern.')';
256     }
257 }