forked from GNUsocial/gnu-social
Fix broken user activitystreams feed due to deleted notices
This commit is contained in:
parent
d2e6519bad
commit
c03ed457a6
@ -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,
|
||||
$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) {
|
||||
@ -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
|
||||
*/
|
||||
|
||||
function isReadOnly($args)
|
||||
protected function handle()
|
||||
{
|
||||
return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD');
|
||||
}
|
||||
parent::handle();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
if ($this->isPost()) {
|
||||
$this->handlePost();
|
||||
} else {
|
||||
$this->showTimeline();
|
||||
}
|
||||
|
||||
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)) {
|
||||
!$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.'), 403);
|
||||
}
|
||||
@ -354,7 +231,7 @@ class ApiTimelineUserAction extends ApiBareAuthAction
|
||||
|
||||
$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
|
||||
// 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("Location: " . common_local_url('ApiStatusesShow', array('id' => $stored->getID(),
|
||||
'format' => 'atom')));
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1962,9 +1962,11 @@ class Notice extends Managed_DataObject
|
||||
/**
|
||||
* 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.
|
||||
* @throws ClientException
|
||||
* @throws ServerException
|
||||
*/
|
||||
|
||||
function asActivity(Profile $scoped=null)
|
||||
|
@ -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)
|
||||
{
|
||||
$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);
|
||||
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,15 +173,14 @@ 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;
|
||||
$this->doc['url'] = $this->url;
|
||||
$this->doc['url'] = $this->url;
|
||||
$this->doc['totalItems'] = $this->count;
|
||||
$this->doc['items'] = $this->items;
|
||||
$this->doc['links'] = $this->links; // extension
|
||||
return json_encode(array_filter($this->doc)); // filter out empty elements
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user