Merge remote branch 'gitorious/0.9.x' into 1.0.x

Conflicts:
	lib/common.php
This commit is contained in:
Evan Prodromou 2010-12-30 15:52:08 -08:00
commit 32eb4c5e2d
37 changed files with 3034 additions and 107 deletions

View File

@ -1058,3 +1058,8 @@ EndImportActivity: when we finish importing an activity
- $activity: The current activity - $activity: The current activity
- $trusted: How "trusted" the process is - $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)

View File

@ -125,10 +125,6 @@ class ApiTimelineGroupAction extends ApiPrivateAuthAction
header('Content-Type: application/atom+xml; charset=utf-8'); header('Content-Type: application/atom+xml; charset=utf-8');
try { try {
$atom->addAuthorRaw($this->group->asAtomAuthor());
$atom->setActivitySubject($this->group->asActivitySubject());
$atom->setId($self);
$atom->setSelfLink($self);
$atom->addEntryFromNotices($this->notices); $atom->addEntryFromNotices($this->notices);
$this->raw($atom->getString()); $this->raw($atom->getString());
} catch (Atom10FeedException $e) { } catch (Atom10FeedException $e) {

View File

@ -458,27 +458,32 @@ class ProfilesettingsAction extends AccountSettingsAction
$this->elementStart('div', array('id' => 'aside_primary', $this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside')); 'class' => 'aside'));
if ($user->hasRight(Right::BACKUPACCOUNT)) { $this->elementStart('ul');
$this->elementStart('li'); if (Event::handle('StartProfileSettingsActions', array($this))) {
$this->element('a', if ($user->hasRight(Right::BACKUPACCOUNT)) {
array('href' => common_local_url('backupaccount')), $this->elementStart('li');
_('Backup account')); $this->element('a',
$this->elementEnd('li'); array('href' => common_local_url('backupaccount')),
} _('Backup account'));
if ($user->hasRight(Right::DELETEACCOUNT)) { $this->elementEnd('li');
$this->elementStart('li'); }
$this->element('a', if ($user->hasRight(Right::DELETEACCOUNT)) {
array('href' => common_local_url('deleteaccount')), $this->elementStart('li');
_('Delete account')); $this->element('a',
$this->elementEnd('li'); array('href' => common_local_url('deleteaccount')),
} _('Delete account'));
if ($user->hasRight(Right::RESTOREACCOUNT)) { $this->elementEnd('li');
$this->elementStart('li'); }
$this->element('a', if ($user->hasRight(Right::RESTOREACCOUNT)) {
array('href' => common_local_url('restoreaccount')), $this->elementStart('li');
_('Restore account')); $this->element('a',
$this->elementEnd('li'); array('href' => common_local_url('restoreaccount')),
_('Restore account'));
$this->elementEnd('li');
}
Event::handle('EndProfileSettingsActions', array($this));
} }
$this->elementEnd('ul');
$this->elementEnd('div'); $this->elementEnd('div');
} }
} }

View File

@ -412,4 +412,102 @@ class File extends Memcached_DataObject
{ {
return File_thumbnail::staticGet('file_id', $this->id); 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;
}
} }

View File

@ -52,6 +52,12 @@ class File_to_post extends Memcached_DataObject
$f2p->file_id = $file_id; $f2p->file_id = $file_id;
$f2p->post_id = $notice_id; $f2p->post_id = $notice_id;
$f2p->insert(); $f2p->insert();
$f = File::staticGet($file_id);
if (!empty($f)) {
$f->blowCache();
}
} }
if (empty($seen[$notice_id])) { if (empty($seen[$notice_id])) {
@ -66,4 +72,13 @@ class File_to_post extends Memcached_DataObject
{ {
return Memcached_DataObject::pkeyGet('File_to_post', $kv); 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();
}
} }

View File

@ -74,7 +74,7 @@ class Memcached_DataObject extends Safe_DataObject
return $i; return $i;
} else { } else {
$i = DB_DataObject::factory($cls); $i = DB_DataObject::factory($cls);
if (empty($i)) { if (empty($i) || PEAR::isError($i)) {
return false; return false;
} }
foreach ($kv as $k => $v) { foreach ($kv as $k => $v) {

View File

@ -135,6 +135,7 @@ class Notice extends Memcached_DataObject
$this->clearFaves(); $this->clearFaves();
$this->clearTags(); $this->clearTags();
$this->clearGroupInboxes(); $this->clearGroupInboxes();
$this->clearFiles();
// NOTE: we don't clear inboxes // NOTE: we don't clear inboxes
// NOTE: we don't clear queue items // NOTE: we don't clear queue items
@ -1785,6 +1786,21 @@ class Notice extends Memcached_DataObject
$reply->free(); $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() function clearRepeats()
{ {
$repeatNotice = new Notice(); $repeatNotice = new Notice();

View File

@ -87,4 +87,19 @@ class Notice_tag extends Memcached_DataObject
{ {
return Memcached_DataObject::pkeyGet('Notice_tag', $kv); 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;
}
} }

View File

@ -377,13 +377,7 @@ class Activity
$xs->element('updated', null, $published); $xs->element('updated', null, $published);
if ($author) { if ($author) {
$xs->elementStart('author'); $this->actor->outputTo($xs, 'author');
$xs->element('uri', array(), $this->actor->id);
if ($this->actor->title) {
$xs->element('name', array(), $this->actor->title);
}
$xs->elementEnd('author');
$this->actor->outputTo($xs, 'activity:actor');
} }
if ($this->verb != ActivityVerb::POST || count($this->objects) != 1) { if ($this->verb != ActivityVerb::POST || count($this->objects) != 1) {
@ -446,7 +440,7 @@ class Activity
} }
foreach ($this->categories as $cat) { foreach ($this->categories as $cat) {
$xs->raw($cat->asString()); $cat->outputTo($xs);
} }
// can be either URLs or enclosure objects // can be either URLs or enclosure objects

View File

@ -172,10 +172,39 @@ class ActivityObject
private function _fromAuthor($element) private function _fromAuthor($element)
{ {
$this->type = self::PERSON; // XXX: is this fair? $this->type = $this->_childContent($element,
$this->title = $this->_childContent($element, self::NAME); Activity::OBJECTTYPE,
Activity::SPEC);
$this->id = $this->_childContent($element, self::URI); if (empty($this->type)) {
$this->type = self::PERSON; // XXX: is this fair?
}
// start with <atom:title>
$title = ActivityUtils::childHtmlContent($element, self::TITLE);
if (!empty($title)) {
$this->title = html_entity_decode(strip_tags($title), ENT_QUOTES, 'UTF-8');
}
// fall back to <atom:name>
if (empty($this->title)) {
$this->title = $this->_childContent($element, self::NAME);
}
// start with <atom:id>
$this->id = $this->_childContent($element, self::ID);
// fall back to <atom:uri>
if (empty($this->id)) {
$this->id = $this->_childContent($element, self::URI);
}
// fall further back to <atom:email>
if (empty($this->id)) { if (empty($this->id)) {
$email = $this->_childContent($element, self::EMAIL); $email = $this->_childContent($element, self::EMAIL);
@ -184,6 +213,14 @@ class ActivityObject
$this->id = 'mailto:'.$email; $this->id = 'mailto:'.$email;
} }
} }
$this->link = ActivityUtils::getPermalink($element);
// fall finally back to <link rel=alternate>
if (empty($this->id) && !empty($this->link)) { // fallback if there's no ID
$this->id = $this->link;
}
} }
private function _fromAtomEntry($element) private function _fromAtomEntry($element)
@ -498,14 +535,21 @@ class ActivityObject
$xo->element('activity:object-type', null, $this->type); $xo->element('activity:object-type', null, $this->type);
$xo->element(self::ID, null, $this->id); // <author> uses URI
if ($tag == 'author') {
$xo->element(self::URI, null, $this->id);
} else {
$xo->element(self::ID, null, $this->id);
}
if (!empty($this->title)) { if (!empty($this->title)) {
$xo->element( $name = common_xml_safe_str($this->title);
self::TITLE, if ($tag == 'author') {
null, $xo->element(self::NAME, null, $name);
common_xml_safe_str($this->title) } else {
); $xo->element(self::TITLE, null, $name);
}
} }
if (!empty($this->summary)) { if (!empty($this->summary)) {
@ -563,7 +607,7 @@ class ActivityObject
} }
if (!empty($this->poco)) { if (!empty($this->poco)) {
$xo->raw($this->poco->asString()); $this->poco->outputTo($xo);
} }
foreach ($this->extra as $el) { foreach ($this->extra as $el) {

View File

@ -146,15 +146,16 @@ class Atom10Feed extends XMLStringer
} }
/** /**
* Add a activity feed subject via raw XML string * Deprecated <activity:subject>; ignored
* *
* @param string $xmlSubject An XML string representation of the subject * @param string $xmlSubject An XML string representation of the subject
* *
* @return void * @return void
*/ */
function setActivitySubject($xmlSubject) function setActivitySubject($xmlSubject)
{ {
$this->subject = $xmlSubject; throw new ServerException(_('Don\'t use this method!'));
} }
function getNamespaces() function getNamespaces()

View File

@ -59,6 +59,13 @@ class AtomCategory
} }
function asString() function asString()
{
$xs = new XMLStringer();
$this->outputTo($xs);
return $xs->getString();
}
function outputTo($xo)
{ {
$attribs = array(); $attribs = array();
if ($this->term !== null) { if ($this->term !== null) {
@ -70,8 +77,6 @@ class AtomCategory
if ($this->label !== null) { if ($this->label !== null) {
$attribs['label'] = $this->label; $attribs['label'] = $this->label;
} }
$xs = new XMLStringer(); $xo->element('category', $attribs);
$xs->element('category', $attribs);
return $xs->getString();
} }
} }

