]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' into noactor
authorEvan Prodromou <evan@status.net>
Wed, 29 Dec 2010 23:29:29 +0000 (15:29 -0800)
committerEvan Prodromou <evan@status.net>
Wed, 29 Dec 2010 23:29:29 +0000 (15:29 -0800)
32 files changed:
EVENTS.txt
actions/apitimelineuser.php
actions/newgroup.php
actions/profilesettings.php
classes/File.php
classes/File_to_post.php
classes/Memcached_DataObject.php
classes/Notice.php
classes/Notice_tag.php
classes/Profile.php
classes/User_group.php
lib/activityimporter.php
lib/oembedhelper.php
lib/right.php
plugins/Bookmark/Bookmark.php [new file with mode: 0644]
plugins/Bookmark/BookmarkPlugin.php [new file with mode: 0644]
plugins/Bookmark/bookmark.css [new file with mode: 0644]
plugins/Bookmark/bookmarkform.php [new file with mode: 0644]
plugins/Bookmark/bookmarklet [new file with mode: 0644]
plugins/Bookmark/bookmarkpopup.js [new file with mode: 0644]
plugins/Bookmark/bookmarkpopup.php [new file with mode: 0644]
plugins/Bookmark/deliciousbackupimporter.php [new file with mode: 0644]
plugins/Bookmark/deliciousbookmarkimporter.php [new file with mode: 0644]
plugins/Bookmark/importbookmarks.php [new file with mode: 0644]
plugins/Bookmark/importdelicious.php [new file with mode: 0644]
plugins/Bookmark/newbookmark.php [new file with mode: 0644]
plugins/Bookmark/noticebyurl.php [new file with mode: 0644]
plugins/Bookmark/showbookmark.php [new file with mode: 0644]
plugins/OStatus/actions/groupsalmon.php
plugins/OStatus/actions/usersalmon.php
plugins/OStatus/classes/Ostatus_profile.php
plugins/OStatus/lib/salmonaction.php

index 65265cdf0f0e0fcfa450c0e2565064747e8e3952..f7cc7df67c4f8bd3b8f6e74359a465c215080426 100644 (file)
@@ -969,9 +969,12 @@ EndRevokeRole: when a role has been revoked
 
 StartAtomPubNewActivity: When a new activity comes in through Atom Pub API
 - &$activity: received activity
+- $user: user publishing the entry
+- &$notice: notice created; initially null, can be set
 
 EndAtomPubNewActivity: When a new activity comes in through Atom Pub API
 - $activity: received activity
+- $user: user publishing the entry
 - $notice: notice that was created
 
 StartXrdActionAliases: About to set aliases for the XRD object for a user
@@ -1023,3 +1026,22 @@ StartActivityObjectFromGroup: When converting a group to an activity:object
 EndActivityObjectFromGroup:  After converting a group to an activity:object
 - $group:  The group being converted
 - &$object: The finished object. Tweak as needed.
+
+StartImportActivity: when we start to import an activity
+- $user: User to make the author import
+- $author: Author of the feed; good for comparisons
+- $activity: The current activity
+- $trusted: How "trusted" the process is
+- &$done: Return value; whether to continue
+
+EndImportActivity: when we finish importing an activity
+- $user: User to make the author import
+- $author: Author of the feed; good for comparisons
+- $activity: The current activity
+- $trusted: How "trusted" the process is
+
+StartProfileSettingsActions: when we're showing account-management action list
+- $action: Action being shown (use for output)
+
+EndProfileSettingsActions: when we're showing account-management action list
+- $action: Action being shown (use for output)
index 42988a00f6b7ed5cc78a9ecf3af98b65503521fa..5809df3b5ee17fb6a18b9fa3db8867b3bba600f1 100644 (file)
@@ -324,7 +324,9 @@ class ApiTimelineUserAction extends ApiBareAuthAction
 
         $activity = new Activity($dom->documentElement);
 
-        if (Event::handle('StartAtomPubNewActivity', array(&$activity))) {
+        $saved = null;
+
+        if (Event::handle('StartAtomPubNewActivity', array(&$activity, $this->user, &$saved))) {
 
             if ($activity->verb != ActivityVerb::POST) {
                 // TRANS: Client error displayed when not using the POST verb.
@@ -347,7 +349,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
 
             $saved = $this->postNote($activity);
 
-            Event::handle('EndAtomPubNewActivity', array($activity, $saved));
+            Event::handle('EndAtomPubNewActivity', array($activity, $this->user, $saved));
         }
 
         if (!empty($saved)) {
index 95af6415e507b303272226511e737c0900dfb283..42d488e54ebfef3a09bfd7b3ce15a96a01cf2807 100644 (file)
@@ -66,6 +66,13 @@ class NewgroupAction extends Action
             return false;
         }
 
+        $user = common_current_user();
+        $profile = $user->getProfile();
+        if (!$profile->hasRight(Right::CREATEGROUP)) {
+            // TRANS: Client exception thrown when a user tries to create a group while banned.
+            throw new ClientException(_('You are not allowed to create groups on this site.'), 403);
+        }
+
         return true;
     }
 
index 8f55a471890931df2fd174bd9c85abd50537d8dc..19fbdbd29336d3d7393a3157b4c8c9d498ba9115 100644 (file)
@@ -458,27 +458,32 @@ class ProfilesettingsAction extends AccountSettingsAction
 
         $this->elementStart('div', array('id' => 'aside_primary',
                                          'class' => 'aside'));
-        if ($user->hasRight(Right::BACKUPACCOUNT)) {
-            $this->elementStart('li');
-            $this->element('a',
-                           array('href' => common_local_url('backupaccount')),
-                           _('Backup account'));
-            $this->elementEnd('li');
-        }
-        if ($user->hasRight(Right::DELETEACCOUNT)) {
-            $this->elementStart('li');
-            $this->element('a',
-                           array('href' => common_local_url('deleteaccount')),
-                           _('Delete account'));
-            $this->elementEnd('li');
-        }
-        if ($user->hasRight(Right::RESTOREACCOUNT)) {
-            $this->elementStart('li');
-            $this->element('a',
-                           array('href' => common_local_url('restoreaccount')),
-                           _('Restore account'));
-            $this->elementEnd('li');
+        $this->elementStart('ul');
+        if (Event::handle('StartProfileSettingsActions', array($this))) {
+            if ($user->hasRight(Right::BACKUPACCOUNT)) {
+                $this->elementStart('li');
+                $this->element('a',
+                               array('href' => common_local_url('backupaccount')),
+                               _('Backup account'));
+                $this->elementEnd('li');
+            }
+            if ($user->hasRight(Right::DELETEACCOUNT)) {
+                $this->elementStart('li');
+                $this->element('a',
+                               array('href' => common_local_url('deleteaccount')),
+                               _('Delete account'));
+                $this->elementEnd('li');
+            }
+            if ($user->hasRight(Right::RESTOREACCOUNT)) {
+                $this->elementStart('li');
+                $this->element('a',
+                               array('href' => common_local_url('restoreaccount')),
+                               _('Restore account'));
+                $this->elementEnd('li');
+            }
+            Event::handle('EndProfileSettingsActions', array($this));
         }
+        $this->elementEnd('ul');
         $this->elementEnd('div');
     }
 }
index ef9dbf14aba1226c744df6f55472350852bef1d7..29a8f0f1c5a16cc8661196c7d23a2d6787d4ac22 100644 (file)
@@ -412,4 +412,102 @@ class File extends Memcached_DataObject
     {
         return File_thumbnail::staticGet('file_id', $this->id);
     }
+
+    /**
+     * Blow the cache of notices that link to this URL
+     *
+     * @param boolean $last Whether to blow the "last" cache too
+     *
+     * @return void
+     */
+
+    function blowCache($last=false)
+    {
+        self::blow('file:notice-ids:%s', $this->url);
+        if ($last) {
+            self::blow('file:notice-ids:%s;last', $this->url);
+        }
+        self::blow('file:notice-count:%d', $this->id);
+    }
+
+    /**
+     * Stream of notices linking to this URL
+     *
+     * @param integer $offset   Offset to show; default is 0
+     * @param integer $limit    Limit of notices to show
+     * @param integer $since_id Since this notice
+     * @param integer $max_id   Before this notice
+     *
+     * @return array ids of notices that link to this file
+     */
+
+    function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
+    {
+        $ids = Notice::stream(array($this, '_streamDirect'),
+                              array(),
+                              'file:notice-ids:'.$this->url,
+                              $offset, $limit, $since_id, $max_id);
+
+        return Notice::getStreamByIds($ids);
+    }
+
+    /**
+     * Stream of notices linking to this URL
+     *
+     * @param integer $offset   Offset to show; default is 0
+     * @param integer $limit    Limit of notices to show
+     * @param integer $since_id Since this notice
+     * @param integer $max_id   Before this notice
+     *
+     * @return array ids of notices that link to this file
+     */
+
+    function _streamDirect($offset, $limit, $since_id, $max_id)
+    {
+        $f2p = new File_to_post();
+
+        $f2p->selectAdd();
+        $f2p->selectAdd('post_id');
+
+        $f2p->file_id = $this->id;
+
+        Notice::addWhereSinceId($f2p, $since_id, 'post_id', 'modified');
+        Notice::addWhereMaxId($f2p, $max_id, 'post_id', 'modified');
+
+        $f2p->orderBy('modified DESC, post_id DESC');
+
+        if (!is_null($offset)) {
+            $f2p->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($f2p->find()) {
+            while ($f2p->fetch()) {
+                $ids[] = $f2p->post_id;
+            }
+        }
+
+        return $ids;
+    }
+
+    function noticeCount()
+    {
+        $cacheKey = sprintf('file:notice-count:%d', $this->id);
+        
+        $count = self::cacheGet($cacheKey);
+
+        if ($count === false) {
+
+            $f2p = new File_to_post();
+
+            $f2p->file_id = $this->id;
+
+            $count = $f2p->count();
+
+            self::cacheSet($cacheKey, $count);
+        } 
+
+        return $count;
+    }
 }
index 530921adcb05aacdf2422d20f108cb0069484eff..bcb6771f4f19077cdd265f2c3c61511f4af586e7 100644 (file)
@@ -52,6 +52,12 @@ class File_to_post extends Memcached_DataObject
                 $f2p->file_id = $file_id;
                 $f2p->post_id = $notice_id;
                 $f2p->insert();
