3 * StatusNet, the distributed open-source microblogging tool
5 * URL routing utilities
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.
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.
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/>.
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/
30 if (!defined('STATUSNET') && !defined('LACONICA')) {
37 * Cheap wrapper around Net_URL_Mapper
41 * @author Evan Prodromou <evan@status.net>
42 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
43 * @link http://status.net/
50 const REGEX_TAG = '[^\/]+'; // [\pL\pN_\-\.]{1,64} better if we can do unicode regexes
55 Router::$inst = new Router();
61 * Clear the global singleton instance for this class.
62 * Needed to ensure reset when switching site configurations.
64 static function clear()
69 function __construct()
71 if (empty($this->m)) {
72 if (!common_config('router', 'cache')) {
73 $this->m = $this->initialize();
75 $k = self::cacheKey();
76 $c = Cache::instance();
81 $this->m = $this->initialize();
82 $c->set($k, $this->m);
89 * Create a unique hashkey for the router.
91 * The router's url map can change based on the version of the software
92 * you're running and the plugins that are enabled. To avoid having bad routes
93 * get stuck in the cache, the key includes a list of plugins and the software
96 * There can still be problems with a) differences in versions of the plugins and
97 * b) people running code between official versions, but these tend to be more
98 * sophisticated users who can grok what's going on and clear their caches.
100 * @return string cache key string that should uniquely identify a router
103 static function cacheKey()
105 $parts = array('router');
107 // Many router paths depend on this setting.
108 if (common_config('singleuser', 'enabled')) {
114 return Cache::codeKey(implode(':', $parts));
117 function initialize()
119 $m = new URLMapper();
121 if (Event::handle('StartInitializeRouter', array(&$m))) {
123 $m->connect('robots.txt', array('action' => 'robotstxt'));
125 $m->connect('opensearch/people', array('action' => 'opensearch',
126 'type' => 'people'));
127 $m->connect('opensearch/notice', array('action' => 'opensearch',
128 'type' => 'notice'));
132 $m->connect('doc/:title', array('action' => 'doc'));
134 $m->connect('main/otp/:user_id/:token',
135 array('action' => 'otp'),
136 array('user_id' => '[0-9]+',
139 // main stuff is repetitive
141 $main = array('login', 'logout', 'register', 'subscribe',
142 'unsubscribe', 'cancelsubscription', 'approvesub',
143 'confirmaddress', 'recoverpassword',
144 'invite', 'favor', 'disfavor', 'sup',
145 'block', 'unblock', 'subedit',
146 'groupblock', 'groupunblock',
147 'sandbox', 'unsandbox',
148 'silence', 'unsilence',
149 'grantrole', 'revokerole',
159 foreach ($main as $a) {
160 $m->connect('main/'.$a, array('action' => $a));
163 // Also need a block variant accepting ID on URL for mail links
164 $m->connect('main/block/:profileid',
165 array('action' => 'block'),
166 array('profileid' => '[0-9]+'));
168 $m->connect('main/sup/:seconds', array('action' => 'sup'),
169 array('seconds' => '[0-9]+'));
171 $m->connect('main/tagprofile', array('action' => 'tagprofile'));
172 $m->connect('main/tagprofile/:id', array('action' => 'tagprofile'),
173 array('id' => '[0-9]+'));
175 $m->connect('main/oembed',
176 array('action' => 'oembed'));
178 $m->connect('main/xrds',
179 array('action' => 'publicxrds'));
180 $m->connect('.well-known/host-meta',
181 array('action' => 'hostmeta'));
182 $m->connect('main/xrd',
183 array('action' => 'userxrd'));
187 foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
188 $m->connect('main/'.$c.'/:code', array('action' => $c));
193 foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections',
194 'oauthapps', 'email', 'sms', 'url') as $s) {
195 $m->connect('settings/'.$s, array('action' => $s.'settings'));
198 $m->connect('settings/oauthapps/show/:id',
199 array('action' => 'showapplication'),
200 array('id' => '[0-9]+')
202 $m->connect('settings/oauthapps/new',
203 array('action' => 'newapplication')
205 $m->connect('settings/oauthapps/edit/:id',
206 array('action' => 'editapplication'),
207 array('id' => '[0-9]+')
209 $m->connect('settings/oauthapps/delete/:id',
210 array('action' => 'deleteapplication'),
211 array('id' => '[0-9]+')
216 foreach (array('group', 'people', 'notice') as $s) {
217 $m->connect('search/'.$s, array('action' => $s.'search'));
218 $m->connect('search/'.$s.'?q=:q',
219 array('action' => $s.'search'),
223 // The second of these is needed to make the link work correctly
224 // when inserted into the page. The first is needed to match the
225 // route on the way in. Seems to be another Net_URL_Mapper bug to me.
226 $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
227 $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
230 $m->connect('attachment/:attachment',
231 array('action' => 'attachment'),
232 array('attachment' => '[0-9]+'));
234 $m->connect('attachment/:attachment/ajax',
235 array('action' => 'attachment_ajax'),
236 array('attachment' => '[0-9]+'));
238 $m->connect('attachment/:attachment/thumbnail',
239 array('action' => 'attachment_thumbnail'),
240 array('attachment' => '[0-9]+'));
242 $m->connect('notice/new', array('action' => 'newnotice'));
243 $m->connect('notice/new?replyto=:replyto',
244 array('action' => 'newnotice'),
245 array('replyto' => Nickname::DISPLAY_FMT));
246 $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
247 array('action' => 'newnotice'),
248 array('replyto' => Nickname::DISPLAY_FMT),
249 array('inreplyto' => '[0-9]+'));
251 $m->connect('notice/:notice/file',
252 array('action' => 'file'),
253 array('notice' => '[0-9]+'));
255 $m->connect('notice/:notice',
256 array('action' => 'shownotice'),
257 array('notice' => '[0-9]+'));
258 $m->connect('notice/delete', array('action' => 'deletenotice'));
259 $m->connect('notice/delete/:notice',
260 array('action' => 'deletenotice'),
261 array('notice' => '[0-9]+'));
263 $m->connect('bookmarklet/new', array('action' => 'bookmarklet'));
267 $m->connect('conversation/:id',
268 array('action' => 'conversation'),
269 array('id' => '[0-9]+'));
270 $m->connect('conversation/:id/replies',
271 array('action' => 'conversationreplies'),
272 array('id' => '[0-9]+'));
274 $m->connect('message/new', array('action' => 'newmessage'));
275 $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => Nickname::DISPLAY_FMT));
276 $m->connect('message/:message',
277 array('action' => 'showmessage'),
278 array('message' => '[0-9]+'));
280 $m->connect('user/:id',
281 array('action' => 'userbyid'),
282 array('id' => '[0-9]+'));
284 $m->connect('tags/', array('action' => 'publictagcloud'));
285 $m->connect('tag/', array('action' => 'publictagcloud'));
286 $m->connect('tags', array('action' => 'publictagcloud'));
287 $m->connect('tag', array('action' => 'publictagcloud'));
288 $m->connect('tag/:tag/rss',
289 array('action' => 'tagrss'),
290 array('tag' => self::REGEX_TAG));
291 $m->connect('tag/:tag',
292 array('action' => 'tag'),
293 array('tag' => self::REGEX_TAG));
297 $m->connect('group/new', array('action' => 'newgroup'));
299 foreach (array('edit', 'join', 'leave', 'delete', 'cancel', 'approve') as $v) {
300 $m->connect('group/:nickname/'.$v,
301 array('action' => $v.'group'),
302 array('nickname' => Nickname::DISPLAY_FMT));
303 $m->connect('group/:id/id/'.$v,
304 array('action' => $v.'group'),
305 array('id' => '[0-9]+'));
308 foreach (array('members', 'logo', 'rss') as $n) {
309 $m->connect('group/:nickname/'.$n,
310 array('action' => 'group'.$n),
311 array('nickname' => Nickname::DISPLAY_FMT));
314 $m->connect('group/:nickname/foaf',
315 array('action' => 'foafgroup'),
316 array('nickname' => Nickname::DISPLAY_FMT));
318 $m->connect('group/:nickname/blocked',
319 array('action' => 'blockedfromgroup'),
320 array('nickname' => Nickname::DISPLAY_FMT));
322 $m->connect('group/:nickname/makeadmin',
323 array('action' => 'makeadmin'),
324 array('nickname' => Nickname::DISPLAY_FMT));
326 $m->connect('group/:nickname/members/pending',
327 array('action' => 'groupqueue'),
328 array('nickname' => Nickname::DISPLAY_FMT));
330 $m->connect('group/:id/id',
331 array('action' => 'groupbyid'),
332 array('id' => '[0-9]+'));
334 $m->connect('group/:nickname',
335 array('action' => 'showgroup'),
336 array('nickname' => Nickname::DISPLAY_FMT));
338 $m->connect('group/', array('action' => 'groups'));
339 $m->connect('group', array('action' => 'groups'));
340 $m->connect('groups/', array('action' => 'groups'));
341 $m->connect('groups', array('action' => 'groups'));
343 // Twitter-compatible API
348 array('action' => 'Redirect',
349 'nextAction' => 'doc',
350 'args' => array('title' => 'api')));
352 $m->connect('api/statuses/public_timeline.:format',
353 array('action' => 'ApiTimelinePublic',
354 'format' => '(xml|json|rss|atom|as)'));
356 $m->connect('api/statuses/friends_timeline.:format',
357 array('action' => 'ApiTimelineFriends',
358 'format' => '(xml|json|rss|atom|as)'));
360 $m->connect('api/statuses/friends_timeline/:id.:format',
361 array('action' => 'ApiTimelineFriends',
362 'id' => Nickname::INPUT_FMT,
363 'format' => '(xml|json|rss|atom|as)'));
365 $m->connect('api/statuses/home_timeline.:format',
366 array('action' => 'ApiTimelineHome',
367 'format' => '(xml|json|rss|atom|as)'));
369 $m->connect('api/statuses/home_timeline/:id.:format',
370 array('action' => 'ApiTimelineHome',
371 'id' => Nickname::INPUT_FMT,
372 'format' => '(xml|json|rss|atom|as)'));
374 $m->connect('api/statuses/user_timeline.:format',
375 array('action' => 'ApiTimelineUser',
376 'format' => '(xml|json|rss|atom|as)'));
378 $m->connect('api/statuses/user_timeline/:id.:format',
379 array('action' => 'ApiTimelineUser',
380 'id' => Nickname::INPUT_FMT,
381 'format' => '(xml|json|rss|atom|as)'));
383 $m->connect('api/statuses/mentions.:format',
384 array('action' => 'ApiTimelineMentions',
385 'format' => '(xml|json|rss|atom|as)'));
387 $m->connect('api/statuses/mentions/:id.:format',
388 array('action' => 'ApiTimelineMentions',
389 'id' => Nickname::INPUT_FMT,
390 'format' => '(xml|json|rss|atom|as)'));
392 $m->connect('api/statuses/replies.:format',
393 array('action' => 'ApiTimelineMentions',
394 'format' => '(xml|json|rss|atom|as)'));
396 $m->connect('api/statuses/replies/:id.:format',
397 array('action' => 'ApiTimelineMentions',
398 'id' => Nickname::INPUT_FMT,
399 'format' => '(xml|json|rss|atom|as)'));
401 $m->connect('api/statuses/retweeted_by_me.:format',
402 array('action' => 'ApiTimelineRetweetedByMe',
403 'format' => '(xml|json|atom|as)'));
405 $m->connect('api/statuses/retweeted_to_me.:format',
406 array('action' => 'ApiTimelineRetweetedToMe',
407 'format' => '(xml|json|atom|as)'));
409 $m->connect('api/statuses/retweets_of_me.:format',
410 array('action' => 'ApiTimelineRetweetsOfMe',
411 'format' => '(xml|json|atom|as)'));
413 $m->connect('api/statuses/friends.:format',
414 array('action' => 'ApiUserFriends',
415 'format' => '(xml|json)'));
417 $m->connect('api/statuses/friends/:id.:format',
418 array('action' => 'ApiUserFriends',
419 'id' => Nickname::INPUT_FMT,
420 'format' => '(xml|json)'));
422 $m->connect('api/statuses/followers.:format',
423 array('action' => 'ApiUserFollowers',
424 'format' => '(xml|json)'));
426 $m->connect('api/statuses/followers/:id.:format',
427 array('action' => 'ApiUserFollowers',
428 'id' => Nickname::INPUT_FMT,
429 'format' => '(xml|json)'));
431 $m->connect('api/statuses/show.:format',
432 array('action' => 'ApiStatusesShow',
433 'format' => '(xml|json|atom)'));
435 $m->connect('api/statuses/show/:id.:format',
436 array('action' => 'ApiStatusesShow',
438 'format' => '(xml|json|atom)'));
440 $m->connect('api/statuses/update.:format',
441 array('action' => 'ApiStatusesUpdate',
442 'format' => '(xml|json)'));
444 $m->connect('api/statuses/destroy.:format',
445 array('action' => 'ApiStatusesDestroy',
446 'format' => '(xml|json)'));
448 $m->connect('api/statuses/destroy/:id.:format',
449 array('action' => 'ApiStatusesDestroy',
451 'format' => '(xml|json)'));
453 $m->connect('api/statuses/retweet/:id.:format',
454 array('action' => 'ApiStatusesRetweet',
456 'format' => '(xml|json)'));
458 $m->connect('api/statuses/retweets/:id.:format',
459 array('action' => 'ApiStatusesRetweets',
461 'format' => '(xml|json)'));
465 $m->connect('api/users/show.:format',
466 array('action' => 'ApiUserShow',
467 'format' => '(xml|json)'));
469 $m->connect('api/users/show/:id.:format',
470 array('action' => 'ApiUserShow',
471 'id' => Nickname::INPUT_FMT,
472 'format' => '(xml|json)'));
474 $m->connect('api/users/profile_image/:screen_name.:format',
475 array('action' => 'ApiUserProfileImage',
476 'screen_name' => Nickname::DISPLAY_FMT,
477 'format' => '(xml|json)'));
481 $m->connect('api/direct_messages.:format',
482 array('action' => 'ApiDirectMessage',
483 'format' => '(xml|json|rss|atom)'));
485 $m->connect('api/direct_messages/sent.:format',
486 array('action' => 'ApiDirectMessage',
487 'format' => '(xml|json|rss|atom)',
490 $m->connect('api/direct_messages/new.:format',
491 array('action' => 'ApiDirectMessageNew',
492 'format' => '(xml|json)'));
496 $m->connect('api/friendships/show.:format',
497 array('action' => 'ApiFriendshipsShow',
498 'format' => '(xml|json)'));
500 $m->connect('api/friendships/exists.:format',
501 array('action' => 'ApiFriendshipsExists',
502 'format' => '(xml|json)'));
504 $m->connect('api/friendships/create.:format',
505 array('action' => 'ApiFriendshipsCreate',
506 'format' => '(xml|json)'));
508 $m->connect('api/friendships/destroy.:format',
509 array('action' => 'ApiFriendshipsDestroy',
510 'format' => '(xml|json)'));
512 $m->connect('api/friendships/create/:id.:format',
513 array('action' => 'ApiFriendshipsCreate',
514 'id' => Nickname::INPUT_FMT,
515 'format' => '(xml|json)'));
517 $m->connect('api/friendships/destroy/:id.:format',
518 array('action' => 'ApiFriendshipsDestroy',
519 'id' => Nickname::INPUT_FMT,
520 'format' => '(xml|json)'));
524 $m->connect('api/friends/ids/:id.:format',
525 array('action' => 'ApiUserFriends',
526 'ids_only' => true));
528 $m->connect('api/followers/ids/:id.:format',
529 array('action' => 'ApiUserFollowers',
530 'ids_only' => true));
532 $m->connect('api/friends/ids.:format',
533 array('action' => 'ApiUserFriends',
534 'ids_only' => true));
536 $m->connect('api/followers/ids.:format',
537 array('action' => 'ApiUserFollowers',
538 'ids_only' => true));
542 $m->connect('api/account/verify_credentials.:format',
543 array('action' => 'ApiAccountVerifyCredentials'));
545 $m->connect('api/account/update_profile.:format',
546 array('action' => 'ApiAccountUpdateProfile'));
548 $m->connect('api/account/update_profile_image.:format',
549 array('action' => 'ApiAccountUpdateProfileImage'));
551 $m->connect('api/account/update_delivery_device.:format',
552 array('action' => 'ApiAccountUpdateDeliveryDevice'));
554 // special case where verify_credentials is called w/out a format
556 $m->connect('api/account/verify_credentials',
557 array('action' => 'ApiAccountVerifyCredentials'));
559 $m->connect('api/account/rate_limit_status.:format',
560 array('action' => 'ApiAccountRateLimitStatus'));
564 $m->connect('api/favorites.:format',
565 array('action' => 'ApiTimelineFavorites',
566 'format' => '(xml|json|rss|atom|as)'));
568 $m->connect('api/favorites/:id.:format',
569 array('action' => 'ApiTimelineFavorites',
570 'id' => Nickname::INPUT_FMT,
571 'format' => '(xml|json|rss|atom|as)'));
573 $m->connect('api/favorites/create/:id.:format',
574 array('action' => 'ApiFavoriteCreate',
576 'format' => '(xml|json)'));
578 $m->connect('api/favorites/destroy/:id.:format',
579 array('action' => 'ApiFavoriteDestroy',
581 'format' => '(xml|json)'));
584 $m->connect('api/blocks/create.:format',
585 array('action' => 'ApiBlockCreate',
586 'format' => '(xml|json)'));
588 $m->connect('api/blocks/create/:id.:format',
589 array('action' => 'ApiBlockCreate',
590 'id' => Nickname::INPUT_FMT,
591 'format' => '(xml|json)'));
593 $m->connect('api/blocks/destroy.:format',
594 array('action' => 'ApiBlockDestroy',
595 'format' => '(xml|json)'));
597 $m->connect('api/blocks/destroy/:id.:format',
598 array('action' => 'ApiBlockDestroy',
599 'id' => Nickname::INPUT_FMT,
600 'format' => '(xml|json)'));
603 $m->connect('api/help/test.:format',
604 array('action' => 'ApiHelpTest',
605 'format' => '(xml|json)'));
609 $m->connect('api/statusnet/version.:format',
610 array('action' => 'ApiStatusnetVersion',
611 'format' => '(xml|json)'));
613 $m->connect('api/statusnet/config.:format',
614 array('action' => 'ApiStatusnetConfig',
615 'format' => '(xml|json)'));
617 // For older methods, we provide "laconica" base action
619 $m->connect('api/laconica/version.:format',
620 array('action' => 'ApiStatusnetVersion',
621 'format' => '(xml|json)'));
623 $m->connect('api/laconica/config.:format',
624 array('action' => 'ApiStatusnetConfig',
625 'format' => '(xml|json)'));
627 // Groups and tags are newer than 0.8.1 so no backward-compatibility
631 //'list' has to be handled differently, as php will not allow a method to be named 'list'
633 $m->connect('api/statusnet/groups/timeline/:id.:format',
634 array('action' => 'ApiTimelineGroup',
635 'id' => Nickname::INPUT_FMT,
636 'format' => '(xml|json|rss|atom|as)'));
638 $m->connect('api/statusnet/groups/show.:format',
639 array('action' => 'ApiGroupShow',
640 'format' => '(xml|json)'));
642 $m->connect('api/statusnet/groups/show/:id.:format',
643 array('action' => 'ApiGroupShow',
644 'id' => Nickname::INPUT_FMT,
645 'format' => '(xml|json)'));
647 $m->connect('api/statusnet/groups/join.:format',
648 array('action' => 'ApiGroupJoin',
649 'id' => Nickname::INPUT_FMT,
650 'format' => '(xml|json)'));
652 $m->connect('api/statusnet/groups/join/:id.:format',
653 array('action' => 'ApiGroupJoin',
654 'format' => '(xml|json)'));
656 $m->connect('api/statusnet/groups/leave.:format',
657 array('action' => 'ApiGroupLeave',
658 'id' => Nickname::INPUT_FMT,
659 'format' => '(xml|json)'));
661 $m->connect('api/statusnet/groups/leave/:id.:format',
662 array('action' => 'ApiGroupLeave',
663 'format' => '(xml|json)'));
665 $m->connect('api/statusnet/groups/is_member.:format',
666 array('action' => 'ApiGroupIsMember',
667 'format' => '(xml|json)'));
669 $m->connect('api/statusnet/groups/list.:format',
670 array('action' => 'ApiGroupList',
671 'format' => '(xml|json|rss|atom)'));
673 $m->connect('api/statusnet/groups/list/:id.:format',
674 array('action' => 'ApiGroupList',
675 'id' => Nickname::INPUT_FMT,
676 'format' => '(xml|json|rss|atom)'));
678 $m->connect('api/statusnet/groups/list_all.:format',
679 array('action' => 'ApiGroupListAll',
680 'format' => '(xml|json|rss|atom)'));
682 $m->connect('api/statusnet/groups/membership.:format',
683 array('action' => 'ApiGroupMembership',
684 'format' => '(xml|json)'));
686 $m->connect('api/statusnet/groups/membership/:id.:format',
687 array('action' => 'ApiGroupMembership',
688 'id' => Nickname::INPUT_FMT,
689 'format' => '(xml|json)'));
691 $m->connect('api/statusnet/groups/create.:format',
692 array('action' => 'ApiGroupCreate',
693 'format' => '(xml|json)'));
695 $m->connect('api/statusnet/groups/update/:id.:format',
696 array('action' => 'ApiGroupProfileUpdate',
697 'id' => '[a-zA-Z0-9]+',
698 'format' => '(xml|json)'));
700 $m->connect('api/statusnet/conversation/:id.:format',
701 array('action' => 'apiconversation',
703 'format' => '(xml|json|rss|atom|as)'));
705 // Lists (people tags)
707 $m->connect('api/lists/memberships.:format',
708 array('action' => 'ApiListMemberships',
709 'format' => '(xml|json)'));
711 $m->connect('api/:user/lists/memberships.:format',
712 array('action' => 'ApiListMemberships',
713 'user' => '[a-zA-Z0-9]+',
714 'format' => '(xml|json)'));
716 $m->connect('api/lists/subscriptions.:format',
717 array('action' => 'ApiListSubscriptions',
718 'format' => '(xml|json)'));
720 $m->connect('api/:user/lists/subscriptions.:format',
721 array('action' => 'ApiListSubscriptions',
722 'user' => '[a-zA-Z0-9]+',
723 'format' => '(xml|json)'));
724 $m->connect('api/lists.:format',
725 array('action' => 'ApiLists',
726 'format' => '(xml|json)'));
728 $m->connect('api/:user/lists.:format',
729 array('action' => 'ApiLists',
730 'user' => '[a-zA-Z0-9]+',
731 'format' => '(xml|json)'));
733 $m->connect('api/:user/lists/:id.:format',
734 array('action' => 'ApiList',
735 'user' => '[a-zA-Z0-9]+',
736 'id' => '[a-zA-Z0-9]+',
737 'format' => '(xml|json)'));
739 $m->connect('api/:user/lists/:id/statuses.:format',
740 array('action' => 'ApiTimelineList',
741 'user' => '[a-zA-Z0-9]+',
742 'id' => '[a-zA-Z0-9]+',
743 'format' => '(xml|json|rss|atom)'));
745 $m->connect('api/:user/:list_id/members.:format',
746 array('action' => 'ApiListMembers',
747 'user' => '[a-zA-Z0-9]+',
748 'list_id' => '[a-zA-Z0-9]+',
749 'format' => '(xml|json)'));
751 $m->connect('api/:user/:list_id/subscribers.:format',
752 array('action' => 'ApiListSubscribers',
753 'user' => '[a-zA-Z0-9]+',
754 'list_id' => '[a-zA-Z0-9]+',
755 'format' => '(xml|json)'));
757 $m->connect('api/:user/:list_id/members/:id.:format',
758 array('action' => 'ApiListMember',
759 'user' => '[a-zA-Z0-9]+',
760 'list_id' => '[a-zA-Z0-9]+',
761 'id' => '[a-zA-Z0-9]+',
762 'format' => '(xml|json)'));
764 $m->connect('api/:user/:list_id/subscribers/:id.:format',
765 array('action' => 'ApiListSubscriber',
766 'user' => '[a-zA-Z0-9]+',
767 'list_id' => '[a-zA-Z0-9]+',
768 'id' => '[a-zA-Z0-9]+',
769 'format' => '(xml|json)'));
772 $m->connect('api/statusnet/tags/timeline/:tag.:format',
773 array('action' => 'ApiTimelineTag',
774 'format' => '(xml|json|rss|atom|as)'));
778 'api/statusnet/media/upload',
779 array('action' => 'ApiMediaUpload')
783 $m->connect('api/search.atom', array('action' => 'ApiSearchAtom'));
784 $m->connect('api/search.json', array('action' => 'ApiSearchJSON'));
785 $m->connect('api/trends.json', array('action' => 'ApiTrends'));
787 $m->connect('api/oauth/request_token',
788 array('action' => 'ApiOauthRequestToken'));
790 $m->connect('api/oauth/access_token',
791 array('action' => 'ApiOauthAccessToken'));
793 $m->connect('api/oauth/authorize',
794 array('action' => 'ApiOauthAuthorize'));
798 $m->connect('panel/site', array('action' => 'siteadminpanel'));
799 $m->connect('panel/user', array('action' => 'useradminpanel'));
800 $m->connect('panel/access', array('action' => 'accessadminpanel'));
801 $m->connect('panel/paths', array('action' => 'pathsadminpanel'));
802 $m->connect('panel/sessions', array('action' => 'sessionsadminpanel'));
803 $m->connect('panel/sitenotice', array('action' => 'sitenoticeadminpanel'));
804 $m->connect('panel/snapshot', array('action' => 'snapshotadminpanel'));
805 $m->connect('panel/license', array('action' => 'licenseadminpanel'));
807 $m->connect('panel/plugins', array('action' => 'pluginsadminpanel'));
808 $m->connect('panel/plugins/enable/:plugin',
809 array('action' => 'pluginenable'),
810 array('plugin' => '[A-Za-z0-9_]+'));
811 $m->connect('panel/plugins/disable/:plugin',
812 array('action' => 'plugindisable'),
813 array('plugin' => '[A-Za-z0-9_]+'));
815 $m->connect('getfile/:filename',
816 array('action' => 'getfile'),
817 array('filename' => '[A-Za-z0-9._-]+'));
821 if (common_config('singleuser', 'enabled')) {
823 $nickname = User::singleUserNickname();
825 foreach (array('subscriptions', 'subscribers',
826 'all', 'foaf', 'replies',
827 'microsummary', 'hcard') as $a) {
829 array('action' => $a,
830 'nickname' => $nickname));
833 foreach (array('subscriptions', 'subscribers') as $a) {
834 $m->connect($a.'/:tag',
835 array('action' => $a,
836 'nickname' => $nickname),
837 array('tag' => self::REGEX_TAG));
840 $m->connect('subscribers/pending',
841 array('action' => 'subqueue',
842 'nickname' => $nickname));
844 foreach (array('rss', 'groups') as $a) {
846 array('action' => 'user'.$a,
847 'nickname' => $nickname));
850 foreach (array('all', 'replies', 'favorites') as $a) {
851 $m->connect($a.'/rss',
852 array('action' => $a.'rss',
853 'nickname' => $nickname));
856 $m->connect('favorites',
857 array('action' => 'showfavorites',
858 'nickname' => $nickname));
860 $m->connect('avatar/:size',
861 array('action' => 'avatarbynickname',
862 'nickname' => $nickname),
863 array('size' => '(original|96|48|24)'));
865 $m->connect('tag/:tag/rss',
866 array('action' => 'userrss',
867 'nickname' => $nickname),
868 array('tag' => self::REGEX_TAG));
870 $m->connect('tag/:tag',
871 array('action' => 'showstream',
872 'nickname' => $nickname),
873 array('tag' => self::REGEX_TAG));
875 $m->connect('rsd.xml',
876 array('action' => 'rsd',
877 'nickname' => $nickname));
880 array('action' => 'showstream',
881 'nickname' => $nickname));
883 $m->connect('', array('action' => 'public'));
884 $m->connect('rss', array('action' => 'publicrss'));
885 $m->connect('featuredrss', array('action' => 'featuredrss'));
886 $m->connect('favoritedrss', array('action' => 'favoritedrss'));
887 $m->connect('featured/', array('action' => 'featured'));
888 $m->connect('featured', array('action' => 'featured'));
889 $m->connect('favorited/', array('action' => 'favorited'));
890 $m->connect('favorited', array('action' => 'favorited'));
891 $m->connect('rsd.xml', array('action' => 'rsd'));
893 foreach (array('subscriptions', 'subscribers',
894 'nudge', 'all', 'foaf', 'replies',
895 'inbox', 'outbox', 'microsummary', 'hcard') as $a) {
896 $m->connect(':nickname/'.$a,
897 array('action' => $a),
898 array('nickname' => Nickname::DISPLAY_FMT));
900 $m->connect(':nickname/subscribers/pending',
901 array('action' => 'subqueue'),
902 array('nickname' => Nickname::DISPLAY_FMT));
906 $m->connect('peopletags', array('action' => 'publicpeopletagcloud'));
908 $m->connect('peopletag/:tag', array('action' => 'peopletag',
909 'tag' => self::REGEX_TAG));
911 $m->connect('selftag/:tag', array('action' => 'selftag',
912 'tag' => self::REGEX_TAG));
914 $m->connect('main/addpeopletag', array('action' => 'addpeopletag'));
916 $m->connect('main/removepeopletag', array('action' => 'removepeopletag'));
918 $m->connect('main/profilecompletion', array('action' => 'profilecompletion'));
920 $m->connect('main/peopletagautocomplete', array('action' => 'peopletagautocomplete'));
922 $m->connect(':nickname/peopletags',
923 array('action' => 'peopletagsbyuser',
924 'nickname' => Nickname::DISPLAY_FMT));
926 $m->connect(':nickname/peopletags/private',
927 array('action' => 'peopletagsbyuser',
928 'nickname' => Nickname::DISPLAY_FMT,
931 $m->connect(':nickname/peopletags/public',
932 array('action' => 'peopletagsbyuser',
933 'nickname' => Nickname::DISPLAY_FMT,
936 $m->connect(':nickname/othertags',
937 array('action' => 'peopletagsforuser',
938 'nickname' => Nickname::DISPLAY_FMT));
940 $m->connect(':nickname/peopletagsubscriptions',
941 array('action' => 'peopletagsubscriptions',
942 'nickname' => Nickname::DISPLAY_FMT));
944 $m->connect(':tagger/all/:tag/subscribers',
945 array('action' => 'peopletagsubscribers',
946 'tagger' => Nickname::DISPLAY_FMT,
947 'tag' => self::REGEX_TAG));
949 $m->connect(':tagger/all/:tag/tagged',
950 array('action' => 'peopletagged',
951 'tagger' => Nickname::DISPLAY_FMT,
952 'tag' => self::REGEX_TAG));
954 $m->connect(':tagger/all/:tag/edit',
955 array('action' => 'editpeopletag',
956 'tagger' => Nickname::DISPLAY_FMT,
957 'tag' => self::REGEX_TAG));
959 foreach(array('subscribe', 'unsubscribe') as $v) {
960 $m->connect('peopletag/:id/'.$v,
961 array('action' => $v.'peopletag',
962 'id' => '[0-9]{1,64}'));
964 $m->connect('user/:tagger_id/profiletag/:id/id',
965 array('action' => 'profiletagbyid',
966 'tagger_id' => '[0-9]+',
969 $m->connect(':tagger/all/:tag',
970 array('action' => 'showprofiletag',
971 'tagger' => Nickname::DISPLAY_FMT,
972 'tag' => self::REGEX_TAG));
974 foreach (array('subscriptions', 'subscribers') as $a) {
975 $m->connect(':nickname/'.$a.'/:tag',
976 array('action' => $a),
977 array('tag' => self::REGEX_TAG,
978 'nickname' => Nickname::DISPLAY_FMT));
981 foreach (array('rss', 'groups') as $a) {
982 $m->connect(':nickname/'.$a,
983 array('action' => 'user'.$a),
984 array('nickname' => Nickname::DISPLAY_FMT));
987 foreach (array('all', 'replies', 'favorites') as $a) {
988 $m->connect(':nickname/'.$a.'/rss',
989 array('action' => $a.'rss'),
990 array('nickname' => Nickname::DISPLAY_FMT));
993 $m->connect(':nickname/favorites',
994 array('action' => 'showfavorites'),
995 array('nickname' => Nickname::DISPLAY_FMT));
997 $m->connect(':nickname/avatar/:size',
998 array('action' => 'avatarbynickname'),
999 array('size' => '(original|96|48|24)',
1000 'nickname' => Nickname::DISPLAY_FMT));
1002 $m->connect(':nickname/tag/:tag/rss',
1003 array('action' => 'userrss'),
1004 array('nickname' => Nickname::DISPLAY_FMT),
1005 array('tag' => self::REGEX_TAG));
1007 $m->connect(':nickname/tag/:tag',
1008 array('action' => 'showstream'),
1009 array('nickname' => Nickname::DISPLAY_FMT),
1010 array('tag' => self::REGEX_TAG));
1012 $m->connect(':nickname/rsd.xml',
1013 array('action' => 'rsd'),
1014 array('nickname' => Nickname::DISPLAY_FMT));
1016 $m->connect(':nickname',
1017 array('action' => 'showstream'),
1018 array('nickname' => Nickname::DISPLAY_FMT));
1023 $m->connect('api/statusnet/app/service/:id.xml',
1024 array('action' => 'ApiAtomService'),
1025 array('id' => Nickname::DISPLAY_FMT));
1027 $m->connect('api/statusnet/app/service.xml',
1028 array('action' => 'ApiAtomService'));
1030 $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom',
1031 array('action' => 'AtomPubShowSubscription'),
1032 array('subscriber' => '[0-9]+',
1033 'subscribed' => '[0-9]+'));
1035 $m->connect('api/statusnet/app/subscriptions/:subscriber.atom',
1036 array('action' => 'AtomPubSubscriptionFeed'),
1037 array('subscriber' => '[0-9]+'));
1039 $m->connect('api/statusnet/app/favorites/:profile/:notice.atom',
1040 array('action' => 'AtomPubShowFavorite'),
1041 array('profile' => '[0-9]+',
1042 'notice' => '[0-9]+'));
1044 $m->connect('api/statusnet/app/favorites/:profile.atom',
1045 array('action' => 'AtomPubFavoriteFeed'),
1046 array('profile' => '[0-9]+'));
1048 $m->connect('api/statusnet/app/memberships/:profile/:group.atom',
1049 array('action' => 'AtomPubShowMembership'),
1050 array('profile' => '[0-9]+',
1051 'group' => '[0-9]+'));
1053 $m->connect('api/statusnet/app/memberships/:profile.atom',
1054 array('action' => 'AtomPubMembershipFeed'),
1055 array('profile' => '[0-9]+'));
1059 $m->connect('url/:id',
1060 array('action' => 'redirecturl',
1065 Event::handle('RouterInitialized', array($m));
1074 $match = $this->m->match($path);
1075 } catch (Exception $e) {
1076 common_log(LOG_ERR, "Problem getting route for $path - " .
1078 // TRANS: Client error on action trying to visit a non-existing page.
1079 $cac = new ClientErrorAction(_('Page not found.'), 404);
1086 function build($action, $args=null, $params=null, $fragment=null)
1088 $action_arg = array('action' => $action);
1091 $args = array_merge($action_arg, $args);
1093 $args = $action_arg;
1096 $url = $this->m->generate($args, $params, $fragment);
1097 // Due to a bug in the Net_URL_Mapper code, the returned URL may
1098 // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We
1099 // repair that here rather than modifying the upstream code...
1101 $qpos = strpos($url, '?');
1102 if ($qpos !== false) {
1103 $url = substr($url, 0, $qpos+1) .
1104 str_replace('?', '&', substr($url, $qpos+1));
1106 // @fixme this is a hacky workaround for http_build_query in the
1107 // lower-level code and bad configs that set the default separator
1108 // to & instead of &. Encoded &s in parameters will not be
1110 $url = substr($url, 0, $qpos+1) .
1111 str_replace('&', '&', substr($url, $qpos+1));