apistatusesupdate.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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->source = $this->trimmed('source');
  150. $this->lat = $this->trimmed('lat');
  151. $this->lon = $this->trimmed('long');
  152. $matches = array();
  153. common_debug(get_called_class().': media_ids=='._ve($this->trimmed('media_ids')));
  154. if (preg_match_all('/\d+/', $this->trimmed('media_ids'), $matches) !== false) {
  155. foreach (array_unique($matches[0]) as $match) {
  156. try {
  157. $this->media_ids[$match] = File::getByID($match);
  158. } catch (EmptyPkeyValueException $e) {
  159. // got a zero from the client, at least Twidere does this on occasion
  160. } catch (NoResultException $e) {
  161. // File ID was not found. Do we abort and report to the client?
  162. }
  163. }
  164. }
  165. $this->in_reply_to_status_id
  166. = intval($this->trimmed('in_reply_to_status_id'));
  167. return true;
  168. }
  169. /**
  170. * Handle the request
  171. *
  172. * Make a new notice for the update, save it, and show it
  173. *
  174. * @return void
  175. */
  176. protected function handle()
  177. {
  178. parent::handle();
  179. // Workaround for PHP returning empty $_POST and $_FILES when POST
  180. // length > post_max_size in php.ini
  181. if (empty($_FILES)
  182. && empty($_POST)
  183. && ($_SERVER['CONTENT_LENGTH'] > 0)
  184. ) {
  185. // TRANS: Client error displayed when the number of bytes in a POST request exceeds a limit.
  186. // TRANS: %s is the number of bytes of the CONTENT_LENGTH.
  187. $msg = _m('The server was unable to handle that much POST data (%s byte) due to its current configuration.',
  188. 'The server was unable to handle that much POST data (%s bytes) due to its current configuration.',
  189. intval($_SERVER['CONTENT_LENGTH']));
  190. $this->clientError(sprintf($msg, $_SERVER['CONTENT_LENGTH']));
  191. }
  192. if (empty($this->status)) {
  193. // TRANS: Client error displayed when the parameter "status" is missing.
  194. $this->clientError(_('Client must provide a \'status\' parameter with a value.'));
  195. }
  196. if (is_null($this->scoped)) {
  197. // TRANS: Client error displayed when updating a status for a non-existing user.
  198. $this->clientError(_('No such user.'), 404);
  199. }
  200. /* Do not call shortenLinks until the whole notice has been build */
  201. // Check for commands
  202. $inter = new CommandInterpreter();
  203. $cmd = $inter->handle_command($this->auth_user, $this->status);
  204. if ($cmd) {
  205. if ($this->supported($cmd)) {
  206. $cmd->execute(new Channel());
  207. }
  208. // Cmd not supported? Twitter just returns your latest status.
  209. // And, it returns your last status whether the cmd was successful
  210. // or not!
  211. $this->notice = $this->auth_user->getCurrentNotice();
  212. } else {
  213. $reply_to = null;
  214. if (!empty($this->in_reply_to_status_id)) {
  215. // Check whether notice actually exists
  216. $reply = Notice::getKV($this->in_reply_to_status_id);
  217. if ($reply) {
  218. $reply_to = $this->in_reply_to_status_id;
  219. } else {
  220. // TRANS: Client error displayed when replying to a non-existing notice.
  221. $this->clientError(_('Parent notice not found.'), 404);
  222. }
  223. }
  224. foreach(array_keys($this->media_ids) as $media_id) {
  225. // FIXME: Validation on this... Worst case is that if someone sends bad media_ids then
  226. // we'll fill the notice with non-working links, so no real harm, done, but let's fix.
  227. // The File objects are in the array, so we could get URLs from them directly.
  228. $this->status .= ' ' . common_local_url('attachment', array('attachment' => $media_id));
  229. }
  230. $upload = null;
  231. try {
  232. $upload = MediaFile::fromUpload('media', $this->scoped);
  233. $this->status .= ' ' . $upload->shortUrl();
  234. /* Do not call shortenLinks until the whole notice has been build */
  235. } catch (NoUploadedMediaException $e) {
  236. // There was no uploaded media for us today.
  237. }
  238. /* Do call shortenlinks here & check notice length since notice is about to be saved & sent */
  239. $status_shortened = $this->auth_user->shortenLinks($this->status);
  240. if (Notice::contentTooLong($status_shortened)) {
  241. if ($upload instanceof MediaFile) {
  242. $upload->delete();
  243. }
  244. // TRANS: Client error displayed exceeding the maximum notice length.
  245. // TRANS: %d is the maximum lenth for a notice.
  246. $msg = _m('Maximum notice size is %d character, including attachment URL.',
  247. 'Maximum notice size is %d characters, including attachment URL.',
  248. Notice::maxContent());
  249. /* Use HTTP 413 error code (Request Entity Too Large)
  250. * instead of basic 400 for better understanding
  251. */
  252. $this->clientError(sprintf($msg, Notice::maxContent()), 413);
  253. }
  254. $content = html_entity_decode($status_shortened, ENT_NOQUOTES, 'UTF-8');
  255. $source = html_entity_decode($this->source, ENT_NOQUOTES, 'UTF-8');
  256. $options = array('reply_to' => $reply_to);
  257. if ($this->scoped->shareLocation()) {
  258. $locOptions = Notice::locationOptions($this->lat,
  259. $this->lon,
  260. null,
  261. null,
  262. $this->scoped);
  263. $options = array_merge($options, $locOptions);
  264. }
  265. try {
  266. $this->notice = Notice::saveNew(
  267. $this->scoped->id,
  268. $content,
  269. $this->source,
  270. $options
  271. );
  272. } catch (Exception $e) {
  273. $this->clientError($e->getMessage(), $e->getCode());
  274. }
  275. if (isset($upload)) {
  276. $upload->attachToNotice($this->notice);
  277. }
  278. }
  279. $this->showNotice();
  280. }
  281. /**
  282. * Show the resulting notice
  283. *
  284. * @return void
  285. */
  286. function showNotice()
  287. {
  288. if (!empty($this->notice)) {
  289. if ($this->format == 'xml') {
  290. $this->showSingleXmlStatus($this->notice);
  291. } elseif ($this->format == 'json') {
  292. $this->show_single_json_status($this->notice);
  293. } elseif ($this->format == 'atom') {
  294. $this->showSingleAtomStatus($this->notice);
  295. }
  296. }
  297. }
  298. /**
  299. * Is this command supported when doing an update from the API?
  300. *
  301. * @param string $cmd the command to check for
  302. *
  303. * @return boolean true or false
  304. */
  305. function supported($cmd)
  306. {
  307. static $cmdlist = array('SubCommand', 'UnsubCommand',
  308. 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand');
  309. $supported = null;
  310. if (Event::handle('CommandSupportedAPI', array($cmd, &$supported))) {
  311. $supported = $supported || in_array(get_class($cmd), $cmdlist);
  312. }
  313. return $supported;
  314. }
  315. }