]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
Output proper HTML and XML headers for single Atom entry
[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'] = $profile->getID();
218         $twitter_user['name'] = $profile->getBestName();
219         $twitter_user['screen_name'] = $profile->getNickname();
220         $twitter_user['location'] = $profile->location;
221         $twitter_user['description'] = $profile->getDescription();
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['notifications'] = ($sub->jabber || $sub->sms);
278             } catch (NoResultException $e) {
279                 // well, the values are already false...
280             }
281             $twitter_user['statusnet_blocking']  = $this->scoped->hasBlocked($profile);            
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         $source_link = null;
341
342         $ns = $notice->getSource();
343         if ($ns instanceof Notice_source) {
344             $source = $ns->code;
345             if (!empty($ns->url)) {
346                 $source_link = $ns->url;
347                 if (!empty($ns->name)) {
348                     $source = $ns->name;
349                 }
350             }
351         }
352
353         $twitter_status['uri'] = $notice->getUri();
354         $twitter_status['source'] = $source;
355         $twitter_status['source_link'] = $source_link;
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->getRendered();
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->getRendered()));
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                     if ($element === 'statusnet_in_groups' && is_array($value)) {
655                         // QVITTERFIX because it would cause an array to be sent as $value
656                         // THIS IS UNDOCUMENTED AND SHOULD NEVER BE RELIED UPON (qvitter uses json output)
657                         $value = json_encode($value);
658                     }
659                     $this->element('statusnet:'.substr($element, 10), null, $value);
660                 } else {
661                     $this->element($element, null, $value);
662                 }
663             }
664         }
665         $this->elementEnd($tag);
666     }
667
668     function showTwitterXmlGroup($twitter_group)
669     {
670         $this->elementStart('group');
671         foreach($twitter_group as $element => $value) {
672             $this->element($element, null, $value);
673         }
674         $this->elementEnd('group');
675     }
676
677     function showTwitterXmlList($twitter_list)
678     {
679         $this->elementStart('list');
680         foreach($twitter_list as $element => $value) {
681             if($element == 'user') {
682                 $this->showTwitterXmlUser($value, 'user');
683             }
684             else {
685                 $this->element($element, null, $value);
686             }
687         }
688         $this->elementEnd('list');
689     }
690
691     function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
692     {
693         $attrs = array();
694         if ($namespaces) {
695             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
696         }
697         $this->elementStart($role, $attrs);
698         foreach($twitter_user as $element => $value) {
699             if ($element == 'status') {
700                 $this->showTwitterXmlStatus($twitter_user['status']);
701             } else if (strncmp($element, 'statusnet_', 10) == 0) {
702                 $this->element('statusnet:'.substr($element, 10), null, $value);
703             } else {
704                 $this->element($element, null, $value);
705             }
706         }
707         $this->elementEnd($role);
708     }
709
710     function showXmlAttachments($attachments) {
711         if (!empty($attachments)) {
712             $this->elementStart('attachments', array('type' => 'array'));
713             foreach ($attachments as $attachment) {
714                 $attrs = array();
715                 $attrs['url'] = $attachment['url'];
716                 $attrs['mimetype'] = $attachment['mimetype'];
717                 $attrs['size'] = $attachment['size'];
718                 $this->element('enclosure', $attrs, '');
719             }
720             $this->elementEnd('attachments');
721         }
722     }
723
724     function showGeoXML($geo)
725     {
726         if (empty($geo)) {
727             // empty geo element
728             $this->element('geo');
729         } else {
730             $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
731             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
732             $this->elementEnd('geo');
733         }
734     }
735
736     function showGeoRSS($geo)
737     {
738         if (!empty($geo)) {
739             $this->element(
740                 'georss:point',
741                 null,
742                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
743             );
744         }
745     }
746
747     function showTwitterRssItem($entry)
748     {
749         $this->elementStart('item');
750         $this->element('title', null, $entry['title']);
751         $this->element('description', null, $entry['description']);
752         $this->element('pubDate', null, $entry['pubDate']);
753         $this->element('guid', null, $entry['guid']);
754         $this->element('link', null, $entry['link']);
755
756         // RSS only supports 1 enclosure per item
757         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
758             $enclosure = $entry['enclosures'][0];
759             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
760         }
761
762         if(array_key_exists('tags', $entry)){
763             foreach($entry['tags'] as $tag){
764                 $this->element('category', null,$tag);
765             }
766         }
767
768         $this->showGeoRSS($entry['geo']);
769         $this->elementEnd('item');
770     }
771
772     function showJsonObjects($objects)
773     {
774         $json_objects = json_encode($objects);
775         if($json_objects === false) {
776             $this->clientError(_('JSON encoding failed. Error: ').json_last_error_msg());                  
777         } else {
778                 print $json_objects;
779         }
780     }
781
782
783     function showSingleXmlStatus($notice)
784     {
785         $this->initDocument('xml');
786         $twitter_status = $this->twitterStatusArray($notice);
787         $this->showTwitterXmlStatus($twitter_status, 'status', true);
788         $this->endDocument('xml');
789     }
790
791     function showSingleAtomStatus($notice)
792     {
793         header('Content-Type: application/atom+xml;type=entry;charset="utf-8"');
794         print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
795         print $notice->asAtomEntry(true, true, true, $this->scoped);
796     }
797
798     function show_single_json_status($notice)
799     {
800         $this->initDocument('json');
801         $status = $this->twitterStatusArray($notice);
802         $this->showJsonObjects($status);
803         $this->endDocument('json');
804     }
805
806     function showXmlTimeline($notice)
807     {
808         $this->initDocument('xml');
809         $this->elementStart('statuses', array('type' => 'array',
810                                               'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
811
812         if (is_array($notice)) {
813             //FIXME: make everything calling showJsonTimeline use only Notice objects
814             $ids = array();
815             foreach ($notice as $n) {
816                 $ids[] = $n->getID();
817             }
818             $notice = Notice::multiGet('id', $ids);
819         }
820
821         while ($notice->fetch()) {
822             try {
823                 $twitter_status = $this->twitterStatusArray($notice);
824                 $this->showTwitterXmlStatus($twitter_status);
825             } catch (Exception $e) {
826                 common_log(LOG_ERR, $e->getMessage());
827                 continue;
828             }
829         }
830
831         $this->elementEnd('statuses');
832         $this->endDocument('xml');
833     }
834
835     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
836     {
837         $this->initDocument('rss');
838
839         $this->element('title', null, $title);
840         $this->element('link', null, $link);
841
842         if (!is_null($self)) {
843             $this->element(
844                 'atom:link',
845                 array(
846                     'type' => 'application/rss+xml',
847                     'href' => $self,
848                     'rel'  => 'self'
849                 )
850            );
851         }
852
853         if (!is_null($suplink)) {
854             // For FriendFeed's SUP protocol
855             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
856                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
857                                          'href' => $suplink,
858                                          'type' => 'application/json'));
859         }
860
861         if (!is_null($logo)) {
862             $this->elementStart('image');
863             $this->element('link', null, $link);
864             $this->element('title', null, $title);
865             $this->element('url', null, $logo);
866             $this->elementEnd('image');
867         }
868
869         $this->element('description', null, $subtitle);
870         $this->element('language', null, 'en-us');
871         $this->element('ttl', null, '40');
872
873         if (is_array($notice)) {
874             //FIXME: make everything calling showJsonTimeline use only Notice objects
875             $ids = array();
876             foreach ($notice as $n) {
877                 $ids[] = $n->getID();
878             }
879             $notice = Notice::multiGet('id', $ids);
880         }
881
882         while ($notice->fetch()) {
883             try {
884                 $entry = $this->twitterRssEntryArray($notice);
885                 $this->showTwitterRssItem($entry);
886             } catch (Exception $e) {
887                 common_log(LOG_ERR, $e->getMessage());
888                 // continue on exceptions
889             }
890         }
891
892         $this->endTwitterRss();
893     }
894
895     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
896     {
897         $this->initDocument('atom');
898
899         $this->element('title', null, $title);
900         $this->element('id', null, $id);
901         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
902
903         if (!is_null($logo)) {
904             $this->element('logo',null,$logo);
905         }
906
907         if (!is_null($suplink)) {
908             // For FriendFeed's SUP protocol
909             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
910                                          'href' => $suplink,
911                                          'type' => 'application/json'));
912         }
913
914         if (!is_null($selfuri)) {
915             $this->element('link', array('href' => $selfuri,
916                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
917         }
918
919         $this->element('updated', null, common_date_iso8601('now'));
920         $this->element('subtitle', null, $subtitle);
921
922         if (is_array($notice)) {
923             //FIXME: make everything calling showJsonTimeline use only Notice objects
924             $ids = array();
925             foreach ($notice as $n) {
926                 $ids[] = $n->getID();
927             }
928             $notice = Notice::multiGet('id', $ids);
929         }
930
931         while ($notice->fetch()) {
932             try {
933                 $this->raw($notice->asAtomEntry());
934             } catch (Exception $e) {
935                 common_log(LOG_ERR, $e->getMessage());
936                 continue;
937             }
938         }
939
940         $this->endDocument('atom');
941     }
942
943     function showRssGroups($group, $title, $link, $subtitle)
944     {
945         $this->initDocument('rss');
946
947         $this->element('title', null, $title);
948         $this->element('link', null, $link);
949         $this->element('description', null, $subtitle);
950         $this->element('language', null, 'en-us');
951         $this->element('ttl', null, '40');
952
953         if (is_array($group)) {
954             foreach ($group as $g) {
955                 $twitter_group = $this->twitterRssGroupArray($g);
956                 $this->showTwitterRssItem($twitter_group);
957             }
958         } else {
959             while ($group->fetch()) {
960                 $twitter_group = $this->twitterRssGroupArray($group);
961                 $this->showTwitterRssItem($twitter_group);
962             }
963         }
964
965         $this->endTwitterRss();
966     }
967
968     function showTwitterAtomEntry($entry)
969     {
970         $this->elementStart('entry');
971         $this->element('title', null, common_xml_safe_str($entry['title']));
972         $this->element(
973             'content',
974             array('type' => 'html'),
975             common_xml_safe_str($entry['content'])
976         );
977         $this->element('id', null, $entry['id']);
978         $this->element('published', null, $entry['published']);
979         $this->element('updated', null, $entry['updated']);
980         $this->element('link', array('type' => 'text/html',
981                                      'href' => $entry['link'],
982                                      'rel' => 'alternate'));
983         $this->element('link', array('type' => $entry['avatar-type'],
984                                      'href' => $entry['avatar'],
985                                      'rel' => 'image'));
986         $this->elementStart('author');
987
988         $this->element('name', null, $entry['author-name']);
989         $this->element('uri', null, $entry['author-uri']);
990
991         $this->elementEnd('author');
992         $this->elementEnd('entry');
993     }
994
995     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
996     {
997         $this->initDocument('atom');
998
999         $this->element('title', null, common_xml_safe_str($title));
1000         $this->element('id', null, $id);
1001         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1002
1003         if (!is_null($selfuri)) {
1004             $this->element('link', array('href' => $selfuri,
1005                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1006         }
1007
1008         $this->element('updated', null, common_date_iso8601('now'));
1009         $this->element('subtitle', null, common_xml_safe_str($subtitle));
1010
1011         if (is_array($group)) {
1012             foreach ($group as $g) {
1013                 $this->raw($g->asAtomEntry());
1014             }
1015         } else {
1016             while ($group->fetch()) {
1017                 $this->raw($group->asAtomEntry());
1018             }
1019         }
1020
1021         $this->endDocument('atom');
1022
1023     }
1024
1025     function showJsonTimeline($notice)
1026     {
1027         $this->initDocument('json');
1028
1029         $statuses = array();
1030
1031         if (is_array($notice)) {
1032             //FIXME: make everything calling showJsonTimeline use only Notice objects
1033             $ids = array();
1034             foreach ($notice as $n) {
1035                 $ids[] = $n->getID();
1036             }
1037             $notice = Notice::multiGet('id', $ids);
1038         }
1039
1040         while ($notice->fetch()) {
1041             try {
1042                 $twitter_status = $this->twitterStatusArray($notice);
1043                 array_push($statuses, $twitter_status);
1044             } catch (Exception $e) {
1045                 common_log(LOG_ERR, $e->getMessage());
1046                 continue;
1047             }
1048         }
1049
1050         $this->showJsonObjects($statuses);
1051
1052         $this->endDocument('json');
1053     }
1054
1055     function showJsonGroups($group)
1056     {
1057         $this->initDocument('json');
1058
1059         $groups = array();
1060
1061         if (is_array($group)) {
1062             foreach ($group as $g) {
1063                 $twitter_group = $this->twitterGroupArray($g);
1064                 array_push($groups, $twitter_group);
1065             }
1066         } else {
1067             while ($group->fetch()) {
1068                 $twitter_group = $this->twitterGroupArray($group);
1069                 array_push($groups, $twitter_group);
1070             }
1071         }
1072
1073         $this->showJsonObjects($groups);
1074
1075         $this->endDocument('json');
1076     }
1077
1078     function showXmlGroups($group)
1079     {
1080
1081         $this->initDocument('xml');
1082         $this->elementStart('groups', array('type' => 'array'));
1083
1084         if (is_array($group)) {
1085             foreach ($group as $g) {
1086                 $twitter_group = $this->twitterGroupArray($g);
1087                 $this->showTwitterXmlGroup($twitter_group);
1088             }
1089         } else {
1090             while ($group->fetch()) {
1091                 $twitter_group = $this->twitterGroupArray($group);
1092                 $this->showTwitterXmlGroup($twitter_group);
1093             }
1094         }
1095
1096         $this->elementEnd('groups');
1097         $this->endDocument('xml');
1098     }
1099
1100     function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1101     {
1102
1103         $this->initDocument('xml');
1104         $this->elementStart('lists_list');
1105         $this->elementStart('lists', array('type' => 'array'));
1106
1107         if (is_array($list)) {
1108             foreach ($list as $l) {
1109                 $twitter_list = $this->twitterListArray($l);
1110                 $this->showTwitterXmlList($twitter_list);
1111             }
1112         } else {
1113             while ($list->fetch()) {
1114                 $twitter_list = $this->twitterListArray($list);
1115                 $this->showTwitterXmlList($twitter_list);
1116             }
1117         }
1118
1119         $this->elementEnd('lists');
1120
1121         $this->element('next_cursor', null, $next_cursor);
1122         $this->element('previous_cursor', null, $prev_cursor);
1123
1124         $this->elementEnd('lists_list');
1125         $this->endDocument('xml');
1126     }
1127
1128     function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1129     {
1130         $this->initDocument('json');
1131
1132         $lists = array();
1133
1134         if (is_array($list)) {
1135             foreach ($list as $l) {
1136                 $twitter_list = $this->twitterListArray($l);
1137                 array_push($lists, $twitter_list);
1138             }
1139         } else {
1140             while ($list->fetch()) {
1141                 $twitter_list = $this->twitterListArray($list);
1142                 array_push($lists, $twitter_list);
1143             }
1144         }
1145
1146         $lists_list = array(
1147             'lists' => $lists,
1148             'next_cursor' => $next_cursor,
1149             'next_cursor_str' => strval($next_cursor),
1150             'previous_cursor' => $prev_cursor,
1151             'previous_cursor_str' => strval($prev_cursor)
1152         );
1153
1154         $this->showJsonObjects($lists_list);
1155
1156         $this->endDocument('json');
1157     }
1158
1159     function showTwitterXmlUsers($user)
1160     {
1161         $this->initDocument('xml');
1162         $this->elementStart('users', array('type' => 'array',
1163                                            'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1164
1165         if (is_array($user)) {
1166             foreach ($user as $u) {
1167                 $twitter_user = $this->twitterUserArray($u);
1168                 $this->showTwitterXmlUser($twitter_user);
1169             }
1170         } else {
1171             while ($user->fetch()) {
1172                 $twitter_user = $this->twitterUserArray($user);
1173                 $this->showTwitterXmlUser($twitter_user);
1174             }
1175         }
1176
1177         $this->elementEnd('users');
1178         $this->endDocument('xml');
1179     }
1180
1181     function showJsonUsers($user)
1182     {
1183         $this->initDocument('json');
1184
1185         $users = array();
1186
1187         if (is_array($user)) {
1188             foreach ($user as $u) {
1189                 $twitter_user = $this->twitterUserArray($u);
1190                 array_push($users, $twitter_user);
1191             }
1192         } else {
1193             while ($user->fetch()) {
1194                 $twitter_user = $this->twitterUserArray($user);
1195                 array_push($users, $twitter_user);
1196             }
1197         }
1198
1199         $this->showJsonObjects($users);
1200
1201         $this->endDocument('json');
1202     }
1203
1204     function showSingleJsonGroup($group)
1205     {
1206         $this->initDocument('json');
1207         $twitter_group = $this->twitterGroupArray($group);
1208         $this->showJsonObjects($twitter_group);
1209         $this->endDocument('json');
1210     }
1211
1212     function showSingleXmlGroup($group)
1213     {
1214         $this->initDocument('xml');
1215         $twitter_group = $this->twitterGroupArray($group);
1216         $this->showTwitterXmlGroup($twitter_group);
1217         $this->endDocument('xml');
1218     }
1219
1220     function showSingleJsonList($list)
1221     {
1222         $this->initDocument('json');
1223         $twitter_list = $this->twitterListArray($list);
1224         $this->showJsonObjects($twitter_list);
1225         $this->endDocument('json');
1226     }
1227
1228     function showSingleXmlList($list)
1229     {
1230         $this->initDocument('xml');
1231         $twitter_list = $this->twitterListArray($list);
1232         $this->showTwitterXmlList($twitter_list);
1233         $this->endDocument('xml');
1234     }
1235
1236     static function dateTwitter($dt)
1237     {
1238         $dateStr = date('d F Y H:i:s', strtotime($dt));
1239         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1240         $d->setTimezone(new DateTimeZone(common_timezone()));
1241         return $d->format('D M d H:i:s O Y');
1242     }
1243
1244     function initDocument($type='xml')
1245     {
1246         switch ($type) {
1247         case 'xml':
1248             header('Content-Type: application/xml; charset=utf-8');
1249             $this->startXML();
1250             break;
1251         case 'json':
1252             header('Content-Type: application/json; charset=utf-8');
1253
1254             // Check for JSONP callback
1255             if (isset($this->callback)) {
1256                 print $this->callback . '(';
1257             }
1258             break;
1259         case 'rss':
1260             header("Content-Type: application/rss+xml; charset=utf-8");
1261             $this->initTwitterRss();
1262             break;
1263         case 'atom':
1264             header('Content-Type: application/atom+xml; charset=utf-8');
1265             $this->initTwitterAtom();
1266             break;
1267         default:
1268             // TRANS: Client error on an API request with an unsupported data format.
1269             $this->clientError(_('Not a supported data format.'));
1270         }
1271
1272         return;
1273     }
1274
1275     function endDocument($type='xml')
1276     {
1277         switch ($type) {
1278         case 'xml':
1279             $this->endXML();
1280             break;
1281         case 'json':
1282             // Check for JSONP callback
1283             if (isset($this->callback)) {
1284                 print ')';
1285             }
1286             break;
1287         case 'rss':
1288             $this->endTwitterRss();
1289             break;
1290         case 'atom':
1291             $this->endTwitterRss();
1292             break;
1293         default:
1294             // TRANS: Client error on an API request with an unsupported data format.
1295             $this->clientError(_('Not a supported data format.'));
1296         }
1297         return;
1298     }
1299
1300     function initTwitterRss()
1301     {
1302         $this->startXML();
1303         $this->elementStart(
1304             'rss',
1305             array(
1306                 'version'      => '2.0',
1307                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1308                 'xmlns:georss' => 'http://www.georss.org/georss'
1309             )
1310         );
1311         $this->elementStart('channel');
1312         Event::handle('StartApiRss', array($this));
1313     }
1314
1315     function endTwitterRss()
1316     {
1317         $this->elementEnd('channel');
1318         $this->elementEnd('rss');
1319         $this->endXML();
1320     }
1321
1322     function initTwitterAtom()
1323     {
1324         $this->startXML();
1325         // FIXME: don't hardcode the language here!
1326         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1327                                           'xml:lang' => 'en-US',
1328                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1329     }
1330
1331     function endTwitterAtom()
1332     {
1333         $this->elementEnd('feed');
1334         $this->endXML();
1335     }
1336
1337     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1338     {
1339         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1340         switch ($content_type) {
1341         case 'xml':
1342             $this->showTwitterXmlUser($profile_array);
1343             break;
1344         case 'json':
1345             $this->showJsonObjects($profile_array);
1346             break;
1347         default:
1348             // TRANS: Client error on an API request with an unsupported data format.
1349             $this->clientError(_('Not a supported data format.'));
1350         }
1351         return;
1352     }
1353
1354     private static function is_decimal($str)
1355     {
1356         return preg_match('/^[0-9]+$/', $str);
1357     }
1358
1359     function getTargetUser($id)
1360     {
1361         if (empty($id)) {
1362             // Twitter supports these other ways of passing the user ID
1363             if (self::is_decimal($this->arg('id'))) {
1364                 return User::getKV($this->arg('id'));
1365             } else if ($this->arg('id')) {
1366                 $nickname = common_canonical_nickname($this->arg('id'));
1367                 return User::getKV('nickname', $nickname);
1368             } else if ($this->arg('user_id')) {
1369                 // This is to ensure that a non-numeric user_id still
1370                 // overrides screen_name even if it doesn't get used
1371                 if (self::is_decimal($this->arg('user_id'))) {
1372                     return User::getKV('id', $this->arg('user_id'));
1373                 }
1374             } else if ($this->arg('screen_name')) {
1375                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1376                 return User::getKV('nickname', $nickname);
1377             } else {
1378                 // Fall back to trying the currently authenticated user
1379                 return $this->scoped->getUser();
1380             }
1381
1382         } else if (self::is_decimal($id)) {
1383             return User::getKV($id);
1384         } else {
1385             $nickname = common_canonical_nickname($id);
1386             return User::getKV('nickname', $nickname);
1387         }
1388     }
1389
1390     function getTargetProfile($id)
1391     {
1392         if (empty($id)) {
1393
1394             // Twitter supports these other ways of passing the user ID
1395             if (self::is_decimal($this->arg('id'))) {
1396                 return Profile::getKV($this->arg('id'));
1397             } else if ($this->arg('id')) {
1398                 // Screen names currently can only uniquely identify a local user.
1399                 $nickname = common_canonical_nickname($this->arg('id'));
1400                 $user = User::getKV('nickname', $nickname);
1401                 return $user ? $user->getProfile() : null;
1402             } else if ($this->arg('user_id')) {
1403                 // This is to ensure that a non-numeric user_id still
1404                 // overrides screen_name even if it doesn't get used
1405                 if (self::is_decimal($this->arg('user_id'))) {
1406                     return Profile::getKV('id', $this->arg('user_id'));
1407                 }
1408             } elseif (mb_strlen($this->arg('screen_name')) > 0) {
1409                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1410                 $user = User::getByNickname($nickname);
1411                 return $user->getProfile();
1412             } else {
1413                 // Fall back to trying the currently authenticated user
1414                 return $this->scoped;
1415             }
1416         } else if (self::is_decimal($id) && intval($id) > 0) {
1417             return Profile::getByID($id);
1418         } else {
1419             // FIXME: check if isAcct to identify remote profiles and not just local nicknames
1420             $nickname = common_canonical_nickname($id);
1421             $user = User::getByNickname($nickname);
1422             return $user->getProfile();
1423         }
1424     }
1425
1426     function getTargetGroup($id)
1427     {
1428         if (empty($id)) {
1429             if (self::is_decimal($this->arg('id'))) {
1430                 return User_group::getKV('id', $this->arg('id'));
1431             } else if ($this->arg('id')) {
1432                 return User_group::getForNickname($this->arg('id'));
1433             } else if ($this->arg('group_id')) {
1434                 // This is to ensure that a non-numeric group_id still
1435                 // overrides group_name even if it doesn't get used
1436                 if (self::is_decimal($this->arg('group_id'))) {
1437                     return User_group::getKV('id', $this->arg('group_id'));
1438                 }
1439             } else if ($this->arg('group_name')) {
1440                 return User_group::getForNickname($this->arg('group_name'));
1441             }
1442
1443         } else if (self::is_decimal($id)) {
1444             return User_group::getKV('id', $id);
1445         } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1446             return User_group::getKV('uri', urldecode($this->arg('uri')));
1447         } else {
1448             return User_group::getForNickname($id);
1449         }
1450     }
1451
1452     function getTargetList($user=null, $id=null)
1453     {
1454         $tagger = $this->getTargetUser($user);
1455         $list = null;
1456
1457         if (empty($id)) {
1458             $id = $this->arg('id');
1459         }
1460
1461         if($id) {
1462             if (is_numeric($id)) {
1463                 $list = Profile_list::getKV('id', $id);
1464
1465                 // only if the list with the id belongs to the tagger
1466                 if(empty($list) || $list->tagger != $tagger->id) {
1467                     $list = null;
1468                 }
1469             }
1470             if (empty($list)) {
1471                 $tag = common_canonical_tag($id);
1472                 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1473             }
1474
1475             if (!empty($list) && $list->private) {
1476                 if ($this->scoped->id == $list->tagger) {
1477                     return $list;
1478                 }
1479             } else {
1480                 return $list;
1481             }
1482         }
1483         return null;
1484     }
1485
1486     /**
1487      * Returns query argument or default value if not found. Certain
1488      * parameters used throughout the API are lightly scrubbed and
1489      * bounds checked.  This overrides Action::arg().
1490      *
1491      * @param string $key requested argument
1492      * @param string $def default value to return if $key is not provided
1493      *
1494      * @return var $var
1495      */
1496     function arg($key, $def=null)
1497     {
1498         // XXX: Do even more input validation/scrubbing?
1499
1500         if (array_key_exists($key, $this->args)) {
1501             switch($key) {
1502             case 'page':
1503                 $page = (int)$this->args['page'];
1504                 return ($page < 1) ? 1 : $page;
1505             case 'count':
1506                 $count = (int)$this->args['count'];
1507                 if ($count < 1) {
1508                     return 20;
1509                 } elseif ($count > 200) {
1510                     return 200;
1511                 } else {
1512                     return $count;
1513                 }
1514             case 'since_id':
1515                 $since_id = (int)$this->args['since_id'];
1516                 return ($since_id < 1) ? 0 : $since_id;
1517             case 'max_id':
1518                 $max_id = (int)$this->args['max_id'];
1519                 return ($max_id < 1) ? 0 : $max_id;
1520             default:
1521                 return parent::arg($key, $def);
1522             }
1523         } else {
1524             return $def;
1525         }
1526     }
1527
1528     /**
1529      * Calculate the complete URI that called up this action.  Used for
1530      * Atom rel="self" links.  Warning: this is funky.
1531      *
1532      * @return string URL    a URL suitable for rel="self" Atom links
1533      */
1534     function getSelfUri()
1535     {
1536         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1537
1538         $id = $this->arg('id');
1539         $aargs = array('format' => $this->format);
1540         if (!empty($id)) {
1541             $aargs['id'] = $id;
1542         }
1543
1544         $user = $this->arg('user');
1545         if (!empty($user)) {
1546             $aargs['user'] = $user;
1547         }
1548
1549         $tag = $this->arg('tag');
1550         if (!empty($tag)) {
1551             $aargs['tag'] = $tag;
1552         }
1553
1554         parse_str($_SERVER['QUERY_STRING'], $params);
1555         $pstring = '';
1556         if (!empty($params)) {
1557             unset($params['p']);
1558             $pstring = http_build_query($params);
1559         }
1560
1561         $uri = common_local_url($action, $aargs);
1562
1563         if (!empty($pstring)) {
1564             $uri .= '?' . $pstring;
1565         }
1566
1567         return $uri;
1568     }
1569 }