]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/apiaction.php
f5928c7652fdbe27db27a64609d4f5b4028de398
[quix0rs-gnu-social.git] / lib / apiaction.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Base API action
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    Dan Moore <dan@moore.cx>
26  * @author    Evan Prodromou <evan@status.net>
27  * @author    Jeffery To <jeffery.to@gmail.com>
28  * @author    Toby Inkster <mail@tobyinkster.co.uk>
29  * @author    Zach Copley <zach@status.net>
30  * @copyright 2009-2010 StatusNet, Inc.
31  * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
32  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
33  * @link      http://status.net/
34  */
35
36 /* External API usage documentation. Please update when you change how the API works. */
37
38 /*! @mainpage StatusNet REST API
39
40     @section Introduction
41
42     Some explanatory text about the API would be nice.
43
44     @section API Methods
45
46     @subsection timelinesmethods_sec Timeline Methods
47
48     @li @ref publictimeline
49     @li @ref friendstimeline
50
51     @subsection statusmethods_sec Status Methods
52
53     @li @ref statusesupdate
54
55     @subsection usermethods_sec User Methods
56
57     @subsection directmessagemethods_sec Direct Message Methods (now a plugin)
58
59     @subsection friendshipmethods_sec Friendship Methods
60
61     @subsection socialgraphmethods_sec Social Graph Methods
62
63     @subsection accountmethods_sec Account Methods
64
65     @subsection favoritesmethods_sec Favorites Methods
66
67     @subsection blockmethods_sec Block Methods
68
69     @subsection oauthmethods_sec OAuth Methods
70
71     @subsection helpmethods_sec Help Methods
72
73     @subsection groupmethods_sec Group Methods
74
75     @page apiroot API Root
76
77     The URLs for methods referred to in this API documentation are
78     relative to the StatusNet API root. The API root is determined by the
79     site's @b server and @b path variables, which are generally specified
80     in config.php. For example:
81
82     @code
83     $config['site']['server'] = 'example.org';
84     $config['site']['path'] = 'statusnet'
85     @endcode
86
87     The pattern for a site's API root is: @c protocol://server/path/api E.g:
88
89     @c http://example.org/statusnet/api
90
91     The @b path can be empty.  In that case the API root would simply be:
92
93     @c http://example.org/api
94
95 */
96
97 if (!defined('STATUSNET')) {
98     exit(1);
99 }
100
101 class ApiValidationException extends Exception
102 {
103 }
104
105 /**
106  * Contains most of the Twitter-compatible API output functions.
107  *
108  * @category API
109  * @package  StatusNet
110  * @author   Craig Andrews <candrews@integralblue.com>
111  * @author   Dan Moore <dan@moore.cx>
112  * @author   Evan Prodromou <evan@status.net>
113  * @author   Jeffery To <jeffery.to@gmail.com>
114  * @author   Toby Inkster <mail@tobyinkster.co.uk>
115  * @author   Zach Copley <zach@status.net>
116  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
117  * @link     http://status.net/
118  */
119 class ApiAction extends Action
120 {
121     const READ_ONLY = 1;
122     const READ_WRITE = 2;
123     public static $reserved_sources = ['web', 'omb', 'ostatus', 'mail', 'xmpp', 'api'];
124     public $user = null;
125     public $auth_user = null;
126     public $page = null;
127     public $count = null;
128     public $offset = null;
129     public $limit = null;
130     public $max_id = null;
131     public $since_id = null;
132     public $source = null;
133     public $callback = null;
134     public $format = null;  // read (default) or read-write
135     public $access = self::READ_ONLY;
136
137     public function twitterRelationshipArray($source, $target)
138     {
139         $relationship = [];
140
141         $relationship['source'] =
142             $this->relationshipDetailsArray($source->getProfile(), $target->getProfile());
143         $relationship['target'] =
144             $this->relationshipDetailsArray($target->getProfile(), $source->getProfile());
145
146         return ['relationship' => $relationship];
147     }
148
149     public function relationshipDetailsArray(Profile $source, Profile $target)
150     {
151         $details = [];
152
153         $details['screen_name'] = $source->getNickname();
154         $details['followed_by'] = $target->isSubscribed($source);
155
156         try {
157             $sub = Subscription::getSubscription($source, $target);
158             $details['following'] = true;
159             $details['notifications_enabled'] = ($sub->jabber || $sub->sms);
160         } catch (NoResultException $e) {
161             $details['following'] = false;
162             $details['notifications_enabled'] = false;
163         }
164
165         $details['blocking'] = $source->hasBlocked($target);
166         $details['id'] = intval($source->id);
167
168         return $details;
169     }
170
171     public function showTwitterXmlRelationship($relationship)
172     {
173         $this->elementStart('relationship');
174
175         foreach ($relationship as $element => $value) {
176             if ($element == 'source' || $element == 'target') {
177                 $this->elementStart($element);
178                 $this->showXmlRelationshipDetails($value);
179                 $this->elementEnd($element);
180             }
181         }
182
183         $this->elementEnd('relationship');
184     }
185
186     public function showXmlRelationshipDetails($details)
187     {
188         foreach ($details as $element => $value) {
189             $this->element($element, null, $value);
190         }
191     }
192
193     /**
194      * Overrides XMLOutputter::element to write booleans as strings (true|false).
195      * See that method's documentation for more info.
196      *
197      * @param string $tag Element type or tagname
198      * @param array|string|null $attrs Array of element attributes, as key-value pairs
199      * @param string|null $content string content of the element
200      *
201      * @return void
202      */
203     public function element(string $tag, $attrs = null, $content = null)
204     {
205         if (is_bool($content)) {
206             $content = ($content ? "true" : "false");
207         }
208
209         parent::element($tag, $attrs, $content);
210     }
211
212     public function showSingleXmlStatus($notice)
213     {
214         $this->initDocument('xml');
215         $twitter_status = $this->twitterStatusArray($notice);
216         $this->showTwitterXmlStatus($twitter_status, 'status', true);
217         $this->endDocument('xml');
218     }
219
220     public function initDocument($type = 'xml')
221     {
222         switch ($type) {
223             case 'xml':
224                 header('Content-Type: application/xml; charset=utf-8');
225                 $this->startXML();
226                 break;
227             case 'json':
228                 header('Content-Type: application/json; charset=utf-8');
229
230                 // Check for JSONP callback
231                 if (isset($this->callback)) {
232                     print $this->callback . '(';
233                 }
234                 break;
235             case 'rss':
236                 header("Content-Type: application/rss+xml; charset=utf-8");
237                 $this->initTwitterRss();
238                 break;
239             case 'atom':
240                 header('Content-Type: application/atom+xml; charset=utf-8');
241                 $this->initTwitterAtom();
242                 break;
243             default:
244                 // TRANS: Client error on an API request with an unsupported data format.
245                 $this->clientError(_('Not a supported data format.'));
246         }
247
248         return;
249     }
250
251     public function initTwitterRss()
252     {
253         $this->startXML();
254         $this->elementStart(
255             'rss',
256             [
257                 'version' => '2.0',
258                 'xmlns:atom' => 'http://www.w3.org/2005/Atom',
259                 'xmlns:georss' => 'http://www.georss.org/georss'
260             ]
261         );
262         $this->elementStart('channel');
263         Event::handle('StartApiRss', [$this]);
264     }
265
266     public function initTwitterAtom()
267     {
268         $this->startXML();
269         // FIXME: don't hardcode the language here!
270         $this->elementStart('feed', ['xmlns' => 'http://www.w3.org/2005/Atom',
271             'xml:lang' => 'en-US',
272             'xmlns:thr' => 'http://purl.org/syndication/thread/1.0']);
273     }
274
275     public function twitterStatusArray($notice, $include_user = true)
276     {
277         $base = $this->twitterSimpleStatusArray($notice, $include_user);
278
279         // FIXME: MOVE TO SHARE PLUGIN
280         if (!empty($notice->repeat_of)) {
281             $original = Notice::getKV('id', $notice->repeat_of);
282             if ($original instanceof Notice) {
283                 $orig_array = $this->twitterSimpleStatusArray($original, $include_user);
284                 $base['retweeted_status'] = $orig_array;
285             }
286         }
287
288         return $base;
289     }
290
291     public function twitterSimpleStatusArray($notice, $include_user = true)
292     {
293         $profile = $notice->getProfile();
294
295         $twitter_status = [];
296         $twitter_status['text'] = $notice->content;
297         $twitter_status['truncated'] = false; # Not possible on StatusNet
298         $twitter_status['created_at'] = self::dateTwitter($notice->created);
299         try {
300             // We could just do $notice->reply_to but maybe the future holds a
301             // different story for parenting.
302             $parent = $notice->getParent();
303             $in_reply_to = $parent->id;
304         } catch (NoParentNoticeException $e) {
305             $in_reply_to = null;
306         } catch (NoResultException $e) {
307             // the in_reply_to message has probably been deleted
308             $in_reply_to = null;
309         }
310         $twitter_status['in_reply_to_status_id'] = $in_reply_to;
311
312         $source = null;
313         $source_link = null;
314
315         $ns = $notice->getSource();
316         if ($ns instanceof Notice_source) {
317             $source = $ns->code;
318             if (!empty($ns->url)) {
319                 $source_link = $ns->url;
320                 if (!empty($ns->name)) {
321                     $source = $ns->name;
322                 }
323             }
324         }
325
326         $twitter_status['uri'] = $notice->getUri();
327         $twitter_status['source'] = $source;
328         $twitter_status['source_link'] = $source_link;
329         $twitter_status['id'] = intval($notice->id);
330
331         $replier_profile = null;
332
333         if ($notice->reply_to) {
334             $reply = Notice::getKV(intval($notice->reply_to));
335             if ($reply) {
336                 $replier_profile = $reply->getProfile();
337             }
338         }
339
340         $twitter_status['in_reply_to_user_id'] =
341             ($replier_profile) ? intval($replier_profile->id) : null;
342         $twitter_status['in_reply_to_screen_name'] =
343             ($replier_profile) ? $replier_profile->nickname : null;
344
345         try {
346             $notloc = Notice_location::locFromStored($notice);
347             // This is the format that GeoJSON expects stuff to be in
348             $twitter_status['geo'] = ['type' => 'Point',
349                 'coordinates' => [(float)$notloc->lat,
350                     (float)$notloc->lon]];
351         } catch (ServerException $e) {
352             $twitter_status['geo'] = null;
353         }
354
355         // Enclosures
356         $attachments = $notice->attachments();
357
358         if (!empty($attachments)) {
359             $twitter_status['attachments'] = [];
360
361             foreach ($attachments as $attachment) {
362                 try {
363                     $enclosure_o = $attachment->getEnclosure();
364                     $enclosure = [];
365                     $enclosure['url'] = $enclosure_o->url;
366                     $enclosure['mimetype'] = $enclosure_o->mimetype;
367                     $enclosure['size'] = $enclosure_o->size;
368                     $twitter_status['attachments'][] = $enclosure;
369                 } catch (ServerException $e) {
370                     // There was not enough metadata available
371                 }
372             }
373         }
374
375         if ($include_user && $profile) {
376             // Don't get notice (recursive!)
377             $twitter_user = $this->twitterUserArray($profile, false);
378             $twitter_status['user'] = $twitter_user;
379         }
380
381         // StatusNet-specific
382
383         $twitter_status['statusnet_html'] = $notice->getRendered();
384         $twitter_status['statusnet_conversation_id'] = intval($notice->conversation);
385
386         // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
387         Event::handle('NoticeSimpleStatusArray', [$notice, &$twitter_status, $this->scoped,
388             ['include_user' => $include_user]]);
389
390         return $twitter_status;
391     }
392
393     public static function dateTwitter($dt)
394     {
395         $dateStr = date('d F Y H:i:s', strtotime($dt));
396         $d = new DateTime($dateStr, new DateTimeZone('UTC'));
397         $d->setTimezone(new DateTimeZone(common_timezone()));
398         return $d->format('D M d H:i:s O Y');
399     }
400
401     public function twitterUserArray($profile, $get_notice = false)
402     {
403         $twitter_user = [];
404
405         try {
406             $user = $profile->getUser();
407         } catch (NoSuchUserException $e) {
408             $user = null;
409         }
410
411         $twitter_user['id'] = $profile->getID();
412         $twitter_user['name'] = $profile->getBestName();
413         $twitter_user['screen_name'] = $profile->getNickname();
414         $twitter_user['location'] = $profile->location;
415         $twitter_user['description'] = $profile->getDescription();
416
417         // TODO: avatar url template (example.com/user/avatar?size={x}x{y})
418         $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE);
419         $twitter_user['profile_image_url_https'] = $twitter_user['profile_image_url'];
420
421         // START introduced by qvitter API, not necessary for StatusNet API
422         $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE);
423         try {
424             $avatar = Avatar::getUploaded($profile);
425             $origurl = $avatar->displayUrl();
426         } catch (Exception $e) {
427             $origurl = $twitter_user['profile_image_url_profile_size'];
428         }
429         $twitter_user['profile_image_url_original'] = $origurl;
430
431         $twitter_user['groups_count'] = $profile->getGroupCount();
432         foreach (['linkcolor', 'backgroundcolor'] as $key) {
433             $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key);
434         }
435         // END introduced by qvitter API, not necessary for StatusNet API
436
437         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
438         $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false;
439         $twitter_user['followers_count'] = $profile->subscriberCount();
440
441         // Note: some profiles don't have an associated user
442
443         $twitter_user['friends_count'] = $profile->subscriptionCount();
444
445         $twitter_user['created_at'] = self::dateTwitter($profile->created);
446
447         $timezone = 'UTC';
448
449         if (!empty($user) && $user->timezone) {
450             $timezone = $user->timezone;
451         }
452
453         $t = new DateTime;
454         $t->setTimezone(new DateTimeZone($timezone));
455
456         $twitter_user['utc_offset'] = $t->format('Z');
457         $twitter_user['time_zone'] = $timezone;
458         $twitter_user['statuses_count'] = $profile->noticeCount();
459
460         // Is the requesting user following this user?
461         // These values might actually also mean "unknown". Ambiguity issues?
462         $twitter_user['following'] = false;
463         $twitter_user['statusnet_blocking'] = false;
464         $twitter_user['notifications'] = false;
465
466         if ($this->scoped instanceof Profile) {
467             try {
468                 $sub = Subscription::getSubscription($this->scoped, $profile);
469                 // Notifications on?
470                 $twitter_user['following'] = true;
471                 $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
472             } catch (NoResultException $e) {
473                 // well, the values are already false...
474             }
475             $twitter_user['statusnet_blocking'] = $this->scoped->hasBlocked($profile);
476         }
477
478         if ($get_notice) {
479             $notice = $profile->getCurrentNotice();
480             if ($notice instanceof Notice) {
481                 // don't get user!
482                 $twitter_user['status'] = $this->twitterStatusArray($notice, false);
483             }
484         }
485
486         // StatusNet-specific
487
488         $twitter_user['statusnet_profile_url'] = $profile->profileurl;
489
490         // The event call to handle NoticeSimpleStatusArray lets plugins add data to the output array
491         Event::handle('TwitterUserArray', [$profile, &$twitter_user, $this->scoped, []]);
492
493         return $twitter_user;
494     }
495
496     public function showTwitterXmlStatus($twitter_status, $tag = 'status', $namespaces = false)
497     {
498         $attrs = [];
499         if ($namespaces) {
500             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
501         }
502         $this->elementStart($tag, $attrs);
503         foreach ($twitter_status as $element => $value) {
504             switch ($element) {
505                 case 'user':
506                     $this->showTwitterXmlUser($twitter_status['user']);
507                     break;
508                 case 'text':
509                     $this->element($element, null, common_xml_safe_str($value));
510                     break;
511                 case 'attachments':
512                     $this->showXmlAttachments($twitter_status['attachments']);
513                     break;
514                 case 'geo':
515                     $this->showGeoXML($value);
516                     break;
517                 case 'retweeted_status':
518                     // FIXME: MOVE TO SHARE PLUGIN
519                     $this->showTwitterXmlStatus($value, 'retweeted_status');
520                     break;
521                 default:
522                     if (strncmp($element, 'statusnet_', 10) == 0) {
523                         if ($element === 'statusnet_in_groups' && is_array($value)) {
524                             // QVITTERFIX because it would cause an array to be sent as $value
525                             // THIS IS UNDOCUMENTED AND SHOULD NEVER BE RELIED UPON (qvitter uses json output)
526                             $value = json_encode($value);
527                         }
528                         $this->element('statusnet:' . substr($element, 10), null, $value);
529                     } else {
530                         $this->element($element, null, $value);
531                     }
532             }
533         }
534         $this->elementEnd($tag);
535     }
536
537     public function showTwitterXmlUser($twitter_user, $role = 'user', $namespaces = false)
538     {
539         $attrs = [];
540         if ($namespaces) {
541             $attrs['xmlns:statusnet'] = 'http://status.net/schema/api/1/';
542         }
543         $this->elementStart($role, $attrs);
544         foreach ($twitter_user as $element => $value) {
545             if ($element == 'status') {
546                 $this->showTwitterXmlStatus($twitter_user['status']);
547             } elseif (strncmp($element, 'statusnet_', 10) == 0) {
548                 $this->element('statusnet:' . substr($element, 10), null, $value);
549             } else {
550                 $this->element($element, null, $value);
551             }
552         }
553         $this->elementEnd($role);
554     }
555
556     public function showXmlAttachments($attachments)
557     {
558         if (!empty($attachments)) {
559             $this->elementStart('attachments', ['type' => 'array']);
560             foreach ($attachments as $attachment) {
561                 $attrs = [];
562                 $attrs['url'] = $attachment['url'];
563                 $attrs['mimetype'] = $attachment['mimetype'];
564                 $attrs['size'] = $attachment['size'];
565                 $this->element('enclosure', $attrs, '');
566             }
567             $this->elementEnd('attachments');
568         }
569     }
570
571     public function showGeoXML($geo)
572     {
573         if (empty($geo)) {
574             // empty geo element
575             $this->element('geo');
576         } else {
577             $this->elementStart('geo', ['xmlns:georss' => 'http://www.georss.org/georss']);
578             $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
579             $this->elementEnd('geo');
580         }
581     }
582
583     public function endDocument($type = 'xml')
584     {
585         switch ($type) {
586             case 'xml':
587                 $this->endXML();
588                 break;
589             case 'json':
590                 // Check for JSONP callback
591                 if (isset($this->callback)) {
592                     print ')';
593                 }
594                 break;
595             case 'rss':
596                 $this->endTwitterRss();
597                 break;
598             case 'atom':
599                 $this->endTwitterRss();
600                 break;
601             default:
602                 // TRANS: Client error on an API request with an unsupported data format.
603                 $this->clientError(_('Not a supported data format.'));
604         }
605         return;
606     }
607
608     public function endTwitterRss()
609     {
610         $this->elementEnd('channel');
611         $this->elementEnd('rss');
612         $this->endXML();
613     }
614
615     public function showSingleAtomStatus($notice)
616     {
617         header('Content-Type: application/atom+xml;type=entry;charset="utf-8"');
618         print '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
619         print $notice->asAtomEntry(true, true, true, $this->scoped);
620     }
621
622     public function show_single_json_status($notice)
623     {
624         $this->initDocument('json');
625         $status = $this->twitterStatusArray($notice);
626         $this->showJsonObjects($status);
627         $this->endDocument('json');
628     }
629
630     public function showJsonObjects($objects)
631     {
632         $json_objects = json_encode($objects);
633         if ($json_objects === false) {
634             $this->clientError(_('JSON encoding failed. Error: ') . json_last_error_msg());
635         } else {
636             print $json_objects;
637         }
638     }
639
640     public function showXmlTimeline($notice)
641     {
642         $this->initDocument('xml');
643         $this->elementStart('statuses', ['type' => 'array',
644             'xmlns:statusnet' => 'http://status.net/schema/api/1/']);
645
646         if (is_array($notice)) {
647             //FIXME: make everything calling showJsonTimeline use only Notice objects
648             $ids = [];
649             foreach ($notice as $n) {
650                 $ids[] = $n->getID();
651             }
652             $notice = Notice::multiGet('id', $ids);
653         }
654
655         while ($notice->fetch()) {
656             try {
657                 $twitter_status = $this->twitterStatusArray($notice);
658                 $this->showTwitterXmlStatus($twitter_status);
659             } catch (Exception $e) {
660                 common_log(LOG_ERR, $e->getMessage());
661                 continue;
662             }
663         }
664
665         $this->elementEnd('statuses');
666         $this->endDocument('xml');
667     }
668
669     public function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
670     {
671         $this->initDocument('rss');
672
673         $this->element('title', null, $title);
674         $this->element('link', null, $link);
675
676         if (!is_null($self)) {
677             $this->element(
678                 'atom:link',
679                 [
680                     'type' => 'application/rss+xml',
681                     'href' => $self,
682                     'rel' => 'self'
683                 ]
684             );
685         }
686
687         if (!is_null($suplink)) {
688             // For FriendFeed's SUP protocol
689             $this->element('link', ['xmlns' => 'http://www.w3.org/2005/Atom',
690                 'rel' => 'http://api.friendfeed.com/2008/03#sup',
691                 'href' => $suplink,
692                 'type' => 'application/json']);
693         }
694
695         if (!is_null($logo)) {
696             $this->elementStart('image');
697             $this->element('link', null, $link);
698             $this->element('title', null, $title);
699             $this->element('url', null, $logo);
700             $this->elementEnd('image');
701         }
702
703         $this->element('description', null, $subtitle);
704         $this->element('language', null, 'en-us');
705         $this->element('ttl', null, '40');
706
707         if (is_array($notice)) {
708             //FIXME: make everything calling showJsonTimeline use only Notice objects
709             $ids = [];
710             foreach ($notice as $n) {
711                 $ids[] = $n->getID();
712             }
713             $notice = Notice::multiGet('id', $ids);
714         }
715
716         while ($notice->fetch()) {
717             try {
718                 $entry = $this->twitterRssEntryArray($notice);
719                 $this->showTwitterRssItem($entry);
720             } catch (Exception $e) {
721                 common_log(LOG_ERR, $e->getMessage());
722                 // continue on exceptions
723             }
724         }
725
726         $this->endTwitterRss();
727     }
728
729     public function twitterRssEntryArray($notice)
730     {
731         $entry = [];
732
733         if (Event::handle('StartRssEntryArray', [$notice, &$entry])) {
734             $profile = $notice->getProfile();
735
736             // We trim() to avoid extraneous whitespace in the output
737
738             $entry['content'] = common_xml_safe_str(trim($notice->getRendered()));
739             $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
740             $entry['link'] = common_local_url('shownotice', ['notice' => $notice->id]);
741             $entry['published'] = common_date_iso8601($notice->created);
742
743             $taguribase = TagURI::base();
744             $entry['id'] = "tag:$taguribase:$entry[link]";
745
746             $entry['updated'] = $entry['published'];
747             $entry['author'] = $profile->getBestName();
748
749             // Enclosures
750             $attachments = $notice->attachments();
751             $enclosures = [];
752
753             foreach ($attachments as $attachment) {
754                 try {
755                     $enclosure_o = $attachment->getEnclosure();
756                     $enclosure = [];
757                     $enclosure['url'] = $enclosure_o->url;
758                     $enclosure['mimetype'] = $enclosure_o->mimetype;
759                     $enclosure['size'] = $enclosure_o->size;
760                     $enclosures[] = $enclosure;
761                 } catch (ServerException $e) {
762                     // There was not enough metadata available
763                 }
764             }
765
766             if (!empty($enclosures)) {
767                 $entry['enclosures'] = $enclosures;
768             }
769
770             // Tags/Categories
771             $tag = new Notice_tag();
772             $tag->notice_id = $notice->id;
773             if ($tag->find()) {
774                 $entry['tags'] = [];
775                 while ($tag->fetch()) {
776                     $entry['tags'][] = $tag->tag;
777                 }
778             }
779             $tag->free();
780
781             // RSS Item specific
782             $entry['description'] = $entry['content'];
783             $entry['pubDate'] = common_date_rfc2822($notice->created);
784             $entry['guid'] = $entry['link'];
785
786             try {
787                 $notloc = Notice_location::locFromStored($notice);
788                 // This is the format that GeoJSON expects stuff to be in.
789                 // showGeoRSS() below uses it for XML output, so we reuse it
790                 $entry['geo'] = ['type' => 'Point',
791                     'coordinates' => [(float)$notloc->lat,
792                         (float)$notloc->lon]];
793             } catch (ServerException $e) {
794                 $entry['geo'] = null;
795             }
796
797             Event::handle('EndRssEntryArray', [$notice, &$entry]);
798         }
799
800         return $entry;
801     }
802
803     public function showTwitterRssItem($entry)
804     {
805         $this->elementStart('item');
806         $this->element('title', null, $entry['title']);
807         $this->element('description', null, $entry['description']);
808         $this->element('pubDate', null, $entry['pubDate']);
809         $this->element('guid', null, $entry['guid']);
810         $this->element('link', null, $entry['link']);
811
812         // RSS only supports 1 enclosure per item
813         if (array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])) {
814             $enclosure = $entry['enclosures'][0];
815             $this->element('enclosure', ['url' => $enclosure['url'], 'type' => $enclosure['mimetype'], 'length' => $enclosure['size']]);
816         }
817
818         if (array_key_exists('tags', $entry)) {
819             foreach ($entry['tags'] as $tag) {
820                 $this->element('category', null, $tag);
821             }
822         }
823
824         $this->showGeoRSS($entry['geo']);
825         $this->elementEnd('item');
826     }
827
828     public function showGeoRSS($geo)
829     {
830         if (!empty($geo)) {
831             $this->element(
832                 'georss:point',
833                 null,
834                 $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
835             );
836         }
837     }
838
839     public function showAtomTimeline($notice, $title, $id, $link, $subtitle = null, $suplink = null, $selfuri = null, $logo = null)
840     {
841         $this->initDocument('atom');
842
843         $this->element('title', null, $title);
844         $this->element('id', null, $id);
845         $this->element('link', ['href' => $link, 'rel' => 'alternate', 'type' => 'text/html']);
846
847         if (!is_null($logo)) {
848             $this->element('logo', null, $logo);
849         }
850
851         if (!is_null($suplink)) {
852             // For FriendFeed's SUP protocol
853             $this->element('link', ['rel' => 'http://api.friendfeed.com/2008/03#sup',
854                 'href' => $suplink,
855                 'type' => 'application/json']);
856         }
857
858         if (!is_null($selfuri)) {
859             $this->element('link', ['href' => $selfuri,
860                 'rel' => 'self', 'type' => 'application/atom+xml']);
861         }
862
863         $this->element('updated', null, common_date_iso8601('now'));
864         $this->element('subtitle', null, $subtitle);
865
866         if (is_array($notice)) {
867             //FIXME: make everything calling showJsonTimeline use only Notice objects
868             $ids = [];
869             foreach ($notice as $n) {
870                 $ids[] = $n->getID();
871             }
872             $notice = Notice::multiGet('id', $ids);
873         }
874
875         while ($notice->fetch()) {
876             try {
877                 $this->raw($notice->asAtomEntry());
878             } catch (Exception $e) {
879                 common_log(LOG_ERR, $e->getMessage());
880                 continue;
881             }
882         }
883
884         $this->endDocument('atom');
885     }
886
887     public function showRssGroups($group, $title, $link, $subtitle)
888     {
889         $this->initDocument('rss');
890
891         $this->element('title', null, $title);
892         $this->element('link', null, $link);
893         $this->element('description', null, $subtitle);
894         $this->element('language', null, 'en-us');
895         $this->element('ttl', null, '40');
896
897         if (is_array($group)) {
898             foreach ($group as $g) {
899                 $twitter_group = $this->twitterRssGroupArray($g);
900                 $this->showTwitterRssItem($twitter_group);
901             }
902         } else {
903             while ($group->fetch()) {
904                 $twitter_group = $this->twitterRssGroupArray($group);
905                 $this->showTwitterRssItem($twitter_group);
906             }
907         }
908
909         $this->endTwitterRss();
910     }
911
912     public function twitterRssGroupArray($group)
913     {
914         $entry = [];
915         $entry['content'] = $group->description;
916         $entry['title'] = $group->nickname;
917         $entry['link'] = $group->permalink();
918         $entry['published'] = common_date_iso8601($group->created);
919         $entry['updated'] = common_date_iso8601($group->modified);
920         $taguribase = common_config('integration', 'groupuri');
921         $entry['id'] = "group:$taguribase:$entry[link]";
922
923         $entry['description'] = $entry['content'];
924         $entry['pubDate'] = common_date_rfc2822($group->created);
925         $entry['guid'] = $entry['link'];
926
927         return $entry;
928     }
929
930     public function showTwitterAtomEntry($entry)
931     {
932         $this->elementStart('entry');
933         $this->element('title', null, common_xml_safe_str($entry['title']));
934         $this->element(
935             'content',
936             ['type' => 'html'],
937             common_xml_safe_str($entry['content'])
938         );
939         $this->element('id', null, $entry['id']);
940         $this->element('published', null, $entry['published']);
941         $this->element('updated', null, $entry['updated']);
942         $this->element('link', ['type' => 'text/html',
943             'href' => $entry['link'],
944             'rel' => 'alternate']);
945         $this->element('link', ['type' => $entry['avatar-type'],
946             'href' => $entry['avatar'],
947             'rel' => 'image']);
948         $this->elementStart('author');
949
950         $this->element('name', null, $entry['author-name']);
951         $this->element('uri', null, $entry['author-uri']);
952
953         $this->elementEnd('author');
954         $this->elementEnd('entry');
955     }
956
957     public function showAtomGroups($group, $title, $id, $link, $subtitle = null, $selfuri = null)
958     {
959         $this->initDocument('atom');
960
961         $this->element('title', null, common_xml_safe_str($title));
962         $this->element('id', null, $id);
963         $this->element('link', ['href' => $link, 'rel' => 'alternate', 'type' => 'text/html']);
964
965         if (!is_null($selfuri)) {
966             $this->element('link', ['href' => $selfuri,
967                 'rel' => 'self', 'type' => 'application/atom+xml']);
968         }
969
970         $this->element('updated', null, common_date_iso8601('now'));
971         $this->element('subtitle', null, common_xml_safe_str($subtitle));
972
973         if (is_array($group)) {
974             foreach ($group as $g) {
975                 $this->raw($g->asAtomEntry());
976             }
977         } else {
978             while ($group->fetch()) {
979                 $this->raw($group->asAtomEntry());
980             }
981         }
982
983         $this->endDocument('atom');
984     }
985
986     public function showJsonTimeline($notice)
987     {
988         $this->initDocument('json');
989
990         $statuses = [];
991
992         if (is_array($notice)) {
993             //FIXME: make everything calling showJsonTimeline use only Notice objects
994             $ids = [];
995             foreach ($notice as $n) {
996                 $ids[] = $n->getID();
997             }
998             $notice = Notice::multiGet('id', $ids);
999         }
1000
1001         while ($notice->fetch()) {
1002             try {
1003                 $twitter_status = $this->twitterStatusArray($notice);
1004                 array_push($statuses, $twitter_status);
1005             } catch (Exception $e) {
1006                 common_log(LOG_ERR, $e->getMessage());
1007                 continue;
1008             }
1009         }
1010
1011         $this->showJsonObjects($statuses);
1012
1013         $this->endDocument('json');
1014     }
1015
1016     public function showJsonGroups($group)
1017     {
1018         $this->initDocument('json');
1019
1020         $groups = [];
1021
1022         if (is_array($group)) {
1023             foreach ($group as $g) {
1024                 $twitter_group = $this->twitterGroupArray($g);
1025                 array_push($groups, $twitter_group);
1026             }
1027         } else {
1028             while ($group->fetch()) {
1029                 $twitter_group = $this->twitterGroupArray($group);
1030                 array_push($groups, $twitter_group);
1031             }
1032         }
1033
1034         $this->showJsonObjects($groups);
1035
1036         $this->endDocument('json');
1037     }
1038
1039     public function twitterGroupArray($group)
1040     {
1041         $twitter_group = [];
1042
1043         $twitter_group['id'] = intval($group->id);
1044         $twitter_group['url'] = $group->permalink();
1045         $twitter_group['nickname'] = $group->nickname;
1046         $twitter_group['fullname'] = $group->fullname;
1047
1048         if ($this->scoped instanceof Profile) {
1049             $twitter_group['member'] = $this->scoped->isMember($group);
1050             $twitter_group['blocked'] = Group_block::isBlocked(
1051                 $group,
1052                 $this->scoped
1053             );
1054         }
1055
1056         $twitter_group['admin_count'] = $group->getAdminCount();
1057         $twitter_group['member_count'] = $group->getMemberCount();
1058         $twitter_group['original_logo'] = $group->original_logo;
1059         $twitter_group['homepage_logo'] = $group->homepage_logo;
1060         $twitter_group['stream_logo'] = $group->stream_logo;
1061         $twitter_group['mini_logo'] = $group->mini_logo;
1062         $twitter_group['homepage'] = $group->homepage;
1063         $twitter_group['description'] = $group->description;
1064         $twitter_group['location'] = $group->location;
1065         $twitter_group['created'] = self::dateTwitter($group->created);
1066         $twitter_group['modified'] = self::dateTwitter($group->modified);
1067
1068         return $twitter_group;
1069     }
1070
1071     public function showXmlGroups($group)
1072     {
1073         $this->initDocument('xml');
1074         $this->elementStart('groups', ['type' => 'array']);
1075
1076         if (is_array($group)) {
1077             foreach ($group as $g) {
1078                 $twitter_group = $this->twitterGroupArray($g);
1079                 $this->showTwitterXmlGroup($twitter_group);
1080             }
1081         } else {
1082             while ($group->fetch()) {
1083                 $twitter_group = $this->twitterGroupArray($group);
1084                 $this->showTwitterXmlGroup($twitter_group);
1085             }
1086         }
1087
1088         $this->elementEnd('groups');
1089         $this->endDocument('xml');
1090     }
1091
1092     public function showTwitterXmlGroup($twitter_group)
1093     {
1094         $this->elementStart('group');
1095         foreach ($twitter_group as $element => $value) {
1096             $this->element($element, null, $value);
1097         }
1098         $this->elementEnd('group');
1099     }
1100
1101     public function showXmlLists($list, $next_cursor = 0, $prev_cursor = 0)
1102     {
1103         $this->initDocument('xml');
1104         $this->elementStart('lists_list');
1105         $this->elementStart('lists', ['type' => 'array']);
1106
1107         if (is_array($list)) {
1108             foreach ($list as $l) {
1109                 $twitter_list = $this->twitterListArray($l);
1110                 $this->showTwitterXmlList($twitter_list);
1111             }
1112         } else {
1113             while ($list->fetch()) {
1114                 $twitter_list = $this->twitterListArray($list);
1115                 $this->showTwitterXmlList($twitter_list);
1116             }
1117         }
1118
1119         $this->elementEnd('lists');
1120
1121         $this->element('next_cursor', null, $next_cursor);
1122         $this->element('previous_cursor', null, $prev_cursor);
1123
1124         $this->elementEnd('lists_list');
1125         $this->endDocument('xml');
1126     }
1127
1128     public function twitterListArray($list)
1129     {
1130         $profile = Profile::getKV('id', $list->tagger);
1131
1132         $twitter_list = [];
1133         $twitter_list['id'] = $list->id;
1134         $twitter_list['name'] = $list->tag;
1135         $twitter_list['full_name'] = '@' . $profile->nickname . '/' . $list->tag;;
1136         $twitter_list['slug'] = $list->tag;
1137         $twitter_list['description'] = $list->description;
1138         $twitter_list['subscriber_count'] = $list->subscriberCount();
1139         $twitter_list['member_count'] = $list->taggedCount();
1140         $twitter_list['uri'] = $list->getUri();
1141
1142         if ($this->scoped instanceof Profile) {
1143             $twitter_list['following'] = $list->hasSubscriber($this->scoped);
1144         } else {
1145             $twitter_list['following'] = false;
1146         }
1147
1148         $twitter_list['mode'] = ($list->private) ? 'private' : 'public';
1149         $twitter_list['user'] = $this->twitterUserArray($profile, false);
1150
1151         return $twitter_list;
1152     }
1153
1154     public function showTwitterXmlList($twitter_list)
1155     {
1156         $this->elementStart('list');
1157         foreach ($twitter_list as $element => $value) {
1158             if ($element == 'user') {
1159                 $this->showTwitterXmlUser($value, 'user');
1160             } else {
1161                 $this->element($element, null, $value);
1162             }
1163         }
1164         $this->elementEnd('list');
1165     }
1166
1167     public function showJsonLists($list, $next_cursor = 0, $prev_cursor = 0)
1168     {
1169         $this->initDocument('json');
1170
1171         $lists = [];
1172
1173         if (is_array($list)) {
1174             foreach ($list as $l) {
1175                 $twitter_list = $this->twitterListArray($l);
1176                 array_push($lists, $twitter_list);
1177             }
1178         } else {
1179             while ($list->fetch()) {
1180                 $twitter_list = $this->twitterListArray($list);
1181                 array_push($lists, $twitter_list);
1182             }
1183         }
1184
1185         $lists_list = [
1186             'lists' => $lists,
1187             'next_cursor' => $next_cursor,
1188             'next_cursor_str' => strval($next_cursor),
1189             'previous_cursor' => $prev_cursor,
1190             'previous_cursor_str' => strval($prev_cursor)
1191         ];
1192
1193         $this->showJsonObjects($lists_list);
1194
1195         $this->endDocument('json');
1196     }
1197
1198     public function showTwitterXmlUsers($user)
1199     {
1200         $this->initDocument('xml');
1201         $this->elementStart('users', ['type' => 'array',
1202             'xmlns:statusnet' => 'http://status.net/schema/api/1/']);
1203
1204         if (is_array($user)) {
1205             foreach ($user as $u) {
1206                 $twitter_user = $this->twitterUserArray($u);
1207                 $this->showTwitterXmlUser($twitter_user);
1208             }
1209         } else {
1210             while ($user->fetch()) {
1211                 $twitter_user = $this->twitterUserArray($user);
1212                 $this->showTwitterXmlUser($twitter_user);
1213             }
1214         }
1215
1216         $this->elementEnd('users');
1217         $this->endDocument('xml');
1218     }
1219
1220     public function showJsonUsers($user)
1221     {
1222         $this->initDocument('json');
1223
1224         $users = [];
1225
1226         if (is_array($user)) {
1227             foreach ($user as $u) {
1228                 $twitter_user = $this->twitterUserArray($u);
1229                 array_push($users, $twitter_user);
1230             }
1231         } else {
1232             while ($user->fetch()) {
1233                 $twitter_user = $this->twitterUserArray($user);
1234                 array_push($users, $twitter_user);
1235             }
1236         }
1237
1238         $this->showJsonObjects($users);
1239
1240         $this->endDocument('json');
1241     }
1242
1243     public function showSingleJsonGroup($group)
1244     {
1245         $this->initDocument('json');
1246         $twitter_group = $this->twitterGroupArray($group);
1247         $this->showJsonObjects($twitter_group);
1248         $this->endDocument('json');
1249     }
1250
1251     public function showSingleXmlGroup($group)
1252     {
1253         $this->initDocument('xml');
1254         $twitter_group = $this->twitterGroupArray($group);
1255         $this->showTwitterXmlGroup($twitter_group);
1256         $this->endDocument('xml');
1257     }
1258
1259     public function showSingleJsonList($list)
1260     {
1261         $this->initDocument('json');
1262         $twitter_list = $this->twitterListArray($list);
1263         $this->showJsonObjects($twitter_list);
1264         $this->endDocument('json');
1265     }
1266
1267     public function showSingleXmlList($list)
1268     {
1269         $this->initDocument('xml');
1270         $twitter_list = $this->twitterListArray($list);
1271         $this->showTwitterXmlList($twitter_list);
1272         $this->endDocument('xml');
1273     }
1274
1275     public function endTwitterAtom()
1276     {
1277         $this->elementEnd('feed');
1278         $this->endXML();
1279     }
1280
1281     public function showProfile($profile, $content_type = 'xml', $notice = null, $includeStatuses = true)
1282     {
1283         $profile_array = $this->twitterUserArray($profile, $includeStatuses);
1284         switch ($content_type) {
1285             case 'xml':
1286                 $this->showTwitterXmlUser($profile_array);
1287                 break;
1288             case 'json':
1289                 $this->showJsonObjects($profile_array);
1290                 break;
1291             default:
1292                 // TRANS: Client error on an API request with an unsupported data format.
1293                 $this->clientError(_('Not a supported data format.'));
1294         }
1295         return;
1296     }
1297
1298     public function getTargetProfile($id)
1299     {
1300         if (empty($id)) {
1301             // Twitter supports these other ways of passing the user ID
1302             if (self::is_decimal($this->arg('id'))) {
1303                 return Profile::getKV($this->arg('id'));
1304             } elseif ($this->arg('id')) {
1305                 // Screen names currently can only uniquely identify a local user.
1306                 $nickname = common_canonical_nickname($this->arg('id'));
1307                 $user = User::getKV('nickname', $nickname);
1308                 return $user ? $user->getProfile() : null;
1309             } elseif ($this->arg('user_id')) {
1310                 // This is to ensure that a non-numeric user_id still
1311                 // overrides screen_name even if it doesn't get used
1312                 if (self::is_decimal($this->arg('user_id'))) {
1313                     return Profile::getKV('id', $this->arg('user_id'));
1314                 }
1315             } elseif (mb_strlen($this->arg('screen_name')) > 0) {
1316                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1317                 $user = User::getByNickname($nickname);
1318                 return $user->getProfile();
1319             } else {
1320                 // Fall back to trying the currently authenticated user
1321                 return $this->scoped;
1322             }
1323         }
1324
1325         if (self::is_decimal($id) && intval($id) > 0) {
1326             return Profile::getByID($id);
1327         }
1328
1329         // FIXME: check if isAcct to identify remote profiles and not just local nicknames
1330         $nickname = common_canonical_nickname($id);
1331         $user = User::getByNickname($nickname);
1332         return $user->getProfile();
1333     }
1334
1335     private static function is_decimal($str)
1336     {
1337         return preg_match('/^[0-9]+$/', $str);
1338     }
1339
1340     /**
1341      * Returns query argument or default value if not found. Certain
1342      * parameters used throughout the API are lightly scrubbed and
1343      * bounds checked.  This overrides Action::arg().
1344      *
1345      * @param string $key requested argument
1346      * @param string $def default value to return if $key is not provided
1347      *
1348      * @return var $var
1349      */
1350     public function arg($key, $def = null)
1351     {
1352         // XXX: Do even more input validation/scrubbing?
1353
1354         if (array_key_exists($key, $this->args)) {
1355             switch ($key) {
1356                 case 'page':
1357                     $page = (int)$this->args['page'];
1358                     return ($page < 1) ? 1 : $page;
1359                 case 'count':
1360                     $count = (int)$this->args['count'];
1361                     if ($count < 1) {
1362                         return 20;
1363                     } elseif ($count > 200) {
1364                         return 200;
1365                     } else {
1366                         return $count;
1367                     }
1368                 // no break
1369                 case 'since_id':
1370                     $since_id = (int)$this->args['since_id'];
1371                     return ($since_id < 1) ? 0 : $since_id;
1372                 case 'max_id':
1373                     $max_id = (int)$this->args['max_id'];
1374                     return ($max_id < 1) ? 0 : $max_id;
1375                 default:
1376                     return parent::arg($key, $def);
1377             }
1378         } else {
1379             return $def;
1380         }
1381     }
1382
1383     public function getTargetGroup($id)
1384     {
1385         if (empty($id)) {
1386             if (self::is_decimal($this->arg('id'))) {
1387                 return User_group::getKV('id', $this->arg('id'));
1388             } elseif ($this->arg('id')) {
1389                 return User_group::getForNickname($this->arg('id'));
1390             } elseif ($this->arg('group_id')) {
1391                 // This is to ensure that a non-numeric group_id still
1392                 // overrides group_name even if it doesn't get used
1393                 if (self::is_decimal($this->arg('group_id'))) {
1394                     return User_group::getKV('id', $this->arg('group_id'));
1395                 }
1396             } elseif ($this->arg('group_name')) {
1397                 return User_group::getForNickname($this->arg('group_name'));
1398             }
1399         }
1400
1401         if (self::is_decimal($id)) {
1402             return User_group::getKV('id', $id);
1403         } elseif ($this->arg('uri')) { // FIXME: move this into empty($id) check?
1404             return User_group::getKV('uri', urldecode($this->arg('uri')));
1405         }
1406
1407         return User_group::getForNickname($id);
1408     }
1409
1410     public function getTargetList($user = null, $id = null)
1411     {
1412         $tagger = $this->getTargetUser($user);
1413         $list = null;
1414
1415         if (empty($id)) {
1416             $id = $this->arg('id');
1417         }
1418
1419         if ($id) {
1420             if (is_numeric($id)) {
1421                 $list = Profile_list::getKV('id', $id);
1422
1423                 // only if the list with the id belongs to the tagger
1424                 if (empty($list) || $list->tagger != $tagger->id) {
1425                     $list = null;
1426                 }
1427             }
1428             if (empty($list)) {
1429                 $tag = common_canonical_tag($id);
1430                 $list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
1431             }
1432
1433             if (!empty($list) && $list->private) {
1434                 if ($this->scoped->id == $list->tagger) {
1435                     return $list;
1436                 }
1437             } else {
1438                 return $list;
1439             }
1440         }
1441         return null;
1442     }
1443
1444     public function getTargetUser($id)
1445     {
1446         if (empty($id)) {
1447             // Twitter supports these other ways of passing the user ID
1448             if (self::is_decimal($this->arg('id'))) {
1449                 return User::getKV($this->arg('id'));
1450             } elseif ($this->arg('id')) {
1451                 $nickname = common_canonical_nickname($this->arg('id'));
1452                 return User::getKV('nickname', $nickname);
1453             } elseif ($this->arg('user_id')) {
1454                 // This is to ensure that a non-numeric user_id still
1455                 // overrides screen_name even if it doesn't get used
1456                 if (self::is_decimal($this->arg('user_id'))) {
1457                     return User::getKV('id', $this->arg('user_id'));
1458                 }
1459             } elseif ($this->arg('screen_name')) {
1460                 $nickname = common_canonical_nickname($this->arg('screen_name'));
1461                 return User::getKV('nickname', $nickname);
1462             } else {
1463                 // Fall back to trying the currently authenticated user
1464                 return $this->scoped->getUser();
1465             }
1466         }
1467
1468         if (self::is_decimal($id)) {
1469             return User::getKV($id);
1470         }
1471
1472         $nickname = common_canonical_nickname($id);
1473         return User::getKV('nickname', $nickname);
1474     }
1475
1476     /**
1477      * Calculate the complete URI that called up this action.  Used for
1478      * Atom rel="self" links.  Warning: this is funky.
1479      *
1480      * @return string URL    a URL suitable for rel="self" Atom links
1481      */
1482     public function getSelfUri()
1483     {
1484         $action = mb_substr(get_class($this), 0, -6); // remove 'Action'
1485
1486         $id = $this->arg('id');
1487         $aargs = ['format' => $this->format];
1488         if (!empty($id)) {
1489             $aargs['id'] = $id;
1490         }
1491
1492         $user = $this->arg('user');
1493         if (!empty($user)) {
1494             $aargs['user'] = $user;
1495         }
1496
1497         $tag = $this->arg('tag');
1498         if (!empty($tag)) {
1499             $aargs['tag'] = $tag;
1500         }
1501
1502         parse_str($_SERVER['QUERY_STRING'], $params);
1503         $pstring = '';
1504         if (!empty($params)) {
1505             unset($params['p']);
1506             $pstring = http_build_query($params);
1507         }
1508
1509         $uri = common_local_url($action, $aargs);
1510
1511         if (!empty($pstring)) {
1512             $uri .= '?' . $pstring;
1513         }
1514
1515         return $uri;
1516     }
1517
1518     /**
1519      * Initialization.
1520      *
1521      * @param array $args Web and URL arguments
1522      *
1523      * @return boolean false if user doesn't exist
1524      * @throws ClientException
1525      */
1526     protected function prepare(array $args = [])
1527     {
1528         GNUsocial::setApi(true); // reduce exception reports to aid in debugging
1529         parent::prepare($args);
1530
1531         $this->format = $this->arg('format');
1532         $this->callback = $this->arg('callback');
1533         $this->page = (int)$this->arg('page', 1);
1534         $this->count = (int)$this->arg('count', 20);
1535         $this->max_id = (int)$this->arg('max_id', 0);
1536         $this->since_id = (int)$this->arg('since_id', 0);
1537
1538         // These two are not used everywhere, mainly just AtompubAction extensions
1539         $this->offset = ($this->page - 1) * $this->count;
1540         $this->limit = $this->count + 1;
1541
1542         if ($this->arg('since')) {
1543             header('X-GNUsocial-Warning: since parameter is disabled; use since_id');
1544         }
1545
1546         $this->source = $this->trimmed('source');
1547
1548         if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
1549             $this->source = 'api';
1550         }
1551
1552         return true;
1553     }
1554
1555     /**
1556      * Handle a request
1557      *
1558      * @return void
1559      */
1560     protected function handle()
1561     {
1562         header('Access-Control-Allow-Origin: *');
1563         parent::handle();
1564     }
1565 }