+                
+                $f = File::staticGet($file_id);
+
+                if (!empty($f)) {
+                    $f->blowCache();
+                }
             }
 
             if (empty($seen[$notice_id])) {
@@ -66,4 +72,13 @@ class File_to_post extends Memcached_DataObject
     {
         return Memcached_DataObject::pkeyGet('File_to_post', $kv);
     }
+
+    function delete()
+    {
+        $f = File::staticGet('id', $this->file_id);
+        if (!empty($f)) {
+            $f->blowCache();
+        }
+        return parent::delete();
+    }
 }
index eb5d2627f27efc664d45ff530305334dd19cc4ae..d50b4071d1eeaef3548dd992675498bd991fad31 100644 (file)
@@ -74,7 +74,7 @@ class Memcached_DataObject extends Safe_DataObject
             return $i;
         } else {
             $i = DB_DataObject::factory($cls);
-            if (empty($i)) {
+            if (empty($i) || PEAR::isError($i)) {
                 return false;
             }
             foreach ($kv as $k => $v) {
index 50909f97071e31f339b0b35d66921386c0a94014..561999966c5842bb3ad2df84beae942d2e8d1416 100644 (file)
@@ -109,6 +109,11 @@ class Notice extends Memcached_DataObject
         // @fixme we have some cases where things get re-run and so the
         // insert fails.
         $deleted = Deleted_notice::staticGet('id', $this->id);
+
+        if (!$deleted) {
+            $deleted = Deleted_notice::staticGet('uri', $this->uri);
+        }
+
         if (!$deleted) {
             $deleted = new Deleted_notice();
 
@@ -130,6 +135,7 @@ class Notice extends Memcached_DataObject
             $this->clearFaves();
             $this->clearTags();
             $this->clearGroupInboxes();
+            $this->clearFiles();
 
             // NOTE: we don't clear inboxes
             // NOTE: we don't clear queue items
@@ -1780,6 +1786,21 @@ class Notice extends Memcached_DataObject
         $reply->free();
     }
 
+    function clearFiles()
+    {
+        $f2p = new File_to_post();
+
+        $f2p->post_id = $this->id;
+
+        if ($f2p->find()) {
+            while ($f2p->fetch()) {
+                $f2p->delete();
+            }
+        }
+        // FIXME: decide whether to delete File objects
+        // ...and related (actual) files
+    }
+
     function clearRepeats()
     {
         $repeatNotice = new Notice();
@@ -2033,7 +2054,7 @@ class Notice extends Memcached_DataObject
      */
     public static function addWhereSinceId(DB_DataObject $obj, $id, $idField='id', $createdField='created')
     {
-        $since = self::whereSinceId($id);
+        $since = self::whereSinceId($id, $idField, $createdField);
         if ($since) {
             $obj->whereAdd($since);
         }
@@ -2072,7 +2093,7 @@ class Notice extends Memcached_DataObject
      */
     public static function addWhereMaxId(DB_DataObject $obj, $id, $idField='id', $createdField='created')
     {
-        $max = self::whereMaxId($id);
+        $max = self::whereMaxId($id, $idField, $createdField);
         if ($max) {
             $obj->whereAdd($max);
         }
index bb67c8f8195740ac82504fad049421d943465259..f795bfc601cfc6765afc121fa417f7f652893137 100644 (file)
@@ -87,4 +87,19 @@ class Notice_tag extends Memcached_DataObject
     {
         return Memcached_DataObject::pkeyGet('Notice_tag', $kv);
     }
+
+       static function url($tag)
+       {
+               if (common_config('singleuser', 'enabled')) {
+                       // regular TagAction isn't set up in 1user mode
+                       $nickname = User::singleUserNickname();
+                       $url = common_local_url('showstream',
+                                                                       array('nickname' => $nickname,
+                                                                                 'tag' => $tag));
+               } else {
+                       $url = common_local_url('tag', array('tag' => $tag));
+               }
+
+               return $url;
+       }
 }
index 972351a75b1a9adefe97b755cff8d21b217f6f84..adad0c6157d5d25c7574e70bebce712417257860 100644 (file)
@@ -850,6 +850,7 @@ class Profile extends Memcached_DataObject
             case Right::NEWNOTICE:
             case Right::NEWMESSAGE:
             case Right::SUBSCRIBE:
+            case Right::CREATEGROUP:
                 $result = !$this->isSilenced();
                 break;
             case Right::PUBLICNOTICE:
index cffc7864586d7a542764fcb1ec3d621725ca6ee1..68f61cb7f48a104b43aae7b84fcf5ba4503d7ca3 100644 (file)
@@ -476,6 +476,16 @@ class User_group extends Memcached_DataObject
     }
 
     static function register($fields) {
+        if (!empty($fields['userid'])) {
+            $profile = Profile::staticGet('id', $fields['userid']);
+            if ($profile && !$profile->hasRight(Right::CREATEGROUP)) {
+                common_log(LOG_WARNING, "Attempted group creation from banned user: " . $profile->nickname);
+
+                // TRANS: Client exception thrown when a user tries to create a group while banned.
+                throw new ClientException(_('You are not allowed to create groups on this site.'), 403);
+            }
+        }
+
         // MAGICALLY put fields into current scope
 
         extract($fields);
index 4a767813285dff30bdb744a9e1a47a035e92dd72..b3b7ffb06609a9582b4ae6805e77b53e42c8316a 100644 (file)
@@ -63,31 +63,40 @@ class ActivityImporter extends QueueHandler
 
         $this->trusted = $trusted;
 
-        try {
-            switch ($activity->verb) {
-            case ActivityVerb::FOLLOW:
-                $this->subscribeProfile($user, $author, $activity);
-                break;
-            case ActivityVerb::JOIN:
-                $this->joinGroup($user, $activity);
-                break;
-            case ActivityVerb::POST:
-                $this->postNote($user, $author, $activity);
-                break;
-            default:
-                throw new Exception("Unknown verb: {$activity->verb}");
+        $done = null;
+
+        if (Event::handle('StartImportActivity', 
+                          array($user, $author, $activity, $trusted, &$done))) {
+
+            try {
+                switch ($activity->verb) {
+                case ActivityVerb::FOLLOW:
+                    $this->subscribeProfile($user, $author, $activity);
+                    break;
+                case ActivityVerb::JOIN:
+                    $this->joinGroup($user, $activity);
+                    break;
+                case ActivityVerb::POST:
+                    $this->postNote($user, $author, $activity);
+                    break;
+                default:
+                    throw new ClientException("Unknown verb: {$activity->verb}");
+                }
+                Event::handle('EndImportActivity', 
+                              array($user, $author, $activity, $trusted));
+                $done = true;
+            } catch (ClientException $ce) {
+                common_log(LOG_WARNING, $ce->getMessage());
+                $done = true;
+            } catch (ServerException $se) {
+                common_log(LOG_ERR, $se->getMessage());
+                $done = false;
+            } catch (Exception $e) {
+                common_log(LOG_ERR, $e->getMessage());
+                $done = false;
             }
-        } catch (ClientException $ce) {
-            common_log(LOG_WARNING, $ce->getMessage());
-            return true;
-        } catch (ServerException $se) {
-            common_log(LOG_ERR, $se->getMessage());
-            return false;
-        } catch (Exception $e) {
-            common_log(LOG_ERR, $e->getMessage());
-            return false;
         }
-        return true;
+        return $done;
     }
     
     function subscribeProfile($user, $author, $activity)
index 84cf1058676a97e9ed6a523b484a1b2ddeb6ec5c..3cd20c8e8efb1c3d82990c95e4cc0b3d79306c2c 100644 (file)
@@ -299,6 +299,10 @@ class oEmbedHelper
 
 class oEmbedHelper_Exception extends Exception
 {
+    public function __construct($message = "", $code = 0, $previous = null)
+    {
+        parent::__construct($message, $code);
+    }
 }
 
 class oEmbedHelper_BadHtmlException extends oEmbedHelper_Exception
index 5bf9c41161adf86972068d7ab8196975b9dac6a8..d144b21ae98469595f24c84ffc8dff47ae9e66f3 100644 (file)
@@ -65,5 +65,6 @@ class Right
     const RESTOREACCOUNT     = 'restoreaccount';
     const DELETEACCOUNT      = 'deleteaccount';
     const MOVEACCOUNT        = 'moveaccount';
+    const CREATEGROUP        = 'creategroup';
 }
 
diff --git a/plugins/Bookmark/Bookmark.php b/plugins/Bookmark/Bookmark.php
new file mode 100644 (file)
index 0000000..61fe3c5
--- /dev/null
@@ -0,0 +1,351 @@
+<?php
+/**
+ * Data class to mark notices as bookmarks
+ *
+ * PHP version 5
+ *
+ * @category Data
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2009, StatusNet, Inc.
+ *
+ * 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/>.
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * For storing the fact that a notice is a bookmark
+ *
+ * @category Bookmark
+ * @package  StatusNet
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://status.net/
+ *
+ * @see      DB_DataObject
+ */
+
+class Bookmark extends Memcached_DataObject
+{
+    public $__table = 'bookmark'; // table name
+    public $profile_id;           // int(4)  primary_key not_null
+    public $url;                  // varchar(255) primary_key not_null
+    public $title;                // varchar(255)
+    public $description;          // text
+    public $uri;                  // varchar(255)
+    public $url_crc32;            // int(4) not_null
+    public $created;              // datetime
+
+    /**
+     * Get an instance by key
+     *
+     * This is a utility method to get a single instance with a given key value.
+     *
+     * @param string $k Key to use to lookup (usually 'user_id' for this class)
+     * @param mixed  $v Value to lookup
+     *
+     * @return User_greeting_count object found, or null for no hits
+     *
+     */
+
+    function staticGet($k, $v=null)
+    {
+        return Memcached_DataObject::staticGet('Bookmark', $k, $v);
+    }
+
+    /**
+     * Get an instance by compound key
+     *
+     * This is a utility method to get a single instance with a given set of
+     * key-value pairs. Usually used for the primary key for a compound key; thus
+     * the name.
+     *
+     * @param array $kv array of key-value mappings
+     *
+     * @return Bookmark object found, or null for no hits
+     *
+     */
+
+    function pkeyGet($kv)
+    {
+        return Memcached_DataObject::pkeyGet('Bookmark', $kv);
+    }
+
+    /**
+     * return table definition for DB_DataObject
+     *
+     * DB_DataObject needs to know something about the table to manipulate
+     * instances. This method provides all the DB_DataObject needs to know.
+     *
+     * @return array array of column definitions
+     */
+
+    function table()
+    {
+        return array('profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+                     'url' => DB_DATAOBJECT_STR,
+                     'title' => DB_DATAOBJECT_STR,
+                     'description' => DB_DATAOBJECT_STR,
+                     'uri' => DB_DATAOBJECT_STR,
+                     'url_crc32' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
+                     'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + 
+                     DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
+    }
+
+    /**
+     * return key definitions for DB_DataObject
+     *
+     * @return array list of key field names
+     */
+
+    function keys()
+    {
+        return array_keys($this->keyTypes());
+    }
+
+    /**
+     * return key definitions for Memcached_DataObject
+     *
+     * @return array associative array of key definitions
+     */
+
+    function keyTypes()
+    {
+        return array('profile_id' => 'K',
+                     'url' => 'K',
+                     'uri' => 'U');
+    }
+
+    /**
+     * Magic formula for non-autoincrementing integer primary keys
+     *
+     * @return array magic three-false array that stops auto-incrementing.
+     */
+
+    function sequenceKey()
+    {
+        return array(false, false, false);
+    }
+
+    /**
+     * Get a bookmark based on a notice
+     * 
+     * @param Notice $notice Notice to check for
+     *
+     * @return Bookmark found bookmark or null
+     */
+    
+    function getByNotice($notice)
+    {
+        return self::staticGet('uri', $notice->uri);
+    }
+
+    /**
+     * Get the bookmark that a user made for an URL
+     *
+     * @param Profile $profile Profile to check for
+     * @param string  $url     URL to check for
+     *
+     * @return Bookmark bookmark found or null
+     */
+     
+    static function getByURL($profile, $url)
+    {
+        return self::pkeyGet(array('profile_id' => $profile->id,
+                                   'url' => $url));
+        return null;
+    }
+
+    /**
+     * Get the bookmark that a user made for an URL
+     *
+     * @param Profile $profile Profile to check for
+     * @param integer $crc32   CRC-32 of URL to check for
+     *
+     * @return array Bookmark objects found (usually 1 or 0)
+     */
+     
+    static function getByCRC32($profile, $crc32)
+    {
+        $bookmarks = array();
+
+        $nb = new Bookmark();
+        
+        $nb->profile_id = $profile->id;
+        $nb->url_crc32  = $crc32;
+
+        if ($nb->find()) {
+            while ($nb->fetch()) {
+                $bookmarks[] = clone($nb);
+            }
+        }
+
+        return $bookmarks;
+    }
+
+    /**
+     * Save a new notice bookmark
+     *
+     * @param Profile $profile     To save the bookmark for
+     * @param string  $title       Title of the bookmark
+     * @param string  $url         URL of the bookmark
+     * @param mixed   $rawtags     array of tags or string
+     * @param string  $description Description of the bookmark
+     * @param array   $options     Options for the Notice::saveNew()
+     *
+     * @return Notice saved notice
+     */
+
+    static function saveNew($profile, $title, $url, $rawtags, $description,
+                            $options=null)
+    {
+        $nb = self::getByURL($profile, $url);
+
+        if (!empty($nb)) {
+            throw new ClientException(_('Bookmark already exists.'));
+        }
+
+        if (empty($options)) {
+            $options = array();
+        }
+
+        if (array_key_exists('uri', $options)) {
+            $other = Bookmark::staticGet('uri', $options['uri']);
+            if (!empty($other)) {
+                throw new ClientException(_('Bookmark already exists.'));
+            }
+        }
+
+        if (is_string($rawtags)) {
+            $rawtags = preg_split('/[\s,]+/', $rawtags);
+        }
+
+        $nb = new Bookmark();
+
+        $nb->profile_id  = $profile->id;
+        $nb->url         = $url;
+        $nb->title       = $title;
+        $nb->description = $description;
+        $nb->url_crc32   = crc32($nb->url);
+
+        if (array_key_exists('created', $options)) {
+            $nb->created = $options['created'];
+        } else {
+            $nb->created = common_sql_now();
+        }
+
+        if (array_key_exists('uri', $options)) {
+            $nb->uri = $options['uri'];
+        } else {
+            $dt = new DateTime($nb->created, new DateTimeZone('UTC'));
+
+            // I posit that it's sufficiently impossible
+            // for the same user to generate two CRC-32-clashing
+            // URLs in the same second that this is a safe unique identifier.
+            // If you find a real counterexample, contact me at acct:evan@status.net
+            // and I will publicly apologize for my hubris.
+
+            $created = $dt->format('YmdHis');
+
+            $crc32 = sprintf('%08x', $nb->url_crc32);
+
+            $nb->uri = common_local_url('showbookmark',
+                                        array('user' => $profile->id,
+                                              'created' => $created,
+                                              'crc32' => $crc32));
+        }
+
+        $nb->insert();
+
+        $tags    = array();
+        $replies = array();
+
+        // filter "for:nickname" tags
+
+        foreach ($rawtags as $tag) {
+            if (strtolower(mb_substr($tag, 0, 4)) == 'for:') {
+                // skip if done by caller
+                if (!array_key_exists('replies', $options)) {
+                    $nickname = mb_substr($tag, 4);
+                    $other    = common_relative_profile($profile,
+                                                        $nickname);
+                    if (!empty($other)) {
+                        $replies[] = $other->getUri();
+                    }
+                }
+            } else {
+                $tags[] = common_canonical_tag($tag);
+            }
+        }
+
+        $hashtags = array();
+        $taglinks = array();
+
+        foreach ($tags as $tag) {
+            $hashtags[] = '#'.$tag;
+            $attrs      = array('href' => Notice_tag::url($tag),
+                                'rel'  => $tag,
+                                'class' => 'tag');
+            $taglinks[] = XMLStringer::estring('a', $attrs, $tag);
+        }
+
+        // Use user's preferences for short URLs, if possible
+
+        $user = User::staticGet('id', $profile->id);
+
+        $shortUrl = File_redirection::makeShort($url, 
+                                                empty($user) ? null : $user);
+
+        $content = sprintf(_('"%s" %s %s %s'),
+                           $title,
+                           $shortUrl,
+                           $description,
+                           implode(' ', $hashtags));
+
+        $rendered = sprintf(_('<span class="xfolkentry">'.
+                              '<a class="taggedlink" href="%s">%s</a> '.
+                              '<span class="description">%s</span> '.
+                              '<span class="meta">%s</span>'.
+                              '</span>'),
+                            htmlspecialchars($url),
+                            htmlspecialchars($title),
+                            htmlspecialchars($description),
+                            implode(' ', $taglinks));
+
+        $options = array_merge(array('urls' => array($url),
+                                     'rendered' => $rendered,
+                                     'tags' => $tags,
+                                     'replies' => $replies),
+                               $options);
+
+        if (!array_key_exists('uri', $options)) {
+            $options['uri'] = $nb->uri;
+        }
+
+        $saved = Notice::saveNew($profile->id,
+                                 $content,
+                                 array_key_exists('source', $options) ?
+                                 $options['source'] : 'web',
+                                 $options);
+
+        return $saved;
+    }
+}
diff --git a/plugins/Bookmark/BookmarkPlugin.php b/plugins/Bookmark/BookmarkPlugin.php
new file mode 100644 (file)
index 0000000..4f31349
--- /dev/null
@@ -0,0 +1,772 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * A plugin to enable social-bookmarking functionality
+ *
+ * PHP version 5
+ *
+ * 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  SocialBookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Bookmark plugin main class
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Brion Vibber <brionv@status.net>
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class BookmarkPlugin extends Plugin
+{
+    const VERSION         = '0.1';
+    const IMPORTDELICIOUS = 'BookmarkPlugin:IMPORTDELICIOUS';
+
+    /**
+     * Authorization for importing delicious bookmarks
+     *
+     * By default, everyone can import bookmarks except silenced people.
+     *
+     * @param Profile $profile Person whose rights to check
+     * @param string  $right   Right to check; const value
+     * @param boolean &$result Result of the check, writeable
+     *
+     * @return boolean hook value
+     */
+
+    function onUserRightsCheck($profile, $right, &$result)
+    {
+        if ($right == self::IMPORTDELICIOUS) {
+            $result = !$profile->isSilenced();
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Database schema setup
+     *
+     * @see Schema
+     * @see ColumnDef
+     *
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+
+    function onCheckSchema()
+    {
+        $schema = Schema::get();
+
+        // For storing user-submitted flags on profiles
+
+        $schema->ensureTable('bookmark',
+                             array(new ColumnDef('profile_id',
+                                                 'integer',
+                                                 null,
+                                                 false,
+                                                 'PRI'),
+                                   new ColumnDef('url',
+                                                 'varchar',
+                                                 255,
+                                                 false,
+                                                 'PRI'),
+                                   new ColumnDef('title',
+                                                 'varchar',
+                                                 255),
+                                   new ColumnDef('description',
+                                                 'text'),
+                                   new ColumnDef('uri',
+                                                 'varchar',
+                                                 255,
+                                                 false,
+                                                 'UNI'),
+                                   new ColumnDef('url_crc32',
+                                                 'integer unsigned',
+                                                 null,
+                                                 false,
+                                                 'MUL'),
+                                   new ColumnDef('created',
+                                                 'datetime',
+                                                 null,
+                                                 false,
+                                                 'MUL')));
+
+        try {
+            $schema->createIndex('bookmark', 
+                                 array('profile_id', 
+                                       'url_crc32'),
+                                 'bookmark_profile_url_idx');
+        } catch (Exception $e) {
+            common_log(LOG_ERR, $e->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * When a notice is deleted, delete the related Bookmark
+     *
+     * @param Notice $notice Notice being deleted
+     * 
+     * @return boolean hook value
+     */
+
+    function onNoticeDeleteRelated($notice)
+    {
+        $nb = Bookmark::getByNotice($notice);
+
+        if (!empty($nb)) {
+            $nb->delete();
+        }
+
+        return true;
+    }
+
+    /**
+     * Show the CSS necessary for this plugin
+     *
+     * @param Action $action the action being run
+     *
+     * @return boolean hook value
+     */
+
+    function onEndShowStyles($action)
+    {
+        $action->cssLink('plugins/Bookmark/bookmark.css');
+        return true;
+    }
+
+    /**
+     * Load related modules when needed
+     *
+     * @param string $cls Name of the class to be loaded
+     *
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+
+    function onAutoload($cls)
+    {
+        $dir = dirname(__FILE__);
+
+        switch ($cls)
+        {
+        case 'ShowbookmarkAction':
+        case 'NewbookmarkAction':
+        case 'BookmarkpopupAction':
+        case 'NoticebyurlAction':
+        case 'ImportdeliciousAction':
+            include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
+            return false;
+        case 'Bookmark':
+            include_once $dir.'/'.$cls.'.php';
+            return false;
+        case 'BookmarkForm':
+        case 'DeliciousBackupImporter':
+        case 'DeliciousBookmarkImporter':
+            include_once $dir.'/'.strtolower($cls).'.php';
+            return false;
+        default:
+            return true;
+        }
+    }
+
+    /**
+     * Map URLs to actions
+     *
+     * @param Net_URL_Mapper $m path-to-action mapper
+     *
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+
+    function onRouterInitialized($m)
+    {
+        $m->connect('main/bookmark/new',
+                    array('action' => 'newbookmark'),
+                    array('id' => '[0-9]+'));
+
+        $m->connect('main/bookmark/popup',
+                    array('action' => 'bookmarkpopup'));
+
+        $m->connect('main/bookmark/import',
+                    array('action' => 'importdelicious'));
+
+        $m->connect('bookmark/:user/:created/:crc32',
+                    array('action' => 'showbookmark'),
+                    array('user' => '[0-9]+',
+                          'created' => '[0-9]{14}',
+                          'crc32' => '[0-9a-f]{8}'));
+
+        $m->connect('notice/by-url/:id',
+                    array('action' => 'noticebyurl'),
+                    array('id' => '[0-9]+'));
+
+        return true;
+    }
+
+    /**
+     * Output the HTML for a bookmark in a list
+     *
+     * @param NoticeListItem $nli The list item being shown.
+     *
+     * @return boolean hook value
+     */
+
+    function onStartShowNoticeItem($nli)
+    {
+        $nb = Bookmark::getByNotice($nli->notice);
+
+        if (!empty($nb)) {
+
+            $out     = $nli->out;
+            $notice  = $nli->notice;
+            $profile = $nli->profile;
+
+            $atts = $notice->attachments();
+
+            if (count($atts) < 1) {
+                // Something wrong; let default code deal with it.
+                return true;
+            }
+
+            $att = $atts[0];
+
+            // XXX: only show the bookmark URL for non-single-page stuff
+
+            if ($out instanceof ShowbookmarkAction) {
+            } else {
+                $out->elementStart('h3');
+                $out->element('a',
+                              array('href' => $att->url),
+                              $nb->title);
+                $out->elementEnd('h3');
+
+                $countUrl = common_local_url('noticebyurl',
+                                             array('id' => $att->id));
+
+                $out->element('a', array('class' => 'bookmark_notice_count',
+                                         'href' => $countUrl),
+                              $att->noticeCount());
+            }
+
+            $out->elementStart('ul', array('class' => 'bookmark_tags'));
+            
+            // Replies look like "for:" tags
+
+            $replies = $nli->notice->getReplies();
+
+            if (!empty($replies)) {
+                foreach ($replies as $reply) {
+                    $other = Profile::staticGet('id', $reply);
+                    $out->elementStart('li');
+                    $out->element('a', array('rel' => 'tag',
+                                             'href' => $other->profileurl,
+                                             'title' => $other->getBestName()),
+                                  sprintf('for:%s', $other->nickname));
+                    $out->elementEnd('li');
+                    $out->text(' ');
+                }
+            }
+
+            $tags = $nli->notice->getTags();
+
+            foreach ($tags as $tag) {
+                $out->elementStart('li');
+                $out->element('a', 
+                              array('rel' => 'tag',
+                                    'href' => Notice_tag::url($tag)),
+                              $tag);
+                $out->elementEnd('li');
+                $out->text(' ');
+            }
+
+            $out->elementEnd('ul');
+
+            $out->element('p',
+                          array('class' => 'bookmark_description'),
+                          $nb->description);
+
+            if (common_config('attachments', 'show_thumbs')) {
+                $al = new InlineAttachmentList($notice, $out);
+                $al->show();
+            }
+
+            $out->elementStart('p', array('style' => 'float: left'));
+
+            $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
+
+            $out->element('img', array('src' => ($avatar) ?
+                                       $avatar->displayUrl() :
+                                       Avatar::defaultImage(AVATAR_MINI_SIZE),
+                                       'class' => 'avatar photo bookmark_avatar',
+                                       'width' => AVATAR_MINI_SIZE,
+                                       'height' => AVATAR_MINI_SIZE,
+                                       'alt' => $profile->getBestName()));
+            $out->raw('&nbsp;');
+            $out->element('a', array('href' => $profile->profileurl,
+                                     'title' => $profile->getBestName()),
+                          $profile->nickname);
+
+            $nli->showNoticeLink();
+            $nli->showNoticeSource();
+            $nli->showNoticeLocation();
+            $nli->showContext();
+            $nli->showRepeat();
+
+            $out->elementEnd('p');
+
+            $nli->showNoticeOptions();
+
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Render a notice as a Bookmark object
+     *
+     * @param Notice         $notice  Notice to render
+     * @param ActivityObject &$object Empty object to fill
+     *
+     * @return boolean hook value
+     */
+     
+    function onStartActivityObjectFromNotice($notice, &$object)
+    {
+        common_log(LOG_INFO,
+                   "Checking {$notice->uri} to see if it's a bookmark.");
+
+        $nb = Bookmark::getByNotice($notice);
+                                         
+        if (!empty($nb)) {
+
+            common_log(LOG_INFO,
+                       "Formatting notice {$notice->uri} as a bookmark.");
+
+            $object->id      = $notice->uri;
+            $object->type    = ActivityObject::BOOKMARK;
+            $object->title   = $nb->title;
+            $object->summary = $nb->description;
+            $object->link    = $notice->bestUrl();
+
+            // Attributes of the URL
+
+            $attachments = $notice->attachments();
+
+            if (count($attachments) != 1) {
+                throw new ServerException(_('Bookmark notice with the '.
+                                            'wrong number of attachments.'));
+            }
+
+            $target = $attachments[0];
+
+            $attrs = array('rel' => 'related',
+                           'href' => $target->url);
+
+            if (!empty($target->title)) {
+                $attrs['title'] = $target->title;
+            }
+
+            $object->extra[] = array('link', $attrs, null);
+                                                   
+            // Attributes of the thumbnail, if any
+
+            $thumbnail = $target->getThumbnail();
+
+            if (!empty($thumbnail)) {
+                $tattrs = array('rel' => 'preview',
+                                'href' => $thumbnail->url);
+
+                if (!empty($thumbnail->width)) {
+                    $tattrs['media:width'] = $thumbnail->width;
+                }
+
+                if (!empty($thumbnail->height)) {
+                    $tattrs['media:height'] = $thumbnail->height;
+                }
+
+                $object->extra[] = array('link', $attrs, null);
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Add our two queue handlers to the queue manager
+     *
+     * @param QueueManager $qm current queue manager
+     * 
+     * @return boolean hook value
+     */
+
+    function onEndInitializeQueueManager($qm)
+    {
+        $qm->connect('dlcsback', 'DeliciousBackupImporter');
+        $qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
+        return true;
+    }
+
+    /**
+     * Plugin version data
+     *
+     * @param array &$versions array of version data
+     * 
+     * @return value
+     */
+
+    function onPluginVersion(&$versions)
+    {
+        $versions[] = array('name' => 'Sample',
+                            'version' => self::VERSION,
+                            'author' => 'Evan Prodromou',
+                            'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
+                            'rawdescription' =>
+                            _m('Simple extension for supporting bookmarks.'));
+        return true;
+    }
+
+    /**
+     * Load our document if requested
+     *
+     * @param string &$title  Title to fetch
+     * @param string &$output HTML to output
+     *
+     * @return boolean hook value
+     */
+
+    function onStartLoadDoc(&$title, &$output)
+    {
+        if ($title == 'bookmarklet') {
+            $filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
+
+            $c      = file_get_contents($filename);
+            $output = common_markup_to_html($c);
+            return false; // success!
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle a posted bookmark from PuSH
+     *
+     * @param Activity        $activity activity to handle
+     * @param Ostatus_profile $oprofile Profile for the feed
+     *
+     * @return boolean hook value
+     */
+
+    function onStartHandleFeedEntryWithProfile($activity, $oprofile)
+    {
+        common_log(LOG_INFO, "BookmarkPlugin called for new feed entry.");
+
+        if (self::_isPostBookmark($activity)) {
+
+            common_log(LOG_INFO, 
+                       "Importing activity {$activity->id} as a bookmark.");
+
+            $author = $oprofile->checkAuthorship($activity);
+
+            if (empty($author)) {
+                throw new ClientException(_('Can\'t get author for activity.'));
+            }
+
+            self::_postRemoteBookmark($author,
+                                      $activity);
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle a posted bookmark from Salmon
+     *
+     * @param Activity $activity activity to handle
+     * @param mixed    $target   user or group targeted
+     *
+     * @return boolean hook value
+     */
+
+    function onStartHandleSalmonTarget($activity, $target)
+    {
+        if (self::_isPostBookmark($activity)) {
+
+            $this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
+
+            if ($target instanceof User_group) {
+                $uri = $target->getUri();
+                if (!in_array($uri, $activity->context->attention)) {
+                    throw new ClientException(_("Bookmark not posted ".
+                                                "to this group."));
+                }
+            } else if ($target instanceof User) {
+                $uri      = $target->uri;
+                $original = null;
+                if (!empty($activity->context->replyToID)) {
+                    $original = Notice::staticGet('uri', 
+                                                  $activity->context->replyToID); 
+                }
+                if (!in_array($uri, $activity->context->attention) &&
+                    (empty($original) ||
+                     $original->profile_id != $target->id)) {
+                    throw new ClientException(_("Bookmark not posted ".
+                                                "to this user."));
+                }
+            } else {
+                throw new ServerException(_("Don't know how to handle ".
+                                            "this kind of target."));
+            }
+
+            $author = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
+
+            self::_postRemoteBookmark($author,
+                                      $activity);
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle bookmark posted via AtomPub
+     *
+     * @param Activity &$activity Activity that was posted
+     * @param User     $user      User that posted it
+     * @param Notice   &$notice   Resulting notice
+     *
+     * @return boolean hook value
+     */
+
+    function onStartAtomPubNewActivity(&$activity, $user, &$notice)
+    {
+        if (self::_isPostBookmark($activity)) {
+            $options = array('source' => 'atompub');
+            $notice  = self::_postBookmark($user->getProfile(),
+                                           $activity,
+                                           $options);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle bookmark imported from a backup file
+     *
+     * @param User           $user     User to import for
+     * @param ActivityObject $author   Original author per import file
+     * @param Activity       $activity Activity to import
+     * @param boolean        $trusted  Is this a trusted user?
+     * @param boolean        &$done    Is this done (success or unrecoverable error)
+     *
+     * @return boolean hook value
+     */
+
+    function onStartImportActivity($user, $author, $activity, $trusted, &$done)
+    {
+        if (self::_isPostBookmark($activity)) {
+
+            $bookmark = $activity->objects[0];
+
+            $this->log(LOG_INFO,
+                       'Importing Bookmark ' . $bookmark->id . 
+                       ' for user ' . $user->nickname);
+
+            $options = array('uri' => $bookmark->id,
+                             'url' => $bookmark->link,
+                             'source' => 'restore');
+
+            $saved = self::_postBookmark($user->getProfile(), $activity, $options);
+
+            if (!empty($saved)) {
+                $done = true;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Show a link to our delicious import page on profile settings form
+     *
+     * @param Action $action Profile settings action being shown
+     *
+     * @return boolean hook value
+     */
+
+    function onEndProfileSettingsActions($action)
+    {
+        $user = common_current_user();
+        
+        if (!empty($user) && $user->hasRight(self::IMPORTDELICIOUS)) {
+            $action->elementStart('li');
+            $action->element('a',
+                             array('href' => common_local_url('importdelicious')),
+                             _('Import del.icio.us bookmarks'));
+            $action->elementEnd('li');
+        }
+
+        return true;
+    }
+
+    /**
+     * Save a remote bookmark (from Salmon or PuSH)
+     *
+     * @param Ostatus_profile $author   Author of the bookmark
+     * @param Activity        $activity Activity to save
+     *
+     * @return Notice resulting notice.
+     */
+
+    static private function _postRemoteBookmark(Ostatus_profile $author,
+                                                Activity $activity)
+    {
+        $bookmark = $activity->objects[0];
+
+        $options = array('uri' => $bookmark->id,
+                         'url' => $bookmark->link,
+                         'is_local' => Notice::REMOTE_OMB,
+                         'source' => 'ostatus');
+        
+        return self::_postBookmark($author->localProfile(), $activity, $options);
+    }
+
+    /**
+     * Save a bookmark from an activity
+     *
+     * @param Profile  $profile  Profile to use as author
+     * @param Activity $activity Activity to save
+     * @param array    $options  Options to pass to bookmark-saving code
+     *
+     * @return Notice resulting notice
+     */
+
+    static private function _postBookmark(Profile $profile,
+                                          Activity $activity,
+                                          $options=array())
+    {
+        $bookmark = $activity->objects[0];
+
+        $relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
+
+        if (count($relLinkEls) < 1) {
+            throw new ClientException(_('Expected exactly 1 link '.
+                                        'rel=related in a Bookmark.'));
+        }
+
+        if (count($relLinkEls) > 1) {
+            common_log(LOG_WARNING,
+                       "Got too many link rel=related in a Bookmark.");
+        }
+
+        $linkEl = $relLinkEls[0];
+
+        $url = $linkEl->getAttribute('href');
+
+        $tags = array();
+
+        foreach ($activity->categories as $category) {
+            $tags[] = common_canonical_tag($category->term);
+        }
+
+        if (!empty($activity->time)) {
+            $options['created'] = common_sql_date($activity->time);
+        }
+
+        // Fill in location if available
+
+        $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;
+            }
+        }
+
+        $replies = $activity->context->attention;
+
+        $options['groups']  = array();
+        $options['replies'] = array();
+
+        foreach ($replies as $replyURI) {
+            $other = Profile::fromURI($replyURI);
+            if (!empty($other)) {
+                $options['replies'][] = $replyURI;
+            } else {
+                $group = User_group::staticGet('uri', $replyURI);
+                if (!empty($group)) {
+                    $options['groups'][] = $replyURI;
+                }
+            }
+        }
+
+        // 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;
+            }
+        }
+
+        return Bookmark::saveNew($profile,
+                                 $bookmark->title,
+                                 $url,
+                                 $tags,
+                                 $bookmark->summary,
+                                 $options);
+    }
+
+    /**
+     * Test if an activity represents posting a bookmark
+     *
+     * @param Activity $activity Activity to test
+     *
+     * @return true if it's a Post of a Bookmark, else false
+     */
+
+    static private function _isPostBookmark($activity)
+    {
+        return ($activity->verb == ActivityVerb::POST &&
+                $activity->objects[0]->type == ActivityObject::BOOKMARK);
+    }
+}
+
diff --git a/plugins/Bookmark/bookmark.css b/plugins/Bookmark/bookmark.css
new file mode 100644 (file)
index 0000000..b86e749
--- /dev/null
@@ -0,0 +1,4 @@
+.bookmark_tags li { display: inline; }
+.bookmark_mentions li { display: inline; }
+.bookmark_avatar { float: left }
+.bookmark_notice_count { float: right }
diff --git a/plugins/Bookmark/bookmarkform.php b/plugins/Bookmark/bookmarkform.php
new file mode 100644 (file)
index 0000000..b99568e
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Form for adding a new bookmark
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Form to add a new bookmark
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class BookmarkForm extends Form
+{
+    private $_title       = null;
+    private $_url         = null;
+    private $_tags        = null;
+    private $_description = null;
+
+    /**
+     * Construct a bookmark form
+     *
+     * @param HTMLOutputter $out         output channel
+     * @param string        $title       Title of the bookmark
+     * @param string        $url         URL of the bookmark
+     * @param string        $tags        Tags to show
+     * @param string        $description Description of the bookmark
+     *
+     * @return void
+     */
+
+    function __construct($out=null, $title=null, $url=null, $tags=null,
+                         $description=null)
+    {
+        parent::__construct($out);
+
+        $this->_title       = $title;
+        $this->_url         = $url;
+        $this->_tags        = $tags;
+        $this->_description = $description;
+    }
+
+    /**
+     * ID of the form
+     *
+     * @return int ID of the form
+     */
+
+    function id()
+    {
+        return 'form_new_bookmark';
+    }
+
+    /**
+     * class of the form
+     *
+     * @return string class of the form
+     */
+
+    function formClass()
+    {
+        return 'form_settings';
+    }
+
+    /**
+     * Action of the form
+     *
+     * @return string URL of the action
+     */
+
+    function action()
+    {
+        return common_local_url('newbookmark');
+    }
+
+    /**
+     * Data elements of the form
+     *
+     * @return void
+     */
+
+    function formData()
+    {
+        $this->out->elementStart('fieldset', array('id' => 'new_bookmark_data'));
+        $this->out->elementStart('ul', 'form_data');
+
+        $this->li();
+        $this->out->input('title',
+                          _('Title'),
+                          $this->_title,
+                          _('Title of the bookmark'));
+        $this->unli();
+
+        $this->li();
+        $this->out->input('url',
+                          _('URL'),
+                          $this->_url,   
+                          _('URL to bookmark'));
+        $this->unli();
+
+        $this->li();
+        $this->out->input('tags',
+                          _('Tags'),
+                          $this->_tags,   
+                          _('Comma- or space-separated list of tags'));
+        $this->unli();
+
+        $this->li();
+        $this->out->input('description',
+                          _('Description'),
+                          $this->_description,   
+                          _('Description of the URL'));
+        $this->unli();
+
+        $this->out->elementEnd('ul');
+        $this->out->elementEnd('fieldset');
+    }
+
+    /**
+     * Action elements
+     *
+     * @return void
+     */
+
+    function formActions()
+    {
+        $this->out->submit('submit', _m('BUTTON', 'Save'));
+    }
+}
diff --git a/plugins/Bookmark/bookmarklet b/plugins/Bookmark/bookmarklet
new file mode 100644 (file)
index 0000000..fc1f8b9
--- /dev/null
@@ -0,0 +1,9 @@
+<!-- Copyright 2008-2010 StatusNet Inc. and contributors. -->
+<!-- Document licensed under Creative Commons Attribution 3.0 Unported. See -->
+<!-- http://creativecommons.org/licenses/by/3.0/ for details. -->
+
+A bookmarklet is a small piece of javascript code used as a bookmark. This one will let you post to %%site.name%% simply by selecting some text on a page and pressing the bookmarklet.
+
+Drag-and-drop the following link to your bookmarks bar or right-click it and add it to your browser favorites to keep it handy.
+
+<a href="javascript:(function(){var%20d=document,w=window,e=w.getSelection,k=d.getSelection,x=d.selection,s=(e?e():(k)?k():(x?x.createRange().text:0)),f='http://%%site.server%%/%%site.path%%/index.php?action=bookmarkpopup',l=d.location,e=encodeURIComponent,g=f+'&title='+((e(s))?e(s):e(document.title))+'&url='+e(l.href);function%20a(){if(!w.open(g,'t','toolbar=0,resizable=0,scrollbars=1,status=1,width=650,height=470')){l.href=g;}}a();})()">Bookmark on %%site.name%%</a>
diff --git a/plugins/Bookmark/bookmarkpopup.js b/plugins/Bookmark/bookmarkpopup.js
new file mode 100644 (file)
index 0000000..29f314e
--- /dev/null
@@ -0,0 +1,23 @@
+$(document).ready(
+    function() {
+       var form = $('#form_new_bookmark');
+        form.append('<input type="hidden" name="ajax" value="1"/>');
+        form.ajaxForm({dataType: 'xml',
+                      timeout: '60000',
+                       beforeSend: function(formData) {
+                          form.addClass('processing');
+                          form.find('#submit').addClass('disabled');
+                      },
+                       error: function (xhr, textStatus, errorThrown) {
+                          form.removeClass('processing');
+                          form.find('#submit').removeClass('disabled');
+                          self.close();
+                      },
+                       success: function(data, textStatus) {
+                          form.removeClass('processing');
+                          form.find('#submit').removeClass('disabled');
+                           self.close();
+                       }});
+
+    }
+);
\ No newline at end of file
diff --git a/plugins/Bookmark/bookmarkpopup.php b/plugins/Bookmark/bookmarkpopup.php
new file mode 100644 (file)
index 0000000..24ed796
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Post a new bookmark in a popup window
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Sarven Capadisli <csarven@status.net>
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2008-2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * Action for posting a new bookmark
+ *
+ * @category Bookmark
+ * @package  StatusNet
+ * @author   Sarven Capadisli <csarven@status.net>
+ * @author   Evan Prodromou <evan@status.net>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link     http://status.net/
+ */
+class BookmarkpopupAction extends NewbookmarkAction
+{
+    /**
+     * Show the title section of the window
+     *
+     * @return void
+     */
+
+    function showTitle()
+    {
+        // TRANS: Title for mini-posting window loaded from bookmarklet.
+        // TRANS: %s is the StatusNet site name.
+        $this->element('title', 
+                       null, sprintf(_('Bookmark on %s'), 
+                                     common_config('site', 'name')));
+    }
+
+    /**
+     * Show the header section of the page
+     *
+     * Shows a stub page and the bookmark form.
+     *
+     * @return void
+     */
+
+    function showHeader()
+    {
+        $this->elementStart('div', array('id' => 'header'));
+        $this->elementStart('address');
+        $this->element('a', array('class' => 'url',
+                                  'href' => common_local_url('public')),
+                         '');
+        $this->elementEnd('address');
+        if (common_logged_in()) {
+            $form = new BookmarkForm($this,
+                                     $this->title,
+                                     $this->url);
+            $form->show();
+        }
+        $this->elementEnd('div');
+    }
+
+    /**
+     * Hide the core section of the page
+     * 
+     * @return void
+     */
+
+    function showCore()
+    {
+    }
+
+    /**
+     * Hide the footer section of the page
+     *
+     * @return void
+     */
+
+    function showFooter()
+    {
+    }
+
+    function showScripts()
+    {
+        parent::showScripts();
+        $this->script(common_path('plugins/Bookmark/bookmarkpopup.js'));
+    }
+}
diff --git a/plugins/Bookmark/deliciousbackupimporter.php b/plugins/Bookmark/deliciousbackupimporter.php
new file mode 100644 (file)
index 0000000..1b55115
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Importer class for Delicious.com backups
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Importer class for Delicious bookmarks
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class DeliciousBackupImporter extends QueueHandler
+{
+    /**
+     * Transport of the importer
+     *
+     * @return string transport string
+     */
+
+    function transport()
+    {
+        return 'dlcsback';
+    }
+
+    /**
+     * Import an in-memory bookmark list to a user's account
+     *
+     * Take a delicious.com backup file (same as Netscape bookmarks.html)
+     * and import to StatusNet as Bookmark activities.
+     *
+     * The document format is terrible. It consists of a <dl> with
+     * a bunch of <dt>'s, occasionally with <dd>'s.
+     * There are sometimes <p>'s lost inside.
+     *
+     * @param array $data pair of user, text
+     *
+     * @return boolean success value
+     */
+
+    function handle($data)
+    {
+        list($user, $body) = $data;
+
+        $doc = $this->importHTML($body);
+
+        $dls = $doc->getElementsByTagName('dl');
+
+        if ($dls->length != 1) {
+            throw new ClientException(_("Bad import file."));
+        }
+
+        $dl = $dls->item(0);
+
+        $children = $dl->childNodes;
+
+        $dt = null;
+
+        for ($i = 0; $i < $children->length; $i++) {
+            try {
+                $child = $children->item($i);
+                if ($child->nodeType != XML_ELEMENT_NODE) {
+                    continue;
+                }
+                switch (strtolower($child->tagName)) {
+                case 'dt':
+                    if (!empty($dt)) {
+                        // No DD provided
+                        $this->importBookmark($user, $dt);
+                        $dt = null;
+                    }
+                    $dt = $child;
+                    break;
+                case 'dd':
+                    $dd = $child;
+
+                    $saved = $this->importBookmark($user, $dt, $dd);
+
+                    $dt = null;
+                    $dd = null;
+                case 'p':
+                    common_log(LOG_INFO, 'Skipping the <p> in the <dl>.');
+                    break;
+                default:
+                    common_log(LOG_WARNING, 
+                               "Unexpected element $child->tagName ".
+                               " found in import.");
+                }
+            } catch (Exception $e) {
+                common_log(LOG_ERR, $e->getMessage());
+                $dt = $dd = null;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Import a single bookmark
+     * 
+     * Takes a <dt>/<dd> pair. The <dt> has a single
+     * <a> in it with some non-standard attributes.
+     * 
+     * A <dt><dt><dd> sequence will appear as a <dt> with
+     * anothe <dt> as a child. We handle this case recursively. 
+     *
+     * @param User       $user User to import data as
+     * @param DOMElement $dt   <dt> element
+     * @param DOMElement $dd   <dd> element
+     *
+     * @return Notice imported notice
+     */
+
+    function importBookmark($user, $dt, $dd = null)
+    {
+        // We have to go squirrelling around in the child nodes
+        // on the off chance that we've received another <dt>
+        // as a child.
+
+        for ($i = 0; $i < $dt->childNodes->length; $i++) {
+            $child = $dt->childNodes->item($i);
+            if ($child->nodeType == XML_ELEMENT_NODE) {
+                if ($child->tagName == 'dt' && !is_null($dd)) {
+                    $this->importBookmark($user, $dt);
+                    $this->importBookmark($user, $child, $dd);
+                    return;
+                }
+            }
+        }
+
+        $qm = QueueManager::get();
+        
+        $qm->enqueue(array($user, $dt, $dd), 'dlcsbkmk');
+    }
+
+    /**
+     * Parse some HTML
+     *
+     * Hides the errors that the dom parser returns
+     *
+     * @param string $body Data to import
+     *
+     * @return DOMDocument parsed document
+     */
+
+    function importHTML($body)
+    {
+        // DOMDocument::loadHTML may throw warnings on unrecognized elements,
+        // and notices on unrecognized namespaces.
+        $old = error_reporting(error_reporting() & ~(E_WARNING | E_NOTICE));
+        $dom = new DOMDocument();
+        $ok  = $dom->loadHTML($body);
+        error_reporting($old);
+
+        if ($ok) {
+            return $dom;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/plugins/Bookmark/deliciousbookmarkimporter.php b/plugins/Bookmark/deliciousbookmarkimporter.php
new file mode 100644 (file)
index 0000000..297ef81
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Importer class for Delicious.com bookmarks
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Importer class for Delicious bookmarks
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class DeliciousBookmarkImporter extends QueueHandler
+{
+    /**
+     * Return the transport for this queue handler
+     *
+     * @return string 'dlcsbkmk'
+     */
+
+    function transport()
+    {
+        return 'dlcsbkmk';
+    }
+
+    /**
+     * Handle the data
+     * 
+     * @param array $data array of user, dt, dd
+     *
+     * @return boolean success value
+     */
+
+    function handle($data)
+    {
+        list($user, $dt, $dd) = $data;
+
+        $as = $dt->getElementsByTagName('a');
+
+        if ($as->length == 0) {
+            throw new ClientException(_("No <A> tag in a <DT>."));
+        }
+
+        $a = $as->item(0);
+                    
+        $private = $a->getAttribute('private');
+
+        if ($private != 0) {
+            throw new ClientException(_('Skipping private bookmark.'));
+        }
+
+        if (!empty($dd)) {
+            $description = $dd->nodeValue;
+        } else {
+            $description = null;
+        }
+
+        $title   = $a->nodeValue;
+        $url     = $a->getAttribute('href');
+        $tags    = $a->getAttribute('tags');
+        $addDate = $a->getAttribute('add_date');
+        $created = common_sql_date(intval($addDate));
+
+        $saved = Bookmark::saveNew($user->getProfile(),
+                                   $title,
+                                   $url,
+                                   $tags,
+                                   $description,
+                                   array('created' => $created,
+                                         'distribute' => false));
+
+        return true;
+    }
+}
diff --git a/plugins/Bookmark/importbookmarks.php b/plugins/Bookmark/importbookmarks.php
new file mode 100644 (file)
index 0000000..5518b00
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010 StatusNet, Inc.
+ *
+ * Import a bookmarks file as notices
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
+
+$shortoptions = 'i:n:f:';
+$longoptions  = array('id=', 'nickname=', 'file=');
+
+$helptext = <<<END_OF_IMPORTBOOKMARKS_HELP
+importbookmarks.php [options]
+Restore a backed-up Delicious.com bookmark file
+
+-i --id       ID of user to import bookmarks for
+-n --nickname nickname of the user to import for
+-f --file     file to read from (STDIN by default)
+END_OF_IMPORTBOOKMARKS_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+/**
+ * Get the bookmarks file as a string
+ * 
+ * Uses the -f or --file parameter to open and read a
+ * a bookmarks file
+ *
+ * @return string Contents of the file
+ */
+
+function getBookmarksFile()
+{
+    $filename = get_option_value('f', 'file');
+
+    if (empty($filename)) {
+        show_help();
+        exit(1);
+    }
+
+    if (!file_exists($filename)) {
+        throw new Exception("No such file '$filename'.");
+    }
+
+    if (!is_file($filename)) {
+        throw new Exception("Not a regular file: '$filename'.");
+    }
+
+    if (!is_readable($filename)) {
+        throw new Exception("File '$filename' not readable.");
+    }
+
+    // TRANS: %s is the filename that contains a backup for a user.
+    printfv(_("Getting backup from file '%s'.")."\n", $filename);
+
+    $html = file_get_contents($filename);
+
+    return $html;
+}
+
+try {
+    $user = getUser();
+    $html = getBookmarksFile();
+
+    $qm = QueueManager::get();
+    
+    $qm->enqueue(array($user, $html), 'dlcsback');
+
+} catch (Exception $e) {
+    print $e->getMessage()."\n";
+    exit(1);
+}
diff --git a/plugins/Bookmark/importdelicious.php b/plugins/Bookmark/importdelicious.php
new file mode 100644 (file)
index 0000000..f8529cc
--- /dev/null
@@ -0,0 +1,336 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Import del.icio.us bookmarks backups
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * UI for importing del.icio.us bookmark backups
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class ImportdeliciousAction extends Action
+{
+    protected $success = false;
+
+    /**
+     * Return the title of the page
+     *
+     * @return string page title
+     */
+
+    function title()
+    {
+        return _("Import del.icio.us bookmarks");
+    }
+
+    /**
+     * For initializing members of the class.
+     *
+     * @param array $argarray misc. arguments
+     *
+     * @return boolean true
+     */
+
+    function prepare($argarray)
+    {
+        parent::prepare($argarray);
+
+        $cur = common_current_user();
+
+        if (empty($cur)) {
+            throw new ClientException(_('Only logged-in users can '.
+                                        'import del.icio.us backups.'),
+                                      403);
+        }
+
+        if (!$cur->hasRight(BookmarkPlugin::IMPORTDELICIOUS)) {
+            throw new ClientException(_('You may not restore your account.'), 403);
+        }
+
+        return true;
+    }
+
+    /**
+     * Handler method
+     *
+     * @param array $argarray is ignored since it's now passed in in prepare()
+     *
+     * @return void
+     */
+
+    function handle($argarray=null)
+    {
+        parent::handle($argarray);
+
+        if ($this->isPost()) {
+            $this->importDelicious();
+        } else {
+            $this->showPage();
+        }
+        return;
+    }
+
+    /**
+     * Queue a file for importation
+     * 
+     * Uses the DeliciousBackupImporter class; may take a long time!
+     *
+     * @return void
+     */
+
+    function importDelicious()
+    {
+        $this->checkSessionToken();
+
+        if (!isset($_FILES[ImportDeliciousForm::FILEINPUT]['error'])) {
+            throw new ClientException(_('No uploaded file.'));
+        }
+
+        switch ($_FILES[ImportDeliciousForm::FILEINPUT]['error']) {
+        case UPLOAD_ERR_OK: // success, jump out
+            break;
+        case UPLOAD_ERR_INI_SIZE:
+            // TRANS: Client exception thrown when an uploaded file is too large.
+            throw new ClientException(_('The uploaded file exceeds the ' .
+                'upload_max_filesize directive in php.ini.'));
+            return;
+        case UPLOAD_ERR_FORM_SIZE:
+            throw new ClientException(
+                // TRANS: Client exception.
+                _('The uploaded file exceeds the MAX_FILE_SIZE directive' .
+                ' that was specified in the HTML form.'));
+            return;
+        case UPLOAD_ERR_PARTIAL:
+            @unlink($_FILES[ImportDeliciousForm::FILEINPUT]['tmp_name']);
+            // TRANS: Client exception.
+            throw new ClientException(_('The uploaded file was only' .
+                ' partially uploaded.'));
+            return;
+        case UPLOAD_ERR_NO_FILE:
+            // No file; probably just a non-AJAX submission.
+            throw new ClientException(_('No uploaded file.'));
+            return;
+        case UPLOAD_ERR_NO_TMP_DIR:
+            // TRANS: Client exception thrown when a temporary folder is not present
+            throw new ClientException(_('Missing a temporary folder.'));
+            return;
+        case UPLOAD_ERR_CANT_WRITE:
+            // TRANS: Client exception thrown when writing to disk is not possible
+            throw new ClientException(_('Failed to write file to disk.'));
+            return;
+        case UPLOAD_ERR_EXTENSION:
+            // TRANS: Client exception thrown when a file upload has been stopped
+            throw new ClientException(_('File upload stopped by extension.'));
+            return;
+        default:
+            common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " .
+                $_FILES[ImportDeliciousForm::FILEINPUT]['error']);
+            // TRANS: Client exception thrown when a file upload operation has failed
+            throw new ClientException(_('System error uploading file.'));
+            return;
+        }
+
+        $filename = $_FILES[ImportDeliciousForm::FILEINPUT]['tmp_name'];
+
+        try {
+            if (!file_exists($filename)) {
+                throw new ServerException("No such file '$filename'.");
+            }
+        
+            if (!is_file($filename)) {
+                throw new ServerException("Not a regular file: '$filename'.");
+            }
+        
+            if (!is_readable($filename)) {
+                throw new ServerException("File '$filename' not readable.");
+            }
+        
+            common_debug(sprintf(_("Getting backup from file '%s'."), $filename));
+
+            $html = file_get_contents($filename);
+
+            // Enqueue for processing.
+
+            $qm = QueueManager::get();
+            $qm->enqueue(array(common_current_user(), $html), 'dlcsback');
+
+            $this->success = true;
+
+            $this->showPage();
+
+        } catch (Exception $e) {
+            // Delete the file and re-throw
+            @unlink($_FILES[ImportDeliciousForm::FILEINPUT]['tmp_name']);
+            throw $e;
+        }
+    }
+
+    /**
+     * Show the content of the page
+     *
+     * @return void
+     */
+
+    function showContent()
+    {
+        if ($this->success) {
+            $this->element('p', null,
+                           _('Feed will be restored. '.
+                             'Please wait a few minutes for results.'));
+        } else {
+            $form = new ImportDeliciousForm($this);
+            $form->show();
+        }
+    }
+
+    /**
+     * Return true if read only.
+     *
+     * MAY override
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean is read only action?
+     */
+
+    function isReadOnly($args)
+    {
+        return !$this->isPost();
+    }
+}
+
+/**
+ * A form for backing up the account.
+ *
+ * @category  Account
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class ImportDeliciousForm extends Form
+{
+    const FILEINPUT = 'deliciousbackupfile';
+
+    /**
+     * Constructor
+     * 
+     * Set the encoding type, since this is a file upload.
+     *
+     * @param HTMLOutputter $out output channel
+     *
+     * @return ImportDeliciousForm this
+     */
+
+    function __construct($out=null)
+    {
+        parent::__construct($out);
+        $this->enctype = 'multipart/form-data';
+    }
+
+    /**
+     * Class of the form.
+     *
+     * @return string the form's class
+     */
+
+    function formClass()
+    {
+        return 'form_import_delicious';
+    }
+
+    /**
+     * URL the form posts to
+     *
+     * @return string the form's action URL
+     */
+
+    function action()
+    {
+        return common_local_url('importdelicious');
+    }
+
+    /**
+     * Output form data
+     * 
+     * Really, just instructions for doing a backup.
+     *
+     * @return void
+     */
+
+    function formData()
+    {
+        $this->out->elementStart('p', 'instructions');
+
+        $this->out->raw(_('You can upload a backed-up '.
+                          'delicious.com bookmarks file.'));
+        
+        $this->out->elementEnd('p');
+
+        $this->out->elementStart('ul', 'form_data');
+
+        $this->out->elementStart('li', array ('id' => 'settings_attach'));
+        $this->out->element('input', array('name' => self::FILEINPUT,
+                                           'type' => 'file',
+                                           'id' => self::FILEINPUT));
+        $this->out->elementEnd('li');
+
+        $this->out->elementEnd('ul');
+    }
+
+    /**
+     * Buttons for the form
+     * 
+     * In this case, a single submit button
+     *
+     * @return void
+     */
+
+    function formActions()
+    {
+        $this->out->submit('submit',
+                           _m('BUTTON', 'Upload'),
+                           'submit',
+                           null,
+                           _('Upload the file'));
+    }
+}
diff --git a/plugins/Bookmark/newbookmark.php b/plugins/Bookmark/newbookmark.php
new file mode 100644 (file)
index 0000000..a0cf3ff
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Add a new bookmark
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Add a new bookmark
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class NewbookmarkAction extends Action
+{
+    protected $user        = null;
+    protected $error       = null;
+    protected $complete    = null;
+    protected $title       = null;
+    protected $url         = null;
+    protected $tags        = null;
+    protected $description = null;
+
+    /**
+     * Returns the title of the action
+     *
+     * @return string Action title
+     */
+
+    function title()
+    {
+        return _('New bookmark');
+    }
+
+    /**
+     * For initializing members of the class.
+     *
+     * @param array $argarray misc. arguments
+     *
+     * @return boolean true
+     */
+
+    function prepare($argarray)
+    {
+        parent::prepare($argarray);
+
+        $this->user = common_current_user();
+
+        if (empty($this->user)) {
+            throw new ClientException(_("Must be logged in to post a bookmark."),
+                                      403);
+        }
+
+        if ($this->isPost()) {
+            $this->checkSessionToken();
+        }
+
+        $this->title       = $this->trimmed('title');
+        $this->url         = $this->trimmed('url');
+        $this->tags        = $this->trimmed('tags');
+        $this->description = $this->trimmed('description');
+
+        return true;
+    }
+
+    /**
+     * Handler method
+     *
+     * @param array $argarray is ignored since it's now passed in in prepare()
+     *
+     * @return void
+     */
+
+    function handle($argarray=null)
+    {
+        parent::handle($argarray);
+
+        if ($this->isPost()) {
+            $this->newBookmark();
+        } else {
+            $this->showPage();
+        }
+
+        return;
+    }
+
+    /**
+     * Add a new bookmark
+     *
+     * @return void
+     */
+
+    function newBookmark()
+    {
+        try {
+            if (empty($this->title)) {
+                throw new ClientException(_('Bookmark must have a title.'));
+            }
+
+            if (empty($this->url)) {
+                throw new ClientException(_('Bookmark must have an URL.'));
+            }
+
+
+            $saved = Bookmark::saveNew($this->user->getProfile(),
+                                              $this->title,
+                                              $this->url,
+                                              $this->tags,
+                                              $this->description);
+
+        } catch (ClientException $ce) {
+            $this->error = $ce->getMessage();
+            $this->showPage();
+            return;
+        }
+
+        common_redirect($saved->bestUrl(), 303);
+    }
+
+    /**
+     * Show the bookmark form
+     *
+     * @return void
+     */
+
+    function showContent()
+    {
+        if (!empty($this->error)) {
+            $this->element('p', 'error', $this->error);
+        }
+
+        $form = new BookmarkForm($this,
+                                 $this->title,
+                                 $this->url,
+                                 $this->tags,
+                                 $this->description);
+
+        $form->show();
+
+        return;
+    }
+
+    /**
+     * Return true if read only.
+     *
+     * MAY override
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean is read only action?
+     */
+
+    function isReadOnly($args)
+    {
+        if ($_SERVER['REQUEST_METHOD'] == 'GET' ||
+            $_SERVER['REQUEST_METHOD'] == 'HEAD') {
+            return true;
+        } else {
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/plugins/Bookmark/noticebyurl.php b/plugins/Bookmark/noticebyurl.php
new file mode 100644 (file)
index 0000000..226c7a3
--- /dev/null
@@ -0,0 +1,177 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Notice stream of notices with a given attachment
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * List notices that contain/link to/use a given URL
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class NoticebyurlAction extends Action
+{
+    protected $url     = null;
+    protected $file    = null;
+    protected $notices = null;
+    protected $page    = null;
+
+    /**
+     * For initializing members of the class.
+     *
+     * @param array $argarray misc. arguments
+     *
+     * @return boolean true
+     */
+
+    function prepare($argarray)
+    {
+        parent::prepare($argarray);
+        
+        $this->file = File::staticGet('id', $this->trimmed('id'));
+
+        if (empty($this->file)) {
+            throw new ClientException(_('Unknown URL'));
+        }
+
+        $pageArg = $this->trimmed('page');
+
+        $this->page = (empty($pageArg)) ? 1 : intval($pageArg);
+
+        $this->notices = $this->file->stream(($this->page - 1) * NOTICES_PER_PAGE,
+                                             NOTICES_PER_PAGE + 1);
+
+        return true;
+    }
+
+    /**
+     * Title of the page
+     *
+     * @return string page title
+     */
+
+    function title()
+    {
+        if ($this->page == 1) {
+            return sprintf(_("Notices linking to %s"), $this->file->url);
+        } else {
+            return sprintf(_("Notices linking to %s, page %d"),
+                           $this->file->url,
+                           $this->page);
+        }
+    }
+
+    /**
+     * Handler method
+     *
+     * @param array $argarray is ignored since it's now passed in in prepare()
+     *
+     * @return void
+     */
+
+    function handle($argarray=null)
+    {
+        $this->showPage();
+    }
+
+    /**
+     * Show main page content.
+     *
+     * Shows a list of the notices that link to the given URL
+     *
+     * @return void
+     */
+
+    function showContent()
+    {
+        $nl = new NoticeList($this->notices, $this);
+
+        $nl->show();
+
+        $cnt = $nl->show();
+
+        $this->pagination($this->page > 1,
+                          $cnt > NOTICES_PER_PAGE,
+                          $this->page,
+                          'noticebyurl',
+                          array('id' => $this->file->id));
+    }
+
+    /**
+     * Return true if read only.
+     *
+     * MAY override
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean is read only action?
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * Return last modified, if applicable.
+     *
+     * MAY override
+     *
+     * @return string last modified http header
+     */
+    function lastModified()
+    {
+        // For comparison with If-Last-Modified
+        // If not applicable, return null
+        return null;
+    }
+
+    /**
+     * Return etag, if applicable.
+     *
+     * MAY override
+     *
+     * @return string etag http header
+     */
+
+    function etag()
+    {
+        return null;
+    }
+}
diff --git a/plugins/Bookmark/showbookmark.php b/plugins/Bookmark/showbookmark.php
new file mode 100644 (file)
index 0000000..e9e656f
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Show a single bookmark
+ * 
+ * PHP version 5
+ *
+ * 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  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Show a single bookmark, with associated information
+ *
+ * @category  Bookmark
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @copyright 2010 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+class ShowbookmarkAction extends ShownoticeAction
+{
+    protected $bookmark = null;
+
+    /**
+     * For initializing members of the class.
+     *
+     * @param array $argarray misc. arguments
+     *
+     * @return boolean true
+     */
+
+    function prepare($argarray)
+    {
+        OwnerDesignAction::prepare($argarray);
+
+        $this->user = User::staticGet('id', $this->trimmed('user'));
+
+        if (empty($this->user)) {
+            throw new ClientException(_('No such user.'), 404);
+        }
+
+        $this->profile = $this->user->getProfile();
+
+        if (empty($this->profile)) {
+            throw new ServerException(_('User without a profile.'));
+        }
+
+        $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
+
+        sscanf($this->trimmed('crc32'), '%08x', $crc32);
+
+        if (empty($crc32)) {
+            throw new ClientException(_('No such URL.'), 404);
+        }
+        
+        $dt = new DateTime($this->trimmed('created'),
+                           new DateTimeZone('UTC'));
+
+        if (empty($dt)) {
+            throw new ClientException(_('No such create date.'), 404);
+        }
+
+        $bookmarks = Bookmark::getByCRC32($this->profile,
+                                          $crc32);
+
+        foreach ($bookmarks as $bookmark) {
+            $bdt = new DateTime($bookmark->created, new DateTimeZone('UTC'));
+            if ($bdt->format('U') == $dt->format('U')) {
+                $this->bookmark = $bookmark;
+                break;
+            }
+        } 
+
+        if (empty($this->bookmark)) {
+            throw new ClientException(_('No such bookmark.'), 404);
+        }
+
+        $this->notice = Notice::staticGet('uri', $this->bookmark->uri);
+
+        if (empty($this->notice)) {
+            // Did we used to have it, and it got deleted?
+            throw new ClientException(_('No such bookmark.'), 404);
+        }
+
+        return true;
+    }
+
+    /**
+     * Title of the page
+     *
+     * Used by Action class for layout.
+     *
+     * @return string page tile
+     */
+
+    function title()
+    {
+        return sprintf(_('%s\'s bookmark for "%s"'),
+                       $this->user->nickname,
+                       $this->bookmark->title);
+    }
+
+    /**
+     * Overload page title display to show bookmark link
+     *
+     * @return void
+     */
+
+    function showPageTitle()
+    {
+        $this->elementStart('h1');
+        $this->element('a',
+                       array('href' => $this->bookmark->url),
+                       $this->bookmark->title);
+        $this->elementEnd('h1');
+    }
+}
index 3a3d63fe20568a11e29ecf899117db137f628c11..024f0cc217a6000cb56cda8526bb601e7bcf90b1 100644 (file)
@@ -47,6 +47,9 @@ class GroupsalmonAction extends SalmonAction
             $this->clientError(_m('No such group.'));
         }
 
+
+        $this->target = $this->group;
+
         $oprofile = Ostatus_profile::staticGet('group_id', $id);
         if ($oprofile) {
             // TRANS: Client error.
index e78c653300da11dd504afa0e21aa1a63badda4c2..5355aeba03fed5b2eca9c762709d3466aae00f4b 100644 (file)
@@ -43,6 +43,8 @@ class UsersalmonAction extends SalmonAction
             $this->clientError(_m('No such user.'));
         }
 
+        $this->target = $this->user;
+
         return true;
     }
 
index 77cf57a6708b4b951a5808bbb7fcf49bdf981abd..9c0f014fc6464cea41cb2c2bbc09c4b1fea73531 100644 (file)
@@ -457,7 +457,8 @@ class Ostatus_profile extends Memcached_DataObject
     {
         $activity = new Activity($entry, $feed);
 
-        if (Event::handle('StartHandleFeedEntry', array($activity))) {
+        if (Event::handle('StartHandleFeedEntryWithProfile', array($activity, $this)) &&
+            Event::handle('StartHandleFeedEntry', array($activity))) {
 
             // @todo process all activity objects
             switch ($activity->objects[0]->type) {
@@ -479,6 +480,7 @@ class Ostatus_profile extends Memcached_DataObject
             }
 
             Event::handle('EndHandleFeedEntry', array($activity));
+            Event::handle('EndHandleFeedEntryWithProfile', array($activity, $this));
         }
     }
 
@@ -491,36 +493,10 @@ class Ostatus_profile extends Memcached_DataObject
      */
     public function processPost($activity, $method)
     {
-        if ($this->isGroup()) {
-            // A group feed will contain posts from multiple authors.
-            // @fixme validate these profiles in some way!
-            $oprofile = self::ensureActorProfile($activity);
-            if ($oprofile->isGroup()) {
-                // Groups can't post notices in StatusNet.
-                common_log(LOG_WARNING, "OStatus: skipping post with group listed as author: $oprofile->uri in feed from $this->uri");
-                return false;
-            }
-        } else {
-            $actor = $activity->actor;
+        $oprofile = $this->checkAuthorship($activity);
 
-            if (empty($actor)) {
-                // OK here! assume the default
-            } else if ($actor->id == $this->uri || $actor->link == $this->uri) {
-                $this->updateFromActivityObject($actor);
-            } else if ($actor->id) {
-                // We have an ActivityStreams actor with an explicit ID that doesn't match the feed owner.
-                // This isn't what we expect from mainline OStatus person feeds!
-                // Group feeds go down another path, with different validation...
-                // Most likely this is a plain ol' blog feed of some kind which
-                // doesn't match our expectations. We'll take the entry, but ignore
-                // the <author> info.
-                common_log(LOG_WARNING, "Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}");
-            } else {
-                // Plain <author> without ActivityStreams actor info.
-                // We'll just ignore this info for now and save the update under the feed's identity.
-            }
-
-            $oprofile = $this;
+        if (empty($oprofile)) {
+            return false;
         }
 
         // It's not always an ActivityObject::NOTE, but... let's just say it is.
@@ -1810,6 +1786,45 @@ class Ostatus_profile extends Memcached_DataObject
         }
         return $oprofile;
     }
+
+    function checkAuthorship($activity)
+    {
+        if ($this->isGroup()) {
+            // A group feed will contain posts from multiple authors.
+            // @fixme validate these profiles in some way!
+            $oprofile = self::ensureActorProfile($activity);
+            if ($oprofile->isGroup()) {
+                // Groups can't post notices in StatusNet.
+                common_log(LOG_WARNING, 
+                           "OStatus: skipping post with group listed as author: ".
+                           "$oprofile->uri in feed from $this->uri");
+                return false;
+            }
+        } else {
+            $actor = $activity->actor;
+
+            if (empty($actor)) {
+                // OK here! assume the default
+            } else if ($actor->id == $this->uri || $actor->link == $this->uri) {
+                $this->updateFromActivityObject($actor);
+            } else if ($actor->id) {
+                // We have an ActivityStreams actor with an explicit ID that doesn't match the feed owner.
+                // This isn't what we expect from mainline OStatus person feeds!
+                // Group feeds go down another path, with different validation...
+                // Most likely this is a plain ol' blog feed of some kind which
+                // doesn't match our expectations. We'll take the entry, but ignore
+                // the <author> info.
+                common_log(LOG_WARNING, "Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}");
+            } else {
+                // Plain <author> without ActivityStreams actor info.
+                // We'll just ignore this info for now and save the update under the feed's identity.
+            }
+
+            $oprofile = $this;
+        }
+
+        return $oprofile;
+    }
 }
 
 /**
index 41bdb489284a75072682c3351f7f748995682711..8bfd7c8261e81ef3ca021d481854c63468289aa3 100644 (file)
@@ -30,6 +30,7 @@ class SalmonAction extends Action
 {
     var $xml      = null;
     var $activity = null;
+    var $target   = null;
 
     function prepare($args)
     {
@@ -82,7 +83,8 @@ class SalmonAction extends Action
         StatusNet::setApi(true); // Send smaller error pages
 
         common_log(LOG_DEBUG, "Got a " . $this->activity->verb);
-        if (Event::handle('StartHandleSalmon', array($this->activity))) {
+        if (Event::handle('StartHandleSalmonTarget', array($this->activity, $this->target)) &&
+            Event::handle('StartHandleSalmon', array($this->activity))) {
             switch ($this->activity->verb)
             {
             case ActivityVerb::POST:
@@ -118,6 +120,7 @@ class SalmonAction extends Action
                 throw new ClientException(_m("Unrecognized activity type."));
             }
             Event::handle('EndHandleSalmon', array($this->activity));
+            Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target));
         }
     }