apiaction.php 52 KB

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