/**
* Upload an image via the API. Returns a shortened URL for the image
- * to the user.
+ * to the user. Apparently modelled after a former Twitpic API.
*
* @category API
* @package StatusNet
function showResponse(MediaFile $upload)
{
$this->initDocument();
- $this->elementStart('rsp', array('stat' => 'ok'));
+ $this->elementStart('rsp', array('stat' => 'ok', 'xmlns:atom'=>Activity::ATOM));
$this->element('mediaid', null, $upload->fileRecord->id);
$this->element('mediaurl', null, $upload->shortUrl());
+
+ $enclosure = $upload->fileRecord->getEnclosure();
+ $this->element('atom:link', array('rel' => 'enclosure',
+ 'href' => $enclosure->url,
+ 'type' => $enclosure->mimetype));
$this->elementEnd('rsp');
$this->endDocument();
}
*
* @param String $msg an error message
*/
- function clientError($msg)
+ function clientError($msg, $code=400, $format=null)
{
$this->initDocument();
$this->elementStart('rsp', array('stat' => 'fail'));
$this->element('err', $errAttr, null);
$this->elementEnd('rsp');
$this->endDocument();
+ exit;
}
}
* @link http://status.net/
*/
-if (!defined('STATUSNET')) {
- exit(1);
-}
+if (!defined('GNUSOCIAL')) { exit(1); }
/**
* Returns the most recent notices (default 20) posted by the authenticating
{
// We'll use the shared params from the Atom stub
// for other feed types.
- $atom = new AtomUserNoticeFeed($this->target->getUser(), $this->auth_user);
+ $atom = new AtomUserNoticeFeed($this->target->getUser(), $this->scoped);
$link = common_local_url(
'showstream',
- array('nickname' => $this->target->nickname)
+ array('nickname' => $this->target->getNickname())
);
$self = $this->getSelfUri();
// FriendFeed's SUP protocol
// Also added RSS and Atom feeds
- $suplink = common_local_url('sup', null, null, $this->target->id);
+ $suplink = common_local_url('sup', null, null, $this->target->getID());
header('X-SUP-ID: ' . $suplink);
$nextUrl = !empty($this->next_id)
? common_local_url('ApiTimelineUser',
array('format' => $this->format,
- 'id' => $this->target->id),
+ 'id' => $this->target->getID()),
array('max_id' => $this->next_id))
: null;
$prevUrl = common_local_url('ApiTimelineUser',
array('format' => $this->format,
- 'id' => $this->target->id),
+ 'id' => $this->target->getID()),
$prevExtra);
$firstUrl = common_local_url('ApiTimelineUser',
array('format' => $this->format,
- 'id' => $this->target->id));
+ 'id' => $this->target->getID()));
switch($this->format) {
case 'xml':
break;
case 'as':
header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE);
- $doc = new ActivityStreamJSONDocument($this->auth_user);
+ $doc = new ActivityStreamJSONDocument($this->scoped);
$doc->setTitle($atom->title);
$doc->addLink($link, 'alternate', 'text/html');
$doc->addItemsFromNotices($this->notices);
return '"' . implode(
':',
array($this->arg('action'),
- common_user_cache_hash($this->auth_user),
+ common_user_cache_hash($this->scoped),
common_language(),
- $this->target->id,
+ $this->target->getID(),
strtotime($this->notices[0]->created),
strtotime($this->notices[$last]->created))
)
function handlePost()
{
- if (empty($this->auth_user) ||
- $this->auth_user->id != $this->target->id) {
+ if (!$this->scoped instanceof Profile ||
+ !$this->target->sameAs($this->scoped)) {
// TRANS: Client error displayed trying to add a notice to another user's timeline.
- $this->clientError(_('Only the user can add to their own timeline.'));
+ $this->clientError(_('Only the user can add to their own timeline.'), 403);
}
// Only handle posts for Atom
$activity = new Activity($dom->documentElement);
- $saved = null;
-
- if (Event::handle('StartAtomPubNewActivity', array(&$activity, $this->target->getUser(), &$saved))) {
- if ($activity->verb != ActivityVerb::POST) {
- // TRANS: Client error displayed when not using the POST verb. Do not translate POST.
- $this->clientError(_('Can only handle POST activities.'));
- }
-
- $note = $activity->objects[0];
-
- if (!in_array($note->type, array(ActivityObject::NOTE,
- ActivityObject::BLOGENTRY,
- ActivityObject::STATUS))) {
- // TRANS: Client error displayed when using an unsupported activity object type.
- // TRANS: %s is the unsupported activity object type.
- $this->clientError(sprintf(_('Cannot handle activity object type "%s".'),
- $note->type));
- }
+ common_debug('AtomPub: Ignoring right now, but this POST was made to collection: '.$activity->id);
- $saved = $this->postNote($activity);
+ // Reset activity data so we can handle it in the same functions as with OStatus
+ // because we don't let clients set their own UUIDs... Not sure what AtomPub thinks
+ // about that though.
+ $activity->id = null;
+ $activity->actor = null; // not used anyway, we use $this->target
+ $activity->objects[0]->id = null;
- Event::handle('EndAtomPubNewActivity', array($activity, $this->target->getUser(), $saved));
+ $stored = null;
+ if (Event::handle('StartAtomPubNewActivity', array($activity, $this->target, &$stored))) {
+ // TRANS: Client error displayed when not using the POST verb. Do not translate POST.
+ throw new ClientException(_('Could not handle this Atom Activity.'));
}
-
- if (!empty($saved)) {
- header('HTTP/1.1 201 Created');
- header("Location: " . common_local_url('ApiStatusesShow', array('id' => $saved->id,
- 'format' => 'atom')));
- $this->showSingleAtomStatus($saved);
- }
- }
-
- function postNote($activity)
- {
- $note = $activity->objects[0];
-
- // Use summary as fallback for content
-
- if (!empty($note->content)) {
- $sourceContent = $note->content;
- } else if (!empty($note->summary)) {
- $sourceContent = $note->summary;
- } else if (!empty($note->title)) {
- $sourceContent = $note->title;
- } else {
- // @fixme fetch from $sourceUrl?
- // TRANS: Client error displayed when posting a notice without content through the API.
- // TRANS: %d is the notice ID (number).
- $this->clientError(sprintf(_('No content for notice %d.'), $note->id));
+ if (!$stored instanceof Notice) {
+ throw new ServerException('Server did not create a Notice object from handled AtomPub activity.');
}
+ Event::handle('EndAtomPubNewActivity', array($activity, $this->target, $stored));
- // Get (safe!) HTML and text versions of the content
-
- $rendered = common_purify($sourceContent);
- $content = common_strip_html($rendered);
-
- $shortened = $this->auth_user->shortenLinks($content);
-
- $options = array('is_local' => Notice::LOCAL_PUBLIC,
- 'rendered' => $rendered,
- 'replies' => array(),
- 'groups' => array(),
- 'tags' => array(),
- 'urls' => array());
-
- // accept remote URI (not necessarily a good idea)
-
- common_debug("Note ID is {$note->id}");
-
- if (!empty($note->id)) {
- $notice = Notice::getKV('uri', trim($note->id));
-
- if (!empty($notice)) {
- // TRANS: Client error displayed when using another format than AtomPub.
- // TRANS: %s is the notice URI.
- $this->clientError(sprintf(_('Notice with URI "%s" already exists.'), $note->id));
- }
- common_log(LOG_NOTICE, "Saving client-supplied notice URI '$note->id'");
- $options['uri'] = $note->id;
- }
-
- // accept remote create time (also maybe not such a good idea)
-
- if (!empty($activity->time)) {
- common_log(LOG_NOTICE, "Saving client-supplied create time {$activity->time}");
- $options['created'] = common_sql_date($activity->time);
- }
-
- // Check for optional attributes...
-
- if ($activity->context instanceof ActivityContext) {
-
- foreach ($activity->context->attention as $uri=>$type) {
- try {
- $profile = Profile::fromUri($uri);
- if ($profile->isGroup()) {
- $options['groups'][] = $profile->id;
- } else {
- $options['replies'][] = $uri;
- }
- } catch (UnknownUriException $e) {
- common_log(LOG_WARNING, sprintf('AtomPub post with unknown attention URI %s', $uri));
- }
- }
-
- // Maintain direct reply associations
- // @fixme what about conversation ID?
-
- if (!empty($activity->context->replyToID)) {
- $orig = Notice::getKV('uri',
- $activity->context->replyToID);
- if (!empty($orig)) {
- $options['reply_to'] = $orig->id;
- }
- }
-
- $location = $activity->context->location;
-
- if ($location) {
- $options['lat'] = $location->lat;
- $options['lon'] = $location->lon;
- if ($location->location_id) {
- $options['location_ns'] = $location->location_ns;
- $options['location_id'] = $location->location_id;
- }
- }
- }
-
- // Atom categories <-> hashtags
-
- foreach ($activity->categories as $cat) {
- if ($cat->term) {
- $term = common_canonical_tag($cat->term);
- if ($term) {
- $options['tags'][] = $term;
- }
- }
- }
-
- // Atom enclosures -> attachment URLs
- foreach ($activity->enclosures as $href) {
- // @fixme save these locally or....?
- $options['urls'][] = $href;
- }
-
- $saved = Notice::saveNew($this->target->id,
- $content,
- 'atompub', // TODO: deal with this
- $options);
-
- return $saved;
+ header('HTTP/1.1 201 Created');
+ header("Location: " . common_local_url('ApiStatusesShow', array('id' => $stored->getID(),
+ 'format' => 'atom')));
+ $this->showSingleAtomStatus($stored);
}
}
}
}
- function showContent()
+ function showPageNotice()
{
- $this->elementStart('p', array('id' => 'new_group'));
- $this->element('a', array('href' => common_local_url('newgroup'),
- 'class' => 'more'),
- // TRANS: Link text on group page to create a new group.
- _('Create a new group'));
- $this->elementEnd('p');
+ if ($this->scoped instanceof Profile && $this->scoped->sameAs($this->getTarget())) {
+ $this->element('p', 'instructions',
+ // TRANS: Page notice for page with an overview of all subscribed groups
+ // TRANS: of the logged in user's own profile.
+ _('These are the groups whose notices '.
+ 'you listen to.'));
+ } else {
+ $this->element('p', 'instructions',
+ // TRANS: Page notice for page with an overview of all groups a user other
+ // TRANS: than the logged in user. %s is the user nickname.
+ sprintf(_('These are the groups whose '.
+ 'notices %s listens to.'),
+ $this->target->getNickname()));
+ }
+ }
- $this->elementStart('p', array('id' => 'group_search'));
- $this->element('a', array('href' => common_local_url('groupsearch'),
- 'class' => 'more'),
- // TRANS: Link text on group page to search for groups.
- _('Search for more groups'));
- $this->elementEnd('p');
+ function showContent()
+ {
+ if ($this->scoped instanceof Profile && $this->scoped->sameAs($this->getTarget())) {
+ $notice =
+ // TRANS: Page notice of user's groups page.
+ // TRANS: %%%%action.groupsearch%%%% and %%%%action.newgroup%%%% are URLs. Do not change them.
+ // TRANS: This message contains Markdown links in the form [link text](link).
+ sprintf(_('Groups let you find and talk with ' .
+ 'people of similar interests. ' .
+ 'You can [search for groups](%%%%action.groups%%%%) in your instance or ' .
+ '[create a new group](%%%%action.newgroup%%%%). ' .
+ 'You can also follow groups ' .
+ 'from other GNU social instances: click on the remote button below ' .
+ 'and copy the group\'s link. ' .
+ 'You can find a list of GNU social groups [here](http://skilledtests.com/wiki/List_of_federated_GNU_social_groups)' .
+ ''));
+ $this->elementStart('div', 'instructions');
+ $this->raw(common_markup_to_html($notice));
+ $this->elementEnd('div');
+ }
if (Event::handle('StartShowUserGroupsContent', array($this))) {
$offset = ($this->page-1) * GROUPS_PER_PAGE;
if ($groups instanceof User_group) {
$gl = new GroupList($groups, $this->getTarget(), $this);
$cnt = $gl->show();
- $this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE,
- $this->page, 'usergroups',
- array('nickname' => $this->getTarget()->getNickname()));
- } else {
- $this->showEmptyListMessage();
+ if (0 == $cnt) {
+ $this->showEmptyListMessage();
+ } else {
+ $this->pagination($this->page > 1, $cnt > GROUPS_PER_PAGE,
+ $this->page, 'usergroups',
+ array('nickname' => $this->getTarget()->getNickname()));
+ }
}
Event::handle('EndShowUserGroupsContent', array($this));
// TRANS: Text on group page for a user that is not a member of any group.
// TRANS: %s is a user nickname.
$message = sprintf(_('%s is not a member of any group.'), $this->getTarget()->getNickname()) . ' ';
-
if (common_logged_in()) {
- $current_user = common_current_user();
if ($this->scoped->sameAs($this->getTarget())) {
// TRANS: Text on group page for a user that is not a member of any group. This message contains
// TRANS: a Markdown link in the form [link text](link) and a variable that should not be changed.
- $message .= _('Try [searching for groups](%%action.groupsearch%%) and joining them.');
+ $message = _('You are not member of any group yet. After you join a group ' .
+ 'you can send messages to its members using the ' .
+ 'syntax "!groupname".');
}
- }
+ }
$this->elementStart('div', 'guide');
$this->raw(common_markup_to_html($message));
$this->elementEnd('div');
$member->group_id = $group_id;
$member->profile_id = $profile_id;
$member->created = common_sql_now();
- $member->uri = self::newURI($profile_id, $group_id, $member->created);
+ $member->uri = self::newUri(Profile::getByID($profile_id),
+ User_group::getByID($group_id),
+ $member->created);
$result = $member->insert();
$act = new Activity();
- $act->id = $this->getURI();
+ $act->id = $this->getUri();
$act->actor = $member->asActivityObject();
$act->verb = ActivityVerb::JOIN;
mail_notify_group_join($this->getGroup(), $this->getMember());
}
- function getURI()
+ function getUri()
{
- if (!empty($this->uri)) {
- return $this->uri;
- } else {
- return self::newURI($this->profile_id, $this->group_id, $this->created);
- }
- }
-
- static function newURI($profile_id, $group_id, $created)
- {
- return TagURI::mint('join:%d:%d:%s',
- $profile_id,
- $group_id,
- common_date_iso8601($created));
+ return $this->uri ?: self::newUri($this->getMember(), $this->getGroup()->getProfile(), $this->created);
}
}
$object = new $classname();
foreach ($pkey as $col) {
if (!array_key_exists($col, $vals)) {
- throw new ServerException("Missing primary key column '{$col}'");
+ throw new ServerException("Missing primary key column '{$col}' for ".get_called_class()." among provided keys: ".implode(',', array_keys($vals)));
} elseif (is_null($vals[$col])) {
throw new ServerException("NULL values not allowed in getByPK for column '{$col}'");
}
{
// NOOP
}
+
+ static function newUri(Profile $actor, Managed_DataObject $object, $created=null)
+ {
+ if (is_null($created)) {
+ $created = common_sql_now();
+ }
+ return TagURI::mint(strtolower(get_called_class()).':%d:%s:%d:%s',
+ $actor->getID(),
+ ActivityUtils::resolveUri($object->getObjectType(), true),
+ $object->getID(),
+ common_date_iso8601($created));
+ }
}
}
}
- public function get_object_type($canonical=false) {
- return $canonical
- ? ActivityObject::canonicalType($this->object_type)
- : $this->object_type;
- }
-
- // activity plugins tend to use this function instead, but it's the same
- public function getObjectType()
- {
- return $this->get_object_type();
+ public function getObjectType($canonical=false) {
+ return ActivityUtils::resolveUri($this->object_type, $canonical);
}
public static function getByUri($uri)
}
extract($options, EXTR_SKIP);
+ // dupe check
$stored = new Notice();
- if (!empty($uri)) {
+ if (!empty($uri) && !ActivityUtils::compareVerbs($act->verb, array(ActivityVerb::DELETE))) {
$stored->uri = $uri;
if ($stored->find()) {
common_debug('cannot create duplicate Notice URI: '.$stored->uri);
$urls[] = $href;
}
+ if (ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::POST))) {
+ if (empty($act->objects[0]->type)) {
+ // Default type for the post verb is 'note', but we know it's
+ // a 'comment' if it is in reply to something.
+ $stored->object_type = empty($stored->reply_to) ? ActivityObject::NOTE : ActivityObject::COMMENT;
+ } else {
+ //TODO: Is it safe to always return a relative URI? The
+ // JSON version of ActivityStreams always use it, so we
+ // should definitely be able to handle it...
+ $stored->object_type = ActivityUtils::resolveUri($act->objects[0]->type, true);
+ }
+ }
+
if (Event::handle('StartNoticeSave', array(&$stored))) {
// XXX: some of these functions write to the DB
try {
- $stored->insert(); // throws exception on error
+ $result = $stored->insert(); // throws exception on error
if ($notloc instanceof Notice_location) {
$notloc->notice_id = $stored->getID();
$object = null;
Event::handle('StoreActivityObject', array($act, $stored, $options, &$object));
if (empty($object)) {
- throw new ServerException('Unsuccessful call to StoreActivityObject '.$stored->uri . ': '.$act->asString());
+ throw new ServerException('Unsuccessful call to StoreActivityObject '.$stored->getUri() . ': '.$act->asString());
}
// If it's not part of a conversation, it's the beginning
$this->uri = sprintf('%s%s=%d:%s=%s',
TagURI::mint(),
'noticeId', $this->id,
- 'objectType', $this->get_object_type(true));
+ 'objectType', $this->getObjectType(true));
$changed = true;
}
'consumer_key' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'application consumer key'),
'name' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'name of the application'),
'description' => array('type' => 'varchar', 'length' => 191, 'description' => 'description of the application'),
- 'icon' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'application icon'),
+ 'icon' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'default' => '/theme/base/default-avatar-stream.png', 'description' => 'application icon'),
'source_url' => array('type' => 'varchar', 'length' => 191, 'description' => 'application homepage - used for source link'),
'organization' => array('type' => 'varchar', 'length' => 191, 'description' => 'name of the organization running the application'),
'homepage' => array('type' => 'varchar', 'length' => 191, 'description' => 'homepage for the organization'),
public function hasPassword()
{
try {
- return !empty($this->getUser()->hasPassword());
+ return $this->getUser()->hasPassword();
} catch (NoSuchUserException $e) {
return false;
}
$sub->jabber = 1;
$sub->sms = 1;
$sub->created = common_sql_now();
- $sub->uri = self::newURI($sub->subscriber,
- $sub->subscribed,
+ $sub->uri = self::newUri($subscriber,
+ $other,
$sub->created);
$result = $sub->insert();
return $sub;
}
- function asActivity()
+ public function getSubscriber()
{
- $subscriber = Profile::getKV('id', $this->subscriber);
- $subscribed = Profile::getKV('id', $this->subscribed);
+ return Profile::getByID($this->subscriber);
+ }
- if (!$subscriber instanceof Profile) {
- throw new NoProfileException($this->subscriber);
- }
+ public function getSubscribed()
+ {
+ return Profile::getByID($this->subscribed);
+ }
- if (!$subscribed instanceof Profile) {
- throw new NoProfileException($this->subscribed);
- }
+ function asActivity()
+ {
+ $subscriber = $this->getSubscriber();
+ $subscribed = $this->getSubscribed();
$act = new Activity();
// XXX: rationalize this with the URL
- $act->id = $this->getURI();
+ $act->id = $this->getUri();
$act->time = strtotime($this->created);
// TRANS: Activity title when subscribing to another person.
return parent::update($dataObject);
}
- function getURI()
- {
- if (!empty($this->uri)) {
- return $this->uri;
- } else {
- return self::newURI($this->subscriber, $this->subscribed, $this->created);
- }
- }
-
- static function newURI($subscriber_id, $subscribed_id, $created)
+ public function getUri()
{
- return TagURI::mint('follow:%d:%d:%s',
- $subscriber_id,
- $subscribed_id,
- common_date_iso8601($created));
+ return $this->uri ?: self::newUri($this->getSubscriber(), $this->getSubscribed(), $this->created);
}
}
if (!empty($this->link)) {
$xs->element('link', array('rel' => 'alternate',
- 'type' => 'text/html'),
- $this->link);
+ 'type' => 'text/html',
+ 'href' => $this->link));
}
}
*
* @return boolean hook value
*/
- function onNoticeDeleteRelated(Notice $notice)
+ public function onNoticeDeleteRelated(Notice $notice)
{
if ($this->isMyNotice($notice)) {
- $this->deleteRelated($notice);
+ try {
+ $this->deleteRelated($notice);
+ } catch (AlreadyFulfilledException $e) {
+ // Nothing to see here, it's obviously already gone...
+ }
}
// Always continue this event in our activity handling plugins.
/**
* Handle object posted via AtomPub
*
- * @param Activity &$activity Activity that was posted
+ * @param Activity $activity Activity that was posted
* @param Profile $scoped Profile of user posting
* @param Notice &$notice Resulting notice
*
* @return boolean hook value
*/
- // FIXME: Make sure we can really do strong Notice typing with a $notice===null without having =null here
- public function onStartAtomPubNewActivity(Activity &$activity, Profile $scoped, Notice &$notice)
+ public function onStartAtomPubNewActivity(Activity $activity, Profile $scoped, Notice &$notice=null)
{
if (!$this->isMyActivity($activity)) {
return true;
$notice = $this->saveNoticeFromActivity($activity, $scoped, $options);
- Event::handle('EndAtomPubNewActivity', array($activity, $scoped, $notice));
-
return false;
}
// TRANS: Client error thrown when authentication fails because a user clicked "Cancel".
$this->clientError(_('Could not authenticate you.'), 401);
- } elseif ($required) {
+ } else {
// $this->auth_user_nickname - i.e. PHP_AUTH_USER - will have a value since it was not empty
$user = common_check_user($this->auth_user_nickname,
$this->auth_user = null;
}
- // By default, basic auth users have rw access
- $this->access = self::READ_WRITE;
-
- if (!$this->auth_user instanceof User) {
+ if ($required && $this->auth_user instanceof User) {
+ // By default, basic auth users have rw access
+ $this->access = self::READ_WRITE;
+ } elseif ($required) {
$msg = sprintf(
"basic auth nickname = %s",
$this->auth_user_nickname
header('WWW-Authenticate: Basic realm="' . $realm . '"');
// TRANS: Client error thrown when authentication fails.
$this->clientError(_('Could not authenticate you.'), 401);
+ } else {
+ // all get rw access for actions that don't require auth
+ $this->access = self::READ_WRITE;
}
- } else {
- // all get rw access for actions that don't require auth
- $this->access = self::READ_WRITE;
}
}
'Favorite' => array(),
'Share' => array(),
'LRDD' => array(),
- 'StrictTransportSecurity' => array(),
),
'default' => array(
'Activity' => array(),
'nickname_desc sort mode can only be use when searching profile.'
);
} else {
- return $this->target->orderBy('nickname DESC');
+ return $this->target->orderBy(sprintf('%1$s.nickname DESC', $this->table));
}
break;
case 'nickname_asc':
'nickname_desc sort mode can only be use when searching profile.'
);
} else {
- return $this->target->orderBy('nickname ASC');
+ return $this->target->orderBy(sprintf('%1$s.nickname ASC', $this->table));
}
break;
default:
function query($q)
{
if ('profile' === $this->table) {
- $qry = sprintf('(nickname LIKE "%%%1$s%%" OR '.
- ' fullname LIKE "%%%1$s%%" OR '.
- ' location LIKE "%%%1$s%%" OR '.
- ' bio LIKE "%%%1$s%%" OR '.
- ' homepage LIKE "%%%1$s%%")', $this->target->escape($q, true));
+ $qry = sprintf('(%2$s.nickname LIKE "%%%1$s%%" OR '.
+ ' %2$s.fullname LIKE "%%%1$s%%" OR '.
+ ' %2$s.location LIKE "%%%1$s%%" OR '.
+ ' %2$s.bio LIKE "%%%1$s%%" OR '.
+ ' %2$s.homepage LIKE "%%%1$s%%")',
+ $this->target->escape($q, true),
+ $this->table);
} else if ('notice' === $this->table) {
$qry = sprintf('content LIKE "%%%1$s%%"', $this->target->escape($q, true));
} else {
*/
protected function saveObjectFromActivity(Activity $act, Notice $stored, array $options=array())
{
+ // Everything is done in the StartNoticeSave event
+ return true;
+ }
+
+ // FIXME: Put this in lib/activityhandlerplugin.php when we're ready
+ // with the other microapps/activityhandlers as well.
+ // Also it should be StartNoticeAsActivity (with a prepped Activity, including ->context etc.)
+ public function onEndNoticeAsActivity(Notice $stored, Activity $act, Profile $scoped=null)
+ {
+ if (!$this->isMyNotice($stored)) {
+ return true;
+ }
+
+ common_debug('Extending activity '.$stored->id.' with '.get_called_class());
+ $this->extendActivity($stored, $act, $scoped);
+ return false;
+ }
+
+ /**
+ * This is run before ->insert, so our task in this function is just to
+ * delete if it is the delete verb.
+ */
+ public function onStartNoticeSave(Notice $stored)
+ {
+ // DELETE is a bit special, we have to remove the existing entry and then
+ // add a new one with the same URI in order to trigger the distribution.
+ // (that's why we don't use $this->isMyNotice(...))
+ if (!ActivityUtils::compareVerbs($stored->verb, array(ActivityVerb::DELETE))) {
+ return true;
+ }
+
+ try {
+ $target = Notice::getByUri($stored->uri);
+ } catch (NoResultException $e) {
+ throw new AlreadyFulfilledException('Notice URI not found, so we have nothing to delete.');
+ }
+
+ $actor = $stored->getProfile();
+ $owner = $target->getProfile();
+
+ if ($owner->hasRole(Profile_role::DELETED)) {
+ // Don't bother with replacing notices if its author is being deleted.
+ // The later "StoreActivityObject" will pick this up and execute
+ // the deletion then.
+ // (the "delete verb notice" is too new to ever pass through Notice::saveNew
+ // which otherwise wouldn't execute the StoreActivityObject event)
+ return true;
+ }
+
+ // Since the user deleting may not be the same as the notice's owner,
+ // double-check this and also set the "re-stored" notice profile_id.
+ if (!$actor->sameAs($owner) && !$actor->hasRight(Right::DELETEOTHERSNOTICE)) {
+ throw new AuthorizationException(_('You are not allowed to delete another user\'s notice.'));
+ }
+
+ // We copy the identifying fields and replace the sensitive ones.
+ //$stored->id = $target->id; // We can't copy this since DB_DataObject won't inject it anyway
+ $props = array('uri', 'profile_id', 'conversation', 'reply_to', 'created', 'repeat_of', 'object_type', 'is_local', 'scope');
+ foreach($props as $prop) {
+ $stored->$prop = $target->$prop;
+ }
+ //$stored->content = $stored->content ?: _('Notice deleted.');
+ //$stored->rendered = $stored->rendered ?: $stored->rendered;
+ common_debug('DELETENOTICE: Replacement notice has been prepared: '.var_export($stored, true));
+
// Let's see if this has been deleted already.
- $deleted = Deleted_notice::getKV('uri', $act->id);
+ $deleted = Deleted_notice::getKV('uri', $stored->getUri());
if ($deleted instanceof Deleted_notice) {
return $deleted;
}
- $target = Notice::getByUri($act->objects[0]->id);
- common_debug('DELETING notice: ' . $act->objects[0]->id . ' on behalf of profile id==' . $target->getProfile()->getID());
-
$deleted = new Deleted_notice();
$deleted->id = $target->getID();
- $deleted->profile_id = $target->getProfile()->getID();
- $deleted->uri = $act->id;
- $deleted->act_uri = $target->getUri();
- $deleted->act_created = $target->created;
+ $deleted->profile_id = $actor->getID();
+ $deleted->uri = $stored->getUri();
+ $deleted->act_uri = $stored->getUri();
+ $deleted->act_created = $stored->created;
$deleted->created = common_sql_now();
$result = $deleted->insert();
throw new ServerException('Could not insert Deleted_notice entry into database!');
}
+ // Now we delete the original notice, leaving the id and uri free.
$target->delete();
- return $deleted;
- }
-
- // FIXME: Put this in lib/activityhandlerplugin.php when we're ready
- // with the other microapps/activityhandlers as well.
- // Also it should be StartNoticeAsActivity (with a prepped Activity, including ->context etc.)
- public function onEndNoticeAsActivity(Notice $stored, Activity $act, Profile $scoped=null)
- {
- if (!$this->isMyNotice($stored)) {
- return true;
- }
-
- common_debug('Extending activity '.$stored->id.' with '.get_called_class());
- $this->extendActivity($stored, $act, $scoped);
- return false;
+ return true;
}
public function extendActivity(Notice $stored, Activity $act, Profile $scoped=null)
public $profile_id; // int(4) not_null
public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
public $act_uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space
+ public $act_created; // datetime() not_null
public $created; // datetime() not_null
- public $deleted; // datetime() not_null
public static function schemaDef()
{
return array(
'fields' => array(
'id' => array('type' => 'int', 'not null' => true, 'description' => 'notice ID'),
- 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'author of the notice'),
+ 'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'profile that deleted the notice'),
'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI of the deleted notice'),
'act_uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'URI of the delete activity, may exist in notice table'),
'act_created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date the notice record was created'),
}
$act = new Activity();
- $act->type = ActivityObject::ACTIVITY;
$act->verb = ActivityVerb::DELETE;
$act->time = time();
- $act->id = self::newUri($actor, $notice);
+ $act->id = $notice->getUri();
$act->content = sprintf(_m('<a href="%1$s">%2$s</a> deleted notice <a href="%3$s">{{%4$s}}</a>.'),
htmlspecialchars($actor->getUrl()),
$act->actor = $actor->asActivityObject();
$act->target = new ActivityObject(); // We don't save the notice object, as it's supposed to be removed!
$act->target->id = $notice->getUri();
+ $act->target->type = $notice->getObjectType();
$act->objects = array(clone($act->target));
$url = $notice->getUrl();
static public function fromStored(Notice $stored)
{
$class = get_called_class();
- $object = new $class;
- $object->uri = $stored->getUri(); // Lookup by delete activity's URI! (that's what is _stored_ in our db!)
- if (!$object->find(true)) {
- throw new NoResultException($object);
- }
- return $object;
+ return self::getByPK(array('uri' => $stored->getUri()));
}
+ // The one who deleted the notice, not the notice's author
public function getActor()
{
return Profile::getByID($this->profile_id);
}
+ // As above: The one who deleted the notice, not the notice's author
public function getActorObject()
{
return $this->getActor()->asActivityObject();
public function getStored()
{
- $uri = $this->getTargetUri();
+ $uri = $this->getUri();
if (!isset($this->_stored[$uri])) {
- $stored = new Notice();
- $stored->uri = $uri;
- if (!$stored->find(true)) {
- throw new NoResultException($stored);
- }
- $this->_stored[$uri] = $stored;
+ $this->_stored[$uri] = Notice::getByPK(array('uri' => $uri));
}
return $this->_stored[$uri];
}
- public function getTargetUri()
- {
- return $this->act_uri;
- }
-
public function getUri()
{
return $this->uri;
$actobj->type = ActivityObject::ACTIVITY;
$actobj->actor = $this->getActorObject();
$actobj->target = new ActivityObject();
- $actobj->target->id = $this->getTargetUri();
+ $actobj->target->id = $this->getUri();
+ // FIXME: actobj->target->type? as in extendActivity, and actobj->target->actor maybe?
$actobj->objects = array(clone($actobj->target));
$actobj->verb = ActivityVerb::DELETE;
$actobj->title = ActivityUtils::verbToTitle($actobj->verb);
$actobj->content = sprintf(_m('<a href="%1$s">%2$s</a> deleted notice {{%3$s}}.'),
htmlspecialchars($actor->getUrl()),
htmlspecialchars($actor->getBestName()),
- htmlspecialchars($this->getTargetUri())
+ htmlspecialchars($this->getUri())
);
return $actobj;
static public function extendActivity(Notice $stored, Activity $act, Profile $scoped=null)
{
- // the original notice is deleted, but we have stored some important data
- $object = self::fromStored($stored);
-
+ // the original notice id and type is still stored in the Notice table
+ // so we use that information to describe the delete activity
$act->target = new ActivityObject();
- $act->target->id = $object->getTargetUri();
+ $act->target->id = $stored->getUri();
+ $act->target->type = $stored->getObjectType();
$act->objects = array(clone($act->target));
- $act->context->replyToID = $object->getTargetUri();
$act->title = ActivityUtils::verbToTitle($act->verb);
}
- static function newUri(Profile $actor, Managed_DataObject $object, $created=null)
- {
- if (is_null($created)) {
- $created = common_sql_now();
- }
- return TagURI::mint(strtolower(get_called_class()).':%d:%s:%d:%s',
- $actor->getID(),
- ActivityUtils::resolveUri($object->getObjectType(), true),
- $object->getID(),
- common_date_iso8601($created));
- }
-
static public function beforeSchemaUpdate()
{
$table = strtolower(get_called_class());
*/
function deleteRelated(Notice $notice)
{
- if ($this->isMyNotice($notice)) {
-
- $nb = Bookmark::getByNotice($notice);
-
- if (!empty($nb)) {
- $nb->delete();
- }
- }
+ try {
+ $nb = Bookmark::fromStored($notice);
+ } catch (NoResultException $e) {
+ throw new AlreadyFulfilledException('Bookmark already gone when deleting: '.$e->getMessage());
+ }
+ $nb->delete();
return true;
}
"Formatting notice {$notice->uri} as a bookmark.");
$object = new ActivityObject();
- $nb = Bookmark::getByNotice($notice);
+ $nb = Bookmark::fromStored($notice);
$object->id = $notice->uri;
$object->type = ActivityObject::BOOKMARK;
- $object->title = $nb->title;
- $object->summary = $nb->description;
+ $object->title = $nb->getTitle();
+ $object->summary = $nb->getDescription();
$object->link = $notice->getUrl();
// Attributes of the URL
{
assert($obj->type == ActivityObject::BOOKMARK);
- $bm = Bookmark::getKV('uri', $obj->id);
-
- if (empty($bm)) {
- throw new ServerException("Unknown bookmark: " . $obj->id);
- }
+ $bm = Bookmark::getByPK(array('uri' => $obj->id));
- $out['displayName'] = $bm->title;
- $out['targetUrl'] = $bm->url;
+ $out['displayName'] = $bm->getTitle();
+ $out['targetUrl'] = $bm->getUrl();
return true;
}
$nli->out->elementEnd('div');
}
- protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ public function getTitle()
{
- $nb = Bookmark::getByNotice($stored);
-
- if (empty($nb)) {
- common_log(LOG_ERR, "No bookmark for notice {$stored->id}");
- parent::showContent();
- return;
- } else if (empty($nb->url)) {
- common_log(LOG_ERR, "No url for bookmark {$nb->id} for notice {$stored->id}");
- parent::showContent();
- return;
+ return $this->title;
+ }
+
+ public function getUrl()
+ {
+ if (empty($this->url)) {
+ throw new InvalidUrlException($this->url);
}
+ return $this->url;
+ }
+
+ protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
+ {
+ $nb = Bookmark::fromStored($stored);
$profile = $stored->getProfile();
// Whether to nofollow
- $attrs = array('href' => $nb->url, 'class' => 'bookmark-title');
+ $attrs = array('href' => $nb->getUrl(), 'class' => 'bookmark-title');
$nf = common_config('nofollow', 'external');
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-if (!defined('STATUSNET')) {
- exit(1);
-}
+if (!defined('GNUSOCIAL')) { exit(1); }
/**
* For storing the fact that a notice is a bookmark
'description' => array('type' => 'text'),
'created' => array('type' => 'datetime', 'not null' => true),
),
- 'primary key' => array('id'),
+ 'primary key' => array('uri'),
'unique keys' => array(
- 'bookmark_uri_key' => array('uri'),
+ 'bookmark_id_key' => array('id'),
),
'foreign keys' => array(
- 'bookmark_profile_id_fkey' => array('profile', array('profile_id' => 'id'))
+ 'bookmark_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
+ 'bookmark_uri_fkey' => array('notice', array('uri' => 'uri')),
),
'indexes' => array('bookmark_created_idx' => array('created'),
'bookmark_url_idx' => array('url'),
/**
* Get a bookmark based on a notice
*
- * @param Notice $notice Notice to check for
+ * @param Notice $stored Notice activity which represents the Bookmark
*
- * @return Bookmark found bookmark or null
+ * @return Bookmark The found bookmark object.
+ * @throws NoResultException When you don't find it after all.
*/
- static function getByNotice($notice)
+ static public function fromStored(Notice $stored)
{
- return self::getKV('uri', $notice->uri);
+ return self::getByPK(array('uri' => $stored->getUri()));
}
/**
'object_type' => ActivityObject::BOOKMARK),
$options);
- if (!array_key_exists('uri', $options)) {
- $options['uri'] = $nb->uri;
- }
+ $options['uri'] = $nb->uri;
try {
$saved = Notice::saveNew($profile->id,
/**
* https://wiki.diasporafoundation.org/Federation_protocol_overview
+ * http://www.rubydoc.info/github/Raven24/diaspora-federation/master/DiasporaFederation/Salmon/EncryptedSlap
*
* Constructing the encryption header
*/
+ // For some reason diaspora wants the salmon slap in a <diaspora> header.
+ $xs->elementStart('diaspora', array('xmlns'=>'https://joindiaspora.com/protocol'));
+
/**
* Choose an AES key and initialization vector, suitable for the
* aes-256-cbc cipher. I shall refer to this as the “inner key”
* 2. Base64-encode the encrypted payload message.
*/
$payload = $inner_key->encrypt($magic_env->getData());
+ //FIXME: This means we don't actually put an <atom:entry> in the payload,
+ // since Diaspora has its own update method! Silly me. Read up on:
+ // https://wiki.diasporafoundation.org/Federation_Message_Semantics
$magic_env->signMessage(base64_encode($payload), 'application/xml');
$xs->element('me:sig', null, $magic_env->getSignature());
$xs->elementEnd('me:env');
+ $xs->elementEnd('entry');
+
return false;
}
*/
class EventPlugin extends MicroAppPlugin
{
+
+ var $oldSaveNew = true;
+
/**
* Set up our tables (event and rsvp)
*
throw new Exception(_m('Wrong type for object.'));
}
+ $dtstart = $happeningObj->element->getElementsByTagName('dtstart');
+ if($dtstart->length == 0) {
+ // TRANS: Exception thrown when has no start date
+ throw new Exception(_m('No start date for event.'));
+ }
+
+ $dtend = $happeningObj->element->getElementsByTagName('dtend');
+ if($dtend->length == 0) {
+ // TRANS: Exception thrown when has no end date
+ throw new Exception(_m('No end date for event.'));
+ }
+
+ // location is optional
+ $location = null;
+ $location_object = $happeningObj->element->getElementsByTagName('location');
+ if($location_object->length > 0) {
+ $location = $location_object->item(0)->nodeValue;
+ }
+
+ // url is optional
+ $url = null;
+ $url_object = $happeningObj->element->getElementsByTagName('url');
+ if($url_object->length > 0) {
+ $url = $url_object->item(0)->nodeValue;
+ }
+
$notice = null;
switch ($activity->verb) {
case ActivityVerb::POST:
// FIXME: get startTime, endTime, location and URL
$notice = Happening::saveNew($actor,
- $start_time,
- $end_time,
+ $dtstart->item(0)->nodeValue,
+ $dtend->item(0)->nodeValue,
$happeningObj->title,
- null,
+ $location,
$happeningObj->summary,
- null,
+ $url,
$options);
break;
case RSVP::POSITIVE:
array('xmlns' => 'urn:ietf:params:xml:ns:xcal'),
common_date_iso8601($happening->end_time));
- // FIXME: add location
- // FIXME: add URL
-
+ $obj->extra[] = array('location', false, $happening->location);
+ $obj->extra[] = array('url', false, $happening->url);
+
// XXX: probably need other stuff here
return $obj;
$profile = $this->user->getProfile();
$saved = Happening::saveNew($profile,
- $this->startTime,
- $this->endTime,
+ common_sql_date($this->startTime),
+ common_sql_date($this->endTime),
$this->title,
$this->location,
$this->description,
$ev->id = UUID::gen();
$ev->profile_id = $profile->id;
- $ev->start_time = common_sql_date($start_time);
- $ev->end_time = common_sql_date($end_time);
+ $ev->start_time = $start_time;
+ $ev->end_time = $end_time;
$ev->title = $title;
$ev->location = $location;
$ev->description = $description;
// We (should've in this case) created it ourselves, so we tag it ourselves
return self::newUri($this->getActor(), $this->getTarget(), $this->created);
}
-
- static function newUri(Profile $actor, Managed_DataObject $target, $created=null)
- {
- if (is_null($created)) {
- $created = common_sql_now();
- }
- return TagURI::mint(strtolower(get_called_class()).':%d:%s:%d:%s',
- $actor->id,
- ActivityUtils::resolveUri(self::getObjectType(), true),
- $target->id,
- common_date_iso8601($created));
- }
}