Fix broken user activitystreams feed due to deleted notices

This commit is contained in:
Diogo Cordeiro 2019-05-06 04:01:07 +01:00
parent d2e6519bad
commit c03ed457a6
3 changed files with 270 additions and 228 deletions

View File

@ -34,7 +34,9 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('GNUSOCIAL')) { exit(1); } if (!defined('GNUSOCIAL')) {
exit(1);
}
/** /**
* Returns the most recent notices (default 20) posted by the authenticating * Returns the most recent notices (default 20) posted by the authenticating
@ -55,9 +57,64 @@ if (!defined('GNUSOCIAL')) { exit(1); }
*/ */
class ApiTimelineUserAction extends ApiBareAuthAction class ApiTimelineUserAction extends ApiBareAuthAction
{ {
var $notices = null; public $notices = null;
var $next_id = null; public $next_id = null;
/**
* We expose AtomPub here, so non-GET/HEAD reqs must be read/write.
*
* @param array $args other arguments
*
* @return boolean true
*/
public 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
*/
public function lastModified()
{
if (!empty($this->notices) && (count($this->notices) > 0)) {
return strtotime($this->notices[0]->created);
}
return null;
}
/**
* An entity tag for this stream
*
* Returns an Etag based on the action name, language, user ID, and
* timestamps of the first and last notice in the timeline
*
* @return string etag
*/
public function etag()
{
if (!empty($this->notices) && (count($this->notices) > 0)) {
$last = count($this->notices) - 1;
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;
}
/** /**
* Take arguments for running * Take arguments for running
@ -65,8 +122,10 @@ class ApiTimelineUserAction extends ApiBareAuthAction
* @param array $args $_REQUEST args * @param array $args $_REQUEST args
* *
* @return boolean success flag * @return boolean success flag
* @throws AuthorizationException
* @throws ClientException
*/ */
protected function prepare(array $args=array()) protected function prepare(array $args = [])
{ {
parent::prepare($args); parent::prepare($args);
@ -86,169 +145,22 @@ class ApiTimelineUserAction extends ApiBareAuthAction
return true; return true;
} }
/**
* Handle the request
*
* Just show the notices
*
* @return void
*/
protected function handle()
{
parent::handle();
if ($this->isPost()) {
$this->handlePost();
} else {
$this->showTimeline();
}
}
/**
* Show the timeline of notices
*
* @return void
*/
function showTimeline()
{
// We'll use the shared params from the Atom stub
// for other feed types.
$atom = new AtomUserNoticeFeed($this->target->getUser(), $this->scoped);
$link = common_local_url(
'showstream',
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->getID());
header('X-SUP-ID: ' . $suplink);
// 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 = array();
if (!empty($this->notices)) {
assert($this->notices[0] instanceof Notice);
$prevExtra['since_id'] = $this->notices[0]->id;
}
$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'));
}
if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
$atom->addLink($prevUrl,
array('rel' => 'prev',
'type' => 'application/atom+xml'));
}
if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
$atom->addLink($firstUrl,
array('rel' => 'first',
'type' => 'application/atom+xml'));
}
$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->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));
}
if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
$doc->addLink($prevUrl,
array('rel' => 'prev',
'type' => ActivityStreamJSONDocument::CONTENT_TYPE));
}
if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
$doc->addLink($firstUrl,
array('rel' => 'first',
'type' => ActivityStreamJSONDocument::CONTENT_TYPE));
}
$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);
}
}
/** /**
* Get notices * Get notices
* *
* @return array notices * @return array notices
*/ */
function getNotices() public function getNotices()
{ {
$notices = array(); $notices = [];
$notice = $this->target->getNotices(($this->page-1) * $this->count, $notice = $this->target->getNotices(
$this->count + 1, ($this->page - 1) * $this->count,
$this->since_id, $this->count + 1,
$this->max_id, $this->since_id,
$this->scoped); $this->max_id,
$this->scoped
);
while ($notice->fetch()) { while ($notice->fetch()) {
if (count($notices) < $this->count) { if (count($notices) < $this->count) {
@ -263,64 +175,29 @@ class ApiTimelineUserAction extends ApiBareAuthAction
} }
/** /**
* We expose AtomPub here, so non-GET/HEAD reqs must be read/write. * Handle the request
* *
* @param array $args other arguments * Just show the notices
* *
* @return boolean true * @return void
* @throws ClientException
* @throws ServerException
*/ */
protected function handle()
function isReadOnly($args)
{ {
return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD'); parent::handle();
}
/** if ($this->isPost()) {
* When was this feed last modified? $this->handlePost();
* } else {
* @return string datestamp of the latest notice in the stream $this->showTimeline();
*/
function lastModified()
{
if (!empty($this->notices) && (count($this->notices) > 0)) {
return strtotime($this->notices[0]->created);
} }
return null;
} }
/** public function handlePost()
* An entity tag for this stream
*
* Returns an Etag based on the action name, language, user ID, and
* timestamps of the first and last notice in the timeline
*
* @return string etag
*/
function etag()
{
if (!empty($this->notices) && (count($this->notices) > 0)) {
$last = count($this->notices) - 1;
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;
}
function handlePost()
{ {
if (!$this->scoped instanceof Profile || if (!$this->scoped instanceof Profile ||
!$this->target->sameAs($this->scoped)) { !$this->target->sameAs($this->scoped)) {
// TRANS: Client error displayed trying to add a notice to another user's timeline. // 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.'), 403); $this->clientError(_('Only the user can add to their own timeline.'), 403);
} }
@ -354,7 +231,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
$activity = new Activity($dom->documentElement); $activity = new Activity($dom->documentElement);
common_debug('AtomPub: Ignoring right now, but this POST was made to collection: '.$activity->id); common_debug('AtomPub: Ignoring right now, but this POST was made to collection: ' . $activity->id);
// Reset activity data so we can handle it in the same functions as with OStatus // 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 // because we don't let clients set their own UUIDs... Not sure what AtomPub thinks
@ -375,7 +252,158 @@ class ApiTimelineUserAction extends ApiBareAuthAction
header('HTTP/1.1 201 Created'); header('HTTP/1.1 201 Created');
header("Location: " . common_local_url('ApiStatusesShow', array('id' => $stored->getID(), header("Location: " . common_local_url('ApiStatusesShow', array('id' => $stored->getID(),
'format' => 'atom'))); 'format' => 'atom')));
$this->showSingleAtomStatus($stored); $this->showSingleAtomStatus($stored);
} }
/**
* Show the timeline of notices
*
* @return void
* @throws ClientException
* @throws ServerException
* @throws UserNoProfileException
*/
public function showTimeline()
{
// We'll use the shared params from the Atom stub
// for other feed types.
$atom = new AtomUserNoticeFeed($this->target->getUser(), $this->scoped);
$link = common_local_url(
'showstream',
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->getID());
header('X-SUP-ID: ' . $suplink);
// 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;
}
$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')
);
}
if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
$atom->addLink(
$prevUrl,
array('rel' => 'prev',
'type' => 'application/atom+xml')
);
}
if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
$atom->addLink(
$firstUrl,
array('rel' => 'first',
'type' => 'application/atom+xml')
);
}
$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->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)
);
}
if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
$doc->addLink(
$prevUrl,
array('rel' => 'prev',
'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
);
}
if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
$doc->addLink(
$firstUrl,
array('rel' => 'first',
'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
);
}
$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);
}
}
} }

