apistatusesupdate.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Post a notice (update your status) through the API
  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 Evan Prodromou <evan@status.net>
  26. * @author Jeffery To <jeffery.to@gmail.com>
  27. * @author Tom Blankenship <mac65@mac65.com>
  28. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  29. * @author Robin Millette <robin@millette.info>
  30. * @author Zach Copley <zach@status.net>
  31. * @copyright 2009-2010 StatusNet, Inc.
  32. * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
  33. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  34. * @link http://status.net/
  35. */
  36. /* External API usage documentation. Please update when you change how this method works. */
  37. /*! @page statusesupdate statuses/update
  38. @section Description
  39. Updates the authenticating user's status. Requires the status parameter specified below.
  40. Request must be a POST.
  41. @par URL pattern
  42. /api/statuses/update.:format
  43. @par Formats (:format)
  44. xml, json, atom
  45. @par HTTP Method(s)
  46. POST
  47. @par Requires Authentication
  48. Yes
  49. @param status (Required) The URL-encoded text of the status update.
  50. @param source (Optional) The source application name, if using HTTP authentication or an anonymous OAuth consumer.
  51. @param in_reply_to_status_id (Optional) The ID of an existing status that the update is in reply to.
  52. @param lat (Optional) The latitude the status refers to.
  53. @param long (Optional) The longitude the status refers to.
  54. @param media (Optional) a media upload, such as an image or movie file.
  55. @sa @ref authentication
  56. @sa @ref apiroot
  57. @subsection usagenotes Usage notes
  58. @li The URL pattern is relative to the @ref apiroot.
  59. @li If the @e source parameter is not supplied the source of the status will default to 'api'. When authenticated via a registered OAuth application, the application's registered name and URL will always override the source parameter.
  60. @li The XML response uses <a href="http://georss.org/Main_Page">GeoRSS</a>
  61. to encode the latitude and longitude (see example response below <georss:point>).
  62. @li Data uploaded via the @e media parameter should be multipart/form-data encoded.
  63. @subsection exampleusage Example usage
  64. @verbatim
  65. curl -u username:password http://example.com/api/statuses/update.xml -d status='Howdy!' -d lat='30.468' -d long='-94.743'
  66. @endverbatim
  67. @subsection exampleresponse Example response
  68. @verbatim
  69. <?xml version="1.0" encoding="UTF-8"?>
  70. <status>
  71. <text>Howdy!</text>
  72. <truncated>false</truncated>
  73. <created_at>Tue Mar 30 23:28:05 +0000 2010</created_at>
  74. <in_reply_to_status_id/>
  75. <source>api</source>
  76. <id>26668724</id>
  77. <in_reply_to_user_id/>
  78. <in_reply_to_screen_name/>
  79. <geo xmlns:georss="http://www.georss.org/georss">
  80. <georss:point>30.468 -94.743</georss:point>
  81. </geo>
  82. <favorited>false</favorited>
  83. <user>
  84. <id>25803</id>
  85. <name>Jed Sanders</name>
  86. <screen_name>jedsanders</screen_name>
  87. <location>Hoop and Holler, Texas</location>
  88. <description>I like to think of myself as America's Favorite.</description>
  89. <profile_image_url>http://avatar.example.com/25803-48-20080924200604.png</profile_image_url>
  90. <url>http://jedsanders.net</url>
  91. <protected>false</protected>
  92. <followers_count>5</followers_count>
  93. <profile_background_color/>
  94. <profile_text_color/>
  95. <profile_link_color/>
  96. <profile_sidebar_fill_color/>
  97. <profile_sidebar_border_color/>
  98. <friends_count>2</friends_count>
  99. <created_at>Wed Sep 24 20:04:00 +0000 2008</created_at>
  100. <favourites_count>0</favourites_count>
  101. <utc_offset>0</utc_offset>
  102. <time_zone>UTC</time_zone>
  103. <profile_background_image_url/>
  104. <profile_background_tile>false</profile_background_tile>
  105. <statuses_count>70</statuses_count>
  106. <following>true</following>
  107. <notifications>true</notifications>
  108. </user>
  109. </status>
  110. @endverbatim
  111. */
  112. if (!defined('STATUSNET')) {
  113. exit(1);
  114. }
  115. /**
  116. * Updates the authenticating user's status (posts a notice).
  117. *
  118. * @category API
  119. * @package StatusNet
  120. * @author Craig Andrews <candrews@integralblue.com>
  121. * @author Evan Prodromou <evan@status.net>
  122. * @author Jeffery To <jeffery.to@gmail.com>
  123. * @author Tom Blankenship <mac65@mac65.com>
  124. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  125. * @author Robin Millette <robin@millette.info>
  126. * @author Zach Copley <zach@status.net>
  127. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  128. * @link http://status.net/
  129. */
  130. class ApiStatusesUpdateAction extends ApiAuthAction
  131. {
  132. protected $needPost = true;
  133. var $status = null;
  134. var $in_reply_to_status_id = null;
  135. var $lat = null;
  136. var $lon = null;
  137. var $media_ids = array(); // file_id in the keys
  138. /**
  139. * Take arguments for running
  140. *
  141. * @param array $args $_REQUEST args
  142. *
  143. * @return boolean success flag
  144. */
  145. protected function prepare(array $args=array())
  146. {
  147. parent::prepare($args);
  148. $this->status = $this->trimmed('status');
  149. $this->lat = $this->trimmed('lat');
  150. $this->lon = $this->trimmed('long');
  151. $matches = array();
  152. common_debug(get_called_class().': media_ids=='._ve($this->trimmed('media_ids')));
  153. if (preg_match_all('/\d+/', $this->trimmed('media_ids'), $matches) !== false) {
  154. foreach (array_unique($matches[0]) as $match) {
  155. try {
  156. $this->media_ids[$match] = File::getByID($match);
  157. } catch (EmptyPkeyValueException $e) {
  158. // got a zero from the client, at least Twidere does this on occasion
  159. } catch (NoResultException $e) {
  160. // File ID was not found. Do we abort and report to the client?
  161. }
  162. }
  163. }
  164. $this->in_reply_to_status_id
  165. = intval($this->trimmed('in_reply_to_status_id'));
  166. return true;
  167. }
  168. /**
  169. * Handle the request
  170. *
  171. * Make a new notice for the update, save it, and show it
  172. *
  173. * @return void
  174. */
  175. protected function handle()
  176. {
  177. parent::handle();
  178. // Workaround for PHP returning empty $_POST and $_FILES when POST
  179. // length > post_max_size in php.ini
  180. if (empty($_FILES)
  181. && empty($_POST)
  182. && ($_SERVER['CONTENT_LENGTH'] > 0)
  183. ) {
  184. // TRANS: Client error displayed when the number of bytes in a POST request exceeds a limit.
  185. // TRANS: %s is the number of bytes of the CONTENT_LENGTH.
  186. $msg = _m('The server was unable to handle that much POST data (%s byte) due to its current configuration.',
  187. 'The server was unable to handle that much POST data (%s bytes) due to its current configuration.',
  188. intval($_SERVER['CONTENT_LENGTH']));
  189. $this->clientError(sprintf($msg, $_SERVER['CONTENT_LENGTH']));
  190. }
  191. if (empty($this->status)) {
  192. // TRANS: Client error displayed when the parameter "status" is missing.
  193. $this->clientError(_('Client must provide a \'status\' parameter with a value.'));
  194. }
  195. if (is_null($this->scoped)) {
  196. // TRANS: Client error displayed when updating a status for a non-existing user.
  197. $this->clientError(_('No such user.'), 404);
  198. }
  199. /* Do not call shortenLinks until the whole notice has been build */
  200. // Check for commands
  201. $inter = new CommandInterpreter();
  202. $cmd = $inter->handle_command($this->auth_user, $this->status);
  203. if ($cmd) {
  204. if ($this->supported($cmd)) {
  205. $cmd->execute(new Channel());
  206. }
  207. // Cmd not supported? Twitter just returns your latest status.
  208. // And, it returns your last status whether the cmd was successful
  209. // or not!
  210. $this->notice = $this->auth_user->getCurrentNotice();
  211. } else {
  212. $reply_to = null;
  213. if (!empty($this->in_reply_to_status_id)) {
  214. // Check whether notice actually exists
  215. $reply = Notice::getKV($this->in_reply_to_status_id);
  216. if ($reply) {
  217. $reply_to = $this->in_reply_to_status_id;
  218. } else {
  219. // TRANS: Client error displayed when replying to a non-existing notice.
  220. $this->clientError(_('Parent notice not found.'), 404);
  221. }
  222. }
  223. foreach(array_keys($this->media_ids) as $media_id) {
  224. // FIXME: Validation on this... Worst case is that if someone sends bad media_ids then
  225. // we'll fill the notice with non-working links, so no real harm, done, but let's fix.
  226. // The File objects are in the array, so we could get URLs from them directly.
  227. $this->status .= ' ' . common_local_url('attachment', array('attachment' => $media_id));
  228. }
  229. $upload = null;
  230. try {
  231. $upload = MediaFile::fromUpload('media', $this->scoped);
  232. $this->status .= ' ' . $upload->shortUrl();
  233. /* Do not call shortenLinks until the whole notice has been build */
  234. } catch (NoUploadedMediaException $e) {
  235. // There was no uploaded media for us today.
  236. }
  237. /* Do call shortenlinks here & check notice length since notice is about to be saved & sent */
  238. $status_shortened = $this->auth_user->shortenLinks($this->status);
  239. if (Notice::contentTooLong($status_shortened)) {
  240. if ($upload instanceof MediaFile) {
  241. $upload->delete();
  242. }
  243. // TRANS: Client error displayed exceeding the maximum notice length.
  244. // TRANS: %d is the maximum lenth for a notice.
  245. $msg = _m('Maximum notice size is %d character, including attachment URL.',
  246. 'Maximum notice size is %d characters, including attachment URL.',
  247. Notice::maxContent());
  248. /* Use HTTP 413 error code (Request Entity Too Large)
  249. * instead of basic 400 for better understanding
  250. */
  251. $this->clientError(sprintf($msg, Notice::maxContent()), 413);
  252. }
  253. $content = html_entity_decode($status_shortened, ENT_NOQUOTES, 'UTF-8');
  254. $options = array('reply_to' => $reply_to);
  255. if ($this->scoped->shareLocation()) {
  256. $locOptions = Notice::locationOptions($this->lat,
  257. $this->lon,
  258. null,
  259. null,
  260. $this->scoped);
  261. $options = array_merge($options, $locOptions);
  262. }
  263. try {
  264. $this->notice = Notice::saveNew(
  265. $this->scoped->id,
  266. $content,
  267. $this->source,
  268. $options
  269. );
  270. } catch (Exception $e) {
  271. $this->clientError($e->getMessage(), $e->getCode());
  272. }
  273. if (isset($upload)) {
  274. $upload->attachToNotice($this->notice);
  275. }
  276. }
  277. $this->showNotice();
  278. }
  279. /**
  280. * Show the resulting notice
  281. *
  282. * @return void
  283. */
  284. function showNotice()
  285. {
  286. if (!empty($this->notice)) {
  287. if ($this->format == 'xml') {
  288. $this->showSingleXmlStatus($this->notice);
  289. } elseif ($this->format == 'json') {
  290. $this->show_single_json_status($this->notice);
  291. } elseif ($this->format == 'atom') {
  292. $this->showSingleAtomStatus($this->notice);
  293. }
  294. }
  295. }
  296. /**
  297. * Is this command supported when doing an update from the API?
  298. *
  299. * @param string $cmd the command to check for
  300. *
  301. * @return boolean true or false
  302. */
  303. function supported($cmd)
  304. {
  305. static $cmdlist = array('SubCommand', 'UnsubCommand',
  306. 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand');
  307. $supported = null;
  308. if (Event::handle('CommandSupportedAPI', array($cmd, &$supported))) {
  309. $supported = $supported || in_array(get_class($cmd), $cmdlist);
  310. }
  311. return $supported;
  312. }
  313. }