Completed
Push — stable9 ( 485cb1...e094cf )
by Lukas
26:41 queued 26:23
created

OC_API::call()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 46
Code Lines 31

Duplication

Lines 16
Ratio 34.78 %

Importance

Changes 0
Metric Value
cc 7
eloc 31
nc 24
nop 1
dl 16
loc 46
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bernhard Posselt <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Jörn Friedrich Dreyer <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author Michael Gapczynski <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Tom Needham <[email protected]>
16
 * @author Vincent Petry <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
use OCP\API;
34
use OCP\AppFramework\Http;
35
36
/**
37
 * @author Bart Visscher <[email protected]>
38
 * @author Bernhard Posselt <[email protected]>
39
 * @author Björn Schießle <[email protected]>
40
 * @author Joas Schilling <[email protected]>
41
 * @author Jörn Friedrich Dreyer <[email protected]>
42
 * @author Lukas Reschke <[email protected]>
43
 * @author Michael Gapczynski <[email protected]>
44
 * @author Morris Jobke <[email protected]>
45
 * @author Robin Appelman <[email protected]>
46
 * @author Thomas Müller <[email protected]>
47
 * @author Tom Needham <[email protected]>
48
 * @author Vincent Petry <[email protected]>
49
 *
50
 * @copyright Copyright (c) 2015, ownCloud, Inc.
51
 * @license AGPL-3.0
52
 *
53
 * This code is free software: you can redistribute it and/or modify
54
 * it under the terms of the GNU Affero General Public License, version 3,
55
 * as published by the Free Software Foundation.
56
 *
57
 * This program is distributed in the hope that it will be useful,
58
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
59
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
60
 * GNU Affero General Public License for more details.
61
 *
62
 * You should have received a copy of the GNU Affero General Public License, version 3,
63
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
64
 *
65
 */
