Completed
Push — master ( 24957e...d8c031 )
by Thomas
16:29 queued 07:32
created

OC_API::respond()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
nc 18
nop 2
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Bart Visscher <[email protected]>
4
 * @author Bernhard Posselt <[email protected]>
5
 * @author Björn Schießle <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Michael Gapczynski <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Thomas Müller <[email protected]>
15
 * @author Tom Needham <[email protected]>
16
 * @author Vincent Petry <[email protected]>
17
 *
18
 * @copyright Copyright (c) 2017, ownCloud GmbH
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
use OCP\API;
35
use OCP\AppFramework\Http;
36
37
/**
38
 * @author Bart Visscher <[email protected]>
39
 * @author Bernhard Posselt <[email protected]>
40
 * @author Björn Schießle <[email protected]>
41
 * @author Joas Schilling <[email protected]>
42
 * @author Jörn Friedrich Dreyer <[email protected]>
43
 * @author Lukas Reschke <[email protected]>
44
 * @author Michael Gapczynski <[email protected]>
45
 * @author Morris Jobke <[email protected]>
46
 * @author Robin Appelman <[email protected]>
47
 * @author Thomas Müller <[email protected]>
48
 * @author Tom Needham <[email protected]>
49
 * @author Vincent Petry <[email protected]>
50
 *
51
 * @copyright Copyright (c) 2015, ownCloud, Inc.
52
 * @license AGPL-3.0
53
 *
54
 * This code is free software: you can redistribute it and/or modify
55
 * it under the terms of the GNU Affero General Public License, version 3,
56
 * as published by the Free Software Foundation.
57
 *
58
 * This program is distributed in the hope that it will be useful,
59
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
60
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
61
 * GNU Affero General Public License for more details.
62
 *
63
 * You should have received a copy of the GNU Affero General Public License, version 3,
64
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
65
 *
66
 */
