api.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. /**
  3. * @author Bart Visscher <bartv@thisnet.nl>
  4. * @author Bernhard Posselt <dev@bernhard-posselt.com>
  5. * @author Björn Schießle <schiessle@owncloud.com>
  6. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  7. * @author Lukas Reschke <lukas@owncloud.com>
  8. * @author Michael Gapczynski <GapczynskiM@gmail.com>
  9. * @author Morris Jobke <hey@morrisjobke.de>
  10. * @author Robin Appelman <icewind@owncloud.com>
  11. * @author Thomas Müller <thomas.mueller@tmit.eu>
  12. * @author Tom Needham <tom@owncloud.com>
  13. * @author Vincent Petry <pvince81@owncloud.com>
  14. *
  15. * @copyright Copyright (c) 2015, ownCloud, Inc.
  16. * @license AGPL-3.0
  17. *
  18. * This code is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License, version 3,
  20. * as published by the Free Software Foundation.
  21. *
  22. * This program is distributed in the hope that it will be useful,
  23. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. * GNU Affero General Public License for more details.
  26. *
  27. * You should have received a copy of the GNU Affero General Public License, version 3,
  28. * along with this program. If not, see <http://www.gnu.org/licenses/>
  29. *
  30. */
  31. class OC_API {
  32. /**
  33. * API authentication levels
  34. */
  35. /** @deprecated Use \OCP\API::GUEST_AUTH instead */
  36. const GUEST_AUTH = 0;
  37. /** @deprecated Use \OCP\API::USER_AUTH instead */
  38. const USER_AUTH = 1;
  39. /** @deprecated Use \OCP\API::SUBADMIN_AUTH instead */
  40. const SUBADMIN_AUTH = 2;
  41. /** @deprecated Use \OCP\API::ADMIN_AUTH instead */
  42. const ADMIN_AUTH = 3;
  43. /**
  44. * API Response Codes
  45. */
  46. /** @deprecated Use \OCP\API::RESPOND_UNAUTHORISED instead */
  47. const RESPOND_UNAUTHORISED = 997;
  48. /** @deprecated Use \OCP\API::RESPOND_SERVER_ERROR instead */
  49. const RESPOND_SERVER_ERROR = 996;
  50. /** @deprecated Use \OCP\API::RESPOND_NOT_FOUND instead */
  51. const RESPOND_NOT_FOUND = 998;
  52. /** @deprecated Use \OCP\API::RESPOND_UNKNOWN_ERROR instead */
  53. const RESPOND_UNKNOWN_ERROR = 999;
  54. /**
  55. * api actions
  56. */
  57. protected static $actions = array();
  58. private static $logoutRequired = false;
  59. private static $isLoggedIn = false;
  60. /**
  61. * registers an api call
  62. * @param string $method the http method
  63. * @param string $url the url to match
  64. * @param callable $action the function to run
  65. * @param string $app the id of the app registering the call
  66. * @param int $authLevel the level of authentication required for the call
  67. * @param array $defaults
  68. * @param array $requirements
  69. */
  70. public static function register($method, $url, $action, $app,
  71. $authLevel = \OCP\API::USER_AUTH,
  72. $defaults = array(),
  73. $requirements = array()) {
  74. $name = strtolower($method).$url;
  75. $name = str_replace(array('/', '{', '}'), '_', $name);
  76. if(!isset(self::$actions[$name])) {
  77. $oldCollection = OC::$server->getRouter()->getCurrentCollection();
  78. OC::$server->getRouter()->useCollection('ocs');
  79. OC::$server->getRouter()->create($name, $url)
  80. ->method($method)
  81. ->defaults($defaults)
  82. ->requirements($requirements)
  83. ->action('OC_API', 'call');
  84. self::$actions[$name] = array();
  85. OC::$server->getRouter()->useCollection($oldCollection);
  86. }
  87. self::$actions[$name][] = array('app' => $app, 'action' => $action, 'authlevel' => $authLevel);
  88. }
  89. /**
  90. * handles an api call
  91. * @param array $parameters
  92. */
  93. public static function call($parameters) {
  94. $request = \OC::$server->getRequest();
  95. $method = $request->getMethod();
  96. // Prepare the request variables
  97. if($method === 'PUT') {
  98. $parameters['_put'] = $request->getParams();
  99. } else if($method === 'DELETE') {
  100. $parameters['_delete'] = $request->getParams();
  101. }
  102. $name = $parameters['_route'];
  103. // Foreach registered action
  104. $responses = array();
  105. foreach(self::$actions[$name] as $action) {
  106. // Check authentication and availability
  107. if(!self::isAuthorised($action)) {
  108. $responses[] = array(
  109. 'app' => $action['app'],
  110. 'response' => new OC_OCS_Result(null, \OCP\API::RESPOND_UNAUTHORISED, 'Unauthorised'),
  111. 'shipped' => OC_App::isShipped($action['app']),
  112. );
  113. continue;
  114. }
  115. if(!is_callable($action['action'])) {
  116. $responses[] = array(
  117. 'app' => $action['app'],
  118. 'response' => new OC_OCS_Result(null, \OCP\API::RESPOND_NOT_FOUND, 'Api method not found'),
  119. 'shipped' => OC_App::isShipped($action['app']),
  120. );
  121. continue;
  122. }
  123. // Run the action
  124. $responses[] = array(
  125. 'app' => $action['app'],
  126. 'response' => call_user_func($action['action'], $parameters),
  127. 'shipped' => OC_App::isShipped($action['app']),
  128. );
  129. }
  130. $response = self::mergeResponses($responses);
  131. $format = self::requestedFormat();
  132. if (self::$logoutRequired) {
  133. OC_User::logout();
  134. }
  135. self::respond($response, $format);
  136. }
  137. /**
  138. * merge the returned result objects into one response
  139. * @param array $responses
  140. * @return array|\OC_OCS_Result
  141. */
  142. public static function mergeResponses($responses) {
  143. // Sort into shipped and third-party
  144. $shipped = array(
  145. 'succeeded' => array(),
  146. 'failed' => array(),
  147. );
  148. $thirdparty = array(
  149. 'succeeded' => array(),
  150. 'failed' => array(),
  151. );
  152. foreach($responses as $response) {
  153. if($response['shipped'] || ($response['app'] === 'core')) {
  154. if($response['response']->succeeded()) {
  155. $shipped['succeeded'][$response['app']] = $response;
  156. } else {
  157. $shipped['failed'][$response['app']] = $response;
  158. }
  159. } else {
  160. if($response['response']->succeeded()) {
  161. $thirdparty['succeeded'][$response['app']] = $response;
  162. } else {
  163. $thirdparty['failed'][$response['app']] = $response;
  164. }
  165. }
  166. }
  167. // Remove any error responses if there is one shipped response that succeeded
  168. if(!empty($shipped['failed'])) {
  169. // Which shipped response do we use if they all failed?
  170. // They may have failed for different reasons (different status codes)
  171. // Which response code should we return?
  172. // Maybe any that are not \OCP\API::RESPOND_SERVER_ERROR
  173. // Merge failed responses if more than one
  174. $data = array();
  175. foreach($shipped['failed'] as $failure) {
  176. $data = array_merge_recursive($data, $failure['response']->getData());
  177. }
  178. $picked = reset($shipped['failed']);
  179. $code = $picked['response']->getStatusCode();
  180. $meta = $picked['response']->getMeta();
  181. $response = new OC_OCS_Result($data, $code, $meta['message']);
  182. return $response;
  183. } elseif(!empty($shipped['succeeded'])) {
  184. $responses = array_merge($shipped['succeeded'], $thirdparty['succeeded']);
  185. } elseif(!empty($thirdparty['failed'])) {
  186. // Merge failed responses if more than one
  187. $data = array();
  188. foreach($thirdparty['failed'] as $failure) {
  189. $data = array_merge_recursive($data, $failure['response']->getData());
  190. }
  191. $picked = reset($thirdparty['failed']);
  192. $code = $picked['response']->getStatusCode();
  193. $meta = $picked['response']->getMeta();
  194. $response = new OC_OCS_Result($data, $code, $meta['message']);
  195. return $response;
  196. } else {
  197. $responses = $thirdparty['succeeded'];
  198. }
  199. // Merge the successful responses
  200. $data = array();
  201. foreach($responses as $response) {
  202. if($response['shipped']) {
  203. $data = array_merge_recursive($response['response']->getData(), $data);
  204. } else {
  205. $data = array_merge_recursive($data, $response['response']->getData());
  206. }
  207. $codes[] = array('code' => $response['response']->getStatusCode(),
  208. 'meta' => $response['response']->getMeta());
  209. }
  210. // Use any non 100 status codes
  211. $statusCode = 100;
  212. $statusMessage = null;
  213. foreach($codes as $code) {
  214. if($code['code'] != 100) {
  215. $statusCode = $code['code'];
  216. $statusMessage = $code['meta']['message'];
  217. break;
  218. }
  219. }
  220. $result = new OC_OCS_Result($data, $statusCode, $statusMessage);
  221. return $result;
  222. }
  223. /**
  224. * authenticate the api call
  225. * @param array $action the action details as supplied to OC_API::register()
  226. * @return bool
  227. */
  228. private static function isAuthorised($action) {
  229. $level = $action['authlevel'];
  230. switch($level) {
  231. case \OCP\API::GUEST_AUTH:
  232. // Anyone can access
  233. return true;
  234. break;
  235. case \OCP\API::USER_AUTH:
  236. // User required
  237. return self::loginUser();
  238. break;
  239. case \OCP\API::SUBADMIN_AUTH:
  240. // Check for subadmin
  241. $user = self::loginUser();
  242. if(!$user) {
  243. return false;
  244. } else {
  245. $subAdmin = OC_SubAdmin::isSubAdmin($user);
  246. $admin = OC_User::isAdminUser($user);
  247. if($subAdmin || $admin) {
  248. return true;
  249. } else {
  250. return false;
  251. }
  252. }
  253. break;
  254. case \OCP\API::ADMIN_AUTH:
  255. // Check for admin
  256. $user = self::loginUser();
  257. if(!$user) {
  258. return false;
  259. } else {
  260. return OC_User::isAdminUser($user);
  261. }
  262. break;
  263. default:
  264. // oops looks like invalid level supplied
  265. return false;
  266. break;
  267. }
  268. }
  269. /**
  270. * http basic auth
  271. * @return string|false (username, or false on failure)
  272. */
  273. private static function loginUser() {
  274. if(self::$isLoggedIn === true) {
  275. return \OC_User::getUser();
  276. }
  277. // reuse existing login
  278. $loggedIn = OC_User::isLoggedIn();
  279. if ($loggedIn === true) {
  280. $ocsApiRequest = isset($_SERVER['HTTP_OCS_APIREQUEST']) ? $_SERVER['HTTP_OCS_APIREQUEST'] === 'true' : false;
  281. if ($ocsApiRequest) {
  282. // initialize the user's filesystem
  283. \OC_Util::setUpFS(\OC_User::getUser());
  284. self::$isLoggedIn = true;
  285. return OC_User::getUser();
  286. }
  287. return false;
  288. }
  289. // basic auth - because OC_User::login will create a new session we shall only try to login
  290. // if user and pass are set
  291. if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) ) {
  292. $authUser = $_SERVER['PHP_AUTH_USER'];
  293. $authPw = $_SERVER['PHP_AUTH_PW'];
  294. $return = OC_User::login($authUser, $authPw);
  295. if ($return === true) {
  296. self::$logoutRequired = true;
  297. // initialize the user's filesystem
  298. \OC_Util::setUpFS(\OC_User::getUser());
  299. self::$isLoggedIn = true;
  300. return \OC_User::getUser();
  301. }
  302. }
  303. return false;
  304. }
  305. /**
  306. * respond to a call
  307. * @param OC_OCS_Result $result
  308. * @param string $format the format xml|json
  309. */
  310. public static function respond($result, $format='xml') {
  311. // Send 401 headers if unauthorised
  312. if($result->getStatusCode() === \OCP\API::RESPOND_UNAUTHORISED) {
  313. header('WWW-Authenticate: Basic realm="Authorisation Required"');
  314. header('HTTP/1.0 401 Unauthorized');
  315. }
  316. $response = array(
  317. 'ocs' => array(
  318. 'meta' => $result->getMeta(),
  319. 'data' => $result->getData(),
  320. ),
  321. );
  322. if ($format == 'json') {
  323. OC_JSON::encodedPrint($response);
  324. } else if ($format == 'xml') {
  325. header('Content-type: text/xml; charset=UTF-8');
  326. $writer = new XMLWriter();
  327. $writer->openMemory();
  328. $writer->setIndent( true );
  329. $writer->startDocument();
  330. self::toXML($response, $writer);
  331. $writer->endDocument();
  332. echo $writer->outputMemory(true);
  333. }
  334. }
  335. /**
  336. * @param XMLWriter $writer
  337. */
  338. private static function toXML($array, $writer) {
  339. foreach($array as $k => $v) {
  340. if ($k[0] === '@') {
  341. $writer->writeAttribute(substr($k, 1), $v);
  342. continue;
  343. } else if (is_numeric($k)) {
  344. $k = 'element';
  345. }
  346. if(is_array($v)) {
  347. $writer->startElement($k);
  348. self::toXML($v, $writer);
  349. $writer->endElement();
  350. } else {
  351. $writer->writeElement($k, $v);
  352. }
  353. }
  354. }
  355. /**
  356. * @return string
  357. */
  358. public static function requestedFormat() {
  359. $formats = array('json', 'xml');
  360. $format = !empty($_GET['format']) && in_array($_GET['format'], $formats) ? $_GET['format'] : 'xml';
  361. return $format;
  362. }
  363. /**
  364. * Based on the requested format the response content type is set
  365. */
  366. public static function setContentType() {
  367. $format = self::requestedFormat();
  368. if ($format === 'xml') {
  369. header('Content-type: text/xml; charset=UTF-8');
  370. return;
  371. }
  372. if ($format === 'json') {
  373. header('Content-Type: application/json; charset=utf-8');
  374. return;
  375. }
  376. header('Content-Type: application/octet-stream; charset=utf-8');
  377. }
  378. }