66
67
class OC_API {
68
69
	/**
70
	 * API authentication levels
71
	 */
72
73
	/** @deprecated Use \OCP\API::GUEST_AUTH instead */
74
	const GUEST_AUTH = 0;
75
76
	/** @deprecated Use \OCP\API::USER_AUTH instead */
77
	const USER_AUTH = 1;
78
79
	/** @deprecated Use \OCP\API::SUBADMIN_AUTH instead */
80
	const SUBADMIN_AUTH = 2;
81
82
	/** @deprecated Use \OCP\API::ADMIN_AUTH instead */
83
	const ADMIN_AUTH = 3;
84
85
	/**
86
	 * API Response Codes
87
	 */
88
89
	/** @deprecated Use \OCP\API::RESPOND_UNAUTHORISED instead */
90
	const RESPOND_UNAUTHORISED = 997;
91
92
	/** @deprecated Use \OCP\API::RESPOND_SERVER_ERROR instead */
93
	const RESPOND_SERVER_ERROR = 996;
94
95
	/** @deprecated Use \OCP\API::RESPOND_NOT_FOUND instead */
96
	const RESPOND_NOT_FOUND = 998;
97
98
	/** @deprecated Use \OCP\API::RESPOND_UNKNOWN_ERROR instead */
99
	const RESPOND_UNKNOWN_ERROR = 999;
100
101
	/**
102
	 * api actions
103
	 */
104
	protected static $actions = array();
105
	private static $logoutRequired = false;
106
	private static $isLoggedIn = false;
107
108
	/**
109
	 * registers an api call
110
	 * @param string $method the http method
111
	 * @param string $url the url to match
112
	 * @param callable $action the function to run
113
	 * @param string $app the id of the app registering the call
114
	 * @param int $authLevel the level of authentication required for the call
115
	 * @param array $defaults
116
	 * @param array $requirements
117
	 */
118
	public static function register($method, $url, $action, $app,
119
				$authLevel = API::USER_AUTH,
120
				$defaults = array(),
121
				$requirements = array()) {
122
		$name = strtolower($method).$url;
123
		$name = str_replace(array('/', '{', '}'), '_', $name);
124
		if(!isset(self::$actions[$name])) {
125
			$oldCollection = OC::$server->getRouter()->getCurrentCollection();
0 ignored issues
show
Deprecated Code introduced by
The method OCP\Route\IRouter::getCurrentCollection() has been deprecated with message: 9.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
126
			OC::$server->getRouter()->useCollection('ocs');
0 ignored issues
show
Deprecated Code introduced by
The method OCP\Route\IRouter::useCollection() has been deprecated with message: 9.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
127
			OC::$server->getRouter()->create($name, $url)
0 ignored issues
show
Deprecated Code introduced by
The method OCP\Route\IRouter::create() has been deprecated with message: 9.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
128
				->method($method)
129
				->defaults($defaults)
130
				->requirements($requirements)
131
				->action('OC_API', 'call');
132
			self::$actions[$name] = array();
133
			OC::$server->getRouter()->useCollection($oldCollection);
0 ignored issues
show
Deprecated Code introduced by
The method OCP\Route\IRouter::useCollection() has been deprecated with message: 9.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
134
		}
135
		self::$actions[$name][] = array('app' => $app, 'action' => $action, 'authlevel' => $authLevel);
136
	}
137
138
	/**
139
	 * handles an api call
140
	 * @param array $parameters
141
	 */
142
	public static function call($parameters) {
143
		$request = \OC::$server->getRequest();
144
		$method = $request->getMethod();
145
146
		// Prepare the request variables
147
		if($method === 'PUT') {
148
			$parameters['_put'] = $request->getParams();
149
		} else if($method === 'DELETE') {
150
			$parameters['_delete'] = $request->getParams();
151
		}
152
		$name = $parameters['_route'];
153
		// Foreach registered action
154
		$responses = array();
155
		foreach(self::$actions[$name] as $action) {
156
			// Check authentication and availability
157 View Code Duplication
			if(!self::isAuthorised($action)) {
158
				$responses[] = array(
159
					'app' => $action['app'],
160
					'response' => new OC_OCS_Result(null, API::RESPOND_UNAUTHORISED, 'Unauthorised'),
161
					'shipped' => OC_App::isShipped($action['app']),
162
					);
163
				continue;
164
			}
165 View Code Duplication
			if(!is_callable($action['action'])) {
166
				$responses[] = array(
167
					'app' => $action['app'],
168
					'response' => new OC_OCS_Result(null, API::RESPOND_NOT_FOUND, 'Api method not found'),
169
					'shipped' => OC_App::isShipped($action['app']),
170
					);
171
				continue;
172
			}
173
			// Run the action
174
			$responses[] = array(
175
				'app' => $action['app'],
176
				'response' => call_user_func($action['action'], $parameters),
177
				'shipped' => OC_App::isShipped($action['app']),
178
				);
179
		}
180
		$response = self::mergeResponses($responses);
181
		$format = self::requestedFormat();
182
		if (self::$logoutRequired) {
183
			OC_User::logout();
184
		}
185
186
		self::respond($response, $format);
187
	}
188
189
	/**
190
	 * merge the returned result objects into one response
191
	 * @param array $responses
192
	 * @return OC_OCS_Result
193
	 */
194
	public static function mergeResponses($responses) {
195
		// Sort into shipped and third-party
196
		$shipped = array(
197
			'succeeded' => array(),
198
			'failed' => array(),
199
			);
200
		$thirdparty = array(
201
			'succeeded' => array(),
202
			'failed' => array(),
203
			);
204
205
		foreach($responses as $response) {
206
			if($response['shipped'] || ($response['app'] === 'core')) {
207 View Code Duplication
				if($response['response']->succeeded()) {
208
					$shipped['succeeded'][$response['app']] = $response;
209
				} else {
210
					$shipped['failed'][$response['app']] = $response;
211
				}
212 View Code Duplication
			} else {
213
				if($response['response']->succeeded()) {
214
					$thirdparty['succeeded'][$response['app']] = $response;
215
				} else {
216
					$thirdparty['failed'][$response['app']] = $response;
217
				}
218
			}
219
		}
220
221
		// Remove any error responses if there is one shipped response that succeeded
222
		if(!empty($shipped['failed'])) {
223
			// Which shipped response do we use if they all failed?
224
			// They may have failed for different reasons (different status codes)
225
			// Which response code should we return?
226
			// Maybe any that are not \OCP\API::RESPOND_SERVER_ERROR
227
			// Merge failed responses if more than one
228
			$data = array();
229
			foreach($shipped['failed'] as $failure) {
230
				$data = array_merge_recursive($data, $failure['response']->getData());
231
			}
232
			$picked = reset($shipped['failed']);
233
			$code = $picked['response']->getStatusCode();
234
			$meta = $picked['response']->getMeta();
235
			$headers = $picked['response']->getHeaders();
236
			$response = new OC_OCS_Result($data, $code, $meta['message'], $headers);
237
			return $response;
238
		} elseif(!empty($shipped['succeeded'])) {
239
			$responses = array_merge($shipped['succeeded'], $thirdparty['succeeded']);
240
		} elseif(!empty($thirdparty['failed'])) {
241
			// Merge failed responses if more than one
242
			$data = array();
243
			foreach($thirdparty['failed'] as $failure) {
244
				$data = array_merge_recursive($data, $failure['response']->getData());
245
			}
246
			$picked = reset($thirdparty['failed']);
247
			$code = $picked['response']->getStatusCode();
248
			$meta = $picked['response']->getMeta();
249
			$headers = $picked['response']->getHeaders();
250
			$response = new OC_OCS_Result($data, $code, $meta['message'], $headers);
251
			return $response;
252
		} else {
253
			$responses = $thirdparty['succeeded'];
254
		}
255
		// Merge the successful responses
256
		$data = [];
257
		$codes = [];
258
		$header = [];
259
260
		foreach($responses as $response) {
261
			if($response['shipped']) {
262
				$data = array_merge_recursive($response['response']->getData(), $data);
263
			} else {
264
				$data = array_merge_recursive($data, $response['response']->getData());
265
			}
266
			$header = array_merge_recursive($header, $response['response']->getHeaders());
267
			$codes[] = ['code' => $response['response']->getStatusCode(),
268
				'meta' => $response['response']->getMeta()];
269
		}
270
271
		// Use any non 100 status codes
272
		$statusCode = 100;
273
		$statusMessage = null;
274
		foreach($codes as $code) {
275
			if($code['code'] != 100) {
276
				$statusCode = $code['code'];
277
				$statusMessage = $code['meta']['message'];
278
				break;
279
			}
280
		}
281
282
		return new OC_OCS_Result($data, $statusCode, $statusMessage, $header);
283
	}
284
285
	/**
286
	 * authenticate the api call
287
	 * @param array $action the action details as supplied to OC_API::register()
288
	 * @return bool
289
	 */
290
	private static function isAuthorised($action) {
291
		$level = $action['authlevel'];
292
		switch($level) {
293
			case API::GUEST_AUTH:
294
				// Anyone can access
295
				return true;
296
			case API::USER_AUTH:
297
				// User required
298
				return self::loginUser();
299
			case API::SUBADMIN_AUTH:
300
				// Check for subadmin
301
				$user = self::loginUser();
302
				if(!$user) {
303
					return false;
304
				} else {
305
					$userObject = \OC::$server->getUserSession()->getUser();
306
					if($userObject === null) {
307
						return false;
308
					}
309
					$isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
310
					$admin = OC_User::isAdminUser($user);
311
					if($isSubAdmin || $admin) {
312
						return true;
313
					} else {
314
						return false;
315
					}
316
				}
317
			case API::ADMIN_AUTH:
318
				// Check for admin
319
				$user = self::loginUser();
320
				if(!$user) {
321
					return false;
322
				} else {
323
					return OC_User::isAdminUser($user);
324
				}
325
			default:
326
				// oops looks like invalid level supplied
327
				return false;
328
		}
329
	}
330
331
	/**
332
	 * http basic auth
333
	 * @return string|false (username, or false on failure)
334
	 */
335
	private static function loginUser() {
336
		if(self::$isLoggedIn === true) {
337
			return \OC_User::getUser();
338
		}
339
340
		// reuse existing login
341
		$loggedIn = OC_User::isLoggedIn();
342
		if ($loggedIn === true) {
343
			$ocsApiRequest = isset($_SERVER['HTTP_OCS_APIREQUEST']) ? $_SERVER['HTTP_OCS_APIREQUEST'] === 'true' : false;
344
			if ($ocsApiRequest) {
345
346
				// initialize the user's filesystem
347
				\OC_Util::setUpFS(\OC_User::getUser());
348
				self::$isLoggedIn = true;
349
350
				return OC_User::getUser();
351
			}
352
			return false;
353
		}
354
355
		// basic auth - because OC_User::login will create a new session we shall only try to login
356
		// if user and pass are set
357
		if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) ) {
358
			$authUser = $_SERVER['PHP_AUTH_USER'];
359
			$authPw = $_SERVER['PHP_AUTH_PW'];
360
			$return = OC_User::login($authUser, $authPw);
361
			if ($return === true) {
362
				self::$logoutRequired = true;
363
364
				// initialize the user's filesystem
365
				\OC_Util::setUpFS(\OC_User::getUser());
366
				self::$isLoggedIn = true;
367
368
				/**
369
				 * Add DAV authenticated. This should in an ideal world not be
370
				 * necessary but the iOS App reads cookies from anywhere instead
371
				 * only the DAV endpoint.
372
				 * This makes sure that the cookies will be valid for the whole scope
373
				 * @see https://github.com/owncloud/core/issues/22893
374
				 */
375
				\OC::$server->getSession()->set(
376
					\OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED,
377
					\OC::$server->getUserSession()->getUser()->getUID()
378
				);
379
380
				return \OC_User::getUser();
381
			}
382
		}
