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