]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
Simplify NoticeSimpleStatusArray inclusion in lib/apiaction.php
[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
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 $max_id    = null;
127     var $since_id  = null;
128     var $source    = null;
129     var $callback  = null;
130
131     var $access    = self::READ_ONLY;  // read (default) or read-write
132
133     static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
134
135     /**
136      * Initialization.
137      *
138      * @param array $args Web and URL arguments
139      *
140      * @return boolean false if user doesn't exist
141      */
142     protected function prepare(array $args=array())
143     {
144         StatusNet::setApi(true); // reduce exception reports to aid in debugging
145         parent::prepare($args);
146
147         $this->format   = $this->arg('format');
148         $this->callback = $this->arg('callback');
149         $this->page     = (int)$this->arg('page', 1);
150         $this->count    = (int)$this->arg('count', 20);
151         $this->max_id   = (int)$this->arg('max_id', 0);
152         $this->since_id = (int)$this->arg('since_id', 0);
153
154         if ($this->arg('since')) {
155             header('X-StatusNet-Warning: since parameter is disabled; use since_id');
156         }
157
158         $this->source = $this->trimmed('source');
159
160         if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
161             $this->source = 'api';
162         }
163
164         return true;
165     }
166
167     /**
168      * Handle a request
169      *
170      * @param array $args Arguments from $_REQUEST
171      *
172      * @return void
173      */
174     protected function handle()
175     {
176         header('Access-Control-Allow-Origin: *');
177         parent::handle();
178     }
179
180     /**
181      * Overrides XMLOutputter::element to write booleans as strings (true|false).
182      * See that method's documentation for more info.
183      *
184      * @param string $tag     Element type or tagname
185      * @param array  $attrs   Array of element attributes, as
186      *                        key-value pairs
187      * @param string $content string content of the element
188      *
189      * @return void
190      */
191     function element($tag, $attrs=null, $content=null)
192     {
193         if (is_bool($content)) {
194             $content = ($content ? 'true' : 'false');
195         }
196
197         return parent::element($tag, $attrs, $content);
198     }
199
200     function twitterUserArray($profile, $get_notice=false)
201     {
202         $twitter_user = array();
203
204         try {
205             $user = $profile->getUser();
206         } catch (NoSuchUserException $e) {
207             $user = null;
208         }
209
210         $twitter_user['id'] = intval($profile->id);
211         $twitter_user['name'] = $profile->getBestName();
212         $twitter_user['screen_name'] = $profile->nickname;
213         $twitter_user['location'] = ($profile->location) ? $profile->location : null;
214         $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
215
216         // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
217         $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
218         $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
219
220         // START introduced by qvitter API, not necessary for StatusNet API
221         $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
222         try {
223             $avatar  = Avatar::getUploaded($profile);
224             $origurl = $avatar->displayUrl();
225         } catch (Exception $e) {
226             $origurl = $twitter_user['profile_image_url_profile_size'];
227         }
228         $twitter_user['profile_image_url_original'] = $origurl;
229
230         $twitter_user['groups_count'] = $profile->getGroupCount();
231         foreach (array('linkcolor', 'backgroundcolor') as $key) {
232             $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
233         }
234         // END introduced by qvitter API, not necessary for StatusNet API
235
236         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
237         $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
238         $twitter_user['followers_count'] = $profile->subscriberCount();
239
240         // Note: some profiles don't have an associated user
241
242         $twitter_user['friends_count'] = $profile->subscriptionCount();
243
244         $twitter_user['created_at'] = $this->dateTwitter($profile->created);
245
246         $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
247
248         $timezone = 'UTC';
249
250         if (!empty($user) && $user->timezone) {
251             $timezone = $user->timezone;
252         }
253
254         $t = new DateTime;
255         $t->setTimezone(new DateTimeZone($timezone));
256
257         $twitter_user['utc_offset'] = $t->format('Z');
258         $twitter_user['time_zone'] = $timezone;
259         $twitter_user['statuses_count'] = $profile->noticeCount();
260
261         // Is the requesting user following this user?
262         $twitter_user['following'] = false;
263         $twitter_user['statusnet_blocking'] = false;
264         $twitter_user['notifications'] = false;
265
266         if (isset($this->auth_user)) {
267
268             $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
269             $twitter_user['statusnet_blocking']  = $this->auth_user->hasBlocked($profile);
270
271             // Notifications on?
272             $sub = Subscription::pkeyGet(array('subscriber' =>
273                                                $this->auth_user->id,
274                                                'subscribed' => $profile->id));
275
276             if ($sub) {
277                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
278             }
279         }
280
281         if ($get_notice) {
282             $notice = $profile->getCurrentNotice();
283             if ($notice instanceof Notice) {
284                 // don't get user!
285                 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
286             }
287         }
288
289         // StatusNet-specific
290
291         $twitter_user['statusnet_profile_url'] = $profile->profileurl;
292
293         return $twitter_user;
294     }
295
296     function twitterStatusArray($notice, $include_user=true)
297     {
298         $base = $this->twitterSimpleStatusArray($notice, $include_user);
299
300         if (!empty($notice->repeat_of)) {
301             $original = Notice::getKV('id', $notice->repeat_of);
302             if ($original instanceof Notice) {
303                 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
304                 $base['retweeted_status'] = $orig_array;
305             }
306         }
307
308         return $base;
309     }
310
311     function twitterSimpleStatusArray($notice, $include_user=true)
312     {
313         $profile = $notice->getProfile();
314
315         $twitter_status = array();
316         $twitter_status['text'] = $notice->content;
317         $twitter_status['truncated'] = false; # Not possible on StatusNet
318         $twitter_status['created_at'] = $this->dateTwitter($notice->created);
319         try {
320             // We could just do $notice->reply_to but maybe the future holds a
321             // different story for parenting.
322             $parent = $notice->getParent();
323             $in_reply_to = $parent->id;
324         } catch (Exception $e) {
325             $in_reply_to = null;
326         }
327         $twitter_status['in_reply_to_status_id'] = $in_reply_to;
328
329         $source = null;
330
331         $ns = $notice->getSource();
332         if ($ns) {
333             if (!empty($ns->name) && !empty($ns->url)) {
334                 $source = '<a href="'
335                     . htmlspecialchars($ns->url)
336                     . '" rel="nofollow">'
337                     . htmlspecialchars($ns->name)
338                     . '</a>';
339             } else {
340                 $source = $ns->code;
341             }
342         }
343
344         $twitter_status['uri'] = $notice->getUri();
345         $twitter_status['source'] = $source;
346         $twitter_status['id'] = intval($notice->id);
347
348         $replier_profile = null;
349
350         if ($notice->reply_to) {
351             $reply = Notice::getKV(intval($notice->reply_to));
352             if ($reply) {
353                 $replier_profile = $reply->getProfile();
354             }
355         }
356
357         $twitter_status['in_reply_to_user_id'] =
358             ($replier_profile) ? intval($replier_profile->id) : null;
359         $twitter_status['in_reply_to_screen_name'] =
360             ($replier_profile) ? $replier_profile->nickname : null;
361
362         if (isset($notice->lat) && isset($notice->lon)) {
363             // This is the format that GeoJSON expects stuff to be in
364             $twitter_status['geo'] = array('type' => 'Point',
365                                            'coordinates' => array((float) $notice->lat,
366                                                                   (float) $notice->lon));
367         } else {
368             $twitter_status['geo'] = null;
369         }
370
371         if (!is_null($this->scoped)) {
372             $twitter_status['repeated']  = $this->scoped->hasRepeated($notice);
373         } else {
374             $twitter_status['repeated'] = false;
375         }
376
377         // Enclosures
378         $attachments = $notice->attachments();
379
380         if (!empty($attachments)) {
381
382             $twitter_status['attachments'] = array();
383
384             foreach ($attachments as $attachment) {
385                 try {
386                     $enclosure_o = $attachment->getEnclosure();
387                     $enclosure = array();
388                     $enclosure['url'] = $enclosure_o->url;
389                     $enclosure['mimetype'] = $enclosure_o->mimetype;
390                     $enclosure['size'] = $enclosure_o->size;
391                     $twitter_status['attachments'][] = $enclosure;
392                 } catch (ServerException $e) {
393                     // There was not enough metadata available
394                 }
395             }
396         }
397
398         if ($include_user && $profile) {
399             // Don't get notice (recursive!)
400             $twitter_user = $this->twitterUserArray($profile, false);
401             $twitter_status['user'] = $twitter_user;
402         }
403
404         // StatusNet-specific
405
406         $twitter_status['statusnet_html'] = $notice->rendered;
407         $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
408
409         // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
410         Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
411                                                        array('include_user'=>$include_user)));
412
413         return $twitter_status;
414     }
415
416     function twitterGroupArray($group)
417     {
418         $twitter_group = array();
419
420         $twitter_group['id'] = intval($group->id);
421         $twitter_group['url'] = $group->permalink();
422         $twitter_group['nickname'] = $group->nickname;
423         $twitter_group['fullname'] = $group->fullname;
424
425         if (isset($this->auth_user)) {
426             $twitter_group['member'] = $this->auth_user->isMember($group);
427             $twitter_group['blocked'] = Group_block::isBlocked(
428                 $group,
429                 $this->auth_user->getProfile()
430             );
431         }
432
433         $twitter_group['admin_count'] = $group->getAdminCount();
434         $twitter_group['member_count'] = $group->getMemberCount();
435         $twitter_group['original_logo'] = $group->original_logo;
436         $twitter_group['homepage_logo'] = $group->homepage_logo;
437         $twitter_group['stream_logo'] = $group->stream_logo;
438         $twitter_group['mini_logo'] = $group->mini_logo;
439         $twitter_group['homepage'] = $group->homepage;
440         $twitter_group['description'] = $group->description;
441         $twitter_group['location'] = $group->location;
442         $twitter_group['created'] = $this->dateTwitter($group->created);
443         $twitter_group['modified'] = $this->dateTwitter($group->modified);
444
445         return $twitter_group;
446     }
447
448     function twitterRssGroupArray($group)
449     {
450         $entry = array();
451         $entry['content']=$group->description;
452         $entry['title']=$group->nickname;
453         $entry['link']=$group->permalink();
454         $entry['published']=common_date_iso8601($group->created);
455         $entry['updated']==common_date_iso8601($group->modified);
456         $taguribase = common_config('integration', 'groupuri');
457         $entry['id'] = "group:$groupuribase:$entry[link]";
458
459         $entry['description'] = $entry['content'];
460         $entry['pubDate'] = common_date_rfc2822($group->created);
461         $entry['guid'] = $entry['link'];
462
463         return $entry;
464     }
465
466     function twitterListArray($list)
467     {
468         $profile = Profile::getKV('id', $list->tagger);
469
470         $twitter_list = array();
471         $twitter_list['id'] = $list->id;
472         $twitter_list['name'] = $list->tag;
473         $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
474         $twitter_list['slug'] = $list->tag;
475         $twitter_list['description'] = $list->description;
476         $twitter_list['subscriber_count'] = $list->subscriberCount();
477         $twitter_list['member_count'] = $list->taggedCount();
478         $twitter_list['uri'] = $list->getUri();
479
480         if (isset($this->auth_user)) {
481             $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
482         } else {
483             $twitter_list['following'] = false;
484         }
485
486         $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
487         $twitter_list['user'] = $this->twitterUserArray($profile, false);
488
489         return $twitter_list;
490     }
491
492     function twitterRssEntryArray($notice)
493     {
494         $entry = array();
495
496         if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
497             $profile = $notice->getProfile();
498
499             // We trim() to avoid extraneous whitespace in the output
500
501             $entry['content'] = common_xml_safe_str(trim($notice->rendered));
502             $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
503             $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
504             $entry['published'] = common_date_iso8601($notice->created);
505
506             $taguribase = TagURI::base();
507             $entry['id'] = "tag:$taguribase:$entry[link]";
508
509             $entry['updated'] = $entry['published'];
510             $entry['author'] = $profile->getBestName();
511
512             // Enclosures
513             $attachments = $notice->attachments();
514             $enclosures = array();
515
516             foreach ($attachments as $attachment) {
517                 try {
518                     $enclosure_o = $attachment->getEnclosure();
519                     $enclosure = array();
520                     $enclosure['url'] = $enclosure_o->url;
521                     $enclosure['mimetype'] = $enclosure_o->mimetype;
522                     $enclosure['size'] = $enclosure_o->size;
523                     $enclosures[] = $enclosure;
524                 } catch (ServerException $e) {
525                     // There was not enough metadata available
526                 }
527             }
528
529             if (!empty($enclosures)) {
530                 $entry['enclosures'] = $enclosures;
531             }
532
533             // Tags/Categories
534             $tag = new Notice_tag();
535             $tag->notice_id = $notice->id;
536             if ($tag->find()) {
537                 $entry['tags']=array();
538                 while ($tag->fetch()) {
539                     $entry['tags'][]=$tag->tag;
540                 }
541             }
542             $tag->free();
543
544             // RSS Item specific
545             $entry['description'] = $entry['content'];
546             $entry['pubDate'] = common_date_rfc2822($notice->created);
547             $entry['guid'] = $entry['link'];
548
549             if (isset($notice->lat) && isset($notice->lon)) {
550                 // This is the format that GeoJSON expects stuff to be in.
551                 // showGeoRSS() below uses it for XML output, so we reuse it
552                 $entry['geo'] = array('type' => 'Point',
553                                       'coordinates' => array((float) $notice->lat,
554                                                              (float) $notice->lon));
555             } else {
556                 $entry['geo'] = null;
557             }
558
559             Event::handle('EndRssEntryArray', array($notice, &$entry));
560         }
561
562         return $entry;
563     }
564
565     function twitterRelationshipArray($source, $target)
566     {
567         $relationship = array();
568
569         $relationship['source'] =
570             $this->relationshipDetailsArray($source, $target);
571         $relationship['target'] =
572             $this->relationshipDetailsArray($target, $source);
573
574         return array('relationship' => $relationship);
575     }
576
577     function relationshipDetailsArray($source, $target)
578     {
579         $details = array();
580
581         $details['screen_name'] = $source->nickname;
582         $details['followed_by'] = $target->isSubscribed($source);
583         $details['following'] = $source->isSubscribed($target);
584
585         $notifications = false;
586
587         if ($source->isSubscribed($target)) {
588             $sub = Subscription::pkeyGet(array('subscriber' =>
589                 $source->id, 'subscribed' => $target->id));
590
591             if (!empty($sub)) {
592                 $notifications = ($sub->jabber || $sub->sms);
593             }
594         }
595
596         $details['notifications_enabled'] = $notifications;
597         $details['blocking'] = $source->hasBlocked($target);
598         $details['id'] = intval($source->id);
599
600         return $details;
601     }
602
603     function showTwitterXmlRelationship($relationship)
604     {
605         $this->elementStart('relationship');
606
607         foreach($relationship as $element => $value) {
608             if ($element == 'source' || $element == 'target') {
609                 $this->elementStart($element);
610                 $this->showXmlRelationshipDetails($value);
611                 $this->elementEnd($element);
612             }
613         }
614
615         $this->elementEnd('relationship');
616     }
617
618     function showXmlRelationshipDetails($details)
619     {
620         foreach($details as $element => $value) {
621             $this->element($element, null, $value);
622         }
623     }
624
625     function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
626     {
627         $attrs = array();
628         if ($namespaces) {
629             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
630         }
631         $this->elementStart($tag, $attrs);
632         foreach($twitter_status as $element => $value) {
633             switch ($element) {
634             case 'user':
635                 $this->showTwitterXmlUser($twitter_status['user']);
636                 break;
637             case 'text':
638                 $this->element($element, null, common_xml_safe_str($value));
639                 break;
640             case 'attachments':
641                 $this->showXmlAttachments($twitter_status['attachments']);
642                 break;
643             case 'geo':
644                 $this->showGeoXML($value);
645                 break;
646             case 'retweeted_status':
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->auth_user);
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             $notice = new ArrayWrapper($notice);
799         }
800
801         while ($notice->fetch()) {
802             try {
803                 $twitter_status = $this->twitterStatusArray($notice);
804                 $this->showTwitterXmlStatus($twitter_status);
805             } catch (Exception $e) {
806                 common_log(LOG_ERR, $e->getMessage());
807                 continue;
808             }
809         }
810
811         $this->elementEnd('statuses');
812         $this->endDocument('xml');
813     }
814
815     function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
816     {
817         $this->initDocument('rss');
818
819         $this->element('title', null, $title);
820         $this->element('link', null, $link);
821
822         if (!is_null($self)) {
823             $this->element(
824                 'atom:link',
825                 array(
826                     'type' => 'application/rss+xml',
827                     'href' => $self,
828                     'rel'  => 'self'
829                 )
830            );
831         }
832
833         if (!is_null($suplink)) {
834             // For FriendFeed's SUP protocol
835             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
836                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
837                                          'href' => $suplink,
838                                          'type' => 'application/json'));
839         }
840
841         if (!is_null($logo)) {
842             $this->elementStart('image');
843             $this->element('link', null, $link);
844             $this->element('title', null, $title);
845             $this->element('url', null, $logo);
846             $this->elementEnd('image');
847         }
848
849         $this->element('description', null, $subtitle);
850         $this->element('language', null, 'en-us');
851         $this->element('ttl', null, '40');
852
853         if (is_array($notice)) {
854             $notice = new ArrayWrapper($notice);
855         }
856
857         while ($notice->fetch()) {
858             try {
859                 $entry = $this->twitterRssEntryArray($notice);
860                 $this->showTwitterRssItem($entry);
861             } catch (Exception $e) {
862                 common_log(LOG_ERR, $e->getMessage());
863                 // continue on exceptions
864             }
865         }
866
867         $this->endTwitterRss();
868     }
869
870     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
871     {
872         $this->initDocument('atom');
873
874         $this->element('title', null, $title);
875         $this->element('id', null, $id);
876         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
877
878         if (!is_null($logo)) {
879             $this->element('logo',null,$logo);
880         }
881
882         if (!is_null($suplink)) {
883             // For FriendFeed's SUP protocol
884             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
885                                          'href' => $suplink,
886                                          'type' => 'application/json'));
887         }
888
889         if (!is_null($selfuri)) {
890             $this->element('link', array('href' => $selfuri,
891                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
892         }
893
894         $this->element('updated', null, common_date_iso8601('now'));
895         $this->element('subtitle', null, $subtitle);
896
897         if (is_array($notice)) {
898             $notice = new ArrayWrapper($notice);
899         }
900
901         while ($notice->fetch()) {
902             try {
903                 $this->raw($notice->asAtomEntry());
904             } catch (Exception $e) {
905                 common_log(LOG_ERR, $e->getMessage());
906                 continue;
907             }
908         }
909
910         $this->endDocument('atom');
911     }
912
913     function showRssGroups($group, $title, $link, $subtitle)
914     {
915         $this->initDocument('rss');
916
917         $this->element('title', null, $title);
918         $this->element('link', null, $link);
919         $this->element('description', null, $subtitle);
920         $this->element('language', null, 'en-us');
921         $this->element('ttl', null, '40');
922
923         if (is_array($group)) {
924             foreach ($group as $g) {
925                 $twitter_group = $this->twitterRssGroupArray($g);
926                 $this->showTwitterRssItem($twitter_group);
927             }
928         } else {
929             while ($group->fetch()) {
930                 $twitter_group = $this->twitterRssGroupArray($group);
931                 $this->showTwitterRssItem($twitter_group);
932             }
933         }
934
935         $this->endTwitterRss();
936     }
937
938     function showTwitterAtomEntry($entry)
939     {
940         $this->elementStart('entry');
941         $this->element('title', null, common_xml_safe_str($entry['title']));
942         $this->element(
943             'content',
944             array('type' => 'html'),
945             common_xml_safe_str($entry['content'])
946         );
947         $this->element('id', null, $entry['id']);
948         $this->element('published', null, $entry['published']);
949         $this->element('updated', null, $entry['updated']);
950         $this->element('link', array('type' => 'text/html',
951                                      'href' => $entry['link'],
952                                      'rel' => 'alternate'));
953         $this->element('link', array('type' => $entry['avatar-type'],
954                                      'href' => $entry['avatar'],
955                                      'rel' => 'image'));
956         $this->elementStart('author');
957
958         $this->element('name', null, $entry['author-name']);
959         $this->element('uri', null, $entry['author-uri']);
960
961         $this->elementEnd('author');
962         $this->elementEnd('entry');
963     }
964
965     function showXmlDirectMessage($dm, $namespaces=false)
966     {
967         $attrs = array();
968         if ($namespaces) {
969             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
970         }
971         $this->elementStart('direct_message', $attrs);
972         foreach($dm as $element => $value) {
973             switch ($element) {
974             case 'sender':
975             case 'recipient':
976                 $this->showTwitterXmlUser($value, $element);
977                 break;
978             case 'text':
979                 $this->element($element, null, common_xml_safe_str($value));
980                 break;
981             default:
982                 $this->element($element, null, $value);
983                 break;
984             }
985         }
986         $this->elementEnd('direct_message');
987     }
988
989     function directMessageArray($message)
990     {
991         $dmsg = array();
992
993         $from_profile = $message->getFrom();
994         $to_profile = $message->getTo();
995
996         $dmsg['id'] = intval($message->id);
997         $dmsg['sender_id'] = intval($from_profile->id);
998         $dmsg['text'] = trim($message->content);
999         $dmsg['recipient_id'] = intval($to_profile->id);
1000         $dmsg['created_at'] = $this->dateTwitter($message->created);
1001         $dmsg['sender_screen_name'] = $from_profile->nickname;
1002         $dmsg['recipient_screen_name'] = $to_profile->nickname;
1003         $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
1004         $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
1005
1006         return $dmsg;
1007     }
1008
1009     function rssDirectMessageArray($message)
1010     {
1011         $entry = array();
1012
1013         $from = $message->getFrom();
1014
1015         $entry['title'] = sprintf('Message from %1$s to %2$s',
1016             $from->nickname, $message->getTo()->nickname);
1017
1018         $entry['content'] = common_xml_safe_str($message->rendered);
1019         $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
1020         $entry['published'] = common_date_iso8601($message->created);
1021
1022         $taguribase = TagURI::base();
1023
1024         $entry['id'] = "tag:$taguribase:$entry[link]";
1025         $entry['updated'] = $entry['published'];
1026
1027         $entry['author-name'] = $from->getBestName();
1028         $entry['author-uri'] = $from->homepage;
1029
1030         $entry['avatar'] = $from->avatarUrl(AVATAR_STREAM_SIZE);
1031         try {
1032             $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
1033             $entry['avatar-type'] = $avatar->mediatype;
1034         } catch (Exception $e) {
1035             $entry['avatar-type'] = 'image/png';
1036         }
1037
1038         // RSS item specific
1039
1040         $entry['description'] = $entry['content'];
1041         $entry['pubDate'] = common_date_rfc2822($message->created);
1042         $entry['guid'] = $entry['link'];
1043
1044         return $entry;
1045     }
1046
1047     function showSingleXmlDirectMessage($message)
1048     {
1049         $this->initDocument('xml');
1050         $dmsg = $this->directMessageArray($message);
1051         $this->showXmlDirectMessage($dmsg, true);
1052         $this->endDocument('xml');
1053     }
1054
1055     function showSingleJsonDirectMessage($message)
1056     {
1057         $this->initDocument('json');
1058         $dmsg = $this->directMessageArray($message);
1059         $this->showJsonObjects($dmsg);
1060         $this->endDocument('json');
1061     }
1062
1063     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
1064     {
1065         $this->initDocument('atom');
1066
1067         $this->element('title', null, common_xml_safe_str($title));
1068         $this->element('id', null, $id);
1069         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1070
1071         if (!is_null($selfuri)) {
1072             $this->element('link', array('href' => $selfuri,
1073                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1074         }
1075
1076         $this->element('updated', null, common_date_iso8601('now'));
1077         $this->element('subtitle', null, common_xml_safe_str($subtitle));
1078
1079         if (is_array($group)) {
1080             foreach ($group as $g) {
1081                 $this->raw($g->asAtomEntry());
1082             }
1083         } else {
1084             while ($group->fetch()) {
1085                 $this->raw($group->asAtomEntry());
1086             }
1087         }
1088
1089         $this->endDocument('atom');
1090
1091     }
1092
1093     function showJsonTimeline($notice)
1094     {
1095         $this->initDocument('json');
1096
1097         $statuses = array();
1098
1099         if (is_array($notice)) {
1100             $notice = new ArrayWrapper($notice);
1101         }
1102
1103         while ($notice->fetch()) {
1104             try {
1105                 $twitter_status = $this->twitterStatusArray($notice);
1106                 array_push($statuses, $twitter_status);
1107             } catch (Exception $e) {
1108                 common_log(LOG_ERR, $e->getMessage());
1109                 continue;
1110             }
1111         }
1112
1113         $this->showJsonObjects($statuses);
1114
1115         $this->endDocument('json');
1116     }
1117
1118     function showJsonGroups($group)
1119     {
1120         $this->initDocument('json');
1121
1122         $groups = array();
1123
1124         if (is_array($group)) {
1125             foreach ($group as $g) {
1126                 $twitter_group = $this->twitterGroupArray($g);
1127                 array_push($groups, $twitter_group);
1128             }
1129         } else {
1130             while ($group->fetch()) {
1131                 $twitter_group = $this->twitterGroupArray($group);
1132                 array_push($groups, $twitter_group);
1133             }
1134         }
1135
1136         $this->showJsonObjects($groups);
1137
1138         $this->endDocument('json');
1139     }
1140
1141     function showXmlGroups($group)
1142     {
1143
1144         $this->initDocument('xml');
1145         $this->elementStart('groups', array('type' => 'array'));
1146
1147         if (is_array($group)) {
1148             foreach ($group as $g) {
1149                 $twitter_group = $this->twitterGroupArray($g);
1150                 $this->showTwitterXmlGroup($twitter_group);
1151             }
1152         } else {
1153             while ($group->fetch()) {
1154                 $twitter_group = $this->twitterGroupArray($group);
1155                 $this->showTwitterXmlGroup($twitter_group);
1156             }
1157         }
1158
1159         $this->elementEnd('groups');
1160         $this->endDocument('xml');
1161     }
1162
1163     function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1164     {
1165
1166         $this->initDocument('xml');
1167         $this->elementStart('lists_list');
1168         $this->elementStart('lists', array('type' => 'array'));
1169
1170         if (is_array($list)) {
1171             foreach ($list as $l) {
1172                 $twitter_list = $this->twitterListArray($l);
1173                 $this->showTwitterXmlList($twitter_list);
1174             }
1175         } else {
1176             while ($list->fetch()) {
1177                 $twitter_list = $this->twitterListArray($list);
1178                 $this->showTwitterXmlList($twitter_list);
1179             }
1180         }
1181
1182         $this->elementEnd('lists');
1183
1184         $this->element('next_cursor', null, $next_cursor);
1185         $this->element('previous_cursor', null, $prev_cursor);
1186
1187         $this->elementEnd('lists_list');
1188         $this->endDocument('xml');
1189     }
1190
1191     function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1192     {
1193         $this->initDocument('json');
1194
1195         $lists = array();
1196
1197         if (is_array($list)) {
1198             foreach ($list as $l) {
1199                 $twitter_list = $this->twitterListArray($l);
1200                 array_push($lists, $twitter_list);
1201             }
1202         } else {
1203             while ($list->fetch()) {
1204                 $twitter_list = $this->twitterListArray($list);
1205                 array_push($lists, $twitter_list);
1206             }
1207         }
1208
1209         $lists_list = array(
1210             'lists' => $lists,
1211             'next_cursor' => $next_cursor,
1212             'next_cursor_str' => strval($next_cursor),
1213             'previous_cursor' => $prev_cursor,
1214             'previous_cursor_str' => strval($prev_cursor)
1215         );
1216
1217         $this->showJsonObjects($lists_list);
1218
1219         $this->endDocument('json');
1220     }
1221
1222     function showTwitterXmlUsers($user)
1223     {
1224         $this->initDocument('xml');
1225         $this->elementStart('users', array('type' => 'array',
1226                                            'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1227
1228         if (is_array($user)) {
1229             foreach ($user as $u) {
1230                 $twitter_user = $this->twitterUserArray($u);
1231                 $this->showTwitterXmlUser($twitter_user);
1232             }
1233         } else {
1234             while ($user->fetch()) {
1235                 $twitter_user = $this->twitterUserArray($user);
1236                 $this->showTwitterXmlUser($twitter_user);
1237             }
1238         }
1239
1240         $this->elementEnd('users');
1241         $this->endDocument('xml');
1242     }
1243
1244     function showJsonUsers($user)
1245     {
1246         $this->initDocument('json');
1247
1248         $users = array();
1249
1250         if (is_array($user)) {
1251             foreach ($user as $u) {
1252                 $twitter_user = $this->twitterUserArray($u);
1253                 array_push($users, $twitter_user);
1254             }
1255         } else {
1256             while ($user->fetch()) {
1257                 $twitter_user = $this->twitterUserArray($user);
1258                 array_push($users, $twitter_user);
1259             }
1260         }
1261
1262         $this->showJsonObjects($users);
1263
1264         $this->endDocument('json');
1265     }
1266
1267     function showSingleJsonGroup($group)
1268     {
1269         $this->initDocument('json');
1270         $twitter_group = $this->twitterGroupArray($group);
1271         $this->showJsonObjects($twitter_group);
1272         $this->endDocument('json');
1273     }
1274
1275     function showSingleXmlGroup($group)
1276     {
1277         $this->initDocument('xml');
1278         $twitter_group = $this->twitterGroupArray($group);
1279         $this->showTwitterXmlGroup($twitter_group);
1280         $this->endDocument('xml');
1281     }
1282
1283     function showSingleJsonList($list)
1284     {
1285         $this->initDocument('json');
1286         $twitter_list = $this->twitterListArray($list);
1287         $this->showJsonObjects($twitter_list);
1288         $this->endDocument('json');
1289     }
1290
1291     function showSingleXmlList($list)
1292     {
1293         $this->initDocument('xml');
1294         $twitter_list = $this->twitterListArray($list);
1295         $this->showTwitterXmlList($twitter_list);
1296         $this->endDocument('xml');
1297     }
1298
1299     function dateTwitter($dt)
1300     {
1301         $dateStr = date('d F Y H:i:s', strtotime($dt));
1302         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1303         $d->setTimezone(new DateTimeZone(common_timezone()));
1304         return $d->format('D M d H:i:s O Y');
1305     }
1306
1307     function initDocument($type='xml')
1308     {
1309         switch ($type) {
1310         case 'xml':
1311             header('Content-Type: application/xml; charset=utf-8');
1312             $this->startXML();
1313             break;
1314         case 'json':
1315             header('Content-Type: application/json; charset=utf-8');
1316
1317             // Check for JSONP callback
1318             if (isset($this->callback)) {
1319                 print $this->callback . '(';
1320             }
1321             break;
1322         case 'rss':
1323             header("Content-Type: application/rss+xml; charset=utf-8");
1324             $this->initTwitterRss();
1325             break;
1326         case 'atom':
1327             header('Content-Type: application/atom+xml; charset=utf-8');
1328             $this->initTwitterAtom();
1329             break;
1330         default:
1331             // TRANS: Client error on an API request with an unsupported data format.
1332             $this->clientError(_('Not a supported data format.'));
1333         }
1334
1335         return;
1336     }
1337
1338     function endDocument($type='xml')
1339     {
1340         switch ($type) {
1341         case 'xml':
1342             $this->endXML();
1343             break;
1344         case 'json':
1345             // Check for JSONP callback
1346             if (isset($this->callback)) {
1347                 print ')';
1348             }
1349             break;
1350         case 'rss':
1351             $this->endTwitterRss();
1352             break;
1353         case 'atom':
1354             $this->endTwitterRss();
1355             break;
1356         default:
1357             // TRANS: Client error on an API request with an unsupported data format.
1358             $this->clientError(_('Not a supported data format.'));
1359         }
1360         return;
1361     }
1362
1363     function initTwitterRss()
1364     {
1365         $this->startXML();
1366         $this->elementStart(
1367             'rss',
1368             array(
1369                 'version'      => '2.0',
1370                 'xmlns:atom'   => 'http://www.w3.org/2005/Atom',
1371                 'xmlns:georss' => 'http://www.georss.org/georss'
1372             )
1373         );
1374         $this->elementStart('channel');
1375         Event::handle('StartApiRss', array($this));
1376     }
1377
1378     function endTwitterRss()
1379     {
1380         $this->elementEnd('channel');
1381         $this->elementEnd('rss');
1382         $this->endXML();
1383     }
1384
1385     function initTwitterAtom()
1386     {
1387         $this->startXML();
1388         // FIXME: don't hardcode the language here!
1389         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1390                                           'xml:lang' => 'en-US',
1391                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1392     }
1393
1394     function endTwitterAtom()
1395     {
1396         $this->elementEnd('feed');
1397         $this->endXML();
1398     }
1399
1400     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1401     {
1402         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1403         switch ($content_type) {
1404         case 'xml':
1405             $this->showTwitterXmlUser($profile_array);
1406             break;
1407         case 'json':
1408             $this->showJsonObjects($profile_array);
1409             break;
1410         default:
1411             // TRANS: Client error on an API request with an unsupported data format.
1412             $this->clientError(_('Not a supported data format.'));
1413         }
1414         return;
1415     }
1416
1417     private static function is_decimal($str)
1418     {
1419         return preg_match('/^[0-9]+$/', $str);
1420     }
1421
1422     function getTargetUser($id)
1423     {
1424         if (empty($id)) {
1425             // Twitter supports these other ways of passing the user ID
1426             if (self::is_decimal($this->arg('id'))) {
1427                 return User::getKV($this->arg('id'));
1428             } else if ($this->arg('id')) {
1429                 $nickname = common_canonical_nickname($this->arg('id'));
1430                 return User::getKV('nickname', $nickname);
1431             } else if ($this->arg('user_id')) {
1432                 // This is to ensure that a non-numeric user_id still
1433                 // overrides screen_name even if it doesn't get used
1434                 if (self::is_decimal($this->arg('user_id'))) {
1435                     return User::getKV('id', $this->arg('user_id'));
1436                 }
1437             } else if ($this->arg('screen_name')) {
1438                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1439                 return User::getKV('nickname', $nickname);
1440             } else {
1441                 // Fall back to trying the currently authenticated user
1442                 return $this->auth_user;
1443             }
1444
1445         } else if (self::is_decimal($id)) {
1446             return User::getKV($id);
1447         } else {
1448             $nickname = common_canonical_nickname($id);
1449             return User::getKV('nickname', $nickname);
1450         }
1451     }
1452
1453     function getTargetProfile($id)
1454     {
1455         if (empty($id)) {
1456
1457             // Twitter supports these other ways of passing the user ID
1458             if (self::is_decimal($this->arg('id'))) {
1459                 return Profile::getKV($this->arg('id'));
1460             } else if ($this->arg('id')) {
1461                 // Screen names currently can only uniquely identify a local user.
1462                 $nickname = common_canonical_nickname($this->arg('id'));
1463                 $user = User::getKV('nickname', $nickname);
1464                 return $user ? $user->getProfile() : null;
1465             } else if ($this->arg('user_id')) {
1466                 // This is to ensure that a non-numeric user_id still
1467                 // overrides screen_name even if it doesn't get used
1468                 if (self::is_decimal($this->arg('user_id'))) {
1469                     return Profile::getKV('id', $this->arg('user_id'));
1470                 }
1471             } else if ($this->arg('screen_name')) {
1472                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1473                 $user = User::getKV('nickname', $nickname);
1474                 return $user instanceof User ? $user->getProfile() : null;
1475             } else {
1476                 // Fall back to trying the currently authenticated user
1477                 return $this->scoped;
1478             }
1479         } else if (self::is_decimal($id)) {
1480             return Profile::getKV($id);
1481         } else {
1482             $nickname = common_canonical_nickname($id);
1483             $user = User::getKV('nickname', $nickname);
1484             return $user ? $user->getProfile() : null;
1485         }
1486     }
1487
1488     function getTargetGroup($id)
1489     {
1490         if (empty($id)) {
1491             if (self::is_decimal($this->arg('id'))) {
1492                 return User_group::getKV('id', $this->arg('id'));
1493             } else if ($this->arg('id')) {
1494                 return User_group::getForNickname($this->arg('id'));
1495             } else if ($this->arg('group_id')) {
1496                 // This is to ensure that a non-numeric group_id still
1497                 // overrides group_name even if it doesn't get used
1498                 if (self::is_decimal($this->arg('group_id'))) {
1499                     return User_group::getKV('id', $this->arg('group_id'));
1500                 }
1501             } else if ($this->arg('group_name')) {
1502                 return User_group::getForNickname($this->arg('group_name'));
1503             }
1504
1505         } else if (self::is_decimal($id)) {
1506             return User_group::getKV('id', $id);
1507         } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1508             return User_group::getKV('uri', urldecode($this->arg('uri')));
1509         } else {
1510             return User_group::getForNickname($id);
1511         }
1512     }
1513
1514     function getTargetList($user=null, $id=null)
1515     {
1516         $tagger = $this->getTargetUser($user);
1517         $list = null;
1518
1519         if (empty($id)) {
1520             $id = $this->arg('id');
1521         }
1522
1523         if($id) {
1524             if (is_numeric($id)) {
1525                 $list = Profile_list::getKV('id', $id);
1526
1527                 // only if the list with the id belongs to the tagger
1528                 if(empty($list) || $list->tagger != $tagger->id) {
1529                     $list = null;
1530                 }
1531             }
1532             if (empty($list)) {
1533                 $tag = common_canonical_tag($id);
1534                 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1535             }
1536
1537             if (!empty($list) && $list->private) {
1538                 if ($this->auth_user->id == $list->tagger) {
1539                     return $list;
1540                 }
1541             } else {
1542                 return $list;
1543             }
1544         }
1545         return null;
1546     }
1547
1548     /**
1549      * Returns query argument or default value if not found. Certain
1550      * parameters used throughout the API are lightly scrubbed and
1551      * bounds checked.  This overrides Action::arg().
1552      *
1553      * @param string $key requested argument
1554      * @param string $def default value to return if $key is not provided
1555      *
1556      * @return var $var
1557      */
1558     function arg($key, $def=null)
1559     {
1560         // XXX: Do even more input validation/scrubbing?
1561
1562         if (array_key_exists($key, $this->args)) {
1563             switch($key) {
1564             case 'page':
1565                 $page = (int)$this->args['page'];
1566                 return ($page < 1) ? 1 : $page;
1567             case 'count':
1568                 $count = (int)$this->args['count'];
1569                 if ($count < 1) {
1570                     return 20;
1571                 } elseif ($count > 200) {
1572                     return 200;
1573                 } else {
1574                     return $count;
1575                 }
1576             case 'since_id':
1577                 $since_id = (int)$this->args['since_id'];
1578                 return ($since_id < 1) ? 0 : $since_id;
1579             case 'max_id':
1580                 $max_id = (int)$this->args['max_id'];
1581                 return ($max_id < 1) ? 0 : $max_id;
1582             default:
1583                 return parent::arg($key, $def);
1584             }
1585         } else {
1586             return $def;
1587         }
1588     }
1589
1590     /**
1591      * Calculate the complete URI that called up this action.  Used for
1592      * Atom rel="self" links.  Warning: this is funky.
1593      *
1594      * @return string URL    a URL suitable for rel="self" Atom links
1595      */
1596     function getSelfUri()
1597     {
1598         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1599
1600         $id = $this->arg('id');
1601         $aargs = array('format' => $this->format);
1602         if (!empty($id)) {
1603             $aargs['id'] = $id;
1604         }
1605
1606         $tag = $this->arg('tag');
1607         if (!empty($tag)) {
1608             $aargs['tag'] = $tag;
1609         }
1610
1611         parse_str($_SERVER['QUERY_STRING'], $params);
1612         $pstring = '';
1613         if (!empty($params)) {
1614             unset($params['p']);
1615             $pstring = http_build_query($params);
1616         }
1617
1618         $uri = common_local_url($action, $aargs);
1619
1620         if (!empty($pstring)) {
1621             $uri .= '?' . $pstring;
1622         }
1623
1624         return $uri;
1625     }
1626 }