* @link http://status.net/
*/
-if (!defined('STATUSNET')) {
+if (!defined('GNUSOCIAL')) {
exit(1);
}
*/
class ApiTimelineUserAction extends ApiBareAuthAction
{
- var $notices = null;
+ public $notices = null;
+
+ public $next_id = null;
/**
- * Take arguments for running
+ * We expose AtomPub here, so non-GET/HEAD reqs must be read/write.
*
- * @param array $args $_REQUEST args
+ * @param array $args other arguments
*
- * @return boolean success flag
+ * @return boolean true
*/
- protected function prepare(array $args=array())
- {
- parent::prepare($args);
- $this->target = $this->getTargetProfile($this->arg('id'));
+ public function isReadOnly($args)
+ {
+ return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD');
+ }
- if (!($this->target instanceof Profile)) {
- // TRANS: Client error displayed requesting most recent notices for a non-existing user.
- $this->clientError(_('No such user.'), 404);
+ /**
+ * When was this feed last modified?
+ *
+ * @return string datestamp of the latest notice in the stream
+ */
+ public function lastModified()
+ {
+ if (!empty($this->notices) && (count($this->notices) > 0)) {
+ return strtotime($this->notices[0]->created);
}
- $this->notices = $this->getNotices();
-
- return true;
+ return null;
}
/**
- * Handle the request
+ * An entity tag for this stream
*
- * Just show the notices
+ * Returns an Etag based on the action name, language, user ID, and
+ * timestamps of the first and last notice in the timeline
*
- * @return void
+ * @return string etag
*/
- protected function handle()
+ public function etag()
{
- parent::handle();
+ if (!empty($this->notices) && (count($this->notices) > 0)) {
+ $last = count($this->notices) - 1;
- if ($this->isPost()) {
- $this->handlePost();
- } else {
- $this->showTimeline();
+ return '"' . implode(
+ ':',
+ array($this->arg('action'),
+ common_user_cache_hash($this->scoped),
+ common_language(),
+ $this->target->getID(),
+ strtotime($this->notices[0]->created),
+ strtotime($this->notices[$last]->created))
+ )
+ . '"';
}
+
+ return null;
}
/**
- * Show the timeline of notices
+ * Take arguments for running
*
- * @return void
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ * @throws AuthorizationException
+ * @throws ClientException
*/
- function showTimeline()
+ protected function prepare(array $args = [])
{
- // We'll use the shared params from the Atom stub
- // for other feed types.
- $atom = new AtomUserNoticeFeed($this->target->getUser(), $this->auth_user);
-
- $link = common_local_url(
- 'showstream',
- array('nickname' => $this->target->nickname)
- );
-
- $self = $this->getSelfUri();
-
- // FriendFeed's SUP protocol
- // Also added RSS and Atom feeds
-
- $suplink = common_local_url('sup', null, null, $this->target->id);
- header('X-SUP-ID: ' . $suplink);
-
- switch($this->format) {
- case 'xml':
- $this->showXmlTimeline($this->notices);
- break;
- case 'rss':
- $this->showRssTimeline(
- $this->notices,
- $atom->title,
- $link,
- $atom->subtitle,
- $suplink,
- $atom->logo,
- $self
- );
- break;
- case 'atom':
- header('Content-Type: application/atom+xml; charset=utf-8');
-
- $atom->setId($self);
- $atom->setSelfLink($self);
-
- // Add navigation links: next, prev, first
- // Note: we use IDs rather than pages for navigation; page boundaries
- // change too quickly!
-
- if (!empty($this->next_id)) {
- $nextUrl = common_local_url('ApiTimelineUser',
- array('format' => 'atom',
- 'id' => $this->target->id),
- array('max_id' => $this->next_id));
-
- $atom->addLink($nextUrl,
- array('rel' => 'next',
- 'type' => 'application/atom+xml'));
- }
-
- if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
-
- $lastNotice = $this->notices[0];
- $lastId = $lastNotice->id;
-
- $prevUrl = common_local_url('ApiTimelineUser',
- array('format' => 'atom',
- 'id' => $this->target->id),
- array('since_id' => $lastId));
-
- $atom->addLink($prevUrl,
- array('rel' => 'prev',
- 'type' => 'application/atom+xml'));
- }
+ parent::prepare($args);
- if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
+ $this->target = $this->getTargetProfile($this->arg('id'));
- $firstUrl = common_local_url('ApiTimelineUser',
- array('format' => 'atom',
- 'id' => $this->target->id));
+ if (!($this->target instanceof Profile)) {
+ // TRANS: Client error displayed requesting most recent notices for a non-existing user.
+ $this->clientError(_('No such user.'), 404);
+ }
- $atom->addLink($firstUrl,
- array('rel' => 'first',
- 'type' => 'application/atom+xml'));
+ if (!$this->target->isLocal()) {
+ $this->serverError(_('Remote user timelines are not available here yet.'), 501);
+ }
- }
+ $this->notices = $this->getNotices();
- $atom->addEntryFromNotices($this->notices);
- $this->raw($atom->getString());
-
- break;
- case 'json':
- $this->showJsonTimeline($this->notices);
- break;
- case 'as':
- header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE);
- $doc = new ActivityStreamJSONDocument($this->auth_user);
- $doc->setTitle($atom->title);
- $doc->addLink($link, 'alternate', 'text/html');
- $doc->addItemsFromNotices($this->notices);
-
- // XXX: Add paging extension?
-
- $this->raw($doc->asString());
- break;
- default:
- // TRANS: Client error displayed when coming across a non-supported API method.
- $this->clientError(_('API method not found.'), $code = 404);
- }
+ return true;
}
/**
*
* @return array notices
*/
- function getNotices()
+ public function getNotices()
{
- $notices = array();
+ $notices = [];
- $notice = $this->target->getNotices(($this->page-1) * $this->count,
- $this->count + 1,
- $this->since_id,
- $this->max_id,
- $this->scoped);
+ $notice = $this->target->getNotices(
+ ($this->page - 1) * $this->count,
+ $this->count + 1,
+ $this->since_id,
+ $this->max_id,
+ $this->scoped
+ );
while ($notice->fetch()) {
if (count($notices) < $this->count) {
}
/**
- * We expose AtomPub here, so non-GET/HEAD reqs must be read/write.
- *
- * @param array $args other arguments
- *
- * @return boolean true
- */
-
- function isReadOnly($args)
- {
- return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD');
- }
-
- /**
- * When was this feed last modified?
- *
- * @return string datestamp of the latest notice in the stream
- */
- function lastModified()
- {
- if (!empty($this->notices) && (count($this->notices) > 0)) {
- return strtotime($this->notices[0]->created);
- }
-
- return null;
- }
-
- /**
- * An entity tag for this stream
+ * Handle the request
*
- * Returns an Etag based on the action name, language, user ID, and
- * timestamps of the first and last notice in the timeline
+ * Just show the notices
*
- * @return string etag
+ * @return void
+ * @throws ClientException
+ * @throws ServerException
*/
- function etag()
+ protected function handle()
{
- if (!empty($this->notices) && (count($this->notices) > 0)) {
- $last = count($this->notices) - 1;
+ parent::handle();
- return '"' . implode(
- ':',
- array($this->arg('action'),
- common_user_cache_hash($this->auth_user),
- common_language(),
- $this->target->id,
- strtotime($this->notices[0]->created),
- strtotime($this->notices[$last]->created))
- )
- . '"';
+ if ($this->isPost()) {
+ $this->handlePost();
+ } else {
+ $this->showTimeline();
}
-
- return null;
}
- function handlePost()
+ public 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.'));
- }
+ common_debug('AtomPub: Ignoring right now, but this POST was made to collection: ' . $activity->id);
- $note = $activity->objects[0];
+ // 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;
- 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));
- }
-
- $saved = $this->postNote($activity);
-
- 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);
+ 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));
+
+ header('HTTP/1.1 201 Created');
+ header("Location: " . common_local_url('ApiStatusesShow', array('id' => $stored->getID(),
+ 'format' => 'atom')));
+ $this->showSingleAtomStatus($stored);
}
- function postNote($activity)
+ /**
+ * Show the timeline of notices
+ *
+ * @return void
+ * @throws ClientException
+ * @throws ServerException
+ * @throws UserNoProfileException
+ */
+ public function showTimeline()
{
- $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));
- }
-
- // Get (safe!) HTML and text versions of the content
-
- $rendered = $this->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());
+ // We'll use the shared params from the Atom stub
+ // for other feed types.
+ $atom = new AtomUserNoticeFeed($this->target->getUser(), $this->scoped);
- // accept remote URI (not necessarily a good idea)
+ $link = common_local_url(
+ 'showstream',
+ array('nickname' => $this->target->getNickname())
+ );
- common_debug("Note ID is {$note->id}");
+ $self = $this->getSelfUri();
- if (!empty($note->id)) {
- $notice = Notice::getKV('uri', trim($note->id));
+ // FriendFeed's SUP protocol
+ // Also added RSS and Atom feeds
- 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;
- }
+ $suplink = common_local_url('sup', null, null, $this->target->getID());
+ header('X-SUP-ID: ' . $suplink);
- // 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);
+ // paging links
+ $nextUrl = !empty($this->next_id)
+ ? common_local_url(
+ 'ApiTimelineUser',
+ array('format' => $this->format,
+ 'id' => $this->target->getID()),
+ array('max_id' => $this->next_id)
+ )
+ : null;
+
+ $prevExtra = [];
+ if (!empty($this->notices)) {
+ assert($this->notices[0] instanceof Notice);
+ $prevExtra['since_id'] = $this->notices[0]->id;
}
- // 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));
+ $prevUrl = common_local_url(
+ 'ApiTimelineUser',
+ array('format' => $this->format,
+ 'id' => $this->target->getID()),
+ $prevExtra
+ );
+ $firstUrl = common_local_url(
+ 'ApiTimelineUser',
+ array('format' => $this->format,
+ 'id' => $this->target->getID())
+ );
+
+ switch ($this->format) {
+ case 'xml':
+ $this->showXmlTimeline($this->notices);
+ break;
+ case 'rss':
+ $this->showRssTimeline(
+ $this->notices,
+ $atom->title,
+ $link,
+ $atom->subtitle,
+ $suplink,
+ $atom->logo,
+ $self
+ );
+ break;
+ case 'atom':
+ header('Content-Type: application/atom+xml; charset=utf-8');
+
+ $atom->setId($self);
+ $atom->setSelfLink($self);
+
+ // Add navigation links: next, prev, first
+ // Note: we use IDs rather than pages for navigation; page boundaries
+ // change too quickly!
+
+ if (!empty($this->next_id)) {
+ $atom->addLink(
+ $nextUrl,
+ array('rel' => 'next',
+ 'type' => 'application/atom+xml')
+ );
}
- }
- // Maintain direct reply associations
- // @fixme what about conversation ID?
+ if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
+ $atom->addLink(
+ $prevUrl,
+ array('rel' => 'prev',
+ 'type' => 'application/atom+xml')
+ );
+ }
- if (!empty($activity->context->replyToID)) {
- $orig = Notice::getKV('uri',
- $activity->context->replyToID);
- if (!empty($orig)) {
- $options['reply_to'] = $orig->id;
+ if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
+ $atom->addLink(
+ $firstUrl,
+ array('rel' => 'first',
+ 'type' => 'application/atom+xml')
+ );
}
- }
- $location = $activity->context->location;
+ $atom->addEntryFromNotices($this->notices);
+ $this->raw($atom->getString());
- 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;
+ break;
+ case 'json':
+ $this->showJsonTimeline($this->notices);
+ break;
+ case 'as':
+ header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE);
+ $doc = new ActivityStreamJSONDocument($this->scoped);
+ $doc->setTitle($atom->title);
+ $doc->addLink($link, 'alternate', 'text/html');
+ $doc->addItemsFromNotices($this->notices);
+
+ if (!empty($this->next_id)) {
+ $doc->addLink(
+ $nextUrl,
+ array('rel' => 'next',
+ 'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
+ );
}
- }
- }
- // Atom categories <-> hashtags
+ if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
+ $doc->addLink(
+ $prevUrl,
+ array('rel' => 'prev',
+ 'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
+ );
+ }
- foreach ($activity->categories as $cat) {
- if ($cat->term) {
- $term = common_canonical_tag($cat->term);
- if ($term) {
- $options['tags'][] = $term;
+ if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
+ $doc->addLink(
+ $firstUrl,
+ array('rel' => 'first',
+ 'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
+ );
}
- }
- }
- // Atom enclosures -> attachment URLs
- foreach ($activity->enclosures as $href) {
- // @fixme save these locally or....?
- $options['urls'][] = $href;
+ $this->raw($doc->asString());
+ break;
+ default:
+ // TRANS: Client error displayed when coming across a non-supported API method.
+ $this->clientError(_('API method not found.'), 404);
}
-
- $saved = Notice::saveNew($this->target->id,
- $content,
- 'atompub', // TODO: deal with this
- $options);
-
- return $saved;
- }
-
- function purify($content)
- {
- require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
-
- $config = array('safe' => 1,
- 'deny_attribute' => 'id,style,on*');
- return htmLawed($content, $config);
}
}