From 43a67b150a4e4285224ccf695171df731c736a1e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 15:58:53 -0400 Subject: show a single notice in atom entry format --- actions/apistatusesshow.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'actions') diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index 84f8079db..c0eab15a4 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -105,8 +105,8 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction { parent::handle($args); - if (!in_array($this->format, array('xml', 'json'))) { - $this->clientError(_('API method not found.'), $code = 404); + if (!in_array($this->format, array('xml', 'json', 'atom'))) { + $this->clientError(_('API method not found.'), 404); return; } @@ -122,10 +122,18 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction function showNotice() { if (!empty($this->notice)) { - if ($this->format == 'xml') { + switch ($this->format) { + case 'xml': $this->showSingleXmlStatus($this->notice); - } elseif ($this->format == 'json') { + break; + case 'json': $this->show_single_json_status($this->notice); + break; + case 'atom': + $this->showSingleAtomStatus($this->notice); + break; + default: + throw new Exception(sprintf(_("Unsupported format: %s"), $this->format)); } } else { -- cgit v1.2.3-54-g00ecf From 292e789584df47834f30d4de1ef143670c079b24 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 21:24:23 -0400 Subject: delete a notice using AtomPub --- actions/apistatusesshow.php | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) (limited to 'actions') diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index c0eab15a4..86ffd6862 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -110,7 +110,17 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction return; } - $this->showNotice(); + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + $this->showNotice(); + break; + case 'DELETE': + $this->deleteNotice(); + break; + default: + $this->clientError(_('HTTP method not supported.'), 405); + return; + } } /** @@ -213,4 +223,30 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction return null; } + function deleteNotice() + { + if ($this->format != 'atom') { + $this->clientError(_("Can only delete using the Atom format.")); + return; + } + + if (empty($this->auth_user) || + ($this->notice->profile_id != $this->auth_user->id && + !$this->auth_user->hasRight(Right::DELETEOTHERSNOTICE))) { + $this->clientError(_('Can\'t delete this notice.'), 403); + return; + } + + if (Event::handle('StartDeleteOwnNotice', array($this->auth_user, $this->notice))) { + $this->notice->delete(); + Event::handle('EndDeleteOwnNotice', array($this->auth_user, $this->notice)); + } + + // @fixme is there better output we could do here? + + header('HTTP/1.1 200 OK'); + header('Content-Type: text/plain'); + print(sprintf(_('Deleted notice %d'), $this->notice->id)); + print("\n"); + } } -- cgit v1.2.3-54-g00ecf From c0664599aa5a90f99d462d7e9d9930e1aaf5dcbc Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 22:50:13 -0400 Subject: allow posting to user timeline using AtomPub --- actions/apitimelineuser.php | 227 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 203 insertions(+), 24 deletions(-) (limited to 'actions') diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 0c97aad21..cb7c847d6 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -101,7 +101,12 @@ class ApiTimelineUserAction extends ApiBareAuthAction function handle($args) { parent::handle($args); - $this->showTimeline(); + + if ($this->isPost()) { + $this->handlePost(); + } else { + $this->showTimeline(); + } } /** @@ -119,9 +124,9 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom = new AtomUserNoticeFeed($this->user, $this->auth_user); $link = common_local_url( - 'showstream', - array('nickname' => $this->user->nickname) - ); + 'showstream', + array('nickname' => $this->user->nickname) + ); $self = $this->getSelfUri(); @@ -137,14 +142,14 @@ class ApiTimelineUserAction extends ApiBareAuthAction break; case 'rss': $this->showRssTimeline( - $this->notices, - $atom->title, - $link, - $atom->subtitle, - $suplink, - $atom->logo, - $self - ); + $this->notices, + $atom->title, + $link, + $atom->subtitle, + $suplink, + $atom->logo, + $self + ); break; case 'atom': @@ -177,9 +182,9 @@ class ApiTimelineUserAction extends ApiBareAuthAction $notices = array(); $notice = $this->user->getNotices( - ($this->page-1) * $this->count, $this->count, - $this->since_id, $this->max_id - ); + ($this->page-1) * $this->count, $this->count, + $this->since_id, $this->max_id + ); while ($notice->fetch()) { $notices[] = clone($notice); @@ -232,18 +237,192 @@ class ApiTimelineUserAction extends ApiBareAuthAction $last = count($this->notices) - 1; return '"' . implode( - ':', - array($this->arg('action'), - common_user_cache_hash($this->auth_user), - common_language(), - $this->user->id, - strtotime($this->notices[0]->created), - strtotime($this->notices[$last]->created)) - ) - . '"'; + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->user->id, + strtotime($this->notices[0]->created), + strtotime($this->notices[$last]->created)) + ) + . '"'; } return null; } + function handlePost() + { + if (empty($this->auth_user) || + $this->auth_user->id != $this->user->id) { + $this->clientError(_("Only the user can add to their own timeline.")); + return; + } + + if ($this->format != 'atom') { + // Only handle posts for Atom + $this->clientError(_("Only accept AtomPub for atom feeds.")); + return; + } + + $xml = file_get_contents('php://input'); + + $dom = DOMDocument::loadXML($xml); + + if ($dom->documentElement->namespaceURI != Activity::ATOM || + $dom->documentElement->localName != 'entry') { + $this->clientError(_('Atom post must be an Atom entry.')); + return; + } + + $activity = new Activity($dom->documentElement); + + if ($activity->verb != ActivityVerb::POST) { + $this->clientError(_('Can only handle post activities.')); + return; + } + + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + $this->clientError(sprintf(_('Cannot handle activity object type "%s"', + $note->type))); + return; + } + + // Use summary as fallback for content + + if (!empty($note->content)) { + $sourceContent = $note->content; + } else if (!empty($note->summary)) { + $sourceContent = $note->summary; + } else if (!empty($note->title)) { + $sourceContent = $note->title; + } else { + // @fixme fetch from $sourceUrl? + // @todo i18n FIXME: use sprintf and add i18n. + $this->clientError("No content for notice {$note->id}."); + return; + } + + // Get (safe!) HTML and text versions of the content + + $rendered = $this->purify($sourceContent); + $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8'); + + $shortened = common_shorten_links($content); + + $options = array('is_local' => Notice::LOCAL_PUBLIC, + 'rendered' => $rendered, + 'replies' => array(), + 'groups' => array(), + 'tags' => array(), + 'urls' => array()); + + // accept remote URI (not necessarily a good idea) + + common_debug("Note ID is {$note->id}"); + + if (!empty($note->id)) { + $notice = Notice::staticGet('uri', trim($note->id)); + + if (!empty($notice)) { + $this->clientError(sprintf(_('Notice with URI "%s" already exists.'), + $note->id)); + return; + } + common_log(LOG_NOTICE, "Saving client-supplied notice URI '$note->id'"); + $options['uri'] = $note->id; + } + + // accept remote create time (also maybe not such a good idea) + + if (!empty($activity->time)) { + common_log(LOG_NOTICE, "Saving client-supplied create time {$activity->time}"); + $options['created'] = common_sql_date($activity->time); + } + + // Check for optional attributes... + + if (!empty($activity->context)) { + + foreach ($activity->context->attention as $uri) { + + $profile = Profile::fromURI($uri); + + if (!empty($profile)) { + $options['replies'] = $uri; + } else { + $group = User_group::staticGet('uri', $uri); + if (!empty($group)) { + $options['groups'] = $uri; + } else { + // @fixme: hook for discovery here + common_log(LOG_WARNING, sprintf(_('AtomPub post with unknown attention URI %s'), $uri)); + } + } + } + + // Maintain direct reply associations + // @fixme what about conversation ID? + + if (!empty($activity->context->replyToID)) { + $orig = Notice::staticGet('uri', + $activity->context->replyToID); + if (!empty($orig)) { + $options['reply_to'] = $orig->id; + } + } + + $location = $activity->context->location; + + if ($location) { + $options['lat'] = $location->lat; + $options['lon'] = $location->lon; + if ($location->location_id) { + $options['location_ns'] = $location->location_ns; + $options['location_id'] = $location->location_id; + } + } + } + + // Atom categories <-> hashtags + + foreach ($activity->categories as $cat) { + if ($cat->term) { + $term = common_canonical_tag($cat->term); + if ($term) { + $options['tags'][] = $term; + } + } + } + + // Atom enclosures -> attachment URLs + foreach ($activity->enclosures as $href) { + // @fixme save these locally or....? + $options['urls'][] = $href; + } + + $saved = Notice::saveNew($this->user->id, + $content, + 'atompub', // TODO: deal with this + $options); + + if (!empty($saved)) { + header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, + 'format' => 'atom'))); + $this->showSingleAtomStatus($saved); + } + } + + function purify($content) + { + require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php'; + + $config = array('safe' => 1, + 'deny_attribute' => 'id,style,on*'); + return htmLawed($content, $config); + } } -- cgit v1.2.3-54-g00ecf From 59a7d78acb09c92622814d55c14e266f8f460fdf Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Oct 2010 23:43:26 -0400 Subject: Atom Service Document --- actions/apiatomservice.php | 100 +++++++++++++++++++++++++++++++++++++++++++++ actions/rsd.php | 14 +++++++ lib/router.php | 7 ++++ 3 files changed, 121 insertions(+) create mode 100644 actions/apiatomservice.php (limited to 'actions') diff --git a/actions/apiatomservice.php b/actions/apiatomservice.php new file mode 100644 index 000000000..fb9d6aee8 --- /dev/null +++ b/actions/apiatomservice.php @@ -0,0 +1,100 @@ +. + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +require_once INSTALLDIR.'/lib/apibareauth.php'; + +/** + * Shows an AtomPub service document for a user + * + * @category API + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3 + * @link http://status.net/ + */ + +class ApiAtomServiceAction extends ApiBareAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + + function prepare($args) + { + parent::prepare($args); + $this->user = $this->getTargetUser($this->arg('id')); + + if (empty($this->user)) { + $this->clientError(_('No such user.'), 404, $this->format); + return; + } + + return true; + } + + /** + * Handle the arguments. In our case, show a service document. + * + * @param Array $args unused. + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + header('Content-Type: application/atomsvc+xml'); + + $this->startXML(); + $this->elementStart('service', array('xmlns' => 'http://www.w3.org/2007/app', + 'xmlns:atom' => 'http://www.w3.org/2005/Atom')); + $this->elementStart('workspace'); + $this->element('atom:title', null, _('Main')); + $this->elementStart('collection', + array('href' => common_local_url('ApiTimelineUser', + array('id' => $this->user->id, + 'format' => 'atom')))); + $this->element('atom:title', + null, + sprintf(_("%s timeline"), + $this->user->nickname)); + $this->element('accept', null, 'application/atom+xml;type=entry'); + $this->elementEnd('collection'); + $this->elementEnd('workspace'); + $this->elementEnd('service'); + $this->endXML(); + } +} diff --git a/actions/rsd.php b/actions/rsd.php index f88bf2e9a..e02c85c41 100644 --- a/actions/rsd.php +++ b/actions/rsd.php @@ -162,6 +162,20 @@ class RsdAction extends Action 'true'); $this->elementEnd('settings'); $this->elementEnd('api'); + + // Atom API + + if (empty($this->user)) { + $service = common_local_url('ApiAtomService'); + } else { + $service = common_local_url('ApiAtomService', array('id' => $this->user->nickname)); + } + + $this->element('api', array('name' => 'Atom', + 'preferred' => 'false', + 'apiLink' => $service, + 'blogID' => $blogID)); + Event::handle('EndRsdListApis', array($this, $this->user)); } $this->elementEnd('apis'); diff --git a/lib/router.php b/lib/router.php index 834445f09..c0f3bf31d 100644 --- a/lib/router.php +++ b/lib/router.php @@ -686,6 +686,13 @@ class Router $m->connect('api/oauth/authorize', array('action' => 'ApiOauthAuthorize')); + $m->connect('api/statusnet/app/service/:id.xml', + array('action' => 'ApiAtomService', + 'id' => '[a-zA-Z0-9]+')); + + $m->connect('api/statusnet/app/service.xml', + array('action' => 'ApiAtomService')); + // Admin $m->connect('admin/site', array('action' => 'siteadminpanel')); -- cgit v1.2.3-54-g00ecf From e6ba379c8bfcf6a057a9cdfc161cae84d031401f Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 25 Oct 2010 11:08:10 -0400 Subject: navigation links in user timeline (for AtomPub) --- actions/apitimelineuser.php | 58 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 5 deletions(-) (limited to 'actions') diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index cb7c847d6..69cd9c2cb 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -157,6 +157,49 @@ class ApiTimelineUserAction extends ApiBareAuthAction $atom->setId($self); $atom->setSelfLink($self); + + // Add navigation links: next, prev, first + // Note: we use IDs rather than pages for navigation; page boundaries + // change too quickly! + + if (!empty($this->next_id)) { + $nextUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('max_id' => $this->next_id)); + + $atom->addLink($nextUrl, + array('rel' => 'next', + 'type' => 'application/atom+xml')); + } + + if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) { + + $lastNotice = $this->notices[0]; + $lastId = $lastNotice->id; + + $prevUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id), + array('since_id' => $lastId)); + + $atom->addLink($prevUrl, + array('rel' => 'prev', + 'type' => 'application/atom+xml')); + } + + if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) { + + $firstUrl = common_local_url('ApiTimelineUser', + array('format' => 'atom', + 'id' => $this->user->id)); + + $atom->addLink($firstUrl, + array('rel' => 'first', + 'type' => 'application/atom+xml')); + + } + $atom->addEntryFromNotices($this->notices); $this->raw($atom->getString()); @@ -181,13 +224,18 @@ class ApiTimelineUserAction extends ApiBareAuthAction { $notices = array(); - $notice = $this->user->getNotices( - ($this->page-1) * $this->count, $this->count, - $this->since_id, $this->max_id - ); + $notice = $this->user->getNotices(($this->page-1) * $this->count, + $this->count + 1, + $this->since_id, + $this->max_id); while ($notice->fetch()) { - $notices[] = clone($notice); + if (count($notices) < $this->count) { + $notices[] = clone($notice); + } else { + $this->next_id = $notice->id; + break; + } } return $notices; -- cgit v1.2.3-54-g00ecf From cb371d65c18771f8fcdcbeb450c063b844c000df Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 15 Nov 2010 11:54:42 -0500 Subject: add hooks for atom pub posts --- EVENTS.txt | 8 ++++++++ actions/apitimelineuser.php | 46 +++++++++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 16 deletions(-) (limited to 'actions') diff --git a/EVENTS.txt b/EVENTS.txt index 8e730945a..2df21f01a 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1158,3 +1158,11 @@ StartRevokeRole: when a role is being revoked EndRevokeRole: when a role has been revoked - $profile: profile that lost the role - $role: string name of the role + +StartAtomPubNewActivity: When a new activity comes in through Atom Pub API +- &$activity: received activity + +EndAtomPubNewActivity: When a new activity comes in through Atom Pub API +- $activity: received activity +- $notice: notice that was created + diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 69cd9c2cb..7e7663646 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -325,20 +325,38 @@ class ApiTimelineUserAction extends ApiBareAuthAction $activity = new Activity($dom->documentElement); - if ($activity->verb != ActivityVerb::POST) { - $this->clientError(_('Can only handle post activities.')); - return; - } + if (Event::handle('StartAtomPubNewActivity', array(&$activity))) { - $note = $activity->objects[0]; + if ($activity->verb != ActivityVerb::POST) { + $this->clientError(_('Can only handle post activities.')); + return; + } - if (!in_array($note->type, array(ActivityObject::NOTE, - ActivityObject::BLOGENTRY, - ActivityObject::STATUS))) { - $this->clientError(sprintf(_('Cannot handle activity object type "%s"', - $note->type))); - return; + $note = $activity->objects[0]; + + if (!in_array($note->type, array(ActivityObject::NOTE, + ActivityObject::BLOGENTRY, + ActivityObject::STATUS))) { + $this->clientError(sprintf(_('Cannot handle activity object type "%s"', + $note->type))); + return; + } + + $saved = $this->postNote($activity); + + Event::handle('EndAtomPubNewActivity', array($activity, $saved)); + } + + if (!empty($saved)) { + header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, + 'format' => 'atom'))); + $this->showSingleAtomStatus($saved); } + } + + function postNote($activity) + { + $note = $activity->objects[0]; // Use summary as fallback for content @@ -458,11 +476,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction 'atompub', // TODO: deal with this $options); - if (!empty($saved)) { - header("Location: " . common_local_url('ApiStatusesShow', array('notice_id' => $saved->id, - 'format' => 'atom'))); - $this->showSingleAtomStatus($saved); - } + return $saved; } function purify($content) -- cgit v1.2.3-54-g00ecf