]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
make event urls federate
[quix0rs-gnu-social.git] / lib / apiaction.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Base API action
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  API
23  * @package   StatusNet
24  * @author    Craig Andrews <candrews@integralblue.com>
25  * @author    Dan Moore <dan@moore.cx>
26  * @author    Evan Prodromou <evan@status.net>
27  * @author    Jeffery To <jeffery.to@gmail.com>
28  * @author    Toby Inkster <mail@tobyinkster.co.uk>
29  * @author    Zach Copley <zach@status.net>
30  * @copyright 2009-2010 StatusNet, Inc.
31  * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33  * @link      http://status.net/
34  */
35
36 /* External API usage documentation. Please update when you change how the API works. */
37
38 /*! @mainpage StatusNet REST API
39
40     @section Introduction
41
42     Some explanatory text about the API would be nice.
43
44     @section API Methods
45
46     @subsection timelinesmethods_sec Timeline Methods
47
48     @li @ref publictimeline
49     @li @ref friendstimeline
50
51     @subsection statusmethods_sec Status Methods
52
53     @li @ref statusesupdate
54
55     @subsection usermethods_sec User Methods
56
57     @subsection directmessagemethods_sec Direct Message Methods (now a plugin)
58
59     @subsection friendshipmethods_sec Friendship Methods
60
61     @subsection socialgraphmethods_sec Social Graph Methods
62
63     @subsection accountmethods_sec Account Methods
64
65     @subsection favoritesmethods_sec Favorites Methods
66
67     @subsection blockmethods_sec Block Methods
68
69     @subsection oauthmethods_sec OAuth Methods
70
71     @subsection helpmethods_sec Help Methods
72
73     @subsection groupmethods_sec Group Methods
74
75     @page apiroot API Root
76
77     The URLs for methods referred to in this API documentation are
78     relative to the StatusNet API root. The API root is determined by the
79     site's @b server and @b path variables, which are generally specified
80     in config.php. For example:
81
82     @code
83     $config['site']['server'] = 'example.org';
84     $config['site']['path'] = 'statusnet'
85     @endcode
86
87     The pattern for a site's API root is: @c protocol://server/path/api E.g:
88
89     @c http://example.org/statusnet/api
90
91     The @b path can be empty.  In that case the API root would simply be:
92
93     @c http://example.org/api
94
95 */
96
97 if (!defined('STATUSNET')) {
98     exit(1);
99 }
100
101 class ApiValidationException extends Exception { }
102
103 /**
104  * Contains most of the Twitter-compatible API output functions.
105  *
106  * @category API
107  * @package  StatusNet
108  * @author   Craig Andrews <candrews@integralblue.com>
109  * @author   Dan Moore <dan@moore.cx>
110  * @author   Evan Prodromou <evan@status.net>
111  * @author   Jeffery To <jeffery.to@gmail.com>
112  * @author   Toby Inkster <mail@tobyinkster.co.uk>
113  * @author   Zach Copley <zach@status.net>
114  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
115  * @link     http://status.net/
116  */
117 class ApiAction extends Action
118 {
119     const READ_ONLY  = 1;
120     const READ_WRITE = 2;
121
122     var $user      = null;
123     var $auth_user = null;
124     var $page      = null;
125     var $count     = null;
126     var $offset    = null;
127     var $limit     = null;
128     var $max_id    = null;
129     var $since_id  = null;
130     var $source    = null;
131     var $callback  = null;
132     var $format    = null;
133
134     var $access    = self::READ_ONLY;  // read (default) or read-write
135
136     static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
137
138     /**
139      * Initialization.
140      *
141      * @param array $args Web and URL arguments
142      *
143      * @return boolean false if user doesn't exist
144      */
145     protected function prepare(array $args=array())
146     {
147         GNUsocial::setApi(true); // reduce exception reports to aid in debugging
148         parent::prepare($args);
149
150         $this->format   = $this->arg('format');
151         $this->callback = $this->arg('callback');
152         $this->page     = (int)$this->arg('page', 1);
153         $this->count    = (int)$this->arg('count', 20);
154         $this->max_id   = (int)$this->arg('max_id', 0);
155         $this->since_id = (int)$this->arg('since_id', 0);
156
157         // These two are not used everywhere, mainly just AtompubAction extensions
158         $this->offset   = ($this->page-1) * $this->count;
159         $this->limit    = $this->count + 1;
160
161         if ($this->arg('since')) {
162             header('X-GNUsocial-Warning: since parameter is disabled; use since_id');
163         }
164
165         $this->source = $this->trimmed('source');
166
167         if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
168             $this->source = 'api';
169         }
170
171         return true;
172     }
173
174     /**
175      * Handle a request
176      *
177      * @param array $args Arguments from $_REQUEST
178      *
179      * @return void
180      */
181     protected function handle()
182     {
183         header('Access-Control-Allow-Origin: *');
184         parent::handle();
185     }
186
187     /**
188      * Overrides XMLOutputter::element to write booleans as strings (true|false).
189      * See that method's documentation for more info.
190      *
191      * @param string $tag     Element type or tagname
192      * @param array  $attrs   Array of element attributes, as
193      *                        key-value pairs
194      * @param string $content string content of the element
195      *
196      * @return void
197      */
198     function element($tag, $attrs=null, $content=null)
199     {
200         if (is_bool($content)) {
201             $content = ($content ? 'true' : 'false');
202         }
203
204         return parent::element($tag, $attrs, $content);
205     }
206
207     function twitterUserArray($profile, $get_notice=false)
208     {
209         $twitter_user = array();
210
211         try {
212             $user = $profile->getUser();
213         } catch (NoSuchUserException $e) {
214             $user = null;
215         }
216
217         $twitter_user['id'] = intval($profile->id);
218         $twitter_user['name'] = $profile->getBestName();
219         $twitter_user['screen_name'] = $profile->nickname;
220         $twitter_user['location'] = ($profile->location) ? $profile->location : null;
221         $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
222
223         // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
224         $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
225         $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
226
227         // START introduced by qvitter API, not necessary for StatusNet API
228         $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
229         try {
230             $avatar  = Avatar::getUploaded($profile);
231             $origurl = $avatar->displayUrl();
232         } catch (Exception $e) {
233             $origurl = $twitter_user['profile_image_url_profile_size'];
234         }
235         $twitter_user['profile_image_url_original'] = $origurl;
236
237         $twitter_user['groups_count'] = $profile->getGroupCount();
238         foreach (array('linkcolor', 'backgroundcolor') as $key) {
239             $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
240         }
241         // END introduced by qvitter API, not necessary for StatusNet API
242
243         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
244         $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
245         $twitter_user['followers_count'] = $profile->subscriberCount();
246
247         // Note: some profiles don't have an associated user
248
249         $twitter_user['friends_count'] = $profile->subscriptionCount();
250
251         $twitter_user['created_at'] = self::dateTwitter($profile->created);
252
253         $timezone = 'UTC';
254
255         if (!empty($user) && $user->timezone) {
256             $timezone = $user->timezone;
257         }
258
259         $t = new DateTime;
260         $t->setTimezone(new DateTimeZone($timezone));
261
262         $twitter_user['utc_offset'] = $t->format('Z');
263         $twitter_user['time_zone'] = $timezone;
264         $twitter_user['statuses_count'] = $profile->noticeCount();
265
266         // Is the requesting user following this user?
267         // These values might actually also mean "unknown". Ambiguity issues?
268         $twitter_user['following'] = false;
269         $twitter_user['statusnet_blocking'] = false;
270         $twitter_user['notifications'] = false;
271
272         if ($this->scoped instanceof Profile) {
273             try {
274                 $sub = Subscription::getSubscription($this->scoped, $profile);
275                 // Notifications on?
276                 $twitter_user['following'] = true;
277                 $twitter_user['statusnet_blocking']  = $this->scoped->hasBlocked($profile);
278                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
279             } catch (NoResultException $e) {
280                 // well, the values are already false...
281             }
282         }
283
284         if ($get_notice) {
285             $notice = $profile->getCurrentNotice();
286             if ($notice instanceof Notice) {
287                 // don't get user!
288                 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
289             }
290         }
291
292         // StatusNet-specific
293
294         $twitter_user['statusnet_profile_url'] = $profile->profileurl;
295
296         // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
297         Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
298
299         return $twitter_user;
300     }
301
302     function twitterStatusArray($notice, $include_user=true)
303     {
304         $base = $this->twitterSimpleStatusArray($notice, $include_user);
305
306         // FIXME: MOVE TO SHARE PLUGIN
307         if (!empty($notice->repeat_of)) {
308             $original = Notice::getKV('id', $notice->repeat_of);
309             if ($original instanceof Notice) {
310                 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
311                 $base['retweeted_status'] = $orig_array;
312             }
313         }
314
315         return $base;
316     }
317
318     function twitterSimpleStatusArray($notice, $include_user=true)
319     {
320         $profile = $notice->getProfile();
321
322         $twitter_status = array();
323         $twitter_status['text'] = $notice->content;
324         $twitter_status['truncated'] = false; # Not possible on StatusNet
325         $twitter_status['created_at'] = self::dateTwitter($notice->created);
326         try {
327             // We could just do $notice->reply_to but maybe the future holds a
328             // different story for parenting.
329             $parent = $notice->getParent();
330             $in_reply_to = $parent->id;
331         } catch (NoParentNoticeException $e) {
332             $in_reply_to = null;
333         } catch (NoResultException $e) {
334             // the in_reply_to message has probably been deleted
335             $in_reply_to = null;
336         }
337         $twitter_status['in_reply_to_status_id'] = $in_reply_to;
338
339         $source = null;
340
341         $ns = $notice->getSource();
342         if ($ns instanceof Notice_source) {
343             if (!empty($ns->name) && !empty($ns->url)) {
344                 $source = '<a href="'
345                     . htmlspecialchars($ns->url)
346                     . '" rel="nofollow">'
347                     . htmlspecialchars($ns->name)
348                     . '</a>';
349             } else {
350                 $source = $ns->code;
351             }
352         }
353
354         $twitter_status['uri'] = $notice->getUri();
355         $twitter_status['source'] = $source;
356         $twitter_status['id'] = intval($notice->id);
357
358         $replier_profile = null;
359
360         if ($notice->reply_to) {
361             $reply = Notice::getKV(intval($notice->reply_to));
362             if ($reply) {
363                 $replier_profile = $reply->getProfile();
364             }
365         }
366
367         $twitter_status['in_reply_to_user_id'] =
368             ($replier_profile) ? intval($replier_profile->id) : null;
369         $twitter_status['in_reply_to_screen_name'] =
370             ($replier_profile) ? $replier_profile->nickname : null;
371
372         try {
373             $notloc = Notice_location::locFromStored($notice);
374             // This is the format that GeoJSON expects stuff to be in
375             $twitter_status['geo'] = array('type' => 'Point',
376                                            'coordinates' => array((float) $notloc->lat,
377                                                                   (float) $notloc->lon));
378         } catch (ServerException $e) {
379             $twitter_status['geo'] = null;
380         }
381
382         // Enclosures
383         $attachments = $notice->attachments();
384
385         if (!empty($attachments)) {
386
387             $twitter_status['attachments'] = array();
388
389             foreach ($attachments as $attachment) {
390                 try {
391                     $enclosure_o = $attachment->getEnclosure();
392                     $enclosure = array();
393                     $enclosure['url'] = $enclosure_o->url;
394                     $enclosure['mimetype'] = $enclosure_o->mimetype;
395                     $enclosure['size'] = $enclosure_o->size;
396                     $twitter_status['attachments'][] = $enclosure;
397                 } catch (ServerException $e) {
398                     // There was not enough metadata available
399                 }
400             }
401         }
402
403         if ($include_user && $profile) {
404             // Don't get notice (recursive!)
405             $twitter_user = $this->twitterUserArray($profile, false);
406             $twitter_status['user'] = $twitter_user;
407         }
408
409         // StatusNet-specific
410
411         $twitter_status['statusnet_html'] = $notice->rendered;
412         $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
413
414         // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
415         Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
416                                                        array('include_user'=>$include_user)));
417
418         return $twitter_status;
419     }
420
421     function twitterGroupArray($group)
422     {
423         $twitter_group = array();
424
425         $twitter_group['id'] = intval($group->id);
426         $twitter_group['url'] = $group->permalink();
427         $twitter_group['nickname'] = $group->nickname;
428         $twitter_group['fullname'] = $group->fullname;
429
430         if ($this->scoped instanceof Profile) {
431             $twitter_group['member'] = $this->scoped->isMember($group);
432             $twitter_group['blocked'] = Group_block::isBlocked(
433                 $group,
434                 $this->scoped
435             );
436         }
437
438         $twitter_group['admin_count'] = $group->getAdminCount();
439         $twitter_group['member_count'] = $group->getMemberCount();
440         $twitter_group['original_logo'] = $group->original_logo;
441         $twitter_group['homepage_logo'] = $group->homepage_logo;
442         $twitter_group['stream_logo'] = $group->stream_logo;
443         $twitter_group['mini_logo'] = $group->mini_logo;
444         $twitter_group['homepage'] = $group->homepage;
445         $twitter_group['description'] = $group->description;
446         $twitter_group['location'] = $group->location;
447         $twitter_group['created'] = self::dateTwitter($group->created);
448         $twitter_group['modified'] = self::dateTwitter($group->modified);
449
450         return $twitter_group;
451     }
452
453     function twitterRssGroupArray($group)
454     {
455         $entry = array();
456         $entry['content']=$group->description;
457         $entry['title']=$group->nickname;
458         $entry['link']=$group->permalink();
459         $entry['published']=common_date_iso8601($group->created);
460         $entry['updated']==common_date_iso8601($group->modified);
461         $taguribase = common_config('integration', 'groupuri');
462         $entry['id'] = "group:$groupuribase:$entry[link]";
463
464         $entry['description'] = $entry['content'];
465         $entry['pubDate'] = common_date_rfc2822($group->created);
466         $entry['guid'] = $entry['link'];
467
468         return $entry;
469     }
470
471     function twitterListArray($list)
472     {
473         $profile = Profile::getKV('id', $list->tagger);
474
475         $twitter_list = array();
476         $twitter_list['id'] = $list->id;
477         $twitter_list['name'] = $list->tag;
478         $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
479         $twitter_list['slug'] = $list->tag;
480         $twitter_list['description'] = $list->description;
481         $twitter_list['subscriber_count'] = $list->subscriberCount();
482         $twitter_list['member_count'] = $list->taggedCount();
483         $twitter_list['uri'] = $list->getUri();
484
485         if ($this->scoped instanceof Profile) {
486             $twitter_list['following'] = $list->hasSubscriber($this->scoped);
487         } else {
488             $twitter_list['following'] = false;
489         }
490
491         $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
492         $twitter_list['user'] = $this->twitterUserArray($profile, false);
493
494         return $twitter_list;
495     }
496
497     function twitterRssEntryArray($notice)
498     {
499         $entry = array();
500
501         if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
502             $profile = $notice->getProfile();
503
504             // We trim() to avoid extraneous whitespace in the output
505
506             $entry['content'] = common_xml_safe_str(trim($notice->rendered));
507             $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
508             $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
509             $entry['published'] = common_date_iso8601($notice->created);
510
511             $taguribase = TagURI::base();
512             $entry['id'] = "tag:$taguribase:$entry[link]";
513
514             $entry['updated'] = $entry['published'];
515             $entry['author'] = $profile->getBestName();
516
517             // Enclosures
518             $attachments = $notice->attachments();
519             $enclosures = array();
520
521             foreach ($attachments as $attachment) {
522                 try {
523                     $enclosure_o = $attachment->getEnclosure();
524                     $enclosure = array();
525                     $enclosure['url'] = $enclosure_o->url;
526                     $enclosure['mimetype'] = $enclosure_o->mimetype;
527                     $enclosure['size'] = $enclosure_o->size;
528                     $enclosures[] = $enclosure;
529                 } catch (ServerException $e) {
530                     // There was not enough metadata available
531                 }
532             }
533
534             if (!empty($enclosures)) {
535                 $entry['enclosures'] = $enclosures;
536             }
537
538             // Tags/Categories
539             $tag = new Notice_tag();
540             $tag->notice_id = $notice->id;
541             if ($tag->find()) {
542                 $entry['tags']=array();
543                 while ($tag->fetch()) {
544                     $entry['tags'][]=$tag->tag;
545                 }
546             }
547             $tag->free();
548
549             // RSS Item specific
550             $entry['description'] = $entry['content'];
551             $entry['pubDate'] = common_date_rfc2822($notice->created);
552             $entry['guid'] = $entry['link'];
553
554             try {
555                 $notloc = Notice_location::locFromStored($notice);
556                 // This is the format that GeoJSON expects stuff to be in.
557                 // showGeoRSS() below uses it for XML output, so we reuse it
558                 $entry['geo'] = array('type' => 'Point',
559                                       'coordinates' => array((float) $notloc->lat,
560                                                              (float) $notloc->lon));
561             } catch (ServerException $e) {
562                 $entry['geo'] = null;
563             }
564
565             Event::handle('EndRssEntryArray', array($notice, &$entry));
566         }
567
568         return $entry;
569     }
570
571     function twitterRelationshipArray($source, $target)
572     {
573         $relationship = array();
574
575         $relationship['source'] =
576             $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
577         $relationship['target'] =
578             $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
579
580         return array('relationship' => $relationship);
581     }
582
583     function relationshipDetailsArray(Profile $source, Profile $target)
584     {
585         $details = array();
586
587         $details['screen_name'] = $source->getNickname();
588         $details['followed_by'] = $target->isSubscribed($source);
589
590         try {
591             $sub = Subscription::getSubscription($source, $target);
592             $details['following'] = true;
593             $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
594         } catch (NoResultException $e) {
595             $details['following'] = false;
596             $details['notifications_enabled'] = false;
597         }
598
599         $details['blocking'] = $source->hasBlocked($target);
600         $details['id'] = intval($source->id);
601
602         return $details;
603     }
604
605     function showTwitterXmlRelationship($relationship)
606     {
607         $this->elementStart('relationship');
608
609         foreach($relationship as $element => $value) {
610             if ($element == 'source' || $element == 'target') {
611                 $this->elementStart($element);
612                 $this->showXmlRelationshipDetails($value);
613                 $this->elementEnd($element);
614             }
615         }
616
617         $this->elementEnd('relationship');
618     }
619
620     function showXmlRelationshipDetails($details)
621     {
622         foreach($details as $element => $value) {
623             $this->element($element, null, $value);
624         }
625     }
626
627     function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
628     {
629         $attrs = array();
630         if ($namespaces) {
631             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
632         }
633         $this->elementStart($tag, $attrs);
634         foreach($twitter_status as $element => $value) {
635             switch ($element) {
636             case 'user':
637                 $this->showTwitterXmlUser($twitter_status['user']);
638                 break;
639             case 'text':
640                 $this->element($element, null, common_xml_safe_str($value));
641                 break;
642             case 'attachments':
643                 $this->showXmlAttachments($twitter_status['attachments']);
644                 break;
645             case 'geo':
646                 $this->showGeoXML($value);
647                 break;
648             case 'retweeted_status':
649                 // FIXME: MOVE TO SHARE PLUGIN
650                 $this->showTwitterXmlStatus($value, 'retweeted_status');
651                 break;
652             default:
653                 if (strncmp($element, 'statusnet_', 10) == 0) {
654                     $this->element('statusnet:'.substr($element, 10), null, $value);
655                 } else {
656                     $this->element($element, null, $value);
657                 }
658             }
659         }
660         $this->elementEnd($tag);
661     }
662
663     function showTwitterXmlGroup($twitter_group)
664     {
665         $this->elementStart('group');
666         foreach($twitter_group as $element => $value) {
667             $this->element($element, null, $value);
668         }
669         $this->elementEnd('group');
670     }
671
672     function showTwitterXmlList($twitter_list)
673     {
674         $this->elementStart('list');
675         foreach($twitter_list as $element => $value) {
676             if($element == 'user') {
677                 $this->showTwitterXmlUser($value, 'user');
678             }
679             else {
680                 $this->element($element, null, $value);
681             }
682         }
683         $this->elementEnd('list');
684     }
685
686     function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
687     {
688         $attrs = array();
689         if ($namespaces) {
690             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
691         }
692         $this->elementStart($role, $attrs);
693         foreach($twitter_user as $element => $value) {
694             if ($element == 'status') {
695                 $this->showTwitterXmlStatus($twitter_user['status']);
696             } else if (strncmp($element, 'statusnet_', 10) == 0) {
697                 $this->element('statusnet:'.substr($element, 10), null, $value);
698             } else {
699                 $this->element($element, null, $value);
700             }
701         }
702         $this->elementEnd($role);
703     }
704
705     function showXmlAttachments($attachments) {
706         if (!empty($attachments)) {
707             $this->elementStart('attachments', array('type' => 'array'));
708             foreach ($attachments as $attachment) {
709                 $attrs = array();
710                 $attrs['url'] = $attachment['url'];
711                 $attrs['mimetype'] = $attachment['mimetype'];
712                 $attrs['size'] = $attachment['size'];
713                 $this->element('enclosure', $attrs, '');
714             }
715             $this->elementEnd('attachments');
716         }
717     }
718
719     function showGeoXML($geo)
720     {
721         if (empty($geo)) {
722             // empty geo element
723             $this->element('geo');
724         } else {
725             $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
726             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
727             $this->elementEnd('geo');
728         }
729     }
730
731     function showGeoRSS($geo)
732     {
733         if (!empty($geo)) {
734             $this->element(
735                 'georss:point',
736                 null,
737                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
738             );
739         }
740     }
741
742     function showTwitterRssItem($entry)
743     {
744         $this->elementStart('item');
745         $this->element('title', null, $entry['title']);
746         $this->element('description', null, $entry['description']);
747         $this->element('pubDate', null, $entry['pubDate']);
748         $this->element('guid', null, $entry['guid']);
749         $this->element('link', null, $entry['link']);
750
751         // RSS only supports 1 enclosure per item
752         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
753             $enclosure = $entry['enclosures'][0];
754             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
755         }
756
757         if(array_key_exists('tags', $entry)){
758             foreach($entry['tags'] as $tag){
759                 $this->element('category', null,$tag);
760             }
761         }
762
763         $this->showGeoRSS($entry['geo']);
764         $this->elementEnd('item');
765     }
766
767     function showJsonObjects($objects)
768     {
769         print(json_encode($objects));
770     }
771
772     function showSingleXmlStatus($notice)
773     {
774         $this->initDocument('xml');
775         $twitter_status = $this->twitterStatusArray($notice);
776         $this->showTwitterXmlStatus($twitter_status, 'status', true);
777         $this->endDocument('xml');
778     }
779
780     function showSingleAtomStatus($notice)
781     {
782         header('Content-Type: application/atom+xml; charset=utf-8');
783         print $notice->asAtomEntry(true, true, true, $this->scoped);
784     }
785
786     function show_single_json_status($notice)
787     {
788         $this->initDocument('json');
789         $status = $this->twitterStatusArray($notice);
790         $this->showJsonObjects($status);
791         $this->endDocument('json');
792     }
793
794     function showXmlTimeline($notice)
795     {
796         $this->initDocument('xml');
797         $this->elementStart('statuses', array('type' => 'array',
798                                               'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
799
800         if (is_array($notice)) {
801             //FIXME: make everything calling showJsonTimeline use only Notice objects
802             $ids = array();
803             foreach ($notice as $n) {
804                 $ids[] = $n->getID();
805             }
806             $notice = Notice::multiGet('id', $ids);
807         }
808
809         while ($notice->fetch()) {
810             try {
811                 $twitter_status = $this->twitterStatusArray($notice);
812                 $this->showTwitterXmlStatus($twitter_status);
813             } catch (Exception $e) {
814                 common_log(LOG_ERR, $e->getMessage());
815                 continue;
816             }
817         }
818
819         $this->elementEnd('statuses');
820         $this->endDocument('xml');
821     }
822
823     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
824     {
825         $this->initDocument('rss');
826
827         $this->element('title', null, $title);
828         $this->element('link', null, $link);
829
830         if (!is_null($self)) {
831             $this->element(
832                 'atom:link',
833                 array(
834                     'type' => 'application/rss+xml',
835                     'href' => $self,
836                     'rel'  => 'self'
837                 )
838            );
839         }
840
841         if (!is_null($suplink)) {
842             // For FriendFeed's SUP protocol
843             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
844                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
845                                          'href' => $suplink,
846                                          'type' => 'application/json'));
847         }
848
849         if (!is_null($logo)) {
850             $this->elementStart('image');
851             $this->element('link', null, $link);
852             $this->element('title', null, $title);
853             $this->element('url', null, $logo);
854             $this->elementEnd('image');
855         }
856
857         $this->element('description', null, $subtitle);
858         $this->element('language', null, 'en-us');
859         $this->element('ttl', null, '40');
860
861         if (is_array($notice)) {
862             //FIXME: make everything calling showJsonTimeline use only Notice objects
863             $ids = array();
864             foreach ($notice as $n) {
865                 $ids[] = $n->getID();
866             }
867             $notice = Notice::multiGet('id', $ids);
868         }
869
870         while ($notice->fetch()) {
871             try {
872                 $entry = $this->twitterRssEntryArray($notice);
873                 $this->showTwitterRssItem($entry);
874             } catch (Exception $e) {
875                 common_log(LOG_ERR, $e->getMessage());
876                 // continue on exceptions
877             }
878         }
879
880         $this->endTwitterRss();
881     }
882
883     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
884     {
885         $this->initDocument('atom');
886
887         $this->element('title', null, $title);
888         $this->element('id', null, $id);
889         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
890
891         if (!is_null($logo)) {
892             $this->element('logo',null,$logo);
893         }
894
895         if (!is_null($suplink)) {
896             // For FriendFeed's SUP protocol
897             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
898                                          'href' => $suplink,
899                                          'type' => 'application/json'));
900         }
901
902         if (!is_null($selfuri)) {
903             $this->element('link', array('href' => $selfuri,
904                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
905         }
906
907         $this->element('updated', null, common_date_iso8601('now'));
908         $this->element('subtitle', null, $subtitle);
909
910         if (is_array($notice)) {
911             //FIXME: make everything calling showJsonTimeline use only Notice objects
912             $ids = array();
913             foreach ($notice as $n) {
914                 $ids[] = $n->getID();
915             }
916             $notice = Notice::multiGet('id', $ids);
917         }
918
919         while ($notice->fetch()) {
920             try {
921                 $this->raw($notice->asAtomEntry());
922             } catch (Exception $e) {
923                 common_log(LOG_ERR, $e->getMessage());
924                 continue;
925             }
926         }
927
928         $this->endDocument('atom');
929     }
930
931     function showRssGroups($group, $title, $link, $subtitle)
932     {
933         $this->initDocument('rss');
934
935         $this->element('title', null, $title);
936         $this->element('link', null, $link);
937         $this->element('description', null, $subtitle);
938         $this->element('language', null, 'en-us');
939         $this->element('ttl', null, '40');
940
941         if (is_array($group)) {
942             foreach ($group as $g) {
943                 $twitter_group = $this->twitterRssGroupArray($g);
944                 $this->showTwitterRssItem($twitter_group);
945             }
946         } else {
947             while ($group->fetch()) {
948                 $twitter_group = $this->twitterRssGroupArray($group);
949                 $this->showTwitterRssItem($twitter_group);
950             }
951         }
952
953         $this->endTwitterRss();
954     }
955
956     function showTwitterAtomEntry($entry)
957     {
958         $this->elementStart('entry');
959         $this->element('title', null, common_xml_safe_str($entry['title']));
960         $this->element(
961             'content',
962             array('type' => 'html'),
963             common_xml_safe_str($entry['content'])
964         );
965         $this->element('id', null, $entry['id']);
966         $this->element('published', null, $entry['published']);
967         $this->element('updated', null, $entry['updated']);
968         $this->element('link', array('type' => 'text/html',
969                                      'href' => $entry['link'],
970                                      'rel' => 'alternate'));
971         $this->element('link', array('type' => $entry['avatar-type'],
972                                      'href' => $entry['avatar'],
973                                      'rel' => 'image'));
974         $this->elementStart('author');
975
976         $this->element('name', null, $entry['author-name']);
977         $this->element('uri', null, $entry['author-uri']);
978
979         $this->elementEnd('author');
980         $this->elementEnd('entry');
981     }
982
983     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
984     {
985         $this->initDocument('atom');
986
987         $this->element('title', null, common_xml_safe_str($title));
988         $this->element('id', null, $id);
989         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
990
991         if (!is_null($selfuri)) {
992             $this->element('link', array('href' => $selfuri,
993                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
994         }
995
996         $this->element('updated', null, common_date_iso8601('now'));
997         $this->element('subtitle', null, common_xml_safe_str($subtitle));
998
999         if (is_array($group)) {
1000             foreach ($group as $g) {
1001                 $this->raw($g->asAtomEntry());
1002             }
1003         } else {
1004             while ($group->fetch()) {
1005                 $this->raw($group->asAtomEntry());
1006             }
1007         }
1008
1009         $this->endDocument('atom');
1010
1011     }
1012
1013     function showJsonTimeline($notice)
1014     {
1015         $this->initDocument('json');
1016
1017         $statuses = array();
1018
1019         if (is_array($notice)) {
1020             //FIXME: make everything calling showJsonTimeline use only Notice objects
1021             $ids = array();
1022             foreach ($notice as $n) {
1023                 $ids[] = $n->getID();
1024             }
1025             $notice = Notice::multiGet('id', $ids);
1026         }
1027
1028         while ($notice->fetch()) {
1029             try {
1030                 $twitter_status = $this->twitterStatusArray($notice);
1031                 array_push($statuses, $twitter_status);
1032             } catch (Exception $e) {
1033                 common_log(LOG_ERR, $e->getMessage());
1034                 continue;
1035             }
1036         }
1037
1038         $this->showJsonObjects($statuses);
1039
1040         $this->endDocument('json');
1041     }
1042
1043     function showJsonGroups($group)
1044     {
1045         $this->initDocument('json');
1046
1047         $groups = array();
1048
1049         if (is_array($group)) {
1050             foreach ($group as $g) {
1051                 $twitter_group = $this->twitterGroupArray($g);
1052                 array_push($groups, $twitter_group);
1053             }
1054         } else {
1055             while ($group->fetch()) {
1056                 $twitter_group = $this->twitterGroupArray($group);
1057                 array_push($groups, $twitter_group);
1058             }
1059         }
1060
1061         $this->showJsonObjects($groups);
1062
1063         $this->endDocument('json');
1064     }
1065
1066     function showXmlGroups($group)
1067     {
1068
1069         $this->initDocument('xml');
1070         $this->elementStart('groups', array('type' => 'array'));
1071
1072         if (is_array($group)) {
1073             foreach ($group as $g) {
1074                 $twitter_group = $this->twitterGroupArray($g);
1075                 $this->showTwitterXmlGroup($twitter_group);
1076             }
1077         } else {
1078             while ($group->fetch()) {
1079                 $twitter_group = $this->twitterGroupArray($group);
1080                 $this->showTwitterXmlGroup($twitter_group);
1081             }
1082         }
1083
1084         $this->elementEnd('groups');
1085         $this->endDocument('xml');
1086     }
1087
1088     function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1089     {
1090
1091         $this->initDocument('xml');
1092         $this->elementStart('lists_list');
1093         $this->elementStart('lists', array('type' => 'array'));
1094
1095         if (is_array($list)) {
1096             foreach ($list as $l) {
1097                 $twitter_list = $this->twitterListArray($l);
1098                 $this->showTwitterXmlList($twitter_list);
1099             }
1100         } else {
1101             while ($list->fetch()) {
1102                 $twitter_list = $this->twitterListArray($list);
1103                 $this->showTwitterXmlList($twitter_list);
1104             }
1105         }
1106
1107         $this->elementEnd('lists');
1108
1109         $this->element('next_cursor', null, $next_cursor);
1110         $this->element('previous_cursor', null, $prev_cursor);
1111
1112         $this->elementEnd('lists_list');
1113         $this->endDocument('xml');
1114     }
1115
1116     function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1117     {
1118         $this->initDocument('json');
1119
1120         $lists = array();
1121
1122         if (is_array($list)) {
1123             foreach ($list as $l) {
1124                 $twitter_list = $this->twitterListArray($l);
1125                 array_push($lists, $twitter_list);
1126             }
1127         } else {
1128             while ($list->fetch()) {
1129                 $twitter_list = $this->twitterListArray($list);
1130                 array_push($lists, $twitter_list);
1131             }
1132         }
1133
1134         $lists_list = array(
1135             'lists' => $lists,
1136             'next_cursor' => $next_cursor,
1137             'next_cursor_str' => strval($next_cursor),
1138             'previous_cursor' => $prev_cursor,
1139             'previous_cursor_str' => strval($prev_cursor)
1140         );
1141
1142         $this->showJsonObjects($lists_list);
1143
1144         $this->endDocument('json');
1145     }
1146
1147     function showTwitterXmlUsers($user)
1148     {
1149         $this->initDocument('xml');
1150         $this->elementStart('users', array('type' => 'array',
1151                                            'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1152
1153         if (is_array($user)) {
1154             foreach ($user as $u) {
1155                 $twitter_user = $this->twitterUserArray($u);
1156                 $this->showTwitterXmlUser($twitter_user);
1157             }
1158         } else {
1159             while ($user->fetch()) {
1160                 $twitter_user = $this->twitterUserArray($user);
1161                 $this->showTwitterXmlUser($twitter_user);
1162             }
1163         }
1164
1165         $this->elementEnd('users');
1166         $this->endDocument('xml');
1167     }
1168
1169     function showJsonUsers($user)
1170     {
1171         $this->initDocument('json');
1172
1173         $users = array();
1174
1175         if (is_array($user)) {
1176             foreach ($user as $u) {
1177                 $twitter_user = $this->twitterUserArray($u);
1178                 array_push($users, $twitter_user);
1179             }
1180         } else {
1181             while ($user->fetch()) {
1182                 $twitter_user = $this->twitterUserArray($user);
1183                 array_push($users, $twitter_user);
1184             }
1185         }
1186
1187         $this->showJsonObjects($users);
1188
1189         $this->endDocument('json');
1190     }
1191
1192     function showSingleJsonGroup($group)
1193     {
1194         $this->initDocument('json');
1195         $twitter_group = $this->twitterGroupArray($group);
1196         $this->showJsonObjects($twitter_group);
1197         $this->endDocument('json');
1198     }
1199
1200     function showSingleXmlGroup($group)
1201     {
1202         $this->initDocument('xml');
1203         $twitter_group = $this->twitterGroupArray($group);
1204         $this->showTwitterXmlGroup($twitter_group);
1205         $this->endDocument('xml');
1206     }
1207
1208     function showSingleJsonList($list)
1209     {
1210         $this->initDocument('json');
1211         $twitter_list = $this->twitterListArray($list);
1212         $this->showJsonObjects($twitter_list);
1213         $this->endDocument('json');
1214     }
1215
1216     function showSingleXmlList($list)
1217     {
1218         $this->initDocument('xml');
1219         $twitter_list = $this->twitterListArray($list);
1220         $this->showTwitterXmlList($twitter_list);
1221         $this->endDocument('xml');
1222     }
1223
1224     static function dateTwitter($dt)
1225     {
1226         $dateStr = date('d F Y H:i:s', strtotime($dt));
1227         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1228         $d->setTimezone(new DateTimeZone(common_timezone()));
1229         return $d->format('D M d H:i:s O Y');
1230     }
1231
1232     function initDocument($type='xml')
1233     {
1234         switch ($type) {
1235         case 'xml':
1236             header('Content-Type: application/xml; charset=utf-8');
1237             $this->startXML();
1238             break;
1239         case 'json':
1240             header('Content-Type: application/json; charset=utf-8');
1241
1242             // Check for JSONP callback
1243             if (isset($this->callback)) {
1244                 print $this->callback . '(';
1245             }
1246             break;
1247         case 'rss':
1248             header("Content-Type: application/rss+xml; charset=utf-8");
1249             $this->initTwitterRss();
1250             break;
1251         case 'atom':
1252             header('Content-Type: application/atom+xml; charset=utf-8');
1253             $this->initTwitterAtom();
1254             break;
1255         default:
1256             // TRANS: Client error on an API request with an unsupported data format.
1257             $this->clientError(_('Not a supported data format.'));
1258         }
1259
1260         return;
1261     }
1262
1263     function endDocument($type='xml')
1264     {
1265         switch ($type) {
1266         case 'xml':
1267             $this->endXML();
1268             break;
1269         case 'json':
1270             // Check for JSONP callback
1271             if (isset($this->callback)) {
1272                 print ')';
1273             }
1274             break;
1275         case 'rss':
1276             $this->endTwitterRss();
1277             break;
1278         case 'atom':
1279             $this->endTwitterRss();
1280             break;
1281         default:
1282             // TRANS: Client error on an API request with an unsupported data format.
1283             $this->clientError(_('Not a supported data format.'));
1284         }
1285         return;
1286     }
1287
1288     function initTwitterRss()
1289     {
1290         $this->startXML();
1291         $this->elementStart(
1292             'rss',
1293             array(
1294                 'version'      => '2.0',
1295                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1296                 'xmlns:georss' => 'http://www.georss.org/georss'
1297             )
1298         );
1299         $this->elementStart('channel');
1300         Event::handle('StartApiRss', array($this));
1301     }
1302
1303     function endTwitterRss()
1304     {
1305         $this->elementEnd('channel');
1306         $this->elementEnd('rss');
1307         $this->endXML();
1308     }
1309
1310     function initTwitterAtom()
1311     {
1312         $this->startXML();
1313         // FIXME: don't hardcode the language here!
1314         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1315                                           'xml:lang' => 'en-US',
1316                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1317     }
1318
1319     function endTwitterAtom()
1320     {
1321         $this->elementEnd('feed');
1322         $this->endXML();
1323     }
1324
1325     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1326     {
1327         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1328         switch ($content_type) {
1329         case 'xml':
1330             $this->showTwitterXmlUser($profile_array);
1331             break;
1332         case 'json':
1333             $this->showJsonObjects($profile_array);
1334             break;
1335         default:
1336             // TRANS: Client error on an API request with an unsupported data format.
1337             $this->clientError(_('Not a supported data format.'));
1338         }
1339         return;
1340     }
1341
1342     private static function is_decimal($str)
1343     {
1344         return preg_match('/^[0-9]+$/', $str);
1345     }
1346
1347     function getTargetUser($id)
1348     {
1349         if (empty($id)) {
1350             // Twitter supports these other ways of passing the user ID
1351             if (self::is_decimal($this->arg('id'))) {
1352                 return User::getKV($this->arg('id'));
1353             } else if ($this->arg('id')) {
1354                 $nickname = common_canonical_nickname($this->arg('id'));
1355                 return User::getKV('nickname', $nickname);
1356             } else if ($this->arg('user_id')) {
1357                 // This is to ensure that a non-numeric user_id still
1358                 // overrides screen_name even if it doesn't get used
1359                 if (self::is_decimal($this->arg('user_id'))) {
1360                     return User::getKV('id', $this->arg('user_id'));
1361                 }
1362             } else if ($this->arg('screen_name')) {
1363                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1364                 return User::getKV('nickname', $nickname);
1365             } else {
1366                 // Fall back to trying the currently authenticated user
1367                 return $this->scoped->getUser();
1368             }
1369
1370         } else if (self::is_decimal($id)) {
1371             return User::getKV($id);
1372         } else {
1373             $nickname = common_canonical_nickname($id);
1374             return User::getKV('nickname', $nickname);
1375         }
1376     }
1377
1378     function getTargetProfile($id)
1379     {
1380         if (empty($id)) {
1381
1382             // Twitter supports these other ways of passing the user ID
1383             if (self::is_decimal($this->arg('id'))) {
1384                 return Profile::getKV($this->arg('id'));
1385             } else if ($this->arg('id')) {
1386                 // Screen names currently can only uniquely identify a local user.
1387                 $nickname = common_canonical_nickname($this->arg('id'));
1388                 $user = User::getKV('nickname', $nickname);
1389                 return $user ? $user->getProfile() : null;
1390             } else if ($this->arg('user_id')) {
1391                 // This is to ensure that a non-numeric user_id still
1392                 // overrides screen_name even if it doesn't get used
1393                 if (self::is_decimal($this->arg('user_id'))) {
1394                     return Profile::getKV('id', $this->arg('user_id'));
1395                 }
1396             } else if ($this->arg('screen_name')) {
1397                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1398                 $user = User::getKV('nickname', $nickname);
1399                 return $user instanceof User ? $user->getProfile() : null;
1400             } else {
1401                 // Fall back to trying the currently authenticated user
1402                 return $this->scoped;
1403             }
1404         } else if (self::is_decimal($id)) {
1405             return Profile::getKV($id);
1406         } else {
1407             $nickname = common_canonical_nickname($id);
1408             $user = User::getKV('nickname', $nickname);
1409             return $user ? $user->getProfile() : null;
1410         }
1411     }
1412
1413     function getTargetGroup($id)
1414     {
1415         if (empty($id)) {
1416             if (self::is_decimal($this->arg('id'))) {
1417                 return User_group::getKV('id', $this->arg('id'));
1418             } else if ($this->arg('id')) {
1419                 return User_group::getForNickname($this->arg('id'));
1420             } else if ($this->arg('group_id')) {
1421                 // This is to ensure that a non-numeric group_id still
1422                 // overrides group_name even if it doesn't get used
1423                 if (self::is_decimal($this->arg('group_id'))) {
1424                     return User_group::getKV('id', $this->arg('group_id'));
1425                 }
1426             } else if ($this->arg('group_name')) {
1427                 return User_group::getForNickname($this->arg('group_name'));
1428             }
1429
1430         } else if (self::is_decimal($id)) {
1431             return User_group::getKV('id', $id);
1432         } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1433             return User_group::getKV('uri', urldecode($this->arg('uri')));
1434         } else {
1435             return User_group::getForNickname($id);
1436         }
1437     }
1438
1439     function getTargetList($user=null, $id=null)
1440     {
1441         $tagger = $this->getTargetUser($user);
1442         $list = null;
1443
1444         if (empty($id)) {
1445             $id = $this->arg('id');
1446         }
1447
1448         if($id) {
1449             if (is_numeric($id)) {
1450                 $list = Profile_list::getKV('id', $id);
1451
1452                 // only if the list with the id belongs to the tagger
1453                 if(empty($list) || $list->tagger != $tagger->id) {
1454                     $list = null;
1455                 }
1456             }
1457             if (empty($list)) {
1458                 $tag = common_canonical_tag($id);
1459                 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1460             }
1461
1462             if (!empty($list) && $list->private) {
1463                 if ($this->scoped->id == $list->tagger) {
1464                     return $list;
1465                 }
1466             } else {
1467                 return $list;
1468             }
1469         }
1470         return null;
1471     }
1472
1473     /**
1474      * Returns query argument or default value if not found. Certain
1475      * parameters used throughout the API are lightly scrubbed and
1476      * bounds checked.  This overrides Action::arg().
1477      *
1478      * @param string $key requested argument
1479      * @param string $def default value to return if $key is not provided
1480      *
1481      * @return var $var
1482      */
1483     function arg($key, $def=null)
1484     {
1485         // XXX: Do even more input validation/scrubbing?
1486
1487         if (array_key_exists($key, $this->args)) {
1488             switch($key) {
1489             case 'page':
1490                 $page = (int)$this->args['page'];
1491                 return ($page < 1) ? 1 : $page;
1492             case 'count':
1493                 $count = (int)$this->args['count'];
1494                 if ($count < 1) {
1495                     return 20;
1496                 } elseif ($count > 200) {
1497                     return 200;
1498                 } else {
1499                     return $count;
1500                 }
1501             case 'since_id':
1502                 $since_id = (int)$this->args['since_id'];
1503                 return ($since_id < 1) ? 0 : $since_id;
1504             case 'max_id':
1505                 $max_id = (int)$this->args['max_id'];
1506                 return ($max_id < 1) ? 0 : $max_id;
1507             default:
1508                 return parent::arg($key, $def);
1509             }
1510         } else {
1511             return $def;
1512         }
1513     }
1514
1515     /**
1516      * Calculate the complete URI that called up this action.  Used for
1517      * Atom rel="self" links.  Warning: this is funky.
1518      *
1519      * @return string URL    a URL suitable for rel="self" Atom links
1520      */
1521     function getSelfUri()
1522     {
1523         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1524
1525         $id = $this->arg('id');
1526         $aargs = array('format' => $this->format);
1527         if (!empty($id)) {
1528             $aargs['id'] = $id;
1529         }
1530
1531         $user = $this->arg('user');
1532         if (!empty($user)) {
1533             $aargs['user'] = $user;
1534         }
1535
1536         $tag = $this->arg('tag');
1537         if (!empty($tag)) {
1538             $aargs['tag'] = $tag;
1539         }
1540
1541         parse_str($_SERVER['QUERY_STRING'], $params);
1542         $pstring = '';
1543         if (!empty($params)) {
1544             unset($params['p']);
1545             $pstring = http_build_query($params);
1546         }
1547
1548         $uri = common_local_url($action, $aargs);
1549
1550         if (!empty($pstring)) {
1551             $uri .= '?' . $pstring;
1552         }
1553
1554         return $uri;
1555     }
1556 }