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