View File

@ -1962,9 +1962,11 @@ class Notice extends Managed_DataObject
/** /**
* Convert a notice into an activity for export. * Convert a notice into an activity for export.
* *
* @param Profile $scoped The currently logged in/scoped profile * @param Profile $scoped The currently logged in/scoped profile
* *
* @return Activity activity object representing this Notice. * @return Activity activity object representing this Notice.
* @throws ClientException
* @throws ServerException
*/ */
function asActivity(Profile $scoped=null) function asActivity(Profile $scoped=null)

View File

@ -27,7 +27,9 @@
* @link http://status.net/ * @link http://status.net/
*/ */
if (!defined('GNUSOCIAL')) { exit(1); } if (!defined('GNUSOCIAL')) {
exit(1);
}
/** /**
* A class for generating JSON documents that represent an Activity Streams * A class for generating JSON documents that represent an Activity Streams
@ -47,7 +49,7 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
const CONTENT_TYPE = 'application/json; charset=utf-8'; const CONTENT_TYPE = 'application/json; charset=utf-8';
/* Top level array representing the document */ /* Top level array representing the document */
protected $doc = array(); protected $doc = [];
/* The current authenticated user */ /* The current authenticated user */
protected $cur; protected $cur;
@ -67,9 +69,10 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* Constructor * Constructor
* *
* @param User $cur the current authenticated user * @param User $cur the current authenticated user
* @throws UserNoProfileException
*/ */
function __construct($cur = null, $title = null, array $items=[], $links = null, $url = null) public function __construct($cur = null, $title = null, array $items = [], $links = null, $url = null)
{ {
parent::__construct($items, $url); parent::__construct($items, $url);
@ -84,26 +87,26 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
} }
/* Array of links associated with the document */ /* Array of links associated with the document */
$this->links = empty($links) ? array() : $items; $this->links = empty($links) ? [] : $items;
/* URL of a document, this document? containing a list of all the items in the stream */ /* URL of a document, this document? containing a list of all the items in the stream */
if (!empty($this->url)) { if (!empty($url)) {
$this->url = $this->url; $this->url = $url;
} }
} }
/** /**
* Set the title of the document * Set the title of the document
* *
* @param String $title the title * @param string $title the title
*/ */
function setTitle($title) public function setTitle($title)
{ {
$this->title = $title; $this->title = $title;
} }
function setUrl($url) public function setUrl($url)
{ {
$this->url = $url; $this->url = $url;
} }
@ -113,10 +116,11 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* Add more than one Item to the document * Add more than one Item to the document
* *
* @param mixed $notices an array of Notice objects or handle * @param mixed $notices an array of Notice objects or handle
* * @throws ClientException
* @throws ServerException
*/ */
function addItemsFromNotices($notices) public function addItemsFromNotices($notices)
{ {
if (is_array($notices)) { if (is_array($notices)) {
foreach ($notices as $notice) { foreach ($notices as $notice) {
@ -135,9 +139,17 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* @param Notice $notice a Notice to add * @param Notice $notice a Notice to add
*/ */
function addItemFromNotice($notice) public function addItemFromNotice($notice)
{ {
$act = $notice->asActivity($this->scoped); try {
$act = $notice->asActivity($this->scoped);
} catch (Exception $e) {
// We know exceptions like
// "No result found on Fave lookup."
// may happen because of deleted notices etc.
// These are irrelevant for the feed purposes.
return;
}
$act->extra[] = $notice->noticeInfo($this->scoped); $act->extra[] = $notice->noticeInfo($this->scoped);
array_push($this->items, $act->asArray()); array_push($this->items, $act->asArray());
$this->count++; $this->count++;
@ -148,8 +160,9 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* *
* @param string $url the URL for the link * @param string $url the URL for the link
* @param string $rel the link relationship * @param string $rel the link relationship
* @throws Exception
*/ */
function addLink($url = null, $rel = null, $mediaType = null) public function addLink($url = null, $rel = null, $mediaType = null)
{ {
$link = new ActivityStreamsLink($url, $rel, $mediaType); $link = new ActivityStreamsLink($url, $rel, $mediaType);
array_push($this->links, $link->asArray()); array_push($this->links, $link->asArray());
@ -160,15 +173,14 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* *
* @return string encoded JSON output * @return string encoded JSON output
*/ */
function asString() public function asString()
{ {
$this->doc['generator'] = 'GNU social ' . GNUSOCIAL_VERSION; // extension $this->doc['generator'] = 'GNU social ' . GNUSOCIAL_VERSION; // extension
$this->doc['title'] = $this->title; $this->doc['title'] = $this->title;
$this->doc['url'] = $this->url; $this->doc['url'] = $this->url;
$this->doc['totalItems'] = $this->count; $this->doc['totalItems'] = $this->count;
$this->doc['items'] = $this->items; $this->doc['items'] = $this->items;
$this->doc['links'] = $this->links; // extension $this->doc['links'] = $this->links; // extension
return json_encode(array_filter($this->doc)); // filter out empty elements return json_encode(array_filter($this->doc)); // filter out empty elements
} }
} }