]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
Let's handle notice dataobjects instead, despite fetching twice from db
[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         }
334         $twitter_status['in_reply_to_status_id'] = $in_reply_to;
335
336         $source = null;
337
338         $ns = $notice->getSource();
339         if ($ns instanceof Notice_source) {
340             if (!empty($ns->name) && !empty($ns->url)) {
341                 $source = '<a href="'
342                     . htmlspecialchars($ns->url)
343                     . '" rel="nofollow">'
344                     . htmlspecialchars($ns->name)
345                     . '</a>';
346             } else {
347                 $source = $ns->code;
348             }
349         }
350
351         $twitter_status['uri'] = $notice->getUri();
352         $twitter_status['source'] = $source;
353         $twitter_status['id'] = intval($notice->id);
354
355         $replier_profile = null;
356
357         if ($notice->reply_to) {
358             $reply = Notice::getKV(intval($notice->reply_to));
359             if ($reply) {
360                 $replier_profile = $reply->getProfile();
361             }
362         }
363
364         $twitter_status['in_reply_to_user_id'] =
365             ($replier_profile) ? intval($replier_profile->id) : null;
366         $twitter_status['in_reply_to_screen_name'] =
367             ($replier_profile) ? $replier_profile->nickname : null;
368
369         try {
370             $notloc = Notice_location::locFromStored($notice);
371             // This is the format that GeoJSON expects stuff to be in
372             $twitter_status['geo'] = array('type' => 'Point',
373                                            'coordinates' => array((float) $notloc->lat,
374                                                                   (float) $notloc->lon));
375         } catch (ServerException $e) {
376             $twitter_status['geo'] = null;
377         }
378
379         // Enclosures
380         $attachments = $notice->attachments();
381
382         if (!empty($attachments)) {
383
384             $twitter_status['attachments'] = array();
385
386             foreach ($attachments as $attachment) {
387                 try {
388                     $enclosure_o = $attachment->getEnclosure();
389                     $enclosure = array();
390                     $enclosure['url'] = $enclosure_o->url;
391                     $enclosure['mimetype'] = $enclosure_o->mimetype;
392                     $enclosure['size'] = $enclosure_o->size;
393                     $twitter_status['attachments'][] = $enclosure;
394                 } catch (ServerException $e) {
395                     // There was not enough metadata available
396                 }
397             }
398         }
399
400         if ($include_user && $profile) {
401             // Don't get notice (recursive!)
402             $twitter_user = $this->twitterUserArray($profile, false);
403             $twitter_status['user'] = $twitter_user;
404         }
405
406         // StatusNet-specific
407
408         $twitter_status['statusnet_html'] = $notice->rendered;
409         $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
410
411         // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
412         Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
413                                                        array('include_user'=>$include_user)));
414
415         return $twitter_status;
416     }
417
418     function twitterGroupArray($group)
419     {
420         $twitter_group = array();
421
422         $twitter_group['id'] = intval($group->id);
423         $twitter_group['url'] = $group->permalink();
424         $twitter_group['nickname'] = $group->nickname;
425         $twitter_group['fullname'] = $group->fullname;
426
427         if ($this->scoped instanceof Profile) {
428             $twitter_group['member'] = $this->scoped->isMember($group);
429             $twitter_group['blocked'] = Group_block::isBlocked(
430                 $group,
431                 $this->scoped
432             );
433         }
434
435         $twitter_group['admin_count'] = $group->getAdminCount();
436         $twitter_group['member_count'] = $group->getMemberCount();
437         $twitter_group['original_logo'] = $group->original_logo;
438         $twitter_group['homepage_logo'] = $group->homepage_logo;
439         $twitter_group['stream_logo'] = $group->stream_logo;
440         $twitter_group['mini_logo'] = $group->mini_logo;
441         $twitter_group['homepage'] = $group->homepage;
442         $twitter_group['description'] = $group->description;
443         $twitter_group['location'] = $group->location;
444         $twitter_group['created'] = self::dateTwitter($group->created);
445         $twitter_group['modified'] = self::dateTwitter($group->modified);
446
447         return $twitter_group;
448     }
449
450     function twitterRssGroupArray($group)
451     {
452         $entry = array();
453         $entry['content']=$group->description;
454         $entry['title']=$group->nickname;
455         $entry['link']=$group->permalink();
456         $entry['published']=common_date_iso8601($group->created);
457         $entry['updated']==common_date_iso8601($group->modified);
458         $taguribase = common_config('integration', 'groupuri');
459         $entry['id'] = "group:$groupuribase:$entry[link]";
460
461         $entry['description'] = $entry['content'];
462         $entry['pubDate'] = common_date_rfc2822($group->created);
463         $entry['guid'] = $entry['link'];
464
465         return $entry;
466     }
467
468     function twitterListArray($list)
469     {
470         $profile = Profile::getKV('id', $list->tagger);
471
472         $twitter_list = array();
473         $twitter_list['id'] = $list->id;
474         $twitter_list['name'] = $list->tag;
475         $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
476         $twitter_list['slug'] = $list->tag;
477         $twitter_list['description'] = $list->description;
478         $twitter_list['subscriber_count'] = $list->subscriberCount();
479         $twitter_list['member_count'] = $list->taggedCount();
480         $twitter_list['uri'] = $list->getUri();
481
482         if ($this->scoped instanceof Profile) {
483             $twitter_list['following'] = $list->hasSubscriber($this->scoped);
484         } else {
485             $twitter_list['following'] = false;
486         }
487
488         $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
489         $twitter_list['user'] = $this->twitterUserArray($profile, false);
490
491         return $twitter_list;
492     }
493
494     function twitterRssEntryArray($notice)
495     {
496         $entry = array();
497
498         if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
499             $profile = $notice->getProfile();
500
501             // We trim() to avoid extraneous whitespace in the output
502
503             $entry['content'] = common_xml_safe_str(trim($notice->rendered));
504             $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
505             $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
506             $entry['published'] = common_date_iso8601($notice->created);
507
508             $taguribase = TagURI::base();
509             $entry['id'] = "tag:$taguribase:$entry[link]";
510
511             $entry['updated'] = $entry['published'];
512             $entry['author'] = $profile->getBestName();
513
514             // Enclosures
515             $attachments = $notice->attachments();
516             $enclosures = array();
517
518             foreach ($attachments as $attachment) {
519                 try {
520                     $enclosure_o = $attachment->getEnclosure();
521                     $enclosure = array();
522                     $enclosure['url'] = $enclosure_o->url;
523                     $enclosure['mimetype'] = $enclosure_o->mimetype;
524                     $enclosure['size'] = $enclosure_o->size;
525                     $enclosures[] = $enclosure;
526                 } catch (ServerException $e) {
527                     // There was not enough metadata available
528                 }
529             }
530
531             if (!empty($enclosures)) {
532                 $entry['enclosures'] = $enclosures;
533             }
534
535             // Tags/Categories
536             $tag = new Notice_tag();
537             $tag->notice_id = $notice->id;
538             if ($tag->find()) {
539                 $entry['tags']=array();
540                 while ($tag->fetch()) {
541                     $entry['tags'][]=$tag->tag;
542                 }
543             }
544             $tag->free();
545
546             // RSS Item specific
547             $entry['description'] = $entry['content'];
548             $entry['pubDate'] = common_date_rfc2822($notice->created);
549             $entry['guid'] = $entry['link'];
550
551             try {
552                 $notloc = Notice_location::locFromStored($notice);
553                 // This is the format that GeoJSON expects stuff to be in.
554                 // showGeoRSS() below uses it for XML output, so we reuse it
555                 $entry['geo'] = array('type' => 'Point',
556                                       'coordinates' => array((float) $notloc->lat,
557                                                              (float) $notloc->lon));
558             } catch (ServerException $e) {
559                 $entry['geo'] = null;
560             }
561
562             Event::handle('EndRssEntryArray', array($notice, &$entry));
563         }
564
565         return $entry;
566     }
567
568     function twitterRelationshipArray($source, $target)
569     {
570         $relationship = array();
571
572         $relationship['source'] =
573             $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
574         $relationship['target'] =
575             $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
576
577         return array('relationship' => $relationship);
578     }
579
580     function relationshipDetailsArray(Profile $source, Profile $target)
581     {
582         $details = array();
583
584         $details['screen_name'] = $source->getNickname();
585         $details['followed_by'] = $target->isSubscribed($source);
586
587         try {
588             $sub = Subscription::getSubscription($source, $target);
589             $details['following'] = true;
590             $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
591         } catch (NoResultException $e) {
592             $details['following'] = false;
593             $details['notifications_enabled'] = false;
594         }
595
596         $details['blocking'] = $source->hasBlocked($target);
597         $details['id'] = intval($source->id);
598
599         return $details;
600     }
601
602     function showTwitterXmlRelationship($relationship)
603     {
604         $this->elementStart('relationship');
605
606         foreach($relationship as $element => $value) {
607             if ($element == 'source' || $element == 'target') {
608                 $this->elementStart($element);
609                 $this->showXmlRelationshipDetails($value);
610                 $this->elementEnd($element);
611             }
612         }
613
614         $this->elementEnd('relationship');
615     }
616
617     function showXmlRelationshipDetails($details)
618     {
619         foreach($details as $element => $value) {
620             $this->element($element, null, $value);
621         }
622     }
623
624     function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
625     {
626         $attrs = array();
627         if ($namespaces) {
628             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
629         }
630         $this->elementStart($tag, $attrs);
631         foreach($twitter_status as $element => $value) {
632             switch ($element) {
633             case 'user':
634                 $this->showTwitterXmlUser($twitter_status['user']);
635                 break;
636             case 'text':
637                 $this->element($element, null, common_xml_safe_str($value));
638                 break;
639             case 'attachments':
640                 $this->showXmlAttachments($twitter_status['attachments']);
641                 break;
642             case 'geo':
643                 $this->showGeoXML($value);
644                 break;
645             case 'retweeted_status':
646                 // FIXME: MOVE TO SHARE PLUGIN
647                 $this->showTwitterXmlStatus($value, 'retweeted_status');
648                 break;
649             default:
650                 if (strncmp($element, 'statusnet_', 10) == 0) {
651                     $this->element('statusnet:'.substr($element, 10), null, $value);
652                 } else {
653                     $this->element($element, null, $value);
654                 }
655             }
656         }
657         $this->elementEnd($tag);
658     }
659
660     function showTwitterXmlGroup($twitter_group)
661     {
662         $this->elementStart('group');
663         foreach($twitter_group as $element => $value) {
664             $this->element($element, null, $value);
665         }
666         $this->elementEnd('group');
667     }
668
669     function showTwitterXmlList($twitter_list)
670     {
671         $this->elementStart('list');
672         foreach($twitter_list as $element => $value) {
673             if($element == 'user') {
674                 $this->showTwitterXmlUser($value, 'user');
675             }
676             else {
677                 $this->element($element, null, $value);
678             }
679         }
680         $this->elementEnd('list');
681     }
682
683     function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
684     {
685         $attrs = array();
686         if ($namespaces) {
687             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
688         }
689         $this->elementStart($role, $attrs);
690         foreach($twitter_user as $element => $value) {
691             if ($element == 'status') {
692                 $this->showTwitterXmlStatus($twitter_user['status']);
693             } else if (strncmp($element, 'statusnet_', 10) == 0) {
694                 $this->element('statusnet:'.substr($element, 10), null, $value);
695             } else {
696                 $this->element($element, null, $value);
697             }
698         }
699         $this->elementEnd($role);
700     }
701
702     function showXmlAttachments($attachments) {
703         if (!empty($attachments)) {
704             $this->elementStart('attachments', array('type' => 'array'));
705             foreach ($attachments as $attachment) {
706                 $attrs = array();
707                 $attrs['url'] = $attachment['url'];
708                 $attrs['mimetype'] = $attachment['mimetype'];
709                 $attrs['size'] = $attachment['size'];
710                 $this->element('enclosure', $attrs, '');
711             }
712             $this->elementEnd('attachments');
713         }
714     }
715
716     function showGeoXML($geo)
717     {
718         if (empty($geo)) {
719             // empty geo element
720             $this->element('geo');
721         } else {
722             $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
723             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
724             $this->elementEnd('geo');
725         }
726     }
727
728     function showGeoRSS($geo)
729     {
730         if (!empty($geo)) {
731             $this->element(
732                 'georss:point',
733                 null,
734                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
735             );
736         }
737     }
738
739     function showTwitterRssItem($entry)
740     {
741         $this->elementStart('item');
742         $this->element('title', null, $entry['title']);
743         $this->element('description', null, $entry['description']);
744         $this->element('pubDate', null, $entry['pubDate']);
745         $this->element('guid', null, $entry['guid']);
746         $this->element('link', null, $entry['link']);
747
748         // RSS only supports 1 enclosure per item
749         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
750             $enclosure = $entry['enclosures'][0];
751             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
752         }
753
754         if(array_key_exists('tags', $entry)){
755             foreach($entry['tags'] as $tag){
756                 $this->element('category', null,$tag);
757             }
758         }
759
760         $this->showGeoRSS($entry['geo']);
761         $this->elementEnd('item');
762     }
763
764     function showJsonObjects($objects)
765     {
766         print(json_encode($objects));
767     }
768
769     function showSingleXmlStatus($notice)
770     {
771         $this->initDocument('xml');
772         $twitter_status = $this->twitterStatusArray($notice);
773         $this->showTwitterXmlStatus($twitter_status, 'status', true);
774         $this->endDocument('xml');
775     }
776
777     function showSingleAtomStatus($notice)
778     {
779         header('Content-Type: application/atom+xml; charset=utf-8');
780         print $notice->asAtomEntry(true, true, true, $this->scoped);
781     }
782
783     function show_single_json_status($notice)
784     {
785         $this->initDocument('json');
786         $status = $this->twitterStatusArray($notice);
787         $this->showJsonObjects($status);
788         $this->endDocument('json');
789     }
790
791     function showXmlTimeline($notice)
792     {
793         $this->initDocument('xml');
794         $this->elementStart('statuses', array('type' => 'array',
795                                               'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
796
797         if (is_array($notice)) {
798             //FIXME: make everything calling showJsonTimeline use only Notice objects
799             common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!');
800             $ids = array();
801             foreach ($notice as $n) {
802                 $ids[] = $n->getID();
803             }
804             $notice = Notice::multiGet('id', $ids);
805         }
806
807         while ($notice->fetch()) {
808             try {
809                 $twitter_status = $this->twitterStatusArray($notice);
810                 $this->showTwitterXmlStatus($twitter_status);
811             } catch (Exception $e) {
812                 common_log(LOG_ERR, $e->getMessage());
813                 continue;
814             }
815         }
816
817         $this->elementEnd('statuses');
818         $this->endDocument('xml');
819     }
820
821     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
822     {
823         $this->initDocument('rss');
824
825         $this->element('title', null, $title);
826         $this->element('link', null, $link);
827
828         if (!is_null($self)) {
829             $this->element(
830                 'atom:link',
831                 array(
832                     'type' => 'application/rss+xml',
833                     'href' => $self,
834                     'rel'  => 'self'
835                 )
836            );
837         }
838
839         if (!is_null($suplink)) {
840             // For FriendFeed's SUP protocol
841             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
842                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
843                                          'href' => $suplink,
844                                          'type' => 'application/json'));
845         }
846
847         if (!is_null($logo)) {
848             $this->elementStart('image');
849             $this->element('link', null, $link);
850             $this->element('title', null, $title);
851             $this->element('url', null, $logo);
852             $this->elementEnd('image');
853         }
854
855         $this->element('description', null, $subtitle);
856         $this->element('language', null, 'en-us');
857         $this->element('ttl', null, '40');
858
859         if (is_array($notice)) {
860             //FIXME: make everything calling showJsonTimeline use only Notice objects
861             common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!');
862             $ids = array();
863             foreach ($notice as $n) {
864                 $ids[] = $n->getID();
865             }
866             $notice = Notice::multiGet('id', $ids);
867         }
868
869         while ($notice->fetch()) {
870             try {
871                 $entry = $this->twitterRssEntryArray($notice);
872                 $this->showTwitterRssItem($entry);
873             } catch (Exception $e) {
874                 common_log(LOG_ERR, $e->getMessage());
875                 // continue on exceptions
876             }
877         }
878
879         $this->endTwitterRss();
880     }
881
882     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
883     {
884         $this->initDocument('atom');
885
886         $this->element('title', null, $title);
887         $this->element('id', null, $id);
888         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
889
890         if (!is_null($logo)) {
891             $this->element('logo',null,$logo);
892         }
893
894         if (!is_null($suplink)) {
895             // For FriendFeed's SUP protocol
896             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
897                                          'href' => $suplink,
898                                          'type' => 'application/json'));
899         }
900
901         if (!is_null($selfuri)) {
902             $this->element('link', array('href' => $selfuri,
903                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
904         }
905
906         $this->element('updated', null, common_date_iso8601('now'));
907         $this->element('subtitle', null, $subtitle);
908
909         if (is_array($notice)) {
910             //FIXME: make everything calling showJsonTimeline use only Notice objects
911             common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive 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             common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!');
1022             $ids = array();
1023             foreach ($notice as $n) {
1024                 $ids[] = $n->getID();
1025             }
1026             $notice = Notice::multiGet('id', $ids);
1027         }
1028
1029         while ($notice->fetch()) {
1030             try {
1031                 $twitter_status = $this->twitterStatusArray($notice);
1032                 array_push($statuses, $twitter_status);
1033             } catch (Exception $e) {
1034                 common_log(LOG_ERR, $e->getMessage());
1035                 continue;
1036             }
1037         }
1038
1039         $this->showJsonObjects($statuses);
1040
1041         $this->endDocument('json');
1042     }
1043
1044     function showJsonGroups($group)
1045     {
1046         $this->initDocument('json');
1047
1048         $groups = array();
1049
1050         if (is_array($group)) {
1051             foreach ($group as $g) {
1052                 $twitter_group = $this->twitterGroupArray($g);
1053                 array_push($groups, $twitter_group);
1054             }
1055         } else {
1056             while ($group->fetch()) {
1057                 $twitter_group = $this->twitterGroupArray($group);
1058                 array_push($groups, $twitter_group);
1059             }
1060         }
1061
1062         $this->showJsonObjects($groups);
1063
1064         $this->endDocument('json');
1065     }
1066
1067     function showXmlGroups($group)
1068     {
1069
1070         $this->initDocument('xml');
1071         $this->elementStart('groups', array('type' => 'array'));
1072
1073         if (is_array($group)) {
1074             foreach ($group as $g) {
1075                 $twitter_group = $this->twitterGroupArray($g);
1076                 $this->showTwitterXmlGroup($twitter_group);
1077             }
1078         } else {
1079             while ($group->fetch()) {
1080                 $twitter_group = $this->twitterGroupArray($group);
1081                 $this->showTwitterXmlGroup($twitter_group);
1082             }
1083         }
1084
1085         $this->elementEnd('groups');
1086         $this->endDocument('xml');
1087     }
1088
1089     function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1090     {
1091
1092         $this->initDocument('xml');
1093         $this->elementStart('lists_list');
1094         $this->elementStart('lists', array('type' => 'array'));
1095
1096         if (is_array($list)) {
1097             foreach ($list as $l) {
1098                 $twitter_list = $this->twitterListArray($l);
1099                 $this->showTwitterXmlList($twitter_list);
1100             }
1101         } else {
1102             while ($list->fetch()) {
1103                 $twitter_list = $this->twitterListArray($list);
1104                 $this->showTwitterXmlList($twitter_list);
1105             }
1106         }
1107
1108         $this->elementEnd('lists');
1109
1110         $this->element('next_cursor', null, $next_cursor);
1111         $this->element('previous_cursor', null, $prev_cursor);
1112
1113         $this->elementEnd('lists_list');
1114         $this->endDocument('xml');
1115     }
1116
1117     function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1118     {
1119         $this->initDocument('json');
1120
1121         $lists = array();
1122
1123         if (is_array($list)) {
1124             foreach ($list as $l) {
1125                 $twitter_list = $this->twitterListArray($l);
1126                 array_push($lists, $twitter_list);
1127             }
1128         } else {
1129             while ($list->fetch()) {
1130                 $twitter_list = $this->twitterListArray($list);
1131                 array_push($lists, $twitter_list);
1132             }
1133         }
1134
1135         $lists_list = array(
1136             'lists' => $lists,
1137             'next_cursor' => $next_cursor,
1138             'next_cursor_str' => strval($next_cursor),
1139             'previous_cursor' => $prev_cursor,
1140             'previous_cursor_str' => strval($prev_cursor)
1141         );
1142
1143         $this->showJsonObjects($lists_list);
1144
1145         $this->endDocument('json');
1146     }
1147
1148     function showTwitterXmlUsers($user)
1149     {
1150         $this->initDocument('xml');
1151         $this->elementStart('users', array('type' => 'array',
1152                                            'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1153
1154         if (is_array($user)) {
1155             foreach ($user as $u) {
1156                 $twitter_user = $this->twitterUserArray($u);
1157                 $this->showTwitterXmlUser($twitter_user);
1158             }
1159         } else {
1160             while ($user->fetch()) {
1161                 $twitter_user = $this->twitterUserArray($user);
1162                 $this->showTwitterXmlUser($twitter_user);
1163             }
1164         }
1165
1166         $this->elementEnd('users');
1167         $this->endDocument('xml');
1168     }
1169
1170     function showJsonUsers($user)
1171     {
1172         $this->initDocument('json');
1173
1174         $users = array();
1175
1176         if (is_array($user)) {
1177             foreach ($user as $u) {
1178                 $twitter_user = $this->twitterUserArray($u);
1179                 array_push($users, $twitter_user);
1180             }
1181         } else {
1182             while ($user->fetch()) {
1183                 $twitter_user = $this->twitterUserArray($user);
1184                 array_push($users, $twitter_user);
1185             }
1186         }
1187
1188         $this->showJsonObjects($users);
1189
1190         $this->endDocument('json');
1191     }
1192
1193     function showSingleJsonGroup($group)
1194     {
1195         $this->initDocument('json');
1196         $twitter_group = $this->twitterGroupArray($group);
1197         $this->showJsonObjects($twitter_group);
1198         $this->endDocument('json');
1199     }
1200
1201     function showSingleXmlGroup($group)
1202     {
1203         $this->initDocument('xml');
1204         $twitter_group = $this->twitterGroupArray($group);
1205         $this->showTwitterXmlGroup($twitter_group);
1206         $this->endDocument('xml');
1207     }
1208
1209     function showSingleJsonList($list)
1210     {
1211         $this->initDocument('json');
1212         $twitter_list = $this->twitterListArray($list);
1213         $this->showJsonObjects($twitter_list);
1214         $this->endDocument('json');
1215     }
1216
1217     function showSingleXmlList($list)
1218     {
1219         $this->initDocument('xml');
1220         $twitter_list = $this->twitterListArray($list);
1221         $this->showTwitterXmlList($twitter_list);
1222         $this->endDocument('xml');
1223     }
1224
1225     static function dateTwitter($dt)
1226     {
1227         $dateStr = date('d F Y H:i:s', strtotime($dt));
1228         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1229         $d->setTimezone(new DateTimeZone(common_timezone()));
1230         return $d->format('D M d H:i:s O Y');
1231     }
1232
1233     function initDocument($type='xml')
1234     {
1235         switch ($type) {
1236         case 'xml':
1237             header('Content-Type: application/xml; charset=utf-8');
1238             $this->startXML();
1239             break;
1240         case 'json':
1241             header('Content-Type: application/json; charset=utf-8');
1242
1243             // Check for JSONP callback
1244             if (isset($this->callback)) {
1245                 print $this->callback . '(';
1246             }
1247             break;
1248         case 'rss':
1249             header("Content-Type: application/rss+xml; charset=utf-8");
1250             $this->initTwitterRss();
1251             break;
1252         case 'atom':
1253             header('Content-Type: application/atom+xml; charset=utf-8');
1254             $this->initTwitterAtom();
1255             break;
1256         default:
1257             // TRANS: Client error on an API request with an unsupported data format.
1258             $this->clientError(_('Not a supported data format.'));
1259         }
1260
1261         return;
1262     }
1263
1264     function endDocument($type='xml')
1265     {
1266         switch ($type) {
1267         case 'xml':
1268             $this->endXML();
1269             break;
1270         case 'json':
1271             // Check for JSONP callback
1272             if (isset($this->callback)) {
1273                 print ')';
1274             }
1275             break;
1276         case 'rss':
1277             $this->endTwitterRss();
1278             break;
1279         case 'atom':
1280             $this->endTwitterRss();
1281             break;
1282         default:
1283             // TRANS: Client error on an API request with an unsupported data format.
1284             $this->clientError(_('Not a supported data format.'));
1285         }
1286         return;
1287     }
1288
1289     function initTwitterRss()
1290     {
1291         $this->startXML();
1292         $this->elementStart(
1293             'rss',
1294             array(
1295                 'version'      => '2.0',
1296                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1297                 'xmlns:georss' => 'http://www.georss.org/georss'
1298             )
1299         );
1300         $this->elementStart('channel');
1301         Event::handle('StartApiRss', array($this));
1302     }
1303
1304     function endTwitterRss()
1305     {
1306         $this->elementEnd('channel');
1307         $this->elementEnd('rss');
1308         $this->endXML();
1309     }
1310
1311     function initTwitterAtom()
1312     {
1313         $this->startXML();
1314         // FIXME: don't hardcode the language here!
1315         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1316                                           'xml:lang' => 'en-US',
1317                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1318     }
1319
1320     function endTwitterAtom()
1321     {
1322         $this->elementEnd('feed');
1323         $this->endXML();
1324     }
1325
1326     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1327     {
1328         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1329         switch ($content_type) {
1330         case 'xml':
1331             $this->showTwitterXmlUser($profile_array);
1332             break;
1333         case 'json':
1334             $this->showJsonObjects($profile_array);
1335             break;
1336         default:
1337             // TRANS: Client error on an API request with an unsupported data format.
1338             $this->clientError(_('Not a supported data format.'));
1339         }
1340         return;
1341     }
1342
1343     private static function is_decimal($str)
1344     {
1345         return preg_match('/^[0-9]+$/', $str);
1346     }
1347
1348     function getTargetUser($id)
1349     {
1350         if (empty($id)) {
1351             // Twitter supports these other ways of passing the user ID
1352             if (self::is_decimal($this->arg('id'))) {
1353                 return User::getKV($this->arg('id'));
1354             } else if ($this->arg('id')) {
1355                 $nickname = common_canonical_nickname($this->arg('id'));
1356                 return User::getKV('nickname', $nickname);
1357             } else if ($this->arg('user_id')) {
1358                 // This is to ensure that a non-numeric user_id still
1359                 // overrides screen_name even if it doesn't get used
1360                 if (self::is_decimal($this->arg('user_id'))) {
1361                     return User::getKV('id', $this->arg('user_id'));
1362                 }
1363             } else if ($this->arg('screen_name')) {
1364                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1365                 return User::getKV('nickname', $nickname);
1366             } else {
1367                 // Fall back to trying the currently authenticated user
1368                 return $this->scoped->getUser();
1369             }
1370
1371         } else if (self::is_decimal($id)) {
1372             return User::getKV($id);
1373         } else {
1374             $nickname = common_canonical_nickname($id);
1375             return User::getKV('nickname', $nickname);
1376         }
1377     }
1378
1379     function getTargetProfile($id)
1380     {
1381         if (empty($id)) {
1382
1383             // Twitter supports these other ways of passing the user ID
1384             if (self::is_decimal($this->arg('id'))) {
1385                 return Profile::getKV($this->arg('id'));
1386             } else if ($this->arg('id')) {
1387                 // Screen names currently can only uniquely identify a local user.
1388                 $nickname = common_canonical_nickname($this->arg('id'));
1389                 $user = User::getKV('nickname', $nickname);
1390                 return $user ? $user->getProfile() : null;
1391             } else if ($this->arg('user_id')) {
1392                 // This is to ensure that a non-numeric user_id still
1393                 // overrides screen_name even if it doesn't get used
1394                 if (self::is_decimal($this->arg('user_id'))) {
1395                     return Profile::getKV('id', $this->arg('user_id'));
1396                 }
1397             } else if ($this->arg('screen_name')) {
1398                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1399                 $user = User::getKV('nickname', $nickname);
1400                 return $user instanceof User ? $user->getProfile() : null;
1401             } else {
1402                 // Fall back to trying the currently authenticated user
1403                 return $this->scoped;
1404             }
1405         } else if (self::is_decimal($id)) {
1406             return Profile::getKV($id);
1407         } else {
1408             $nickname = common_canonical_nickname($id);
1409             $user = User::getKV('nickname', $nickname);
1410             return $user ? $user->getProfile() : null;
1411         }
1412     }
1413
1414     function getTargetGroup($id)
1415     {
1416         if (empty($id)) {
1417             if (self::is_decimal($this->arg('id'))) {
1418                 return User_group::getKV('id', $this->arg('id'));
1419             } else if ($this->arg('id')) {
1420                 return User_group::getForNickname($this->arg('id'));
1421             } else if ($this->arg('group_id')) {
1422                 // This is to ensure that a non-numeric group_id still
1423                 // overrides group_name even if it doesn't get used
1424                 if (self::is_decimal($this->arg('group_id'))) {
1425                     return User_group::getKV('id', $this->arg('group_id'));
1426                 }
1427             } else if ($this->arg('group_name')) {
1428                 return User_group::getForNickname($this->arg('group_name'));
1429             }
1430
1431         } else if (self::is_decimal($id)) {
1432             return User_group::getKV('id', $id);
1433         } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1434             return User_group::getKV('uri', urldecode($this->arg('uri')));
1435         } else {
1436             return User_group::getForNickname($id);
1437         }
1438     }
1439
1440     function getTargetList($user=null, $id=null)
1441     {
1442         $tagger = $this->getTargetUser($user);
1443         $list = null;
1444
1445         if (empty($id)) {
1446             $id = $this->arg('id');
1447         }
1448
1449         if($id) {
1450             if (is_numeric($id)) {
1451                 $list = Profile_list::getKV('id', $id);
1452
1453                 // only if the list with the id belongs to the tagger
1454                 if(empty($list) || $list->tagger != $tagger->id) {
1455                     $list = null;
1456                 }
1457             }
1458             if (empty($list)) {
1459                 $tag = common_canonical_tag($id);
1460                 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1461             }
1462
1463             if (!empty($list) && $list->private) {
1464                 if ($this->scoped->id == $list->tagger) {
1465                     return $list;
1466                 }
1467             } else {
1468                 return $list;
1469             }
1470         }
1471         return null;
1472     }
1473
1474     /**
1475      * Returns query argument or default value if not found. Certain
1476      * parameters used throughout the API are lightly scrubbed and
1477      * bounds checked.  This overrides Action::arg().
1478      *
1479      * @param string $key requested argument
1480      * @param string $def default value to return if $key is not provided
1481      *
1482      * @return var $var
1483      */
1484     function arg($key, $def=null)
1485     {
1486         // XXX: Do even more input validation/scrubbing?
1487
1488         if (array_key_exists($key, $this->args)) {
1489             switch($key) {
1490             case 'page':
1491                 $page = (int)$this->args['page'];
1492                 return ($page < 1) ? 1 : $page;
1493             case 'count':
1494                 $count = (int)$this->args['count'];
1495                 if ($count < 1) {
1496                     return 20;
1497                 } elseif ($count > 200) {
1498                     return 200;
1499                 } else {
1500                     return $count;
1501                 }
1502             case 'since_id':
1503                 $since_id = (int)$this->args['since_id'];
1504                 return ($since_id < 1) ? 0 : $since_id;
1505             case 'max_id':
1506                 $max_id = (int)$this->args['max_id'];
1507                 return ($max_id < 1) ? 0 : $max_id;
1508             default:
1509                 return parent::arg($key, $def);
1510             }
1511         } else {
1512             return $def;
1513         }
1514     }
1515
1516     /**
1517      * Calculate the complete URI that called up this action.  Used for
1518      * Atom rel="self" links.  Warning: this is funky.
1519      *
1520      * @return string URL    a URL suitable for rel="self" Atom links
1521      */
1522     function getSelfUri()
1523     {
1524         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1525
1526         $id = $this->arg('id');
1527         $aargs = array('format' => $this->format);
1528         if (!empty($id)) {
1529             $aargs['id'] = $id;
1530         }
1531
1532         $user = $this->arg('user');
1533         if (!empty($user)) {
1534             $aargs['user'] = $user;
1535         }
1536
1537         $tag = $this->arg('tag');
1538         if (!empty($tag)) {
1539             $aargs['tag'] = $tag;
1540         }
1541
1542         parse_str($_SERVER['QUERY_STRING'], $params);
1543         $pstring = '';
1544         if (!empty($params)) {
1545             unset($params['p']);
1546             $pstring = http_build_query($params);
1547         }
1548
1549         $uri = common_local_url($action, $aargs);
1550
1551         if (!empty($pstring)) {
1552             $uri .= '?' . $pstring;
1553         }
1554
1555         return $uri;
1556     }
1557 }