67
68
class OC_API {
69
70
	/**
71
	 * API authentication levels
72
	 */
73
74
	/** @deprecated Use \OCP\API::GUEST_AUTH instead */
75
	const GUEST_AUTH = 0;
76
77
	/** @deprecated Use \OCP\API::USER_AUTH instead */
78
	const USER_AUTH = 1;
79
80
	/** @deprecated Use \OCP\API::SUBADMIN_AUTH instead */
81
	const SUBADMIN_AUTH = 2;
82
83
	/** @deprecated Use \OCP\API::ADMIN_AUTH instead */
84
	const ADMIN_AUTH = 3;
85
86
	/**
87
	 * API Response Codes
88
	 */
89
90
	/** @deprecated Use \OCP\API::RESPOND_UNAUTHORISED instead */
91
	const RESPOND_UNAUTHORISED = 997;
92
93
	/** @deprecated Use \OCP\API::RESPOND_SERVER_ERROR instead */
94
	const RESPOND_SERVER_ERROR = 996;
95
96
	/** @deprecated Use \OCP\API::RESPOND_NOT_FOUND instead */
97
	const RESPOND_NOT_FOUND = 998;
98
99
	/** @deprecated Use \OCP\API::RESPOND_UNKNOWN_ERROR instead */
100
	const RESPOND_UNKNOWN_ERROR = 999;
101
102
	/**
103
	 * api actions
104
	 */
105
	protected static $actions = [];
106
	private static $logoutRequired = false;
107
	private static $isLoggedIn = false;
108
109
	/**
110
	 * registers an api call
111
	 * @param string $method the http method
112
	 * @param string $url the url to match
113
	 * @param callable $action the function to run
114
	 * @param string $app the id of the app registering the call
115
	 * @param int $authLevel the level of authentication required for the call
116
	 * @param array $defaults
117
	 * @param array $requirements
118
	 * @param boolean $cors whether to enable cors for this route
119
	 */
120
	public static function register($method, $url, $action, $app,
121
				$authLevel = API::USER_AUTH,
122
				$defaults = [],
123
				$requirements = [], $cors = true) {
124
		$name = strtolower($method).$url;
125
		$name = str_replace(['/', '{', '}'], '_', $name);
126
		if(!isset(self::$actions[$name])) {
127
			$oldCollection = OC::$server->getRouter()->getCurrentCollection();
128
			OC::$server->getRouter()->useCollection('ocs');
129
			OC::$server->getRouter()->create($name, $url)
130
				->method($method)
131
				->defaults($defaults)
132
				->requirements($requirements)
133
				->action('OC_API', 'call');
134
			self::$actions[$name] = [];
135
			OC::$server->getRouter()->useCollection($oldCollection);
136
		}
137
		self::$actions[$name][] = ['app' => $app, 'action' => $action, 'authlevel' => $authLevel, 'cors' => $cors];
138
	}
139
140
	/**
141
	 * handles an api call
142
	 * @param array $parameters
143
	 */
144
	public static function call($parameters) {
145
		$request = \OC::$server->getRequest();
146
		$method = $request->getMethod();
147
148
		// Prepare the request variables
149
		if($method === 'PUT') {
150
			$parameters['_put'] = $request->getParams();
151
		} else if($method === 'DELETE') {
152
			$parameters['_delete'] = $request->getParams();
153
		}
154
		$name = $parameters['_route'];
155
		// Foreach registered action
156
		$responses = [];
157
		foreach(self::$actions[$name] as $action) {
158
			// Check authentication and availability
159 View Code Duplication
			if(!self::isAuthorised($action)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
160
				$responses[] = [
161
					'app' => $action['app'],
162
					'response' => new \OC\OCS\Result(null, API::RESPOND_UNAUTHORISED, 'Unauthorised'),
163
					'shipped' => OC_App::isShipped($action['app']),
164
				];
165
				continue;
166
			}
167 View Code Duplication
			if(!is_callable($action['action'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
168
				$responses[] = [
169
					'app' => $action['app'],
170
					'response' => new \OC\OCS\Result(null, API::RESPOND_NOT_FOUND, 'Api method not found'),
171
					'shipped' => OC_App::isShipped($action['app']),
172
				];
173
				continue;
174
			}
175
			// Run the action
176
			$responses[] = [
177
				'app' => $action['app'],
178
				'response' => call_user_func($action['action'], $parameters),
179
				'shipped' => OC_App::isShipped($action['app']),
180
			];
181
		}
182
		$response = self::mergeResponses($responses);
183
184
		// If CORS is set to active for some method, try to add CORS headers
185
		if (self::$actions[$name][0]['cors'] &&
186
			!is_null(\OC::$server->getUserSession()->getUser()) &&
187
			!is_null(\OC::$server->getRequest()->getHeader('Origin'))) {
188
			$requesterDomain = \OC::$server->getRequest()->getHeader('Origin');
189
			$userId = \OC::$server->getUserSession()->getUser()->getUID();
190
			$response = \OC_Response::setCorsHeaders($userId, $requesterDomain, $response);
0 ignored issues
show
Documentation introduced by
$response is of type object<OC\OCS\Result>, but the function expects a object<Sabre\HTTP\ResponseInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
191
		}
192
193
		$format = self::requestedFormat();
194
		if (self::$logoutRequired) {
195
			\OC::$server->getUserSession()->logout();
196
		}
197
198
		self::respond($response, $format);
0 ignored issues
show
Bug introduced by
It seems like $response defined by \OC_Response::setCorsHea...esterDomain, $response) on line 190 can also be of type object<Sabre\HTTP\ResponseInterface>; however, OC_API::respond() does only seem to accept object<OC\OCS\Result>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
199
	}
200
201
	/**
202
	 * merge the returned result objects into one response
203
	 * @param array $responses
204
	 * @return \OC\OCS\Result
205
	 */
206
	public static function mergeResponses($responses) {
207
		// Sort into shipped and third-party
208
		$shipped = [
209
			'succeeded' => [],
210
			'failed' => [],
211
		];
212
		$thirdparty = [
213
			'succeeded' => [],
214
			'failed' => [],
215
		];
216
217
		foreach($responses as $response) {
218
			if($response['shipped'] || ($response['app'] === 'core')) {
219 View Code Duplication
				if($response['response']->succeeded()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
					$shipped['succeeded'][$response['app']] = $response;
221
				} else {
222
					$shipped['failed'][$response['app']] = $response;
223
				}
224 View Code Duplication
			} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
225
				if($response['response']->succeeded()) {
226
					$thirdparty['succeeded'][$response['app']] = $response;
227
				} else {
228
					$thirdparty['failed'][$response['app']] = $response;
229
				}
230
			}
231
		}
232
233
		// Remove any error responses if there is one shipped response that succeeded
234
		if(!empty($shipped['failed'])) {
235
			// Which shipped response do we use if they all failed?
236
			// They may have failed for different reasons (different status codes)
237
			// Which response code should we return?
238
			// Maybe any that are not \OCP\API::RESPOND_SERVER_ERROR
239
			// Merge failed responses if more than one
240
			$data = [];
241
			foreach($shipped['failed'] as $failure) {
242
				$data = array_merge_recursive($data, $failure['response']->getData());
243
			}
244
			$picked = reset($shipped['failed']);
245
			$code = $picked['response']->getStatusCode();
246
			$meta = $picked['response']->getMeta();
247
			$headers = $picked['response']->getHeaders();
248
			$response = new \OC\OCS\Result($data, $code, $meta['message'], $headers);
249
			return $response;
250
		} elseif(!empty($shipped['succeeded'])) {
251
			$responses = array_merge($shipped['succeeded'], $thirdparty['succeeded']);
252
		} elseif(!empty($thirdparty['failed'])) {
253
			// Merge failed responses if more than one
254
			$data = [];
255
			foreach($thirdparty['failed'] as $failure) {
256
				$data = array_merge_recursive($data, $failure['response']->getData());
257
			}
258
			$picked = reset($thirdparty['failed']);
259
			$code = $picked['response']->getStatusCode();
260
			$meta = $picked['response']->getMeta();
261
			$headers = $picked['response']->getHeaders();
262
			$response = new \OC\OCS\Result($data, $code, $meta['message'], $headers);
263
			return $response;
264
		} else {
265
			$responses = $thirdparty['succeeded'];
266
		}
267
		// Merge the successful responses
268
		$data = [];
269
		$codes = [];
270
		$header = [];
271
272
		foreach($responses as $response) {
273
			if($response['shipped']) {
274
				$data = array_merge_recursive($response['response']->getData(), $data);
275
			} else {
276
				$data = array_merge_recursive($data, $response['response']->getData());
277
			}
278
			$header = array_merge_recursive($header, $response['response']->getHeaders());
279
			$codes[] = ['code' => $response['response']->getStatusCode(),
280
				'meta' => $response['response']->getMeta()];
281
		}
282
283
		// Use any non 100 status codes
284
		$statusCode = 100;
285
		$statusMessage = null;
286
		foreach($codes as $code) {
287
			if($code['code'] != 100) {
288
				$statusCode = $code['code'];
289
				$statusMessage = $code['meta']['message'];
290
				break;
291
			}
292
		}
293
294
		return new \OC\OCS\Result($data, $statusCode, $statusMessage, $header);
295
	}
296
297
	/**
298
	 * authenticate the api call
299
	 * @param array $action the action details as supplied to OC_API::register()
300
	 * @return bool
301
	 */
302
	private static function isAuthorised($action) {
303
		$level = $action['authlevel'];
304
		switch($level) {
305
			case API::GUEST_AUTH:
306
				// Anyone can access
307
				return true;
308
			case API::USER_AUTH:
309
				// User required
310
				return self::loginUser();
311
			case API::SUBADMIN_AUTH:
312
				// Check for subadmin
313
				$user = self::loginUser();
314
				if(!$user) {
315
					return false;
316
				} else {
317
					$userObject = \OC::$server->getUserSession()->getUser();
318
					if($userObject === null) {
319
						return false;
320
					}
321
					$isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
322
					$admin = OC_User::isAdminUser($user);
323
					if($isSubAdmin || $admin) {
324
						return true;
325
					} else {
326
						return false;
327
					}
328
				}
329
			case API::ADMIN_AUTH:
330
				// Check for admin
331
				$user = self::loginUser();
332
				if(!$user) {
333
					return false;
334
				} else {
335
					return OC_User::isAdminUser($user);
336
				}
337
			default:
338
				// oops looks like invalid level supplied
339
				return false;
340
		}
341
	}
342
343
	/**
344
	 * http basic auth
345
	 * @return string|false (username, or false on failure)
346
	 */
347
	private static function loginUser() {
348
		if(self::$isLoggedIn === true) {
349
			return \OC_User::getUser();
350
		}
351
352
		// reuse existing login
353
		$loggedIn = \OC::$server->getUserSession()->isLoggedIn();
354
		if ($loggedIn === true) {
355
			if (\OC::$server->getTwoFactorAuthManager()->needsSecondFactor()) {
356
				// Do not allow access to OCS until the 2FA challenge was solved successfully
357
				return false;
358
			}
359
			$ocsApiRequest = isset($_SERVER['HTTP_OCS_APIREQUEST']) ? $_SERVER['HTTP_OCS_APIREQUEST'] === 'true' : false;
360
			if ($ocsApiRequest) {
361
362
				// initialize the user's filesystem
363
				\OC_Util::setupFS(\OC_User::getUser());
364
				self::$isLoggedIn = true;
365
366
				return OC_User::getUser();
367
			}
368
			return false;
369
		}
370
371
		// basic auth - because OC_User::login will create a new session we shall only try to login
372
		// if user and pass are set
373
		$userSession = \OC::$server->getUserSession();
374
		$request = \OC::$server->getRequest();
375
		try {
376
			if (OC_User::handleApacheAuth()) {
377
				self::$logoutRequired = false;
378
			} else if ($userSession->tryTokenLogin($request)
379
				|| $userSession->tryAuthModuleLogin($request)
380
				|| $userSession->tryBasicAuthLogin($request)) {
381
				self::$logoutRequired = true;
382
			} else {
383
				return false;
384
			}
385
			// initialize the user's filesystem
386
			\OC_Util::setupFS(\OC_User::getUser());
387
			self::$isLoggedIn = true;
388
389
			return \OC_User::getUser();
390
		} catch (\OC\User\LoginException $e) {
391
			return false;
392
		}
393
	}
394
395
	/**
396
	 * respond to a call
397
	 * @param \OC\OCS\Result $result
398
	 * @param string $format the format xml|json
399
	 */
400
	public static function respond($result, $format='xml') {
401
		$request = \OC::$server->getRequest();
402
403
		// Send 401 headers if unauthorised
404
		if($result->getStatusCode() === API::RESPOND_UNAUTHORISED) {
405
			// If request comes from JS return dummy auth request
406
			if($request->getHeader('X-Requested-With') === 'XMLHttpRequest') {
407
				header('WWW-Authenticate: DummyBasic realm="Authorisation Required"');
408
			} else {
409
				header('WWW-Authenticate: Basic realm="Authorisation Required"');
410
			}
411
			header('HTTP/1.0 401 Unauthorized');
412
		}
413
414
		foreach($result->getHeaders() as $name => $value) {
415
			header($name . ': ' . $value);
416
		}
417
418
		$meta = $result->getMeta();
419
		$data = $result->getData();
420
		if (self::isV2($request)) {
421
			$statusCode = self::mapStatusCodes($result->getStatusCode());
422
			if (!is_null($statusCode)) {
423
				$meta['statuscode'] = $statusCode;
424
				OC_Response::setStatus($statusCode);
425
			}
426
		}
427
428
		self::setContentType($format);
429
		$body = self::renderResult($format, $meta, $data);
430
		echo $body;
431
	}
432
433
	/**
434
	 * @param XMLWriter $writer
435
	 */
436
	private static function toXML($array, $writer) {
437
		foreach($array as $k => $v) {
438
			if ($k[0] === '@') {
439
				if (is_array($v)) {
440
					foreach ($v as $name => $value) {
441
						$writer->writeAttribute($name, $value);
442
					}
443
				} else {
444
					$writer->writeAttribute(substr($k, 1), $v);
445
				}
446
				continue;
447
			} else if (is_numeric($k)) {
448
				$k = 'element';
449
			}
450
			if(is_array($v)) {
451
				$writer->startElement($k);
452
				self::toXML($v, $writer);
453
				$writer->endElement();
454
			} else {
455
				$writer->writeElement($k, $v);
456
			}
457
		}
458
	}
459
460
	/**
461
	 * @return string
462
	 */
463
	public static function requestedFormat() {
464
		$formats = ['json', 'xml'];
465
466
		$format = !empty($_GET['format']) && in_array($_GET['format'], $formats) ? $_GET['format'] : 'xml';
467
		return $format;
468
	}
469
470
	/**
471
	 * Based on the requested format the response content type is set
472
	 * @param string $format
473
	 */
474
	public static function setContentType($format = null) {
475
		$format = is_null($format) ? self::requestedFormat() : $format;
476
		if ($format === 'xml') {
477
			header('Content-type: text/xml; charset=UTF-8');
478
			return;
479
		}
480
481
		if ($format === 'json') {
482
			header('Content-Type: application/json; charset=utf-8');
483
			return;
484
		}
485
486
		header('Content-Type: application/octet-stream; charset=utf-8');
487
	}
488
489
	/**
490
	 * @param \OCP\IRequest $request
491
	 * @return bool
492
	 */
493
	protected static function isV2(\OCP\IRequest $request) {
494
		$script = $request->getScriptName();
495
496
		return substr($script, -11) === '/ocs/v2.php';
497
	}
498
499
	/**
500
	 * @param integer $sc
501
	 * @return int
502
	 */
503
	public static function mapStatusCodes($sc) {
504
		switch ($sc) {
505
			case API::RESPOND_NOT_FOUND:
506
				return Http::STATUS_NOT_FOUND;
507
			case API::RESPOND_SERVER_ERROR:
508
				return Http::STATUS_INTERNAL_SERVER_ERROR;
509
			case API::RESPOND_UNKNOWN_ERROR:
510
				return Http::STATUS_INTERNAL_SERVER_ERROR;
511
			case API::RESPOND_UNAUTHORISED:
512
				// already handled for v1
513
				return null;
514
			case 100:
515
				return Http::STATUS_OK;
516
		}
517
		// any 2xx, 4xx and 5xx will be used as is
518
		if ($sc >= 200 && $sc < 600) {
519
			return $sc;
520
		}
521
522
		return Http::STATUS_BAD_REQUEST;
523
	}
524
525
	/**
526
	 * @param string $format
527
	 * @return string
528
	 */
529
	public static function renderResult($format, $meta, $data) {
530
		$response = [
531
			'ocs' => [
532
				'meta' => $meta,
533
				'data' => $data,
534
			],
535
		];
536
		if ($format == 'json') {
537
			return OC_JSON::encode($response);
538
		}
539
540
		$writer = new XMLWriter();
541
		$writer->openMemory();
542
		$writer->setIndent(true);
543
		$writer->startDocument();
544
		self::toXML($response, $writer);
545
		$writer->endDocument();
546
		return $writer->outputMemory(true);
547
	}
548
549
	/**
550
	 * Called when a not existing OCS endpoint has been called
551
	 */
552
	public static function notFound() {
553
		$format = \OC::$server->getRequest()->getParam('format', 'xml');
554
		$txt='Invalid query, please check the syntax. API specifications are here:'
555
			.' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services. DEBUG OUTPUT:'."\n";
556
		OC_API::respond(new \OC\OCS\Result(null, API::RESPOND_UNKNOWN_ERROR, $txt), $format);
557
	}
558
}
559