View File

@ -85,8 +85,14 @@ class AtomGroupNoticeFeed extends AtomNoticeFeed
$this->setId($self); $this->setId($self);
$this->setSelfLink($self); $this->setSelfLink($self);
$this->addAuthorRaw($group->asAtomAuthor()); // For groups, we generate an author _AND_ an <activity:subject>
$this->setActivitySubject($group->asActivitySubject()); // Versions of StatusNet under 0.9.7 treat <author> as a person
// XXX: remove this workaround in future versions
$ao = ActivityObject::fromGroup($group);
$this->addAuthorRaw($ao->asString('author').
$ao->asString('activity:subject'));
$this->addLink($group->homeUrl()); $this->addLink($group->homeUrl());
} }

View File

@ -60,8 +60,8 @@ class AtomUserNoticeFeed extends AtomNoticeFeed
$this->user = $user; $this->user = $user;
if (!empty($user)) { if (!empty($user)) {
$profile = $user->getProfile(); $profile = $user->getProfile();
$this->addAuthor($profile->nickname, $user->uri); $ao = ActivityObject::fromProfile($profile);
$this->setActivitySubject($profile->asActivityNoun('subject')); $this->addAuthorRaw($ao->asString('author'));
} }
// TRANS: Title in atom user notice feed. %s is a user name. // TRANS: Title in atom user notice feed. %s is a user name.

View File

@ -299,6 +299,10 @@ class oEmbedHelper
class oEmbedHelper_Exception extends Exception class oEmbedHelper_Exception extends Exception
{ {
public function __construct($message = "", $code = 0, $previous = null)
{
parent::__construct($message, $code);
}
} }
class oEmbedHelper_BadHtmlException extends oEmbedHelper_Exception class oEmbedHelper_BadHtmlException extends oEmbedHelper_Exception

View File

@ -211,30 +211,34 @@ class PoCo
function asString() function asString()
{ {
$xs = new XMLStringer(true); $xs = new XMLStringer(true);
$xs->element( $this->outputTo($xs);
return $xs->getString();
}
function outputTo($xo)
{
$xo->element(
'poco:preferredUsername', 'poco:preferredUsername',
null, null,
$this->preferredUsername $this->preferredUsername
); );
$xs->element( $xo->element(
'poco:displayName', 'poco:displayName',
null, null,
$this->displayName $this->displayName
); );
if (!empty($this->note)) { if (!empty($this->note)) {
$xs->element('poco:note', null, common_xml_safe_str($this->note)); $xo->element('poco:note', null, common_xml_safe_str($this->note));
} }
if (!empty($this->address)) { if (!empty($this->address)) {
$xs->raw($this->address->asString()); $this->address->outputTo($xo);
} }
foreach ($this->urls as $url) { foreach ($this->urls as $url) {
$xs->raw($url->asString()); $url->outputTo($xo);
} }
return $xs->getString();
} }
} }

View File

@ -43,14 +43,17 @@ class PoCoAddress
function asString() function asString()
{ {
if (!empty($this->formatted)) { $xs = new XMLStringer(true);
$xs = new XMLStringer(true); $this->outputTo($xs);
$xs->elementStart('poco:address'); return $xs->getString();
$xs->element('poco:formatted', null, common_xml_safe_str($this->formatted)); }
$xs->elementEnd('poco:address');
return $xs->getString();
}
return null; function outputTo($xo)
{
if (!empty($this->formatted)) {
$xo->elementStart('poco:address');
$xo->element('poco:formatted', null, common_xml_safe_str($this->formatted));
$xo->elementEnd('poco:address');
}
} }
} }

