owncloud/core

View on GitHub
lib/private/legacy/api.php

Summary

Maintainability
F
4 days
Test Coverage
<?php
/**
 * @author Bart Visscher <bartv@thisnet.nl>
 * @author Bernhard Posselt <dev@bernhard-posselt.com>
 * @author Björn Schießle <bjoern@schiessle.org>
 * @author Christoph Wurst <christoph@owncloud.com>
 * @author Joas Schilling <coding@schilljs.com>
 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
 * @author Lukas Reschke <lukas@statuscode.ch>
 * @author Michael Gapczynski <GapczynskiM@gmail.com>
 * @author Morris Jobke <hey@morrisjobke.de>
 * @author Robin Appelman <icewind@owncloud.com>
 * @author Roeland Jago Douma <rullzer@owncloud.com>
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 * @author Tom Needham <tom@owncloud.com>
 * @author Vincent Petry <pvince81@owncloud.com>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */
use OCP\API;
use OCP\AppFramework\Http;
use OCP\Authentication\Exceptions\AccountCheckException;

/**
 * @author Bart Visscher <bartv@thisnet.nl>
 * @author Bernhard Posselt <dev@bernhard-posselt.com>
 * @author Björn Schießle <schiessle@owncloud.com>
 * @author Joas Schilling <nickvergessen@owncloud.com>
 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
 * @author Lukas Reschke <lukas@owncloud.com>
 * @author Michael Gapczynski <GapczynskiM@gmail.com>
 * @author Morris Jobke <hey@morrisjobke.de>
 * @author Robin Appelman <icewind@owncloud.com>
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 * @author Tom Needham <tom@owncloud.com>
 * @author Vincent Petry <pvince81@owncloud.com>
 *
 * @copyright Copyright (c) 2018, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */

class OC_API {
    /**
     * API authentication levels
     */

    /** @deprecated Use \OCP\API::GUEST_AUTH instead */
    public const GUEST_AUTH = 0;

    /** @deprecated Use \OCP\API::USER_AUTH instead */
    public const USER_AUTH = 1;

    /** @deprecated Use \OCP\API::SUBADMIN_AUTH instead */
    public const SUBADMIN_AUTH = 2;

    /** @deprecated Use \OCP\API::ADMIN_AUTH instead */
    public const ADMIN_AUTH = 3;

    /**
     * API Response Codes
     */

    /** @deprecated Use \OCP\API::RESPOND_UNAUTHORISED instead */
    public const RESPOND_UNAUTHORISED = 997;

    /** @deprecated Use \OCP\API::RESPOND_SERVER_ERROR instead */
    public const RESPOND_SERVER_ERROR = 996;

    /** @deprecated Use \OCP\API::RESPOND_NOT_FOUND instead */
    public const RESPOND_NOT_FOUND = 998;

    /** @deprecated Use \OCP\API::RESPOND_UNKNOWN_ERROR instead */
    public const RESPOND_UNKNOWN_ERROR = 999;

    /**
     * api actions
     */
    protected static $actions = [];
    private static $logoutRequired = false;
    private static $isLoggedIn = false;

    /**
     * registers an api call
     * @param string $method the http method
     * @param string $url the url to match
     * @param callable $action the function to run
     * @param string $app the id of the app registering the call
     * @param int $authLevel the level of authentication required for the call
     * @param array $defaults
     * @param array $requirements
     * @param boolean $cors whether to enable cors for this route
     */
    public static function register(
        $method,
        $url,
        $action,
        $app,
        $authLevel = API::USER_AUTH,
        $defaults = [],
        $requirements = [],
        $cors = true
    ) {
        $name = \strtolower($method).$url;
        $name = \str_replace(['/', '{', '}'], '_', $name);
        if (!isset(self::$actions[$name])) {
            $oldCollection = OC::$server->getRouter()->getCurrentCollection();
            OC::$server->getRouter()->useCollection('ocs');
            OC::$server->getRouter()->create($name, $url)
                ->method($method)
                ->defaults($defaults)
                ->requirements($requirements)
                ->action('OC_API', 'call');
            self::$actions[$name] = [];
            OC::$server->getRouter()->useCollection($oldCollection);
        }
        self::$actions[$name][] = ['app' => $app, 'action' => $action, 'authlevel' => $authLevel, 'cors' => $cors];
    }

