]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge remote branch 'gitorious/0.9.x' into 0.9.x
authorEvan Prodromou <evan@status.net>
Mon, 15 Nov 2010 16:57:31 +0000 (11:57 -0500)
committerEvan Prodromou <evan@status.net>
Mon, 15 Nov 2010 16:57:31 +0000 (11:57 -0500)
EVENTS.txt
actions/apiatomservice.php [new file with mode: 0644]
actions/apistatusesshow.php
actions/apitimelineuser.php
actions/rsd.php
classes/Notice.php
lib/action.php
lib/apiaction.php
lib/router.php

index fed489705c29a7a26854f7cc70edc9633c3495ac..a3b0804e8f6bc0e3f787d0742faf2ea4459de00c 100644 (file)
@@ -1172,3 +1172,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/apiatomservice.php b/actions/apiatomservice.php
new file mode 100644 (file)
index 0000000..fb9d6ae
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * An AtomPub service document for a user
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @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 <evan@status.net>
+ * @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();
+    }
+}
index a98e45f79ca04776b67a9e8de706dd8b27b04fe7..f4a79ddbcb819c4cd2adeae710e267b43312c594 100644 (file)
@@ -100,13 +100,23 @@ class ApiStatusesShowAction extends ApiPrivateAuthAction
     {
         parent::handle($args);
 
-        if (!in_array($this->format, array('xml', 'json'))) {
+        if (!in_array($this->format, array('xml', 'json', 'atom'))) {
             // TRANS: Client error displayed when trying to handle an unknown API method.
-            $this->clientError(_('API method not found.'), $code = 404);
+            $this->clientError(_('API method not found.'), 404);
             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;
+        }
     }
 
     /**
@@ -117,10 +127,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 {
             // XXX: Twitter just sets a 404 header and doens't bother
@@ -197,4 +215,31 @@ 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");
+    }
 }
index 0046c462d71cc7ef5970bdd01147378aa767eb26..e4a8b596ee7d6491a817f7f2b826af420d719aa6 100644 (file)
@@ -97,7 +97,12 @@ class ApiTimelineUserAction extends ApiBareAuthAction
     function handle($args)
     {
         parent::handle($args);
-        $this->showTimeline();
+
+        if ($this->isPost()) {
+            $this->handlePost();
+        } else {
+            $this->showTimeline();
+        }
     }
 
     /**
@@ -114,9 +119,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();
 
@@ -132,20 +137,63 @@ 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':
             header('Content-Type: application/atom+xml; charset=utf-8');
 
             $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());
 
@@ -169,13 +217,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;
@@ -221,17 +274,206 @@ 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 (Event::handle('StartAtomPubNewActivity', array(&$activity))) {
+
+            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;
+            }
+
+            $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
+
+        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);
+
+        return $saved;
+    }
+
+    function purify($content)
+    {
+        require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
+
+        $config = array('safe' => 1,
+                        'deny_attribute' => 'id,style,on*');
+        return htmLawed($content, $config);
+    }
 }
index f88bf2e9a8bbfaedd7d33a0748bb0a880ac895d8..e02c85c41b42b2977a15dad2834456d8759f3a15 100644 (file)
@@ -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');
index eff0d32515c557335338b1712022ace55548d010..85c7dabea48f5d9f31667042cab2c5b07ed8fccd 100644 (file)
@@ -1611,6 +1611,35 @@ class Notice extends Memcached_DataObject
             Event::handle('EndActivityGeo', array(&$this, &$xs, $lat, $lon));
         }
 
+        // @fixme check this logic
+
+        if ($this->isLocal()) {
+
+            $selfUrl = common_local_url('ApiStatusesShow', array('id' => $this->id,
+                                                                 'format' => 'atom'));
+
+            if (Event::handle('StartActivityRelSelf', array(&$this, &$xs, &$selfUrl))) {
+                $xs->element('link', array('rel' => 'self',
+                                           'type' => 'application/atom+xml',
+                                           'href' => $selfUrl));
+                Event::handle('EndActivityRelSelf', array(&$this, &$xs, $selfUrl));
+            }
+
+            if (!empty($cur) && $cur->id == $this->profile_id) {
+
+                // note: $selfUrl may have been changed by a plugin
+                $relEditUrl = common_local_url('ApiStatusesShow', array('id' => $this->id,
+                                                                        'format' => 'atom'));
+
+                if (Event::handle('StartActivityRelEdit', array(&$this, &$xs, &$relEditUrl))) {
+                    $xs->element('link', array('rel' => 'edit',
+                                               'type' => 'application/atom+xml',
+                                               'href' => $relEditUrl));
+                    Event::handle('EndActivityRelEdit', array(&$this, &$xs, $relEditUrl));
+                }
+            }
+        }
+
         if (Event::handle('StartActivityEnd', array(&$this, &$xs))) {
             $xs->elementEnd('entry');
             Event::handle('EndActivityEnd', array(&$this, &$xs));
index 427b85427bd2549f6e64daf24727eaabada498e2..17d3e2311aa9a81f945572a638c48ad0b603ad23 100644 (file)
@@ -1404,4 +1404,15 @@ class Action extends HTMLOutputter // lawsuit
             $this->clientError(_('There was a problem with your session token.'));
         }
     }
+
+    /**
+     * Check if the current request is a POST
+     *
+     * @return boolean true if POST; otherwise false.
+     */
+
+    function isPost()
+    {
+        return ($_SERVER['REQUEST_METHOD'] == 'POST');
+    }
 }
index 4e9dbb310bcfc4e10253c5b39e59840ba009eae3..8a7be31502f29e89ca02f5370a22b4d89f0351ac 100644 (file)
@@ -726,6 +726,12 @@ class ApiAction extends Action
         $this->endDocument('xml');
     }
 
+    function showSingleAtomStatus($notice)
+    {
+        header('Content-Type: application/atom+xml; charset=utf-8');
+        print $notice->asAtomEntry(true, true, true, $this->auth_user);
+    }
+
     function show_single_json_status($notice)
     {
         $this->initDocument('json');
index 9aaac7dfe39cb8cbc051ed264868a545b3e8fb0f..c0f3bf31d7880cff97dc312a9deff1f82351957f 100644 (file)
@@ -399,12 +399,12 @@ class Router
 
             $m->connect('api/statuses/show.:format',
                         array('action' => 'ApiStatusesShow',
-                              'format' => '(xml|json)'));
+                              'format' => '(xml|json|atom)'));
 
             $m->connect('api/statuses/show/:id.:format',
                         array('action' => 'ApiStatusesShow',
                               'id' => '[0-9]+',
-                              'format' => '(xml|json)'));
+                              'format' => '(xml|json|atom)'));
 
             $m->connect('api/statuses/update.:format',
                         array('action' => 'ApiStatusesUpdate',
@@ -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'));