apiaction.php 52 KB

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