    /**
     * handles an api call
     * @param array $parameters
     */
    public static function call($parameters) {
        $request = \OC::$server->getRequest();
        $method = $request->getMethod();

        // Prepare the request variables
        if ($method === 'PUT') {
            $parameters['_put'] = $request->getParams();
        } elseif ($method === 'DELETE') {
            $parameters['_delete'] = $request->getParams();
        }
        $name = $parameters['_route'];
        // Foreach registered action
        $responses = [];
        foreach (self::$actions[$name] as $action) {
            // Check authentication and availability
            if (!self::isAuthorised($action)) {
                $responses[] = [
                    'app' => $action['app'],
                    'response' => new \OC\OCS\Result(null, API::RESPOND_UNAUTHORISED, 'Unauthorised'),
                    'shipped' => OC_App::isShipped($action['app']),
                ];
                continue;
            }
            if (!\is_callable($action['action'])) {
                $responses[] = [
                    'app' => $action['app'],
                    'response' => new \OC\OCS\Result(null, API::RESPOND_NOT_FOUND, 'Api method not found'),
                    'shipped' => OC_App::isShipped($action['app']),
                ];
                continue;
            }
            // Run the action
            $responses[] = [
                'app' => $action['app'],
                'response' => \call_user_func($action['action'], $parameters),
                'shipped' => OC_App::isShipped($action['app']),
            ];
        }
        $response = self::mergeResponses($responses);

        // If CORS is set to active for some method, try to add CORS headers
        if (self::$actions[$name][0]['cors'] &&
            \OC::$server->getRequest()->getHeader('Origin') !== null) {
            $requesterDomain = \OC::$server->getRequest()->getHeader('Origin');

            // unauthenticated request shall add cors headers as well
            $userId = null;
            if (\OC::$server->getUserSession()->getUser() !== null) {
                $userId = \OC::$server->getUserSession()->getUser()->getUID();
            }
            $headers = \OC_Response::setCorsHeaders($userId, $requesterDomain);
            foreach ($headers as $key => $value) {
                $response->addHeader($key, \implode(',', $value));
            }
        }

        $format = self::requestedFormat();
        if (self::$logoutRequired) {
            \OC::$server->getUserSession()->logout();
        }

        self::respond($response, $format);
    }

    /**
     * merge the returned result objects into one response
     * @param array $responses
     * @return \OC\OCS\Result
     */
    public static function mergeResponses($responses) {
        // Sort into shipped and third-party
        $shipped = [
            'succeeded' => [],
            'failed' => [],
        ];
        $thirdparty = [
            'succeeded' => [],
            'failed' => [],
        ];

        foreach ($responses as $response) {
            if ($response['shipped'] || ($response['app'] === 'core')) {
                if ($response['response']->succeeded()) {
                    $shipped['succeeded'][$response['app']] = $response;
                } else {
                    $shipped['failed'][$response['app']] = $response;
                }
            } else {
                if ($response['response']->succeeded()) {
                    $thirdparty['succeeded'][$response['app']] = $response;
                } else {
                    $thirdparty['failed'][$response['app']] = $response;
                }
            }
        }

        // Remove any error responses if there is one shipped response that succeeded
        if (!empty($shipped['failed'])) {
            // Which shipped response do we use if they all failed?
            // They may have failed for different reasons (different status codes)
            // Which response code should we return?
            // Maybe any that are not \OCP\API::RESPOND_SERVER_ERROR
            // Merge failed responses if more than one
            $data = [];
            foreach ($shipped['failed'] as $failure) {
                $data = \array_merge_recursive($data, $failure['response']->getData());
            }
            $picked = \reset($shipped['failed']);
            $code = $picked['response']->getStatusCode();
            $meta = $picked['response']->getMeta();
            $headers = $picked['response']->getHeaders();
            $response = new \OC\OCS\Result($data, $code, $meta['message'], $headers);
            return $response;
        } elseif (!empty($shipped['succeeded'])) {
            $responses = \array_merge($shipped['succeeded'], $thirdparty['succeeded']);
        } elseif (!empty($thirdparty['failed'])) {
            // Merge failed responses if more than one
            $data = [];
            foreach ($thirdparty['failed'] as $failure) {
                $data = \array_merge_recursive($data, $failure['response']->getData());
            }
            $picked = \reset($thirdparty['failed']);
            $code = $picked['response']->getStatusCode();
            $meta = $picked['response']->getMeta();
            $headers = $picked['response']->getHeaders();
            $response = new \OC\OCS\Result($data, $code, $meta['message'], $headers);
            return $response;
        } else {
            $responses = $thirdparty['succeeded'];
        }
        // Merge the successful responses
        $data = [];
        $codes = [];
        $header = [];

        foreach ($responses as $response) {
            if ($response['shipped']) {
                $data = \array_merge_recursive($response['response']->getData(), $data);
            } else {
                $data = \array_merge_recursive($data, $response['response']->getData());
            }
            $header = \array_merge_recursive($header, $response['response']->getHeaders());
            $codes[] = ['code' => $response['response']->getStatusCode(),
                'meta' => $response['response']->getMeta()];
        }

        // Use any non 100 status codes
        $statusCode = 100;
        $statusMessage = null;
        foreach ($codes as $code) {
            if ($code['code'] != 100) {
                $statusCode = $code['code'];
                $statusMessage = $code['meta']['message'];
                break;
            }
        }

        return new \OC\OCS\Result($data, $statusCode, $statusMessage, $header);
    }