View File

@ -53,13 +53,18 @@ class PoCoURL
function asString() function asString()
{ {
$xs = new XMLStringer(true); $xs = new XMLStringer(true);
$xs->elementStart('poco:urls'); $this->outputTo($xs);
$xs->element('poco:type', null, $this->type);
$xs->element('poco:value', null, $this->value);
if (!empty($this->primary)) {
$xs->element('poco:primary', null, 'true');
}
$xs->elementEnd('poco:urls');
return $xs->getString(); return $xs->getString();
} }
function outputTo($xo)
{
$xo->elementStart('poco:urls');
$xo->element('poco:type', null, $this->type);
$xo->element('poco:value', null, $this->value);
if (!empty($this->primary)) {
$xo->element('poco:primary', null, 'true');
}
$xo->elementEnd('poco:urls');
}
} }

View File

@ -0,0 +1,351 @@
<?php
/**
* Data class to mark notices as bookmarks
*
* PHP version 5
*
* @category Data
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2009, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* For storing the fact that a notice is a bookmark
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*
* @see DB_DataObject
*/
class Bookmark extends Memcached_DataObject
{
public $__table = 'bookmark'; // table name
public $profile_id; // int(4) primary_key not_null
public $url; // varchar(255) primary_key not_null
public $title; // varchar(255)
public $description; // text
public $uri; // varchar(255)
public $url_crc32; // int(4) not_null
public $created; // datetime
/**
* Get an instance by key
*
* This is a utility method to get a single instance with a given key value.
*
* @param string $k Key to use to lookup (usually 'user_id' for this class)
* @param mixed $v Value to lookup
*
* @return User_greeting_count object found, or null for no hits
*
*/
function staticGet($k, $v=null)
{
return Memcached_DataObject::staticGet('Bookmark', $k, $v);
}
/**
* Get an instance by compound key
*
* This is a utility method to get a single instance with a given set of
* key-value pairs. Usually used for the primary key for a compound key; thus
* the name.
*
* @param array $kv array of key-value mappings
*
* @return Bookmark object found, or null for no hits
*
*/
function pkeyGet($kv)
{
return Memcached_DataObject::pkeyGet('Bookmark', $kv);
}
/**
* return table definition for DB_DataObject
*
* DB_DataObject needs to know something about the table to manipulate
* instances. This method provides all the DB_DataObject needs to know.
*
* @return array array of column definitions
*/
function table()
{
return array('profile_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'url' => DB_DATAOBJECT_STR,
'title' => DB_DATAOBJECT_STR,
'description' => DB_DATAOBJECT_STR,
'uri' => DB_DATAOBJECT_STR,
'url_crc32' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE +
DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
}
/**
* return key definitions for DB_DataObject
*
* @return array list of key field names
*/
function keys()
{
return array_keys($this->keyTypes());
}
/**
* return key definitions for Memcached_DataObject
*
* @return array associative array of key definitions
*/
function keyTypes()
{
return array('profile_id' => 'K',
'url' => 'K',
'uri' => 'U');
}
/**
* Magic formula for non-autoincrementing integer primary keys
*
* @return array magic three-false array that stops auto-incrementing.
*/
function sequenceKey()
{
return array(false, false, false);
}
/**
* Get a bookmark based on a notice
*
* @param Notice $notice Notice to check for
*
* @return Bookmark found bookmark or null
*/
function getByNotice($notice)
{
return self::staticGet('uri', $notice->uri);
}
/**
* Get the bookmark that a user made for an URL
*
* @param Profile $profile Profile to check for
* @param string $url URL to check for
*
* @return Bookmark bookmark found or null
*/
static function getByURL($profile, $url)
{
return self::pkeyGet(array('profile_id' => $profile->id,
'url' => $url));
return null;
}
/**
* Get the bookmark that a user made for an URL
*
* @param Profile $profile Profile to check for
* @param integer $crc32 CRC-32 of URL to check for
*
* @return array Bookmark objects found (usually 1 or 0)
*/
static function getByCRC32($profile, $crc32)
{
$bookmarks = array();
$nb = new Bookmark();
$nb->profile_id = $profile->id;
$nb->url_crc32 = $crc32;
if ($nb->find()) {
while ($nb->fetch()) {
$bookmarks[] = clone($nb);
}
}
return $bookmarks;
}
/**
* Save a new notice bookmark
*
* @param Profile $profile To save the bookmark for
* @param string $title Title of the bookmark
* @param string $url URL of the bookmark
* @param mixed $rawtags array of tags or string
* @param string $description Description of the bookmark
* @param array $options Options for the Notice::saveNew()
*
* @return Notice saved notice
*/
static function saveNew($profile, $title, $url, $rawtags, $description,
$options=null)
{
$nb = self::getByURL($profile, $url);
if (!empty($nb)) {
throw new ClientException(_('Bookmark already exists.'));
}
if (empty($options)) {
$options = array();
}
if (array_key_exists('uri', $options)) {
$other = Bookmark::staticGet('uri', $options['uri']);
if (!empty($other)) {
throw new ClientException(_('Bookmark already exists.'));
}
}
if (is_string($rawtags)) {
$rawtags = preg_split('/[\s,]+/', $rawtags);
}
$nb = new Bookmark();
$nb->profile_id = $profile->id;
$nb->url = $url;
$nb->title = $title;
$nb->description = $description;
$nb->url_crc32 = crc32($nb->url);
if (array_key_exists('created', $options)) {
$nb->created = $options['created'];
} else {
$nb->created = common_sql_now();
}
if (array_key_exists('uri', $options)) {
$nb->uri = $options['uri'];
} else {
$dt = new DateTime($nb->created, new DateTimeZone('UTC'));
// I posit that it's sufficiently impossible
// for the same user to generate two CRC-32-clashing
// URLs in the same second that this is a safe unique identifier.
// If you find a real counterexample, contact me at acct:evan@status.net
// and I will publicly apologize for my hubris.
$created = $dt->format('YmdHis');
$crc32 = sprintf('%08x', $nb->url_crc32);
$nb->uri = common_local_url('showbookmark',
array('user' => $profile->id,
'created' => $created,
'crc32' => $crc32));
}
$nb->insert();
$tags = array();
$replies = array();
// filter "for:nickname" tags
foreach ($rawtags as $tag) {
if (strtolower(mb_substr($tag, 0, 4)) == 'for:') {
// skip if done by caller
if (!array_key_exists('replies', $options)) {
$nickname = mb_substr($tag, 4);
$other = common_relative_profile($profile,
$nickname);
if (!empty($other)) {
$replies[] = $other->getUri();
}
}
} else {
$tags[] = common_canonical_tag($tag);
}
}
$hashtags = array();
$taglinks = array();
foreach ($tags as $tag) {
$hashtags[] = '#'.$tag;
$attrs = array('href' => Notice_tag::url($tag),
'rel' => $tag,
'class' => 'tag');
$taglinks[] = XMLStringer::estring('a', $attrs, $tag);
}
// Use user's preferences for short URLs, if possible
$user = User::staticGet('id', $profile->id);
$shortUrl = File_redirection::makeShort($url,
empty($user) ? null : $user);
$content = sprintf(_('"%s" %s %s %s'),
$title,
$shortUrl,
$description,
implode(' ', $hashtags));
$rendered = sprintf(_('<span class="xfolkentry">'.
'<a class="taggedlink" href="%s">%s</a> '.
'<span class="description">%s</span> '.
'<span class="meta">%s</span>'.
'</span>'),
htmlspecialchars($url),
htmlspecialchars($title),
htmlspecialchars($description),
implode(' ', $taglinks));
$options = array_merge(array('urls' => array($url),
'rendered' => $rendered,
'tags' => $tags,
'replies' => $replies),
$options);
if (!array_key_exists('uri', $options)) {
$options['uri'] = $nb->uri;
}
$saved = Notice::saveNew($profile->id,
$content,
array_key_exists('source', $options) ?
$options['source'] : 'web',
$options);
return $saved;
}
}

View File

@ -0,0 +1,772 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* A plugin to enable social-bookmarking functionality
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category SocialBookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Bookmark plugin main class
*
* @category Bookmark
* @package StatusNet
* @author Brion Vibber <brionv@status.net>
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class BookmarkPlugin extends Plugin
{
const VERSION = '0.1';
const IMPORTDELICIOUS = 'BookmarkPlugin:IMPORTDELICIOUS';
/**
* Authorization for importing delicious bookmarks
*
* By default, everyone can import bookmarks except silenced people.
*
* @param Profile $profile Person whose rights to check
* @param string $right Right to check; const value
* @param boolean &$result Result of the check, writeable
*
* @return boolean hook value
*/
function onUserRightsCheck($profile, $right, &$result)
{
if ($right == self::IMPORTDELICIOUS) {
$result = !$profile->isSilenced();
return false;
}
return true;
}
/**
* Database schema setup
*
* @see Schema
* @see ColumnDef
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onCheckSchema()
{
$schema = Schema::get();
// For storing user-submitted flags on profiles
$schema->ensureTable('bookmark',
array(new ColumnDef('profile_id',
'integer',
null,
false,
'PRI'),
new ColumnDef('url',
'varchar',
255,
false,
'PRI'),
new ColumnDef('title',
'varchar',
255),
new ColumnDef('description',
'text'),
new ColumnDef('uri',
'varchar',
255,
false,
'UNI'),
new ColumnDef('url_crc32',
'integer unsigned',
null,
false,
'MUL'),
new ColumnDef('created',
'datetime',
null,
false,
'MUL')));
try {
$schema->createIndex('bookmark',
array('profile_id',
'url_crc32'),
'bookmark_profile_url_idx');
} catch (Exception $e) {
common_log(LOG_ERR, $e->getMessage());
}
return true;
}
/**
* When a notice is deleted, delete the related Bookmark
*
* @param Notice $notice Notice being deleted
*
* @return boolean hook value
*/
function onNoticeDeleteRelated($notice)
{
$nb = Bookmark::getByNotice($notice);
if (!empty($nb)) {
$nb->delete();
}
return true;
}
/**
* Show the CSS necessary for this plugin
*
* @param Action $action the action being run
*
* @return boolean hook value
*/
function onEndShowStyles($action)
{
$action->cssLink('plugins/Bookmark/bookmark.css');
return true;
}
/**
* Load related modules when needed
*
* @param string $cls Name of the class to be loaded
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onAutoload($cls)
{
$dir = dirname(__FILE__);
switch ($cls)
{
case 'ShowbookmarkAction':
case 'NewbookmarkAction':
case 'BookmarkpopupAction':
case 'NoticebyurlAction':
case 'ImportdeliciousAction':
include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
return false;
case 'Bookmark':
include_once $dir.'/'.$cls.'.php';
return false;
case 'BookmarkForm':
case 'DeliciousBackupImporter':
case 'DeliciousBookmarkImporter':
include_once $dir.'/'.strtolower($cls).'.php';
return false;
default:
return true;
}
}
/**
* Map URLs to actions
*
* @param Net_URL_Mapper $m path-to-action mapper
*
* @return boolean hook value; true means continue processing, false means stop.
*/
function onRouterInitialized($m)
{
$m->connect('main/bookmark/new',
array('action' => 'newbookmark'),
array('id' => '[0-9]+'));
$m->connect('main/bookmark/popup',
array('action' => 'bookmarkpopup'));
$m->connect('main/bookmark/import',
array('action' => 'importdelicious'));
$m->connect('bookmark/:user/:created/:crc32',
array('action' => 'showbookmark'),
array('user' => '[0-9]+',
'created' => '[0-9]{14}',
'crc32' => '[0-9a-f]{8}'));
$m->connect('notice/by-url/:id',
array('action' => 'noticebyurl'),
array('id' => '[0-9]+'));
return true;
}
/**
* Output the HTML for a bookmark in a list
*
* @param NoticeListItem $nli The list item being shown.
*
* @return boolean hook value
*/
function onStartShowNoticeItem($nli)
{
$nb = Bookmark::getByNotice($nli->notice);
if (!empty($nb)) {
$out = $nli->out;
$notice = $nli->notice;
$profile = $nli->profile;
$atts = $notice->attachments();
if (count($atts) < 1) {
// Something wrong; let default code deal with it.
return true;
}
$att = $atts[0];
// XXX: only show the bookmark URL for non-single-page stuff
if ($out instanceof ShowbookmarkAction) {
} else {
$out->elementStart('h3');
$out->element('a',
array('href' => $att->url),
$nb->title);
$out->elementEnd('h3');
$countUrl = common_local_url('noticebyurl',
array('id' => $att->id));
$out->element('a', array('class' => 'bookmark_notice_count',
'href' => $countUrl),
$att->noticeCount());
}
$out->elementStart('ul', array('class' => 'bookmark_tags'));
// Replies look like "for:" tags
$replies = $nli->notice->getReplies();
if (!empty($replies)) {
foreach ($replies as $reply) {
$other = Profile::staticGet('id', $reply);
$out->elementStart('li');
$out->element('a', array('rel' => 'tag',
'href' => $other->profileurl,
'title' => $other->getBestName()),
sprintf('for:%s', $other->nickname));
$out->elementEnd('li');
$out->text(' ');
}
}
$tags = $nli->notice->getTags();
foreach ($tags as $tag) {
$out->elementStart('li');
$out->element('a',
array('rel' => 'tag',
'href' => Notice_tag::url($tag)),
$tag);
$out->elementEnd('li');
$out->text(' ');
}
$out->elementEnd('ul');
$out->element('p',
array('class' => 'bookmark_description'),
$nb->description);
if (common_config('attachments', 'show_thumbs')) {
$al = new InlineAttachmentList($notice, $out);
$al->show();
}
$out->elementStart('p', array('style' => 'float: left'));
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
$out->element('img', array('src' => ($avatar) ?
$avatar->displayUrl() :
Avatar::defaultImage(AVATAR_MINI_SIZE),
'class' => 'avatar photo bookmark_avatar',
'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE,
'alt' => $profile->getBestName()));
$out->raw('&nbsp;');
$out->element('a', array('href' => $profile->profileurl,
'title' => $profile->getBestName()),
$profile->nickname);
$nli->showNoticeLink();
$nli->showNoticeSource();
$nli->showNoticeLocation();
$nli->showContext();
$nli->showRepeat();
$out->elementEnd('p');
$nli->showNoticeOptions();
return false;
}
return true;
}
/**
* Render a notice as a Bookmark object
*
* @param Notice $notice Notice to render
* @param ActivityObject &$object Empty object to fill
*
* @return boolean hook value
*/
function onStartActivityObjectFromNotice($notice, &$object)
{
common_log(LOG_INFO,
"Checking {$notice->uri} to see if it's a bookmark.");
$nb = Bookmark::getByNotice($notice);
if (!empty($nb)) {
common_log(LOG_INFO,
"Formatting notice {$notice->uri} as a bookmark.");
$object->id = $notice->uri;
$object->type = ActivityObject::BOOKMARK;
$object->title = $nb->title;
$object->summary = $nb->description;
$object->link = $notice->bestUrl();
// Attributes of the URL
$attachments = $notice->attachments();
if (count($attachments) != 1) {
throw new ServerException(_('Bookmark notice with the '.
'wrong number of attachments.'));
}
$target = $attachments[0];
$attrs = array('rel' => 'related',
'href' => $target->url);
if (!empty($target->title)) {
$attrs['title'] = $target->title;
}
$object->extra[] = array('link', $attrs, null);
// Attributes of the thumbnail, if any
$thumbnail = $target->getThumbnail();
if (!empty($thumbnail)) {
$tattrs = array('rel' => 'preview',
'href' => $thumbnail->url);
if (!empty($thumbnail->width)) {
$tattrs['media:width'] = $thumbnail->width;
}
if (!empty($thumbnail->height)) {
$tattrs['media:height'] = $thumbnail->height;
}
$object->extra[] = array('link', $attrs, null);
}
return false;
}
return true;
}
/**
* Add our two queue handlers to the queue manager
*
* @param QueueManager $qm current queue manager
*
* @return boolean hook value
*/
function onEndInitializeQueueManager($qm)
{
$qm->connect('dlcsback', 'DeliciousBackupImporter');
$qm->connect('dlcsbkmk', 'DeliciousBookmarkImporter');
return true;
}
/**
* Plugin version data
*
* @param array &$versions array of version data
*
* @return value
*/
function onPluginVersion(&$versions)
{
$versions[] = array('name' => 'Sample',
'version' => self::VERSION,
'author' => 'Evan Prodromou',
'homepage' => 'http://status.net/wiki/Plugin:Bookmark',
'rawdescription' =>
_m('Simple extension for supporting bookmarks.'));
return true;
}
/**
* Load our document if requested
*
* @param string &$title Title to fetch
* @param string &$output HTML to output
*
* @return boolean hook value
*/
function onStartLoadDoc(&$title, &$output)
{
if ($title == 'bookmarklet') {
$filename = INSTALLDIR.'/plugins/Bookmark/bookmarklet';
$c = file_get_contents($filename);
$output = common_markup_to_html($c);
return false; // success!
}
return true;
}
/**
* Handle a posted bookmark from PuSH
*
* @param Activity $activity activity to handle
* @param Ostatus_profile $oprofile Profile for the feed
*
* @return boolean hook value
*/
function onStartHandleFeedEntryWithProfile($activity, $oprofile)
{
common_log(LOG_INFO, "BookmarkPlugin called for new feed entry.");
if (self::_isPostBookmark($activity)) {
common_log(LOG_INFO,
"Importing activity {$activity->id} as a bookmark.");
$author = $oprofile->checkAuthorship($activity);
if (empty($author)) {
throw new ClientException(_('Can\'t get author for activity.'));
}
self::_postRemoteBookmark($author,
$activity);
return false;
}
return true;
}
/**
* Handle a posted bookmark from Salmon
*
* @param Activity $activity activity to handle
* @param mixed $target user or group targeted
*
* @return boolean hook value
*/
function onStartHandleSalmonTarget($activity, $target)
{
if (self::_isPostBookmark($activity)) {
$this->log(LOG_INFO, "Checking {$activity->id} as a valid Salmon slap.");
if ($target instanceof User_group) {
$uri = $target->getUri();
if (!in_array($uri, $activity->context->attention)) {
throw new ClientException(_("Bookmark not posted ".
"to this group."));
}
} else if ($target instanceof User) {
$uri = $target->uri;
$original = null;
if (!empty($activity->context->replyToID)) {
$original = Notice::staticGet('uri',
$activity->context->replyToID);
}
if (!in_array($uri, $activity->context->attention) &&
(empty($original) ||
$original->profile_id != $target->id)) {
throw new ClientException(_("Bookmark not posted ".
"to this user."));
}
} else {
throw new ServerException(_("Don't know how to handle ".
"this kind of target."));
}
$author = Ostatus_profile::ensureActivityObjectProfile($activity->actor);
self::_postRemoteBookmark($author,
$activity);
return false;
}
return true;
}
/**
* Handle bookmark posted via AtomPub
*
* @param Activity &$activity Activity that was posted
* @param User $user User that posted it
* @param Notice &$notice Resulting notice
*
* @return boolean hook value
*/
function onStartAtomPubNewActivity(&$activity, $user, &$notice)
{
if (self::_isPostBookmark($activity)) {
$options = array('source' => 'atompub');
$notice = self::_postBookmark($user->getProfile(),
$activity,
$options);
return false;
}
return true;
}
/**
* Handle bookmark imported from a backup file
*
* @param User $user User to import for
* @param ActivityObject $author Original author per import file
* @param Activity $activity Activity to import
* @param boolean $trusted Is this a trusted user?
* @param boolean &$done Is this done (success or unrecoverable error)
*
* @return boolean hook value
*/
function onStartImportActivity($user, $author, $activity, $trusted, &$done)
{
if (self::_isPostBookmark($activity)) {
$bookmark = $activity->objects[0];
$this->log(LOG_INFO,
'Importing Bookmark ' . $bookmark->id .
' for user ' . $user->nickname);
$options = array('uri' => $bookmark->id,
'url' => $bookmark->link,
'source' => 'restore');
$saved = self::_postBookmark($user->getProfile(), $activity, $options);
if (!empty($saved)) {
$done = true;
}
return false;
}
return true;
}
/**
* Show a link to our delicious import page on profile settings form
*
* @param Action $action Profile settings action being shown
*
* @return boolean hook value
*/
function onEndProfileSettingsActions($action)
{
$user = common_current_user();
if (!empty($user) && $user->hasRight(self::IMPORTDELICIOUS)) {
$action->elementStart('li');
$action->element('a',
array('href' => common_local_url('importdelicious')),
_('Import del.icio.us bookmarks'));
$action->elementEnd('li');
}
return true;
}
/**
* Save a remote bookmark (from Salmon or PuSH)
*
* @param Ostatus_profile $author Author of the bookmark
* @param Activity $activity Activity to save
*
* @return Notice resulting notice.
*/
static private function _postRemoteBookmark(Ostatus_profile $author,
Activity $activity)
{
$bookmark = $activity->objects[0];
$options = array('uri' => $bookmark->id,
'url' => $bookmark->link,
'is_local' => Notice::REMOTE_OMB,
'source' => 'ostatus');
return self::_postBookmark($author->localProfile(), $activity, $options);
}
/**
* Save a bookmark from an activity
*
* @param Profile $profile Profile to use as author
* @param Activity $activity Activity to save
* @param array $options Options to pass to bookmark-saving code
*
* @return Notice resulting notice
*/
static private function _postBookmark(Profile $profile,
Activity $activity,
$options=array())
{
$bookmark = $activity->objects[0];
$relLinkEls = ActivityUtils::getLinks($bookmark->element, 'related');
if (count($relLinkEls) < 1) {
throw new ClientException(_('Expected exactly 1 link '.
'rel=related in a Bookmark.'));
}
if (count($relLinkEls) > 1) {
common_log(LOG_WARNING,
"Got too many link rel=related in a Bookmark.");
}
$linkEl = $relLinkEls[0];
$url = $linkEl->getAttribute('href');
$tags = array();
foreach ($activity->categories as $category) {
$tags[] = common_canonical_tag($category->term);
}
if (!empty($activity->time)) {
$options['created'] = common_sql_date($activity->time);
}
// Fill in location if available
$location = $activity->context->location;
if ($location) {
$options['lat'] = $location->lat;
$options['lon'] = $location->lon;
if ($location->location_id) {
$options['location_ns'] = $location->location_ns;
$options['location_id'] = $location->location_id;
}
}
$replies = $activity->context->attention;
$options['groups'] = array();
$options['replies'] = array();
foreach ($replies as $replyURI) {
$other = Profile::fromURI($replyURI);
if (!empty($other)) {
$options['replies'][] = $replyURI;
} else {
$group = User_group::staticGet('uri', $replyURI);
if (!empty($group)) {
$options['groups'][] = $replyURI;
}
}
}
// Maintain direct reply associations
// @fixme what about conversation ID?
if (!empty($activity->context->replyToID)) {
$orig = Notice::staticGet('uri',
$activity->context->replyToID);
if (!empty($orig)) {
$options['reply_to'] = $orig->id;
}
}
return Bookmark::saveNew($profile,
$bookmark->title,
$url,
$tags,
$bookmark->summary,
$options);
}
/**
* Test if an activity represents posting a bookmark
*
* @param Activity $activity Activity to test
*
* @return true if it's a Post of a Bookmark, else false
*/
static private function _isPostBookmark($activity)
{
return ($activity->verb == ActivityVerb::POST &&
$activity->objects[0]->type == ActivityObject::BOOKMARK);
}
}

View File

@ -0,0 +1,4 @@
.bookmark_tags li { display: inline; }
.bookmark_mentions li { display: inline; }
.bookmark_avatar { float: left }
.bookmark_notice_count { float: right }

View File

@ -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'));
}
}

