]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/router.php
conversation API
[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', 'cancelsubscription', 'approvesub',
212                           'confirmaddress', 'recoverpassword',
213                           'invite', 'favor', 'disfavor', 'sup',
214                           'block', 'unblock', 'subedit',
215                           'groupblock', 'groupunblock',
216                           'sandbox', 'unsandbox',
217                           'silence', 'unsilence',
218                           'grantrole', 'revokerole',
219                           'repeat',
220                           'deleteuser',
221                           'geocode',
222                           'version',
223                           'backupaccount',
224                           'deleteaccount',
225                           'restoreaccount',
226             );
227
228             foreach ($main as $a) {
229                 $m->connect('main/'.$a, array('action' => $a));
230             }
231
232             // Also need a block variant accepting ID on URL for mail links
233             $m->connect('main/block/:profileid',
234                         array('action' => 'block'),
235                         array('profileid' => '[0-9]+'));
236
237             $m->connect('main/sup/:seconds', array('action' => 'sup'),
238                         array('seconds' => '[0-9]+'));
239
240             $m->connect('main/tagprofile', array('action' => 'tagprofile'));
241             $m->connect('main/tagprofile/:id', array('action' => 'tagprofile'),
242                                                array('id' => '[0-9]+'));
243
244             $m->connect('main/oembed',
245                         array('action' => 'oembed'));
246
247             $m->connect('main/xrds',
248                         array('action' => 'publicxrds'));
249             $m->connect('.well-known/host-meta',
250                         array('action' => 'hostmeta'));
251             $m->connect('main/xrd',
252                         array('action' => 'userxrd'));
253
254             // these take a code
255
256             foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
257                 $m->connect('main/'.$c.'/:code', array('action' => $c));
258             }
259
260             // exceptional
261
262             $m->connect('main/remote', array('action' => 'remotesubscribe'));
263             $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+'));
264
265             foreach (Router::$bare as $action) {
266                 $m->connect('index.php?action=' . $action, array('action' => $action));
267             }
268
269             // settings
270
271             foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections',
272                            'oauthapps', 'email', 'sms', 'url') as $s) {
273                 $m->connect('settings/'.$s, array('action' => $s.'settings'));
274             }
275
276             $m->connect('settings/oauthapps/show/:id',
277                         array('action' => 'showapplication'),
278                         array('id' => '[0-9]+')
279             );
280             $m->connect('settings/oauthapps/new',
281                         array('action' => 'newapplication')
282             );
283             $m->connect('settings/oauthapps/edit/:id',
284                         array('action' => 'editapplication'),
285                         array('id' => '[0-9]+')
286             );
287             $m->connect('settings/oauthapps/delete/:id',
288                         array('action' => 'deleteapplication'),
289                         array('id' => '[0-9]+')
290             );
291
292             // search
293
294             foreach (array('group', 'people', 'notice') as $s) {
295                 $m->connect('search/'.$s, array('action' => $s.'search'));
296                 $m->connect('search/'.$s.'?q=:q',
297                             array('action' => $s.'search'),
298                             array('q' => '.+'));
299             }
300
301             // The second of these is needed to make the link work correctly
302             // when inserted into the page. The first is needed to match the
303             // route on the way in. Seems to be another Net_URL_Mapper bug to me.
304             $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
305             $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
306                         array('q' => '.+'));
307
308             $m->connect('attachment/:attachment',
309                         array('action' => 'attachment'),
310                         array('attachment' => '[0-9]+'));
311
312             $m->connect('attachment/:attachment/ajax',
313                         array('action' => 'attachment_ajax'),
314                         array('attachment' => '[0-9]+'));
315
316             $m->connect('attachment/:attachment/thumbnail',
317                         array('action' => 'attachment_thumbnail'),
318                         array('attachment' => '[0-9]+'));
319
320             $m->connect('notice/new', array('action' => 'newnotice'));
321             $m->connect('notice/new?replyto=:replyto',
322                         array('action' => 'newnotice'),
323                         array('replyto' => Nickname::DISPLAY_FMT));
324             $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
325                         array('action' => 'newnotice'),
326                         array('replyto' => Nickname::DISPLAY_FMT),
327                         array('inreplyto' => '[0-9]+'));
328
329             $m->connect('notice/:notice/file',
330                         array('action' => 'file'),
331                         array('notice' => '[0-9]+'));
332
333             $m->connect('notice/:notice',
334                         array('action' => 'shownotice'),
335                         array('notice' => '[0-9]+'));
336             $m->connect('notice/delete', array('action' => 'deletenotice'));
337             $m->connect('notice/delete/:notice',
338                         array('action' => 'deletenotice'),
339                         array('notice' => '[0-9]+'));
340
341             $m->connect('bookmarklet/new', array('action' => 'bookmarklet'));
342
343             // conversation
344
345             $m->connect('conversation/:id',
346                         array('action' => 'conversation'),
347                         array('id' => '[0-9]+'));
348             $m->connect('conversation/:id/replies',
349                         array('action' => 'conversationreplies'),
350                         array('id' => '[0-9]+'));
351
352             $m->connect('message/new', array('action' => 'newmessage'));
353             $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => Nickname::DISPLAY_FMT));
354             $m->connect('message/:message',
355                         array('action' => 'showmessage'),
356                         array('message' => '[0-9]+'));
357
358             $m->connect('user/:id',
359                         array('action' => 'userbyid'),
360                         array('id' => '[0-9]+'));
361
362             $m->connect('tags/', array('action' => 'publictagcloud'));
363             $m->connect('tag/', array('action' => 'publictagcloud'));
364             $m->connect('tags', array('action' => 'publictagcloud'));
365             $m->connect('tag', array('action' => 'publictagcloud'));
366             $m->connect('tag/:tag/rss',
367                         array('action' => 'tagrss'),
368                         array('tag' => self::REGEX_TAG));
369             $m->connect('tag/:tag',
370                         array('action' => 'tag'),
371                         array('tag' => self::REGEX_TAG));
372
373             // groups
374
375             $m->connect('group/new', array('action' => 'newgroup'));
376
377             foreach (array('edit', 'join', 'leave', 'delete', 'cancel', 'approve') as $v) {
378                 $m->connect('group/:nickname/'.$v,
379                             array('action' => $v.'group'),
380                             array('nickname' => Nickname::DISPLAY_FMT));
381                 $m->connect('group/:id/id/'.$v,
382                             array('action' => $v.'group'),
383                             array('id' => '[0-9]+'));
384             }
385
386             foreach (array('members', 'logo', 'rss') as $n) {
387                 $m->connect('group/:nickname/'.$n,
388                             array('action' => 'group'.$n),
389                             array('nickname' => Nickname::DISPLAY_FMT));
390             }
391
392             $m->connect('group/:nickname/foaf',
393                         array('action' => 'foafgroup'),
394                         array('nickname' => Nickname::DISPLAY_FMT));
395
396             $m->connect('group/:nickname/blocked',
397                         array('action' => 'blockedfromgroup'),
398                         array('nickname' => Nickname::DISPLAY_FMT));
399
400             $m->connect('group/:nickname/makeadmin',
401                         array('action' => 'makeadmin'),
402                         array('nickname' => Nickname::DISPLAY_FMT));
403
404             $m->connect('group/:nickname/members/pending',
405                         array('action' => 'groupqueue'),
406                         array('nickname' => Nickname::DISPLAY_FMT));
407
408             $m->connect('group/:id/id',
409                         array('action' => 'groupbyid'),
410                         array('id' => '[0-9]+'));
411
412             $m->connect('group/:nickname',
413                         array('action' => 'showgroup'),
414                         array('nickname' => Nickname::DISPLAY_FMT));
415
416             $m->connect('group/', array('action' => 'groups'));
417             $m->connect('group', array('action' => 'groups'));
418             $m->connect('groups/', array('action' => 'groups'));
419             $m->connect('groups', array('action' => 'groups'));
420
421             // Twitter-compatible API
422
423             // statuses API
424
425             $m->connect('api',
426                         array('action' => 'Redirect',
427                               'nextAction' => 'doc',
428                               'args' => array('title' => 'api')));
429
430             $m->connect('api/statuses/public_timeline.:format',
431                         array('action' => 'ApiTimelinePublic',
432                               'format' => '(xml|json|rss|atom|as)'));
433
434             $m->connect('api/statuses/friends_timeline.:format',
435                         array('action' => 'ApiTimelineFriends',
436                               'format' => '(xml|json|rss|atom|as)'));
437
438             $m->connect('api/statuses/friends_timeline/:id.:format',
439                         array('action' => 'ApiTimelineFriends',
440                               'id' => Nickname::INPUT_FMT,
441                               'format' => '(xml|json|rss|atom|as)'));
442
443             $m->connect('api/statuses/home_timeline.:format',
444                         array('action' => 'ApiTimelineHome',
445                               'format' => '(xml|json|rss|atom|as)'));
446
447             $m->connect('api/statuses/home_timeline/:id.:format',
448                         array('action' => 'ApiTimelineHome',
449                               'id' => Nickname::INPUT_FMT,
450                               'format' => '(xml|json|rss|atom|as)'));
451
452             $m->connect('api/statuses/user_timeline.:format',
453                         array('action' => 'ApiTimelineUser',
454                               'format' => '(xml|json|rss|atom|as)'));
455
456             $m->connect('api/statuses/user_timeline/:id.:format',
457                         array('action' => 'ApiTimelineUser',
458                               'id' => Nickname::INPUT_FMT,
459                               'format' => '(xml|json|rss|atom|as)'));
460
461             $m->connect('api/statuses/mentions.:format',
462                         array('action' => 'ApiTimelineMentions',
463                               'format' => '(xml|json|rss|atom|as)'));
464
465             $m->connect('api/statuses/mentions/:id.:format',
466                         array('action' => 'ApiTimelineMentions',
467                               'id' => Nickname::INPUT_FMT,
468                               'format' => '(xml|json|rss|atom|as)'));
469
470             $m->connect('api/statuses/replies.:format',
471                         array('action' => 'ApiTimelineMentions',
472                               'format' => '(xml|json|rss|atom|as)'));
473
474             $m->connect('api/statuses/replies/:id.:format',
475                         array('action' => 'ApiTimelineMentions',
476                               'id' => Nickname::INPUT_FMT,
477                               'format' => '(xml|json|rss|atom|as)'));
478
479             $m->connect('api/statuses/retweeted_by_me.:format',
480                         array('action' => 'ApiTimelineRetweetedByMe',
481                               'format' => '(xml|json|atom|as)'));
482
483             $m->connect('api/statuses/retweeted_to_me.:format',
484                         array('action' => 'ApiTimelineRetweetedToMe',
485                               'format' => '(xml|json|atom|as)'));
486
487             $m->connect('api/statuses/retweets_of_me.:format',
488                         array('action' => 'ApiTimelineRetweetsOfMe',
489                               'format' => '(xml|json|atom|as)'));
490
491             $m->connect('api/statuses/friends.:format',
492                         array('action' => 'ApiUserFriends',
493                               'format' => '(xml|json)'));
494
495             $m->connect('api/statuses/friends/:id.:format',
496                         array('action' => 'ApiUserFriends',
497                               'id' => Nickname::INPUT_FMT,
498                               'format' => '(xml|json)'));
499
500             $m->connect('api/statuses/followers.:format',
501                         array('action' => 'ApiUserFollowers',
502                               'format' => '(xml|json)'));
503
504             $m->connect('api/statuses/followers/:id.:format',
505                         array('action' => 'ApiUserFollowers',
506                               'id' => Nickname::INPUT_FMT,
507                               'format' => '(xml|json)'));
508
509             $m->connect('api/statuses/show.:format',
510                         array('action' => 'ApiStatusesShow',
511                               'format' => '(xml|json|atom)'));
512
513             $m->connect('api/statuses/show/:id.:format',
514                         array('action' => 'ApiStatusesShow',
515                               'id' => '[0-9]+',
516                               'format' => '(xml|json|atom)'));
517
518             $m->connect('api/statuses/update.:format',
519                         array('action' => 'ApiStatusesUpdate',
520                               'format' => '(xml|json)'));
521
522             $m->connect('api/statuses/destroy.:format',
523                         array('action' => 'ApiStatusesDestroy',
524                               'format' => '(xml|json)'));
525
526             $m->connect('api/statuses/destroy/:id.:format',
527                         array('action' => 'ApiStatusesDestroy',
528                               'id' => '[0-9]+',
529                               'format' => '(xml|json)'));
530
531             $m->connect('api/statuses/retweet/:id.:format',
532                         array('action' => 'ApiStatusesRetweet',
533                               'id' => '[0-9]+',
534                               'format' => '(xml|json)'));
535
536             $m->connect('api/statuses/retweets/:id.:format',
537                         array('action' => 'ApiStatusesRetweets',
538                               'id' => '[0-9]+',
539                               'format' => '(xml|json)'));
540
541             // users
542
543             $m->connect('api/users/show.:format',
544                         array('action' => 'ApiUserShow',
545                               'format' => '(xml|json)'));
546
547             $m->connect('api/users/show/:id.:format',
548                         array('action' => 'ApiUserShow',
549                               'id' => Nickname::INPUT_FMT,
550                               'format' => '(xml|json)'));
551
552             $m->connect('api/users/profile_image/:screen_name.:format',
553                         array('action' => 'ApiUserProfileImage',
554                               'screen_name' => Nickname::DISPLAY_FMT,
555                               'format' => '(xml|json)'));
556
557             // direct messages
558
559             $m->connect('api/direct_messages.:format',
560                         array('action' => 'ApiDirectMessage',
561                               'format' => '(xml|json|rss|atom)'));
562
563             $m->connect('api/direct_messages/sent.:format',
564                         array('action' => 'ApiDirectMessage',
565                               'format' => '(xml|json|rss|atom)',
566                               'sent' => true));
567
568             $m->connect('api/direct_messages/new.:format',
569                         array('action' => 'ApiDirectMessageNew',
570                               'format' => '(xml|json)'));
571
572             // friendships
573
574             $m->connect('api/friendships/show.:format',
575                         array('action' => 'ApiFriendshipsShow',
576                               'format' => '(xml|json)'));
577
578             $m->connect('api/friendships/exists.:format',
579                         array('action' => 'ApiFriendshipsExists',
580                               'format' => '(xml|json)'));
581
582             $m->connect('api/friendships/create.:format',
583                         array('action' => 'ApiFriendshipsCreate',
584                               'format' => '(xml|json)'));
585
586             $m->connect('api/friendships/destroy.:format',
587                         array('action' => 'ApiFriendshipsDestroy',
588                               'format' => '(xml|json)'));
589
590             $m->connect('api/friendships/create/:id.:format',
591                         array('action' => 'ApiFriendshipsCreate',
592                               'id' => Nickname::INPUT_FMT,
593                               'format' => '(xml|json)'));
594
595             $m->connect('api/friendships/destroy/:id.:format',
596                         array('action' => 'ApiFriendshipsDestroy',
597                               'id' => Nickname::INPUT_FMT,
598                               'format' => '(xml|json)'));
599
600             // Social graph
601
602             $m->connect('api/friends/ids/:id.:format',
603                         array('action' => 'ApiUserFriends',
604                               'ids_only' => true));
605
606             $m->connect('api/followers/ids/:id.:format',
607                         array('action' => 'ApiUserFollowers',
608                               'ids_only' => true));
609
610             $m->connect('api/friends/ids.:format',
611                         array('action' => 'ApiUserFriends',
612                               'ids_only' => true));
613
614             $m->connect('api/followers/ids.:format',
615                         array('action' => 'ApiUserFollowers',
616                               'ids_only' => true));
617
618             // account
619
620             $m->connect('api/account/verify_credentials.:format',
621                         array('action' => 'ApiAccountVerifyCredentials'));
622
623             $m->connect('api/account/update_profile.:format',
624                         array('action' => 'ApiAccountUpdateProfile'));
625
626             $m->connect('api/account/update_profile_image.:format',
627                         array('action' => 'ApiAccountUpdateProfileImage'));
628
629             $m->connect('api/account/update_delivery_device.:format',
630                         array('action' => 'ApiAccountUpdateDeliveryDevice'));
631
632             // special case where verify_credentials is called w/out a format
633
634             $m->connect('api/account/verify_credentials',
635                         array('action' => 'ApiAccountVerifyCredentials'));
636
637             $m->connect('api/account/rate_limit_status.:format',
638                         array('action' => 'ApiAccountRateLimitStatus'));
639
640             // favorites
641
642             $m->connect('api/favorites.:format',
643                         array('action' => 'ApiTimelineFavorites',
644                               'format' => '(xml|json|rss|atom|as)'));
645
646             $m->connect('api/favorites/:id.:format',
647                         array('action' => 'ApiTimelineFavorites',
648                               'id' => Nickname::INPUT_FMT,
649                               'format' => '(xml|json|rss|atom|as)'));
650
651             $m->connect('api/favorites/create/:id.:format',
652                         array('action' => 'ApiFavoriteCreate',
653                               'id' => '[0-9]+',
654                               'format' => '(xml|json)'));
655
656             $m->connect('api/favorites/destroy/:id.:format',
657                         array('action' => 'ApiFavoriteDestroy',
658                               'id' => '[0-9]+',
659                               'format' => '(xml|json)'));
660             // blocks
661
662             $m->connect('api/blocks/create.:format',
663                         array('action' => 'ApiBlockCreate',
664                               'format' => '(xml|json)'));
665
666             $m->connect('api/blocks/create/:id.:format',
667                         array('action' => 'ApiBlockCreate',
668                               'id' => Nickname::INPUT_FMT,
669                               'format' => '(xml|json)'));
670
671             $m->connect('api/blocks/destroy.:format',
672                         array('action' => 'ApiBlockDestroy',
673                               'format' => '(xml|json)'));
674
675             $m->connect('api/blocks/destroy/:id.:format',
676                         array('action' => 'ApiBlockDestroy',
677                               'id' => Nickname::INPUT_FMT,
678                               'format' => '(xml|json)'));
679             // help
680
681             $m->connect('api/help/test.:format',
682                         array('action' => 'ApiHelpTest',
683                               'format' => '(xml|json)'));
684
685             // statusnet
686
687             $m->connect('api/statusnet/version.:format',
688                         array('action' => 'ApiStatusnetVersion',
689                               'format' => '(xml|json)'));
690
691             $m->connect('api/statusnet/config.:format',
692                         array('action' => 'ApiStatusnetConfig',
693                               'format' => '(xml|json)'));
694
695             // For older methods, we provide "laconica" base action
696
697             $m->connect('api/laconica/version.:format',
698                         array('action' => 'ApiStatusnetVersion',
699                               'format' => '(xml|json)'));
700
701             $m->connect('api/laconica/config.:format',
702                         array('action' => 'ApiStatusnetConfig',
703                               'format' => '(xml|json)'));
704
705             // Groups and tags are newer than 0.8.1 so no backward-compatibility
706             // necessary
707
708             // Groups
709             //'list' has to be handled differently, as php will not allow a method to be named 'list'
710
711             $m->connect('api/statusnet/groups/timeline/:id.:format',
712                         array('action' => 'ApiTimelineGroup',
713                               'id' => Nickname::INPUT_FMT,
714                               'format' => '(xml|json|rss|atom|as)'));
715
716             $m->connect('api/statusnet/groups/show.:format',
717                         array('action' => 'ApiGroupShow',
718                               'format' => '(xml|json)'));
719
720             $m->connect('api/statusnet/groups/show/:id.:format',
721                         array('action' => 'ApiGroupShow',
722                               'id' => Nickname::INPUT_FMT,
723                               'format' => '(xml|json)'));
724
725             $m->connect('api/statusnet/groups/join.:format',
726                         array('action' => 'ApiGroupJoin',
727                               'id' => Nickname::INPUT_FMT,
728                               'format' => '(xml|json)'));
729
730             $m->connect('api/statusnet/groups/join/:id.:format',
731                         array('action' => 'ApiGroupJoin',
732                               'format' => '(xml|json)'));
733
734             $m->connect('api/statusnet/groups/leave.:format',
735                         array('action' => 'ApiGroupLeave',
736                               'id' => Nickname::INPUT_FMT,
737                               'format' => '(xml|json)'));
738
739             $m->connect('api/statusnet/groups/leave/:id.:format',
740                         array('action' => 'ApiGroupLeave',
741                               'format' => '(xml|json)'));
742
743             $m->connect('api/statusnet/groups/is_member.:format',
744                         array('action' => 'ApiGroupIsMember',
745                               'format' => '(xml|json)'));
746
747             $m->connect('api/statusnet/groups/list.:format',
748                         array('action' => 'ApiGroupList',
749                               'format' => '(xml|json|rss|atom)'));
750
751             $m->connect('api/statusnet/groups/list/:id.:format',
752                         array('action' => 'ApiGroupList',
753                               'id' => Nickname::INPUT_FMT,
754                               'format' => '(xml|json|rss|atom)'));
755
756             $m->connect('api/statusnet/groups/list_all.:format',
757                         array('action' => 'ApiGroupListAll',
758                               'format' => '(xml|json|rss|atom)'));
759
760             $m->connect('api/statusnet/groups/membership.:format',
761                         array('action' => 'ApiGroupMembership',
762                               'format' => '(xml|json)'));
763
764             $m->connect('api/statusnet/groups/membership/:id.:format',
765                         array('action' => 'ApiGroupMembership',
766                               'id' => Nickname::INPUT_FMT,
767                               'format' => '(xml|json)'));
768
769             $m->connect('api/statusnet/groups/create.:format',
770                         array('action' => 'ApiGroupCreate',
771                               'format' => '(xml|json)'));
772
773             $m->connect('api/statusnet/groups/update/:id.:format',
774                         array('action' => 'ApiGroupProfileUpdate',
775                               'id' => '[a-zA-Z0-9]+',
776                               'format' => '(xml|json)'));
777                               
778             $m->connect('api/statusnet/conversation/:id.:format',
779                         array('action' => 'apiconversation',
780                               'id' => '[0-9]+',
781                               'format' => '(xml|json|rss|atom|as)'));
782
783             // Lists (people tags)
784
785             $m->connect('api/lists/memberships.:format',
786                         array('action' => 'ApiListMemberships',
787                               'format' => '(xml|json)'));
788
789             $m->connect('api/:user/lists/memberships.:format',
790                         array('action' => 'ApiListMemberships',
791                               'user' => '[a-zA-Z0-9]+',
792                               'format' => '(xml|json)'));
793
794             $m->connect('api/lists/subscriptions.:format',
795                         array('action' => 'ApiListSubscriptions',
796                               'format' => '(xml|json)'));
797
798             $m->connect('api/:user/lists/subscriptions.:format',
799                         array('action' => 'ApiListSubscriptions',
800                               'user' => '[a-zA-Z0-9]+',
801                               'format' => '(xml|json)'));
802             $m->connect('api/lists.:format',
803                         array('action' => 'ApiLists',
804                               'format' => '(xml|json)'));
805
806             $m->connect('api/:user/lists.:format',
807                         array('action' => 'ApiLists',
808                               'user' => '[a-zA-Z0-9]+',
809                               'format' => '(xml|json)'));
810
811             $m->connect('api/:user/lists/:id.:format',
812                         array('action' => 'ApiList',
813                               'user' => '[a-zA-Z0-9]+',
814                               'id' => '[a-zA-Z0-9]+',
815                               'format' => '(xml|json)'));
816
817             $m->connect('api/:user/lists/:id/statuses.:format',
818                         array('action' => 'ApiTimelineList',
819                               'user' => '[a-zA-Z0-9]+',
820                               'id' => '[a-zA-Z0-9]+',
821                               'format' => '(xml|json|rss|atom)'));
822
823             $m->connect('api/:user/:list_id/members.:format',
824                         array('action' => 'ApiListMembers',
825                               'user' => '[a-zA-Z0-9]+',
826                               'list_id' => '[a-zA-Z0-9]+',
827                               'format' => '(xml|json)'));
828
829             $m->connect('api/:user/:list_id/subscribers.:format',
830                         array('action' => 'ApiListSubscribers',
831                               'user' => '[a-zA-Z0-9]+',
832                               'list_id' => '[a-zA-Z0-9]+',
833                               'format' => '(xml|json)'));
834
835             $m->connect('api/:user/:list_id/members/:id.:format',
836                         array('action' => 'ApiListMember',
837                               'user' => '[a-zA-Z0-9]+',
838                               'list_id' => '[a-zA-Z0-9]+',
839                               'id' => '[a-zA-Z0-9]+',
840                               'format' => '(xml|json)'));
841
842             $m->connect('api/:user/:list_id/subscribers/:id.:format',
843                         array('action' => 'ApiListSubscriber',
844                               'user' => '[a-zA-Z0-9]+',
845                               'list_id' => '[a-zA-Z0-9]+',
846                               'id' => '[a-zA-Z0-9]+',
847                               'format' => '(xml|json)'));
848
849             // Tags
850             $m->connect('api/statusnet/tags/timeline/:tag.:format',
851                         array('action' => 'ApiTimelineTag',
852                               'format' => '(xml|json|rss|atom|as)'));
853
854             // media related
855             $m->connect(
856                 'api/statusnet/media/upload',
857                 array('action' => 'ApiMediaUpload')
858             );
859
860             // search
861             $m->connect('api/search.atom', array('action' => 'ApiSearchAtom'));
862             $m->connect('api/search.json', array('action' => 'ApiSearchJSON'));
863             $m->connect('api/trends.json', array('action' => 'ApiTrends'));
864
865             $m->connect('api/oauth/request_token',
866                         array('action' => 'ApiOauthRequestToken'));
867
868             $m->connect('api/oauth/access_token',
869                         array('action' => 'ApiOauthAccessToken'));
870
871             $m->connect('api/oauth/authorize',
872                         array('action' => 'ApiOauthAuthorize'));
873
874             // Admin
875
876             $m->connect('panel/site', array('action' => 'siteadminpanel'));
877             $m->connect('panel/user', array('action' => 'useradminpanel'));
878                 $m->connect('panel/access', array('action' => 'accessadminpanel'));
879             $m->connect('panel/paths', array('action' => 'pathsadminpanel'));
880             $m->connect('panel/sessions', array('action' => 'sessionsadminpanel'));
881             $m->connect('panel/sitenotice', array('action' => 'sitenoticeadminpanel'));
882             $m->connect('panel/snapshot', array('action' => 'snapshotadminpanel'));
883             $m->connect('panel/license', array('action' => 'licenseadminpanel'));
884
885             $m->connect('panel/plugins', array('action' => 'pluginsadminpanel'));
886             $m->connect('panel/plugins/enable/:plugin',
887                         array('action' => 'pluginenable'),
888                         array('plugin' => '[A-Za-z0-9_]+'));
889             $m->connect('panel/plugins/disable/:plugin',
890                         array('action' => 'plugindisable'),
891                         array('plugin' => '[A-Za-z0-9_]+'));
892
893             $m->connect('getfile/:filename',
894                         array('action' => 'getfile'),
895                         array('filename' => '[A-Za-z0-9._-]+'));
896
897             // In the "root"
898
899             if (common_config('singleuser', 'enabled')) {
900
901                 $nickname = User::singleUserNickname();
902
903                 foreach (array('subscriptions', 'subscribers',
904                                'all', 'foaf', 'xrds',
905                                'replies', 'microsummary', 'hcard') as $a) {
906                     $m->connect($a,
907                                 array('action' => $a,
908                                       'nickname' => $nickname));
909                 }
910
911                 foreach (array('subscriptions', 'subscribers') as $a) {
912                     $m->connect($a.'/:tag',
913                                 array('action' => $a,
914                                       'nickname' => $nickname),
915                                 array('tag' => self::REGEX_TAG));
916                 }
917
918                 $m->connect('subscribers/pending',
919                             array('action' => 'subqueue',
920                                   'nickname' => $nickname));
921
922                 foreach (array('rss', 'groups') as $a) {
923                     $m->connect($a,
924                                 array('action' => 'user'.$a,
925                                       'nickname' => $nickname));
926                 }
927
928                 foreach (array('all', 'replies', 'favorites') as $a) {
929                     $m->connect($a.'/rss',
930                                 array('action' => $a.'rss',
931                                       'nickname' => $nickname));
932                 }
933
934                 $m->connect('favorites',
935                             array('action' => 'showfavorites',
936                                   'nickname' => $nickname));
937
938                 $m->connect('avatar/:size',
939                             array('action' => 'avatarbynickname',
940                                   'nickname' => $nickname),
941                             array('size' => '(original|96|48|24)'));
942
943                 $m->connect('tag/:tag/rss',
944                             array('action' => 'userrss',
945                                   'nickname' => $nickname),
946                             array('tag' => self::REGEX_TAG));
947
948                 $m->connect('tag/:tag',
949                             array('action' => 'showstream',
950                                   'nickname' => $nickname),
951                             array('tag' => self::REGEX_TAG));
952
953                 $m->connect('rsd.xml',
954                             array('action' => 'rsd',
955                                   'nickname' => $nickname));
956
957                 $m->connect('',
958                             array('action' => 'showstream',
959                                   'nickname' => $nickname));
960             } else {
961                 $m->connect('', array('action' => 'public'));
962                 $m->connect('rss', array('action' => 'publicrss'));
963                 $m->connect('featuredrss', array('action' => 'featuredrss'));
964                 $m->connect('favoritedrss', array('action' => 'favoritedrss'));
965                 $m->connect('featured/', array('action' => 'featured'));
966                 $m->connect('featured', array('action' => 'featured'));
967                 $m->connect('favorited/', array('action' => 'favorited'));
968                 $m->connect('favorited', array('action' => 'favorited'));
969                 $m->connect('rsd.xml', array('action' => 'rsd'));
970
971                 foreach (array('subscriptions', 'subscribers',
972                                'nudge', 'all', 'foaf', 'xrds',
973                                'replies', 'inbox', 'outbox', 'microsummary', 'hcard') as $a) {
974                     $m->connect(':nickname/'.$a,
975                                 array('action' => $a),
976                                 array('nickname' => Nickname::DISPLAY_FMT));
977                 }
978                 $m->connect(':nickname/subscribers/pending',
979                             array('action' => 'subqueue'),
980                             array('nickname' => Nickname::DISPLAY_FMT));
981
982                 // people tags
983
984                 $m->connect('peopletags', array('action' => 'publicpeopletagcloud'));
985
986                 $m->connect('peopletag/:tag', array('action' => 'peopletag',
987                                                     'tag'    => self::REGEX_TAG));
988
989                 $m->connect('selftag/:tag', array('action' => 'selftag',
990                                                   'tag'    => self::REGEX_TAG));
991
992                 $m->connect('main/addpeopletag', array('action' => 'addpeopletag'));
993
994                 $m->connect('main/removepeopletag', array('action' => 'removepeopletag'));
995
996                 $m->connect('main/profilecompletion', array('action' => 'profilecompletion'));
997
998                 $m->connect('main/peopletagautocomplete', array('action' => 'peopletagautocomplete'));
999
1000                 $m->connect(':nickname/peopletags',
1001                                 array('action' => 'peopletagsbyuser',
1002                                       'nickname' => Nickname::DISPLAY_FMT));
1003
1004                 $m->connect(':nickname/peopletags/private',
1005                                 array('action' => 'peopletagsbyuser',
1006                                       'nickname' => Nickname::DISPLAY_FMT,
1007                                       'private' => 1));
1008
1009                 $m->connect(':nickname/peopletags/public',
1010                                 array('action' => 'peopletagsbyuser',
1011                                       'nickname' => Nickname::DISPLAY_FMT,
1012                                       'public' => 1));
1013
1014                 $m->connect(':nickname/othertags',
1015                                 array('action' => 'peopletagsforuser',
1016                                       'nickname' => Nickname::DISPLAY_FMT));
1017
1018                 $m->connect(':nickname/peopletagsubscriptions',
1019                                 array('action' => 'peopletagsubscriptions',
1020                                       'nickname' => Nickname::DISPLAY_FMT));
1021
1022                 $m->connect(':tagger/all/:tag/subscribers',
1023                                 array('action' => 'peopletagsubscribers',
1024                                       'tagger' => Nickname::DISPLAY_FMT,
1025                                       'tag' => self::REGEX_TAG));
1026
1027                 $m->connect(':tagger/all/:tag/tagged',
1028                                 array('action' => 'peopletagged',
1029                                       'tagger' => Nickname::DISPLAY_FMT,
1030                                       'tag' => self::REGEX_TAG));
1031
1032                 $m->connect(':tagger/all/:tag/edit',
1033                                 array('action' => 'editpeopletag',
1034                                       'tagger' => Nickname::DISPLAY_FMT,
1035                                       'tag' => self::REGEX_TAG));
1036
1037                 foreach(array('subscribe', 'unsubscribe') as $v) {
1038                     $m->connect('peopletag/:id/'.$v,
1039                                     array('action' => $v.'peopletag',
1040                                           'id' => '[0-9]{1,64}'));
1041                 }
1042                 $m->connect('user/:tagger_id/profiletag/:id/id',
1043                                 array('action' => 'profiletagbyid',
1044                                       'tagger_id' => '[0-9]+',
1045                                       'id' => '[0-9]+'));
1046
1047                 $m->connect(':tagger/all/:tag',
1048                                 array('action' => 'showprofiletag',
1049                                       'tagger' => Nickname::DISPLAY_FMT,
1050                                       'tag' => self::REGEX_TAG));
1051
1052                 foreach (array('subscriptions', 'subscribers') as $a) {
1053                     $m->connect(':nickname/'.$a.'/:tag',
1054                                 array('action' => $a),
1055                                 array('tag' => self::REGEX_TAG,
1056                                       'nickname' => Nickname::DISPLAY_FMT));
1057                 }
1058
1059                 foreach (array('rss', 'groups') as $a) {
1060                     $m->connect(':nickname/'.$a,
1061                                 array('action' => 'user'.$a),
1062                                 array('nickname' => Nickname::DISPLAY_FMT));
1063                 }
1064
1065                 foreach (array('all', 'replies', 'favorites') as $a) {
1066                     $m->connect(':nickname/'.$a.'/rss',
1067                                 array('action' => $a.'rss'),
1068                                 array('nickname' => Nickname::DISPLAY_FMT));
1069                 }
1070
1071                 $m->connect(':nickname/favorites',
1072                             array('action' => 'showfavorites'),
1073                             array('nickname' => Nickname::DISPLAY_FMT));
1074
1075                 $m->connect(':nickname/avatar/:size',
1076                             array('action' => 'avatarbynickname'),
1077                             array('size' => '(original|96|48|24)',
1078                                   'nickname' => Nickname::DISPLAY_FMT));
1079
1080                 $m->connect(':nickname/tag/:tag/rss',
1081                             array('action' => 'userrss'),
1082                             array('nickname' => Nickname::DISPLAY_FMT),
1083                             array('tag' => self::REGEX_TAG));
1084
1085                 $m->connect(':nickname/tag/:tag',
1086                             array('action' => 'showstream'),
1087                             array('nickname' => Nickname::DISPLAY_FMT),
1088                             array('tag' => self::REGEX_TAG));
1089
1090                 $m->connect(':nickname/rsd.xml',
1091                             array('action' => 'rsd'),
1092                             array('nickname' => Nickname::DISPLAY_FMT));
1093
1094                 $m->connect(':nickname',
1095                             array('action' => 'showstream'),
1096                             array('nickname' => Nickname::DISPLAY_FMT));
1097             }
1098
1099             // AtomPub API
1100
1101             $m->connect('api/statusnet/app/service/:id.xml',
1102                         array('action' => 'ApiAtomService'),
1103                         array('id' => Nickname::DISPLAY_FMT));
1104
1105             $m->connect('api/statusnet/app/service.xml',
1106                         array('action' => 'ApiAtomService'));
1107
1108             $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom',
1109                         array('action' => 'AtomPubShowSubscription'),
1110                         array('subscriber' => '[0-9]+',
1111                               'subscribed' => '[0-9]+'));
1112
1113             $m->connect('api/statusnet/app/subscriptions/:subscriber.atom',
1114                         array('action' => 'AtomPubSubscriptionFeed'),
1115                         array('subscriber' => '[0-9]+'));
1116
1117             $m->connect('api/statusnet/app/favorites/:profile/:notice.atom',
1118                         array('action' => 'AtomPubShowFavorite'),
1119                         array('profile' => '[0-9]+',
1120                               'notice' => '[0-9]+'));
1121
1122             $m->connect('api/statusnet/app/favorites/:profile.atom',
1123                         array('action' => 'AtomPubFavoriteFeed'),
1124                         array('profile' => '[0-9]+'));
1125
1126             $m->connect('api/statusnet/app/memberships/:profile/:group.atom',
1127                         array('action' => 'AtomPubShowMembership'),
1128                         array('profile' => '[0-9]+',
1129                               'group' => '[0-9]+'));
1130
1131             $m->connect('api/statusnet/app/memberships/:profile.atom',
1132                         array('action' => 'AtomPubMembershipFeed'),
1133                         array('profile' => '[0-9]+'));
1134
1135             // URL shortening
1136
1137             $m->connect('url/:id',
1138                         array('action' => 'redirecturl',
1139                               'id' => '[0-9]+'));
1140
1141             // user stuff
1142
1143             Event::handle('RouterInitialized', array($m));
1144         }
1145
1146         return $m;
1147     }
1148
1149     function map($path)
1150     {
1151         try {
1152             $match = $this->m->match($path);
1153         } catch (Net_URL_Mapper_InvalidException $e) {
1154             common_log(LOG_ERR, "Problem getting route for $path - " .
1155                        $e->getMessage());
1156             // TRANS: Client error on action trying to visit a non-existing page.
1157             $cac = new ClientErrorAction(_('Page not found.'), 404);
1158             $cac->showPage();
1159         }
1160
1161         return $match;
1162     }
1163
1164     function build($action, $args=null, $params=null, $fragment=null)
1165     {
1166         $action_arg = array('action' => $action);
1167
1168         if ($args) {
1169             $args = array_merge($action_arg, $args);
1170         } else {
1171             $args = $action_arg;
1172         }
1173
1174         $url = $this->m->generate($args, $params, $fragment);
1175
1176         // Due to a bug in the Net_URL_Mapper code, the returned URL may
1177         // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We
1178         // repair that here rather than modifying the upstream code...
1179
1180         $qpos = strpos($url, '?');
1181         if ($qpos !== false) {
1182             $url = substr($url, 0, $qpos+1) .
1183                 str_replace('?', '&', substr($url, $qpos+1));
1184
1185             // @fixme this is a hacky workaround for http_build_query in the
1186             // lower-level code and bad configs that set the default separator
1187             // to &amp; instead of &. Encoded &s in parameters will not be
1188             // affected.
1189             $url = substr($url, 0, $qpos+1) .
1190                 str_replace('&amp;', '&', substr($url, $qpos+1));
1191
1192         }
1193
1194         return $url;
1195     }
1196 }