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'] = 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'] = 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['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
278 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
279 } catch (NoResultException $e) {
280 // well, the values are already false...
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) {
334 $twitter_status['in_reply_to_status_id'] = $in_reply_to;
338 $ns = $notice->getSource();
339 if ($ns instanceof Notice_source) {
340 if (!empty($ns->name) && !empty($ns->url)) {
341 $source = '<a href="'
342 . htmlspecialchars($ns->url)
343 . '" rel="nofollow">'
344 . htmlspecialchars($ns->name)
351 $twitter_status['uri'] = $notice->getUri();
352 $twitter_status['source'] = $source;
353 $twitter_status['id'] = intval($notice->id);
355 $replier_profile = null;
357 if ($notice->reply_to) {
358 $reply = Notice::getKV(intval($notice->reply_to));
360 $replier_profile = $reply->getProfile();
364 $twitter_status['in_reply_to_user_id'] =
365 ($replier_profile) ? intval($replier_profile->id) : null;
366 $twitter_status['in_reply_to_screen_name'] =
367 ($replier_profile) ? $replier_profile->nickname : null;
370 $notloc = Notice_location::locFromStored($notice);
371 // This is the format that GeoJSON expects stuff to be in
372 $twitter_status['geo'] = array('type' => 'Point',
373 'coordinates' => array((float) $notloc->lat,
374 (float) $notloc->lon));
375 } catch (ServerException $e) {
376 $twitter_status['geo'] = null;
380 $attachments = $notice->attachments();
382 if (!empty($attachments)) {
384 $twitter_status['attachments'] = array();
386 foreach ($attachments as $attachment) {
388 $enclosure_o = $attachment->getEnclosure();
389 $enclosure = array();
390 $enclosure['url'] = $enclosure_o->url;
391 $enclosure['mimetype'] = $enclosure_o->mimetype;
392 $enclosure['size'] = $enclosure_o->size;
393 $twitter_status['attachments'][] = $enclosure;
394 } catch (ServerException $e) {
395 // There was not enough metadata available
400 if ($include_user && $profile) {
401 // Don't get notice (recursive!)
402 $twitter_user = $this->twitterUserArray($profile, false);
403 $twitter_status['user'] = $twitter_user;
406 // StatusNet-specific
408 $twitter_status['statusnet_html'] = $notice->rendered;
409 $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
411 // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
412 Event::handle('NoticeSimpleStatusArray', array($notice, &$twitter_status, $this->scoped,
413 array('include_user'=>$include_user)));
415 return $twitter_status;
418 function twitterGroupArray($group)
420 $twitter_group = array();
422 $twitter_group['id'] = intval($group->id);
423 $twitter_group['url'] = $group->permalink();
424 $twitter_group['nickname'] = $group->nickname;
425 $twitter_group['fullname'] = $group->fullname;
427 if ($this->scoped instanceof Profile) {
428 $twitter_group['member'] = $this->scoped->isMember($group);
429 $twitter_group['blocked'] = Group_block::isBlocked(
435 $twitter_group['admin_count'] = $group->getAdminCount();
436 $twitter_group['member_count'] = $group->getMemberCount();
437 $twitter_group['original_logo'] = $group->original_logo;
438 $twitter_group['homepage_logo'] = $group->homepage_logo;
439 $twitter_group['stream_logo'] = $group->stream_logo;
440 $twitter_group['mini_logo'] = $group->mini_logo;
441 $twitter_group['homepage'] = $group->homepage;
442 $twitter_group['description'] = $group->description;
443 $twitter_group['location'] = $group->location;
444 $twitter_group['created'] = self::dateTwitter($group->created);
445 $twitter_group['modified'] = self::dateTwitter($group->modified);
447 return $twitter_group;
450 function twitterRssGroupArray($group)
453 $entry['content']=$group->description;
454 $entry['title']=$group->nickname;
455 $entry['link']=$group->permalink();
456 $entry['published']=common_date_iso8601($group->created);
457 $entry['updated']==common_date_iso8601($group->modified);
458 $taguribase = common_config('integration', 'groupuri');
459 $entry['id'] = "group:$groupuribase:$entry[link]";
461 $entry['description'] = $entry['content'];
462 $entry['pubDate'] = common_date_rfc2822($group->created);
463 $entry['guid'] = $entry['link'];
468 function twitterListArray($list)
470 $profile = Profile::getKV('id', $list->tagger);
472 $twitter_list = array();
473 $twitter_list['id'] = $list->id;
474 $twitter_list['name'] = $list->tag;
475 $twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
476 $twitter_list['slug'] = $list->tag;
477 $twitter_list['description'] = $list->description;
478 $twitter_list['subscriber_count'] = $list->subscriberCount();
479 $twitter_list['member_count'] = $list->taggedCount();
480 $twitter_list['uri'] = $list->getUri();
482 if ($this->scoped instanceof Profile) {
483 $twitter_list['following'] = $list->hasSubscriber($this->scoped);
485 $twitter_list['following'] = false;
488 $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
489 $twitter_list['user'] = $this->twitterUserArray($profile, false);
491 return $twitter_list;
494 function twitterRssEntryArray($notice)
498 if (Event::handle('StartRssEntryArray', array($notice, &$entry))) {
499 $profile = $notice->getProfile();
501 // We trim() to avoid extraneous whitespace in the output
503 $entry['content'] = common_xml_safe_str(trim($notice->rendered));
504 $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
505 $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
506 $entry['published'] = common_date_iso8601($notice->created);
508 $taguribase = TagURI::base();
509 $entry['id'] = "tag:$taguribase:$entry[link]";
511 $entry['updated'] = $entry['published'];
512 $entry['author'] = $profile->getBestName();
515 $attachments = $notice->attachments();
516 $enclosures = array();
518 foreach ($attachments as $attachment) {
520 $enclosure_o = $attachment->getEnclosure();
521 $enclosure = array();
522 $enclosure['url'] = $enclosure_o->url;
523 $enclosure['mimetype'] = $enclosure_o->mimetype;
524 $enclosure['size'] = $enclosure_o->size;
525 $enclosures[] = $enclosure;
526 } catch (ServerException $e) {
527 // There was not enough metadata available
531 if (!empty($enclosures)) {
532 $entry['enclosures'] = $enclosures;
536 $tag = new Notice_tag();
537 $tag->notice_id = $notice->id;
539 $entry['tags']=array();
540 while ($tag->fetch()) {
541 $entry['tags'][]=$tag->tag;
547 $entry['description'] = $entry['content'];
548 $entry['pubDate'] = common_date_rfc2822($notice->created);
549 $entry['guid'] = $entry['link'];
552 $notloc = Notice_location::locFromStored($notice);
553 // This is the format that GeoJSON expects stuff to be in.
554 // showGeoRSS() below uses it for XML output, so we reuse it
555 $entry['geo'] = array('type' => 'Point',
556 'coordinates' => array((float) $notloc->lat,
557 (float) $notloc->lon));
558 } catch (ServerException $e) {
559 $entry['geo'] = null;
562 Event::handle('EndRssEntryArray', array($notice, &$entry));
568 function twitterRelationshipArray($source, $target)
570 $relationship = array();
572 $relationship['source'] =
573 $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
574 $relationship['target'] =
575 $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
577 return array('relationship' => $relationship);
580 function relationshipDetailsArray(Profile $source, Profile $target)
584 $details['screen_name'] = $source->getNickname();
585 $details['followed_by'] = $target->isSubscribed($source);
588 $sub = Subscription::getSubscription($source, $target);
589 $details['following'] = true;
590 $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
591 } catch (NoResultException $e) {
592 $details['following'] = false;
593 $details['notifications_enabled'] = false;
596 $details['blocking'] = $source->hasBlocked($target);
597 $details['id'] = intval($source->id);
602 function showTwitterXmlRelationship($relationship)
604 $this->elementStart('relationship');
606 foreach($relationship as $element => $value) {
607 if ($element == 'source' || $element == 'target') {
608 $this->elementStart($element);
609 $this->showXmlRelationshipDetails($value);
610 $this->elementEnd($element);
614 $this->elementEnd('relationship');
617 function showXmlRelationshipDetails($details)
619 foreach($details as $element => $value) {
620 $this->element($element, null, $value);
624 function showTwitterXmlStatus($twitter_status, $tag='status', $namespaces=false)
628 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
630 $this->elementStart($tag, $attrs);
631 foreach($twitter_status as $element => $value) {
634 $this->showTwitterXmlUser($twitter_status['user']);
637 $this->element($element, null, common_xml_safe_str($value));
640 $this->showXmlAttachments($twitter_status['attachments']);
643 $this->showGeoXML($value);
645 case 'retweeted_status':
646 // FIXME: MOVE TO SHARE PLUGIN
647 $this->showTwitterXmlStatus($value, 'retweeted_status');
650 if (strncmp($element, 'statusnet_', 10) == 0) {
651 $this->element('statusnet:'.substr($element, 10), null, $value);
653 $this->element($element, null, $value);
657 $this->elementEnd($tag);
660 function showTwitterXmlGroup($twitter_group)
662 $this->elementStart('group');
663 foreach($twitter_group as $element => $value) {
664 $this->element($element, null, $value);
666 $this->elementEnd('group');
669 function showTwitterXmlList($twitter_list)
671 $this->elementStart('list');
672 foreach($twitter_list as $element => $value) {
673 if($element == 'user') {
674 $this->showTwitterXmlUser($value, 'user');
677 $this->element($element, null, $value);
680 $this->elementEnd('list');
683 function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
687 $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
689 $this->elementStart($role, $attrs);
690 foreach($twitter_user as $element => $value) {
691 if ($element == 'status') {
692 $this->showTwitterXmlStatus($twitter_user['status']);
693 } else if (strncmp($element, 'statusnet_', 10) == 0) {
694 $this->element('statusnet:'.substr($element, 10), null, $value);
696 $this->element($element, null, $value);
699 $this->elementEnd($role);
702 function showXmlAttachments($attachments) {
703 if (!empty($attachments)) {
704 $this->elementStart('attachments', array('type' => 'array'));
705 foreach ($attachments as $attachment) {
707 $attrs['url'] = $attachment['url'];
708 $attrs['mimetype'] = $attachment['mimetype'];
709 $attrs['size'] = $attachment['size'];
710 $this->element('enclosure', $attrs, '');
712 $this->elementEnd('attachments');
716 function showGeoXML($geo)
720 $this->element('geo');
722 $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
723 $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
724 $this->elementEnd('geo');
728 function showGeoRSS($geo)
734 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
739 function showTwitterRssItem($entry)
741 $this->elementStart('item');
742 $this->element('title', null, $entry['title']);
743 $this->element('description', null, $entry['description']);
744 $this->element('pubDate', null, $entry['pubDate']);
745 $this->element('guid', null, $entry['guid']);
746 $this->element('link', null, $entry['link']);
748 // RSS only supports 1 enclosure per item
749 if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
750 $enclosure = $entry['enclosures'][0];
751 $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
754 if(array_key_exists('tags', $entry)){
755 foreach($entry['tags'] as $tag){
756 $this->element('category', null,$tag);
760 $this->showGeoRSS($entry['geo']);
761 $this->elementEnd('item');
764 function showJsonObjects($objects)
766 print(json_encode($objects));
769 function showSingleXmlStatus($notice)
771 $this->initDocument('xml');
772 $twitter_status = $this->twitterStatusArray($notice);
773 $this->showTwitterXmlStatus($twitter_status, 'status', true);
774 $this->endDocument('xml');
777 function showSingleAtomStatus($notice)
779 header('Content-Type: application/atom+xml; charset=utf-8');
780 print $notice->asAtomEntry(true, true, true, $this->scoped);
783 function show_single_json_status($notice)
785 $this->initDocument('json');
786 $status = $this->twitterStatusArray($notice);
787 $this->showJsonObjects($status);
788 $this->endDocument('json');
791 function showXmlTimeline($notice)
793 $this->initDocument('xml');
794 $this->elementStart('statuses', array('type' => 'array',
795 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
797 if (is_array($notice)) {
798 //FIXME: make everything calling showJsonTimeline use only Notice objects
799 common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!');
801 foreach ($notice as $n) {
802 $ids[] = $n->getID();
804 $notice = Notice::multiGet('id', $ids);
807 while ($notice->fetch()) {
809 $twitter_status = $this->twitterStatusArray($notice);
810 $this->showTwitterXmlStatus($twitter_status);
811 } catch (Exception $e) {
812 common_log(LOG_ERR, $e->getMessage());
817 $this->elementEnd('statuses');
818 $this->endDocument('xml');
821 function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
823 $this->initDocument('rss');
825 $this->element('title', null, $title);
826 $this->element('link', null, $link);
828 if (!is_null($self)) {
832 'type' => 'application/rss+xml',
839 if (!is_null($suplink)) {
840 // For FriendFeed's SUP protocol
841 $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
842 'rel' => 'http://api.friendfeed.com/2008/03#sup',
844 'type' => 'application/json'));
847 if (!is_null($logo)) {
848 $this->elementStart('image');
849 $this->element('link', null, $link);
850 $this->element('title', null, $title);
851 $this->element('url', null, $logo);
852 $this->elementEnd('image');
855 $this->element('description', null, $subtitle);
856 $this->element('language', null, 'en-us');
857 $this->element('ttl', null, '40');
859 if (is_array($notice)) {
860 //FIXME: make everything calling showJsonTimeline use only Notice objects
861 common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!');
863 foreach ($notice as $n) {
864 $ids[] = $n->getID();
866 $notice = Notice::multiGet('id', $ids);
869 while ($notice->fetch()) {
871 $entry = $this->twitterRssEntryArray($notice);
872 $this->showTwitterRssItem($entry);
873 } catch (Exception $e) {
874 common_log(LOG_ERR, $e->getMessage());
875 // continue on exceptions
879 $this->endTwitterRss();
882 function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
884 $this->initDocument('atom');
886 $this->element('title', null, $title);
887 $this->element('id', null, $id);
888 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
890 if (!is_null($logo)) {
891 $this->element('logo',null,$logo);
894 if (!is_null($suplink)) {
895 // For FriendFeed's SUP protocol
896 $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
898 'type' => 'application/json'));
901 if (!is_null($selfuri)) {
902 $this->element('link', array('href' => $selfuri,
903 'rel' => 'self', 'type' => 'application/atom+xml'), null);
906 $this->element('updated', null, common_date_iso8601('now'));
907 $this->element('subtitle', null, $subtitle);
909 if (is_array($notice)) {
910 //FIXME: make everything calling showJsonTimeline use only Notice objects
911 common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!');
913 foreach ($notice as $n) {
914 $ids[] = $n->getID();
916 $notice = Notice::multiGet('id', $ids);
919 while ($notice->fetch()) {
921 $this->raw($notice->asAtomEntry());
922 } catch (Exception $e) {
923 common_log(LOG_ERR, $e->getMessage());
928 $this->endDocument('atom');
931 function showRssGroups($group, $title, $link, $subtitle)
933 $this->initDocument('rss');
935 $this->element('title', null, $title);
936 $this->element('link', null, $link);
937 $this->element('description', null, $subtitle);
938 $this->element('language', null, 'en-us');
939 $this->element('ttl', null, '40');
941 if (is_array($group)) {
942 foreach ($group as $g) {
943 $twitter_group = $this->twitterRssGroupArray($g);
944 $this->showTwitterRssItem($twitter_group);
947 while ($group->fetch()) {
948 $twitter_group = $this->twitterRssGroupArray($group);
949 $this->showTwitterRssItem($twitter_group);
953 $this->endTwitterRss();
956 function showTwitterAtomEntry($entry)
958 $this->elementStart('entry');
959 $this->element('title', null, common_xml_safe_str($entry['title']));
962 array('type' => 'html'),
963 common_xml_safe_str($entry['content'])
965 $this->element('id', null, $entry['id']);
966 $this->element('published', null, $entry['published']);
967 $this->element('updated', null, $entry['updated']);
968 $this->element('link', array('type' => 'text/html',
969 'href' => $entry['link'],
970 'rel' => 'alternate'));
971 $this->element('link', array('type' => $entry['avatar-type'],
972 'href' => $entry['avatar'],
974 $this->elementStart('author');
976 $this->element('name', null, $entry['author-name']);
977 $this->element('uri', null, $entry['author-uri']);
979 $this->elementEnd('author');
980 $this->elementEnd('entry');
983 function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
985 $this->initDocument('atom');
987 $this->element('title', null, common_xml_safe_str($title));
988 $this->element('id', null, $id);
989 $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
991 if (!is_null($selfuri)) {
992 $this->element('link', array('href' => $selfuri,
993 'rel' => 'self', 'type' => 'application/atom+xml'), null);
996 $this->element('updated', null, common_date_iso8601('now'));
997 $this->element('subtitle', null, common_xml_safe_str($subtitle));
999 if (is_array($group)) {
1000 foreach ($group as $g) {
1001 $this->raw($g->asAtomEntry());
1004 while ($group->fetch()) {
1005 $this->raw($group->asAtomEntry());
1009 $this->endDocument('atom');
1013 function showJsonTimeline($notice)
1015 $this->initDocument('json');
1017 $statuses = array();
1019 if (is_array($notice)) {
1020 //FIXME: make everything calling showJsonTimeline use only Notice objects
1021 common_debug('ArrayWrapper avoidance in progress! Beep boop, make showJsonTimeline only receive Notice objects!');
1023 foreach ($notice as $n) {
1024 $ids[] = $n->getID();
1026 $notice = Notice::multiGet('id', $ids);
1029 while ($notice->fetch()) {
1031 $twitter_status = $this->twitterStatusArray($notice);
1032 array_push($statuses, $twitter_status);
1033 } catch (Exception $e) {
1034 common_log(LOG_ERR, $e->getMessage());
1039 $this->showJsonObjects($statuses);
1041 $this->endDocument('json');
1044 function showJsonGroups($group)
1046 $this->initDocument('json');
1050 if (is_array($group)) {
1051 foreach ($group as $g) {
1052 $twitter_group = $this->twitterGroupArray($g);
1053 array_push($groups, $twitter_group);
1056 while ($group->fetch()) {
1057 $twitter_group = $this->twitterGroupArray($group);
1058 array_push($groups, $twitter_group);
1062 $this->showJsonObjects($groups);
1064 $this->endDocument('json');
1067 function showXmlGroups($group)
1070 $this->initDocument('xml');
1071 $this->elementStart('groups', array('type' => 'array'));
1073 if (is_array($group)) {
1074 foreach ($group as $g) {
1075 $twitter_group = $this->twitterGroupArray($g);
1076 $this->showTwitterXmlGroup($twitter_group);
1079 while ($group->fetch()) {
1080 $twitter_group = $this->twitterGroupArray($group);
1081 $this->showTwitterXmlGroup($twitter_group);
1085 $this->elementEnd('groups');
1086 $this->endDocument('xml');
1089 function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
1092 $this->initDocument('xml');
1093 $this->elementStart('lists_list');
1094 $this->elementStart('lists', array('type' => 'array'));
1096 if (is_array($list)) {
1097 foreach ($list as $l) {
1098 $twitter_list = $this->twitterListArray($l);
1099 $this->showTwitterXmlList($twitter_list);
1102 while ($list->fetch()) {
1103 $twitter_list = $this->twitterListArray($list);
1104 $this->showTwitterXmlList($twitter_list);
1108 $this->elementEnd('lists');
1110 $this->element('next_cursor', null, $next_cursor);
1111 $this->element('previous_cursor', null, $prev_cursor);
1113 $this->elementEnd('lists_list');
1114 $this->endDocument('xml');
1117 function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
1119 $this->initDocument('json');
1123 if (is_array($list)) {
1124 foreach ($list as $l) {
1125 $twitter_list = $this->twitterListArray($l);
1126 array_push($lists, $twitter_list);
1129 while ($list->fetch()) {
1130 $twitter_list = $this->twitterListArray($list);
1131 array_push($lists, $twitter_list);
1135 $lists_list = array(
1137 'next_cursor' => $next_cursor,
1138 'next_cursor_str' => strval($next_cursor),
1139 'previous_cursor' => $prev_cursor,
1140 'previous_cursor_str' => strval($prev_cursor)
1143 $this->showJsonObjects($lists_list);
1145 $this->endDocument('json');
1148 function showTwitterXmlUsers($user)
1150 $this->initDocument('xml');
1151 $this->elementStart('users', array('type' => 'array',
1152 'xmlns:statusnet' => 'http://status.net/schema/api/1/'));
1154 if (is_array($user)) {
1155 foreach ($user as $u) {
1156 $twitter_user = $this->twitterUserArray($u);
1157 $this->showTwitterXmlUser($twitter_user);
1160 while ($user->fetch()) {
1161 $twitter_user = $this->twitterUserArray($user);
1162 $this->showTwitterXmlUser($twitter_user);
1166 $this->elementEnd('users');
1167 $this->endDocument('xml');
1170 function showJsonUsers($user)
1172 $this->initDocument('json');
1176 if (is_array($user)) {
1177 foreach ($user as $u) {
1178 $twitter_user = $this->twitterUserArray($u);
1179 array_push($users, $twitter_user);
1182 while ($user->fetch()) {
1183 $twitter_user = $this->twitterUserArray($user);
1184 array_push($users, $twitter_user);
1188 $this->showJsonObjects($users);
1190 $this->endDocument('json');
1193 function showSingleJsonGroup($group)
1195 $this->initDocument('json');
1196 $twitter_group = $this->twitterGroupArray($group);
1197 $this->showJsonObjects($twitter_group);
1198 $this->endDocument('json');
1201 function showSingleXmlGroup($group)
1203 $this->initDocument('xml');
1204 $twitter_group = $this->twitterGroupArray($group);
1205 $this->showTwitterXmlGroup($twitter_group);
1206 $this->endDocument('xml');
1209 function showSingleJsonList($list)
1211 $this->initDocument('json');
1212 $twitter_list = $this->twitterListArray($list);
1213 $this->showJsonObjects($twitter_list);
1214 $this->endDocument('json');
1217 function showSingleXmlList($list)
1219 $this->initDocument('xml');
1220 $twitter_list = $this->twitterListArray($list);
1221 $this->showTwitterXmlList($twitter_list);
1222 $this->endDocument('xml');
1225 static function dateTwitter($dt)
1227 $dateStr = date('d F Y H:i:s', strtotime($dt));
1228 $d = new DateTime($dateStr, new DateTimeZone('UTC'));
1229 $d->setTimezone(new DateTimeZone(common_timezone()));
1230 return $d->format('D M d H:i:s O Y');
1233 function initDocument($type='xml')
1237 header('Content-Type: application/xml; charset=utf-8');
1241 header('Content-Type: application/json; charset=utf-8');
1243 // Check for JSONP callback
1244 if (isset($this->callback)) {
1245 print $this->callback . '(';
1249 header("Content-Type: application/rss+xml; charset=utf-8");
1250 $this->initTwitterRss();
1253 header('Content-Type: application/atom+xml; charset=utf-8');
1254 $this->initTwitterAtom();
1257 // TRANS: Client error on an API request with an unsupported data format.
1258 $this->clientError(_('Not a supported data format.'));
1264 function endDocument($type='xml')
1271 // Check for JSONP callback
1272 if (isset($this->callback)) {
1277 $this->endTwitterRss();
1280 $this->endTwitterRss();
1283 // TRANS: Client error on an API request with an unsupported data format.
1284 $this->clientError(_('Not a supported data format.'));
1289 function initTwitterRss()
1292 $this->elementStart(
1296 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
1297 'xmlns:georss' => 'http://www.georss.org/georss'
1300 $this->elementStart('channel');
1301 Event::handle('StartApiRss', array($this));
1304 function endTwitterRss()
1306 $this->elementEnd('channel');
1307 $this->elementEnd('rss');
1311 function initTwitterAtom()
1314 // FIXME: don't hardcode the language here!
1315 $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom',
1316 'xml:lang' => 'en-US',
1317 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0'));
1320 function endTwitterAtom()
1322 $this->elementEnd('feed');
1326 function showProfile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
1328 $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1329 switch ($content_type) {
1331 $this->showTwitterXmlUser($profile_array);
1334 $this->showJsonObjects($profile_array);
1337 // TRANS: Client error on an API request with an unsupported data format.
1338 $this->clientError(_('Not a supported data format.'));
1343 private static function is_decimal($str)
1345 return preg_match('/^[0-9]+$/', $str);
1348 function getTargetUser($id)
1351 // Twitter supports these other ways of passing the user ID
1352 if (self::is_decimal($this->arg('id'))) {
1353 return User::getKV($this->arg('id'));
1354 } else if ($this->arg('id')) {
1355 $nickname = common_canonical_nickname($this->arg('id'));
1356 return User::getKV('nickname', $nickname);
1357 } else if ($this->arg('user_id')) {
1358 // This is to ensure that a non-numeric user_id still
1359 // overrides screen_name even if it doesn't get used
1360 if (self::is_decimal($this->arg('user_id'))) {
1361 return User::getKV('id', $this->arg('user_id'));
1363 } else if ($this->arg('screen_name')) {
1364 $nickname = common_canonical_nickname($this->arg('screen_name'));
1365 return User::getKV('nickname', $nickname);
1367 // Fall back to trying the currently authenticated user
1368 return $this->scoped->getUser();
1371 } else if (self::is_decimal($id)) {
1372 return User::getKV($id);
1374 $nickname = common_canonical_nickname($id);
1375 return User::getKV('nickname', $nickname);
1379 function getTargetProfile($id)
1383 // Twitter supports these other ways of passing the user ID
1384 if (self::is_decimal($this->arg('id'))) {
1385 return Profile::getKV($this->arg('id'));
1386 } else if ($this->arg('id')) {
1387 // Screen names currently can only uniquely identify a local user.
1388 $nickname = common_canonical_nickname($this->arg('id'));
1389 $user = User::getKV('nickname', $nickname);
1390 return $user ? $user->getProfile() : null;
1391 } else if ($this->arg('user_id')) {
1392 // This is to ensure that a non-numeric user_id still
1393 // overrides screen_name even if it doesn't get used
1394 if (self::is_decimal($this->arg('user_id'))) {
1395 return Profile::getKV('id', $this->arg('user_id'));
1397 } else if ($this->arg('screen_name')) {
1398 $nickname = common_canonical_nickname($this->arg('screen_name'));
1399 $user = User::getKV('nickname', $nickname);
1400 return $user instanceof User ? $user->getProfile() : null;
1402 // Fall back to trying the currently authenticated user
1403 return $this->scoped;
1405 } else if (self::is_decimal($id)) {
1406 return Profile::getKV($id);
1408 $nickname = common_canonical_nickname($id);
1409 $user = User::getKV('nickname', $nickname);
1410 return $user ? $user->getProfile() : null;
1414 function getTargetGroup($id)
1417 if (self::is_decimal($this->arg('id'))) {
1418 return User_group::getKV('id', $this->arg('id'));
1419 } else if ($this->arg('id')) {
1420 return User_group::getForNickname($this->arg('id'));
1421 } else if ($this->arg('group_id')) {
1422 // This is to ensure that a non-numeric group_id still
1423 // overrides group_name even if it doesn't get used
1424 if (self::is_decimal($this->arg('group_id'))) {
1425 return User_group::getKV('id', $this->arg('group_id'));
1427 } else if ($this->arg('group_name')) {
1428 return User_group::getForNickname($this->arg('group_name'));
1431 } else if (self::is_decimal($id)) {
1432 return User_group::getKV('id', $id);
1433 } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1434 return User_group::getKV('uri', urldecode($this->arg('uri')));
1436 return User_group::getForNickname($id);
1440 function getTargetList($user=null, $id=null)
1442 $tagger = $this->getTargetUser($user);
1446 $id = $this->arg('id');
1450 if (is_numeric($id)) {
1451 $list = Profile_list::getKV('id', $id);
1453 // only if the list with the id belongs to the tagger
1454 if(empty($list) || $list->tagger != $tagger->id) {
1459 $tag = common_canonical_tag($id);
1460 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1463 if (!empty($list) && $list->private) {
1464 if ($this->scoped->id == $list->tagger) {
1475 * Returns query argument or default value if not found. Certain
1476 * parameters used throughout the API are lightly scrubbed and
1477 * bounds checked. This overrides Action::arg().
1479 * @param string $key requested argument
1480 * @param string $def default value to return if $key is not provided
1484 function arg($key, $def=null)
1486 // XXX: Do even more input validation/scrubbing?
1488 if (array_key_exists($key, $this->args)) {
1491 $page = (int)$this->args['page'];
1492 return ($page < 1) ? 1 : $page;
1494 $count = (int)$this->args['count'];
1497 } elseif ($count > 200) {
1503 $since_id = (int)$this->args['since_id'];
1504 return ($since_id < 1) ? 0 : $since_id;
1506 $max_id = (int)$this->args['max_id'];
1507 return ($max_id < 1) ? 0 : $max_id;
1509 return parent::arg($key, $def);
1517 * Calculate the complete URI that called up this action. Used for
1518 * Atom rel="self" links. Warning: this is funky.
1520 * @return string URL a URL suitable for rel="self" Atom links
1522 function getSelfUri()
1524 $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1526 $id = $this->arg('id');
1527 $aargs = array('format' => $this->format);
1532 $user = $this->arg('user');
1533 if (!empty($user)) {
1534 $aargs['user'] = $user;
1537 $tag = $this->arg('tag');
1539 $aargs['tag'] = $tag;
1542 parse_str($_SERVER['QUERY_STRING'], $params);
1544 if (!empty($params)) {
1545 unset($params['p']);
1546 $pstring = http_build_query($params);
1549 $uri = common_local_url($action, $aargs);
1551 if (!empty($pstring)) {
1552 $uri .= '?' . $pstring;