]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/apitimelineuser.php
Fix broken user activitystreams feed due to deleted notices
[quix0rs-gnu-social.git] / actions / apitimelineuser.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Show a user's timeline
6  *
7  * PHP version 5
8  *
9  * LICENCE: This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU Affero General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU Affero General Public License for more details.
18  *
19  * You should have received a copy of the GNU Affero General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  * @category  API
23  * @package   StatusNet
24  * @author    Craig Andrews <candrews@integralblue.com>
25  * @author    Evan Prodromou <evan@status.net>
26  * @author    Jeffery To <jeffery.to@gmail.com>
27  * @author    mac65 <mac65@mac65.com>
28  * @author    Mike Cochrane <mikec@mikenz.geek.nz>
29  * @author    Robin Millette <robin@millette.info>
30  * @author    Zach Copley <zach@status.net>
31  * @copyright 2009 StatusNet, Inc.
32  * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
33  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
34  * @link      http://status.net/
35  */
36
37 if (!defined('GNUSOCIAL')) {
38     exit(1);
39 }
40
41 /**
42  * Returns the most recent notices (default 20) posted by the authenticating
43  * user. Another user's timeline can be requested via the id parameter. This
44  * is the API equivalent of the user profile web page.
45  *
46  * @category API
47  * @package  StatusNet
48  * @author   Craig Andrews <candrews@integralblue.com>
49  * @author   Evan Prodromou <evan@status.net>
50  * @author   Jeffery To <jeffery.to@gmail.com>
51  * @author   mac65 <mac65@mac65.com>
52  * @author   Mike Cochrane <mikec@mikenz.geek.nz>
53  * @author   Robin Millette <robin@millette.info>
54  * @author   Zach Copley <zach@status.net>
55  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
56  * @link     http://status.net/
57  */
58 class ApiTimelineUserAction extends ApiBareAuthAction
59 {
60     public $notices = null;
61
62     public $next_id = null;
63
64     /**
65      * We expose AtomPub here, so non-GET/HEAD reqs must be read/write.
66      *
67      * @param array $args other arguments
68      *
69      * @return boolean true
70      */
71
72     public function isReadOnly($args)
73     {
74         return ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD');
75     }
76
77     /**
78      * When was this feed last modified?
79      *
80      * @return string datestamp of the latest notice in the stream
81      */
82     public function lastModified()
83     {
84         if (!empty($this->notices) && (count($this->notices) > 0)) {
85             return strtotime($this->notices[0]->created);
86         }
87
88         return null;
89     }
90
91     /**
92      * An entity tag for this stream
93      *
94      * Returns an Etag based on the action name, language, user ID, and
95      * timestamps of the first and last notice in the timeline
96      *
97      * @return string etag
98      */
99     public function etag()
100     {
101         if (!empty($this->notices) && (count($this->notices) > 0)) {
102             $last = count($this->notices) - 1;
103
104             return '"' . implode(
105                     ':',
106                     array($this->arg('action'),
107                         common_user_cache_hash($this->scoped),
108                         common_language(),
109                         $this->target->getID(),
110                         strtotime($this->notices[0]->created),
111                         strtotime($this->notices[$last]->created))
112                 )
113                 . '"';
114         }
115
116         return null;
117     }
118
119     /**
120      * Take arguments for running
121      *
122      * @param array $args $_REQUEST args
123      *
124      * @return boolean success flag
125      * @throws AuthorizationException
126      * @throws ClientException
127      */
128     protected function prepare(array $args = [])
129     {
130         parent::prepare($args);
131
132         $this->target = $this->getTargetProfile($this->arg('id'));
133
134         if (!($this->target instanceof Profile)) {
135             // TRANS: Client error displayed requesting most recent notices for a non-existing user.
136             $this->clientError(_('No such user.'), 404);
137         }
138
139         if (!$this->target->isLocal()) {
140             $this->serverError(_('Remote user timelines are not available here yet.'), 501);
141         }
142
143         $this->notices = $this->getNotices();
144
145         return true;
146     }
147
148     /**
149      * Get notices
150      *
151      * @return array notices
152      */
153     public function getNotices()
154     {
155         $notices = [];
156
157         $notice = $this->target->getNotices(
158             ($this->page - 1) * $this->count,
159             $this->count + 1,
160             $this->since_id,
161             $this->max_id,
162             $this->scoped
163         );
164
165         while ($notice->fetch()) {
166             if (count($notices) < $this->count) {
167                 $notices[] = clone($notice);
168             } else {
169                 $this->next_id = $notice->id;
170                 break;
171             }
172         }
173
174         return $notices;
175     }
176
177     /**
178      * Handle the request
179      *
180      * Just show the notices
181      *
182      * @return void
183      * @throws ClientException
184      * @throws ServerException
185      */
186     protected function handle()
187     {
188         parent::handle();
189
190         if ($this->isPost()) {
191             $this->handlePost();
192         } else {
193             $this->showTimeline();
194         }
195     }
196
197     public function handlePost()
198     {
199         if (!$this->scoped instanceof Profile ||
200             !$this->target->sameAs($this->scoped)) {
201             // TRANS: Client error displayed trying to add a notice to another user's timeline.
202             $this->clientError(_('Only the user can add to their own timeline.'), 403);
203         }
204
205         // Only handle posts for Atom
206         if ($this->format != 'atom') {
207             // TRANS: Client error displayed when using another format than AtomPub.
208             $this->clientError(_('Only accept AtomPub for Atom feeds.'));
209         }
210
211         $xml = trim(file_get_contents('php://input'));
212         if (empty($xml)) {
213             // TRANS: Client error displayed attempting to post an empty API notice.
214             $this->clientError(_('Atom post must not be empty.'));
215         }
216
217         $old = error_reporting(error_reporting() & ~(E_WARNING | E_NOTICE));
218         $dom = new DOMDocument();
219         $ok = $dom->loadXML($xml);
220         error_reporting($old);
221         if (!$ok) {
222             // TRANS: Client error displayed attempting to post an API that is not well-formed XML.
223             $this->clientError(_('Atom post must be well-formed XML.'));
224         }
225
226         if ($dom->documentElement->namespaceURI != Activity::ATOM ||
227             $dom->documentElement->localName != 'entry') {
228             // TRANS: Client error displayed when not using an Atom entry.
229             $this->clientError(_('Atom post must be an Atom entry.'));
230         }
231
232         $activity = new Activity($dom->documentElement);
233
234         common_debug('AtomPub: Ignoring right now, but this POST was made to collection: ' . $activity->id);
235
236         // Reset activity data so we can handle it in the same functions as with OStatus
237         // because we don't let clients set their own UUIDs... Not sure what AtomPub thinks
238         // about that though.
239         $activity->id = null;
240         $activity->actor = null;    // not used anyway, we use $this->target
241         $activity->objects[0]->id = null;
242
243         $stored = null;
244         if (Event::handle('StartAtomPubNewActivity', array($activity, $this->target, &$stored))) {
245             // TRANS: Client error displayed when not using the POST verb. Do not translate POST.
246             throw new ClientException(_('Could not handle this Atom Activity.'));
247         }
248         if (!$stored instanceof Notice) {
249             throw new ServerException('Server did not create a Notice object from handled AtomPub activity.');
250         }
251         Event::handle('EndAtomPubNewActivity', array($activity, $this->target, $stored));
252
253         header('HTTP/1.1 201 Created');
254         header("Location: " . common_local_url('ApiStatusesShow', array('id' => $stored->getID(),
255                 'format' => 'atom')));
256         $this->showSingleAtomStatus($stored);
257     }
258
259     /**
260      * Show the timeline of notices
261      *
262      * @return void
263      * @throws ClientException
264      * @throws ServerException
265      * @throws UserNoProfileException
266      */
267     public function showTimeline()
268     {
269         // We'll use the shared params from the Atom stub
270         // for other feed types.
271         $atom = new AtomUserNoticeFeed($this->target->getUser(), $this->scoped);
272
273         $link = common_local_url(
274             'showstream',
275             array('nickname' => $this->target->getNickname())
276         );
277
278         $self = $this->getSelfUri();
279
280         // FriendFeed's SUP protocol
281         // Also added RSS and Atom feeds
282
283         $suplink = common_local_url('sup', null, null, $this->target->getID());
284         header('X-SUP-ID: ' . $suplink);
285
286
287         // paging links
288         $nextUrl = !empty($this->next_id)
289             ? common_local_url(
290                 'ApiTimelineUser',
291                 array('format' => $this->format,
292                     'id' => $this->target->getID()),
293                 array('max_id' => $this->next_id)
294             )
295             : null;
296
297         $prevExtra = [];
298         if (!empty($this->notices)) {
299             assert($this->notices[0] instanceof Notice);
300             $prevExtra['since_id'] = $this->notices[0]->id;
301         }
302
303         $prevUrl = common_local_url(
304             'ApiTimelineUser',
305             array('format' => $this->format,
306                 'id' => $this->target->getID()),
307             $prevExtra
308         );
309         $firstUrl = common_local_url(
310             'ApiTimelineUser',
311             array('format' => $this->format,
312                 'id' => $this->target->getID())
313         );
314
315         switch ($this->format) {
316             case 'xml':
317                 $this->showXmlTimeline($this->notices);
318                 break;
319             case 'rss':
320                 $this->showRssTimeline(
321                     $this->notices,
322                     $atom->title,
323                     $link,
324                     $atom->subtitle,
325                     $suplink,
326                     $atom->logo,
327                     $self
328                 );
329                 break;
330             case 'atom':
331                 header('Content-Type: application/atom+xml; charset=utf-8');
332
333                 $atom->setId($self);
334                 $atom->setSelfLink($self);
335
336                 // Add navigation links: next, prev, first
337                 // Note: we use IDs rather than pages for navigation; page boundaries
338                 // change too quickly!
339
340                 if (!empty($this->next_id)) {
341                     $atom->addLink(
342                         $nextUrl,
343                         array('rel' => 'next',
344                             'type' => 'application/atom+xml')
345                     );
346                 }
347
348                 if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
349                     $atom->addLink(
350                         $prevUrl,
351                         array('rel' => 'prev',
352                             'type' => 'application/atom+xml')
353                     );
354                 }
355
356                 if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
357                     $atom->addLink(
358                         $firstUrl,
359                         array('rel' => 'first',
360                             'type' => 'application/atom+xml')
361                     );
362                 }
363
364                 $atom->addEntryFromNotices($this->notices);
365                 $this->raw($atom->getString());
366
367                 break;
368             case 'json':
369                 $this->showJsonTimeline($this->notices);
370                 break;
371             case 'as':
372                 header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE);
373                 $doc = new ActivityStreamJSONDocument($this->scoped);
374                 $doc->setTitle($atom->title);
375                 $doc->addLink($link, 'alternate', 'text/html');
376                 $doc->addItemsFromNotices($this->notices);
377
378                 if (!empty($this->next_id)) {
379                     $doc->addLink(
380                         $nextUrl,
381                         array('rel' => 'next',
382                             'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
383                     );
384                 }
385
386                 if (($this->page > 1 || !empty($this->max_id)) && !empty($this->notices)) {
387                     $doc->addLink(
388                         $prevUrl,
389                         array('rel' => 'prev',
390                             'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
391                     );
392                 }
393
394                 if ($this->page > 1 || !empty($this->since_id) || !empty($this->max_id)) {
395                     $doc->addLink(
396                         $firstUrl,
397                         array('rel' => 'first',
398                             'type' => ActivityStreamJSONDocument::CONTENT_TYPE)
399                     );
400                 }
401
402                 $this->raw($doc->asString());
403                 break;
404             default:
405                 // TRANS: Client error displayed when coming across a non-supported API method.
406                 $this->clientError(_('API method not found.'), 404);
407         }
408     }
409 }