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