]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/router.php
Merge remote-tracking branch 'origin/1.0.x' into 1.0.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
117     const REGEX_TAG = '[^\/]+'; // [\pL\pN_\-\.]{1,64} better if we can do unicode regexes
118
119     static function get()
120     {
121         if (!Router::$inst) {
122             Router::$inst = new Router();
123         }
124         return Router::$inst;
125     }
126
127     /**
128      * Clear the global singleton instance for this class.
129      * Needed to ensure reset when switching site configurations.
130      */
131     static function clear()
132     {
133         Router::$inst = null;
134     }
135
136     function __construct()
137     {
138         if (empty($this->m)) {
139             if (!common_config('router', 'cache')) {
140                 $this->m = $this->initialize();
141             } else {
142                 $k = self::cacheKey();
143                 $c = Cache::instance();
144                 $m = $c->get($k);
145                 if (!empty($m)) {
146                     $this->m = $m;
147                 } else {
148                     $this->m = $this->initialize();
149                     $c->set($k, $this->m);
150                 }
151             }
152         }
153     }
154
155     /**
156      * Create a unique hashkey for the router.
157      *
158      * The router's url map can change based on the version of the software
159      * you're running and the plugins that are enabled. To avoid having bad routes
160      * get stuck in the cache, the key includes a list of plugins and the software
161      * version.
162      *
163      * There can still be problems with a) differences in versions of the plugins and
164      * b) people running code between official versions, but these tend to be more
165      * sophisticated users who can grok what's going on and clear their caches.
166      *
167      * @return string cache key string that should uniquely identify a router
168      */
169
170     static function cacheKey()
171     {
172         $parts = array('router');
173
174         // Many router paths depend on this setting.
175         if (common_config('singleuser', 'enabled')) {
176             $parts[] = '1user';
177         } else {
178             $parts[] = 'multi';
179         }
180
181         return Cache::codeKey(implode(':', $parts));
182     }
183
184     function initialize()
185     {
186         $m = StatusNet_URL_Mapper::getInstance();
187
188         if (Event::handle('StartInitializeRouter', array(&$m))) {
189
190             $m->connect('robots.txt', array('action' => 'robotstxt'));
191
192             $m->connect('opensearch/people', array('action' => 'opensearch',
193                                                    'type' => 'people'));
194             $m->connect('opensearch/notice', array('action' => 'opensearch',
195                                                    'type' => 'notice'));
196
197             // docs
198
199             $m->connect('doc/:title', array('action' => 'doc'));
200
201             $m->connect('main/otp/:user_id/:token',
202                         array('action' => 'otp'),
203                         array('user_id' => '[0-9]+',
204                               'token' => '.+'));
205
206             // main stuff is repetitive
207
208             $main = array('login', 'logout', 'register', 'subscribe',
209                           'unsubscribe', 'cancelsubscription', 'approvesub',
210                           'confirmaddress', 'recoverpassword',
211                           'invite', 'favor', 'disfavor', 'sup',
212                           'block', 'unblock', 'subedit',
213                           'groupblock', 'groupunblock',
214                           'sandbox', 'unsandbox',
215                           'silence', 'unsilence',
216                           'grantrole', 'revokerole',
217                           'repeat',
218                           'deleteuser',
219                           'geocode',
220                           'version',
221                           'backupaccount',
222                           'deleteaccount',
223                           'restoreaccount',
224             );
225
226             foreach ($main as $a) {
227                 $m->connect('main/'.$a, array('action' => $a));
228             }
229
230             // Also need a block variant accepting ID on URL for mail links
231             $m->connect('main/block/:profileid',
232                         array('action' => 'block'),
233                         array('profileid' => '[0-9]+'));
234
235             $m->connect('main/sup/:seconds', array('action' => 'sup'),
236                         array('seconds' => '[0-9]+'));
237
238             $m->connect('main/tagprofile', array('action' => 'tagprofile'));
239             $m->connect('main/tagprofile/:id', array('action' => 'tagprofile'),
240                                                array('id' => '[0-9]+'));
241
242             $m->connect('main/oembed',
243                         array('action' => 'oembed'));
244
245             $m->connect('main/xrds',
246                         array('action' => 'publicxrds'));
247             $m->connect('.well-known/host-meta',
248                         array('action' => 'hostmeta'));
249             $m->connect('main/xrd',
250                         array('action' => 'userxrd'));
251
252             // these take a code
253
254             foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
255                 $m->connect('main/'.$c.'/:code', array('action' => $c));
256             }
257
258             // settings
259
260             foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections',
261                            'oauthapps', 'email', 'sms', 'url') as $s) {
262                 $m->connect('settings/'.$s, array('action' => $s.'settings'));
263             }
264
265             $m->connect('settings/oauthapps/show/:id',
266                         array('action' => 'showapplication'),
267                         array('id' => '[0-9]+')
268             );
269             $m->connect('settings/oauthapps/new',
270                         array('action' => 'newapplication')
271             );
272             $m->connect('settings/oauthapps/edit/:id',
273                         array('action' => 'editapplication'),
274                         array('id' => '[0-9]+')
275             );
276             $m->connect('settings/oauthapps/delete/:id',
277                         array('action' => 'deleteapplication'),
278                         array('id' => '[0-9]+')
279             );
280
281             // search
282
283             foreach (array('group', 'people', 'notice') as $s) {
284                 $m->connect('search/'.$s, array('action' => $s.'search'));
285                 $m->connect('search/'.$s.'?q=:q',
286                             array('action' => $s.'search'),
287                             array('q' => '.+'));
288             }
289
290             // The second of these is needed to make the link work correctly
291             // when inserted into the page. The first is needed to match the
292             // route on the way in. Seems to be another Net_URL_Mapper bug to me.
293             $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
294             $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
295                         array('q' => '.+'));
296
297             $m->connect('attachment/:attachment',
298                         array('action' => 'attachment'),
299                         array('attachment' => '[0-9]+'));
300
301             $m->connect('attachment/:attachment/ajax',
302                         array('action' => 'attachment_ajax'),
303                         array('attachment' => '[0-9]+'));
304
305             $m->connect('attachment/:attachment/thumbnail',
306                         array('action' => 'attachment_thumbnail'),
307                         array('attachment' => '[0-9]+'));
308
309             $m->connect('notice/new', array('action' => 'newnotice'));
310             $m->connect('notice/new?replyto=:replyto',
311                         array('action' => 'newnotice'),
312                         array('replyto' => Nickname::DISPLAY_FMT));
313             $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
314                         array('action' => 'newnotice'),
315                         array('replyto' => Nickname::DISPLAY_FMT),
316                         array('inreplyto' => '[0-9]+'));
317
318             $m->connect('notice/:notice/file',
319                         array('action' => 'file'),
320                         array('notice' => '[0-9]+'));
321
322             $m->connect('notice/:notice',
323                         array('action' => 'shownotice'),
324                         array('notice' => '[0-9]+'));
325             $m->connect('notice/delete', array('action' => 'deletenotice'));
326             $m->connect('notice/delete/:notice',
327                         array('action' => 'deletenotice'),
328                         array('notice' => '[0-9]+'));
329
330             $m->connect('bookmarklet/new', array('action' => 'bookmarklet'));
331
332             // conversation
333
334             $m->connect('conversation/:id',
335                         array('action' => 'conversation'),
336                         array('id' => '[0-9]+'));
337             $m->connect('conversation/:id/replies',
338                         array('action' => 'conversationreplies'),
339                         array('id' => '[0-9]+'));
340
341             $m->connect('message/new', array('action' => 'newmessage'));
342             $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => Nickname::DISPLAY_FMT));
343             $m->connect('message/:message',
344                         array('action' => 'showmessage'),
345                         array('message' => '[0-9]+'));
346
347             $m->connect('user/:id',
348                         array('action' => 'userbyid'),
349                         array('id' => '[0-9]+'));
350
351             $m->connect('tags/', array('action' => 'publictagcloud'));
352             $m->connect('tag/', array('action' => 'publictagcloud'));
353             $m->connect('tags', array('action' => 'publictagcloud'));
354             $m->connect('tag', array('action' => 'publictagcloud'));
355             $m->connect('tag/:tag/rss',
356                         array('action' => 'tagrss'),
357                         array('tag' => self::REGEX_TAG));
358             $m->connect('tag/:tag',
359                         array('action' => 'tag'),
360                         array('tag' => self::REGEX_TAG));
361
362             // groups
363
364             $m->connect('group/new', array('action' => 'newgroup'));
365
366             foreach (array('edit', 'join', 'leave', 'delete', 'cancel', 'approve') as $v) {
367                 $m->connect('group/:nickname/'.$v,
368                             array('action' => $v.'group'),
369                             array('nickname' => Nickname::DISPLAY_FMT));
370                 $m->connect('group/:id/id/'.$v,
371                             array('action' => $v.'group'),
372                             array('id' => '[0-9]+'));
373             }
374
375             foreach (array('members', 'logo', 'rss') as $n) {
376                 $m->connect('group/:nickname/'.$n,
377                             array('action' => 'group'.$n),
378                             array('nickname' => Nickname::DISPLAY_FMT));
379             }
380
381             $m->connect('group/:nickname/foaf',
382                         array('action' => 'foafgroup'),
383                         array('nickname' => Nickname::DISPLAY_FMT));
384
385             $m->connect('group/:nickname/blocked',
386                         array('action' => 'blockedfromgroup'),
387                         array('nickname' => Nickname::DISPLAY_FMT));
388
389             $m->connect('group/:nickname/makeadmin',
390                         array('action' => 'makeadmin'),
391                         array('nickname' => Nickname::DISPLAY_FMT));
392
393             $m->connect('group/:nickname/members/pending',
394                         array('action' => 'groupqueue'),
395                         array('nickname' => Nickname::DISPLAY_FMT));
396
397             $m->connect('group/:id/id',
398                         array('action' => 'groupbyid'),
399                         array('id' => '[0-9]+'));
400
401             $m->connect('group/:nickname',
402                         array('action' => 'showgroup'),
403                         array('nickname' => Nickname::DISPLAY_FMT));
404
405             $m->connect('group/', array('action' => 'groups'));
406             $m->connect('group', array('action' => 'groups'));
407             $m->connect('groups/', array('action' => 'groups'));
408             $m->connect('groups', array('action' => 'groups'));
409
410             // Twitter-compatible API
411
412             // statuses API
413
414             $m->connect('api',
415                         array('action' => 'Redirect',
416                               'nextAction' => 'doc',
417                               'args' => array('title' => '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_delivery_device.:format',
619                         array('action' => 'ApiAccountUpdateDeliveryDevice'));
620
621             // special case where verify_credentials is called w/out a format
622
623             $m->connect('api/account/verify_credentials',
624                         array('action' => 'ApiAccountVerifyCredentials'));
625
626             $m->connect('api/account/rate_limit_status.:format',
627                         array('action' => 'ApiAccountRateLimitStatus'));
628
629             // favorites
630
631             $m->connect('api/favorites.:format',
632                         array('action' => 'ApiTimelineFavorites',
633                               'format' => '(xml|json|rss|atom|as)'));
634
635             $m->connect('api/favorites/:id.:format',
636                         array('action' => 'ApiTimelineFavorites',
637                               'id' => Nickname::INPUT_FMT,
638                               'format' => '(xml|json|rss|atom|as)'));
639
640             $m->connect('api/favorites/create/:id.:format',
641                         array('action' => 'ApiFavoriteCreate',
642                               'id' => '[0-9]+',
643                               'format' => '(xml|json)'));
644
645             $m->connect('api/favorites/destroy/:id.:format',
646                         array('action' => 'ApiFavoriteDestroy',
647                               'id' => '[0-9]+',
648                               'format' => '(xml|json)'));
649             // blocks
650
651             $m->connect('api/blocks/create.:format',
652                         array('action' => 'ApiBlockCreate',
653                               'format' => '(xml|json)'));
654
655             $m->connect('api/blocks/create/:id.:format',
656                         array('action' => 'ApiBlockCreate',
657                               'id' => Nickname::INPUT_FMT,
658                               'format' => '(xml|json)'));
659
660             $m->connect('api/blocks/destroy.:format',
661                         array('action' => 'ApiBlockDestroy',
662                               'format' => '(xml|json)'));
663
664             $m->connect('api/blocks/destroy/:id.:format',
665                         array('action' => 'ApiBlockDestroy',
666                               'id' => Nickname::INPUT_FMT,
667                               'format' => '(xml|json)'));
668             // help
669
670             $m->connect('api/help/test.:format',
671                         array('action' => 'ApiHelpTest',
672                               'format' => '(xml|json)'));
673
674             // statusnet
675
676             $m->connect('api/statusnet/version.:format',
677                         array('action' => 'ApiStatusnetVersion',
678                               'format' => '(xml|json)'));
679
680             $m->connect('api/statusnet/config.:format',
681                         array('action' => 'ApiStatusnetConfig',
682                               'format' => '(xml|json)'));
683
684             // For older methods, we provide "laconica" base action
685
686             $m->connect('api/laconica/version.:format',
687                         array('action' => 'ApiStatusnetVersion',
688                               'format' => '(xml|json)'));
689
690             $m->connect('api/laconica/config.:format',
691                         array('action' => 'ApiStatusnetConfig',
692                               'format' => '(xml|json)'));
693
694             // Groups and tags are newer than 0.8.1 so no backward-compatibility
695             // necessary
696
697             // Groups
698             //'list' has to be handled differently, as php will not allow a method to be named 'list'
699
700             $m->connect('api/statusnet/groups/timeline/:id.:format',
701                         array('action' => 'ApiTimelineGroup',
702                               'id' => Nickname::INPUT_FMT,
703                               'format' => '(xml|json|rss|atom|as)'));
704
705             $m->connect('api/statusnet/groups/show.:format',
706                         array('action' => 'ApiGroupShow',
707                               'format' => '(xml|json)'));
708
709             $m->connect('api/statusnet/groups/show/:id.:format',
710                         array('action' => 'ApiGroupShow',
711                               'id' => Nickname::INPUT_FMT,
712                               'format' => '(xml|json)'));
713
714             $m->connect('api/statusnet/groups/join.:format',
715                         array('action' => 'ApiGroupJoin',
716                               'id' => Nickname::INPUT_FMT,
717                               'format' => '(xml|json)'));
718
719             $m->connect('api/statusnet/groups/join/:id.:format',
720                         array('action' => 'ApiGroupJoin',
721                               'format' => '(xml|json)'));
722
723             $m->connect('api/statusnet/groups/leave.:format',
724                         array('action' => 'ApiGroupLeave',
725                               'id' => Nickname::INPUT_FMT,
726                               'format' => '(xml|json)'));
727
728             $m->connect('api/statusnet/groups/leave/:id.:format',
729                         array('action' => 'ApiGroupLeave',
730                               'format' => '(xml|json)'));
731
732             $m->connect('api/statusnet/groups/is_member.:format',
733                         array('action' => 'ApiGroupIsMember',
734                               'format' => '(xml|json)'));
735
736             $m->connect('api/statusnet/groups/list.:format',
737                         array('action' => 'ApiGroupList',
738                               'format' => '(xml|json|rss|atom)'));
739
740             $m->connect('api/statusnet/groups/list/:id.:format',
741                         array('action' => 'ApiGroupList',
742                               'id' => Nickname::INPUT_FMT,
743                               'format' => '(xml|json|rss|atom)'));
744
745             $m->connect('api/statusnet/groups/list_all.:format',
746                         array('action' => 'ApiGroupListAll',
747                               'format' => '(xml|json|rss|atom)'));
748
749             $m->connect('api/statusnet/groups/membership.:format',
750                         array('action' => 'ApiGroupMembership',
751                               'format' => '(xml|json)'));
752
753             $m->connect('api/statusnet/groups/membership/:id.:format',
754                         array('action' => 'ApiGroupMembership',
755                               'id' => Nickname::INPUT_FMT,
756                               'format' => '(xml|json)'));
757
758             $m->connect('api/statusnet/groups/create.:format',
759                         array('action' => 'ApiGroupCreate',
760                               'format' => '(xml|json)'));
761
762             $m->connect('api/statusnet/groups/update/:id.:format',
763                         array('action' => 'ApiGroupProfileUpdate',
764                               'id' => '[a-zA-Z0-9]+',
765                               'format' => '(xml|json)'));
766                               
767             $m->connect('api/statusnet/conversation/:id.:format',
768                         array('action' => 'apiconversation',
769                               'id' => '[0-9]+',
770                               'format' => '(xml|json|rss|atom|as)'));
771
772             // Lists (people tags)
773
774             $m->connect('api/lists/memberships.:format',
775                         array('action' => 'ApiListMemberships',
776                               'format' => '(xml|json)'));
777
778             $m->connect('api/:user/lists/memberships.:format',
779                         array('action' => 'ApiListMemberships',
780                               'user' => '[a-zA-Z0-9]+',
781                               'format' => '(xml|json)'));
782
783             $m->connect('api/lists/subscriptions.:format',
784                         array('action' => 'ApiListSubscriptions',
785                               'format' => '(xml|json)'));
786
787             $m->connect('api/:user/lists/subscriptions.:format',
788                         array('action' => 'ApiListSubscriptions',
789                               'user' => '[a-zA-Z0-9]+',
790                               'format' => '(xml|json)'));
791             $m->connect('api/lists.:format',
792                         array('action' => 'ApiLists',
793                               'format' => '(xml|json)'));
794
795             $m->connect('api/:user/lists.:format',
796                         array('action' => 'ApiLists',
797                               'user' => '[a-zA-Z0-9]+',
798                               'format' => '(xml|json)'));
799
800             $m->connect('api/:user/lists/:id.:format',
801                         array('action' => 'ApiList',
802                               'user' => '[a-zA-Z0-9]+',
803                               'id' => '[a-zA-Z0-9]+',
804                               'format' => '(xml|json)'));
805
806             $m->connect('api/:user/lists/:id/statuses.:format',
807                         array('action' => 'ApiTimelineList',
808                               'user' => '[a-zA-Z0-9]+',
809                               'id' => '[a-zA-Z0-9]+',
810                               'format' => '(xml|json|rss|atom)'));
811
812             $m->connect('api/:user/:list_id/members.:format',
813                         array('action' => 'ApiListMembers',
814                               'user' => '[a-zA-Z0-9]+',
815                               'list_id' => '[a-zA-Z0-9]+',
816                               'format' => '(xml|json)'));
817
818             $m->connect('api/:user/:list_id/subscribers.:format',
819                         array('action' => 'ApiListSubscribers',
820                               'user' => '[a-zA-Z0-9]+',
821                               'list_id' => '[a-zA-Z0-9]+',
822                               'format' => '(xml|json)'));
823
824             $m->connect('api/:user/:list_id/members/:id.:format',
825                         array('action' => 'ApiListMember',
826                               'user' => '[a-zA-Z0-9]+',
827                               'list_id' => '[a-zA-Z0-9]+',
828                               'id' => '[a-zA-Z0-9]+',
829                               'format' => '(xml|json)'));
830
831             $m->connect('api/:user/:list_id/subscribers/:id.:format',
832                         array('action' => 'ApiListSubscriber',
833                               'user' => '[a-zA-Z0-9]+',
834                               'list_id' => '[a-zA-Z0-9]+',
835                               'id' => '[a-zA-Z0-9]+',
836                               'format' => '(xml|json)'));
837
838             // Tags
839             $m->connect('api/statusnet/tags/timeline/:tag.:format',
840                         array('action' => 'ApiTimelineTag',
841                               'format' => '(xml|json|rss|atom|as)'));
842
843             // media related
844             $m->connect(
845                 'api/statusnet/media/upload',
846                 array('action' => 'ApiMediaUpload')
847             );
848
849             // search
850             $m->connect('api/search.atom', array('action' => 'ApiSearchAtom'));
851             $m->connect('api/search.json', array('action' => 'ApiSearchJSON'));
852             $m->connect('api/trends.json', array('action' => 'ApiTrends'));
853
854             $m->connect('api/oauth/request_token',
855                         array('action' => 'ApiOauthRequestToken'));
856
857             $m->connect('api/oauth/access_token',
858                         array('action' => 'ApiOauthAccessToken'));
859
860             $m->connect('api/oauth/authorize',
861                         array('action' => 'ApiOauthAuthorize'));
862
863             // Admin
864
865             $m->connect('panel/site', array('action' => 'siteadminpanel'));
866             $m->connect('panel/user', array('action' => 'useradminpanel'));
867                 $m->connect('panel/access', array('action' => 'accessadminpanel'));
868             $m->connect('panel/paths', array('action' => 'pathsadminpanel'));
869             $m->connect('panel/sessions', array('action' => 'sessionsadminpanel'));
870             $m->connect('panel/sitenotice', array('action' => 'sitenoticeadminpanel'));
871             $m->connect('panel/snapshot', array('action' => 'snapshotadminpanel'));
872             $m->connect('panel/license', array('action' => 'licenseadminpanel'));
873
874             $m->connect('panel/plugins', array('action' => 'pluginsadminpanel'));
875             $m->connect('panel/plugins/enable/:plugin',
876                         array('action' => 'pluginenable'),
877                         array('plugin' => '[A-Za-z0-9_]+'));
878             $m->connect('panel/plugins/disable/:plugin',
879                         array('action' => 'plugindisable'),
880                         array('plugin' => '[A-Za-z0-9_]+'));
881
882             $m->connect('getfile/:filename',
883                         array('action' => 'getfile'),
884                         array('filename' => '[A-Za-z0-9._-]+'));
885
886             // In the "root"
887
888             if (common_config('singleuser', 'enabled')) {
889
890                 $nickname = User::singleUserNickname();
891
892                 foreach (array('subscriptions', 'subscribers',
893                                'all', 'foaf', 'replies',
894                                'microsummary', 'hcard') as $a) {
895                     $m->connect($a,
896                                 array('action' => $a,
897                                       'nickname' => $nickname));
898                 }
899
900                 foreach (array('subscriptions', 'subscribers') as $a) {
901                     $m->connect($a.'/:tag',
902                                 array('action' => $a,
903                                       'nickname' => $nickname),
904                                 array('tag' => self::REGEX_TAG));
905                 }
906
907                 $m->connect('subscribers/pending',
908                             array('action' => 'subqueue',
909                                   'nickname' => $nickname));
910
911                 foreach (array('rss', 'groups') as $a) {
912                     $m->connect($a,
913                                 array('action' => 'user'.$a,
914                                       'nickname' => $nickname));
915                 }
916
917                 foreach (array('all', 'replies', 'favorites') as $a) {
918                     $m->connect($a.'/rss',
919                                 array('action' => $a.'rss',
920                                       'nickname' => $nickname));
921                 }
922
923                 $m->connect('favorites',
924                             array('action' => 'showfavorites',
925                                   'nickname' => $nickname));
926
927                 $m->connect('avatar/:size',
928                             array('action' => 'avatarbynickname',
929                                   'nickname' => $nickname),
930                             array('size' => '(original|96|48|24)'));
931
932                 $m->connect('tag/:tag/rss',
933                             array('action' => 'userrss',
934                                   'nickname' => $nickname),
935                             array('tag' => self::REGEX_TAG));
936
937                 $m->connect('tag/:tag',
938                             array('action' => 'showstream',
939                                   'nickname' => $nickname),
940                             array('tag' => self::REGEX_TAG));
941
942                 $m->connect('rsd.xml',
943                             array('action' => 'rsd',
944                                   'nickname' => $nickname));
945
946                 $m->connect('',
947                             array('action' => 'showstream',
948                                   'nickname' => $nickname));
949             } else {
950                 $m->connect('', array('action' => 'public'));
951                 $m->connect('rss', array('action' => 'publicrss'));
952                 $m->connect('featuredrss', array('action' => 'featuredrss'));
953                 $m->connect('favoritedrss', array('action' => 'favoritedrss'));
954                 $m->connect('featured/', array('action' => 'featured'));
955                 $m->connect('featured', array('action' => 'featured'));
956                 $m->connect('favorited/', array('action' => 'favorited'));
957                 $m->connect('favorited', array('action' => 'favorited'));
958                 $m->connect('rsd.xml', array('action' => 'rsd'));
959
960                 foreach (array('subscriptions', 'subscribers',
961                                'nudge', 'all', 'foaf', 'replies',
962                                'inbox', 'outbox', 'microsummary', 'hcard') as $a) {
963                     $m->connect(':nickname/'.$a,
964                                 array('action' => $a),
965                                 array('nickname' => Nickname::DISPLAY_FMT));
966                 }
967                 $m->connect(':nickname/subscribers/pending',
968                             array('action' => 'subqueue'),
969                             array('nickname' => Nickname::DISPLAY_FMT));
970
971                 // people tags
972
973                 $m->connect('peopletags', array('action' => 'publicpeopletagcloud'));
974
975                 $m->connect('peopletag/:tag', array('action' => 'peopletag',
976                                                     'tag'    => self::REGEX_TAG));
977
978                 $m->connect('selftag/:tag', array('action' => 'selftag',
979                                                   'tag'    => self::REGEX_TAG));
980
981                 $m->connect('main/addpeopletag', array('action' => 'addpeopletag'));
982
983                 $m->connect('main/removepeopletag', array('action' => 'removepeopletag'));
984
985                 $m->connect('main/profilecompletion', array('action' => 'profilecompletion'));
986
987                 $m->connect('main/peopletagautocomplete', array('action' => 'peopletagautocomplete'));
988
989                 $m->connect(':nickname/peopletags',
990                                 array('action' => 'peopletagsbyuser',
991                                       'nickname' => Nickname::DISPLAY_FMT));
992
993                 $m->connect(':nickname/peopletags/private',
994                                 array('action' => 'peopletagsbyuser',
995                                       'nickname' => Nickname::DISPLAY_FMT,
996                                       'private' => 1));
997
998                 $m->connect(':nickname/peopletags/public',
999                                 array('action' => 'peopletagsbyuser',
1000                                       'nickname' => Nickname::DISPLAY_FMT,
1001                                       'public' => 1));
1002
1003                 $m->connect(':nickname/othertags',
1004                                 array('action' => 'peopletagsforuser',
1005                                       'nickname' => Nickname::DISPLAY_FMT));
1006
1007                 $m->connect(':nickname/peopletagsubscriptions',
1008                                 array('action' => 'peopletagsubscriptions',
1009                                       'nickname' => Nickname::DISPLAY_FMT));
1010
1011                 $m->connect(':tagger/all/:tag/subscribers',
1012                                 array('action' => 'peopletagsubscribers',
1013                                       'tagger' => Nickname::DISPLAY_FMT,
1014                                       'tag' => self::REGEX_TAG));
1015
1016                 $m->connect(':tagger/all/:tag/tagged',
1017                                 array('action' => 'peopletagged',
1018                                       'tagger' => Nickname::DISPLAY_FMT,
1019                                       'tag' => self::REGEX_TAG));
1020
1021                 $m->connect(':tagger/all/:tag/edit',
1022                                 array('action' => 'editpeopletag',
1023                                       'tagger' => Nickname::DISPLAY_FMT,
1024                                       'tag' => self::REGEX_TAG));
1025
1026                 foreach(array('subscribe', 'unsubscribe') as $v) {
1027                     $m->connect('peopletag/:id/'.$v,
1028                                     array('action' => $v.'peopletag',
1029                                           'id' => '[0-9]{1,64}'));
1030                 }
1031                 $m->connect('user/:tagger_id/profiletag/:id/id',
1032                                 array('action' => 'profiletagbyid',
1033                                       'tagger_id' => '[0-9]+',
1034                                       'id' => '[0-9]+'));
1035
1036                 $m->connect(':tagger/all/:tag',
1037                                 array('action' => 'showprofiletag',
1038                                       'tagger' => Nickname::DISPLAY_FMT,
1039                                       'tag' => self::REGEX_TAG));
1040
1041                 foreach (array('subscriptions', 'subscribers') as $a) {
1042                     $m->connect(':nickname/'.$a.'/:tag',
1043                                 array('action' => $a),
1044                                 array('tag' => self::REGEX_TAG,
1045                                       'nickname' => Nickname::DISPLAY_FMT));
1046                 }
1047
1048                 foreach (array('rss', 'groups') as $a) {
1049                     $m->connect(':nickname/'.$a,
1050                                 array('action' => 'user'.$a),
1051                                 array('nickname' => Nickname::DISPLAY_FMT));
1052                 }
1053
1054                 foreach (array('all', 'replies', 'favorites') as $a) {
1055                     $m->connect(':nickname/'.$a.'/rss',
1056                                 array('action' => $a.'rss'),
1057                                 array('nickname' => Nickname::DISPLAY_FMT));
1058                 }
1059
1060                 $m->connect(':nickname/favorites',
1061                             array('action' => 'showfavorites'),
1062                             array('nickname' => Nickname::DISPLAY_FMT));
1063
1064                 $m->connect(':nickname/avatar/:size',
1065                             array('action' => 'avatarbynickname'),
1066                             array('size' => '(original|96|48|24)',
1067                                   'nickname' => Nickname::DISPLAY_FMT));
1068
1069                 $m->connect(':nickname/tag/:tag/rss',
1070                             array('action' => 'userrss'),
1071                             array('nickname' => Nickname::DISPLAY_FMT),
1072                             array('tag' => self::REGEX_TAG));
1073
1074                 $m->connect(':nickname/tag/:tag',
1075                             array('action' => 'showstream'),
1076                             array('nickname' => Nickname::DISPLAY_FMT),
1077                             array('tag' => self::REGEX_TAG));
1078
1079                 $m->connect(':nickname/rsd.xml',
1080                             array('action' => 'rsd'),
1081                             array('nickname' => Nickname::DISPLAY_FMT));
1082
1083                 $m->connect(':nickname',
1084                             array('action' => 'showstream'),
1085                             array('nickname' => Nickname::DISPLAY_FMT));
1086             }
1087
1088             // AtomPub API
1089
1090             $m->connect('api/statusnet/app/service/:id.xml',
1091                         array('action' => 'ApiAtomService'),
1092                         array('id' => Nickname::DISPLAY_FMT));
1093
1094             $m->connect('api/statusnet/app/service.xml',
1095                         array('action' => 'ApiAtomService'));
1096
1097             $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom',
1098                         array('action' => 'AtomPubShowSubscription'),
1099                         array('subscriber' => '[0-9]+',
1100                               'subscribed' => '[0-9]+'));
1101
1102             $m->connect('api/statusnet/app/subscriptions/:subscriber.atom',
1103                         array('action' => 'AtomPubSubscriptionFeed'),
1104                         array('subscriber' => '[0-9]+'));
1105
1106             $m->connect('api/statusnet/app/favorites/:profile/:notice.atom',
1107                         array('action' => 'AtomPubShowFavorite'),
1108                         array('profile' => '[0-9]+',
1109                               'notice' => '[0-9]+'));
1110
1111             $m->connect('api/statusnet/app/favorites/:profile.atom',
1112                         array('action' => 'AtomPubFavoriteFeed'),
1113                         array('profile' => '[0-9]+'));
1114
1115             $m->connect('api/statusnet/app/memberships/:profile/:group.atom',
1116                         array('action' => 'AtomPubShowMembership'),
1117                         array('profile' => '[0-9]+',
1118                               'group' => '[0-9]+'));
1119
1120             $m->connect('api/statusnet/app/memberships/:profile.atom',
1121                         array('action' => 'AtomPubMembershipFeed'),
1122                         array('profile' => '[0-9]+'));
1123
1124             // URL shortening
1125
1126             $m->connect('url/:id',
1127                         array('action' => 'redirecturl',
1128                               'id' => '[0-9]+'));
1129
1130             // user stuff
1131
1132             Event::handle('RouterInitialized', array($m));
1133         }
1134
1135         return $m;
1136     }
1137
1138     function map($path)
1139     {
1140         try {
1141             $match = $this->m->match($path);
1142         } catch (Net_URL_Mapper_InvalidException $e) {
1143             common_log(LOG_ERR, "Problem getting route for $path - " .
1144                        $e->getMessage());
1145             // TRANS: Client error on action trying to visit a non-existing page.
1146             $cac = new ClientErrorAction(_('Page not found.'), 404);
1147             $cac->showPage();
1148         }
1149
1150         return $match;
1151     }
1152
1153     function build($action, $args=null, $params=null, $fragment=null)
1154     {
1155         $action_arg = array('action' => $action);
1156
1157         if ($args) {
1158             $args = array_merge($action_arg, $args);
1159         } else {
1160             $args = $action_arg;
1161         }
1162
1163         $url = $this->m->generate($args, $params, $fragment);
1164
1165         // Due to a bug in the Net_URL_Mapper code, the returned URL may
1166         // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We
1167         // repair that here rather than modifying the upstream code...
1168
1169         $qpos = strpos($url, '?');
1170         if ($qpos !== false) {
1171             $url = substr($url, 0, $qpos+1) .
1172                 str_replace('?', '&', substr($url, $qpos+1));
1173
1174             // @fixme this is a hacky workaround for http_build_query in the
1175             // lower-level code and bad configs that set the default separator
1176             // to &amp; instead of &. Encoded &s in parameters will not be
1177             // affected.
1178             $url = substr($url, 0, $qpos+1) .
1179                 str_replace('&amp;', '&', substr($url, $qpos+1));
1180
1181         }
1182
1183         return $url;
1184     }
1185 }