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 GNUsocial::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-GNUsocial-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'] = $profile->getID();
218 $twitter_user['name'] = $profile->getBestName();
219 $twitter_user['screen_name'] = $profile->getNickname();
220 $twitter_user['location'] = $profile->location;
221 $twitter_user['description'] = $profile->getDescription();
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'] = self::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 // These values might actually also mean "unknown". Ambiguity issues?
268 $twitter_user['following'] = false;
269 $twitter_user['statusnet_blocking'] = false;
270 $twitter_user['notifications'] = false;
272 if ($this->scoped instanceof Profile) {
274 $sub = Subscription::getSubscription($this->scoped, $profile);
276 $twitter_user['following'] = true;
277 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
278 } catch (NoResultException $e) {
279 // well, the values are already false...
281 $twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
285 $notice = $profile->getCurrentNotice();
286 if ($notice instanceof Notice) {
288 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
292 // StatusNet-specific
294 $twitter_user['statusnet_profile_url'] = $profile->profileurl;
296 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
297 Event::handle('TwitterUserArray', array($profile, &$twitter_user, $this->scoped, array()));
299 return $twitter_user;
302 function twitterStatusArray($notice, $include_user=true)
304 $base = $this->twitterSimpleStatusArray($notice, $include_user);
306 // FIXME: MOVE TO SHARE PLUGIN
307 if (!empty($notice->repeat_of)) {
308 $original = Notice::getKV('id', $notice->repeat_of);
309 if ($original instanceof Notice) {
310 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
311 $base['retweeted_status'] = $orig_array;
318 function twitterSimpleStatusArray($notice, $include_user=true)
320 $profile = $notice->getProfile();
322 $twitter_status = array();
323 $twitter_status['text'] = $notice->content;
324 $twitter_status['truncated'] = false; # Not possible on StatusNet
325 $twitter_status['created_at'] = self::dateTwitter($notice->created);
327 // We could just do $notice->reply_to but maybe the future holds a
328 // different story for parenting.
329 $parent = $notice->getParent();
330 $in_reply_to = $parent->id;
331 } catch (NoParentNoticeException $e) {
333 } catch (NoResultException $e) {
334 // the in_reply_to message has probably been deleted
337 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
342 $ns = $notice->getSource();
343 if ($ns instanceof Notice_source) {
345 if (!empty($ns->url)) {
346 $source_link = $ns->url;
347 if (!empty($ns->name)) {
353 $twitter_status['uri'] = $notice->getUri();
354 $twitter_status['source'] = $source;
355 $twitter_status['source_link'] = $source_link;
356 $twitter_status['id'] = intval($notice->id);
358 $replier_profile = null;
360 if ($notice->reply_to) {
361 $reply = Notice::getKV(intval($notice->reply_to));
363 $replier_profile = $reply->getProfile();
367 $twitter_status['in_reply_to_user_id'] =
368 ($replier_profile) ? intval($replier_profile->id) : null;
369 $twitter_status['in_reply_to_screen_name'] =
370 ($replier_profile) ? $replier_profile->nickname : null;
373 $notloc = Notice_location::locFromStored($notice);
374 // This is the format that GeoJSON expects stuff to be in
375 $twitter_status['geo'] = array('type' => 'Point',
376 'coordinates' => array((float) $notloc->lat,
377 (float) $notloc->lon));
378 } catch (ServerException $e) {
379 $twitter_status['geo'] = null;
383 $attachments = $notice->attachments();
385 if (!empty($attachments)) {
387 $twitter_status['attachments'] = array();
389 foreach ($attachments as $attachment) {
391 $enclosure_o = $attachment->getEnclosure();
392 $enclosure = array();
393 $enclosure['url'] = $enclosure_o->url;
394 $enclosure['mimetype'] = $enclosure_o->mimetype;
395 $enclosure['size'] = $enclosure_o->size;
396 $twitter_status['attachments'][] = $enclosure;
397 } catch (ServerException $e) {
398 // There was not enough metadata available
403 if ($include_user && $profile) {
404 // Don't get notice (recursive!)
405 $twitter_user = $this->twitterUserArray($profile, false);
406 $twitter_status['user'] = $twitter_user;
409 // StatusNet-specific
411 $twitter_status['statusnet_html'] = $notice->getRendered();
412 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
414 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
415 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
416 array('include_user'=>$include_user)));
418 return $twitter_status;
421 function twitterGroupArray($group)
423 $twitter_group = array();
425 $twitter_group['id'] = intval($group->id);
426 $twitter_group['url'] = $group->permalink();
427 $twitter_group['nickname'] = $group->nickname;
428 $twitter_group['fullname'] = $group->fullname;
430 if ($this->scoped instanceof Profile) {
431 $twitter_group['member'] = $this->scoped->isMember($group);
432 $twitter_group['blocked'] = Group_block::isBlocked(
438 $twitter_group['admin_count'] = $group->getAdminCount();
439 $twitter_group['member_count'] = $group->getMemberCount();
440 $twitter_group['original_logo'] = $group->original_logo;
441 $twitter_group['homepage_logo'] = $group->homepage_logo;
442 $twitter_group['stream_logo'] = $group->stream_logo;
443 $twitter_group['mini_logo'] = $group->mini_logo;
444 $twitter_group['homepage'] = $group->homepage;
445 $twitter_group['description'] = $group->description;
446 $twitter_group['location'] = $group->location;
447 $twitter_group['created'] = self::dateTwitter($group->created);
448 $twitter_group['modified'] = self::dateTwitter($group->modified);
450 return $twitter_group;
453 function twitterRssGroupArray($group)
456 $entry['content']=$group->description;
457 $entry['title']=$group->nickname;
458 $entry['link']=$group->permalink();
459 $entry['published']=common_date_iso8601($group->created);
460 $entry['updated']==common_date_iso8601($group->modified);
461 $taguribase = common_config('integration', 'groupuri');
462 $entry['id'] = "group:$groupuribase:$entry[link]";
464 $entry['description'] = $entry['content'];
465 $entry['pubDate'] = common_date_rfc2822($group->created);
466 $entry['guid'] = $entry['link'];
471 function twitterListArray($list)
473 $profile = Profile::getKV('id', $list->tagger);
475 $twitter_list = array();
476 $twitter_list['id'] = $list->id;
477 $twitter_list['name'] = $list->tag;
478 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
479 $twitter_list['slug'] = $list->tag;
480 $twitter_list['description'] = $list->description;
481 $twitter_list['subscriber_count'] = $list->subscriberCount();
482 $twitter_list['member_count'] = $list->taggedCount();
483 $twitter_list['uri'] = $list->getUri();
485 if ($this->scoped instanceof Profile) {
486 $twitter_list['following'] = $list->hasSubscriber($this->scoped);
488 $twitter_list['following'] = false;
491 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
492 $twitter_list['user'] = $this->twitterUserArray($profile, false);
494 return $twitter_list;
497 function twitterRssEntryArray($notice)
501 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
502 $profile = $notice->getProfile();
504 // We trim() to avoid extraneous whitespace in the output
506 $entry['content'] = common_xml_safe_str(trim($notice->getRendered()));
507 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
508 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
509 $entry['published'] = common_date_iso8601($notice->created);
511 $taguribase = TagURI::base();
512 $entry['id'] = "tag:$taguribase:$entry[link]";
514 $entry['updated'] = $entry['published'];
515 $entry['author'] = $profile->getBestName();
518 $attachments = $notice->attachments();
519 $enclosures = array();
521 foreach ($attachments as $attachment) {
523 $enclosure_o = $attachment->getEnclosure();
524 $enclosure = array();
525 $enclosure['url'] = $enclosure_o->url;
526 $enclosure['mimetype'] = $enclosure_o->mimetype;
527 $enclosure['size'] = $enclosure_o->size;
528 $enclosures[] = $enclosure;
529 } catch (ServerException $e) {
530 // There was not enough metadata available
534 if (!empty($enclosures)) {
535 $entry['enclosures'] = $enclosures;
539 $tag = new Notice_tag();
540 $tag->notice_id = $notice->id;
542 $entry['tags']=array();
543 while ($tag->fetch()) {
544 $entry['tags'][]=$tag->tag;
550 $entry['description'] = $entry['content'];
551 $entry['pubDate'] = common_date_rfc2822($notice->created);
552 $entry['guid'] = $entry['link'];
555 $notloc = Notice_location::locFromStored($notice);
556 // This is the format that GeoJSON expects stuff to be in.
557 // showGeoRSS() below uses it for XML output, so we reuse it
558 $entry['geo'] = array('type' => 'Point',
559 'coordinates' => array((float) $notloc->lat,
560 (float) $notloc->lon));
561 } catch (ServerException $e) {
562 $entry['geo'] = null;
565 Event::handle('EndRssEntryArray', array($notice, &$entry));
571 function twitterRelationshipArray($source, $target)
573 $relationship = array();
575 $relationship['source'] =
576 $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
577 $relationship['target'] =
578 $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
580 return array('relationship' => $relationship);
583 function relationshipDetailsArray(Profile $source, Profile $target)
587 $details['screen_name'] = $source->getNickname();
588 $details['followed_by'] = $target->isSubscribed($source);
591 $sub = Subscription::getSubscription($source, $target);
592 $details['following'] = true;
593 $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
594 } catch (NoResultException $e) {
595 $details['following'] = false;
596 $details['notifications_enabled'] = false;
599 $details['blocking'] = $source->hasBlocked($target);
600 $details['id'] = intval($source->id);
605 function showTwitterXmlRelationship($relationship)
607 $this->elementStart('relationship');
609 foreach($relationship as $element => $value) {
610 if ($element == 'source' || $element == 'target') {
611 $this->elementStart($element);
612 $this->showXmlRelationshipDetails($value);
613 $this->elementEnd($element);
617 $this->elementEnd('relationship');
620 function showXmlRelationshipDetails($details)
622 foreach($details as $element => $value) {
623 $this->element($element, null, $value);
627 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
631 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
633 $this->elementStart($tag, $attrs);
634 foreach($twitter_status as $element => $value) {
637 $this->showTwitterXmlUser($twitter_status['user']);
640 $this->element($element, null, common_xml_safe_str($value));
643 $this->showXmlAttachments($twitter_status['attachments']);
646 $this->showGeoXML($value);
648 case 'retweeted_status':
649 // FIXME: MOVE TO SHARE PLUGIN
650 $this->showTwitterXmlStatus($value, 'retweeted_status');
653 if (strncmp($element, 'statusnet_', 10) == 0) {
654 if ($element === 'statusnet_in_groups' && is_array($value)) {
655 // QVITTERFIX because it would cause an array to be sent as $value
656 // THIS IS UNDOCUMENTED AND SHOULD NEVER BE RELIED UPON (qvitter uses json output)
657 $value = json_encode($value);
659 $this->element('statusnet:'.substr($element, 10), null, $value);
661 $this->element($element, null, $value);
665 $this->elementEnd($tag);
668 function showTwitterXmlGroup($twitter_group)
670 $this->elementStart('group');
671 foreach($twitter_group as $element => $value) {
672 $this->element($element, null, $value);
674 $this->elementEnd('group');
677 function showTwitterXmlList($twitter_list)
679 $this->elementStart('list');
680 foreach($twitter_list as $element => $value) {
681 if($element == 'user') {
682 $this->showTwitterXmlUser($value, 'user');
685 $this->element($element, null, $value);
688 $this->elementEnd('list');
691 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
695 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
697 $this->elementStart($role, $attrs);
698 foreach($twitter_user as $element => $value) {
699 if ($element == 'status') {
700 $this->showTwitterXmlStatus($twitter_user['status']);
701 } else if (strncmp($element, 'statusnet_', 10) == 0) {
702 $this->element('statusnet:'.substr($element, 10), null, $value);
704 $this->element($element, null, $value);
707 $this->elementEnd($role);
710 function showXmlAttachments($attachments) {
711 if (!empty($attachments)) {
712 $this->elementStart('attachments', array('type' => 'array'));
713 foreach ($attachments as $attachment) {
715 $attrs['url'] = $attachment['url'];
716 $attrs['mimetype'] = $attachment['mimetype'];
717 $attrs['size'] = $attachment['size'];
718 $this->element('enclosure', $attrs, '');
720 $this->elementEnd('attachments');
724 function showGeoXML($geo)
728 $this->element('geo');
730 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
731 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
732 $this->elementEnd('geo');
736 function showGeoRSS($geo)
742 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
747 function showTwitterRssItem($entry)
749 $this->elementStart('item');
750 $this->element('title', null, $entry['title']);
751 $this->element('description', null, $entry['description']);
752 $this->element('pubDate', null, $entry['pubDate']);
753 $this->element('guid', null, $entry['guid']);
754 $this->element('link', null, $entry['link']);
756 // RSS only supports 1 enclosure per item
757 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
758 $enclosure = $entry['enclosures'][0];
759 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
762 if(array_key_exists('tags', $entry)){
763 foreach($entry['tags'] as $tag){
764 $this->element('category', null,$tag);
768 $this->showGeoRSS($entry['geo']);
769 $this->elementEnd('item');
772 function showJsonObjects($objects)
774 $json_objects = json_encode($objects);
775 if($json_objects === false) {
776 $this->clientError(_('JSON encoding failed. Error: ').json_last_error_msg());
783 function showSingleXmlStatus($notice)
785 $this->initDocument('xml');
786 $twitter_status = $this->twitterStatusArray($notice);
787 $this->showTwitterXmlStatus($twitter_status, 'status', true);
788 $this->endDocument('xml');
791 function showSingleAtomStatus($notice)
793 header('Content-Type: application/atom+xml;type=entry;charset="utf-8"');
794 print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
795 print $notice->asAtomEntry(true, true, true, $this->scoped);
798 function show_single_json_status($notice)
800 $this->initDocument('json');
801 $status = $this->twitterStatusArray($notice);
802 $this->showJsonObjects($status);
803 $this->endDocument('json');
806 function showXmlTimeline($notice)
808 $this->initDocument('xml');
809 $this->elementStart('statuses', array('type' => 'array',
810 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
812 if (is_array($notice)) {
813 //FIXME: make everything calling showJsonTimeline use only Notice objects
815 foreach ($notice as $n) {
816 $ids[] = $n->getID();
818 $notice = Notice::multiGet('id', $ids);
821 while ($notice->fetch()) {
823 $twitter_status = $this->twitterStatusArray($notice);
824 $this->showTwitterXmlStatus($twitter_status);
825 } catch (Exception $e) {
826 common_log(LOG_ERR, $e->getMessage());
831 $this->elementEnd('statuses');
832 $this->endDocument('xml');
835 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
837 $this->initDocument('rss');
839 $this->element('title', null, $title);
840 $this->element('link', null, $link);
842 if (!is_null($self)) {
846 'type' => 'application/rss+xml',
853 if (!is_null($suplink)) {
854 // For FriendFeed's SUP protocol
855 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
856 'rel' => 'http://api.friendfeed.com/2008/03#sup',
858 'type' => 'application/json'));
861 if (!is_null($logo)) {
862 $this->elementStart('image');
863 $this->element('link', null, $link);
864 $this->element('title', null, $title);
865 $this->element('url', null, $logo);
866 $this->elementEnd('image');
869 $this->element('description', null, $subtitle);
870 $this->element('language', null, 'en-us');
871 $this->element('ttl', null, '40');
873 if (is_array($notice)) {
874 //FIXME: make everything calling showJsonTimeline use only Notice objects
876 foreach ($notice as $n) {
877 $ids[] = $n->getID();
879 $notice = Notice::multiGet('id', $ids);
882 while ($notice->fetch()) {
884 $entry = $this->twitterRssEntryArray($notice);
885 $this->showTwitterRssItem($entry);
886 } catch (Exception $e) {
887 common_log(LOG_ERR, $e->getMessage());
888 // continue on exceptions
892 $this->endTwitterRss();
895 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
897 $this->initDocument('atom');
899 $this->element('title', null, $title);
900 $this->element('id', null, $id);
901 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
903 if (!is_null($logo)) {
904 $this->element('logo',null,$logo);
907 if (!is_null($suplink)) {
908 // For FriendFeed's SUP protocol
909 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
911 'type' => 'application/json'));
914 if (!is_null($selfuri)) {
915 $this->element('link', array('href' => $selfuri,
916 'rel' => 'self', 'type' => 'application/atom+xml'), null);
919 $this->element('updated', null, common_date_iso8601('now'));
920 $this->element('subtitle', null, $subtitle);
922 if (is_array($notice)) {
923 //FIXME: make everything calling showJsonTimeline use only Notice objects
925 foreach ($notice as $n) {
926 $ids[] = $n->getID();
928 $notice = Notice::multiGet('id', $ids);
931 while ($notice->fetch()) {
933 $this->raw($notice->asAtomEntry());
934 } catch (Exception $e) {
935 common_log(LOG_ERR, $e->getMessage());
940 $this->endDocument('atom');
943 function showRssGroups($group, $title, $link, $subtitle)
945 $this->initDocument('rss');
947 $this->element('title', null, $title);
948 $this->element('link', null, $link);
949 $this->element('description', null, $subtitle);
950 $this->element('language', null, 'en-us');
951 $this->element('ttl', null, '40');
953 if (is_array($group)) {
954 foreach ($group as $g) {
955 $twitter_group = $this->twitterRssGroupArray($g);
956 $this->showTwitterRssItem($twitter_group);
959 while ($group->fetch()) {
960 $twitter_group = $this->twitterRssGroupArray($group);
961 $this->showTwitterRssItem($twitter_group);
965 $this->endTwitterRss();
968 function showTwitterAtomEntry($entry)
970 $this->elementStart('entry');
971 $this->element('title', null, common_xml_safe_str($entry['title']));
974 array('type' => 'html'),
975 common_xml_safe_str($entry['content'])
977 $this->element('id', null, $entry['id']);
978 $this->element('published', null, $entry['published']);
979 $this->element('updated', null, $entry['updated']);
980 $this->element('link', array('type' => 'text/html',
981 'href' => $entry['link'],
982 'rel' => 'alternate'));
983 $this->element('link', array('type' => $entry['avatar-type'],
984 'href' => $entry['avatar'],
986 $this->elementStart('author');
988 $this->element('name', null, $entry['author-name']);
989 $this->element('uri', null, $entry['author-uri']);
991 $this->elementEnd('author');
992 $this->elementEnd('entry');
995 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
997 $this->initDocument('atom');
999 $this->element('title', null, common_xml_safe_str($title));
1000 $this->element('id', null, $id);
1001 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
1003 if (!is_null($selfuri)) {
1004 $this->element('link', array('href' => $selfuri,
1005 'rel' => 'self', 'type' => 'application/atom+xml'), null);
1008 $this->element('updated', null, common_date_iso8601('now'));
1009 $this->element('subtitle', null, common_xml_safe_str($subtitle));
1011 if (is_array($group)) {
1012 foreach ($group as $g) {
1013 $this->raw($g->asAtomEntry());
1016 while ($group->fetch()) {
1017 $this->raw($group->asAtomEntry());
1021 $this->endDocument('atom');
1025 function showJsonTimeline($notice)
1027 $this->initDocument('json');
1029 $statuses = array();
1031 if (is_array($notice)) {
1032 //FIXME: make everything calling showJsonTimeline use only Notice objects
1034 foreach ($notice as $n) {
1035 $ids[] = $n->getID();
1037 $notice = Notice::multiGet('id', $ids);
1040 while ($notice->fetch()) {
1042 $twitter_status = $this->twitterStatusArray($notice);
1043 array_push($statuses, $twitter_status);
1044 } catch (Exception $e) {
1045 common_log(LOG_ERR, $e->getMessage());
1050 $this->showJsonObjects($statuses);
1052 $this->endDocument('json');
1055 function showJsonGroups($group)
1057 $this->initDocument('json');
1061 if (is_array($group)) {
1062 foreach ($group as $g) {
1063 $twitter_group = $this->twitterGroupArray($g);
1064 array_push($groups, $twitter_group);
1067 while ($group->fetch()) {
1068 $twitter_group = $this->twitterGroupArray($group);
1069 array_push($groups, $twitter_group);
1073 $this->showJsonObjects($groups);
1075 $this->endDocument('json');
1078 function showXmlGroups($group)
1081 $this->initDocument('xml');
1082 $this->elementStart('groups', array('type' => 'array'));
1084 if (is_array($group)) {
1085 foreach ($group as $g) {
1086 $twitter_group = $this->twitterGroupArray($g);
1087 $this->showTwitterXmlGroup($twitter_group);
1090 while ($group->fetch()) {
1091 $twitter_group = $this->twitterGroupArray($group);
1092 $this->showTwitterXmlGroup($twitter_group);
1096 $this->elementEnd('groups');
1097 $this->endDocument('xml');
1100 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1103 $this->initDocument('xml');
1104 $this->elementStart('lists_list');
1105 $this->elementStart('lists', array('type' => 'array'));
1107 if (is_array($list)) {
1108 foreach ($list as $l) {
1109 $twitter_list = $this->twitterListArray($l);
1110 $this->showTwitterXmlList($twitter_list);
1113 while ($list->fetch()) {
1114 $twitter_list = $this->twitterListArray($list);
1115 $this->showTwitterXmlList($twitter_list);
1119 $this->elementEnd('lists');
1121 $this->element('next_cursor', null, $next_cursor);
1122 $this->element('previous_cursor', null, $prev_cursor);
1124 $this->elementEnd('lists_list');
1125 $this->endDocument('xml');
1128 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1130 $this->initDocument('json');
1134 if (is_array($list)) {
1135 foreach ($list as $l) {
1136 $twitter_list = $this->twitterListArray($l);
1137 array_push($lists, $twitter_list);
1140 while ($list->fetch()) {
1141 $twitter_list = $this->twitterListArray($list);
1142 array_push($lists, $twitter_list);
1146 $lists_list = array(
1148 'next_cursor' => $next_cursor,
1149 'next_cursor_str' => strval($next_cursor),
1150 'previous_cursor' => $prev_cursor,
1151 'previous_cursor_str' => strval($prev_cursor)
1154 $this->showJsonObjects($lists_list);
1156 $this->endDocument('json');
1159 function showTwitterXmlUsers($user)
1161 $this->initDocument('xml');
1162 $this->elementStart('users', array('type' => 'array',
1163 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1165 if (is_array($user)) {
1166 foreach ($user as $u) {
1167 $twitter_user = $this->twitterUserArray($u);
1168 $this->showTwitterXmlUser($twitter_user);
1171 while ($user->fetch()) {
1172 $twitter_user = $this->twitterUserArray($user);
1173 $this->showTwitterXmlUser($twitter_user);
1177 $this->elementEnd('users');
1178 $this->endDocument('xml');
1181 function showJsonUsers($user)
1183 $this->initDocument('json');
1187 if (is_array($user)) {
1188 foreach ($user as $u) {
1189 $twitter_user = $this->twitterUserArray($u);
1190 array_push($users, $twitter_user);
1193 while ($user->fetch()) {
1194 $twitter_user = $this->twitterUserArray($user);
1195 array_push($users, $twitter_user);
1199 $this->showJsonObjects($users);
1201 $this->endDocument('json');
1204 function showSingleJsonGroup($group)
1206 $this->initDocument('json');
1207 $twitter_group = $this->twitterGroupArray($group);
1208 $this->showJsonObjects($twitter_group);
1209 $this->endDocument('json');
1212 function showSingleXmlGroup($group)
1214 $this->initDocument('xml');
1215 $twitter_group = $this->twitterGroupArray($group);
1216 $this->showTwitterXmlGroup($twitter_group);
1217 $this->endDocument('xml');
1220 function showSingleJsonList($list)
1222 $this->initDocument('json');
1223 $twitter_list = $this->twitterListArray($list);
1224 $this->showJsonObjects($twitter_list);
1225 $this->endDocument('json');
1228 function showSingleXmlList($list)
1230 $this->initDocument('xml');
1231 $twitter_list = $this->twitterListArray($list);
1232 $this->showTwitterXmlList($twitter_list);
1233 $this->endDocument('xml');
1236 static function dateTwitter($dt)
1238 $dateStr = date('d F Y H:i:s', strtotime($dt));
1239 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1240 $d->setTimezone(new DateTimeZone(common_timezone()));
1241 return $d->format('D M d H:i:s O Y');
1244 function initDocument($type='xml')
1248 header('Content-Type: application/xml; charset=utf-8');
1252 header('Content-Type: application/json; charset=utf-8');
1254 // Check for JSONP callback
1255 if (isset($this->callback)) {
1256 print $this->callback . '(';
1260 header("Content-Type: application/rss+xml; charset=utf-8");
1261 $this->initTwitterRss();
1264 header('Content-Type: application/atom+xml; charset=utf-8');
1265 $this->initTwitterAtom();
1268 // TRANS: Client error on an API request with an unsupported data format.
1269 $this->clientError(_('Not a supported data format.'));
1275 function endDocument($type='xml')
1282 // Check for JSONP callback
1283 if (isset($this->callback)) {
1288 $this->endTwitterRss();
1291 $this->endTwitterRss();
1294 // TRANS: Client error on an API request with an unsupported data format.
1295 $this->clientError(_('Not a supported data format.'));
1300 function initTwitterRss()
1303 $this->elementStart(
1307 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1308 'xmlns:georss' => 'http://www.georss.org/georss'
1311 $this->elementStart('channel');
1312 Event::handle('StartApiRss', array($this));
1315 function endTwitterRss()
1317 $this->elementEnd('channel');
1318 $this->elementEnd('rss');
1322 function initTwitterAtom()
1325 // FIXME: don't hardcode the language here!
1326 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1327 'xml:lang' => 'en-US',
1328 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1331 function endTwitterAtom()
1333 $this->elementEnd('feed');
1337 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1339 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1340 switch ($content_type) {
1342 $this->showTwitterXmlUser($profile_array);
1345 $this->showJsonObjects($profile_array);
1348 // TRANS: Client error on an API request with an unsupported data format.
1349 $this->clientError(_('Not a supported data format.'));
1354 private static function is_decimal($str)
1356 return preg_match('/^[0-9]+$/', $str);
1359 function getTargetUser($id)
1362 // Twitter supports these other ways of passing the user ID
1363 if (self::is_decimal($this->arg('id'))) {
1364 return User::getKV($this->arg('id'));
1365 } else if ($this->arg('id')) {
1366 $nickname = common_canonical_nickname($this->arg('id'));
1367 return User::getKV('nickname', $nickname);
1368 } else if ($this->arg('user_id')) {
1369 // This is to ensure that a non-numeric user_id still
1370 // overrides screen_name even if it doesn't get used
1371 if (self::is_decimal($this->arg('user_id'))) {
1372 return User::getKV('id', $this->arg('user_id'));
1374 } else if ($this->arg('screen_name')) {
1375 $nickname = common_canonical_nickname($this->arg('screen_name'));
1376 return User::getKV('nickname', $nickname);
1378 // Fall back to trying the currently authenticated user
1379 return $this->scoped->getUser();
1382 } else if (self::is_decimal($id)) {
1383 return User::getKV($id);
1385 $nickname = common_canonical_nickname($id);
1386 return User::getKV('nickname', $nickname);
1390 function getTargetProfile($id)
1394 // Twitter supports these other ways of passing the user ID
1395 if (self::is_decimal($this->arg('id'))) {
1396 return Profile::getKV($this->arg('id'));
1397 } else if ($this->arg('id')) {
1398 // Screen names currently can only uniquely identify a local user.
1399 $nickname = common_canonical_nickname($this->arg('id'));
1400 $user = User::getKV('nickname', $nickname);
1401 return $user ? $user->getProfile() : null;
1402 } else if ($this->arg('user_id')) {
1403 // This is to ensure that a non-numeric user_id still
1404 // overrides screen_name even if it doesn't get used
1405 if (self::is_decimal($this->arg('user_id'))) {
1406 return Profile::getKV('id', $this->arg('user_id'));
1408 } elseif (mb_strlen($this->arg('screen_name')) > 0) {
1409 $nickname = common_canonical_nickname($this->arg('screen_name'));
1410 $user = User::getByNickname($nickname);
1411 return $user->getProfile();
1413 // Fall back to trying the currently authenticated user
1414 return $this->scoped;
1416 } else if (self::is_decimal($id) && intval($id) > 0) {
1417 return Profile::getByID($id);
1419 // FIXME: check if isAcct to identify remote profiles and not just local nicknames
1420 $nickname = common_canonical_nickname($id);
1421 $user = User::getByNickname($nickname);
1422 return $user->getProfile();
1426 function getTargetGroup($id)
1429 if (self::is_decimal($this->arg('id'))) {
1430 return User_group::getKV('id', $this->arg('id'));
1431 } else if ($this->arg('id')) {
1432 return User_group::getForNickname($this->arg('id'));
1433 } else if ($this->arg('group_id')) {
1434 // This is to ensure that a non-numeric group_id still
1435 // overrides group_name even if it doesn't get used
1436 if (self::is_decimal($this->arg('group_id'))) {
1437 return User_group::getKV('id', $this->arg('group_id'));
1439 } else if ($this->arg('group_name')) {
1440 return User_group::getForNickname($this->arg('group_name'));
1443 } else if (self::is_decimal($id)) {
1444 return User_group::getKV('id', $id);
1445 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1446 return User_group::getKV('uri', urldecode($this->arg('uri')));
1448 return User_group::getForNickname($id);
1452 function getTargetList($user=null, $id=null)
1454 $tagger = $this->getTargetUser($user);
1458 $id = $this->arg('id');
1462 if (is_numeric($id)) {
1463 $list = Profile_list::getKV('id', $id);
1465 // only if the list with the id belongs to the tagger
1466 if(empty($list) || $list->tagger != $tagger->id) {
1471 $tag = common_canonical_tag($id);
1472 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1475 if (!empty($list) && $list->private) {
1476 if ($this->scoped->id == $list->tagger) {
1487 * Returns query argument or default value if not found. Certain
1488 * parameters used throughout the API are lightly scrubbed and
1489 * bounds checked. This overrides Action::arg().
1491 * @param string $key requested argument
1492 * @param string $def default value to return if $key is not provided
1496 function arg($key, $def=null)
1498 // XXX: Do even more input validation/scrubbing?
1500 if (array_key_exists($key, $this->args)) {
1503 $page = (int)$this->args['page'];
1504 return ($page < 1) ? 1 : $page;
1506 $count = (int)$this->args['count'];
1509 } elseif ($count > 200) {
1515 $since_id = (int)$this->args['since_id'];
1516 return ($since_id < 1) ? 0 : $since_id;
1518 $max_id = (int)$this->args['max_id'];
1519 return ($max_id < 1) ? 0 : $max_id;
1521 return parent::arg($key, $def);
1529 * Calculate the complete URI that called up this action. Used for
1530 * Atom rel="self" links. Warning: this is funky.
1532 * @return string URL a URL suitable for rel="self" Atom links
1534 function getSelfUri()
1536 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1538 $id = $this->arg('id');
1539 $aargs = array('format' => $this->format);
1544 $user = $this->arg('user');
1545 if (!empty($user)) {
1546 $aargs['user'] = $user;
1549 $tag = $this->arg('tag');
1551 $aargs['tag'] = $tag;
1554 parse_str($_SERVER['QUERY_STRING'], $params);
1556 if (!empty($params)) {
1557 unset($params['p']);
1558 $pstring = http_build_query($params);
1561 $uri = common_local_url($action, $aargs);
1563 if (!empty($pstring)) {
1564 $uri .= '?' . $pstring;