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('GNUSOCIAL')) { exit(1); }
35 * Cheap wrapper around Net_URL_Mapper
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/
48 const REGEX_TAG = '[^\/]+'; // [\pL\pN_\-\.]{1,64} better if we can do unicode regexes
53 Router::$inst = new Router();
59 * Clear the global singleton instance for this class.
60 * Needed to ensure reset when switching site configurations.
62 static function clear()
67 function __construct()
69 if (empty($this->m)) {
70 $this->m = $this->initialize();
75 * Create a unique hashkey for the router.
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
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.
86 * @return string cache key string that should uniquely identify a router
89 static function cacheKey()
91 $parts = array('router');
93 // Many router paths depend on this setting.
94 if (common_config('singleuser', 'enabled')) {
100 return Cache::codeKey(implode(':', $parts));
103 function initialize()
105 $m = new URLMapper();
107 if (Event::handle('StartInitializeRouter', [&$m])) {
109 // top of the menu hierarchy, sometimes "Home"
110 $m->connect('', ['action' => 'top']);
114 $m->connect('robots.txt', ['action' => 'robotstxt']);
116 $m->connect('opensearch/people',
117 ['action' => 'opensearch',
118 'type' => 'people']);
120 $m->connect('opensearch/notice',
121 ['action' => 'opensearch',
122 'type' => 'notice']);
126 $m->connect('doc/:title', ['action' => 'doc']);
128 $m->connect('main/otp/:user_id/:token',
130 ['user_id' => '[0-9]+',
133 // these take a code; before the main part
135 foreach (['register', 'confirmaddress', 'recoverpassword'] as $c) {
136 $m->connect('main/'.$c.'/:code', ['action' => $c]);
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]+']);
144 $m->connect('main/sup/:seconds',
146 ['seconds' => '[0-9]+']);
148 // main stuff is repetitive
150 $main = ['login', 'logout', 'register', 'subscribe',
151 'unsubscribe', 'cancelsubscription', 'approvesub',
152 'confirmaddress', 'recoverpassword',
154 'block', 'unblock', 'subedit',
155 'groupblock', 'groupunblock',
156 'sandbox', 'unsandbox',
157 'silence', 'unsilence',
158 'grantrole', 'revokerole',
168 foreach ($main as $a) {
169 $m->connect('main/'.$a, ['action' => $a]);
172 $m->connect('main/all', ['action' => 'networkpublic']);
174 $m->connect('main/tagprofile/:id',
175 ['action' => 'tagprofile'],
178 $m->connect('main/tagprofile', ['action' => 'tagprofile']);
180 $m->connect('main/xrds',
181 ['action' => 'publicxrds']);
185 foreach (['profile', 'avatar', 'password', 'im', 'oauthconnections',
186 'oauthapps', 'email', 'sms', 'url'] as $s) {
187 $m->connect('settings/'.$s, ['action' => $s.'settings']);
190 if (common_config('oldschool', 'enabled')) {
191 $m->connect('settings/oldschool', ['action' => 'oldschoolsettings']);
194 $m->connect('settings/oauthapps/show/:id',
195 ['action' => 'showapplication'],
198 $m->connect('settings/oauthapps/new',
199 ['action' => 'newapplication']);
201 $m->connect('settings/oauthapps/edit/:id',
202 ['action' => 'editapplication'],
205 $m->connect('settings/oauthapps/delete/:id',
206 ['action' => 'deleteapplication'],
211 foreach (['group', 'people', 'notice'] as $s) {
212 $m->connect('search/'.$s.'?q=:q',
213 ['action' => $s.'search'],
215 $m->connect('search/'.$s, ['action' => $s.'search']);
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'],
224 $m->connect('search/notice/rss', ['action' => 'noticesearchrss']);
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],
238 $m->connect('notice/new?replyto=:replyto&inreplyto=:inreplyto',
239 ['action' => 'newnotice'],
240 ['replyto' => Nickname::DISPLAY_FMT,
241 'inreplyto' => '[0-9]+']);
243 $m->connect('notice/new?replyto=:replyto',
244 ['action' => 'newnotice'],
245 ['replyto' => Nickname::DISPLAY_FMT]);
247 $m->connect('notice/new', ['action' => 'newnotice']);
249 $m->connect('notice/:notice',
250 ['action' => 'shownotice'],
251 ['notice' => '[0-9]+']);
253 $m->connect('notice/:notice/delete',
254 ['action' => 'deletenotice'],
255 ['notice' => '[0-9]+']);
259 $m->connect('conversation/:id',
260 ['action' => 'conversation'],
263 $m->connect('user/:id',
264 ['action' => 'userbyid'],
267 $m->connect('tag/:tag/rss',
268 ['action' => 'tagrss'],
269 ['tag' => self::REGEX_TAG]);
270 $m->connect('tag/:tag',
272 ['tag' => self::REGEX_TAG]);
276 $m->connect('group/new', ['action' => 'newgroup']);
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'],
287 foreach (['members', 'logo', 'rss'] as $n) {
288 $m->connect('group/:nickname/'.$n,
289 ['action' => 'group'.$n],
290 ['nickname' => Nickname::DISPLAY_FMT]);
293 $m->connect('group/:nickname/foaf',
294 ['action' => 'foafgroup'],
295 ['nickname' => Nickname::DISPLAY_FMT]);
297 $m->connect('group/:nickname/blocked',
298 ['action' => 'blockedfromgroup'],
299 ['nickname' => Nickname::DISPLAY_FMT]);
301 $m->connect('group/:nickname/makeadmin',
302 ['action' => 'makeadmin'],
303 ['nickname' => Nickname::DISPLAY_FMT]);
305 $m->connect('group/:nickname/members/pending',
306 ['action' => 'groupqueue'],
307 ['nickname' => Nickname::DISPLAY_FMT]);
309 $m->connect('group/:id/id',
310 ['action' => 'groupbyid'],
313 $m->connect('group/:nickname',
314 ['action' => 'showgroup'],
315 ['nickname' => Nickname::DISPLAY_FMT]);
317 $m->connect('group/:nickname/',
318 ['action' => 'showgroup'],
319 ['nickname' => Nickname::DISPLAY_FMT]);
321 $m->connect('group/', ['action' => 'groups']);
322 $m->connect('group', ['action' => 'groups']);
323 $m->connect('groups/', ['action' => 'groups']);
324 $m->connect('groups', ['action' => 'groups']);
326 // Twitter-compatible API
331 ['action' => 'Redirect',
332 'nextAction' => 'doc',
333 'args' => ['title' => 'api']]);
335 $m->connect('api/statuses/public_timeline.:format',
336 ['action' => 'ApiTimelinePublic'],
337 ['format' => '(xml|json|rss|atom|as)']);
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)']);
344 $m->connect('api/statuses/friends_timeline/:id.:format',
345 ['action' => 'ApiTimelineFriends'],
346 ['id' => Nickname::INPUT_FMT,
347 'format' => '(xml|json|rss|atom|as)']);
349 $m->connect('api/statuses/friends_timeline.:format',
350 ['action' => 'ApiTimelineFriends'],
351 ['format' => '(xml|json|rss|atom|as)']);
353 $m->connect('api/statuses/home_timeline/:id.:format',
354 ['action' => 'ApiTimelineHome'],
355 ['id' => Nickname::INPUT_FMT,
356 'format' => '(xml|json|rss|atom|as)']);
358 $m->connect('api/statuses/home_timeline.:format',
359 ['action' => 'ApiTimelineHome'],
360 ['format' => '(xml|json|rss|atom|as)']);
362 $m->connect('api/statuses/user_timeline/:id.:format',
363 ['action' => 'ApiTimelineUser'],
364 ['id' => Nickname::INPUT_FMT,
365 'format' => '(xml|json|rss|atom|as)']);
367 $m->connect('api/statuses/user_timeline.:format',
368 ['action' => 'ApiTimelineUser'],
369 ['format' => '(xml|json|rss|atom|as)']);
371 $m->connect('api/statuses/mentions/:id.:format',
372 ['action' => 'ApiTimelineMentions'],
373 ['id' => Nickname::INPUT_FMT,
374 'format' => '(xml|json|rss|atom|as)']);
376 $m->connect('api/statuses/mentions.:format',
377 ['action' => 'ApiTimelineMentions'],
378 ['format' => '(xml|json|rss|atom|as)']);
380 $m->connect('api/statuses/replies/:id.:format',
381 ['action' => 'ApiTimelineMentions'],
382 ['id' => Nickname::INPUT_FMT,
383 'format' => '(xml|json|rss|atom|as)']);
385 $m->connect('api/statuses/replies.:format',
386 ['action' => 'ApiTimelineMentions'],
387 ['format' => '(xml|json|rss|atom|as)']);
389 $m->connect('api/statuses/mentions_timeline/:id.:format',
390 ['action' => 'ApiTimelineMentions'],
391 ['id' => Nickname::INPUT_FMT,
392 'format' => '(xml|json|rss|atom|as)']);
394 $m->connect('api/statuses/mentions_timeline.:format',
395 ['action' => 'ApiTimelineMentions'],
396 ['format' => '(xml|json|rss|atom|as)']);
398 $m->connect('api/statuses/friends/:id.:format',
399 ['action' => 'ApiUserFriends'],
400 ['id' => Nickname::INPUT_FMT,
401 'format' => '(xml|json)']);
403 $m->connect('api/statuses/friends.:format',
404 ['action' => 'ApiUserFriends'],
405 ['format' => '(xml|json)']);
407 $m->connect('api/statuses/followers/:id.:format',
408 ['action' => 'ApiUserFollowers'],
409 ['id' => Nickname::INPUT_FMT,
410 'format' => '(xml|json)']);
412 $m->connect('api/statuses/followers.:format',
413 ['action' => 'ApiUserFollowers'],
414 ['format' => '(xml|json)']);
416 $m->connect('api/statuses/show/:id.:format',
417 ['action' => 'ApiStatusesShow'],
419 'format' => '(xml|json|atom)']);
421 $m->connect('api/statuses/show.:format',
422 ['action' => 'ApiStatusesShow'],
423 ['format' => '(xml|json|atom)']);
425 $m->connect('api/statuses/update.:format',
426 ['action' => 'ApiStatusesUpdate'],
427 ['format' => '(xml|json|atom)']);
429 $m->connect('api/statuses/destroy/:id.:format',
430 ['action' => 'ApiStatusesDestroy'],
432 'format' => '(xml|json)']);
434 $m->connect('api/statuses/destroy.:format',
435 ['action' => 'ApiStatusesDestroy'],
436 ['format' => '(xml|json)']);
438 // START qvitter API additions
440 $m->connect('api/attachment/:id.:format',
441 ['action' => 'ApiAttachment'],
443 'format' => '(xml|json)']);
445 $m->connect('api/checkhub.:format',
446 ['action' => 'ApiCheckHub'],
447 ['format' => '(xml|json)']);
449 $m->connect('api/externalprofile/show.:format',
450 ['action' => 'ApiExternalProfileShow'],
451 ['format' => '(xml|json)']);
453 $m->connect('api/statusnet/groups/admins/:id.:format',
454 ['action' => 'ApiGroupAdmins'],
455 ['id' => Nickname::INPUT_FMT,
456 'format' => '(xml|json)']);
458 $m->connect('api/account/update_link_color.:format',
459 ['action' => 'ApiAccountUpdateLinkColor'],
460 ['format' => '(xml|json)']);
462 $m->connect('api/account/update_background_color.:format',
463 ['action' => 'ApiAccountUpdateBackgroundColor'],
464 ['format' => '(xml|json)']);
466 $m->connect('api/account/register.:format',
467 ['action' => 'ApiAccountRegister'],
468 ['format' => '(xml|json)']);
470 $m->connect('api/check_nickname.:format',
471 ['action' => 'ApiCheckNickname'],
472 ['format' => '(xml|json)']);
474 // END qvitter API additions
478 $m->connect('api/users/show/:id.:format',
479 ['action' => 'ApiUserShow'],
480 ['id' => Nickname::INPUT_FMT,
481 'format' => '(xml|json)']);
483 $m->connect('api/users/show.:format',
484 ['action' => 'ApiUserShow'],
485 ['format' => '(xml|json)']);
487 $m->connect('api/users/profile_image/:screen_name.:format',
488 ['action' => 'ApiUserProfileImage'],
489 ['screen_name' => Nickname::DISPLAY_FMT,
490 'format' => '(xml|json)']);
494 $m->connect('api/friendships/show.:format',
495 ['action' => 'ApiFriendshipsShow'],
496 ['format' => '(xml|json)']);
498 $m->connect('api/friendships/exists.:format',
499 ['action' => 'ApiFriendshipsExists'],
500 ['format' => '(xml|json)']);
502 $m->connect('api/friendships/create/:id.:format',
503 ['action' => 'ApiFriendshipsCreate'],
504 ['id' => Nickname::INPUT_FMT,
505 'format' => '(xml|json)']);
507 $m->connect('api/friendships/create.:format',
508 ['action' => 'ApiFriendshipsCreate'],
509 ['format' => '(xml|json)']);
511 $m->connect('api/friendships/destroy/:id.:format',
512 ['action' => 'ApiFriendshipsDestroy'],
513 ['id' => Nickname::INPUT_FMT,
514 'format' => '(xml|json)']);
516 $m->connect('api/friendships/destroy.:format',
517 ['action' => 'ApiFriendshipsDestroy'],
518 ['format' => '(xml|json)']);
522 $m->connect('api/friends/ids/:id.:format',
523 ['action' => 'ApiUserFriends',
525 ['id' => Nickname::INPUT_FMT,
526 'format' => '(xml|json)']);
528 $m->connect('api/followers/ids/:id.:format',
529 ['action' => 'ApiUserFollowers',
531 ['id' => Nickname::INPUT_FMT,
532 'format' => '(xml|json)']);
534 $m->connect('api/friends/ids.:format',
535 ['action' => 'ApiUserFriends',
537 ['format' => '(xml|json)']);
539 $m->connect('api/followers/ids.:format',
540 ['action' => 'ApiUserFollowers',
542 ['format' => '(xml|json)']);
546 $m->connect('api/account/verify_credentials.:format',
547 ['action' => 'ApiAccountVerifyCredentials'],
548 ['format' => '(xml|json)']);
550 $m->connect('api/account/update_profile.:format',
551 ['action' => 'ApiAccountUpdateProfile'],
552 ['format' => '(xml|json)']);
554 $m->connect('api/account/update_profile_image.:format',
555 ['action' => 'ApiAccountUpdateProfileImage'],
556 ['format' => '(xml|json)']);
558 $m->connect('api/account/update_delivery_device.:format',
559 ['action' => 'ApiAccountUpdateDeliveryDevice'],
560 ['format' => '(xml|json)']);
562 // special case where verify_credentials is called w/out a format
564 $m->connect('api/account/verify_credentials',
565 ['action' => 'ApiAccountVerifyCredentials']);
567 $m->connect('api/account/rate_limit_status.:format',
568 ['action' => 'ApiAccountRateLimitStatus'],
569 ['format' => '(xml|json)']);
573 $m->connect('api/blocks/create/:id.:format',
574 ['action' => 'ApiBlockCreate'],
575 ['id' => Nickname::INPUT_FMT,
576 'format' => '(xml|json)']);
578 $m->connect('api/blocks/create.:format',
579 ['action' => 'ApiBlockCreate'],
580 ['format' => '(xml|json)']);
582 $m->connect('api/blocks/destroy/:id.:format',
583 ['action' => 'ApiBlockDestroy'],
584 ['id' => Nickname::INPUT_FMT,
585 'format' => '(xml|json)']);
587 $m->connect('api/blocks/destroy.:format',
588 ['action' => 'ApiBlockDestroy'],
589 ['format' => '(xml|json)']);
593 $m->connect('api/help/test.:format',
594 ['action' => 'ApiHelpTest'],
595 ['format' => '(xml|json)']);
599 $m->connect('api/statusnet/version.:format',
600 ['action' => 'ApiGNUsocialVersion'],
601 ['format' => '(xml|json)']);
603 $m->connect('api/statusnet/config.:format',
604 ['action' => 'ApiGNUsocialConfig'],
605 ['format' => '(xml|json)']);
607 // For our current software name, we provide "gnusocial" base action
609 $m->connect('api/gnusocial/version.:format',
610 ['action' => 'ApiGNUsocialVersion'],
611 ['format' => '(xml|json)']);
613 $m->connect('api/gnusocial/config.:format',
614 ['action' => 'ApiGNUsocialConfig'],
615 ['format' => '(xml|json)']);
617 // Groups and tags are newer than 0.8.1 so no backward-compatibility
621 //'list' has to be handled differently, as php will not allow a method to be named 'list'
623 $m->connect('api/statusnet/groups/timeline/:id.:format',
624 ['action' => 'ApiTimelineGroup'],
625 ['id' => Nickname::INPUT_FMT,
626 'format' => '(xml|json|rss|atom|as)']);
628 $m->connect('api/statusnet/groups/show/:id.:format',
629 ['action' => 'ApiGroupShow'],
630 ['id' => Nickname::INPUT_FMT,
631 'format' => '(xml|json)']);
633 $m->connect('api/statusnet/groups/show.:format',
634 ['action' => 'ApiGroupShow'],
635 ['format' => '(xml|json)']);
637 $m->connect('api/statusnet/groups/join/:id.:format',
638 ['action' => 'ApiGroupJoin'],
639 ['id' => Nickname::INPUT_FMT,
640 'format' => '(xml|json)']);
642 $m->connect('api/statusnet/groups/join.:format',
643 ['action' => 'ApiGroupJoin'],
644 ['format' => '(xml|json)']);
646 $m->connect('api/statusnet/groups/leave/:id.:format',
647 ['action' => 'ApiGroupLeave'],
648 ['id' => Nickname::INPUT_FMT,
649 'format' => '(xml|json)']);
651 $m->connect('api/statusnet/groups/leave.:format',
652 ['action' => 'ApiGroupLeave'],
653 ['format' => '(xml|json)']);
655 $m->connect('api/statusnet/groups/is_member.:format',
656 ['action' => 'ApiGroupIsMember'],
657 ['format' => '(xml|json)']);
659 $m->connect('api/statusnet/groups/list/:id.:format',
660 ['action' => 'ApiGroupList'],
661 ['id' => Nickname::INPUT_FMT,
662 'format' => '(xml|json|rss|atom)']);
664 $m->connect('api/statusnet/groups/list.:format',
665 ['action' => 'ApiGroupList'],
666 ['format' => '(xml|json|rss|atom)']);
668 $m->connect('api/statusnet/groups/list_all.:format',
669 ['action' => 'ApiGroupListAll'],
670 ['format' => '(xml|json|rss|atom)']);
672 $m->connect('api/statusnet/groups/membership/:id.:format',
673 ['action' => 'ApiGroupMembership'],
674 ['id' => Nickname::INPUT_FMT,
675 'format' => '(xml|json)']);
677 $m->connect('api/statusnet/groups/membership.:format',
678 ['action' => 'ApiGroupMembership'],
679 ['format' => '(xml|json)']);
681 $m->connect('api/statusnet/groups/create.:format',
682 ['action' => 'ApiGroupCreate'],
683 ['format' => '(xml|json)']);
685 $m->connect('api/statusnet/groups/update/:id.:format',
686 ['action' => 'ApiGroupProfileUpdate'],
687 ['id' => '[a-zA-Z0-9]+',
688 'format' => '(xml|json)']);
690 $m->connect('api/statusnet/conversation/:id.:format',
691 ['action' => 'apiconversation'],
693 'format' => '(xml|json|rss|atom|as)']);
695 // Lists (people tags)
696 $m->connect('api/lists/list.:format',
697 ['action' => 'ApiListSubscriptions'],
698 ['format' => '(xml|json)']);
700 $m->connect('api/lists/memberships.:format',
701 ['action' => 'ApiListMemberships'],
702 ['format' => '(xml|json)']);
704 $m->connect('api/:user/lists/memberships.:format',
705 ['action' => 'ApiListMemberships'],
706 ['user' => '[a-zA-Z0-9]+',
707 'format' => '(xml|json)']);
709 $m->connect('api/lists/subscriptions.:format',
710 ['action' => 'ApiListSubscriptions'],
711 ['format' => '(xml|json)']);
713 $m->connect('api/:user/lists/subscriptions.:format',
714 ['action' => 'ApiListSubscriptions'],
715 ['user' => '[a-zA-Z0-9]+',
716 'format' => '(xml|json)']);
718 $m->connect('api/lists.:format',
719 ['action' => 'ApiLists'],
720 ['format' => '(xml|json)']);
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)']);
728 $m->connect('api/:user/lists.:format',
729 ['action' => 'ApiLists'],
730 ['user' => '[a-zA-Z0-9]+',
731 'format' => '(xml|json)']);
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)']);
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)']);
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)']);
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)']);
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)']);
766 $m->connect('api/statusnet/tags/timeline/:tag.:format',
767 ['action' => 'ApiTimelineTag'],
768 ['tag' => self::REGEX_TAG,
769 'format' => '(xml|json|rss|atom|as)']);
772 $m->connect('api/statusnet/media/upload',
773 ['action' => 'ApiMediaUpload']);
775 $m->connect('api/statuses/update_with_media.json',
776 ['action' => 'ApiMediaUpload']);
778 // Twitter Media upload API v1.1
779 $m->connect('api/media/upload.:format',
780 ['action' => 'ApiMediaUpload'],
781 ['format' => '(xml|json)']);
784 $m->connect('api/search.atom', ['action' => 'ApiSearchAtom']);
785 $m->connect('api/search.json', ['action' => 'ApiSearchJSON']);
786 $m->connect('api/trends.json', ['action' => 'ApiTrends']);
788 $m->connect('api/oauth/request_token',
789 ['action' => 'ApiOAuthRequestToken']);
791 $m->connect('api/oauth/access_token',
792 ['action' => 'ApiOAuthAccessToken']);
794 $m->connect('api/oauth/authorize',
795 ['action' => 'ApiOAuthAuthorize']);
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']);
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_]+']);
815 // Common people-tag stuff
817 $m->connect('peopletag/:tag',
818 ['action' => 'peopletag'],
819 ['tag' => self::REGEX_TAG]);
821 $m->connect('selftag/:tag',
822 ['action' => 'selftag'],
823 ['tag' => self::REGEX_TAG]);
825 $m->connect('main/addpeopletag', ['action' => 'addpeopletag']);
827 $m->connect('main/removepeopletag', ['action' => 'removepeopletag']);
829 $m->connect('main/profilecompletion', ['action' => 'profilecompletion']);
831 $m->connect('main/peopletagautocomplete', ['action' => 'peopletagautocomplete']);
835 if (common_config('singleuser', 'enabled')) {
837 $nickname = User::singleUserNickname();
839 foreach (['subscriptions', 'subscribers', 'all', 'foaf', 'replies'] as $a) {
842 'nickname' => $nickname]);
845 foreach (['subscriptions', 'subscribers'] as $a) {
846 $m->connect($a.'/:tag',
848 'nickname' => $nickname],
849 ['tag' => self::REGEX_TAG]);
852 $m->connect('subscribers/pending',
853 ['action' => 'subqueue',
854 'nickname' => $nickname]);
856 foreach (['rss', 'groups'] as $a) {
858 ['action' => 'user'.$a,
859 'nickname' => $nickname]);
862 foreach (['all', 'replies'] as $a) {
863 $m->connect($a.'/rss',
864 ['action' => $a.'rss',
865 'nickname' => $nickname]);
868 $m->connect('avatar',
869 ['action' => 'avatarbynickname',
870 'nickname' => $nickname]);
872 $m->connect('avatar/:size',
873 ['action' => 'avatarbynickname',
874 'nickname' => $nickname],
875 ['size' => '(|original|\d+)']);
877 $m->connect('tag/:tag/rss',
878 ['action' => 'userrss',
879 'nickname' => $nickname],
880 ['tag' => self::REGEX_TAG]);
882 $m->connect('tag/:tag',
883 ['action' => 'showstream',
884 'nickname' => $nickname],
885 ['tag' => self::REGEX_TAG]);
887 $m->connect('rsd.xml',
889 'nickname' => $nickname]);
893 $m->connect('peopletags',
894 ['action' => 'peopletagsbyuser']);
896 $m->connect('peopletags/private',
897 ['action' => 'peopletagsbyuser',
900 $m->connect('peopletags/public',
901 ['action' => 'peopletagsbyuser',
904 $m->connect('othertags',
905 ['action' => 'peopletagsforuser']);
907 $m->connect('peopletagsubscriptions',
908 ['action' => 'peopletagsubscriptions']);
910 $m->connect('all/:tag/subscribers',
911 ['action' => 'peopletagsubscribers'],
912 ['tag' => self::REGEX_TAG]);
914 $m->connect('all/:tag/tagged',
915 ['action' => 'peopletagged'],
916 ['tag' => self::REGEX_TAG]);
918 $m->connect('all/:tag/edit',
919 ['action' => 'editpeopletag'],
920 ['tag' => self::REGEX_TAG]);
922 foreach (['subscribe', 'unsubscribe'] as $v) {
923 $m->connect('peopletag/:id/'.$v,
924 ['action' => $v.'peopletag'],
925 ['id' => '[0-9]{1,64}']);
928 $m->connect('user/:tagger_id/profiletag/:id/id',
929 ['action' => 'profiletagbyid'],
930 ['tagger_id' => '[0-9]+',
933 $m->connect('all/:tag',
934 ['action' => 'showprofiletag',
935 'tagger' => $nickname],
936 ['tag' => self::REGEX_TAG]);
938 foreach (['subscriptions', 'subscribers'] as $a) {
939 $m->connect($a.'/:tag',
941 ['tag' => self::REGEX_TAG]);
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']);
951 foreach (['subscriptions', 'subscribers',
952 'nudge', 'all', 'foaf', 'replies',
953 'inbox', 'outbox'] as $a) {
954 $m->connect(':nickname/'.$a,
956 ['nickname' => Nickname::DISPLAY_FMT]);
959 $m->connect(':nickname/subscribers/pending',
960 ['action' => 'subqueue'],
961 ['nickname' => Nickname::DISPLAY_FMT]);
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]);
972 $m->connect(':nickname/peopletags',
973 ['action' => 'peopletagsbyuser'],
974 ['nickname' => Nickname::DISPLAY_FMT]);
976 $m->connect(':nickname/peopletags/private',
977 ['action' => 'peopletagsbyuser',
979 ['nickname' => Nickname::DISPLAY_FMT]);
981 $m->connect(':nickname/peopletags/public',
982 ['action' => 'peopletagsbyuser',
984 ['nickname' => Nickname::DISPLAY_FMT]);
986 $m->connect(':nickname/othertags',
987 ['action' => 'peopletagsforuser'],
988 ['nickname' => Nickname::DISPLAY_FMT]);
990 $m->connect(':nickname/peopletagsubscriptions',
991 ['action' => 'peopletagsubscriptions'],
992 ['nickname' => Nickname::DISPLAY_FMT]);
994 $m->connect(':tagger/all/:tag/subscribers',
995 ['action' => 'peopletagsubscribers'],
996 ['tagger' => Nickname::DISPLAY_FMT,
997 'tag' => self::REGEX_TAG]);
999 $m->connect(':tagger/all/:tag/tagged',
1000 ['action' => 'peopletagged'],
1001 ['tagger' => Nickname::DISPLAY_FMT,
1002 'tag' => self::REGEX_TAG]);
1004 $m->connect(':tagger/all/:tag/edit',
1005 ['action' => 'editpeopletag'],
1006 ['tagger' => Nickname::DISPLAY_FMT,
1007 'tag' => self::REGEX_TAG]);
1009 foreach (['subscribe', 'unsubscribe'] as $v) {
1010 $m->connect('peopletag/:id/'.$v,
1011 ['action' => $v.'peopletag'],
1012 ['id' => '[0-9]{1,64}']);
1015 $m->connect('user/:tagger_id/profiletag/:id/id',
1016 ['action' => 'profiletagbyid'],
1017 ['tagger_id' => '[0-9]+',
1020 $m->connect(':nickname/all/:tag',
1021 ['action' => 'showprofiletag'],
1022 ['nickname' => Nickname::DISPLAY_FMT,
1023 'tag' => self::REGEX_TAG]);
1025 foreach (['subscriptions', 'subscribers'] as $a) {
1026 $m->connect(':nickname/'.$a.'/:tag',
1028 ['tag' => self::REGEX_TAG,
1029 'nickname' => Nickname::DISPLAY_FMT]);
1032 foreach (['rss', 'groups'] as $a) {
1033 $m->connect(':nickname/'.$a,
1034 ['action' => 'user'.$a],
1035 ['nickname' => Nickname::DISPLAY_FMT]);
1038 $m->connect(':nickname/avatar',
1039 ['action' => 'avatarbynickname'],
1040 ['nickname' => Nickname::DISPLAY_FMT]);
1042 $m->connect(':nickname/avatar/:size',
1043 ['action' => 'avatarbynickname'],
1044 ['size' => '(|original|\d+)',
1045 'nickname' => Nickname::DISPLAY_FMT]);
1047 $m->connect(':nickname/tag/:tag/rss',
1048 ['action' => 'userrss'],
1049 ['nickname' => Nickname::DISPLAY_FMT,
1050 'tag' => self::REGEX_TAG]);
1052 $m->connect(':nickname/tag/:tag',
1053 ['action' => 'showstream'],
1054 ['nickname' => Nickname::DISPLAY_FMT,
1055 'tag' => self::REGEX_TAG]);
1057 $m->connect(':nickname/rsd.xml',
1058 ['action' => 'rsd'],
1059 ['nickname' => Nickname::DISPLAY_FMT]);
1061 $m->connect(':nickname',
1062 ['action' => 'showstream'],
1063 ['nickname' => Nickname::DISPLAY_FMT]);
1065 $m->connect(':nickname/',
1066 ['action' => 'showstream'],
1067 ['nickname' => Nickname::DISPLAY_FMT]);
1071 $m->connect('api/statusnet/app/service/:id.xml',
1072 ['action' => 'ApiAtomService'],
1073 ['id' => Nickname::DISPLAY_FMT]);
1075 $m->connect('api/statusnet/app/service.xml',
1076 ['action' => 'ApiAtomService']);
1078 $m->connect('api/statusnet/app/subscriptions/:subscriber/:subscribed.atom',
1079 ['action' => 'AtomPubShowSubscription'],
1080 ['subscriber' => '[0-9]+',
1081 'subscribed' => '[0-9]+']);
1083 $m->connect('api/statusnet/app/subscriptions/:subscriber.atom',
1084 ['action' => 'AtomPubSubscriptionFeed'],
1085 ['subscriber' => '[0-9]+']);
1087 $m->connect('api/statusnet/app/memberships/:profile/:group.atom',
1088 ['action' => 'AtomPubShowMembership'],
1089 ['profile' => '[0-9]+',
1090 'group' => '[0-9]+']);
1092 $m->connect('api/statusnet/app/memberships/:profile.atom',
1093 ['action' => 'AtomPubMembershipFeed'],
1094 ['profile' => '[0-9]+']);
1098 $m->connect('url/:id',
1099 ['action' => 'redirecturl'],
1100 ['id' => '[0-9]+']);
1104 Event::handle('RouterInitialized', [$m]);
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);
1121 function build($action, $args=null, $params=null, $fragment=null)
1123 $action_arg = array('action' => $action);
1126 $args = array_merge($action_arg, $args);
1128 $args = $action_arg;
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...
1136 $qpos = strpos($url, '?');
1137 if ($qpos !== false) {
1138 $url = substr($url, 0, $qpos+1) .
1139 str_replace('?', '&', substr($url, $qpos+1));
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 & instead of &. Encoded &s in parameters will not be
1145 $url = substr($url, 0, $qpos+1) .
1146 str_replace('&', '&', substr($url, $qpos+1));