diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 4bd8f96454e25..6d0cb11791c52 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1714,11 +1714,10 @@ 'OC\\OCM\\Model\\OCMProvider' => $baseDir . '/lib/private/OCM/Model/OCMProvider.php', 'OC\\OCM\\Model\\OCMResource' => $baseDir . '/lib/private/OCM/Model/OCMResource.php', 'OC\\OCM\\OCMDiscoveryService' => $baseDir . '/lib/private/OCM/OCMDiscoveryService.php', + 'OC\\OCS\\ApiHelper' => $baseDir . '/lib/private/OCS/ApiHelper.php', 'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\DiscoveryService' => $baseDir . '/lib/private/OCS/DiscoveryService.php', - 'OC\\OCS\\Exception' => $baseDir . '/lib/private/OCS/Exception.php', 'OC\\OCS\\Provider' => $baseDir . '/lib/private/OCS/Provider.php', - 'OC\\OCS\\Result' => $baseDir . '/lib/private/OCS/Result.php', 'OC\\PhoneNumberUtil' => $baseDir . '/lib/private/PhoneNumberUtil.php', 'OC\\PreviewManager' => $baseDir . '/lib/private/PreviewManager.php', 'OC\\PreviewNotAvailableException' => $baseDir . '/lib/private/PreviewNotAvailableException.php', @@ -2000,7 +1999,6 @@ 'OC\\User\\OutOfOfficeData' => $baseDir . '/lib/private/User/OutOfOfficeData.php', 'OC\\User\\Session' => $baseDir . '/lib/private/User/Session.php', 'OC\\User\\User' => $baseDir . '/lib/private/User/User.php', - 'OC_API' => $baseDir . '/lib/private/legacy/OC_API.php', 'OC_App' => $baseDir . '/lib/private/legacy/OC_App.php', 'OC_Defaults' => $baseDir . '/lib/private/legacy/OC_Defaults.php', 'OC_Files' => $baseDir . '/lib/private/legacy/OC_Files.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index a9737d27c8373..2328d36106db8 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1747,11 +1747,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\OCM\\Model\\OCMProvider' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMProvider.php', 'OC\\OCM\\Model\\OCMResource' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMResource.php', 'OC\\OCM\\OCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMDiscoveryService.php', + 'OC\\OCS\\ApiHelper' => __DIR__ . '/../../..' . '/lib/private/OCS/ApiHelper.php', 'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\DiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCS/DiscoveryService.php', - 'OC\\OCS\\Exception' => __DIR__ . '/../../..' . '/lib/private/OCS/Exception.php', 'OC\\OCS\\Provider' => __DIR__ . '/../../..' . '/lib/private/OCS/Provider.php', - 'OC\\OCS\\Result' => __DIR__ . '/../../..' . '/lib/private/OCS/Result.php', 'OC\\PhoneNumberUtil' => __DIR__ . '/../../..' . '/lib/private/PhoneNumberUtil.php', 'OC\\PreviewManager' => __DIR__ . '/../../..' . '/lib/private/PreviewManager.php', 'OC\\PreviewNotAvailableException' => __DIR__ . '/../../..' . '/lib/private/PreviewNotAvailableException.php', @@ -2033,7 +2032,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\User\\OutOfOfficeData' => __DIR__ . '/../../..' . '/lib/private/User/OutOfOfficeData.php', 'OC\\User\\Session' => __DIR__ . '/../../..' . '/lib/private/User/Session.php', 'OC\\User\\User' => __DIR__ . '/../../..' . '/lib/private/User/User.php', - 'OC_API' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_API.php', 'OC_App' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_App.php', 'OC_Defaults' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_Defaults.php', 'OC_Files' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_Files.php', diff --git a/lib/private/OCS/ApiHelper.php b/lib/private/OCS/ApiHelper.php new file mode 100644 index 0000000000000..f69b540eafa01 --- /dev/null +++ b/lib/private/OCS/ApiHelper.php @@ -0,0 +1,80 @@ +getParam('format', 'xml'); + if (self::isV2($request)) { + $response = new V2Response(new DataResponse([], $statusCode, $headers), $format, $statusMessage); + } else { + $response = new V1Response(new DataResponse([], $statusCode, $headers), $format, $statusMessage); + } + + // Send 401 headers if unauthorised + if ($response->getOCSStatus() === OCSController::RESPOND_UNAUTHORISED) { + // If request comes from JS return dummy auth request + if ($request->getHeader('X-Requested-With') === 'XMLHttpRequest') { + header('WWW-Authenticate: DummyBasic realm="Authorisation Required"'); + } else { + header('WWW-Authenticate: Basic realm="Authorisation Required"'); + } + http_response_code(401); + } + + foreach ($response->getHeaders() as $name => $value) { + header($name . ': ' . $value); + } + + http_response_code($overrideHttpStatusCode ?? $response->getStatus()); + + self::setContentType($format); + $body = $response->render(); + echo $body; + } + + /** + * Based on the requested format the response content type is set + */ + public static function setContentType(?string $format = null): void { + $format ??= Server::get(IRequest::class)->getParam('format', 'xml'); + if ($format === 'xml') { + header('Content-type: text/xml; charset=UTF-8'); + return; + } + + if ($format === 'json') { + header('Content-Type: application/json; charset=utf-8'); + return; + } + + header('Content-Type: application/octet-stream; charset=utf-8'); + } + + protected static function isV2(IRequest $request): bool { + $script = $request->getScriptName(); + + return str_ends_with($script, '/ocs/v2.php'); + } +} diff --git a/lib/private/OCS/Exception.php b/lib/private/OCS/Exception.php deleted file mode 100644 index eca8ec26df07f..0000000000000 --- a/lib/private/OCS/Exception.php +++ /dev/null @@ -1,19 +0,0 @@ -result; - } -} diff --git a/lib/private/OCS/Result.php b/lib/private/OCS/Result.php deleted file mode 100644 index 5460a8b275cf3..0000000000000 --- a/lib/private/OCS/Result.php +++ /dev/null @@ -1,137 +0,0 @@ -data = []; - } elseif (!is_array($data)) { - $this->data = [$this->data]; - } else { - $this->data = $data; - } - $this->statusCode = $code; - $this->message = $message; - $this->headers = $headers; - } - - /** - * optionally set the total number of items available - * - * @param int $items - */ - public function setTotalItems(int $items): void { - $this->items = $items; - } - - /** - * optionally set the number of items per page - * - * @param int $items - */ - public function setItemsPerPage(int $items): void { - $this->perPage = $items; - } - - /** - * get the status code - * @return int - */ - public function getStatusCode(): int { - return $this->statusCode; - } - - /** - * get the meta data for the result - * @return array - */ - public function getMeta(): array { - $meta = []; - $meta['status'] = $this->succeeded() ? 'ok' : 'failure'; - $meta['statuscode'] = $this->statusCode; - $meta['message'] = $this->message; - if ($this->items !== null) { - $meta['totalitems'] = $this->items; - } - if ($this->perPage !== null) { - $meta['itemsperpage'] = $this->perPage; - } - return $meta; - } - - /** - * get the result data - * @return array - */ - public function getData(): array { - return $this->data; - } - - /** - * return bool Whether the method succeeded - * @return bool - */ - public function succeeded(): bool { - return ($this->statusCode == 100); - } - - /** - * Adds a new header to the response - * - * @param string $name The name of the HTTP header - * @param string $value The value, null will delete it - * @return $this - */ - public function addHeader(string $name, ?string $value): static { - $name = trim($name); // always remove leading and trailing whitespace - // to be able to reliably check for security - // headers - - if (is_null($value)) { - unset($this->headers[$name]); - } else { - $this->headers[$name] = $value; - } - - return $this; - } - - /** - * Returns the set headers - * @return array the headers - */ - public function getHeaders(): array { - return $this->headers; - } -} diff --git a/lib/private/legacy/OC_API.php b/lib/private/legacy/OC_API.php deleted file mode 100644 index bb2376d4ea0d8..0000000000000 --- a/lib/private/legacy/OC_API.php +++ /dev/null @@ -1,162 +0,0 @@ -getRequest(); - - // Send 401 headers if unauthorised - if ($result->getStatusCode() === \OCP\AppFramework\OCSController::RESPOND_UNAUTHORISED) { - // If request comes from JS return dummy auth request - if ($request->getHeader('X-Requested-With') === 'XMLHttpRequest') { - header('WWW-Authenticate: DummyBasic realm="Authorisation Required"'); - } else { - header('WWW-Authenticate: Basic realm="Authorisation Required"'); - } - http_response_code(401); - } - - foreach ($result->getHeaders() as $name => $value) { - header($name . ': ' . $value); - } - - $meta = $result->getMeta(); - $data = $result->getData(); - if (self::isV2($request)) { - $statusCode = self::mapStatusCodes($result->getStatusCode()); - if (!is_null($statusCode)) { - $meta['statuscode'] = $statusCode; - http_response_code($statusCode); - } - } - - self::setContentType($format); - $body = self::renderResult($format, $meta, $data); - echo $body; - } - - /** - * @param XMLWriter $writer - */ - private static function toXML($array, $writer) { - foreach ($array as $k => $v) { - if ($k[0] === '@') { - $writer->writeAttribute(substr($k, 1), $v); - continue; - } elseif (is_numeric($k)) { - $k = 'element'; - } - if (is_array($v)) { - $writer->startElement($k); - self::toXML($v, $writer); - $writer->endElement(); - } else { - $writer->writeElement($k, $v); - } - } - } - - public static function requestedFormat(): string { - $formats = ['json', 'xml']; - - $format = (isset($_GET['format']) && is_string($_GET['format']) && in_array($_GET['format'], $formats)) ? $_GET['format'] : 'xml'; - return $format; - } - - /** - * Based on the requested format the response content type is set - * @param string $format - */ - public static function setContentType($format = null) { - $format = is_null($format) ? self::requestedFormat() : $format; - if ($format === 'xml') { - header('Content-type: text/xml; charset=UTF-8'); - return; - } - - if ($format === 'json') { - header('Content-Type: application/json; charset=utf-8'); - return; - } - - header('Content-Type: application/octet-stream; charset=utf-8'); - } - - /** - * @param \OCP\IRequest $request - * @return bool - */ - protected static function isV2(\OCP\IRequest $request) { - $script = $request->getScriptName(); - - return str_ends_with($script, '/ocs/v2.php'); - } - - /** - * @param integer $sc - * @return int - */ - public static function mapStatusCodes($sc) { - switch ($sc) { - case \OCP\AppFramework\OCSController::RESPOND_NOT_FOUND: - return Http::STATUS_NOT_FOUND; - case \OCP\AppFramework\OCSController::RESPOND_SERVER_ERROR: - return Http::STATUS_INTERNAL_SERVER_ERROR; - case \OCP\AppFramework\OCSController::RESPOND_UNKNOWN_ERROR: - return Http::STATUS_INTERNAL_SERVER_ERROR; - case \OCP\AppFramework\OCSController::RESPOND_UNAUTHORISED: - // already handled for v1 - return null; - case 100: - return Http::STATUS_OK; - } - // any 2xx, 4xx and 5xx will be used as is - if ($sc >= 200 && $sc < 600) { - return $sc; - } - - return Http::STATUS_BAD_REQUEST; - } - - /** - * @param string $format - * @return string - */ - public static function renderResult($format, $meta, $data) { - $response = [ - 'ocs' => [ - 'meta' => $meta, - 'data' => $data, - ], - ]; - if ($format == 'json') { - return json_encode($response, JSON_HEX_TAG); - } - - $writer = new XMLWriter(); - $writer->openMemory(); - $writer->setIndent(true); - $writer->startDocument(); - self::toXML($response, $writer); - $writer->endDocument(); - return $writer->outputMemory(true); - } -} diff --git a/ocs/v1.php b/ocs/v1.php index 91f52a66943f3..90d7971c549c8 100644 --- a/ocs/v1.php +++ b/ocs/v1.php @@ -1,27 +1,33 @@ getConfig()->getSystemValueBool('maintenance')) { // since the behavior of apps or remotes are unpredictable during // an upgrade, return a 503 directly - http_response_code(503); - header('X-Nextcloud-Maintenance-Mode: 1'); - $response = new \OC\OCS\Result(null, 503, 'Service unavailable'); - OC_API::respond($response, OC_API::requestedFormat()); + ApiHelper::respond(503, 'Service unavailable', ['X-Nextcloud-Maintenance-Mode' => '1'], 503); exit; } -use OCP\Security\Bruteforce\MaxDelayReached; -use Psr\Log\LoggerInterface; -use Symfony\Component\Routing\Exception\MethodNotAllowedException; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; /* * Try the appframework routes @@ -42,27 +48,19 @@ OC::$server->get(\OC\Route\Router::class)->match('/ocsapp'.\OC::$server->getRequest()->getRawPathInfo()); } catch (MaxDelayReached $ex) { - $format = \OC::$server->getRequest()->getParam('format', 'xml'); - OC_API::respond(new \OC\OCS\Result(null, OCP\AppFramework\Http::STATUS_TOO_MANY_REQUESTS, $ex->getMessage()), $format); + ApiHelper::respond(Http::STATUS_TOO_MANY_REQUESTS, $ex->getMessage()); } catch (ResourceNotFoundException $e) { - OC_API::setContentType(); - - $format = \OC::$server->getRequest()->getParam('format', 'xml'); $txt = 'Invalid query, please check the syntax. API specifications are here:' .' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services.'."\n"; - OC_API::respond(new \OC\OCS\Result(null, \OCP\AppFramework\OCSController::RESPOND_NOT_FOUND, $txt), $format); + ApiHelper::respond(OCSController::RESPOND_NOT_FOUND, $txt); } catch (MethodNotAllowedException $e) { - OC_API::setContentType(); + ApiHelper::setContentType(); http_response_code(405); -} catch (\OC\OCS\Exception $ex) { - OC_API::respond($ex->getResult(), OC_API::requestedFormat()); } catch (\OC\User\LoginException $e) { - OC_API::respond(new \OC\OCS\Result(null, \OCP\AppFramework\OCSController::RESPOND_UNAUTHORISED, 'Unauthorised')); + ApiHelper::respond(OCSController::RESPOND_UNAUTHORISED, 'Unauthorised'); } catch (\Exception $e) { \OCP\Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); - OC_API::setContentType(); - $format = \OC::$server->getRequest()->getParam('format', 'xml'); $txt = 'Internal Server Error'."\n"; try { if (\OC::$server->getSystemConfig()->getValue('debug', false)) { @@ -71,5 +69,5 @@ } catch (\Throwable $e) { // Just to be save } - OC_API::respond(new \OC\OCS\Result(null, \OCP\AppFramework\OCSController::RESPOND_SERVER_ERROR, $txt), $format); + ApiHelper::respond(OCSController::RESPOND_SERVER_ERROR, $txt); } diff --git a/tests/lib/APITest.php b/tests/lib/APITest.php deleted file mode 100644 index cc255b929ad2e..0000000000000 --- a/tests/lib/APITest.php +++ /dev/null @@ -1,84 +0,0 @@ -addHeader('KEY', 'VALUE'); - return [ - 'shipped' => $shipped, - 'response' => $resp, - 'app' => $this->getUniqueID('testapp_'), - ]; - } - - // Validate details of the result - - /** - * @param \OC\OCS\Result $result - */ - public function checkResult($result, $success) { - // Check response is of correct type - $this->assertInstanceOf(\OC\OCS\Result::class, $result); - // Check if it succeeded - /** @var \OC\OCS\Result $result */ - $this->assertEquals($success, $result->succeeded()); - } - - /** - * @return array - */ - public function versionDataScriptNameProvider() { - return [ - // Valid script name - [ - '/master/ocs/v2.php', - true, - ], - - // Invalid script names - [ - '/master/ocs/v2.php/someInvalidPathName', - false, - ], - [ - '/master/ocs/v1.php', - false, - ], - [ - '', - false, - ], - ]; - } - - /** - * @dataProvider versionDataScriptNameProvider - * @param string $scriptName - * @param bool $expected - */ - public function testIsV2($scriptName, $expected) { - $request = $this->getMockBuilder(IRequest::class) - ->disableOriginalConstructor() - ->getMock(); - $request - ->expects($this->once()) - ->method('getScriptName') - ->willReturn($scriptName); - - $this->assertEquals($expected, $this->invokePrivate(new \OC_API, 'isV2', [$request])); - } -} diff --git a/tests/lib/AppFramework/OCS/BaseResponseTest.php b/tests/lib/AppFramework/OCS/BaseResponseTest.php index aaa107ef0139b..159459a4aec2b 100644 --- a/tests/lib/AppFramework/OCS/BaseResponseTest.php +++ b/tests/lib/AppFramework/OCS/BaseResponseTest.php @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace Test\AppFramework\Middleware; +namespace Test\AppFramework\OCS; use OC\AppFramework\OCS\BaseResponse; diff --git a/tests/lib/AppFramework/OCS/V2ResponseTest.php b/tests/lib/AppFramework/OCS/V2ResponseTest.php new file mode 100644 index 0000000000000..97a227418f3f7 --- /dev/null +++ b/tests/lib/AppFramework/OCS/V2ResponseTest.php @@ -0,0 +1,38 @@ +assertEquals($expected, $response->getStatus()); + } + + public function providesStatusCodes(): array { + return [ + [Http::STATUS_OK, 200], + [Http::STATUS_BAD_REQUEST, 104], + [Http::STATUS_BAD_REQUEST, 1000], + [201, 201], + [Http::STATUS_UNAUTHORIZED, OCSController::RESPOND_UNAUTHORISED], + [Http::STATUS_INTERNAL_SERVER_ERROR, OCSController::RESPOND_SERVER_ERROR], + [Http::STATUS_NOT_FOUND, OCSController::RESPOND_NOT_FOUND], + [Http::STATUS_INTERNAL_SERVER_ERROR, OCSController::RESPOND_UNKNOWN_ERROR], + ]; + } +} diff --git a/tests/lib/OCS/ApiHelperTest.php b/tests/lib/OCS/ApiHelperTest.php new file mode 100644 index 0000000000000..fdbc1f4c53890 --- /dev/null +++ b/tests/lib/OCS/ApiHelperTest.php @@ -0,0 +1,54 @@ +getMockBuilder(IRequest::class) + ->disableOriginalConstructor() + ->getMock(); + $request + ->expects($this->once()) + ->method('getScriptName') + ->willReturn($scriptName); + + $this->assertEquals($expected, $this->invokePrivate(new ApiHelper, 'isV2', [$request])); + } +} diff --git a/tests/lib/OCS/MapStatusCodeTest.php b/tests/lib/OCS/MapStatusCodeTest.php deleted file mode 100644 index c6d6d5edd069c..0000000000000 --- a/tests/lib/OCS/MapStatusCodeTest.php +++ /dev/null @@ -1,29 +0,0 @@ -assertEquals($expected, $result); - } - - public function providesStatusCodes() { - return [ - [Http::STATUS_OK, 100], - [Http::STATUS_BAD_REQUEST, 104], - [Http::STATUS_BAD_REQUEST, 1000], - [201, 201], - ]; - } -}