]> 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             // Lists (people tags)
768
769             $m->connect('api/lists/memberships.:format',
770                         array('action' => 'ApiListMemberships',
771                               'format' => '(xml|json)'));
772
773             $m->connect('api/:user/lists/memberships.:format',
774                         array('action' => 'ApiListMemberships',
775                               'user' => '[a-zA-Z0-9]+',
776                               'format' => '(xml|json)'));
777
778             $m->connect('api/lists/subscriptions.:format',
779                         array('action' => 'ApiListSubscriptions',
780                               'format' => '(xml|json)'));
781
782             $m->connect('api/:user/lists/subscriptions.:format',
783                         array('action' => 'ApiListSubscriptions',
784                               'user' => '[a-zA-Z0-9]+',
785                               'format' => '(xml|json)'));
786             $m->connect('api/lists.:format',
787                         array('action' => 'ApiLists',
788                               'format' => '(xml|json)'));
789
790             $m->connect('api/:user/lists.:format',
791                         array('action' => 'ApiLists',
792                               'user' => '[a-zA-Z0-9]+',
793                               'format' => '(xml|json)'));
794
795             $m->connect('api/:user/lists/:id.:format',
796                         array('action' => 'ApiList',
797                               'user' => '[a-zA-Z0-9]+',
798                               'id' => '[a-zA-Z0-9]+',
799                               'format' => '(xml|json)'));
800
801             $m->connect('api/:user/lists/:id/statuses.:format',
802                         array('action' => 'ApiTimelineList',
803                               'user' => '[a-zA-Z0-9]+',
804                               'id' => '[a-zA-Z0-9]+',
805                               'format' => '(xml|json|rss|atom)'));
806
807             $m->connect('api/:user/:list_id/members.:format',
808                         array('action' => 'ApiListMembers',
809                               'user' => '[a-zA-Z0-9]+',
810                               'list_id' => '[a-zA-Z0-9]+',
811                               'format' => '(xml|json)'));
812
813             $m->connect('api/:user/:list_id/subscribers.:format',
814                         array('action' => 'ApiListSubscribers',
815                               'user' => '[a-zA-Z0-9]+',
816                               'list_id' => '[a-zA-Z0-9]+',
817                               'format' => '(xml|json)'));
818
819             $m->connect('api/:user/:list_id/members/:id.:format',
820                         array('action' => 'ApiListMember',
821                               'user' => '[a-zA-Z0-9]+',
822                               'list_id' => '[a-zA-Z0-9]+',
823                               'id' => '[a-zA-Z0-9]+',
824                               'format' => '(xml|json)'));
825
826             $m->connect('api/:user/:list_id/subscribers/:id.:format',
827                         array('action' => 'ApiListSubscriber',
828                               'user' => '[a-zA-Z0-9]+',
829                               'list_id' => '[a-zA-Z0-9]+',
830                               'id' => '[a-zA-Z0-9]+',
831                               'format' => '(xml|json)'));
832
833             // Tags
834             $m->connect('api/statusnet/tags/timeline/:tag.:format',
835                         array('action' => 'ApiTimelineTag',
836                               'format' => '(xml|json|rss|atom|as)'));
837
838             // media related
839             $m->connect(
840                 'api/statusnet/media/upload',
841                 array('action' => 'ApiMediaUpload')
842             );
843
844             // search
845             $m->connect('api/search.atom', array('action' => 'ApiSearchAtom'));
846             $m->connect('api/search.json', array('action' => 'ApiSearchJSON'));
847             $m->connect('api/trends.json', array('action' => 'ApiTrends'));
848
849             $m->connect('api/oauth/request_token',
850                         array('action' => 'ApiOauthRequestToken'));
851
852             $m->connect('api/oauth/access_token',
853                         array('action' => 'ApiOauthAccessToken'));
854
855             $m->connect('api/oauth/authorize',
856                         array('action' => 'ApiOauthAuthorize'));
857
858             // Admin
859
860             $m->connect('panel/site', array('action' => 'siteadminpanel'));
861             $m->connect('panel/user', array('action' => 'useradminpanel'));
862                 $m->connect('panel/access', array('action' => 'accessadminpanel'));
863             $m->connect('panel/paths', array('action' => 'pathsadminpanel'));
864             $m->connect('panel/sessions', array('action' => 'sessionsadminpanel'));
865             $m->connect('panel/sitenotice', array('action' => 'sitenoticeadminpanel'));
866             $m->connect('panel/snapshot', array('action' => 'snapshotadminpanel'));
867             $m->connect('panel/license', array('action' => 'licenseadminpanel'));
868
869             $m->connect('panel/plugins', array('action' => 'pluginsadminpanel'));
870             $m->connect('panel/plugins/enable/:plugin',
871                         array('action' => 'pluginenable'),
872                         array('plugin' => '[A-Za-z0-9_]+'));
873             $m->connect('panel/plugins/disable/:plugin',
874                         array('action' => 'plugindisable'),
875                         array('plugin' => '[A-Za-z0-9_]+'));
876
877             $m->connect('getfile/:filename',
878                         array('action' => 'getfile'),
879                         array('filename' => '[A-Za-z0-9._-]+'));
880
881             // In the "root"
882
883             if (common_config('singleuser', 'enabled')) {
884
885                 $nickname = User::singleUserNickname();
886
887                 foreach (array('subscriptions', 'subscribers',
888                                'all', 'foaf', 'replies',
889                                'microsummary', 'hcard') as $a) {
890                     $m->connect($a,
891                                 array('action' => $a,
892                                       'nickname' => $nickname));
893                 }
894
895                 foreach (array('subscriptions', 'subscribers') as $a) {
896                     $m->connect($a.'/:tag',
897                                 array('action' => $a,
898                                       'nickname' => $nickname),
899                                 array('tag' => self::REGEX_TAG));
900                 }
901
902                 $m->connect('subscribers/pending',
903                             array('action' => 'subqueue',
904                                   'nickname' => $nickname));
905
906                 foreach (array('rss', 'groups') as $a) {
907                     $m->connect($a,
908                                 array('action' => 'user'.$a,
909                                       'nickname' => $nickname));
910                 }
911
912                 foreach (array('all', 'replies', 'favorites') as $a) {
913                     $m->connect($a.'/rss',
914                                 array('action' => $a.'rss',
915                                       'nickname' => $nickname));
916                 }
917
918                 $m->connect('favorites',
919                             array('action' => 'showfavorites',
920                                   'nickname' => $nickname));
921
922                 $m->connect('avatar/:size',
923                             array('action' => 'avatarbynickname',
924                                   'nickname' => $nickname),
925                             array('size' => '(original|96|48|24)'));
926
927                 $m->connect('tag/:tag/rss',
928                             array('action' => 'userrss',
929                                   'nickname' => $nickname),
930                             array('tag' => self::REGEX_TAG));
931
932                 $m->connect('tag/:tag',
933                             array('action' => 'showstream',
934                                   'nickname' => $nickname),
935                             array('tag' => self::REGEX_TAG));
936
937                 $m->connect('rsd.xml',
938                             array('action' => 'rsd',
939                                   'nickname' => $nickname));
940
941                 $m->connect('',
942                             array('action' => 'showstream',
943                                   'nickname' => $nickname));
944             } else {
945                 $m->connect('', array('action' => 'public'));
946                 $m->connect('rss', array('action' => 'publicrss'));
947                 $m->connect('featuredrss', array('action' => 'featuredrss'));
948                 $m->connect('favoritedrss', array('action' => 'favoritedrss'));
949                 $m->connect('featured/', array('action' => 'featured'));
950                 $m->connect('featured', array('action' => 'featured'));
951                 $m->connect('favorited/', array('action' => 'favorited'));
952                 $m->connect('favorited', array('action' => 'favorited'));
953                 $m->connect('rsd.xml', array('action' => 'rsd'));
954
955                 foreach (array('subscriptions', 'subscribers',
956                                'nudge', 'all', 'foaf', 'replies',
957                                'inbox', 'outbox', 'microsummary', 'hcard') as $a) {
958                     $m->connect(':nickname/'.$a,
959                                 array('action' => $a),
960                                 array('nickname' => Nickname::DISPLAY_FMT));
961                 }
962                 $m->connect(':nickname/subscribers/pending',
963                             array('action' => 'subqueue'),
964                             array('nickname' => Nickname::DISPLAY_FMT));
965
966                 // people tags
967
968                 $m->connect('peopletags', array('action' => 'publicpeopletagcloud'));
969
970                 $m->connect('peopletag/:tag', array('action' => 'peopletag',
971                                                     'tag'    => self::REGEX_TAG));
972
973                 $m->connect('selftag/:tag', array('action' => 'selftag',
974                                                   'tag'    => self::REGEX_TAG));
975
976                 $m->connect('main/addpeopletag', array('action' => 'addpeopletag'));
977
978                 $m->connect('main/removepeopletag', array('action' => 'removepeopletag'));
979
980                 $m->connect('main/profilecompletion', array('action' => 'profilecompletion'));
981
982                 $m->connect('main/peopletagautocomplete', array('action' => 'peopletagautocomplete'));
983
984                 $m->connect(':nickname/peopletags',
985                                 array('action' => 'peopletagsbyuser',
986                                       'nickname' => Nickname::DISPLAY_FMT));
987
988                 $m->connect(':nickname/peopletags/private',
989                                 array('action' => 'peopletagsbyuser',
990                                       'nickname' => Nickname::DISPLAY_FMT,
991                                       'private' => 1));
992
993                 $m->connect(':nickname/peopletags/public',
994                                 array('action' => 'peopletagsbyuser',
995                                       'nickname' => Nickname::DISPLAY_FMT,
996                                       'public' => 1));
997
998                 $m->connect(':nickname/othertags',
999                                 array('action' => 'peopletagsforuser',
1000                                       'nickname' => Nickname::DISPLAY_FMT));
1001
1002                 $m->connect(':nickname/peopletagsubscriptions',
1003                                 array('action' => 'peopletagsubscriptions',
1004                                       'nickname' => Nickname::DISPLAY_FMT));
1005
1006                 $m->connect(':tagger/all/:tag/subscribers',
1007                                 array('action' => 'peopletagsubscribers',
1008                                       'tagger' => Nickname::DISPLAY_FMT,
1009                                       'tag' => self::REGEX_TAG));
1010
1011                 $m->connect(':tagger/all/:tag/tagged',
1012                                 array('action' => 'peopletagged',
1013                                       'tagger' => Nickname::DISPLAY_FMT,
1014                                       'tag' => self::REGEX_TAG));
1015
1016                 $m->connect(':tagger/all/:tag/edit',
1017                                 array('action' => 'editpeopletag',
1018                                       'tagger' => Nickname::DISPLAY_FMT,
1019                                       'tag' => self::REGEX_TAG));
1020
1021                 foreach(array('subscribe', 'unsubscribe') as $v) {
1022                     $m->connect('peopletag/:id/'.$v,
1023                                     array('action' => $v.'peopletag',
1024                                           'id' => '[0-9]{1,64}'));
1025                 }
1026                 $m->connect('user/:tagger_id/profiletag/:id/id',
1027                                 array('action' => 'profiletagbyid',
1028                                       'tagger_id' => '[0-9]+',
1029                                       'id' => '[0-9]+'));
1030
1031                 $m->connect(':tagger/all/:tag',
1032                                 array('action' => 'showprofiletag',
1033                                       'tagger' => Nickname::DISPLAY_FMT,
1034                                       'tag' => self::REGEX_TAG));
1035
1036                 foreach (array('subscriptions', 'subscribers') as $a) {
1037                     $m->connect(':nickname/'.$a.'/:tag',
1038                                 array('action' => $a),
1039                                 array('tag' => self::REGEX_TAG,
1040                                       'nickname' => Nickname::DISPLAY_FMT));
1041                 }
1042
1043                 foreach (array('rss', 'groups') as $a) {
1044                     $m->connect(':nickname/'.$a,
1045                                 array('action' => 'user'.$a),
1046                                 array('nickname' => Nickname::DISPLAY_FMT));
1047                 }
1048
1049                 foreach (array('all', 'replies', 'favorites') as $a) {
1050                     $m->connect(':nickname/'.$a.'/rss',
1051                                 array('action' => $a.'rss'),
1052                                 array('nickname' => Nickname::DISPLAY_FMT));
1053                 }
1054
1055                 $m->connect(':nickname/favorites',
1056                             array('action' => 'showfavorites'),
1057                             array('nickname' => Nickname::DISPLAY_FMT));
1058
1059                 $m->connect(':nickname/avatar/:size',
1060                             array('action' => 'avatarbynickname'),
1061                             array('size' => '(original|96|48|24)',
1062                                   'nickname' => Nickname::DISPLAY_FMT));
1063
1064                 $m->connect(':nickname/tag/:tag/rss',
1065                             array('action' => 'userrss'),
1066                             array('nickname' => Nickname::DISPLAY_FMT),
1067                             array('tag' => self::REGEX_TAG));
1068
1069                 $m->connect(':nickname/tag/:tag',
1070                             array('action' => 'showstream'),
1071                             array('nickname' => Nickname::DISPLAY_FMT),
1072                             array('tag' => self::REGEX_TAG));
1073
1074                 $m->connect(':nickname/rsd.xml',
1075                             array('action' => 'rsd'),
1076                             array('nickname' => Nickname::DISPLAY_FMT));
1077
1078                 $m->connect(':nickname',
1079                             array('action' => 'showstream'),
1080                             array('nickname' => Nickname::DISPLAY_FMT));
1081             }
1082
1083             // AtomPub API
1084
1085             $m->connect('api/statusnet/app/service/:id.xml',
1086                         array('action' => 'ApiAtomService'),
1087                         array('id' => Nickname::DISPLAY_FMT));
1088
1089             $m->connect('api/statusnet/app/service.xml',
1090                         array('action' => 'ApiAtomService'));
1091
1092             $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom',
1093                         array('action' => 'AtomPubShowSubscription'),
1094                         array('subscriber' => '[0-9]+',
1095                               'subscribed' => '[0-9]+'));
1096
1097             $m->connect('api/statusnet/app/subscriptions/:subscriber.atom',
1098                         array('action' => 'AtomPubSubscriptionFeed'),
1099                         array('subscriber' => '[0-9]+'));
1100
1101             $m->connect('api/statusnet/app/favorites/:profile/:notice.atom',
1102                         array('action' => 'AtomPubShowFavorite'),
1103                         array('profile' => '[0-9]+',
1104                               'notice' => '[0-9]+'));
1105
1106             $m->connect('api/statusnet/app/favorites/:profile.atom',
1107                         array('action' => 'AtomPubFavoriteFeed'),
1108                         array('profile' => '[0-9]+'));
1109
1110             $m->connect('api/statusnet/app/memberships/:profile/:group.atom',
1111                         array('action' => 'AtomPubShowMembership'),
1112                         array('profile' => '[0-9]+',
1113                               'group' => '[0-9]+'));
1114
1115             $m->connect('api/statusnet/app/memberships/:profile.atom',
1116                         array('action' => 'AtomPubMembershipFeed'),
1117                         array('profile' => '[0-9]+'));
1118
1119             // URL shortening
1120
1121             $m->connect('url/:id',
1122                         array('action' => 'redirecturl',
1123                               'id' => '[0-9]+'));
1124
1125             // user stuff
1126
1127             Event::handle('RouterInitialized', array($m));
1128         }
1129
1130         return $m;
1131     }
1132
1133     function map($path)
1134     {
1135         try {
1136             $match = $this->m->match($path);
1137         } catch (Net_URL_Mapper_InvalidException $e) {
1138             common_log(LOG_ERR, "Problem getting route for $path - " .
1139                        $e->getMessage());
1140             // TRANS: Client error on action trying to visit a non-existing page.
1141             $cac = new ClientErrorAction(_('Page not found.'), 404);
1142             $cac->showPage();
1143         }
1144
1145         return $match;
1146     }
1147
1148     function build($action, $args=null, $params=null, $fragment=null)
1149     {
1150         $action_arg = array('action' => $action);
1151
1152         if ($args) {
1153             $args = array_merge($action_arg, $args);
1154         } else {
1155             $args = $action_arg;
1156         }
1157
1158         $url = $this->m->generate($args, $params, $fragment);
1159
1160         // Due to a bug in the Net_URL_Mapper code, the returned URL may
1161         // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We
1162         // repair that here rather than modifying the upstream code...
1163
1164         $qpos = strpos($url, '?');
1165         if ($qpos !== false) {
1166             $url = substr($url, 0, $qpos+1) .
1167                 str_replace('?', '&', substr($url, $qpos+1));
1168
1169             // @fixme this is a hacky workaround for http_build_query in the
1170             // lower-level code and bad configs that set the default separator
1171             // to &amp; instead of &. Encoded &s in parameters will not be
1172             // affected.
1173             $url = substr($url, 0, $qpos+1) .
1174                 str_replace('&amp;', '&', substr($url, $qpos+1));
1175
1176         }
1177
1178         return $url;
1179     }
1180 }