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