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/
*/
if (!defined('GNUSOCIAL')) { exit(1); }
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
* Returns the most recent notices (default 20) posted by the authenticating
@ -55,9 +57,64 @@ if (!defined('GNUSOCIAL')) { exit(1); }
*/
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
@ -65,8 +122,10 @@ class ApiTimelineUserAction extends ApiBareAuthAction
* @param array $args $_REQUEST args
*
* @return boolean success flag
* @throws AuthorizationException
* @throws ClientException
*/
protected function prepare(array $args=array())
protected function prepare(array $args = [])
{
parent::prepare($args);
@ -86,169 +145,22 @@ class ApiTimelineUserAction extends ApiBareAuthAction
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
*
* @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->page - 1) * $this->count,
$this->count + 1,
$this->since_id,
$this->max_id,
$this->scoped);
$this->scoped
);
while ($notice->fetch()) {
if (count($notices) < $this->count) {
@ -263,61 +175,26 @@ 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
*/
function isReadOnly($args)
protected function handle()
{
return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD');
parent::handle();
if ($this->isPost()) {
$this->handlePost();
} else {
$this->showTimeline();
}
}
/**
* 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
*
* 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()
public function handlePost()
{
if (!$this->scoped instanceof Profile ||
!$this->target->sameAs($this->scoped)) {
@ -378,4 +255,155 @@ class ApiTimelineUserAction extends ApiBareAuthAction
'format' => 'atom')));
$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

@ -1965,6 +1965,8 @@ class Notice extends Managed_DataObject
* @param Profile $scoped The currently logged in/scoped profile
*
* @return Activity activity object representing this Notice.
* @throws ClientException
* @throws ServerException
*/
function asActivity(Profile $scoped=null)

View File

@ -27,7 +27,9 @@
* @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
@ -47,7 +49,7 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
const CONTENT_TYPE = 'application/json; charset=utf-8';
/* Top level array representing the document */
protected $doc = array();
protected $doc = [];
/* The current authenticated user */
protected $cur;
@ -67,9 +69,10 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* Constructor
*
* @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);
@ -84,26 +87,26 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
}
/* 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 */
if (!empty($this->url)) {
$this->url = $this->url;
if (!empty($url)) {
$this->url = $url;
}
}
/**
* 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;
}
function setUrl($url)
public function setUrl($url)
{
$this->url = $url;
}
@ -113,10 +116,11 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* Add more than one Item to the document
*
* @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)) {
foreach ($notices as $notice) {
@ -135,9 +139,17 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
* @param Notice $notice a Notice to add
*/
function addItemFromNotice($notice)
public function addItemFromNotice($notice)
{
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);
array_push($this->items, $act->asArray());
$this->count++;
@ -148,8 +160,9 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
*
* @param string $url the URL for the link
* @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);
array_push($this->links, $link->asArray());
@ -160,7 +173,7 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
*
* @return string encoded JSON output
*/
function asString()
public function asString()
{
$this->doc['generator'] = 'GNU social ' . GNUSOCIAL_VERSION; // extension
$this->doc['title'] = $this->title;
@ -170,5 +183,4 @@ class ActivityStreamJSONDocument extends JSONActivityCollection
$this->doc['links'] = $this->links; // extension
return json_encode(array_filter($this->doc)); // filter out empty elements
}
}