View File

@ -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>

View File

@ -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();
}});
}
);

View File

@ -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'));
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* Importer class for Delicious.com bookmarks
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/**
* Importer class for Delicious bookmarks
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class DeliciousBookmarkImporter extends QueueHandler
{
/**
* Return the transport for this queue handler
*
* @return string 'dlcsbkmk'
*/
function transport()
{
return 'dlcsbkmk';
}
/**
* Handle the data
*
* @param array $data array of user, dt, dd
*
* @return boolean success value
*/
function handle($data)
{
list($user, $dt, $dd) = $data;
$as = $dt->getElementsByTagName('a');
if ($as->length == 0) {
throw new ClientException(_("No <A> tag in a <DT>."));
}
$a = $as->item(0);
$private = $a->getAttribute('private');
if ($private != 0) {
throw new ClientException(_('Skipping private bookmark.'));
}
if (!empty($dd)) {
$description = $dd->nodeValue;
} else {
$description = null;
}
$title = $a->nodeValue;
$url = $a->getAttribute('href');
$tags = $a->getAttribute('tags');
$addDate = $a->getAttribute('add_date');
$created = common_sql_date(intval($addDate));
$saved = Bookmark::saveNew($user->getProfile(),
$title,
$url,
$tags,
$description,
array('created' => $created,
'distribute' => false));
return true;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,336 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* Import del.icio.us bookmarks backups
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/**
* UI for importing del.icio.us bookmark backups
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class ImportdeliciousAction extends Action
{
protected $success = false;
/**
* Return the title of the page
*
* @return string page title
*/
function title()
{
return _("Import del.icio.us bookmarks");
}
/**
* For initializing members of the class.
*
* @param array $argarray misc. arguments
*
* @return boolean true
*/
function prepare($argarray)
{
parent::prepare($argarray);
$cur = common_current_user();
if (empty($cur)) {
throw new ClientException(_('Only logged-in users can '.
'import del.icio.us backups.'),
403);
}
if (!$cur->hasRight(BookmarkPlugin::IMPORTDELICIOUS)) {
throw new ClientException(_('You may not restore your account.'), 403);
}
return true;
}
/**
* Handler method
*
* @param array $argarray is ignored since it's now passed in in prepare()
*
* @return void
*/
function handle($argarray=null)
{
parent::handle($argarray);
if ($this->isPost()) {
$this->importDelicious();
} else {
$this->showPage();
}
return;
}
/**
* Queue a file for importation
*
* Uses the DeliciousBackupImporter class; may take a long time!
*
* @return void
*/
function importDelicious()
{
$this->checkSessionToken();
if (!isset($_FILES[ImportDeliciousForm::FILEINPUT]['error'])) {
throw new ClientException(_('No uploaded file.'));
}
switch ($_FILES[ImportDeliciousForm::FILEINPUT]['error']) {
case UPLOAD_ERR_OK: // success, jump out
break;
case UPLOAD_ERR_INI_SIZE:
// TRANS: Client exception thrown when an uploaded file is too large.
throw new ClientException(_('The uploaded file exceeds the ' .
'upload_max_filesize directive in php.ini.'));
return;
case UPLOAD_ERR_FORM_SIZE:
throw new ClientException(
// TRANS: Client exception.
_('The uploaded file exceeds the MAX_FILE_SIZE directive' .
' that was specified in the HTML form.'));
return;
case UPLOAD_ERR_PARTIAL:
@unlink($_FILES[ImportDeliciousForm::FILEINPUT]['tmp_name']);
// TRANS: Client exception.
throw new ClientException(_('The uploaded file was only' .
' partially uploaded.'));
return;
case UPLOAD_ERR_NO_FILE:
// No file; probably just a non-AJAX submission.
throw new ClientException(_('No uploaded file.'));
return;
case UPLOAD_ERR_NO_TMP_DIR:
// TRANS: Client exception thrown when a temporary folder is not present
throw new ClientException(_('Missing a temporary folder.'));
return;
case UPLOAD_ERR_CANT_WRITE:
// TRANS: Client exception thrown when writing to disk is not possible
throw new ClientException(_('Failed to write file to disk.'));
return;
case UPLOAD_ERR_EXTENSION:
// TRANS: Client exception thrown when a file upload has been stopped
throw new ClientException(_('File upload stopped by extension.'));
return;
default:
common_log(LOG_ERR, __METHOD__ . ": Unknown upload error " .
$_FILES[ImportDeliciousForm::FILEINPUT]['error']);
// TRANS: Client exception thrown when a file upload operation has failed
throw new ClientException(_('System error uploading file.'));
return;
}
$filename = $_FILES[ImportDeliciousForm::FILEINPUT]['tmp_name'];
try {
if (!file_exists($filename)) {
throw new ServerException("No such file '$filename'.");
}
if (!is_file($filename)) {
throw new ServerException("Not a regular file: '$filename'.");
}
if (!is_readable($filename)) {
throw new ServerException("File '$filename' not readable.");
}
common_debug(sprintf(_("Getting backup from file '%s'."), $filename));
$html = file_get_contents($filename);
// Enqueue for processing.
$qm = QueueManager::get();
$qm->enqueue(array(common_current_user(), $html), 'dlcsback');
$this->success = true;
$this->showPage();
} catch (Exception $e) {
// Delete the file and re-throw
@unlink($_FILES[ImportDeliciousForm::FILEINPUT]['tmp_name']);
throw $e;
}
}
/**
* Show the content of the page
*
* @return void
*/
function showContent()
{
if ($this->success) {
$this->element('p', null,
_('Feed will be restored. '.
'Please wait a few minutes for results.'));
} else {
$form = new ImportDeliciousForm($this);
$form->show();
}
}
/**
* Return true if read only.
*
* MAY override
*
* @param array $args other arguments
*
* @return boolean is read only action?
*/
function isReadOnly($args)
{
return !$this->isPost();
}
}
/**
* A form for backing up the account.
*
* @category Account
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class ImportDeliciousForm extends Form
{
const FILEINPUT = 'deliciousbackupfile';
/**
* Constructor
*
* Set the encoding type, since this is a file upload.
*
* @param HTMLOutputter $out output channel
*
* @return ImportDeliciousForm this
*/
function __construct($out=null)
{
parent::__construct($out);
$this->enctype = 'multipart/form-data';
}
/**
* Class of the form.
*
* @return string the form's class
*/
function formClass()
{
return 'form_import_delicious';
}
/**
* URL the form posts to
*
* @return string the form's action URL
*/
function action()
{
return common_local_url('importdelicious');
}
/**
* Output form data
*
* Really, just instructions for doing a backup.
*
* @return void
*/
function formData()
{
$this->out->elementStart('p', 'instructions');
$this->out->raw(_('You can upload a backed-up '.
'delicious.com bookmarks file.'));
$this->out->elementEnd('p');
$this->out->elementStart('ul', 'form_data');
$this->out->elementStart('li', array ('id' => 'settings_attach'));
$this->out->element('input', array('name' => self::FILEINPUT,
'type' => 'file',
'id' => self::FILEINPUT));
$this->out->elementEnd('li');
$this->out->elementEnd('ul');
}
/**
* Buttons for the form
*
* In this case, a single submit button
*
* @return void
*/
function formActions()
{
$this->out->submit('submit',
_m('BUTTON', 'Upload'),
'submit',
null,
_('Upload the file'));
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,177 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* Notice stream of notices with a given attachment
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/**
* List notices that contain/link to/use a given URL
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class NoticebyurlAction extends Action
{
protected $url = null;
protected $file = null;
protected $notices = null;
protected $page = null;
/**
* For initializing members of the class.
*
* @param array $argarray misc. arguments
*
* @return boolean true
*/
function prepare($argarray)
{
parent::prepare($argarray);
$this->file = File::staticGet('id', $this->trimmed('id'));
if (empty($this->file)) {
throw new ClientException(_('Unknown URL'));
}
$pageArg = $this->trimmed('page');
$this->page = (empty($pageArg)) ? 1 : intval($pageArg);
$this->notices = $this->file->stream(($this->page - 1) * NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
return true;
}
/**
* Title of the page
*
* @return string page title
*/
function title()
{
if ($this->page == 1) {
return sprintf(_("Notices linking to %s"), $this->file->url);
} else {
return sprintf(_("Notices linking to %s, page %d"),
$this->file->url,
$this->page);
}
}
/**
* Handler method
*
* @param array $argarray is ignored since it's now passed in in prepare()
*
* @return void
*/
function handle($argarray=null)
{
$this->showPage();
}
/**
* Show main page content.
*
* Shows a list of the notices that link to the given URL
*
* @return void
*/
function showContent()
{
$nl = new NoticeList($this->notices, $this);
$nl->show();
$cnt = $nl->show();
$this->pagination($this->page > 1,
$cnt > NOTICES_PER_PAGE,
$this->page,
'noticebyurl',
array('id' => $this->file->id));
}
/**
* Return true if read only.
*
* MAY override
*
* @param array $args other arguments
*
* @return boolean is read only action?
*/
function isReadOnly($args)
{
return true;
}
/**
* Return last modified, if applicable.
*
* MAY override
*
* @return string last modified http header
*/
function lastModified()
{
// For comparison with If-Last-Modified
// If not applicable, return null
return null;
}
/**
* Return etag, if applicable.
*
* MAY override
*
* @return string etag http header
*/
function etag()
{
return null;
}
}

View File

@ -0,0 +1,145 @@
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2010, StatusNet, Inc.
*
* Show a single bookmark
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
if (!defined('STATUSNET')) {
// This check helps protect against security problems;
// your code file can't be executed directly from the web.
exit(1);
}
/**
* Show a single bookmark, with associated information
*
* @category Bookmark
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
* @link http://status.net/
*/
class ShowbookmarkAction extends ShownoticeAction
{
protected $bookmark = null;
/**
* For initializing members of the class.
*
* @param array $argarray misc. arguments
*
* @return boolean true
*/
function prepare($argarray)
{
OwnerDesignAction::prepare($argarray);
$this->user = User::staticGet('id', $this->trimmed('user'));
if (empty($this->user)) {
throw new ClientException(_('No such user.'), 404);
}
$this->profile = $this->user->getProfile();
if (empty($this->profile)) {
throw new ServerException(_('User without a profile.'));
}
$this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
sscanf($this->trimmed('crc32'), '%08x', $crc32);
if (empty($crc32)) {
throw new ClientException(_('No such URL.'), 404);
}
$dt = new DateTime($this->trimmed('created'),
new DateTimeZone('UTC'));
if (empty($dt)) {
throw new ClientException(_('No such create date.'), 404);
}
$bookmarks = Bookmark::getByCRC32($this->profile,
$crc32);
foreach ($bookmarks as $bookmark) {
$bdt = new DateTime($bookmark->created, new DateTimeZone('UTC'));
if ($bdt->format('U') == $dt->format('U')) {
$this->bookmark = $bookmark;
break;
}
}
if (empty($this->bookmark)) {
throw new ClientException(_('No such bookmark.'), 404);
}
$this->notice = Notice::staticGet('uri', $this->bookmark->uri);
if (empty($this->notice)) {
// Did we used to have it, and it got deleted?
throw new ClientException(_('No such bookmark.'), 404);
}
return true;
}
/**
* Title of the page
*
* Used by Action class for layout.
*
* @return string page tile
*/
function title()
{
return sprintf(_('%s\'s bookmark for "%s"'),
$this->user->nickname,
$this->bookmark->title);
}
/**
* Overload page title display to show bookmark link
*
* @return void
*/
function showPageTitle()
{
$this->elementStart('h1');
$this->element('a',
array('href' => $this->bookmark->url),
$this->bookmark->title);
$this->elementEnd('h1');
}
}

View File

@ -145,12 +145,10 @@ class OStatusPlugin extends Plugin
$user = $feed->getUser(); $user = $feed->getUser();
$id = $user->id; $id = $user->id;
$profile = $user->getProfile(); $profile = $user->getProfile();
$feed->setActivitySubject($profile->asActivityNoun('subject'));
} else if ($feed instanceof AtomGroupNoticeFeed) { } else if ($feed instanceof AtomGroupNoticeFeed) {
$salmonAction = 'groupsalmon'; $salmonAction = 'groupsalmon';
$group = $feed->getGroup(); $group = $feed->getGroup();
$id = $group->id; $id = $group->id;
$feed->setActivitySubject($group->asActivitySubject());
} else { } else {
return true; return true;
} }

