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