    /**
     * authenticate the api call
     * @param array $action the action details as supplied to OC_API::register()
     * @return bool
     */
    private static function isAuthorised($action) {
        $level = $action['authlevel'];
        switch ($level) {
            case API::GUEST_AUTH:
                // Anyone can access
                return true;
            case API::USER_AUTH:
                // User required
                return self::loginUser();
            case API::SUBADMIN_AUTH:
                // Check for subadmin
                $user = self::loginUser();
                if (!$user) {
                    return false;
                } else {
                    $userObject = \OC::$server->getUserSession()->getUser();
                    if ($userObject === null) {
                        return false;
                    }
                    $isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
                    $admin = OC_User::isAdminUser($user);
                    if ($isSubAdmin || $admin) {
                        return true;
                    } else {
                        return false;
                    }
                }
                // no break
            case API::ADMIN_AUTH:
                // Check for admin
                $user = self::loginUser();
                if (!$user) {
                    return false;
                } else {
                    return OC_User::isAdminUser($user);
                }
                // no break
            default:
                // oops looks like invalid level supplied
                return false;
        }
    }

    /**
     * http basic auth
     * @return string|false (username, or false on failure)
     */
    private static function loginUser() {
        if (self::$isLoggedIn === true) {
            return \OC_User::getUser();
        }

        // reuse existing login
        $userSession = \OC::$server->getUserSession();
        $request = \OC::$server->getRequest();
        $loggedIn = $userSession->isLoggedIn();
        if ($loggedIn === true) {
            if (\OC::$server->getTwoFactorAuthManager()->needsSecondFactor()) {
                // Do not allow access to OCS until the 2FA challenge was solved successfully
                return false;
            }
            try {
                \OC::$server->getAccountModuleManager()->check($userSession->getUser());
            } catch (AccountCheckException $ex) {
                // Deny login if any IAuthModule check fails
                return false;
            }
            if ($userSession->verifyAuthHeaders($request)) {
                $ocsApiRequest = isset($_SERVER['HTTP_OCS_APIREQUEST']) ? $_SERVER['HTTP_OCS_APIREQUEST'] === 'true' : false;
                if ($ocsApiRequest) {
                    // initialize the user's filesystem
                    \OC_Util::setupFS(\OC_User::getUser());
                    self::$isLoggedIn = true;

                    return OC_User::getUser();
                }

                return false;
            }
        }

        // basic auth - because OC_User::login will create a new session we shall only try to login
        // if user and pass are set
        try {
            if (OC_User::handleApacheAuth()) {
                self::$logoutRequired = false;
            } elseif ($userSession->tryTokenLogin($request)
                || $userSession->tryAuthModuleLogin($request)
                || $userSession->tryBasicAuthLogin($request)
                || $userSession->tryRememberMeLogin($request)) {
                self::$logoutRequired = true;
            } else {
                return false;
            }
            try {
                \OC::$server->getAccountModuleManager()->check($userSession->getUser());
            } catch (AccountCheckException $ex) {
                // Deny login if any IAuthModule check fails
                return false;
            }
            // initialize the user's filesystem
            \OC_Util::setupFS(\OC_User::getUser());
            self::$isLoggedIn = true;

            return \OC_User::getUser();
        } catch (\OC\User\LoginException $e) {
            return false;
        }
    }

    /**
     * respond to a call
     * @param \OC\OCS\Result $result
     * @param string $format the format xml|json
     */
    public static function respond($result, $format='xml') {
        $request = \OC::$server->getRequest();

        // Send 401 headers if unauthorised
        if ($result->getStatusCode() === API::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 ($statusCode !== null) {
                $meta['statuscode'] = $statusCode;
                OC_Response::setStatus($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 (isset($k[0]) && ($k[0] === '@')) {
                if (\is_array($v)) {
                    foreach ($v as $name => $value) {
                        $writer->writeAttribute($name, $value);
                    }
                } else {
                    $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);
            }
        }
    }

    /**
     * @return string
     */
    public static function requestedFormat() {
        $formats = ['json', 'xml'];

        $format = !empty($_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 = $format === null ? 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 \substr($script, -11) === '/ocs/v2.php';
    }

    /**
     * @param integer $sc
     * @return int
     */
    public static function mapStatusCodes($sc) {
        switch ($sc) {
            case API::RESPOND_NOT_FOUND:
                return Http::STATUS_NOT_FOUND;
            case API::RESPOND_SERVER_ERROR:
                return Http::STATUS_INTERNAL_SERVER_ERROR;
            case API::RESPOND_UNKNOWN_ERROR:
                return Http::STATUS_INTERNAL_SERVER_ERROR;
            case API::RESPOND_UNAUTHORISED:
                // already handled for v1
                return null;
            case 100:
                return Http::STATUS_OK;
            case 104:
                return Http::STATUS_FORBIDDEN;
        }
        // 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 OC_JSON::encode($response);
        }

        $writer = new XMLWriter();
        $writer->openMemory();
        $writer->setIndent(true);
        $writer->startDocument();
        self::toXML($response, $writer);
        $writer->endDocument();
        return $writer->outputMemory(true);
    }

    /**
     * Called when a nonexistent OCS endpoint has been called
     */
    public static function notFound() {
        $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. DEBUG OUTPUT:'."\n";
        OC_API::respond(new \OC\OCS\Result(null, API::RESPOND_UNKNOWN_ERROR, $txt), $format);
    }
}