383
384
		return false;
385
	}
386
387
	/**
388
	 * respond to a call
389
	 * @param OC_OCS_Result $result
390
	 * @param string $format the format xml|json
391
	 */
392
	public static function respond($result, $format='xml') {
393
		$request = \OC::$server->getRequest();
394
395
		// Send 401 headers if unauthorised
396
		if($result->getStatusCode() === API::RESPOND_UNAUTHORISED) {
397
			// If request comes from JS return dummy auth request
398
			if($request->getHeader('X-Requested-With') === 'XMLHttpRequest') {
399
				header('WWW-Authenticate: DummyBasic realm="Authorisation Required"');
400
			} else {
401
				header('WWW-Authenticate: Basic realm="Authorisation Required"');
402
			}
403
			header('HTTP/1.0 401 Unauthorized');
404
		}
405
406
		foreach($result->getHeaders() as $name => $value) {
407
			header($name . ': ' . $value);
408
		}
409
410
		$meta = $result->getMeta();
411
		$data = $result->getData();
412
		if (self::isV2($request)) {
413
			$statusCode = self::mapStatusCodes($result->getStatusCode());
414
			if (!is_null($statusCode)) {
415
				$meta['statuscode'] = $statusCode;
416
				OC_Response::setStatus($statusCode);
417
			}
418
		}
419
420
		self::setContentType($format);
421
		$body = self::renderResult($format, $meta, $data);
422
		echo $body;
423
	}
