3 * StatusNet, the distributed open-source microblogging tool
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.
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.
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/>.
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/
36 /* External API usage documentation. Please update when you change how the API works. */
38 /*! @mainpage StatusNet REST API
42 Some explanatory text about the API would be nice.
46 @subsection timelinesmethods_sec Timeline Methods
48 @li @ref publictimeline
49 @li @ref friendstimeline
51 @subsection statusmethods_sec Status Methods
53 @li @ref statusesupdate
55 @subsection usermethods_sec User Methods
57 @subsection directmessagemethods_sec Direct Message Methods (now a plugin)
59 @subsection friendshipmethods_sec Friendship Methods
61 @subsection socialgraphmethods_sec Social Graph Methods
63 @subsection accountmethods_sec Account Methods
65 @subsection favoritesmethods_sec Favorites Methods
67 @subsection blockmethods_sec Block Methods
69 @subsection oauthmethods_sec OAuth Methods
71 @subsection helpmethods_sec Help Methods
73 @subsection groupmethods_sec Group Methods
75 @page apiroot API Root
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:
83 $config['site']['server'] = 'example.org';
84 $config['site']['path'] = 'statusnet'
87 The pattern for a site's API root is: @c protocol://server/path/api E.g:
89 @c http://example.org/statusnet/api
91 The @b path can be empty. In that case the API root would simply be:
93 @c http://example.org/api
97 if (!defined('STATUSNET')) {
101 class ApiValidationException extends Exception { }
104 * Contains most of the Twitter-compatible API output functions.
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/
117 class ApiAction extends Action
120 const READ_WRITE = 2;
123 var $auth_user = null;
129 var $since_id = null;
131 var $callback = null;
134 var $access = self::READ_ONLY; // read (default) or read-write
136 static $reserved_sources = array('web', 'omb', 'ostatus', 'mail', 'xmpp', 'api');
141 * @param array $args Web and URL arguments
143 * @return boolean false if user doesn't exist
145 protected function prepare(array $args=array())
147 StatusNet::setApi(true); // reduce exception reports to aid in debugging
148 parent::prepare($args);
150 $this->format = $this->arg('format');
151 $this->callback = $this->arg('callback');
152 $this->page = (int)$this->arg('page', 1);
153 $this->count = (int)$this->arg('count', 20);
154 $this->max_id = (int)$this->arg('max_id', 0);
155 $this->since_id = (int)$this->arg('since_id', 0);
157 // These two are not used everywhere, mainly just AtompubAction extensions
158 $this->offset = ($this->page-1) * $this->count;
159 $this->limit = $this->count + 1;
161 if ($this->arg('since')) {
162 header('X-StatusNet-Warning: since parameter is disabled; use since_id');
165 $this->source = $this->trimmed('source');
167 if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
168 $this->source = 'api';
177 * @param array $args Arguments from $_REQUEST
181 protected function handle()
183 header('Access-Control-Allow-Origin: *');
188 * Overrides XMLOutputter::element to write booleans as strings (true|false).
189 * See that method's documentation for more info.
191 * @param string $tag Element type or tagname
192 * @param array $attrs Array of element attributes, as
194 * @param string $content string content of the element
198 function element($tag, $attrs=null, $content=null)
200 if (is_bool($content)) {
201 $content = ($content ? 'true' : 'false');
204 return parent::element($tag, $attrs, $content);
207 function twitterUserArray($profile, $get_notice=false)
209 $twitter_user = array();
212 $user = $profile->getUser();
213 } catch (NoSuchUserException $e) {
217 $twitter_user['id'] = intval($profile->id);
218 $twitter_user['name'] = $profile->getBestName();
219 $twitter_user['screen_name'] = $profile->nickname;
220 $twitter_user['location'] = ($profile->location) ? $profile->location : null;
221 $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
223 // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
224 $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
225 $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
227 // START introduced by qvitter API, not necessary for StatusNet API
228 $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
230 $avatar = Avatar::getUploaded($profile);
231 $origurl = $avatar->displayUrl();
232 } catch (Exception $e) {
233 $origurl = $twitter_user['profile_image_url_profile_size'];
235 $twitter_user['profile_image_url_original'] = $origurl;
237 $twitter_user['groups_count'] = $profile->getGroupCount();
238 foreach (array('linkcolor', 'backgroundcolor') as $key) {
239 $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
241 // END introduced by qvitter API, not necessary for StatusNet API
243 $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
244 $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
245 $twitter_user['followers_count'] = $profile->subscriberCount();
247 // Note: some profiles don't have an associated user
249 $twitter_user['friends_count'] = $profile->subscriptionCount();
251 $twitter_user['created_at'] = $this->dateTwitter($profile->created);
255 if (!empty($user) && $user->timezone) {
256 $timezone = $user->timezone;
260 $t->setTimezone(new DateTimeZone($timezone));
262 $twitter_user['utc_offset'] = $t->format('Z');
263 $twitter_user['time_zone'] = $timezone;
264 $twitter_user['statuses_count'] = $profile->noticeCount();
266 // Is the requesting user following this user?
267 $twitter_user['following'] = false;
268 $twitter_user['statusnet_blocking'] = false;
269 $twitter_user['notifications'] = false;
271 if (isset($this->auth_user)) {
273 $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
274 $twitter_user['statusnet_blocking'] = $this->auth_user->hasBlocked($profile);
277 $sub = Subscription::pkeyGet(array('subscriber' =>
278 $this->auth_user->id,
279 'subscribed' => $profile->id));
282 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
287 $notice = $profile->getCurrentNotice();
288 if ($notice instanceof Notice) {
290 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
294 // StatusNet-specific
296 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
298 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
299 Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
301 return $twitter_user;
304 function twitterStatusArray($notice, $include_user=true)
306 $base = $this->twitterSimpleStatusArray($notice, $include_user);
308 if (!empty($notice->repeat_of)) {
309 $original = Notice::getKV('id', $notice->repeat_of);
310 if ($original instanceof Notice) {
311 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
312 $base['retweeted_status'] = $orig_array;
319 function twitterSimpleStatusArray($notice, $include_user=true)
321 $profile = $notice->getProfile();
323 $twitter_status = array();
324 $twitter_status['text'] = $notice->content;
325 $twitter_status['truncated'] = false; # Not possible on StatusNet
326 $twitter_status['created_at'] = $this->dateTwitter($notice->created);
328 // We could just do $notice->reply_to but maybe the future holds a
329 // different story for parenting.
330 $parent = $notice->getParent();
331 $in_reply_to = $parent->id;
332 } catch (Exception $e) {
335 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
339 $ns = $notice->getSource();
340 if ($ns instanceof Notice_source) {
341 if (!empty($ns->name) && !empty($ns->url)) {
342 $source = '<a href="'
343 . htmlspecialchars($ns->url)
344 . '" rel="nofollow">'
345 . htmlspecialchars($ns->name)
352 $twitter_status['uri'] = $notice->getUri();
353 $twitter_status['source'] = $source;
354 $twitter_status['id'] = intval($notice->id);
356 $replier_profile = null;
358 if ($notice->reply_to) {
359 $reply = Notice::getKV(intval($notice->reply_to));
361 $replier_profile = $reply->getProfile();
365 $twitter_status['in_reply_to_user_id'] =
366 ($replier_profile) ? intval($replier_profile->id) : null;
367 $twitter_status['in_reply_to_screen_name'] =
368 ($replier_profile) ? $replier_profile->nickname : null;
370 if (isset($notice->lat) && isset($notice->lon)) {
371 // This is the format that GeoJSON expects stuff to be in
372 $twitter_status['geo'] = array('type' => 'Point',
373 'coordinates' => array((float) $notice->lat,
374 (float) $notice->lon));
376 $twitter_status['geo'] = null;
379 if (!is_null($this->scoped)) {
380 $twitter_status['repeated'] = $this->scoped->hasRepeated($notice);
382 $twitter_status['repeated'] = false;
386 $attachments = $notice->attachments();
388 if (!empty($attachments)) {
390 $twitter_status['attachments'] = array();
392 foreach ($attachments as $attachment) {
394 $enclosure_o = $attachment->getEnclosure();
395 $enclosure = array();
396 $enclosure['url'] = $enclosure_o->url;
397 $enclosure['mimetype'] = $enclosure_o->mimetype;
398 $enclosure['size'] = $enclosure_o->size;
399 $twitter_status['attachments'][] = $enclosure;
400 } catch (ServerException $e) {
401 // There was not enough metadata available
406 if ($include_user && $profile) {
407 // Don't get notice (recursive!)
408 $twitter_user = $this->twitterUserArray($profile, false);
409 $twitter_status['user'] = $twitter_user;
412 // StatusNet-specific
414 $twitter_status['statusnet_html'] = $notice->rendered;
415 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
417 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
418 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
419 array('include_user'=>$include_user)));
421 return $twitter_status;
424 function twitterGroupArray($group)
426 $twitter_group = array();
428 $twitter_group['id'] = intval($group->id);
429 $twitter_group['url'] = $group->permalink();
430 $twitter_group['nickname'] = $group->nickname;
431 $twitter_group['fullname'] = $group->fullname;
433 if (isset($this->auth_user)) {
434 $twitter_group['member'] = $this->auth_user->isMember($group);
435 $twitter_group['blocked'] = Group_block::isBlocked(
437 $this->auth_user->getProfile()
441 $twitter_group['admin_count'] = $group->getAdminCount();
442 $twitter_group['member_count'] = $group->getMemberCount();
443 $twitter_group['original_logo'] = $group->original_logo;
444 $twitter_group['homepage_logo'] = $group->homepage_logo;
445 $twitter_group['stream_logo'] = $group->stream_logo;
446 $twitter_group['mini_logo'] = $group->mini_logo;
447 $twitter_group['homepage'] = $group->homepage;
448 $twitter_group['description'] = $group->description;
449 $twitter_group['location'] = $group->location;
450 $twitter_group['created'] = $this->dateTwitter($group->created);
451 $twitter_group['modified'] = $this->dateTwitter($group->modified);
453 return $twitter_group;
456 function twitterRssGroupArray($group)
459 $entry['content']=$group->description;
460 $entry['title']=$group->nickname;
461 $entry['link']=$group->permalink();
462 $entry['published']=common_date_iso8601($group->created);
463 $entry['updated']==common_date_iso8601($group->modified);
464 $taguribase = common_config('integration', 'groupuri');
465 $entry['id'] = "group:$groupuribase:$entry[link]";
467 $entry['description'] = $entry['content'];
468 $entry['pubDate'] = common_date_rfc2822($group->created);
469 $entry['guid'] = $entry['link'];
474 function twitterListArray($list)
476 $profile = Profile::getKV('id', $list->tagger);
478 $twitter_list = array();
479 $twitter_list['id'] = $list->id;
480 $twitter_list['name'] = $list->tag;
481 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
482 $twitter_list['slug'] = $list->tag;
483 $twitter_list['description'] = $list->description;
484 $twitter_list['subscriber_count'] = $list->subscriberCount();
485 $twitter_list['member_count'] = $list->taggedCount();
486 $twitter_list['uri'] = $list->getUri();
488 if (isset($this->auth_user)) {
489 $twitter_list['following'] = $list->hasSubscriber($this->auth_user);
491 $twitter_list['following'] = false;
494 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
495 $twitter_list['user'] = $this->twitterUserArray($profile, false);
497 return $twitter_list;
500 function twitterRssEntryArray($notice)
504 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
505 $profile = $notice->getProfile();
507 // We trim() to avoid extraneous whitespace in the output
509 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
510 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
511 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
512 $entry['published'] = common_date_iso8601($notice->created);
514 $taguribase = TagURI::base();
515 $entry['id'] = "tag:$taguribase:$entry[link]";
517 $entry['updated'] = $entry['published'];
518 $entry['author'] = $profile->getBestName();
521 $attachments = $notice->attachments();
522 $enclosures = array();
524 foreach ($attachments as $attachment) {
526 $enclosure_o = $attachment->getEnclosure();
527 $enclosure = array();
528 $enclosure['url'] = $enclosure_o->url;
529 $enclosure['mimetype'] = $enclosure_o->mimetype;
530 $enclosure['size'] = $enclosure_o->size;
531 $enclosures[] = $enclosure;
532 } catch (ServerException $e) {
533 // There was not enough metadata available
537 if (!empty($enclosures)) {
538 $entry['enclosures'] = $enclosures;
542 $tag = new Notice_tag();
543 $tag->notice_id = $notice->id;
545 $entry['tags']=array();
546 while ($tag->fetch()) {
547 $entry['tags'][]=$tag->tag;
553 $entry['description'] = $entry['content'];
554 $entry['pubDate'] = common_date_rfc2822($notice->created);
555 $entry['guid'] = $entry['link'];
557 if (isset($notice->lat) && isset($notice->lon)) {
558 // This is the format that GeoJSON expects stuff to be in.
559 // showGeoRSS() below uses it for XML output, so we reuse it
560 $entry['geo'] = array('type' => 'Point',
561 'coordinates' => array((float) $notice->lat,
562 (float) $notice->lon));
564 $entry['geo'] = null;
567 Event::handle('EndRssEntryArray', array($notice, &$entry));
573 function twitterRelationshipArray($source, $target)
575 $relationship = array();
577 $relationship['source'] =
578 $this->relationshipDetailsArray($source, $target);
579 $relationship['target'] =
580 $this->relationshipDetailsArray($target, $source);
582 return array('relationship' => $relationship);
585 function relationshipDetailsArray($source, $target)
589 $source_profile = $source->getProfile();
590 $target_profile = $target->getProfile();
592 $details['screen_name'] = $source->nickname;
593 $details['followed_by'] = $target->isSubscribed($source_profile);
594 $details['following'] = $source->isSubscribed($target_profile);
596 $notifications = false;
598 if ($source->isSubscribed($target_profile)) {
599 $sub = Subscription::pkeyGet(array('subscriber' =>
600 $source->id, 'subscribed' => $target->id));
603 $notifications = ($sub->jabber || $sub->sms);
607 $details['notifications_enabled'] = $notifications;
608 $details['blocking'] = $source->hasBlocked($target_profile);
609 $details['id'] = intval($source->id);
614 function showTwitterXmlRelationship($relationship)
616 $this->elementStart('relationship');
618 foreach($relationship as $element => $value) {
619 if ($element == 'source' || $element == 'target') {
620 $this->elementStart($element);
621 $this->showXmlRelationshipDetails($value);
622 $this->elementEnd($element);
626 $this->elementEnd('relationship');
629 function showXmlRelationshipDetails($details)
631 foreach($details as $element => $value) {
632 $this->element($element, null, $value);
636 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
640 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
642 $this->elementStart($tag, $attrs);
643 foreach($twitter_status as $element => $value) {
646 $this->showTwitterXmlUser($twitter_status['user']);
649 $this->element($element, null, common_xml_safe_str($value));
652 $this->showXmlAttachments($twitter_status['attachments']);
655 $this->showGeoXML($value);
657 case 'retweeted_status':
658 $this->showTwitterXmlStatus($value, 'retweeted_status');
661 if (strncmp($element, 'statusnet_', 10) == 0) {
662 $this->element('statusnet:'.substr($element, 10), null, $value);
664 $this->element($element, null, $value);
668 $this->elementEnd($tag);
671 function showTwitterXmlGroup($twitter_group)
673 $this->elementStart('group');
674 foreach($twitter_group as $element => $value) {
675 $this->element($element, null, $value);
677 $this->elementEnd('group');
680 function showTwitterXmlList($twitter_list)
682 $this->elementStart('list');
683 foreach($twitter_list as $element => $value) {
684 if($element == 'user') {
685 $this->showTwitterXmlUser($value, 'user');
688 $this->element($element, null, $value);
691 $this->elementEnd('list');
694 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
698 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
700 $this->elementStart($role, $attrs);
701 foreach($twitter_user as $element => $value) {
702 if ($element == 'status') {
703 $this->showTwitterXmlStatus($twitter_user['status']);
704 } else if (strncmp($element, 'statusnet_', 10) == 0) {
705 $this->element('statusnet:'.substr($element, 10), null, $value);
707 $this->element($element, null, $value);
710 $this->elementEnd($role);
713 function showXmlAttachments($attachments) {
714 if (!empty($attachments)) {
715 $this->elementStart('attachments', array('type' => 'array'));
716 foreach ($attachments as $attachment) {
718 $attrs['url'] = $attachment['url'];
719 $attrs['mimetype'] = $attachment['mimetype'];
720 $attrs['size'] = $attachment['size'];
721 $this->element('enclosure', $attrs, '');
723 $this->elementEnd('attachments');
727 function showGeoXML($geo)
731 $this->element('geo');
733 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
734 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
735 $this->elementEnd('geo');
739 function showGeoRSS($geo)
745 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
750 function showTwitterRssItem($entry)
752 $this->elementStart('item');
753 $this->element('title', null, $entry['title']);
754 $this->element('description', null, $entry['description']);
755 $this->element('pubDate', null, $entry['pubDate']);
756 $this->element('guid', null, $entry['guid']);
757 $this->element('link', null, $entry['link']);
759 // RSS only supports 1 enclosure per item
760 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
761 $enclosure = $entry['enclosures'][0];
762 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
765 if(array_key_exists('tags', $entry)){
766 foreach($entry['tags'] as $tag){
767 $this->element('category', null,$tag);
771 $this->showGeoRSS($entry['geo']);
772 $this->elementEnd('item');
775 function showJsonObjects($objects)
777 print(json_encode($objects));
780 function showSingleXmlStatus($notice)
782 $this->initDocument('xml');
783 $twitter_status = $this->twitterStatusArray($notice);
784 $this->showTwitterXmlStatus($twitter_status, 'status', true);
785 $this->endDocument('xml');
788 function showSingleAtomStatus($notice)
790 header('Content-Type: application/atom+xml; charset=utf-8');
791 print $notice->asAtomEntry(true, true, true, $this->auth_user);
794 function show_single_json_status($notice)
796 $this->initDocument('json');
797 $status = $this->twitterStatusArray($notice);
798 $this->showJsonObjects($status);
799 $this->endDocument('json');
802 function showXmlTimeline($notice)
804 $this->initDocument('xml');
805 $this->elementStart('statuses', array('type' => 'array',
806 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
808 if (is_array($notice)) {
809 $notice = new ArrayWrapper($notice);
812 while ($notice->fetch()) {
814 $twitter_status = $this->twitterStatusArray($notice);
815 $this->showTwitterXmlStatus($twitter_status);
816 } catch (Exception $e) {
817 common_log(LOG_ERR, $e->getMessage());
822 $this->elementEnd('statuses');
823 $this->endDocument('xml');
826 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
828 $this->initDocument('rss');
830 $this->element('title', null, $title);
831 $this->element('link', null, $link);
833 if (!is_null($self)) {
837 'type' => 'application/rss+xml',
844 if (!is_null($suplink)) {
845 // For FriendFeed's SUP protocol
846 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
847 'rel' => 'http://api.friendfeed.com/2008/03#sup',
849 'type' => 'application/json'));
852 if (!is_null($logo)) {
853 $this->elementStart('image');
854 $this->element('link', null, $link);
855 $this->element('title', null, $title);
856 $this->element('url', null, $logo);
857 $this->elementEnd('image');
860 $this->element('description', null, $subtitle);
861 $this->element('language', null, 'en-us');
862 $this->element('ttl', null, '40');
864 if (is_array($notice)) {
865 $notice = new ArrayWrapper($notice);
868 while ($notice->fetch()) {
870 $entry = $this->twitterRssEntryArray($notice);
871 $this->showTwitterRssItem($entry);
872 } catch (Exception $e) {
873 common_log(LOG_ERR, $e->getMessage());
874 // continue on exceptions
878 $this->endTwitterRss();
881 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
883 $this->initDocument('atom');
885 $this->element('title', null, $title);
886 $this->element('id', null, $id);
887 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
889 if (!is_null($logo)) {
890 $this->element('logo',null,$logo);
893 if (!is_null($suplink)) {
894 // For FriendFeed's SUP protocol
895 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
897 'type' => 'application/json'));
900 if (!is_null($selfuri)) {
901 $this->element('link', array('href' => $selfuri,
902 'rel' => 'self', 'type' => 'application/atom+xml'), null);
905 $this->element('updated', null, common_date_iso8601('now'));
906 $this->element('subtitle', null, $subtitle);
908 if (is_array($notice)) {
909 $notice = new ArrayWrapper($notice);
912 while ($notice->fetch()) {
914 $this->raw($notice->asAtomEntry());
915 } catch (Exception $e) {
916 common_log(LOG_ERR, $e->getMessage());
921 $this->endDocument('atom');
924 function showRssGroups($group, $title, $link, $subtitle)
926 $this->initDocument('rss');
928 $this->element('title', null, $title);
929 $this->element('link', null, $link);
930 $this->element('description', null, $subtitle);
931 $this->element('language', null, 'en-us');
932 $this->element('ttl', null, '40');
934 if (is_array($group)) {
935 foreach ($group as $g) {
936 $twitter_group = $this->twitterRssGroupArray($g);
937 $this->showTwitterRssItem($twitter_group);
940 while ($group->fetch()) {
941 $twitter_group = $this->twitterRssGroupArray($group);
942 $this->showTwitterRssItem($twitter_group);
946 $this->endTwitterRss();
949 function showTwitterAtomEntry($entry)
951 $this->elementStart('entry');
952 $this->element('title', null, common_xml_safe_str($entry['title']));
955 array('type' => 'html'),
956 common_xml_safe_str($entry['content'])
958 $this->element('id', null, $entry['id']);
959 $this->element('published', null, $entry['published']);
960 $this->element('updated', null, $entry['updated']);
961 $this->element('link', array('type' => 'text/html',
962 'href' => $entry['link'],
963 'rel' => 'alternate'));
964 $this->element('link', array('type' => $entry['avatar-type'],
965 'href' => $entry['avatar'],
967 $this->elementStart('author');
969 $this->element('name', null, $entry['author-name']);
970 $this->element('uri', null, $entry['author-uri']);
972 $this->elementEnd('author');
973 $this->elementEnd('entry');
976 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
978 $this->initDocument('atom');
980 $this->element('title', null, common_xml_safe_str($title));
981 $this->element('id', null, $id);
982 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
984 if (!is_null($selfuri)) {
985 $this->element('link', array('href' => $selfuri,
986 'rel' => 'self', 'type' => 'application/atom+xml'), null);
989 $this->element('updated', null, common_date_iso8601('now'));
990 $this->element('subtitle', null, common_xml_safe_str($subtitle));
992 if (is_array($group)) {
993 foreach ($group as $g) {
994 $this->raw($g->asAtomEntry());
997 while ($group->fetch()) {
998 $this->raw($group->asAtomEntry());
1002 $this->endDocument('atom');
1006 function showJsonTimeline($notice)
1008 $this->initDocument('json');
1010 $statuses = array();
1012 if (is_array($notice)) {
1013 $notice = new ArrayWrapper($notice);
1016 while ($notice->fetch()) {
1018 $twitter_status = $this->twitterStatusArray($notice);
1019 array_push($statuses, $twitter_status);
1020 } catch (Exception $e) {
1021 common_log(LOG_ERR, $e->getMessage());
1026 $this->showJsonObjects($statuses);
1028 $this->endDocument('json');
1031 function showJsonGroups($group)
1033 $this->initDocument('json');
1037 if (is_array($group)) {
1038 foreach ($group as $g) {
1039 $twitter_group = $this->twitterGroupArray($g);
1040 array_push($groups, $twitter_group);
1043 while ($group->fetch()) {
1044 $twitter_group = $this->twitterGroupArray($group);
1045 array_push($groups, $twitter_group);
1049 $this->showJsonObjects($groups);
1051 $this->endDocument('json');
1054 function showXmlGroups($group)
1057 $this->initDocument('xml');
1058 $this->elementStart('groups', array('type' => 'array'));
1060 if (is_array($group)) {
1061 foreach ($group as $g) {
1062 $twitter_group = $this->twitterGroupArray($g);
1063 $this->showTwitterXmlGroup($twitter_group);
1066 while ($group->fetch()) {
1067 $twitter_group = $this->twitterGroupArray($group);
1068 $this->showTwitterXmlGroup($twitter_group);
1072 $this->elementEnd('groups');
1073 $this->endDocument('xml');
1076 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1079 $this->initDocument('xml');
1080 $this->elementStart('lists_list');
1081 $this->elementStart('lists', array('type' => 'array'));
1083 if (is_array($list)) {
1084 foreach ($list as $l) {
1085 $twitter_list = $this->twitterListArray($l);
1086 $this->showTwitterXmlList($twitter_list);
1089 while ($list->fetch()) {
1090 $twitter_list = $this->twitterListArray($list);
1091 $this->showTwitterXmlList($twitter_list);
1095 $this->elementEnd('lists');
1097 $this->element('next_cursor', null, $next_cursor);
1098 $this->element('previous_cursor', null, $prev_cursor);
1100 $this->elementEnd('lists_list');
1101 $this->endDocument('xml');
1104 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1106 $this->initDocument('json');
1110 if (is_array($list)) {
1111 foreach ($list as $l) {
1112 $twitter_list = $this->twitterListArray($l);
1113 array_push($lists, $twitter_list);
1116 while ($list->fetch()) {
1117 $twitter_list = $this->twitterListArray($list);
1118 array_push($lists, $twitter_list);
1122 $lists_list = array(
1124 'next_cursor' => $next_cursor,
1125 'next_cursor_str' => strval($next_cursor),
1126 'previous_cursor' => $prev_cursor,
1127 'previous_cursor_str' => strval($prev_cursor)
1130 $this->showJsonObjects($lists_list);
1132 $this->endDocument('json');
1135 function showTwitterXmlUsers($user)
1137 $this->initDocument('xml');
1138 $this->elementStart('users', array('type' => 'array',
1139 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1141 if (is_array($user)) {
1142 foreach ($user as $u) {
1143 $twitter_user = $this->twitterUserArray($u);
1144 $this->showTwitterXmlUser($twitter_user);
1147 while ($user->fetch()) {
1148 $twitter_user = $this->twitterUserArray($user);
1149 $this->showTwitterXmlUser($twitter_user);
1153 $this->elementEnd('users');
1154 $this->endDocument('xml');
1157 function showJsonUsers($user)
1159 $this->initDocument('json');
1163 if (is_array($user)) {
1164 foreach ($user as $u) {
1165 $twitter_user = $this->twitterUserArray($u);
1166 array_push($users, $twitter_user);
1169 while ($user->fetch()) {
1170 $twitter_user = $this->twitterUserArray($user);
1171 array_push($users, $twitter_user);
1175 $this->showJsonObjects($users);
1177 $this->endDocument('json');
1180 function showSingleJsonGroup($group)
1182 $this->initDocument('json');
1183 $twitter_group = $this->twitterGroupArray($group);
1184 $this->showJsonObjects($twitter_group);
1185 $this->endDocument('json');
1188 function showSingleXmlGroup($group)
1190 $this->initDocument('xml');
1191 $twitter_group = $this->twitterGroupArray($group);
1192 $this->showTwitterXmlGroup($twitter_group);
1193 $this->endDocument('xml');
1196 function showSingleJsonList($list)
1198 $this->initDocument('json');
1199 $twitter_list = $this->twitterListArray($list);
1200 $this->showJsonObjects($twitter_list);
1201 $this->endDocument('json');
1204 function showSingleXmlList($list)
1206 $this->initDocument('xml');
1207 $twitter_list = $this->twitterListArray($list);
1208 $this->showTwitterXmlList($twitter_list);
1209 $this->endDocument('xml');
1212 function dateTwitter($dt)
1214 $dateStr = date('d F Y H:i:s', strtotime($dt));
1215 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1216 $d->setTimezone(new DateTimeZone(common_timezone()));
1217 return $d->format('D M d H:i:s O Y');
1220 function initDocument($type='xml')
1224 header('Content-Type: application/xml; charset=utf-8');
1228 header('Content-Type: application/json; charset=utf-8');
1230 // Check for JSONP callback
1231 if (isset($this->callback)) {
1232 print $this->callback . '(';
1236 header("Content-Type: application/rss+xml; charset=utf-8");
1237 $this->initTwitterRss();
1240 header('Content-Type: application/atom+xml; charset=utf-8');
1241 $this->initTwitterAtom();
1244 // TRANS: Client error on an API request with an unsupported data format.
1245 $this->clientError(_('Not a supported data format.'));
1251 function endDocument($type='xml')
1258 // Check for JSONP callback
1259 if (isset($this->callback)) {
1264 $this->endTwitterRss();
1267 $this->endTwitterRss();
1270 // TRANS: Client error on an API request with an unsupported data format.
1271 $this->clientError(_('Not a supported data format.'));
1276 function initTwitterRss()
1279 $this->elementStart(
1283 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1284 'xmlns:georss' => 'http://www.georss.org/georss'
1287 $this->elementStart('channel');
1288 Event::handle('StartApiRss', array($this));
1291 function endTwitterRss()
1293 $this->elementEnd('channel');
1294 $this->elementEnd('rss');
1298 function initTwitterAtom()
1301 // FIXME: don't hardcode the language here!
1302 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1303 'xml:lang' => 'en-US',
1304 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1307 function endTwitterAtom()
1309 $this->elementEnd('feed');
1313 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1315 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1316 switch ($content_type) {
1318 $this->showTwitterXmlUser($profile_array);
1321 $this->showJsonObjects($profile_array);
1324 // TRANS: Client error on an API request with an unsupported data format.
1325 $this->clientError(_('Not a supported data format.'));
1330 private static function is_decimal($str)
1332 return preg_match('/^[0-9]+$/', $str);
1335 function getTargetUser($id)
1338 // Twitter supports these other ways of passing the user ID
1339 if (self::is_decimal($this->arg('id'))) {
1340 return User::getKV($this->arg('id'));
1341 } else if ($this->arg('id')) {
1342 $nickname = common_canonical_nickname($this->arg('id'));
1343 return User::getKV('nickname', $nickname);
1344 } else if ($this->arg('user_id')) {
1345 // This is to ensure that a non-numeric user_id still
1346 // overrides screen_name even if it doesn't get used
1347 if (self::is_decimal($this->arg('user_id'))) {
1348 return User::getKV('id', $this->arg('user_id'));
1350 } else if ($this->arg('screen_name')) {
1351 $nickname = common_canonical_nickname($this->arg('screen_name'));
1352 return User::getKV('nickname', $nickname);
1354 // Fall back to trying the currently authenticated user
1355 return $this->auth_user;
1358 } else if (self::is_decimal($id)) {
1359 return User::getKV($id);
1361 $nickname = common_canonical_nickname($id);
1362 return User::getKV('nickname', $nickname);
1366 function getTargetProfile($id)
1370 // Twitter supports these other ways of passing the user ID
1371 if (self::is_decimal($this->arg('id'))) {
1372 return Profile::getKV($this->arg('id'));
1373 } else if ($this->arg('id')) {
1374 // Screen names currently can only uniquely identify a local user.
1375 $nickname = common_canonical_nickname($this->arg('id'));
1376 $user = User::getKV('nickname', $nickname);
1377 return $user ? $user->getProfile() : null;
1378 } else if ($this->arg('user_id')) {
1379 // This is to ensure that a non-numeric user_id still
1380 // overrides screen_name even if it doesn't get used
1381 if (self::is_decimal($this->arg('user_id'))) {
1382 return Profile::getKV('id', $this->arg('user_id'));
1384 } else if ($this->arg('screen_name')) {
1385 $nickname = common_canonical_nickname($this->arg('screen_name'));
1386 $user = User::getKV('nickname', $nickname);
1387 return $user instanceof User ? $user->getProfile() : null;
1389 // Fall back to trying the currently authenticated user
1390 return $this->scoped;
1392 } else if (self::is_decimal($id)) {
1393 return Profile::getKV($id);
1395 $nickname = common_canonical_nickname($id);
1396 $user = User::getKV('nickname', $nickname);
1397 return $user ? $user->getProfile() : null;
1401 function getTargetGroup($id)
1404 if (self::is_decimal($this->arg('id'))) {
1405 return User_group::getKV('id', $this->arg('id'));
1406 } else if ($this->arg('id')) {
1407 return User_group::getForNickname($this->arg('id'));
1408 } else if ($this->arg('group_id')) {
1409 // This is to ensure that a non-numeric group_id still
1410 // overrides group_name even if it doesn't get used
1411 if (self::is_decimal($this->arg('group_id'))) {
1412 return User_group::getKV('id', $this->arg('group_id'));
1414 } else if ($this->arg('group_name')) {
1415 return User_group::getForNickname($this->arg('group_name'));
1418 } else if (self::is_decimal($id)) {
1419 return User_group::getKV('id', $id);
1420 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1421 return User_group::getKV('uri', urldecode($this->arg('uri')));
1423 return User_group::getForNickname($id);
1427 function getTargetList($user=null, $id=null)
1429 $tagger = $this->getTargetUser($user);
1433 $id = $this->arg('id');
1437 if (is_numeric($id)) {
1438 $list = Profile_list::getKV('id', $id);
1440 // only if the list with the id belongs to the tagger
1441 if(empty($list) || $list->tagger != $tagger->id) {
1446 $tag = common_canonical_tag($id);
1447 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1450 if (!empty($list) && $list->private) {
1451 if ($this->auth_user->id == $list->tagger) {
1462 * Returns query argument or default value if not found. Certain
1463 * parameters used throughout the API are lightly scrubbed and
1464 * bounds checked. This overrides Action::arg().
1466 * @param string $key requested argument
1467 * @param string $def default value to return if $key is not provided
1471 function arg($key, $def=null)
1473 // XXX: Do even more input validation/scrubbing?
1475 if (array_key_exists($key, $this->args)) {
1478 $page = (int)$this->args['page'];
1479 return ($page < 1) ? 1 : $page;
1481 $count = (int)$this->args['count'];
1484 } elseif ($count > 200) {
1490 $since_id = (int)$this->args['since_id'];
1491 return ($since_id < 1) ? 0 : $since_id;
1493 $max_id = (int)$this->args['max_id'];
1494 return ($max_id < 1) ? 0 : $max_id;
1496 return parent::arg($key, $def);
1504 * Calculate the complete URI that called up this action. Used for
1505 * Atom rel="self" links. Warning: this is funky.
1507 * @return string URL a URL suitable for rel="self" Atom links
1509 function getSelfUri()
1511 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1513 $id = $this->arg('id');
1514 $aargs = array('format' => $this->format);
1519 $tag = $this->arg('tag');
1521 $aargs['tag'] = $tag;
1524 parse_str($_SERVER['QUERY_STRING'], $params);
1526 if (!empty($params)) {
1527 unset($params['p']);
1528 $pstring = http_build_query($params);
1531 $uri = common_local_url($action, $aargs);
1533 if (!empty($pstring)) {
1534 $uri .= '?' . $pstring;