apiaction.php 52 KB

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