View File

@ -47,6 +47,9 @@ class GroupsalmonAction extends SalmonAction
$this->clientError(_m('No such group.')); $this->clientError(_m('No such group.'));
} }
$this->target = $this->group;
$oprofile = Ostatus_profile::staticGet('group_id', $id); $oprofile = Ostatus_profile::staticGet('group_id', $id);
if ($oprofile) { if ($oprofile) {
// TRANS: Client error. // TRANS: Client error.

View File

@ -43,6 +43,8 @@ class UsersalmonAction extends SalmonAction
$this->clientError(_m('No such user.')); $this->clientError(_m('No such user.'));
} }
$this->target = $this->user;
return true; return true;
} }

View File

@ -419,7 +419,8 @@ class Ostatus_profile extends Managed_DataObject
{ {
$activity = new Activity($entry, $feed); $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 // @todo process all activity objects
switch ($activity->objects[0]->type) { switch ($activity->objects[0]->type) {
@ -441,6 +442,7 @@ class Ostatus_profile extends Managed_DataObject
} }
Event::handle('EndHandleFeedEntry', array($activity)); Event::handle('EndHandleFeedEntry', array($activity));
Event::handle('EndHandleFeedEntryWithProfile', array($activity, $this));
} }
} }
@ -453,36 +455,10 @@ class Ostatus_profile extends Managed_DataObject
*/ */
public function processPost($activity, $method) public function processPost($activity, $method)
{ {
if ($this->isGroup()) { $oprofile = $this->checkAuthorship($activity);
// 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)) { if (empty($oprofile)) {
// OK here! assume the default return false;
} 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;
} }
// It's not always an ActivityObject::NOTE, but... let's just say it is. // It's not always an ActivityObject::NOTE, but... let's just say it is.
@ -1772,6 +1748,45 @@ class Ostatus_profile extends Managed_DataObject
} }
return $oprofile; 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;
}
} }
/** /**

View File

@ -30,6 +30,7 @@ class SalmonAction extends Action
{ {
var $xml = null; var $xml = null;
var $activity = null; var $activity = null;
var $target = null;
function prepare($args) function prepare($args)
{ {
@ -82,7 +83,8 @@ class SalmonAction extends Action
StatusNet::setApi(true); // Send smaller error pages StatusNet::setApi(true); // Send smaller error pages
common_log(LOG_DEBUG, "Got a " . $this->activity->verb); 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) switch ($this->activity->verb)
{ {
case ActivityVerb::POST: case ActivityVerb::POST:
@ -118,6 +120,7 @@ class SalmonAction extends Action
throw new ClientException(_m("Unrecognized activity type.")); throw new ClientException(_m("Unrecognized activity type."));
} }
Event::handle('EndHandleSalmon', array($this->activity)); Event::handle('EndHandleSalmon', array($this->activity));
Event::handle('EndHandleSalmonTarget', array($this->activity, $this->target));
} }
} }