]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/router.php
Merge branch 'master' into 0.9.x
[quix0rs-gnu-social.git] / lib / router.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * URL routing utilities
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  URL
23  * @package   StatusNet
24  * @author    Evan Prodromou <evan@status.net>
25  * @copyright 2009 StatusNet, Inc.
26  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
27  * @link      http://status.net/
28  */
29
30 if (!defined('STATUSNET') && !defined('LACONICA')) {
31     exit(1);
32 }
33
34 require_once 'Net/URL/Mapper.php';
35
36 class StatusNet_URL_Mapper extends Net_URL_Mapper
37 {
38     private static $_singleton = null;
39     private $_actionToPath = array();
40
41     private function __construct()
42     {
43     }
44     
45     public static function getInstance($id = '__default__')
46     {
47         if (empty(self::$_singleton)) {
48             self::$_singleton = new StatusNet_URL_Mapper();
49         }
50         return self::$_singleton;
51     }
52
53     public function connect($path, $defaults = array(), $rules = array())
54     {
55         $result = null;
56         if (Event::handle('StartConnectPath', array(&$path, &$defaults, &$rules, &$result))) {
57             $result = parent::connect($path, $defaults, $rules);
58             if (array_key_exists('action', $defaults)) {
59                 $action = $defaults['action'];
60             } elseif (array_key_exists('action', $rules)) {
61                 $action = $rules['action'];
62             } else {
63                 $action = null;
64             }
65             $this->_mapAction($action, $result);
66             Event::handle('EndConnectPath', array($path, $defaults, $rules, $result));
67         }
68         return $result;
69     }
70     
71     protected function _mapAction($action, $path)
72     {
73         if (!array_key_exists($action, $this->_actionToPath)) {
74             $this->_actionToPath[$action] = array();
75         }
76         $this->_actionToPath[$action][] = $path;
77         return;
78     }
79     
80     public function generate($values = array(), $qstring = array(), $anchor = '')
81     {
82         if (!array_key_exists('action', $values)) {
83             return parent::generate($values, $qstring, $anchor);
84         }
85         
86         $action = $values['action'];
87
88         if (!array_key_exists($action, $this->_actionToPath)) {
89             return parent::generate($values, $qstring, $anchor);
90         }
91         
92         $oldPaths    = $this->paths;
93         $this->paths = $this->_actionToPath[$action];
94         $result      = parent::generate($values, $qstring, $anchor);
95         $this->paths = $oldPaths;
96
97         return $result;
98     }
99 }
100
101 /**
102  * URL Router
103  *
104  * Cheap wrapper around Net_URL_Mapper
105  *
106  * @category URL
107  * @package  StatusNet
108  * @author   Evan Prodromou <evan@status.net>
109  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
110  * @link     http://status.net/
111  */
112 class Router
113 {
114     var $m = null;
115     static $inst = null;
116     static $bare = array('requesttoken', 'accesstoken', 'userauthorization',
117                          'postnotice', 'updateprofile', 'finishremotesubscribe');
118
119     const REGEX_TAG = '[^\/]+'; // [\pL\pN_\-\.]{1,64} better if we can do unicode regexes
120
121     static function get()
122     {
123         if (!Router::$inst) {
124             Router::$inst = new Router();
125         }
126         return Router::$inst;
127     }
128
129     /**
130      * Clear the global singleton instance for this class.
131      * Needed to ensure reset when switching site configurations.
132      */
133     static function clear()
134     {
135         Router::$inst = null;
136     }
137
138     function __construct()
139     {
140         if (empty($this->m)) {
141             if (!common_config('router', 'cache')) {
142                 $this->m = $this->initialize();
143             } else {
144                 $k = self::cacheKey();
145                 $c = Cache::instance();
146                 $m = $c->get($k);
147                 if (!empty($m)) {
148                     $this->m = $m;
149                 } else {
150                     $this->m = $this->initialize();
151                     $c->set($k, $this->m);
152                 }
153             }
154         }
155     }
156
157     /**
158      * Create a unique hashkey for the router.
159      * 
160      * The router's url map can change based on the version of the software
161      * you're running and the plugins that are enabled. To avoid having bad routes
162      * get stuck in the cache, the key includes a list of plugins and the software
163      * version.
164      * 
165      * There can still be problems with a) differences in versions of the plugins and 
166      * b) people running code between official versions, but these tend to be more
167      * sophisticated users who can grok what's going on and clear their caches.
168      * 
169      * @return string cache key string that should uniquely identify a router
170      */
171     
172     static function cacheKey()
173     {
174         $parts = array('router');
175
176         // Many router paths depend on this setting.
177         if (common_config('singleuser', 'enabled')) {
178             $parts[] = '1user';
179         } else {
180             $parts[] = 'multi';
181         }
182
183         return Cache::codeKey(implode(':', $parts));
184     }
185     
186     function initialize()
187     {
188         $m = StatusNet_URL_Mapper::getInstance();
189
190         if (Event::handle('StartInitializeRouter', array(&$m))) {
191
192             $m->connect('robots.txt', array('action' => 'robotstxt'));
193
194             $m->connect('opensearch/people', array('action' => 'opensearch',
195                                                    'type' => 'people'));
196             $m->connect('opensearch/notice', array('action' => 'opensearch',
197                                                    'type' => 'notice'));
198
199             // docs
200
201             $m->connect('doc/:title', array('action' => 'doc'));
202
203             $m->connect('main/otp/:user_id/:token',
204                         array('action' => 'otp'),
205                         array('user_id' => '[0-9]+',
206                               'token' => '.+'));
207
208             // main stuff is repetitive
209
210             $main = array('login', 'logout', 'register', 'subscribe',
211                           'unsubscribe', 'confirmaddress', 'recoverpassword',
212                           'invite', 'favor', 'disfavor', 'sup',
213                           'block', 'unblock', 'subedit',
214                           'groupblock', 'groupunblock',
215                           'sandbox', 'unsandbox',
216                           'silence', 'unsilence',
217                           'grantrole', 'revokerole',
218                           'repeat',
219                           'deleteuser',
220                           'geocode',
221                           'version',
222                           'backupaccount',
223                           'deleteaccount',
224                           'restoreaccount',
225             );
226
227             foreach ($main as $a) {
228                 $m->connect('main/'.$a, array('action' => $a));
229             }
230
231             // Also need a block variant accepting ID on URL for mail links
232             $m->connect('main/block/:profileid',
233                         array('action' => 'block'),
234                         array('profileid' => '[0-9]+'));
235
236             $m->connect('main/sup/:seconds', array('action' => 'sup'),
237                         array('seconds' => '[0-9]+'));
238
239             $m->connect('main/tagother/:id', array('action' => 'tagother'));
240
241             $m->connect('main/oembed',
242                         array('action' => 'oembed'));
243
244             $m->connect('main/xrds',
245                         array('action' => 'publicxrds'));
246             $m->connect('.well-known/host-meta',
247                         array('action' => 'hostmeta'));
248             $m->connect('main/xrd',
249                         array('action' => 'userxrd'));
250
251             // these take a code
252
253             foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
254                 $m->connect('main/'.$c.'/:code', array('action' => $c));
255             }
256
257             // exceptional
258
259             $m->connect('main/remote', array('action' => 'remotesubscribe'));
260             $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+'));
261
262             foreach (Router::$bare as $action) {
263                 $m->connect('index.php?action=' . $action, array('action' => $action));
264             }
265
266             // settings
267
268             foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections',
269                            'oauthapps', 'email', 'sms', 'userdesign', 'other') as $s) {
270                 $m->connect('settings/'.$s, array('action' => $s.'settings'));
271             }
272
273             $m->connect('settings/oauthapps/show/:id',
274                         array('action' => 'showapplication'),
275                         array('id' => '[0-9]+')
276             );
277             $m->connect('settings/oauthapps/new',
278                         array('action' => 'newapplication')
279             );
280             $m->connect('settings/oauthapps/edit/:id',
281                         array('action' => 'editapplication'),
282                         array('id' => '[0-9]+')
283             );
284             $m->connect('settings/oauthapps/delete/:id',
285                         array('action' => 'deleteapplication'),
286                         array('id' => '[0-9]+')
287             );
288
289             // search
290
291             foreach (array('group', 'people', 'notice') as $s) {
292                 $m->connect('search/'.$s, array('action' => $s.'search'));
293                 $m->connect('search/'.$s.'?q=:q',
294                             array('action' => $s.'search'),
295                             array('q' => '.+'));
296             }
297
298             // The second of these is needed to make the link work correctly
299             // when inserted into the page. The first is needed to match the
300             // route on the way in. Seems to be another Net_URL_Mapper bug to me.
301             $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
302             $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
303                         array('q' => '.+'));
304
305             $m->connect('attachment/:attachment',
306                         array('action' => 'attachment'),
307                         array('attachment' => '[0-9]+'));
308
309             $m->connect('attachment/:attachment/ajax',
310                         array('action' => 'attachment_ajax'),
311                         array('attachment' => '[0-9]+'));
312
313             $m->connect('attachment/:attachment/thumbnail',
314                         array('action' => 'attachment_thumbnail'),
315                         array('attachment' => '[0-9]+'));
316
317             $m->connect('notice/new', array('action' => 'newnotice'));
318             $m->connect('notice/new?replyto=:replyto',
319                         array('action' => 'newnotice'),
320                         array('replyto' => Nickname::DISPLAY_FMT));
321             $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
322                         array('action' => 'newnotice'),
323                         array('replyto' => Nickname::DISPLAY_FMT),
324                         array('inreplyto' => '[0-9]+'));
325
326             $m->connect('notice/:notice/file',
327                         array('action' => 'file'),
328                         array('notice' => '[0-9]+'));
329
330             $m->connect('notice/:notice',
331                         array('action' => 'shownotice'),
332                         array('notice' => '[0-9]+'));
333             $m->connect('notice/delete', array('action' => 'deletenotice'));
334             $m->connect('notice/delete/:notice',
335                         array('action' => 'deletenotice'),
336                         array('notice' => '[0-9]+'));
337
338             $m->connect('bookmarklet/new', array('action' => 'bookmarklet'));
339
340             // conversation
341
342             $m->connect('conversation/:id',
343                         array('action' => 'conversation'),
344                         array('id' => '[0-9]+'));
345
346             $m->connect('message/new', array('action' => 'newmessage'));
347             $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => Nickname::DISPLAY_FMT));
348             $m->connect('message/:message',
349                         array('action' => 'showmessage'),
350                         array('message' => '[0-9]+'));
351
352             $m->connect('user/:id',
353                         array('action' => 'userbyid'),
354                         array('id' => '[0-9]+'));
355
356             $m->connect('tags/', array('action' => 'publictagcloud'));
357             $m->connect('tag/', array('action' => 'publictagcloud'));
358             $m->connect('tags', array('action' => 'publictagcloud'));
359             $m->connect('tag', array('action' => 'publictagcloud'));
360             $m->connect('tag/:tag/rss',
361                         array('action' => 'tagrss'),
362                         array('tag' => self::REGEX_TAG));
363             $m->connect('tag/:tag',
364                         array('action' => 'tag'),
365                         array('tag' => self::REGEX_TAG));
366
367             $m->connect('peopletag/:tag',
368                         array('action' => 'peopletag'),
369                         array('tag' => self::REGEX_TAG));
370
371             // groups
372
373             $m->connect('group/new', array('action' => 'newgroup'));
374
375             foreach (array('edit', 'join', 'leave', 'delete') as $v) {
376                 $m->connect('group/:nickname/'.$v,
377                             array('action' => $v.'group'),
378                             array('nickname' => Nickname::DISPLAY_FMT));
379                 $m->connect('group/:id/id/'.$v,
380                             array('action' => $v.'group'),
381                             array('id' => '[0-9]+'));
382             }
383
384             foreach (array('members', 'logo', 'rss', 'designsettings') as $n) {
385                 $m->connect('group/:nickname/'.$n,
386                             array('action' => 'group'.$n),
387                             array('nickname' => Nickname::DISPLAY_FMT));
388             }
389
390             $m->connect('group/:nickname/foaf',
391                         array('action' => 'foafgroup'),
392                         array('nickname' => Nickname::DISPLAY_FMT));
393
394             $m->connect('group/:nickname/blocked',
395                         array('action' => 'blockedfromgroup'),
396                         array('nickname' => Nickname::DISPLAY_FMT));
397
398             $m->connect('group/:nickname/makeadmin',
399                         array('action' => 'makeadmin'),
400                         array('nickname' => Nickname::DISPLAY_FMT));
401
402             $m->connect('group/:id/id',
403                         array('action' => 'groupbyid'),
404                         array('id' => '[0-9]+'));
405
406             $m->connect('group/:nickname',
407                         array('action' => 'showgroup'),
408                         array('nickname' => Nickname::DISPLAY_FMT));
409
410             $m->connect('group/', array('action' => 'groups'));
411             $m->connect('group', array('action' => 'groups'));
412             $m->connect('groups/', array('action' => 'groups'));
413             $m->connect('groups', array('action' => 'groups'));
414
415             // Twitter-compatible API
416
417             // statuses API
418
419             $m->connect('api/statuses/public_timeline.:format',
420                         array('action' => 'ApiTimelinePublic',
421                               'format' => '(xml|json|rss|atom|as)'));
422
423             $m->connect('api/statuses/friends_timeline.:format',
424                         array('action' => 'ApiTimelineFriends',
425                               'format' => '(xml|json|rss|atom|as)'));
426
427             $m->connect('api/statuses/friends_timeline/:id.:format',
428                         array('action' => 'ApiTimelineFriends',
429                               'id' => Nickname::INPUT_FMT,
430                               'format' => '(xml|json|rss|atom|as)'));
431
432             $m->connect('api/statuses/home_timeline.:format',
433                         array('action' => 'ApiTimelineHome',
434                               'format' => '(xml|json|rss|atom|as)'));
435
436             $m->connect('api/statuses/home_timeline/:id.:format',
437                         array('action' => 'ApiTimelineHome',
438                               'id' => Nickname::INPUT_FMT,
439                               'format' => '(xml|json|rss|atom|as)'));
440
441             $m->connect('api/statuses/user_timeline.:format',
442                         array('action' => 'ApiTimelineUser',
443                               'format' => '(xml|json|rss|atom|as)'));
444
445             $m->connect('api/statuses/user_timeline/:id.:format',
446                         array('action' => 'ApiTimelineUser',
447                               'id' => Nickname::INPUT_FMT,
448                               'format' => '(xml|json|rss|atom|as)'));
449
450             $m->connect('api/statuses/mentions.:format',
451                         array('action' => 'ApiTimelineMentions',
452                               'format' => '(xml|json|rss|atom|as)'));
453
454             $m->connect('api/statuses/mentions/:id.:format',
455                         array('action' => 'ApiTimelineMentions',
456                               'id' => Nickname::INPUT_FMT,
457                               'format' => '(xml|json|rss|atom|as)'));
458
459             $m->connect('api/statuses/replies.:format',
460                         array('action' => 'ApiTimelineMentions',
461                               'format' => '(xml|json|rss|atom|as)'));
462
463             $m->connect('api/statuses/replies/:id.:format',
464                         array('action' => 'ApiTimelineMentions',
465                               'id' => Nickname::INPUT_FMT,
466                               'format' => '(xml|json|rss|atom|as)'));
467
468             $m->connect('api/statuses/retweeted_by_me.:format',
469                         array('action' => 'ApiTimelineRetweetedByMe',
470                               'format' => '(xml|json|atom|as)'));
471
472             $m->connect('api/statuses/retweeted_to_me.:format',
473                         array('action' => 'ApiTimelineRetweetedToMe',
474                               'format' => '(xml|json|atom|as)'));
475
476             $m->connect('api/statuses/retweets_of_me.:format',
477                         array('action' => 'ApiTimelineRetweetsOfMe',
478                               'format' => '(xml|json|atom|as)'));
479
480             $m->connect('api/statuses/friends.:format',
481                         array('action' => 'ApiUserFriends',
482                               'format' => '(xml|json)'));
483
484             $m->connect('api/statuses/friends/:id.:format',
485                         array('action' => 'ApiUserFriends',
486                               'id' => Nickname::INPUT_FMT,
487                               'format' => '(xml|json)'));
488
489             $m->connect('api/statuses/followers.:format',
490                         array('action' => 'ApiUserFollowers',
491                               'format' => '(xml|json)'));
492
493             $m->connect('api/statuses/followers/:id.:format',
494                         array('action' => 'ApiUserFollowers',
495                               'id' => Nickname::INPUT_FMT,
496                               'format' => '(xml|json)'));
497
498             $m->connect('api/statuses/show.:format',
499                         array('action' => 'ApiStatusesShow',
500                               'format' => '(xml|json|atom)'));
501
502             $m->connect('api/statuses/show/:id.:format',
503                         array('action' => 'ApiStatusesShow',
504                               'id' => '[0-9]+',
505                               'format' => '(xml|json|atom)'));
506
507             $m->connect('api/statuses/update.:format',
508                         array('action' => 'ApiStatusesUpdate',
509                               'format' => '(xml|json)'));
510
511             $m->connect('api/statuses/destroy.:format',
512                         array('action' => 'ApiStatusesDestroy',
513                               'format' => '(xml|json)'));
514
515             $m->connect('api/statuses/destroy/:id.:format',
516                         array('action' => 'ApiStatusesDestroy',
517                               'id' => '[0-9]+',
518                               'format' => '(xml|json)'));
519
520             $m->connect('api/statuses/retweet/:id.:format',
521                         array('action' => 'ApiStatusesRetweet',
522                               'id' => '[0-9]+',
523                               'format' => '(xml|json)'));
524
525             $m->connect('api/statuses/retweets/:id.:format',
526                         array('action' => 'ApiStatusesRetweets',
527                               'id' => '[0-9]+',
528                               'format' => '(xml|json)'));
529
530             // users
531
532             $m->connect('api/users/show.:format',
533                         array('action' => 'ApiUserShow',
534                               'format' => '(xml|json)'));
535
536             $m->connect('api/users/show/:id.:format',
537                         array('action' => 'ApiUserShow',
538                               'id' => Nickname::INPUT_FMT,
539                               'format' => '(xml|json)'));
540
541             $m->connect('api/users/profile_image/:screen_name.:format',
542                         array('action' => 'ApiUserProfileImage',
543                               'screen_name' => Nickname::DISPLAY_FMT,
544                               'format' => '(xml|json)'));
545
546             // direct messages
547
548             $m->connect('api/direct_messages.:format',
549                         array('action' => 'ApiDirectMessage',
550                               'format' => '(xml|json|rss|atom)'));
551
552             $m->connect('api/direct_messages/sent.:format',
553                         array('action' => 'ApiDirectMessage',
554                               'format' => '(xml|json|rss|atom)',
555                               'sent' => true));
556
557             $m->connect('api/direct_messages/new.:format',
558                         array('action' => 'ApiDirectMessageNew',
559                               'format' => '(xml|json)'));
560
561             // friendships
562
563             $m->connect('api/friendships/show.:format',
564                         array('action' => 'ApiFriendshipsShow',
565                               'format' => '(xml|json)'));
566
567             $m->connect('api/friendships/exists.:format',
568                         array('action' => 'ApiFriendshipsExists',
569                               'format' => '(xml|json)'));
570
571             $m->connect('api/friendships/create.:format',
572                         array('action' => 'ApiFriendshipsCreate',
573                               'format' => '(xml|json)'));
574
575             $m->connect('api/friendships/destroy.:format',
576                         array('action' => 'ApiFriendshipsDestroy',
577                               'format' => '(xml|json)'));
578
579             $m->connect('api/friendships/create/:id.:format',
580                         array('action' => 'ApiFriendshipsCreate',
581                               'id' => Nickname::INPUT_FMT,
582                               'format' => '(xml|json)'));
583
584             $m->connect('api/friendships/destroy/:id.:format',
585                         array('action' => 'ApiFriendshipsDestroy',
586                               'id' => Nickname::INPUT_FMT,
587                               'format' => '(xml|json)'));
588
589             // Social graph
590
591             $m->connect('api/friends/ids/:id.:format',
592                         array('action' => 'ApiUserFriends',
593                               'ids_only' => true));
594
595             $m->connect('api/followers/ids/:id.:format',
596                         array('action' => 'ApiUserFollowers',
597                               'ids_only' => true));
598
599             $m->connect('api/friends/ids.:format',
600                         array('action' => 'ApiUserFriends',
601                               'ids_only' => true));
602
603             $m->connect('api/followers/ids.:format',
604                         array('action' => 'ApiUserFollowers',
605                               'ids_only' => true));
606
607             // account
608
609             $m->connect('api/account/verify_credentials.:format',
610                         array('action' => 'ApiAccountVerifyCredentials'));
611
612             $m->connect('api/account/update_profile.:format',
613                         array('action' => 'ApiAccountUpdateProfile'));
614
615             $m->connect('api/account/update_profile_image.:format',
616                         array('action' => 'ApiAccountUpdateProfileImage'));
617
618             $m->connect('api/account/update_profile_background_image.:format',
619                         array('action' => 'ApiAccountUpdateProfileBackgroundImage'));
620
621             $m->connect('api/account/update_profile_colors.:format',
622                         array('action' => 'ApiAccountUpdateProfileColors'));
623
624             $m->connect('api/account/update_delivery_device.:format',
625                         array('action' => 'ApiAccountUpdateDeliveryDevice'));
626
627             // special case where verify_credentials is called w/out a format
628
629             $m->connect('api/account/verify_credentials',
630                         array('action' => 'ApiAccountVerifyCredentials'));
631
632             $m->connect('api/account/rate_limit_status.:format',
633                         array('action' => 'ApiAccountRateLimitStatus'));
634
635             // favorites
636
637             $m->connect('api/favorites.:format',
638                         array('action' => 'ApiTimelineFavorites',
639                               'format' => '(xml|json|rss|atom|as)'));
640
641             $m->connect('api/favorites/:id.:format',
642                         array('action' => 'ApiTimelineFavorites',
643                               'id' => Nickname::INPUT_FMT,
644                               'format' => '(xml|json|rss|atom|as)'));
645
646             $m->connect('api/favorites/create/:id.:format',
647                         array('action' => 'ApiFavoriteCreate',
648                               'id' => '[0-9]+',
649                               'format' => '(xml|json)'));
650
651             $m->connect('api/favorites/destroy/:id.:format',
652                         array('action' => 'ApiFavoriteDestroy',
653                               'id' => '[0-9]+',
654                               'format' => '(xml|json)'));
655             // blocks
656
657             $m->connect('api/blocks/create.:format',
658                         array('action' => 'ApiBlockCreate',
659                               'format' => '(xml|json)'));
660
661             $m->connect('api/blocks/create/:id.:format',
662                         array('action' => 'ApiBlockCreate',
663                               'id' => Nickname::INPUT_FMT,
664                               'format' => '(xml|json)'));
665
666             $m->connect('api/blocks/destroy.:format',
667                         array('action' => 'ApiBlockDestroy',
668                               'format' => '(xml|json)'));
669
670             $m->connect('api/blocks/destroy/:id.:format',
671                         array('action' => 'ApiBlockDestroy',
672                               'id' => Nickname::INPUT_FMT,
673                               'format' => '(xml|json)'));
674             // help
675
676             $m->connect('api/help/test.:format',
677                         array('action' => 'ApiHelpTest',
678                               'format' => '(xml|json)'));
679
680             // statusnet
681
682             $m->connect('api/statusnet/version.:format',
683                         array('action' => 'ApiStatusnetVersion',
684                               'format' => '(xml|json)'));
685
686             $m->connect('api/statusnet/config.:format',
687                         array('action' => 'ApiStatusnetConfig',
688                               'format' => '(xml|json)'));
689
690             // For older methods, we provide "laconica" base action
691
692             $m->connect('api/laconica/version.:format',
693                         array('action' => 'ApiStatusnetVersion',
694                               'format' => '(xml|json)'));
695
696             $m->connect('api/laconica/config.:format',
697                         array('action' => 'ApiStatusnetConfig',
698                               'format' => '(xml|json)'));
699
700             // Groups and tags are newer than 0.8.1 so no backward-compatibility
701             // necessary
702
703             // Groups
704             //'list' has to be handled differently, as php will not allow a method to be named 'list'
705
706             $m->connect('api/statusnet/groups/timeline/:id.:format',
707                         array('action' => 'ApiTimelineGroup',
708                               'id' => Nickname::INPUT_FMT,
709                               'format' => '(xml|json|rss|atom|as)'));
710
711             $m->connect('api/statusnet/groups/show.:format',
712                         array('action' => 'ApiGroupShow',
713                               'format' => '(xml|json)'));
714
715             $m->connect('api/statusnet/groups/show/:id.:format',
716                         array('action' => 'ApiGroupShow',
717                               'id' => Nickname::INPUT_FMT,
718                               'format' => '(xml|json)'));
719
720             $m->connect('api/statusnet/groups/join.:format',
721                         array('action' => 'ApiGroupJoin',
722                               'id' => Nickname::INPUT_FMT,
723                               'format' => '(xml|json)'));
724
725             $m->connect('api/statusnet/groups/join/:id.:format',
726                         array('action' => 'ApiGroupJoin',
727                               'format' => '(xml|json)'));
728
729             $m->connect('api/statusnet/groups/leave.:format',
730                         array('action' => 'ApiGroupLeave',
731                               'id' => Nickname::INPUT_FMT,
732                               'format' => '(xml|json)'));
733
734             $m->connect('api/statusnet/groups/leave/:id.:format',
735                         array('action' => 'ApiGroupLeave',
736                               'format' => '(xml|json)'));
737
738             $m->connect('api/statusnet/groups/is_member.:format',
739                         array('action' => 'ApiGroupIsMember',
740                               'format' => '(xml|json)'));
741
742             $m->connect('api/statusnet/groups/list.:format',
743                         array('action' => 'ApiGroupList',
744                               'format' => '(xml|json|rss|atom)'));
745
746             $m->connect('api/statusnet/groups/list/:id.:format',
747                         array('action' => 'ApiGroupList',
748                               'id' => Nickname::INPUT_FMT,
749                               'format' => '(xml|json|rss|atom)'));
750
751             $m->connect('api/statusnet/groups/list_all.:format',
752                         array('action' => 'ApiGroupListAll',
753                               'format' => '(xml|json|rss|atom)'));
754
755             $m->connect('api/statusnet/groups/membership.:format',
756                         array('action' => 'ApiGroupMembership',
757                               'format' => '(xml|json)'));
758
759             $m->connect('api/statusnet/groups/membership/:id.:format',
760                         array('action' => 'ApiGroupMembership',
761                               'id' => Nickname::INPUT_FMT,
762                               'format' => '(xml|json)'));
763
764             $m->connect('api/statusnet/groups/create.:format',
765                         array('action' => 'ApiGroupCreate',
766                               'format' => '(xml|json)'));
767             // Tags
768             $m->connect('api/statusnet/tags/timeline/:tag.:format',
769                         array('action' => 'ApiTimelineTag',
770                               'format' => '(xml|json|rss|atom|as)'));
771
772             // media related
773             $m->connect(
774                 'api/statusnet/media/upload',
775                 array('action' => 'ApiMediaUpload')
776             );
777
778             // search
779             $m->connect('api/search.atom', array('action' => 'ApiSearchAtom'));
780             $m->connect('api/search.json', array('action' => 'ApiSearchJSON'));
781             $m->connect('api/trends.json', array('action' => 'ApiTrends'));
782
783             $m->connect('api/oauth/request_token',
784                         array('action' => 'ApiOauthRequestToken'));
785
786             $m->connect('api/oauth/access_token',
787                         array('action' => 'ApiOauthAccessToken'));
788
789             $m->connect('api/oauth/authorize',
790                         array('action' => 'ApiOauthAuthorize'));
791
792             // Admin
793
794             $m->connect('admin/site', array('action' => 'siteadminpanel'));
795             $m->connect('admin/design', array('action' => 'designadminpanel'));
796             $m->connect('admin/user', array('action' => 'useradminpanel'));
797                 $m->connect('admin/access', array('action' => 'accessadminpanel'));
798             $m->connect('admin/paths', array('action' => 'pathsadminpanel'));
799             $m->connect('admin/sessions', array('action' => 'sessionsadminpanel'));
800             $m->connect('admin/sitenotice', array('action' => 'sitenoticeadminpanel'));
801             $m->connect('admin/snapshot', array('action' => 'snapshotadminpanel'));
802             $m->connect('admin/license', array('action' => 'licenseadminpanel'));
803
804             $m->connect('getfile/:filename',
805                         array('action' => 'getfile'),
806                         array('filename' => '[A-Za-z0-9._-]+'));
807
808             // In the "root"
809
810             if (common_config('singleuser', 'enabled')) {
811
812                 $nickname = User::singleUserNickname();
813
814                 foreach (array('subscriptions', 'subscribers',
815                                'all', 'foaf', 'xrds',
816                                'replies', 'microsummary', 'hcard') as $a) {
817                     $m->connect($a,
818                                 array('action' => $a,
819                                       'nickname' => $nickname));
820                 }
821
822                 foreach (array('subscriptions', 'subscribers') as $a) {
823                     $m->connect($a.'/:tag',
824                                 array('action' => $a,
825                                       'nickname' => $nickname),
826                                 array('tag' => self::REGEX_TAG));
827                 }
828
829                 foreach (array('rss', 'groups') as $a) {
830                     $m->connect($a,
831                                 array('action' => 'user'.$a,
832                                       'nickname' => $nickname));
833                 }
834
835                 foreach (array('all', 'replies', 'favorites') as $a) {
836                     $m->connect($a.'/rss',
837                                 array('action' => $a.'rss',
838                                       'nickname' => $nickname));
839                 }
840
841                 $m->connect('favorites',
842                             array('action' => 'showfavorites',
843                                   'nickname' => $nickname));
844
845                 $m->connect('avatar/:size',
846                             array('action' => 'avatarbynickname',
847                                   'nickname' => $nickname),
848                             array('size' => '(original|96|48|24)'));
849
850                 $m->connect('tag/:tag/rss',
851                             array('action' => 'userrss',
852                                   'nickname' => $nickname),
853                             array('tag' => self::REGEX_TAG));
854
855                 $m->connect('tag/:tag',
856                             array('action' => 'showstream',
857                                   'nickname' => $nickname),
858                             array('tag' => self::REGEX_TAG));
859
860                 $m->connect('rsd.xml',
861                             array('action' => 'rsd',
862                                   'nickname' => $nickname));
863
864                 $m->connect('',
865                             array('action' => 'showstream',
866                                   'nickname' => $nickname));
867             } else {
868                 $m->connect('', array('action' => 'public'));
869                 $m->connect('rss', array('action' => 'publicrss'));
870                 $m->connect('featuredrss', array('action' => 'featuredrss'));
871                 $m->connect('favoritedrss', array('action' => 'favoritedrss'));
872                 $m->connect('featured/', array('action' => 'featured'));
873                 $m->connect('featured', array('action' => 'featured'));
874                 $m->connect('favorited/', array('action' => 'favorited'));
875                 $m->connect('favorited', array('action' => 'favorited'));
876                 $m->connect('rsd.xml', array('action' => 'rsd'));
877
878                 foreach (array('subscriptions', 'subscribers',
879                                'nudge', 'all', 'foaf', 'xrds',
880                                'replies', 'inbox', 'outbox', 'microsummary', 'hcard') as $a) {
881                     $m->connect(':nickname/'.$a,
882                                 array('action' => $a),
883                                 array('nickname' => Nickname::DISPLAY_FMT));
884                 }
885
886                 foreach (array('subscriptions', 'subscribers') as $a) {
887                     $m->connect(':nickname/'.$a.'/:tag',
888                                 array('action' => $a),
889                                 array('tag' => self::REGEX_TAG,
890                                       'nickname' => Nickname::DISPLAY_FMT));
891                 }
892
893                 foreach (array('rss', 'groups') as $a) {
894                     $m->connect(':nickname/'.$a,
895                                 array('action' => 'user'.$a),
896                                 array('nickname' => Nickname::DISPLAY_FMT));
897                 }
898
899                 foreach (array('all', 'replies', 'favorites') as $a) {
900                     $m->connect(':nickname/'.$a.'/rss',
901                                 array('action' => $a.'rss'),
902                                 array('nickname' => Nickname::DISPLAY_FMT));
903                 }
904
905                 $m->connect(':nickname/favorites',
906                             array('action' => 'showfavorites'),
907                             array('nickname' => Nickname::DISPLAY_FMT));
908
909                 $m->connect(':nickname/avatar/:size',
910                             array('action' => 'avatarbynickname'),
911                             array('size' => '(original|96|48|24)',
912                                   'nickname' => Nickname::DISPLAY_FMT));
913
914                 $m->connect(':nickname/tag/:tag/rss',
915                             array('action' => 'userrss'),
916                             array('nickname' => Nickname::DISPLAY_FMT),
917                             array('tag' => self::REGEX_TAG));
918
919                 $m->connect(':nickname/tag/:tag',
920                             array('action' => 'showstream'),
921                             array('nickname' => Nickname::DISPLAY_FMT),
922                             array('tag' => self::REGEX_TAG));
923
924                 $m->connect(':nickname/rsd.xml',
925                             array('action' => 'rsd'),
926                             array('nickname' => Nickname::DISPLAY_FMT));
927
928                 $m->connect(':nickname',
929                             array('action' => 'showstream'),
930                             array('nickname' => Nickname::DISPLAY_FMT));
931             }
932
933             // AtomPub API
934
935             $m->connect('api/statusnet/app/service/:id.xml',
936                         array('action' => 'ApiAtomService'),
937                         array('id' => Nickname::DISPLAY_FMT));
938
939             $m->connect('api/statusnet/app/service.xml',
940                         array('action' => 'ApiAtomService'));
941
942             $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom',
943                         array('action' => 'AtomPubShowSubscription'),
944                         array('subscriber' => '[0-9]+',
945                               'subscribed' => '[0-9]+'));
946
947             $m->connect('api/statusnet/app/subscriptions/:subscriber.atom',
948                         array('action' => 'AtomPubSubscriptionFeed'),
949                         array('subscriber' => '[0-9]+'));
950
951             $m->connect('api/statusnet/app/favorites/:profile/:notice.atom',
952                         array('action' => 'AtomPubShowFavorite'),
953                         array('profile' => '[0-9]+',
954                               'notice' => '[0-9]+'));
955
956             $m->connect('api/statusnet/app/favorites/:profile.atom',
957                         array('action' => 'AtomPubFavoriteFeed'),
958                         array('profile' => '[0-9]+'));
959
960             $m->connect('api/statusnet/app/memberships/:profile/:group.atom',
961                         array('action' => 'AtomPubShowMembership'),
962                         array('profile' => '[0-9]+',
963                               'group' => '[0-9]+'));
964
965             $m->connect('api/statusnet/app/memberships/:profile.atom',
966                         array('action' => 'AtomPubMembershipFeed'),
967                         array('profile' => '[0-9]+'));
968
969             // user stuff
970
971             Event::handle('RouterInitialized', array($m));
972         }
973
974         return $m;
975     }
976
977     function map($path)
978     {
979         try {
980             $match = $this->m->match($path);
981         } catch (Net_URL_Mapper_InvalidException $e) {
982             common_log(LOG_ERR, "Problem getting route for $path - " .
983                        $e->getMessage());
984             // TRANS: Client error on action trying to visit a non-existing page.
985             $cac = new ClientErrorAction(_('Page not found.'), 404);
986             $cac->showPage();
987         }
988
989         return $match;
990     }
991
992     function build($action, $args=null, $params=null, $fragment=null)
993     {
994         $action_arg = array('action' => $action);
995
996         if ($args) {
997             $args = array_merge($action_arg, $args);
998         } else {
999             $args = $action_arg;
1000         }
1001
1002         $url = $this->m->generate($args, $params, $fragment);
1003
1004         // Due to a bug in the Net_URL_Mapper code, the returned URL may
1005         // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We
1006         // repair that here rather than modifying the upstream code...
1007
1008         $qpos = strpos($url, '?');
1009         if ($qpos !== false) {
1010             $url = substr($url, 0, $qpos+1) .
1011                 str_replace('?', '&', substr($url, $qpos+1));
1012
1013             // @fixme this is a hacky workaround for http_build_query in the
1014             // lower-level code and bad configs that set the default separator
1015             // to &amp; instead of &. Encoded &s in parameters will not be
1016             // affected.
1017             $url = substr($url, 0, $qpos+1) .
1018                 str_replace('&amp;', '&', substr($url, $qpos+1));
1019
1020         }
1021
1022         return $url;
1023     }
1024 }