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