]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' into socialbookmark
authorEvan Prodromou <evan@status.net>
Tue, 28 Dec 2010 06:13:23 +0000 (22:13 -0800)
committerEvan Prodromou <evan@status.net>
Tue, 28 Dec 2010 06:13:23 +0000 (22:13 -0800)
19 files changed:
classes/Memcached_DataObject.php
classes/Notice_tag.php
lib/oembedhelper.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/newbookmark.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 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 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 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
diff --git a/plugins/Bookmark/Bookmark.php b/plugins/Bookmark/Bookmark.php
new file mode 100644 (file)
index 0000000..684532d
--- /dev/null
@@ -0,0 +1,344 @@
+<?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 (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:') {
+                if (!array_key_exists('replies', $options)) { // skip if done by caller
+                    $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..470ddaa
--- /dev/null
@@ -0,0 +1,622 @@
+<?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';
+
+    /**
+     * 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':
+            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('bookmark/:user/:created/:crc32',
+                    array('action' => 'showbookmark'),
+                    array('user' => '[0-9]+',
+                          'created' => '[0-9]{14}',
+                          'crc32' => '[0-9a-f]{8}'));
+
+        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');
+            }
+
+            $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;
+    }
+
+    function onStartAtomPubNewActivity(&$activity, $user)
+    {
+        if (self::_isPostBookmark($activity)) {
+            $options = array('source' => 'atompub');
+            self::_postBookmark($user->getProfile(), $activity, $options);
+            return false;
+        }
+
+        return true;
+    }
+
+    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);
+    }
+
+    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);
+    }
+
+    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..27d716d
--- /dev/null
@@ -0,0 +1,3 @@
+.bookmark_tags li { display: inline; }
+.bookmark_mentions li { display: inline; }
+.bookmark_avatar { float: left }
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..061572d
--- /dev/null
@@ -0,0 +1,108 @@
+<?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));
+
+        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/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/showbookmark.php b/plugins/Bookmark/showbookmark.php
new file mode 100644 (file)
index 0000000..07dec5a
--- /dev/null
@@ -0,0 +1,132 @@
+<?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 = DateTime::createFromFormat('YmdHis',
+                                         $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->getTimestamp() == $dt->getTimestamp()) {
+                $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;
+    }
+
+    function title()
+    {
+        return sprintf(_('%s\'s bookmark for "%s"'),
+                       $this->user->nickname,
+                       $this->bookmark->title);
+    }
+
+    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));
         }
     }