424
425
	/**
426
	 * @param XMLWriter $writer
427
	 */
428
	private static function toXML($array, $writer) {
429
		foreach($array as $k => $v) {
430
			if ($k[0] === '@') {
431
				$writer->writeAttribute(substr($k, 1), $v);
432
				continue;
433
			} else if (is_numeric($k)) {
434
				$k = 'element';
435
			}
436
			if(is_array($v)) {
437
				$writer->startElement($k);
438
				self::toXML($v, $writer);
439
				$writer->endElement();
440
			} else {
441
				$writer->writeElement($k, $v);
442
			}
443
		}
444
	}
445
446
	/**
447
	 * @return string
448
	 */
449
	public static function requestedFormat() {
450
		$formats = array('json', 'xml');
451
452
		$format = !empty($_GET['format']) && in_array($_GET['format'], $formats) ? $_GET['format'] : 'xml';
453
		return $format;
454
	}
455
456
	/**
457
	 * Based on the requested format the response content type is set
458
	 * @param string $format
459
	 */
460
	public static function setContentType($format = null) {
461
		$format = is_null($format) ? self::requestedFormat() : $format;
462
		if ($format === 'xml') {
463
			header('Content-type: text/xml; charset=UTF-8');
464
			return;
465
		}
466
467
		if ($format === 'json') {
468
			header('Content-Type: application/json; charset=utf-8');
469
			return;
470
		}
471
472
		header('Content-Type: application/octet-stream; charset=utf-8');
473
	}
474
475
	/**
476
	 * @param \OCP\IRequest $request
477
	 * @return bool
478
	 */
479
	protected static function isV2(\OCP\IRequest $request) {
480
		$script = $request->getScriptName();
481
482
		return substr($script, -11) === '/ocs/v2.php';
483
	}
484
485
	/**
486
	 * @param integer $sc
487
	 * @return int
488
	 */
489
	public static function mapStatusCodes($sc) {
490
		switch ($sc) {
491
			case API::RESPOND_NOT_FOUND:
492
				return Http::STATUS_NOT_FOUND;
493
			case API::RESPOND_SERVER_ERROR:
494
				return Http::STATUS_INTERNAL_SERVER_ERROR;
495
			case API::RESPOND_UNKNOWN_ERROR:
496
				return Http::STATUS_INTERNAL_SERVER_ERROR;
497
			case API::RESPOND_UNAUTHORISED:
498
				// already handled for v1
499
				return null;
500
			case 100:
501
				return Http::STATUS_OK;
502
		}
503
		// any 2xx, 4xx and 5xx will be used as is
504
		if ($sc >= 200 && $sc < 600) {
505
			return $sc;
506
		}
507
508
		return Http::STATUS_BAD_REQUEST;
509
	}
510
511
	/**
512
	 * @param string $format
513
	 * @return string
514
	 */
515
	public static function renderResult($format, $meta, $data) {
516
		$response = array(
517
			'ocs' => array(
518
				'meta' => $meta,
519
				'data' => $data,
520
			),
521
		);
522
		if ($format == 'json') {
523
			return OC_JSON::encode($response);
0 ignored issues
show
Deprecated Code introduced by
The method OC_JSON::encode() has been deprecated with message: Use a AppFramework JSONResponse instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
524
		}
525
526
		$writer = new XMLWriter();
527
		$writer->openMemory();
528
		$writer->setIndent(true);
529
		$writer->startDocument();
530
		self::toXML($response, $writer);
531
		$writer->endDocument();
532
		return $writer->outputMemory(true);
533
	}
534
}
535