diff --git a/EVENTS.txt b/EVENTS.txt index 65265cdf0f..f7cc7df67c 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -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) diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 42988a00f6..5809df3b5e 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -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)) { diff --git a/actions/newgroup.php b/actions/newgroup.php index 95af6415e5..42d488e54e 100644 --- a/actions/newgroup.php +++ b/actions/newgroup.php @@ -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; } diff --git a/actions/profilesettings.php b/actions/profilesettings.php index 8f55a47189..19fbdbd293 100644 --- a/actions/profilesettings.php +++ b/actions/profilesettings.php @@ -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'); } } diff --git a/classes/File.php b/classes/File.php index ef9dbf14ab..29a8f0f1c5 100644 --- a/classes/File.php +++ b/classes/File.php @@ -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; + } } diff --git a/classes/File_to_post.php b/classes/File_to_post.php index 530921adcb..bcb6771f4f 100644 --- a/classes/File_to_post.php +++ b/classes/File_to_post.php @@ -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(); + } } diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index eb5d2627f2..d50b4071d1 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -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) { diff --git a/classes/Notice.php b/classes/Notice.php index 50909f9707..561999966c 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -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); } diff --git a/classes/Notice_tag.php b/classes/Notice_tag.php index bb67c8f819..f795bfc601 100644 --- a/classes/Notice_tag.php +++ b/classes/Notice_tag.php @@ -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; + } } diff --git a/classes/Profile.php b/classes/Profile.php index 972351a75b..adad0c6157 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -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: diff --git a/classes/User_group.php b/classes/User_group.php index cffc786458..68f61cb7f4 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -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); diff --git a/lib/activityimporter.php b/lib/activityimporter.php index 4a76781328..b3b7ffb066 100644 --- a/lib/activityimporter.php +++ b/lib/activityimporter.php @@ -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) diff --git a/lib/oembedhelper.php b/lib/oembedhelper.php index 84cf105867..3cd20c8e8e 100644 --- a/lib/oembedhelper.php +++ b/lib/oembedhelper.php @@ -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/lib/right.php b/lib/right.php index 5bf9c41161..d144b21ae9 100644 --- a/lib/right.php +++ b/lib/right.php @@ -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 index 0000000000..61fe3c5b97 --- /dev/null +++ b/plugins/Bookmark/Bookmark.php @@ -0,0 +1,351 @@ + + * @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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * For storing the fact that a notice is a bookmark + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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(_(''. + '%s '. + '%s '. + '%s'. + ''), + 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 index 0000000000..4f31349801 --- /dev/null +++ b/plugins/Bookmark/BookmarkPlugin.php @@ -0,0 +1,772 @@ +. + * + * @category SocialBookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @author Evan Prodromou + * @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(' '); + $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 index 0000000000..b86e749fd9 --- /dev/null +++ b/plugins/Bookmark/bookmark.css @@ -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 index 0000000000..b99568e154 --- /dev/null +++ b/plugins/Bookmark/bookmarkform.php @@ -0,0 +1,164 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @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 index 0000000000..fc1f8b9d05 --- /dev/null +++ b/plugins/Bookmark/bookmarklet @@ -0,0 +1,9 @@ + + + + +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. + +Bookmark on %%site.name%% diff --git a/plugins/Bookmark/bookmarkpopup.js b/plugins/Bookmark/bookmarkpopup.js new file mode 100644 index 0000000000..29f314ed06 --- /dev/null +++ b/plugins/Bookmark/bookmarkpopup.js @@ -0,0 +1,23 @@ +$(document).ready( + function() { + var form = $('#form_new_bookmark'); + form.append(''); + 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 index 0000000000..24ed79612b --- /dev/null +++ b/plugins/Bookmark/bookmarkpopup.php @@ -0,0 +1,112 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Sarven Capadisli + * @author Evan Prodromou + * @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 + * @author Evan Prodromou + * @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 index 0000000000..1b55115d6d --- /dev/null +++ b/plugins/Bookmark/deliciousbackupimporter.php @@ -0,0 +1,196 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @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
with + * a bunch of
's, occasionally with
's. + * There are sometimes

'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

in the

.'); + 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
/
pair. The
has a single + * in it with some non-standard attributes. + * + * A
sequence will appear as a
with + * anothe
as a child. We handle this case recursively. + * + * @param User $user User to import data as + * @param DOMElement $dt
element + * @param DOMElement $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
+ // 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 index 0000000000..297ef81246 --- /dev/null +++ b/plugins/Bookmark/deliciousbookmarkimporter.php @@ -0,0 +1,109 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @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 tag in a
.")); + } + + $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 index 0000000000..5518b00e97 --- /dev/null +++ b/plugins/Bookmark/importbookmarks.php @@ -0,0 +1,96 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 = <<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 index 0000000000..f8529cc914 --- /dev/null +++ b/plugins/Bookmark/importdelicious.php @@ -0,0 +1,336 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @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 + * @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 index 0000000000..a0cf3fffb2 --- /dev/null +++ b/plugins/Bookmark/newbookmark.php @@ -0,0 +1,196 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @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 index 0000000000..226c7a32bf --- /dev/null +++ b/plugins/Bookmark/noticebyurl.php @@ -0,0 +1,177 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @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 index 0000000000..e9e656f84c --- /dev/null +++ b/plugins/Bookmark/showbookmark.php @@ -0,0 +1,145 @@ +. + * + * @category Bookmark + * @package StatusNet + * @author Evan Prodromou + * @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 + * @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'); + } +} diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php index 3a3d63fe20..024f0cc217 100644 --- a/plugins/OStatus/actions/groupsalmon.php +++ b/plugins/OStatus/actions/groupsalmon.php @@ -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. diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index e78c653300..5355aeba03 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -43,6 +43,8 @@ class UsersalmonAction extends SalmonAction $this->clientError(_m('No such user.')); } + $this->target = $this->user; + return true; } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 77cf57a670..9c0f014fc6 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -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 info. - common_log(LOG_WARNING, "Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}"); - } else { - // Plain 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 info. + common_log(LOG_WARNING, "Got an actor '{$actor->title}' ({$actor->id}) on single-user feed for {$this->uri}"); + } else { + // Plain 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; + } } /** diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 41bdb48928..8bfd7c8261 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -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)); } }