]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/api.php
Merge branch '0.9.x' of git@gitorious.org:statusnet/mainline into 0.9.x
[quix0rs-gnu-social.git] / lib / api.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 StatusNet, Inc.
31  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
32  * @link      http://status.net/
33  */
34
35 if (!defined('STATUSNET')) {
36     exit(1);
37 }
38
39 /**
40  * Contains most of the Twitter-compatible API output functions.
41  *
42  * @category API
43  * @package  StatusNet
44  * @author   Craig Andrews <candrews@integralblue.com>
45  * @author   Dan Moore <dan@moore.cx>
46  * @author   Evan Prodromou <evan@status.net>
47  * @author   Jeffery To <jeffery.to@gmail.com>
48  * @author   Toby Inkster <mail@tobyinkster.co.uk>
49  * @author   Zach Copley <zach@status.net>
50  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
51  * @link     http://status.net/
52  */
53
54 class ApiAction extends Action
55 {
56      var $format   = null;
57      var $user     = null;
58      var $page     = null;
59      var $count    = null;
60      var $max_id   = null;
61      var $since_id = null;
62      var $since    = null;
63      
64     /**
65      * Initialization.
66      *
67      * @param array $args Web and URL arguments
68      *
69      * @return boolean false if user doesn't exist
70      */
71
72     function prepare($args)
73     {
74         parent::prepare($args);
75         
76         $this->format   = $this->arg('format');
77         $this->page     = (int)$this->arg('page', 1);
78         $this->count    = (int)$this->arg('count', 20);
79         $this->max_id   = (int)$this->arg('max_id', 0);
80         $this->since_id = (int)$this->arg('since_id', 0);
81         $this->since    = $this->arg('since');
82         
83         return true;
84     }
85
86     /**
87      * Handle a request
88      *
89      * @param array $args Arguments from $_REQUEST
90      *
91      * @return void
92      */
93
94     function handle($args)
95     {
96         parent::handle($args);
97     }
98
99     /**
100      * Overrides XMLOutputter::element to write booleans as strings (true|false).
101      * See that method's documentation for more info.
102      *
103      * @param string $tag     Element type or tagname
104      * @param array  $attrs   Array of element attributes, as
105      *                        key-value pairs
106      * @param string $content string content of the element
107      *
108      * @return void
109      */
110     function element($tag, $attrs=null, $content=null)
111     {
112         if (is_bool($content)) {
113             $content = ($content ? 'true' : 'false');
114         }
115
116         return parent::element($tag, $attrs, $content);
117     }
118
119     function twitterUserArray($profile, $get_notice=false)
120     {
121         $twitter_user = array();
122
123         $twitter_user['id'] = intval($profile->id);
124         $twitter_user['name'] = $profile->getBestName();
125         $twitter_user['screen_name'] = $profile->nickname;
126         $twitter_user['location'] = ($profile->location) ? $profile->location : null;
127         $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
128
129         $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
130         $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
131             Avatar::defaultImage(AVATAR_STREAM_SIZE);
132
133         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
134         $twitter_user['protected'] = false; # not supported by StatusNet yet
135         $twitter_user['followers_count'] = $profile->subscriberCount();
136
137         // To be supported soon...
138         $twitter_user['profile_background_color'] = '';
139         $twitter_user['profile_text_color'] = '';
140         $twitter_user['profile_link_color'] = '';
141         $twitter_user['profile_sidebar_fill_color'] = '';
142         $twitter_user['profile_sidebar_border_color'] = '';
143
144         $twitter_user['friends_count'] = $profile->subscriptionCount();
145
146         $twitter_user['created_at'] = $this->dateTwitter($profile->created);
147
148         $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
149
150         // Need to pull up the user for some of this
151         $user = User::staticGet($profile->id);
152
153         $timezone = 'UTC';
154
155         if ($user->timezone) {
156             $timezone = $user->timezone;
157         }
158
159         $t = new DateTime;
160         $t->setTimezone(new DateTimeZone($timezone));
161
162         $twitter_user['utc_offset'] = $t->format('Z');
163         $twitter_user['time_zone'] = $timezone;
164
165         // To be supported some day, perhaps
166         $twitter_user['profile_background_image_url'] = '';
167         $twitter_user['profile_background_tile'] = false;
168
169         $twitter_user['statuses_count'] = $profile->noticeCount();
170
171         // Is the requesting user following this user?
172         $twitter_user['following'] = false;
173         $twitter_user['notifications'] = false;
174
175         if (isset($apidata['user'])) {
176
177             $twitter_user['following'] = $apidata['user']->isSubscribed($profile);
178
179             // Notifications on?
180             $sub = Subscription::pkeyGet(array('subscriber' =>
181                 $apidata['user']->id, 'subscribed' => $profile->id));
182
183             if ($sub) {
184                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
185             }
186         }
187
188         if ($get_notice) {
189             $notice = $profile->getCurrentNotice();
190             if ($notice) {
191                 # don't get user!
192                 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
193             }
194         }
195
196         return $twitter_user;
197     }
198
199     function twitterStatusArray($notice, $include_user=true)
200     {
201         $profile = $notice->getProfile();
202
203         $twitter_status = array();
204         $twitter_status['text'] = $notice->content;
205         $twitter_status['truncated'] = false; # Not possible on StatusNet
206         $twitter_status['created_at'] = $this->dateTwitter($notice->created);
207         $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
208             intval($notice->reply_to) : null;
209         $twitter_status['source'] = $this->sourceLink($notice->source);
210         $twitter_status['id'] = intval($notice->id);
211
212         $replier_profile = null;
213
214         if ($notice->reply_to) {
215             $reply = Notice::staticGet(intval($notice->reply_to));
216             if ($reply) {
217                 $replier_profile = $reply->getProfile();
218             }
219         }
220
221         $twitter_status['in_reply_to_user_id'] =
222             ($replier_profile) ? intval($replier_profile->id) : null;
223         $twitter_status['in_reply_to_screen_name'] =
224             ($replier_profile) ? $replier_profile->nickname : null;
225
226         if (isset($this->auth_user)) {
227             $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
228         } else {
229             $twitter_status['favorited'] = false;
230         }
231
232         // Enclosures
233         $attachments = $notice->attachments();
234
235         if (!empty($attachments)) {
236
237             $twitter_status['attachments'] = array();
238
239             foreach ($attachments as $attachment) {
240                 if ($attachment->isEnclosure()) {
241                     $enclosure = array();
242                     $enclosure['url'] = $attachment->url;
243                     $enclosure['mimetype'] = $attachment->mimetype;
244                     $enclosure['size'] = $attachment->size;
245                     $twitter_status['attachments'][] = $enclosure;
246                 }
247             }
248         }
249
250         if ($include_user) {
251             # Don't get notice (recursive!)
252             $twitter_user = $this->twitterUserArray($profile, false);
253             $twitter_status['user'] = $twitter_user;
254         }
255
256         return $twitter_status;
257     }
258
259     function twitterGroupArray($group)
260     {
261         $twitter_group=array();
262         $twitter_group['id']=$group->id;
263         $twitter_group['url']=$group->permalink();
264         $twitter_group['nickname']=$group->nickname;
265         $twitter_group['fullname']=$group->fullname;
266         $twitter_group['homepage_url']=$group->homepage_url;
267         $twitter_group['original_logo']=$group->original_logo;
268         $twitter_group['homepage_logo']=$group->homepage_logo;
269         $twitter_group['stream_logo']=$group->stream_logo;
270         $twitter_group['mini_logo']=$group->mini_logo;
271         $twitter_group['homepage']=$group->homepage;
272         $twitter_group['description']=$group->description;
273         $twitter_group['location']=$group->location;
274         $twitter_group['created']=$this->dateTwitter($group->created);
275         $twitter_group['modified']=$this->dateTwitter($group->modified);
276         return $twitter_group;
277     }
278
279     function twitterRssGroupArray($group)
280     {
281         $entry = array();
282         $entry['content']=$group->description;
283         $entry['title']=$group->nickname;
284         $entry['link']=$group->permalink();
285         $entry['published']=common_date_iso8601($group->created);
286         $entry['updated']==common_date_iso8601($group->modified);
287         $taguribase = common_config('integration', 'groupuri');
288         $entry['id'] = "group:$groupuribase:$entry[link]";
289
290         $entry['description'] = $entry['content'];
291         $entry['pubDate'] = common_date_rfc2822($group->created);
292         $entry['guid'] = $entry['link'];
293
294         return $entry;
295     }
296
297     function twitterRssEntryArray($notice)
298     {
299         $profile = $notice->getProfile();
300         $entry = array();
301
302         // We trim() to avoid extraneous whitespace in the output
303
304         $entry['content'] = common_xml_safe_str(trim($notice->rendered));
305         $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
306         $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
307         $entry['published'] = common_date_iso8601($notice->created);
308
309         $taguribase = common_config('integration', 'taguri');
310         $entry['id'] = "tag:$taguribase:$entry[link]";
311
312         $entry['updated'] = $entry['published'];
313         $entry['author'] = $profile->getBestName();
314
315         // Enclosures
316         $attachments = $notice->attachments();
317         $enclosures = array();
318
319         foreach ($attachments as $attachment) {
320             $enclosure_o=$attachment->getEnclosure();
321             if ($enclosure_o) {
322                  $enclosure = array();
323                  $enclosure['url'] = $enclosure_o->url;
324                  $enclosure['mimetype'] = $enclosure_o->mimetype;
325                  $enclosure['size'] = $enclosure_o->size;
326                  $enclosures[] = $enclosure;
327             }
328         }
329
330         if (!empty($enclosures)) {
331             $entry['enclosures'] = $enclosures;
332         }
333
334         // Tags/Categories
335         $tag = new Notice_tag();
336         $tag->notice_id = $notice->id;
337         if ($tag->find()) {
338             $entry['tags']=array();
339             while ($tag->fetch()) {
340                 $entry['tags'][]=$tag->tag;
341             }
342         }
343         $tag->free();
344
345         // RSS Item specific
346         $entry['description'] = $entry['content'];
347         $entry['pubDate'] = common_date_rfc2822($notice->created);
348         $entry['guid'] = $entry['link'];
349
350         return $entry;
351     }
352
353
354     function twitterRelationshipArray($source, $target)
355     {
356         $relationship = array();
357
358         $relationship['source'] =
359             $this->relationshipDetailsArray($source, $target);
360         $relationship['target'] =
361             $this->relationshipDetailsArray($target, $source);
362
363         return array('relationship' => $relationship);
364     }
365
366     function relationshipDetailsArray($source, $target)
367     {
368         $details = array();
369
370         $details['screen_name'] = $source->nickname;
371         $details['followed_by'] = $target->isSubscribed($source);
372         $details['following'] = $source->isSubscribed($target);
373
374         $notifications = false;
375
376         if ($source->isSubscribed($target)) {
377
378             $sub = Subscription::pkeyGet(array('subscriber' =>
379                 $source->id, 'subscribed' => $target->id));
380
381             if (!empty($sub)) {
382                 $notifications = ($sub->jabber || $sub->sms);
383             }
384         }
385
386         $details['notifications_enabled'] = $notifications;
387         $details['blocking'] = $source->hasBlocked($target);
388         $details['id'] = $source->id;
389
390         return $details;
391     }
392
393     function showTwitterXmlRelationship($relationship)
394     {
395         $this->elementStart('relationship');
396
397         foreach($relationship as $element => $value) {
398             if ($element == 'source' || $element == 'target') {
399                 $this->elementStart($element);
400                 $this->showXmlRelationshipDetails($value);
401                 $this->elementEnd($element);
402             }
403         }
404
405         $this->elementEnd('relationship');
406     }
407
408     function showXmlRelationshipDetails($details)
409     {
410         foreach($details as $element => $value) {
411             $this->element($element, null, $value);
412         }
413     }
414
415     function showTwitterXmlStatus($twitter_status)
416     {
417         $this->elementStart('status');
418         foreach($twitter_status as $element => $value) {
419             switch ($element) {
420             case 'user':
421                 $this->showTwitterXmlUser($twitter_status['user']);
422                 break;
423             case 'text':
424                 $this->element($element, null, common_xml_safe_str($value));
425                 break;
426             case 'attachments':
427                 $this->showXmlAttachments($twitter_status['attachments']);
428                 break;
429             default:
430                 $this->element($element, null, $value);
431             }
432         }
433         $this->elementEnd('status');
434     }
435
436     function showTwitterXmlGroup($twitter_group)
437     {
438         $this->elementStart('group');
439         foreach($twitter_group as $element => $value) {
440             $this->element($element, null, $value);
441         }
442         $this->elementEnd('group');
443     }
444
445     function showTwitterXmlUser($twitter_user, $role='user')
446     {
447         $this->elementStart($role);
448         foreach($twitter_user as $element => $value) {
449             if ($element == 'status') {
450                 $this->showTwitterXmlStatus($twitter_user['status']);
451             } else {
452                 $this->element($element, null, $value);
453             }
454         }
455         $this->elementEnd($role);
456     }
457
458     function showXmlAttachments($attachments) {
459         if (!empty($attachments)) {
460             $this->elementStart('attachments', array('type' => 'array'));
461             foreach ($attachments as $attachment) {
462                 $attrs = array();
463                 $attrs['url'] = $attachment['url'];
464                 $attrs['mimetype'] = $attachment['mimetype'];
465                 $attrs['size'] = $attachment['size'];
466                 $this->element('enclosure', $attrs, '');
467             }
468             $this->elementEnd('attachments');
469         }
470     }
471
472     function showTwitterRssItem($entry)
473     {
474         $this->elementStart('item');
475         $this->element('title', null, $entry['title']);
476         $this->element('description', null, $entry['description']);
477         $this->element('pubDate', null, $entry['pubDate']);
478         $this->element('guid', null, $entry['guid']);
479         $this->element('link', null, $entry['link']);
480
481         # RSS only supports 1 enclosure per item
482         if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
483             $enclosure = $entry['enclosures'][0];
484             $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
485         }
486
487         if(array_key_exists('tags', $entry)){
488             foreach($entry['tags'] as $tag){
489                 $this->element('category', null,$tag);
490             }
491         }
492
493         $this->elementEnd('item');
494     }
495
496     function showJsonObjects($objects)
497     {
498         print(json_encode($objects));
499     }
500
501     function showSingleXmlStatus($notice)
502     {
503         $this->initDocument('xml');
504         $twitter_status = $this->twitterStatusArray($notice);
505         $this->showTwitterXmlStatus($twitter_status);
506         $this->endDocument('xml');
507     }
508
509     function show_single_json_status($notice)
510     {
511         $this->initDocument('json');
512         $status = $this->twitterStatusArray($notice);
513         $this->showJsonObjects($status);
514         $this->endDocument('json');
515     }
516
517
518     function showXmlTimeline($notice)
519     {
520
521         $this->initDocument('xml');
522         $this->elementStart('statuses', array('type' => 'array'));
523
524         if (is_array($notice)) {
525             foreach ($notice as $n) {
526                 $twitter_status = $this->twitterStatusArray($n);
527                 $this->showTwitterXmlStatus($twitter_status);
528             }
529         } else {
530             while ($notice->fetch()) {
531                 $twitter_status = $this->twitterStatusArray($notice);
532                 $this->showTwitterXmlStatus($twitter_status);
533             }
534         }
535
536         $this->elementEnd('statuses');
537         $this->endDocument('xml');
538     }
539
540     function showRssTimeline($notice, $title, $link, $subtitle, $suplink=null)
541     {
542
543         $this->initDocument('rss');
544
545         $this->element('title', null, $title);
546         $this->element('link', null, $link);
547         if (!is_null($suplink)) {
548             // For FriendFeed's SUP protocol
549             $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
550                                          'rel' => 'http://api.friendfeed.com/2008/03#sup',
551                                          'href' => $suplink,
552                                          'type' => 'application/json'));
553         }
554         $this->element('description', null, $subtitle);
555         $this->element('language', null, 'en-us');
556         $this->element('ttl', null, '40');
557
558         if (is_array($notice)) {
559             foreach ($notice as $n) {
560                 $entry = $this->twitterRssEntryArray($n);
561                 $this->showTwitterRssItem($entry);
562             }
563         } else {
564             while ($notice->fetch()) {
565                 $entry = $this->twitterRssEntryArray($notice);
566                 $this->showTwitterRssItem($entry);
567             }
568         }
569
570         $this->endTwitterRss();
571     }
572
573     function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null)
574     {
575
576         $this->initDocument('atom');
577
578         $this->element('title', null, $title);
579         $this->element('id', null, $id);
580         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
581
582         if (!is_null($suplink)) {
583             # For FriendFeed's SUP protocol
584             $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
585                                          'href' => $suplink,
586                                          'type' => 'application/json'));
587         }
588
589         if (!is_null($selfuri)) {
590             $this->element('link', array('href' => $selfuri,
591                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
592         }
593
594         $this->element('updated', null, common_date_iso8601('now'));
595         $this->element('subtitle', null, $subtitle);
596
597         if (is_array($notice)) {
598             foreach ($notice as $n) {
599                 $this->raw($n->asAtomEntry());
600             }
601         } else {
602             while ($notice->fetch()) {
603                 $this->raw($notice->asAtomEntry());
604             }
605         }
606
607         $this->endDocument('atom');
608
609     }
610
611     function showRssGroups($group, $title, $link, $subtitle)
612     {
613
614         $this->initDocument('rss');
615
616         $this->element('title', null, $title);
617         $this->element('link', null, $link);
618         $this->element('description', null, $subtitle);
619         $this->element('language', null, 'en-us');
620         $this->element('ttl', null, '40');
621
622         if (is_array($group)) {
623             foreach ($group as $g) {
624                 $twitter_group = $this->twitterRssGroupArray($g);
625                 $this->showTwitterRssItem($twitter_group);
626             }
627         } else {
628             while ($group->fetch()) {
629                 $twitter_group = $this->twitterRssGroupArray($group);
630                 $this->showTwitterRssItem($twitter_group);
631             }
632         }
633
634         $this->endTwitterRss();
635     }
636
637
638     function showTwitterAtomEntry($entry)
639     {
640         $this->elementStart('entry');
641         $this->element('title', null, $entry['title']);
642         $this->element('content', array('type' => 'html'), $entry['content']);
643         $this->element('id', null, $entry['id']);
644         $this->element('published', null, $entry['published']);
645         $this->element('updated', null, $entry['updated']);
646         $this->element('link', array('type' => 'text/html',
647                                      'href' => $entry['link'],
648                                      'rel' => 'alternate'));
649         $this->element('link', array('type' => $entry['avatar-type'],
650                                      'href' => $entry['avatar'],
651                                      'rel' => 'image'));
652         $this->elementStart('author');
653
654         $this->element('name', null, $entry['author-name']);
655         $this->element('uri', null, $entry['author-uri']);
656
657         $this->elementEnd('author');
658         $this->elementEnd('entry');
659     }
660
661     function showXmlDirectMessage($dm)
662     {
663         $this->elementStart('direct_message');
664         foreach($dm as $element => $value) {
665             switch ($element) {
666             case 'sender':
667             case 'recipient':
668                 $this->showTwitterXmlUser($value, $element);
669                 break;
670             case 'text':
671                 $this->element($element, null, common_xml_safe_str($value));
672                 break;
673             default:
674                 $this->element($element, null, $value);
675                 break;
676             }
677         }
678         $this->elementEnd('direct_message');
679     }
680
681     function directMessageArray($message)
682     {
683         $dmsg = array();
684
685         $from_profile = $message->getFrom();
686         $to_profile = $message->getTo();
687
688         $dmsg['id'] = $message->id;
689         $dmsg['sender_id'] = $message->from_profile;
690         $dmsg['text'] = trim($message->content);
691         $dmsg['recipient_id'] = $message->to_profile;
692         $dmsg['created_at'] = $this->dateTwitter($message->created);
693         $dmsg['sender_screen_name'] = $from_profile->nickname;
694         $dmsg['recipient_screen_name'] = $to_profile->nickname;
695         $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
696         $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
697
698         return $dmsg;
699     }
700
701     function rssDirectMessageArray($message)
702     {
703         $entry = array();
704
705         $from = $message->getFrom();
706
707         $entry['title'] = sprintf('Message from %s to %s',
708             $from->nickname, $message->getTo()->nickname);
709
710         $entry['content'] = common_xml_safe_str($message->rendered);
711         $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
712         $entry['published'] = common_date_iso8601($message->created);
713
714         $taguribase = common_config('integration', 'taguri');
715
716         $entry['id'] = "tag:$taguribase:$entry[link]";
717         $entry['updated'] = $entry['published'];
718
719         $entry['author-name'] = $from->getBestName();
720         $entry['author-uri'] = $from->homepage;
721
722         $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);
723
724         $entry['avatar']      = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
725         $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';
726
727         // RSS item specific
728
729         $entry['description'] = $entry['content'];
730         $entry['pubDate'] = common_date_rfc2822($message->created);
731         $entry['guid'] = $entry['link'];
732
733         return $entry;
734     }
735
736     function showSingleXmlDirectMessage($message)
737     {
738         $this->initDocument('xml');
739         $dmsg = $this->directMessageArray($message);
740         $this->showXmlDirectMessage($dmsg);
741         $this->endDocument('xml');
742     }
743
744     function showSingleJsonDirectMessage($message)
745     {
746         $this->initDocument('json');
747         $dmsg = $this->directMessageArray($message);
748         $this->showJsonObjects($dmsg);
749         $this->endDocument('json');
750     }
751
752     function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
753     {
754
755         $this->initDocument('atom');
756
757         $this->element('title', null, $title);
758         $this->element('id', null, $id);
759         $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
760
761         if (!is_null($selfuri)) {
762             $this->element('link', array('href' => $selfuri,
763                 'rel' => 'self', 'type' => 'application/atom+xml'), null);
764         }
765
766         $this->element('updated', null, common_date_iso8601('now'));
767         $this->element('subtitle', null, $subtitle);
768
769         if (is_array($group)) {
770             foreach ($group as $g) {
771                 $this->raw($g->asAtomEntry());
772             }
773         } else {
774             while ($group->fetch()) {
775                 $this->raw($group->asAtomEntry());
776             }
777         }
778
779         $this->endDocument('atom');
780
781     }
782
783     function showJsonTimeline($notice)
784     {
785
786         $this->initDocument('json');
787
788         $statuses = array();
789
790         if (is_array($notice)) {
791             foreach ($notice as $n) {
792                 $twitter_status = $this->twitterStatusArray($n);
793                 array_push($statuses, $twitter_status);
794             }
795         } else {
796             while ($notice->fetch()) {
797                 $twitter_status = $this->twitterStatusArray($notice);
798                 array_push($statuses, $twitter_status);
799             }
800         }
801
802         $this->showJsonObjects($statuses);
803
804         $this->endDocument('json');
805     }
806
807     function showJsonGroups($group)
808     {
809
810         $this->initDocument('json');
811
812         $groups = array();
813
814         if (is_array($group)) {
815             foreach ($group as $g) {
816                 $twitter_group = $this->twitterGroupArray($g);
817                 array_push($groups, $twitter_group);
818             }
819         } else {
820             while ($group->fetch()) {
821                 $twitter_group = $this->twitterGroupArray($group);
822                 array_push($groups, $twitter_group);
823             }
824         }
825
826         $this->showJsonObjects($groups);
827
828         $this->endDocument('json');
829     }
830
831     function showXmlGroups($group)
832     {
833
834         $this->initDocument('xml');
835         $this->elementStart('groups', array('type' => 'array'));
836
837         if (is_array($group)) {
838             foreach ($group as $g) {
839                 $twitter_group = $this->twitterGroupArray($g);
840                 $this->showTwitterXmlGroup($twitter_group);
841             }
842         } else {
843             while ($group->fetch()) {
844                 $twitter_group = $this->twitterGroupArray($group);
845                 $this->showTwitterXmlGroup($twitter_group);
846             }
847         }
848
849         $this->elementEnd('groups');
850         $this->endDocument('xml');
851     }
852
853     function showTwitterXmlUsers($user)
854     {
855
856         $this->initDocument('xml');
857         $this->elementStart('users', array('type' => 'array'));
858
859         if (is_array($user)) {
860             foreach ($user as $u) {
861                 $twitter_user = $this->twitterUserArray($u);
862                 $this->showTwitterXmlUser($twitter_user);
863             }
864         } else {
865             while ($user->fetch()) {
866                 $twitter_user = $this->twitterUserArray($user);
867                 $this->showTwitterXmlUser($twitter_user);
868             }
869         }
870
871         $this->elementEnd('users');
872         $this->endDocument('xml');
873     }
874
875     function showJsonUsers($user)
876     {
877
878         $this->initDocument('json');
879
880         $users = array();
881
882         if (is_array($user)) {
883             foreach ($user as $u) {
884                 $twitter_user = $this->twitterUserArray($u);
885                 array_push($users, $twitter_user);
886             }
887         } else {
888             while ($user->fetch()) {
889                 $twitter_user = $this->twitterUserArray($user);
890                 array_push($users, $twitter_user);
891             }
892         }
893
894         $this->showJsonObjects($users);
895
896         $this->endDocument('json');
897     }
898
899     function showSingleJsonGroup($group)
900     {
901         $this->initDocument('json');
902         $twitter_group = $this->twitterGroupArray($group);
903         $this->showJsonObjects($twitter_group);
904         $this->endDocument('json');
905     }
906
907     function showSingleXmlGroup($group)
908     {
909         $this->initDocument('xml');
910         $twitter_group = $this->twitterGroupArray($group);
911         $this->showTwitterXmlGroup($twitter_group);
912         $this->endDocument('xml');
913     }
914
915     function dateTwitter($dt)
916     {
917         $dateStr = date('d F Y H:i:s', strtotime($dt));
918         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
919         $d->setTimezone(new DateTimeZone(common_timezone()));
920         return $d->format('D M d H:i:s O Y');
921     }
922
923     function initDocument($type='xml')
924     {
925         switch ($type) {
926         case 'xml':
927             header('Content-Type: application/xml; charset=utf-8');
928             $this->startXML();
929             break;
930         case 'json':
931             header('Content-Type: application/json; charset=utf-8');
932
933             // Check for JSONP callback
934             $callback = $this->arg('callback');
935             if ($callback) {
936                 print $callback . '(';
937             }
938             break;
939         case 'rss':
940             header("Content-Type: application/rss+xml; charset=utf-8");
941             $this->initTwitterRss();
942             break;
943         case 'atom':
944             header('Content-Type: application/atom+xml; charset=utf-8');
945             $this->initTwitterAtom();
946             break;
947         default:
948             $this->clientError(_('Not a supported data format.'));
949             break;
950         }
951
952         return;
953     }
954
955     function endDocument($type='xml')
956     {
957         switch ($type) {
958         case 'xml':
959             $this->endXML();
960             break;
961         case 'json':
962
963             // Check for JSONP callback
964             $callback = $this->arg('callback');
965             if ($callback) {
966                 print ')';
967             }
968             break;
969         case 'rss':
970             $this->endTwitterRss();
971             break;
972         case 'atom':
973             $this->endTwitterRss();
974             break;
975         default:
976             $this->clientError(_('Not a supported data format.'));
977             break;
978         }
979         return;
980     }
981
982     function clientError($msg, $code = 400, $format = 'xml')
983     {
984         $action = $this->trimmed('action');
985
986         common_debug("User error '$code' on '$action': $msg", __FILE__);
987
988         if (!array_key_exists($code, ClientErrorAction::$status)) {
989             $code = 400;
990         }
991
992         $status_string = ClientErrorAction::$status[$code];
993
994         header('HTTP/1.1 '.$code.' '.$status_string);
995
996         if ($format == 'xml') {
997             $this->initDocument('xml');
998             $this->elementStart('hash');
999             $this->element('error', null, $msg);
1000             $this->element('request', null, $_SERVER['REQUEST_URI']);
1001             $this->elementEnd('hash');
1002             $this->endDocument('xml');
1003         } elseif ($format == 'json'){
1004             $this->initDocument('json');
1005             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1006             print(json_encode($error_array));
1007             $this->endDocument('json');
1008         } else {
1009
1010             // If user didn't request a useful format, throw a regular client error
1011             throw new ClientException($msg, $code);
1012         }
1013     }
1014
1015     function serverError($msg, $code = 500, $content_type = 'json')
1016     {
1017         $action = $this->trimmed('action');
1018
1019         common_debug("Server error '$code' on '$action': $msg", __FILE__);
1020
1021         if (!array_key_exists($code, ServerErrorAction::$status)) {
1022             $code = 400;
1023         }
1024
1025         $status_string = ServerErrorAction::$status[$code];
1026
1027         header('HTTP/1.1 '.$code.' '.$status_string);
1028
1029         if ($content_type == 'xml') {
1030             $this->initDocument('xml');
1031             $this->elementStart('hash');
1032             $this->element('error', null, $msg);
1033             $this->element('request', null, $_SERVER['REQUEST_URI']);
1034             $this->elementEnd('hash');
1035             $this->endDocument('xml');
1036         } else {
1037             $this->initDocument('json');
1038             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
1039             print(json_encode($error_array));
1040             $this->endDocument('json');
1041         }
1042     }
1043
1044     function initTwitterRss()
1045     {
1046         $this->startXML();
1047         $this->elementStart('rss', array('version' => '2.0', 'xmlns:atom'=>'http://www.w3.org/2005/Atom'));
1048         $this->elementStart('channel');
1049         Event::handle('StartApiRss', array($this));
1050     }
1051
1052     function endTwitterRss()
1053     {
1054         $this->elementEnd('channel');
1055         $this->elementEnd('rss');
1056         $this->endXML();
1057     }
1058
1059     function initTwitterAtom()
1060     {
1061         $this->startXML();
1062         // FIXME: don't hardcode the language here!
1063         $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1064                                           'xml:lang' => 'en-US',
1065                                           'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1066         Event::handle('StartApiAtom', array($this));
1067     }
1068
1069     function endTwitterAtom()
1070     {
1071         $this->elementEnd('feed');
1072         $this->endXML();
1073     }
1074
1075     function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1076     {
1077         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1078         switch ($content_type) {
1079         case 'xml':
1080             $this->showTwitterXmlUser($profile_array);
1081             break;
1082         case 'json':
1083             $this->showJsonObjects($profile_array);
1084             break;
1085         default:
1086             $this->clientError(_('Not a supported data format.'));
1087             return;
1088         }
1089         return;
1090     }
1091
1092     function getTargetUser($id)
1093     {
1094         if (empty($id)) {
1095
1096             // Twitter supports these other ways of passing the user ID
1097             if (is_numeric($this->arg('id'))) {
1098                 return User::staticGet($this->arg('id'));
1099             } else if ($this->arg('id')) {
1100                 $nickname = common_canonical_nickname($this->arg('id'));
1101                 return User::staticGet('nickname', $nickname);
1102             } else if ($this->arg('user_id')) {
1103                 // This is to ensure that a non-numeric user_id still
1104                 // overrides screen_name even if it doesn't get used
1105                 if (is_numeric($this->arg('user_id'))) {
1106                     return User::staticGet('id', $this->arg('user_id'));
1107                 }
1108             } else if ($this->arg('screen_name')) {
1109                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1110                 return User::staticGet('nickname', $nickname);
1111             } else {
1112                 // Fall back to trying the currently authenticated user
1113                 return $this->auth_user;
1114             }
1115
1116         } else if (is_numeric($id)) {
1117             return User::staticGet($id);
1118         } else {
1119             $nickname = common_canonical_nickname($id);
1120             return User::staticGet('nickname', $nickname);
1121         }
1122     }
1123
1124     function getTargetGroup($id)
1125     {
1126         if (empty($id)) {
1127             if (is_numeric($this->arg('id'))) {
1128                 return User_group::staticGet($this->arg('id'));
1129             } else if ($this->arg('id')) {
1130                 $nickname = common_canonical_nickname($this->arg('id'));
1131                 return User_group::staticGet('nickname', $nickname);
1132             } else if ($this->arg('group_id')) {
1133                 // This is to ensure that a non-numeric user_id still
1134                 // overrides screen_name even if it doesn't get used
1135                 if (is_numeric($this->arg('group_id'))) {
1136                     return User_group::staticGet('id', $this->arg('group_id'));
1137                 }
1138             } else if ($this->arg('group_name')) {
1139                 $nickname = common_canonical_nickname($this->arg('group_name'));
1140                 return User_group::staticGet('nickname', $nickname);
1141             }
1142
1143         } else if (is_numeric($id)) {
1144             return User_group::staticGet($id);
1145         } else {
1146             $nickname = common_canonical_nickname($id);
1147             return User_group::staticGet('nickname', $nickname);
1148         }
1149     }
1150
1151     function sourceLink($source)
1152     {
1153         $source_name = _($source);
1154         switch ($source) {
1155         case 'web':
1156         case 'xmpp':
1157         case 'mail':
1158         case 'omb':
1159         case 'api':
1160             break;
1161         default:
1162             $ns = Notice_source::staticGet($source);
1163             if ($ns) {
1164                 $source_name = '<a href="' . $ns->url . '">' . $ns->name . '</a>';
1165             }
1166             break;
1167         }
1168         return $source_name;
1169     }
1170
1171     /**
1172      * Returns query argument or default value if not found. Certain
1173      * parameters used throughout the API are lightly scrubbed and
1174      * bounds checked.  This overrides Action::arg().
1175      *
1176      * @param string $key requested argument
1177      * @param string $def default value to return if $key is not provided
1178      *
1179      * @return var $var
1180      */
1181     function arg($key, $def=null)
1182     {
1183
1184         // XXX: Do even more input validation/scrubbing?
1185
1186         if (array_key_exists($key, $this->args)) {
1187             switch($key) {
1188             case 'page':
1189                 $page = (int)$this->args['page'];
1190                 return ($page < 1) ? 1 : $page;
1191             case 'count':
1192                 $count = (int)$this->args['count'];
1193                 if ($count < 1) {
1194                     return 20;
1195                 } elseif ($count > 200) {
1196                     return 200;
1197                 } else {
1198                     return $count;
1199                 }
1200             case 'since_id':
1201                 $since_id = (int)$this->args['since_id'];
1202                 return ($since_id < 1) ? 0 : $since_id;
1203             case 'max_id':
1204                 $max_id = (int)$this->args['max_id'];
1205                 return ($max_id < 1) ? 0 : $max_id;
1206             case 'since':
1207                 return strtotime($this->args['since']);
1208             default:
1209                 return parent::arg($key, $def);
1210             }
1211         } else {
1212             return $def;
1213         }
1214     }
1215
1216 }