Merged stuff from upstream/master
[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 /**
35  * URL Router
36  *
37  * Cheap wrapper around URLMapper
38  *
39  * @category URL
40  * @package  StatusNet
41  * @author   Evan Prodromou <evan@status.net>
42  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
43  * @link     http://status.net/
44  */
45 class Router
46 {
47     var $m = null;
48     static $inst = null;
49
50     const REGEX_TAG = '[^\/]+'; // [\pL\pN_\-\.]{1,64} better if we can do unicode regexes
51
52     static function get()
53     {
54         if (!Router::$inst) {
55             Router::$inst = new Router();
56         }
57         return Router::$inst;
58     }
59
60     /**
61      * Clear the global singleton instance for this class.
62      * Needed to ensure reset when switching site configurations.
63      */
64     static function clear()
65     {
66         Router::$inst = null;
67     }
68
69     function __construct()
70     {
71         if (empty($this->m)) {
72             $this->m = $this->initialize();
73         }
74     }
75
76     /**
77      * Create a unique hashkey for the router.
78      *
79      * The router's url map can change based on the version of the software
80      * you're running and the plugins that are enabled. To avoid having bad routes
81      * get stuck in the cache, the key includes a list of plugins and the software
82      * version.
83      * 
84     * There can still be problems with a) differences in versions of the plugins and
85      * b) people running code between official versions, but these tend to be more
86      * sophisticated users who can grok what's going on and clear their caches.
87      *
88      * @return string cache key string that should uniquely identify a router
89      */
90
91     static function cacheKey()
92     {
93         $parts = array('router');
94
95         // Many router paths depend on this setting.
96         if (common_config('singleuser', 'enabled')) {
97             $parts[] = '1user';
98         } else {
99             $parts[] = 'multi';
100         }
101
102         return Cache::codeKey(implode(':', $parts));
103     }
104
105     function initialize()
106     {
107         $m = new URLMapper();
108
109         if (Event::handle('StartInitializeRouter', array(&$m))) {
110
111             // top of the menu hierarchy, sometimes "Home"
112             $m->connect('', array('action' => 'top'));
113
114             // public endpoints
115
116             $m->connect('robots.txt', array('action' => 'robotstxt'));
117
118             $m->connect('opensearch/people', array('action' => 'opensearch',
119                                                    'type' => 'people'));
120             $m->connect('opensearch/notice', array('action' => 'opensearch',
121                                                    'type' => 'notice'));
122
123             // docs
124
125             $m->connect('doc/:title', array('action' => 'doc'));
126
127             $m->connect('main/otp/:user_id/:token',
128                         array('action' => 'otp'),
129                         array('user_id' => '[0-9]+',
130                               'token' => '.+'));
131
132             // these take a code; before the main part
133
134             foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
135                 $m->connect('main/'.$c.'/:code', array('action' => $c));
136             }
137
138             // Also need a block variant accepting ID on URL for mail links
139             $m->connect('main/block/:profileid',
140                         array('action' => 'block'),
141                         array('profileid' => '[0-9]+'));
142
143             $m->connect('main/sup/:seconds', array('action' => 'sup'),
144                         array('seconds' => '[0-9]+'));
145
146             // main stuff is repetitive
147
148             $main = array('login', 'logout', 'register', 'subscribe',
149                           'unsubscribe', 'cancelsubscription', 'approvesub',
150                           'confirmaddress', 'recoverpassword',
151                           'invite', 'sup',
152                           'block', 'unblock', 'subedit',
153                           'groupblock', 'groupunblock',
154                           'sandbox', 'unsandbox',
155                           'silence', 'unsilence',
156                           'grantrole', 'revokerole',
157                           'deleteuser',
158                           'geocode',
159                           'version',
160                           'backupaccount',
161                           'deleteaccount',
162                           'restoreaccount',
163                           'top',
164                           'public',
165             );
166
167             foreach ($main as $a) {
168                 $m->connect('main/'.$a, array('action' => $a));
169             }
170
171             $m->connect('main/all', array('action' => 'networkpublic'));
172
173             $m->connect('main/tagprofile/:id', array('action' => 'tagprofile'),
174                                                array('id' => '[0-9]+'));
175
176             $m->connect('main/tagprofile', array('action' => 'tagprofile'));
177
178             $m->connect('main/xrds',
179                         array('action' => 'publicxrds'));
180
181             // settings
182
183             foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections',
184                            'oauthapps', 'email', 'sms', 'url') as $s) {
185                 $m->connect('settings/' . $s, array('action' => $s . 'settings'));
186             }
187
188             if (common_config('oldschool', 'enabled')) {
189                 $m->connect('settings/oldschool', array('action' => 'oldschoolsettings'));
190             }
191
192             $m->connect('settings/oauthapps/show/:id',
193                         array('action' => 'showapplication'),
194                         array('id' => '[0-9]+')
195             );
196             $m->connect('settings/oauthapps/new',
197                         array('action' => 'newapplication')
198             );
199             $m->connect('settings/oauthapps/edit/:id',
200                         array('action' => 'editapplication'),
201                         array('id' => '[0-9]+')
202             );
203             $m->connect('settings/oauthapps/delete/:id',
204                         array('action' => 'deleteapplication'),
205                         array('id' => '[0-9]+')
206             );
207
208             // search
209
210             foreach (array('group', 'people', 'notice') as $s) {
211                 $m->connect('search/'.$s.'?q=:q',
212                             array('action' => $s.'search'),
213                             array('q' => '.+'));
214                 $m->connect('search/'.$s, array('action' => $s.'search'));
215             }
216
217             // The second of these is needed to make the link work correctly
218             // when inserted into the page. The first is needed to match the
219             // route on the way in. Seems to be another URLMapper bug to me.
220             $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
221                         array('q' => '.+'));
222             $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
223
224             $m->connect('attachment/:attachment',
225                         array('action' => 'attachment'),
226                         array('attachment' => '[0-9]+'));
227
228             $m->connect('attachment/:attachment/thumbnail',
229                         array('action' => 'attachment_thumbnail'),
230                         array('attachment' => '[0-9]+'));
231
232             $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
233                         array('action' => 'newnotice'),
234                         array('replyto' => Nickname::DISPLAY_FMT),
235                         array('inreplyto' => '[0-9]+'));
236
237             $m->connect('notice/new?replyto=:replyto',
238                         array('action' => 'newnotice'),
239                         array('replyto' => Nickname::DISPLAY_FMT));
240
241             $m->connect('notice/new', array('action' => 'newnotice'));
242
243             $m->connect('notice/:notice',
244                         array('action' => 'shownotice'),
245                         array('notice' => '[0-9]+'));
246
247             $m->connect('notice/:notice/delete',
248                         array('action' => 'deletenotice'),
249                         array('notice' => '[0-9]+'));
250
251             // conversation
252
253             $m->connect('conversation/:id',
254                         array('action' => 'conversation'),
255                         array('id' => '[0-9]+'));
256
257             $m->connect('user/:id',
258                         array('action' => 'userbyid'),
259                         array('id' => '[0-9]+'));
260
261             if (!common_config('performance', 'high')) {
262                 $m->connect('tags/', array('action' => 'publictagcloud'));
263                 $m->connect('tag/', array('action' => 'publictagcloud'));
264                 $m->connect('tags', array('action' => 'publictagcloud'));
265                 $m->connect('tag', array('action' => 'publictagcloud'));
266             }
267             $m->connect('tag/:tag/rss',
268                         array('action' => 'tagrss'),
269                         array('tag' => self::REGEX_TAG));
270             $m->connect('tag/:tag',
271                         array('action' => 'tag'),
272                         array('tag' => self::REGEX_TAG));
273
274             // groups
275
276             $m->connect('group/new', array('action' => 'newgroup'));
277
278             foreach (array('edit', 'join', 'leave', 'delete', 'cancel', 'approve') as $v) {
279                 $m->connect('group/:nickname/'.$v,
280                             array('action' => $v.'group'),
281                             array('nickname' => Nickname::DISPLAY_FMT));
282                 $m->connect('group/:id/id/'.$v,
283                             array('action' => $v.'group'),
284                             array('id' => '[0-9]+'));
285             }
286
287             foreach (array('members', 'logo', 'rss') as $n) {
288                 $m->connect('group/:nickname/'.$n,
289                             array('action' => 'group'.$n),
290                             array('nickname' => Nickname::DISPLAY_FMT));
291             }
292
293             $m->connect('group/:nickname/foaf',
294                         array('action' => 'foafgroup'),
295                         array('nickname' => Nickname::DISPLAY_FMT));
296
297             $m->connect('group/:nickname/blocked',
298                         array('action' => 'blockedfromgroup'),
299                         array('nickname' => Nickname::DISPLAY_FMT));
300
301             $m->connect('group/:nickname/makeadmin',
302                         array('action' => 'makeadmin'),
303                         array('nickname' => Nickname::DISPLAY_FMT));
304
305             $m->connect('group/:nickname/members/pending',
306                         array('action' => 'groupqueue'),
307                         array('nickname' => Nickname::DISPLAY_FMT));
308
309             $m->connect('group/:id/id',
310                         array('action' => 'groupbyid'),
311                         array('id' => '[0-9]+'));
312
313             $m->connect('group/:nickname',
314                         array('action' => 'showgroup'),
315                         array('nickname' => Nickname::DISPLAY_FMT));
316
317             $m->connect('group/:nickname/',
318                         array('action' => 'showgroup'),
319                         array('nickname' => Nickname::DISPLAY_FMT));
320
321             $m->connect('group/', array('action' => 'groups'));
322             $m->connect('group', array('action' => 'groups'));
323             $m->connect('groups/', array('action' => 'groups'));
324             $m->connect('groups', array('action' => 'groups'));
325
326             // Twitter-compatible API
327
328             // statuses API
329
330             $m->connect('api',
331                         array('action' => 'Redirect',
332                               'nextAction' => 'doc',
333                               'args' => array('title' => 'api')));
334
335             $m->connect('api/statuses/public_timeline.:format',
336                         array('action' => 'ApiTimelinePublic',
337                               'format' => '(xml|json|rss|atom|as)'));
338
339             // this is not part of the Twitter API. Also may require authentication depending on server config!
340             $m->connect('api/statuses/networkpublic_timeline.:format',
341                         array('action' => 'ApiTimelineNetworkPublic',
342                               'format' => '(xml|json|rss|atom|as)'));
343
344             $m->connect('api/statuses/friends_timeline/:id.:format',
345                         array('action' => 'ApiTimelineFriends',
346                               'id' => Nickname::INPUT_FMT,
347                               'format' => '(xml|json|rss|atom|as)'));
348
349             $m->connect('api/statuses/friends_timeline.:format',
350                         array('action' => 'ApiTimelineFriends',
351                               'format' => '(xml|json|rss|atom|as)'));
352
353             $m->connect('api/statuses/home_timeline/:id.:format',
354                         array('action' => 'ApiTimelineHome',
355                               'id' => Nickname::INPUT_FMT,
356                               'format' => '(xml|json|rss|atom|as)'));
357
358             $m->connect('api/statuses/home_timeline.:format',
359                         array('action' => 'ApiTimelineHome',
360                               'format' => '(xml|json|rss|atom|as)'));
361
362             $m->connect('api/statuses/user_timeline/:id.:format',
363                         array('action' => 'ApiTimelineUser',
364                               'id' => Nickname::INPUT_FMT,
365                               'format' => '(xml|json|rss|atom|as)'));
366
367             $m->connect('api/statuses/user_timeline.:format',
368                         array('action' => 'ApiTimelineUser',
369                               'format' => '(xml|json|rss|atom|as)'));
370
371             $m->connect('api/statuses/mentions/:id.:format',
372                         array('action' => 'ApiTimelineMentions',
373                               'id' => Nickname::INPUT_FMT,
374                               'format' => '(xml|json|rss|atom|as)'));
375
376             $m->connect('api/statuses/mentions.:format',
377                         array('action' => 'ApiTimelineMentions',
378                               'format' => '(xml|json|rss|atom|as)'));
379
380             $m->connect('api/statuses/replies/:id.:format',
381                         array('action' => 'ApiTimelineMentions',
382                               'id' => Nickname::INPUT_FMT,
383                               'format' => '(xml|json|rss|atom|as)'));
384
385             $m->connect('api/statuses/replies.:format',
386                         array('action' => 'ApiTimelineMentions',
387                               'format' => '(xml|json|rss|atom|as)'));
388  
389             $m->connect('api/statuses/mentions_timeline/:id.:format',
390                         array('action' => 'ApiTimelineMentions',
391                               'id' => Nickname::INPUT_FMT,
392                               'format' => '(xml|json|rss|atom|as)'));
393
394             $m->connect('api/statuses/mentions_timeline.:format',
395                         array('action' => 'ApiTimelineMentions',
396                               'format' => '(xml|json|rss|atom|as)'));
397
398             $m->connect('api/statuses/friends/:id.:format',
399                         array('action' => 'ApiUserFriends',
400                               'id' => Nickname::INPUT_FMT,
401                               'format' => '(xml|json)'));
402
403             $m->connect('api/statuses/friends.:format',
404                         array('action' => 'ApiUserFriends',
405                               'format' => '(xml|json)'));
406
407             $m->connect('api/statuses/followers/:id.:format',
408                         array('action' => 'ApiUserFollowers',
409                               'id' => Nickname::INPUT_FMT,
410                               'format' => '(xml|json)'));
411
412             $m->connect('api/statuses/followers.:format',
413                         array('action' => 'ApiUserFollowers',
414                               'format' => '(xml|json)'));
415
416             $m->connect('api/statuses/show/:id.:format',
417                         array('action' => 'ApiStatusesShow',
418                               'id' => '[0-9]+',
419                               'format' => '(xml|json|atom)'));
420
421             $m->connect('api/statuses/show.:format',
422                         array('action' => 'ApiStatusesShow',
423                               'format' => '(xml|json|atom)'));
424
425             $m->connect('api/statuses/update.:format',
426                         array('action' => 'ApiStatusesUpdate',
427                               'format' => '(xml|json)'));
428
429             $m->connect('api/statuses/destroy/:id.:format',
430                         array('action' => 'ApiStatusesDestroy',
431                               'id' => '[0-9]+',
432                               'format' => '(xml|json)'));
433
434             $m->connect('api/statuses/destroy.:format',
435                         array('action' => 'ApiStatusesDestroy',
436                               'format' => '(xml|json)'));
437
438             // START qvitter API additions
439             
440             $m->connect('api/attachment/:id.:format',
441                         array('action' => 'ApiAttachment',
442                               'id' => '[0-9]+',
443                               'format' => '(xml|json)'));
444             
445             $m->connect('api/checkhub.:format',
446                         array('action' => 'ApiCheckHub',
447                               'format' => '(xml|json)'));
448             
449             $m->connect('api/externalprofile/show.:format',
450                         array('action' => 'ApiExternalProfileShow',
451                               'format' => '(xml|json)'));
452
453             $m->connect('api/statusnet/groups/admins/:id.:format',
454                         array('action' => 'ApiGroupAdmins',
455                               'id' => Nickname::INPUT_FMT,
456                               'format' => '(xml|json)'));
457             
458             $m->connect('api/account/update_link_color.:format',
459                         array('action' => 'ApiAccountUpdateLinkColor',
460                               'format' => '(xml|json)'));
461                 
462             $m->connect('api/account/update_background_color.:format',
463                         array('action' => 'ApiAccountUpdateBackgroundColor',
464                               'format' => '(xml|json)'));
465
466             $m->connect('api/account/register.:format',
467                         array('action' => 'ApiAccountRegister',
468                               'format' => '(xml|json)'));
469             
470             $m->connect('api/check_nickname.:format',
471                         array('action' => 'ApiCheckNickname',
472                               'format' => '(xml|json)'));
473
474             // END qvitter API additions
475
476             // users
477
478             $m->connect('api/users/show/:id.:format',
479                         array('action' => 'ApiUserShow',
480                               'id' => Nickname::INPUT_FMT,
481                               'format' => '(xml|json)'));
482
483             $m->connect('api/users/show.:format',
484                         array('action' => 'ApiUserShow',
485                               'format' => '(xml|json)'));
486
487             $m->connect('api/users/profile_image/:screen_name.:format',
488                         array('action' => 'ApiUserProfileImage',
489                               'screen_name' => Nickname::DISPLAY_FMT,
490                               'format' => '(xml|json)'));
491
492             // friendships
493
494             $m->connect('api/friendships/show.:format',
495                         array('action' => 'ApiFriendshipsShow',
496                               'format' => '(xml|json)'));
497
498             $m->connect('api/friendships/exists.:format',
499                         array('action' => 'ApiFriendshipsExists',
500                               'format' => '(xml|json)'));
501
502             $m->connect('api/friendships/create/:id.:format',
503                         array('action' => 'ApiFriendshipsCreate',
504                               'id' => Nickname::INPUT_FMT,
505                               'format' => '(xml|json)'));
506
507             $m->connect('api/friendships/create.:format',
508                         array('action' => 'ApiFriendshipsCreate',
509                               'format' => '(xml|json)'));
510
511             $m->connect('api/friendships/destroy/:id.:format',
512                         array('action' => 'ApiFriendshipsDestroy',
513                               'id' => Nickname::INPUT_FMT,
514                               'format' => '(xml|json)'));
515
516             $m->connect('api/friendships/destroy.:format',
517                         array('action' => 'ApiFriendshipsDestroy',
518                               'format' => '(xml|json)'));
519
520             // Social graph
521
522             $m->connect('api/friends/ids/:id.:format',
523                         array('action' => 'ApiUserFriends',
524                               'ids_only' => true));
525
526             $m->connect('api/followers/ids/:id.:format',
527                         array('action' => 'ApiUserFollowers',
528                               'ids_only' => true));
529
530             $m->connect('api/friends/ids.:format',
531                         array('action' => 'ApiUserFriends',
532                               'ids_only' => true));
533
534             $m->connect('api/followers/ids.:format',
535                         array('action' => 'ApiUserFollowers',
536                               'ids_only' => true));
537
538             // account
539
540             $m->connect('api/account/verify_credentials.:format',
541                         array('action' => 'ApiAccountVerifyCredentials'));
542
543             $m->connect('api/account/update_profile.:format',
544                         array('action' => 'ApiAccountUpdateProfile'));
545
546             $m->connect('api/account/update_profile_image.:format',
547                         array('action' => 'ApiAccountUpdateProfileImage'));
548
549             $m->connect('api/account/update_delivery_device.:format',
550                         array('action' => 'ApiAccountUpdateDeliveryDevice'));
551
552             // special case where verify_credentials is called w/out a format
553
554             $m->connect('api/account/verify_credentials',
555                         array('action' => 'ApiAccountVerifyCredentials'));
556
557             $m->connect('api/account/rate_limit_status.:format',
558                         array('action' => 'ApiAccountRateLimitStatus'));
559
560             // blocks
561
562             $m->connect('api/blocks/create/:id.:format',
563                         array('action' => 'ApiBlockCreate',
564                               'id' => Nickname::INPUT_FMT,
565                               'format' => '(xml|json)'));
566
567             $m->connect('api/blocks/create.:format',
568                         array('action' => 'ApiBlockCreate',
569                               'format' => '(xml|json)'));
570
571             $m->connect('api/blocks/destroy/:id.:format',
572                         array('action' => 'ApiBlockDestroy',
573                               'id' => Nickname::INPUT_FMT,
574                               'format' => '(xml|json)'));
575
576             $m->connect('api/blocks/destroy.:format',
577                         array('action' => 'ApiBlockDestroy',
578                               'format' => '(xml|json)'));
579
580             // help
581
582             $m->connect('api/help/test.:format',
583                         array('action' => 'ApiHelpTest',
584                               'format' => '(xml|json)'));
585
586             // statusnet
587
588             $m->connect('api/statusnet/version.:format',
589                         array('action' => 'ApiGNUsocialVersion',
590                               'format' => '(xml|json)'));
591
592             $m->connect('api/statusnet/config.:format',
593                         array('action' => 'ApiGNUsocialConfig',
594                               'format' => '(xml|json)'));
595
596             // For our current software name, we provide "gnusocial" base action
597
598             $m->connect('api/gnusocial/version.:format',
599                         array('action' => 'ApiGNUsocialVersion',
600                               'format' => '(xml|json)'));
601
602             $m->connect('api/gnusocial/config.:format',
603                         array('action' => 'ApiGNUsocialConfig',
604                               'format' => '(xml|json)'));
605
606             // Groups and tags are newer than 0.8.1 so no backward-compatibility
607             // necessary
608
609             // Groups
610             //'list' has to be handled differently, as php will not allow a method to be named 'list'
611
612             $m->connect('api/statusnet/groups/timeline/:id.:format',
613                         array('action' => 'ApiTimelineGroup',
614                               'id' => Nickname::INPUT_FMT,
615                               'format' => '(xml|json|rss|atom|as)'));
616
617             $m->connect('api/statusnet/groups/show/:id.:format',
618                         array('action' => 'ApiGroupShow',
619                               'id' => Nickname::INPUT_FMT,
620                               'format' => '(xml|json)'));
621
622             $m->connect('api/statusnet/groups/show.:format',
623                         array('action' => 'ApiGroupShow',
624                               'format' => '(xml|json)'));
625
626             $m->connect('api/statusnet/groups/join/:id.:format',
627                         array('action' => 'ApiGroupJoin',
628                               'id' => Nickname::INPUT_FMT,
629                               'format' => '(xml|json)'));
630
631             $m->connect('api/statusnet/groups/join.:format',
632                         array('action' => 'ApiGroupJoin',
633                               'format' => '(xml|json)'));
634
635             $m->connect('api/statusnet/groups/leave/:id.:format',
636                         array('action' => 'ApiGroupLeave',
637                               'format' => '(xml|json)'));
638
639             $m->connect('api/statusnet/groups/leave.:format',
640                         array('action' => 'ApiGroupLeave',
641                               'id' => Nickname::INPUT_FMT,
642                               'format' => '(xml|json)'));
643
644             $m->connect('api/statusnet/groups/is_member.:format',
645                         array('action' => 'ApiGroupIsMember',
646                               'format' => '(xml|json)'));
647
648             $m->connect('api/statusnet/groups/list/:id.:format',
649                         array('action' => 'ApiGroupList',
650                               'id' => Nickname::INPUT_FMT,
651                               'format' => '(xml|json|rss|atom)'));
652
653             $m->connect('api/statusnet/groups/list.:format',
654                         array('action' => 'ApiGroupList',
655                               'format' => '(xml|json|rss|atom)'));
656
657             $m->connect('api/statusnet/groups/list_all.:format',
658                         array('action' => 'ApiGroupListAll',
659                               'format' => '(xml|json|rss|atom)'));
660
661             $m->connect('api/statusnet/groups/membership/:id.:format',
662                         array('action' => 'ApiGroupMembership',
663                               'id' => Nickname::INPUT_FMT,
664                               'format' => '(xml|json)'));
665
666             $m->connect('api/statusnet/groups/membership.:format',
667                         array('action' => 'ApiGroupMembership',
668                               'format' => '(xml|json)'));
669
670             $m->connect('api/statusnet/groups/create.:format',
671                         array('action' => 'ApiGroupCreate',
672                               'format' => '(xml|json)'));
673
674             $m->connect('api/statusnet/groups/update/:id.:format',
675                         array('action' => 'ApiGroupProfileUpdate',
676                               'id' => '[a-zA-Z0-9]+',
677                               'format' => '(xml|json)'));
678                               
679             $m->connect('api/statusnet/conversation/:id.:format',
680                         array('action' => 'apiconversation',
681                               'id' => '[0-9]+',
682                               'format' => '(xml|json|rss|atom|as)'));
683
684             // Lists (people tags)
685             $m->connect('api/lists/list.:format',
686                         array('action' => 'ApiListSubscriptions',
687                               'format' => '(xml|json)'));
688
689             $m->connect('api/lists/memberships.:format',
690                         array('action' => 'ApiListMemberships',
691                               'format' => '(xml|json)'));
692
693             $m->connect('api/:user/lists/memberships.:format',
694                         array('action' => 'ApiListMemberships',
695                               'user' => '[a-zA-Z0-9]+',
696                               'format' => '(xml|json)'));
697
698             $m->connect('api/lists/subscriptions.:format',
699                         array('action' => 'ApiListSubscriptions',
700                               'format' => '(xml|json)'));
701
702             $m->connect('api/:user/lists/subscriptions.:format',
703                         array('action' => 'ApiListSubscriptions',
704                               'user' => '[a-zA-Z0-9]+',
705                               'format' => '(xml|json)'));
706
707             $m->connect('api/lists.:format',
708                         array('action' => 'ApiLists',
709                               'format' => '(xml|json)'));
710
711             $m->connect('api/:user/lists/:id.:format',
712                         array('action' => 'ApiList',
713                               'user' => '[a-zA-Z0-9]+',
714                               'id' => '[a-zA-Z0-9]+',
715                               'format' => '(xml|json)'));
716
717             $m->connect('api/:user/lists.:format',
718                         array('action' => 'ApiLists',
719                               'user' => '[a-zA-Z0-9]+',
720                               'format' => '(xml|json)'));
721
722             $m->connect('api/:user/lists/:id/statuses.:format',
723                         array('action' => 'ApiTimelineList',
724                               'user' => '[a-zA-Z0-9]+',
725                               'id' => '[a-zA-Z0-9]+',
726                               'format' => '(xml|json|rss|atom)'));
727
728             $m->connect('api/:user/:list_id/members/:id.:format',
729                         array('action' => 'ApiListMember',
730                               'user' => '[a-zA-Z0-9]+',
731                               'list_id' => '[a-zA-Z0-9]+',
732                               'id' => '[a-zA-Z0-9]+',
733                               'format' => '(xml|json)'));
734
735             $m->connect('api/:user/:list_id/members.:format',
736                         array('action' => 'ApiListMembers',
737                               'user' => '[a-zA-Z0-9]+',
738                               'list_id' => '[a-zA-Z0-9]+',
739                               'format' => '(xml|json)'));
740
741             $m->connect('api/:user/:list_id/subscribers/:id.:format',
742                         array('action' => 'ApiListSubscriber',
743                               'user' => '[a-zA-Z0-9]+',
744                               'list_id' => '[a-zA-Z0-9]+',
745                               'id' => '[a-zA-Z0-9]+',
746                               'format' => '(xml|json)'));
747
748             $m->connect('api/:user/:list_id/subscribers.:format',
749                         array('action' => 'ApiListSubscribers',
750                               'user' => '[a-zA-Z0-9]+',
751                               'list_id' => '[a-zA-Z0-9]+',
752                               'format' => '(xml|json)'));
753
754             // Tags
755             $m->connect('api/statusnet/tags/timeline/:tag.:format',
756                         array('action' => 'ApiTimelineTag',
757                               'tag'    => self::REGEX_TAG,
758                               'format' => '(xml|json|rss|atom|as)'));
759
760             // media related
761             $m->connect(
762                 'api/statusnet/media/upload',
763                 array('action' => 'ApiMediaUpload')
764             );
765             $m->connect(
766                 'api/statuses/update_with_media.json',
767                 array('action' => 'ApiMediaUpload')
768             );
769
770             // search
771             $m->connect('api/search.atom', array('action' => 'ApiSearchAtom'));
772             $m->connect('api/search.json', array('action' => 'ApiSearchJSON'));
773             $m->connect('api/trends.json', array('action' => 'ApiTrends'));
774
775             $m->connect('api/oauth/request_token',
776                         array('action' => 'ApiOAuthRequestToken'));
777
778             $m->connect('api/oauth/access_token',
779                         array('action' => 'ApiOAuthAccessToken'));
780
781             $m->connect('api/oauth/authorize',
782                         array('action' => 'ApiOAuthAuthorize'));
783
784             // Admin
785
786             $m->connect('panel/site', array('action' => 'siteadminpanel'));
787             $m->connect('panel/user', array('action' => 'useradminpanel'));
788             $m->connect('panel/access', array('action' => 'accessadminpanel'));
789             $m->connect('panel/paths', array('action' => 'pathsadminpanel'));
790             $m->connect('panel/sessions', array('action' => 'sessionsadminpanel'));
791             $m->connect('panel/sitenotice', array('action' => 'sitenoticeadminpanel'));
792             $m->connect('panel/license', array('action' => 'licenseadminpanel'));
793
794             $m->connect('panel/plugins', array('action' => 'pluginsadminpanel'));
795             $m->connect('panel/plugins/enable/:plugin',
796                         array('action' => 'pluginenable'),
797                         array('plugin' => '[A-Za-z0-9_]+'));
798             $m->connect('panel/plugins/disable/:plugin',
799                         array('action' => 'plugindisable'),
800                         array('plugin' => '[A-Za-z0-9_]+'));
801
802             $m->connect('getfile/:filename',
803                         array('action' => 'getfile'),
804                         array('filename' => '[A-Za-z0-9._-]+'));
805
806             // Common people-tag stuff
807
808             $m->connect('peopletag/:tag', array('action' => 'peopletag',
809                                                 'tag'    => self::REGEX_TAG));
810
811             $m->connect('selftag/:tag', array('action' => 'selftag',
812                                               'tag'    => self::REGEX_TAG));
813
814             $m->connect('main/addpeopletag', array('action' => 'addpeopletag'));
815
816             $m->connect('main/removepeopletag', array('action' => 'removepeopletag'));
817
818             $m->connect('main/profilecompletion', array('action' => 'profilecompletion'));
819
820             $m->connect('main/peopletagautocomplete', array('action' => 'peopletagautocomplete'));
821
822             // In the "root"
823
824             if (common_config('singleuser', 'enabled')) {
825
826                 $nickname = User::singleUserNickname();
827
828                 foreach (array('subscriptions', 'subscribers',
829                                'all', 'foaf', 'replies',
830                                'microsummary') as $a) {
831                     $m->connect($a,
832                                 array('action' => $a,
833                                       'nickname' => $nickname));
834                 }
835
836                 foreach (array('subscriptions', 'subscribers') as $a) {
837                     $m->connect($a.'/:tag',
838                                 array('action' => $a,
839                                       'nickname' => $nickname),
840                                 array('tag' => self::REGEX_TAG));
841                 }
842
843                 $m->connect('subscribers/pending',
844                             array('action' => 'subqueue',
845                                   'nickname' => $nickname));
846
847                 foreach (array('rss', 'groups') as $a) {
848                     $m->connect($a,
849                                 array('action' => 'user'.$a,
850                                       'nickname' => $nickname));
851                 }
852
853                 foreach (array('all', 'replies') as $a) {
854                     $m->connect($a.'/rss',
855                                 array('action' => $a.'rss',
856                                       'nickname' => $nickname));
857                 }
858
859                 $m->connect('avatar',
860                             array('action' => 'avatarbynickname',
861                                   'nickname' => $nickname));
862                 $m->connect('avatar/:size',
863                             array('action' => 'avatarbynickname',
864                                   'nickname' => $nickname),
865                             array('size' => '(|original|\d+)'));
866
867                 $m->connect('tag/:tag/rss',
868                             array('action' => 'userrss',
869                                   'nickname' => $nickname),
870                             array('tag' => self::REGEX_TAG));
871
872                 $m->connect('tag/:tag',
873                             array('action' => 'showstream',
874                                   'nickname' => $nickname),
875                             array('tag' => self::REGEX_TAG));
876
877                 $m->connect('rsd.xml',
878                             array('action' => 'rsd',
879                                   'nickname' => $nickname));
880
881                 // peopletags
882
883                 $m->connect('peopletags',
884                             array('action' => 'peopletagsbyuser'));
885
886                 $m->connect('peopletags/private',
887                             array('action' => 'peopletagsbyuser',
888                                   'private' => 1));
889
890                 $m->connect('peopletags/public',
891                             array('action' => 'peopletagsbyuser',
892                                   'public' => 1));
893
894                 $m->connect('othertags',
895                             array('action' => 'peopletagsforuser'));
896
897                 $m->connect('peopletagsubscriptions',
898                             array('action' => 'peopletagsubscriptions'));
899
900                 $m->connect('all/:tag/subscribers',
901                             array('action' => 'peopletagsubscribers',
902                                   'tag' => self::REGEX_TAG));
903
904                 $m->connect('all/:tag/tagged',
905                                 array('action' => 'peopletagged',
906                                       'tag' => self::REGEX_TAG));
907
908                 $m->connect('all/:tag/edit',
909                                 array('action' => 'editpeopletag',
910                                       'tag' => self::REGEX_TAG));
911
912                 foreach(array('subscribe', 'unsubscribe') as $v) {
913                     $m->connect('peopletag/:id/'.$v,
914                                     array('action' => $v.'peopletag',
915                                           'id' => '[0-9]{1,64}'));
916                 }
917                 $m->connect('user/:tagger_id/profiletag/:id/id',
918                                 array('action' => 'profiletagbyid',
919                                       'tagger_id' => '[0-9]+',
920                                       'id' => '[0-9]+'));
921
922                 $m->connect('all/:tag',
923                                 array('action' => 'showprofiletag',
924                                       'nickname' => $nickname,
925                                       'tag' => self::REGEX_TAG));
926
927                 foreach (array('subscriptions', 'subscribers') as $a) {
928                     $m->connect($a.'/:tag',
929                                 array('action' => $a),
930                                 array('tag' => self::REGEX_TAG));
931                 }
932             }
933
934             $m->connect('rss', array('action' => 'publicrss'));
935             $m->connect('featuredrss', array('action' => 'featuredrss'));
936             $m->connect('featured/', array('action' => 'featured'));
937             $m->connect('featured', array('action' => 'featured'));
938             $m->connect('rsd.xml', array('action' => 'rsd'));
939
940             foreach (array('subscriptions', 'subscribers',
941                            'nudge', 'all', 'foaf', 'replies',
942                            'inbox', 'outbox', 'microsummary') as $a) {
943                 $m->connect(':nickname/'.$a,
944                             array('action' => $a),
945                             array('nickname' => Nickname::DISPLAY_FMT));
946             }
947             $m->connect(':nickname/subscribers/pending',
948                         array('action' => 'subqueue'),
949                         array('nickname' => Nickname::DISPLAY_FMT));
950
951             // some targeted RSS 1.0 actions (extends TargetedRss10Action)
952             foreach (array('all', 'replies') as $a) {
953                 $m->connect(':nickname/'.$a.'/rss',
954                             array('action' => $a.'rss'),
955                             array('nickname' => Nickname::DISPLAY_FMT));
956             }
957
958             // people tags
959
960             $m->connect(':nickname/peopletags',
961                             array('action' => 'peopletagsbyuser',
962                                   'nickname' => Nickname::DISPLAY_FMT));
963
964             $m->connect(':nickname/peopletags/private',
965                             array('action' => 'peopletagsbyuser',
966                                   'nickname' => Nickname::DISPLAY_FMT,
967                                   'private' => 1));
968
969             $m->connect(':nickname/peopletags/public',
970                             array('action' => 'peopletagsbyuser',
971                                   'nickname' => Nickname::DISPLAY_FMT,
972                                   'public' => 1));
973
974             $m->connect(':nickname/othertags',
975                             array('action' => 'peopletagsforuser',
976                                   'nickname' => Nickname::DISPLAY_FMT));
977
978             $m->connect(':nickname/peopletagsubscriptions',
979                             array('action' => 'peopletagsubscriptions',
980                                   'nickname' => Nickname::DISPLAY_FMT));
981
982             $m->connect(':tagger/all/:tag/subscribers',
983                             array('action' => 'peopletagsubscribers',
984                                   'tagger' => Nickname::DISPLAY_FMT,
985                                   'tag' => self::REGEX_TAG));
986
987             $m->connect(':tagger/all/:tag/tagged',
988                             array('action' => 'peopletagged',
989                                   'tagger' => Nickname::DISPLAY_FMT,
990                                   'tag' => self::REGEX_TAG));
991
992             $m->connect(':tagger/all/:tag/edit',
993                             array('action' => 'editpeopletag',
994                                   'tagger' => Nickname::DISPLAY_FMT,
995                                   'tag' => self::REGEX_TAG));
996
997             foreach(array('subscribe', 'unsubscribe') as $v) {
998                 $m->connect('peopletag/:id/'.$v,
999                                 array('action' => $v.'peopletag',
1000                                       'id' => '[0-9]{1,64}'));
1001             }
1002             $m->connect('user/:tagger_id/profiletag/:id/id',
1003                             array('action' => 'profiletagbyid',
1004                                   'tagger_id' => '[0-9]+',
1005                                   'id' => '[0-9]+'));
1006
1007             $m->connect(':nickname/all/:tag',
1008                             array('action' => 'showprofiletag'),
1009                             array('nickname' => Nickname::DISPLAY_FMT,
1010                                   'tag' => self::REGEX_TAG));
1011
1012             foreach (array('subscriptions', 'subscribers') as $a) {
1013                 $m->connect(':nickname/'.$a.'/:tag',
1014                             array('action' => $a),
1015                             array('tag' => self::REGEX_TAG,
1016                                   'nickname' => Nickname::DISPLAY_FMT));
1017             }
1018
1019             foreach (array('rss', 'groups') as $a) {
1020                 $m->connect(':nickname/'.$a,
1021                             array('action' => 'user'.$a),
1022                             array('nickname' => Nickname::DISPLAY_FMT));
1023             }
1024
1025             $m->connect(':nickname/avatar',
1026                         array('action' => 'avatarbynickname'),
1027                         array('nickname' => Nickname::DISPLAY_FMT));
1028             $m->connect(':nickname/avatar/:size',
1029                         array('action' => 'avatarbynickname'),
1030                         array('size' => '(|original|\d+)',
1031                               'nickname' => Nickname::DISPLAY_FMT));
1032
1033             $m->connect(':nickname/tag/:tag/rss',
1034                         array('action' => 'userrss'),
1035                         array('nickname' => Nickname::DISPLAY_FMT),
1036                         array('tag' => self::REGEX_TAG));
1037
1038             $m->connect(':nickname/tag/:tag',
1039                         array('action' => 'showstream'),
1040                         array('nickname' => Nickname::DISPLAY_FMT),
1041                         array('tag' => self::REGEX_TAG));
1042
1043             $m->connect(':nickname/rsd.xml',
1044                         array('action' => 'rsd'),
1045                         array('nickname' => Nickname::DISPLAY_FMT));
1046
1047             $m->connect(':nickname',
1048                         array('action' => 'showstream'),
1049                         array('nickname' => Nickname::DISPLAY_FMT));
1050
1051             $m->connect(':nickname/',
1052                         array('action' => 'showstream'),
1053                         array('nickname' => Nickname::DISPLAY_FMT));
1054
1055             // AtomPub API
1056
1057             $m->connect('api/statusnet/app/service/:id.xml',
1058                         array('action' => 'ApiAtomService'),
1059                         array('id' => Nickname::DISPLAY_FMT));
1060
1061             $m->connect('api/statusnet/app/service.xml',
1062                         array('action' => 'ApiAtomService'));
1063
1064             $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom',
1065                         array('action' => 'AtomPubShowSubscription'),
1066                         array('subscriber' => '[0-9]+',
1067                               'subscribed' => '[0-9]+'));
1068
1069             $m->connect('api/statusnet/app/subscriptions/:subscriber.atom',
1070                         array('action' => 'AtomPubSubscriptionFeed'),
1071                         array('subscriber' => '[0-9]+'));
1072
1073             $m->connect('api/statusnet/app/memberships/:profile/:group.atom',
1074                         array('action' => 'AtomPubShowMembership'),
1075                         array('profile' => '[0-9]+',
1076                               'group' => '[0-9]+'));
1077
1078             $m->connect('api/statusnet/app/memberships/:profile.atom',
1079                         array('action' => 'AtomPubMembershipFeed'),
1080                         array('profile' => '[0-9]+'));
1081
1082             // URL shortening
1083
1084             $m->connect('url/:id',
1085                         array('action' => 'redirecturl',
1086                               'id' => '[0-9]+'));
1087
1088             // user stuff
1089
1090             Event::handle('RouterInitialized', array($m));
1091         }
1092
1093         return $m;
1094     }
1095
1096     function map($path)
1097     {
1098         try {
1099             $match = $this->m->match($path);
1100         } catch (Exception $e) {
1101             common_log(LOG_ERR, "Problem getting route for $path - " .
1102                        $e->getMessage());
1103             // TRANS: Client error on action trying to visit a non-existing page.
1104             $cac = new ClientErrorAction(_('Page not found.'), 404);
1105             $cac->showPage();
1106         }
1107
1108         return $match;
1109     }
1110
1111     function build($action, $args=null, $params=null, $fragment=null)
1112     {
1113         $action_arg = array('action' => $action);
1114
1115         if ($args) {
1116             $args = array_merge($action_arg, $args);
1117         } else {
1118             $args = $action_arg;
1119         }
1120
1121         $url = $this->m->generate($args, $params, $fragment);
1122         // Due to a bug in the URLMapper code, the returned URL may
1123         // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We
1124         // repair that here rather than modifying the upstream code...
1125
1126         $qpos = strpos($url, '?');
1127         if ($qpos !== false) {
1128             $url = substr($url, 0, $qpos+1) .
1129                 str_replace('?', '&', substr($url, $qpos+1));
1130
1131             // @fixme this is a hacky workaround for http_build_query in the
1132             // lower-level code and bad configs that set the default separator
1133             // to &amp; instead of &. Encoded &s in parameters will not be
1134             // affected.
1135             $url = substr($url, 0, $qpos+1) .
1136                 str_replace('&amp;', '&', substr($url, $qpos+1));
1137
1138         }
1139
1140         return $url;
1141     }
1142 }