diff options
Diffstat (limited to 'lib/private/AppFramework')
22 files changed, 3683 insertions, 0 deletions
diff --git a/lib/private/AppFramework/App.php b/lib/private/AppFramework/App.php new file mode 100644 index 00000000000..376a8559454 --- /dev/null +++ b/lib/private/AppFramework/App.php @@ -0,0 +1,171 @@ +<?php +/** + * @author Andreas Fischer <bantu@owncloud.com> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework; + +use OC\AppFramework\Http\Dispatcher; +use OC_App; +use OC\AppFramework\DependencyInjection\DIContainer; +use OCP\AppFramework\QueryException; +use OCP\AppFramework\Http\ICallbackResponse; + +/** + * Entry point for every request in your app. You can consider this as your + * public static void main() method + * + * Handles all the dependency injection, controllers and output flow + */ +class App { + + + /** + * Turns an app id into a namespace by either reading the appinfo.xml's + * namespace tag or uppercasing the appid's first letter + * @param string $appId the app id + * @param string $topNamespace the namespace which should be prepended to + * the transformed app id, defaults to OCA\ + * @return string the starting namespace for the app + */ + public static function buildAppNamespace($appId, $topNamespace='OCA\\') { + // first try to parse the app's appinfo/info.xml <namespace> tag + $appPath = OC_App::getAppPath($appId); + if ($appPath !== false) { + $filePath = "$appPath/appinfo/info.xml"; + if (is_file($filePath)) { + $loadEntities = libxml_disable_entity_loader(false); + $xml = @simplexml_load_file($filePath); + libxml_disable_entity_loader($loadEntities); + if ($xml) { + $result = $xml->xpath('/info/namespace'); + if ($result && count($result) > 0) { + // take first namespace result + return $topNamespace . trim((string) $result[0]); + } + } + } + } + // if the tag is not found, fall back to uppercasing the first letter + return $topNamespace . ucfirst($appId); + } + + + /** + * Shortcut for calling a controller method and printing the result + * @param string $controllerName the name of the controller under which it is + * stored in the DI container + * @param string $methodName the method that you want to call + * @param DIContainer $container an instance of a pimple container. + * @param array $urlParams list of URL parameters (optional) + */ + public static function main($controllerName, $methodName, DIContainer $container, array $urlParams = null) { + if (!is_null($urlParams)) { + $container['OCP\\IRequest']->setUrlParameters($urlParams); + } else if (isset($container['urlParams']) && !is_null($container['urlParams'])) { + $container['OCP\\IRequest']->setUrlParameters($container['urlParams']); + } + $appName = $container['AppName']; + + // first try $controllerName then go for \OCA\AppName\Controller\$controllerName + try { + $controller = $container->query($controllerName); + } catch(QueryException $e) { + $appNameSpace = self::buildAppNamespace($appName); + $controllerName = $appNameSpace . '\\Controller\\' . $controllerName; + $controller = $container->query($controllerName); + } + + // initialize the dispatcher and run all the middleware before the controller + /** @var Dispatcher $dispatcher */ + $dispatcher = $container['Dispatcher']; + + list( + $httpHeaders, + $responseHeaders, + $responseCookies, + $output, + $response + ) = $dispatcher->dispatch($controller, $methodName); + + $io = $container['OCP\\AppFramework\\Http\\IOutput']; + + if(!is_null($httpHeaders)) { + $io->setHeader($httpHeaders); + } + + foreach($responseHeaders as $name => $value) { + $io->setHeader($name . ': ' . $value); + } + + foreach($responseCookies as $name => $value) { + $expireDate = null; + if($value['expireDate'] instanceof \DateTime) { + $expireDate = $value['expireDate']->getTimestamp(); + } + $io->setCookie( + $name, + $value['value'], + $expireDate, + $container->getServer()->getWebRoot(), + null, + $container->getServer()->getRequest()->getServerProtocol() === 'https', + true + ); + } + + if ($response instanceof ICallbackResponse) { + $response->callback($io); + } else if(!is_null($output)) { + $io->setHeader('Content-Length: ' . strlen($output)); + $io->setOutput($output); + } + + } + + /** + * Shortcut for calling a controller method and printing the result. + * Similar to App:main except that no headers will be sent. + * This should be used for example when registering sections via + * \OC\AppFramework\Core\API::registerAdmin() + * + * @param string $controllerName the name of the controller under which it is + * stored in the DI container + * @param string $methodName the method that you want to call + * @param array $urlParams an array with variables extracted from the routes + * @param DIContainer $container an instance of a pimple container. + */ + public static function part($controllerName, $methodName, array $urlParams, + DIContainer $container){ + + $container['urlParams'] = $urlParams; + $controller = $container[$controllerName]; + + $dispatcher = $container['Dispatcher']; + + list(, , $output) = $dispatcher->dispatch($controller, $methodName); + return $output; + } + +} diff --git a/lib/private/AppFramework/Core/API.php b/lib/private/AppFramework/Core/API.php new file mode 100644 index 00000000000..67b696948f0 --- /dev/null +++ b/lib/private/AppFramework/Core/API.php @@ -0,0 +1,200 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework\Core; +use OCP\AppFramework\IApi; + + +/** + * This is used to wrap the owncloud static api calls into an object to make the + * code better abstractable for use in the dependency injection container + * + * Should you find yourself in need for more methods, simply inherit from this + * class and add your methods + * @deprecated + */ +class API implements IApi{ + + private $appName; + + /** + * constructor + * @param string $appName the name of your application + */ + public function __construct($appName){ + $this->appName = $appName; + } + + + /** + * Gets the userid of the current user + * @return string the user id of the current user + * @deprecated Use \OC::$server->getUserSession()->getUser()->getUID() + */ + public function getUserId(){ + return \OCP\User::getUser(); + } + + + /** + * Adds a new javascript file + * @deprecated include javascript and css in template files + * @param string $scriptName the name of the javascript in js/ without the suffix + * @param string $appName the name of the app, defaults to the current one + */ + public function addScript($scriptName, $appName=null){ + if($appName === null){ + $appName = $this->appName; + } + \OCP\Util::addScript($appName, $scriptName); + } + + + /** + * Adds a new css file + * @deprecated include javascript and css in template files + * @param string $styleName the name of the css file in css/without the suffix + * @param string $appName the name of the app, defaults to the current one + */ + public function addStyle($styleName, $appName=null){ + if($appName === null){ + $appName = $this->appName; + } + \OCP\Util::addStyle($appName, $styleName); + } + + + /** + * @deprecated include javascript and css in template files + * shorthand for addScript for files in the 3rdparty directory + * @param string $name the name of the file without the suffix + */ + public function add3rdPartyScript($name){ + \OCP\Util::addScript($this->appName . '/3rdparty', $name); + } + + + /** + * @deprecated include javascript and css in template files + * shorthand for addStyle for files in the 3rdparty directory + * @param string $name the name of the file without the suffix + */ + public function add3rdPartyStyle($name){ + \OCP\Util::addStyle($this->appName . '/3rdparty', $name); + } + + + /** + * @deprecated communication between apps should happen over built in + * callbacks or interfaces (check the contacts and calendar managers) + * Checks if an app is enabled + * also use \OC::$server->getAppManager()->isEnabledForUser($appName) + * @param string $appName the name of an app + * @return bool true if app is enabled + */ + public function isAppEnabled($appName){ + return \OCP\App::isEnabled($appName); + } + + + /** + * used to return and open a new event source + * @return \OCP\IEventSource a new open EventSource class + * @deprecated Use \OC::$server->createEventSource(); + */ + public function openEventSource(){ + return \OC::$server->createEventSource(); + } + + /** + * @deprecated register hooks directly for class that build in hook interfaces + * connects a function to a hook + * @param string $signalClass class name of emitter + * @param string $signalName name of signal + * @param string $slotClass class name of slot + * @param string $slotName name of slot, in another word, this is the + * name of the method that will be called when registered + * signal is emitted. + * @return bool always true + */ + public function connectHook($signalClass, $signalName, $slotClass, $slotName) { + return \OCP\Util::connectHook($signalClass, $signalName, $slotClass, $slotName); + } + + /** + * @deprecated implement the emitter interface instead + * Emits a signal. To get data from the slot use references! + * @param string $signalClass class name of emitter + * @param string $signalName name of signal + * @param array $params default: array() array with additional data + * @return bool true if slots exists or false if not + */ + public function emitHook($signalClass, $signalName, $params = array()) { + return \OCP\Util::emitHook($signalClass, $signalName, $params); + } + + /** + * clear hooks + * @deprecated clear hooks directly for class that build in hook interfaces + * @param string $signalClass + * @param string $signalName + */ + public function clearHook($signalClass=false, $signalName=false) { + if ($signalClass) { + \OC_Hook::clear($signalClass, $signalName); + } + } + + + /** + * Register a backgroundjob task + * @param string $className full namespace and class name of the class + * @param string $methodName the name of the static method that should be + * called + * @deprecated Use \OC::$server->getJobList()->add(); + */ + public function addRegularTask($className, $methodName) { + \OCP\Backgroundjob::addRegularTask($className, $methodName); + } + + + /** + * Tells ownCloud to include a template in the admin overview + * @param string $mainPath the path to the main php file without the php + * suffix, relative to your apps directory! not the template directory + * @param string $appName the name of the app, defaults to the current one + */ + public function registerAdmin($mainPath, $appName=null) { + if($appName === null){ + $appName = $this->appName; + } + + \OCP\App::registerAdmin($appName, $mainPath); + } + + +} diff --git a/lib/private/AppFramework/Db/Db.php b/lib/private/AppFramework/Db/Db.php new file mode 100644 index 00000000000..0d17d7bc225 --- /dev/null +++ b/lib/private/AppFramework/Db/Db.php @@ -0,0 +1,287 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Db; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDb; +use OCP\IDBConnection; + +/** + * @deprecated use IDBConnection directly, will be removed in ownCloud 10 + * Small Facade for being able to inject the database connection for tests + */ +class Db implements IDb { + /** + * @var IDBConnection + */ + protected $connection; + + /** + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + /** + * Gets the ExpressionBuilder for the connection. + * + * @return \OCP\DB\QueryBuilder\IQueryBuilder + */ + public function getQueryBuilder() { + return $this->connection->getQueryBuilder(); + } + + /** + * Used to abstract the ownCloud database access away + * + * @param string $sql the sql query with ? placeholder for params + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @deprecated use prepare instead, will be removed in ownCloud 10 + * @return \OC_DB_StatementWrapper prepared SQL query + */ + public function prepareQuery($sql, $limit = null, $offset = null) { + $isManipulation = \OC_DB::isManipulation($sql); + $statement = $this->connection->prepare($sql, $limit, $offset); + return new \OC_DB_StatementWrapper($statement, $isManipulation); + } + + + /** + * Used to get the id of the just inserted element + * + * @deprecated use lastInsertId instead, will be removed in ownCloud 10 + * @param string $tableName the name of the table where we inserted the item + * @return int the id of the inserted element + */ + public function getInsertId($tableName) { + return $this->connection->lastInsertId($tableName); + } + + /** + * Used to abstract the ownCloud database access away + * @param string $sql the sql query with ? placeholder for params + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @return \Doctrine\DBAL\Driver\Statement The prepared statement. + */ + public function prepare($sql, $limit=null, $offset=null) { + return $this->connection->prepare($sql, $limit, $offset); + } + + /** + * Executes an, optionally parameterized, SQL query. + * + * If the query is parameterized, a prepared statement is used. + * If an SQLLogger is configured, the execution is logged. + * + * @param string $query The SQL query to execute. + * @param string[] $params The parameters to bind to the query, if any. + * @param array $types The types the previous parameters are in. + * @return \Doctrine\DBAL\Driver\Statement The executed statement. + */ + public function executeQuery($query, array $params = array(), $types = array()) { + return $this->connection->executeQuery($query, $params, $types); + } + + /** + * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters + * and returns the number of affected rows. + * + * This method supports PDO binding types as well as DBAL mapping types. + * + * @param string $query The SQL query. + * @param array $params The query parameters. + * @param array $types The parameter types. + * @return integer The number of affected rows. + */ + public function executeUpdate($query, array $params = array(), array $types = array()) { + return $this->connection->executeUpdate($query, $params, $types); + } + + /** + * Used to get the id of the just inserted element + * @param string $table the name of the table where we inserted the item + * @return int the id of the inserted element + */ + public function lastInsertId($table = null) { + return $this->connection->lastInsertId($table); + } + + /** + * Insert a row if the matching row does not exists. + * + * @param string $table The table name (will replace *PREFIX* with the actual prefix) + * @param array $input data that should be inserted into the table (column name => value) + * @param array|null $compare List of values that should be checked for "if not exists" + * If this is null or an empty array, all keys of $input will be compared + * Please note: text fields (clob) must not be used in the compare array + * @return int number of inserted rows + * @throws \Doctrine\DBAL\DBALException + */ + public function insertIfNotExist($table, $input, array $compare = null) { + return $this->connection->insertIfNotExist($table, $input, $compare); + } + + /** + * Insert or update a row value + * + * @param string $table + * @param array $keys (column name => value) + * @param array $values (column name => value) + * @param array $updatePreconditionValues ensure values match preconditions (column name => value) + * @return int number of new rows + * @throws \Doctrine\DBAL\DBALException + * @throws PreconditionNotMetException + */ + public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []) { + return $this->connection->setValues($table, $keys, $values, $updatePreconditionValues); + } + + /** + * Start a transaction + */ + public function beginTransaction() { + $this->connection->beginTransaction(); + } + + /** + * Check if a transaction is active + * + * @return bool + */ + public function inTransaction() { + return $this->connection->inTransaction(); + } + + /** + * Commit the database changes done during a transaction that is in progress + */ + public function commit() { + $this->connection->commit(); + } + + /** + * Rollback the database changes done during a transaction that is in progress + */ + public function rollBack() { + $this->connection->rollBack(); + } + + /** + * Gets the error code and message as a string for logging + * @return string + */ + public function getError() { + return $this->connection->getError(); + } + + /** + * Fetch the SQLSTATE associated with the last database operation. + * + * @return integer The last error code. + */ + public function errorCode() { + return $this->connection->errorCode(); + } + + /** + * Fetch extended error information associated with the last database operation. + * + * @return array The last error information. + */ + public function errorInfo() { + return $this->connection->errorInfo(); + } + + /** + * Establishes the connection with the database. + * + * @return bool + */ + public function connect() { + return $this->connection->connect(); + } + + /** + * Close the database connection + */ + public function close() { + $this->connection->close(); + } + + /** + * Quotes a given input parameter. + * + * @param mixed $input Parameter to be quoted. + * @param int $type Type of the parameter. + * @return string The quoted parameter. + */ + public function quote($input, $type = IQueryBuilder::PARAM_STR) { + return $this->connection->quote($input, $type); + } + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return \Doctrine\DBAL\Platforms\AbstractPlatform The database platform. + */ + public function getDatabasePlatform() { + return $this->connection->getDatabasePlatform(); + } + + /** + * Drop a table from the database if it exists + * + * @param string $table table name without the prefix + */ + public function dropTable($table) { + $this->connection->dropTable($table); + } + + /** + * Check if a table exists + * + * @param string $table table name without the prefix + * @return bool + */ + public function tableExists($table) { + return $this->connection->tableExists($table); + } + + /** + * Espace a parameter to be used in a LIKE query + * + * @param string $param + * @return string + */ + public function escapeLikeParameter($param) { + return $this->connection->escapeLikeParameter($param); + } +} diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php new file mode 100644 index 00000000000..f74fe4aeb99 --- /dev/null +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -0,0 +1,461 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework\DependencyInjection; + +use OC; +use OC\AppFramework\Http; +use OC\AppFramework\Http\Dispatcher; +use OC\AppFramework\Http\Output; +use OC\AppFramework\Core\API; +use OC\AppFramework\Middleware\MiddlewareDispatcher; +use OC\AppFramework\Middleware\Security\SecurityMiddleware; +use OC\AppFramework\Middleware\Security\CORSMiddleware; +use OC\AppFramework\Middleware\SessionMiddleware; +use OC\AppFramework\Utility\SimpleContainer; +use OCP\AppFramework\IApi; +use OCP\AppFramework\IAppContainer; + + +class DIContainer extends SimpleContainer implements IAppContainer { + + /** + * @var array + */ + private $middleWares = array(); + + /** + * Put your class dependencies in here + * @param string $appName the name of the app + */ + public function __construct($appName, $urlParams = array()){ + parent::__construct(); + $this['AppName'] = $appName; + $this['urlParams'] = $urlParams; + + /** @var \OC\ServerContainer $server */ + $server = $this->getServer(); + $server->registerAppContainer($appName, $this); + + // aliases + $this->registerAlias('appName', 'AppName'); + $this->registerAlias('webRoot', 'WebRoot'); + $this->registerAlias('userId', 'UserId'); + + /** + * Core services + */ + $this->registerService('OCP\\IAppConfig', function($c) { + return $this->getServer()->getAppConfig(); + }); + + $this->registerService('OCP\\App\\IAppManager', function($c) { + return $this->getServer()->getAppManager(); + }); + + $this->registerService('OCP\\AppFramework\\Http\\IOutput', function($c){ + return new Output($this->getServer()->getWebRoot()); + }); + + $this->registerService('OCP\\IAvatarManager', function($c) { + return $this->getServer()->getAvatarManager(); + }); + + $this->registerService('OCP\\Activity\\IManager', function($c) { + return $this->getServer()->getActivityManager(); + }); + + $this->registerService('OCP\\ICache', function($c) { + return $this->getServer()->getCache(); + }); + + $this->registerService('OCP\\ICacheFactory', function($c) { + return $this->getServer()->getMemCacheFactory(); + }); + + $this->registerService('OC\\CapabilitiesManager', function($c) { + return $this->getServer()->getCapabilitiesManager(); + }); + + $this->registerService('OCP\Comments\ICommentsManager', function($c) { + return $this->getServer()->getCommentsManager(); + }); + + $this->registerService('OCP\\IConfig', function($c) { + return $this->getServer()->getConfig(); + }); + + $this->registerService('OCP\\Contacts\\IManager', function($c) { + return $this->getServer()->getContactsManager(); + }); + + $this->registerService('OCP\\IDateTimeZone', function($c) { + return $this->getServer()->getDateTimeZone(); + }); + + $this->registerService('OCP\\IDb', function($c) { + return $this->getServer()->getDb(); + }); + + $this->registerService('OCP\\IDBConnection', function($c) { + return $this->getServer()->getDatabaseConnection(); + }); + + $this->registerService('OCP\\Diagnostics\\IEventLogger', function($c) { + return $this->getServer()->getEventLogger(); + }); + + $this->registerService('OCP\\Diagnostics\\IQueryLogger', function($c) { + return $this->getServer()->getQueryLogger(); + }); + + $this->registerService('OCP\\Files\\IMimeTypeDetector', function($c) { + return $this->getServer()->getMimeTypeDetector(); + }); + + $this->registerService('OCP\\Files\\Config\\IMountProviderCollection', function($c) { + return $this->getServer()->getMountProviderCollection(); + }); + + $this->registerService('OCP\\Files\\IRootFolder', function($c) { + return $this->getServer()->getRootFolder(); + }); + + $this->registerService('OCP\\IGroupManager', function($c) { + return $this->getServer()->getGroupManager(); + }); + + $this->registerService('OCP\\IL10N', function($c) { + return $this->getServer()->getL10N($c->query('AppName')); + }); + + $this->registerService('OCP\\L10N\\IFactory', function($c) { + return $this->getServer()->getL10NFactory(); + }); + + $this->registerService('OCP\\ILogger', function($c) { + return $this->getServer()->getLogger(); + }); + + $this->registerService('OCP\\BackgroundJob\\IJobList', function($c) { + return $this->getServer()->getJobList(); + }); + + $this->registerAlias('OCP\\AppFramework\\Utility\\IControllerMethodReflector', 'OC\AppFramework\Utility\ControllerMethodReflector'); + $this->registerAlias('ControllerMethodReflector', 'OCP\\AppFramework\\Utility\\IControllerMethodReflector'); + + $this->registerService('OCP\\Files\\IMimeTypeDetector', function($c) { + return $this->getServer()->getMimeTypeDetector(); + }); + + $this->registerService('OCP\\INavigationManager', function($c) { + return $this->getServer()->getNavigationManager(); + }); + + $this->registerService('OCP\\Notification\IManager', function($c) { + return $this->getServer()->getNotificationManager(); + }); + + $this->registerService('OCP\\IPreview', function($c) { + return $this->getServer()->getPreviewManager(); + }); + + $this->registerService('OCP\\IRequest', function () { + return $this->getServer()->getRequest(); + }); + $this->registerAlias('Request', 'OCP\\IRequest'); + + $this->registerService('OCP\\ITagManager', function($c) { + return $this->getServer()->getTagManager(); + }); + + $this->registerService('OCP\\ITempManager', function($c) { + return $this->getServer()->getTempManager(); + }); + + $this->registerAlias('OCP\\AppFramework\\Utility\\ITimeFactory', 'OC\AppFramework\Utility\TimeFactory'); + $this->registerAlias('TimeFactory', 'OCP\\AppFramework\\Utility\\ITimeFactory'); + + + $this->registerService('OCP\\Route\\IRouter', function($c) { + return $this->getServer()->getRouter(); + }); + + $this->registerService('OCP\\ISearch', function($c) { + return $this->getServer()->getSearch(); + }); + + $this->registerService('OCP\\ISearch', function($c) { + return $this->getServer()->getSearch(); + }); + + $this->registerService('OCP\\Security\\ICrypto', function($c) { + return $this->getServer()->getCrypto(); + }); + + $this->registerService('OCP\\Security\\IHasher', function($c) { + return $this->getServer()->getHasher(); + }); + + $this->registerService('OCP\\Security\\ICredentialsManager', function($c) { + return $this->getServer()->getCredentialsManager(); + }); + + $this->registerService('OCP\\Security\\ISecureRandom', function($c) { + return $this->getServer()->getSecureRandom(); + }); + + $this->registerService('OCP\\Share\\IManager', function($c) { + return $this->getServer()->getShareManager(); + }); + + $this->registerService('OCP\\SystemTag\\ISystemTagManager', function() { + return $this->getServer()->getSystemTagManager(); + }); + + $this->registerService('OCP\\SystemTag\\ISystemTagObjectMapper', function() { + return $this->getServer()->getSystemTagObjectMapper(); + }); + + $this->registerService('OCP\\IURLGenerator', function($c) { + return $this->getServer()->getURLGenerator(); + }); + + $this->registerService('OCP\\IUserManager', function($c) { + return $this->getServer()->getUserManager(); + }); + + $this->registerService('OCP\\IUserSession', function($c) { + return $this->getServer()->getUserSession(); + }); + + $this->registerService('OCP\\ISession', function($c) { + return $this->getServer()->getSession(); + }); + + $this->registerService('OCP\\Security\\IContentSecurityPolicyManager', function($c) { + return $this->getServer()->getContentSecurityPolicyManager(); + }); + + $this->registerService('ServerContainer', function ($c) { + return $this->getServer(); + }); + $this->registerAlias('OCP\\IServerContainer', 'ServerContainer'); + + $this->registerService('Symfony\Component\EventDispatcher\EventDispatcherInterface', function ($c) { + return $this->getServer()->getEventDispatcher(); + }); + + $this->registerService('OCP\\AppFramework\\IAppContainer', function ($c) { + return $c; + }); + + // commonly used attributes + $this->registerService('UserId', function ($c) { + return $c->query('OCP\\IUserSession')->getSession()->get('user_id'); + }); + + $this->registerService('WebRoot', function ($c) { + return $c->query('ServerContainer')->getWebRoot(); + }); + + + /** + * App Framework APIs + */ + $this->registerService('API', function($c){ + $c->query('OCP\\ILogger')->debug( + 'Accessing the API class is deprecated! Use the appropriate ' . + 'services instead!' + ); + return new API($c['AppName']); + }); + + $this->registerService('Protocol', function($c){ + /** @var \OC\Server $server */ + $server = $c->query('ServerContainer'); + $protocol = $server->getRequest()->getHttpProtocol(); + return new Http($_SERVER, $protocol); + }); + + $this->registerService('Dispatcher', function($c) { + return new Dispatcher( + $c['Protocol'], + $c['MiddlewareDispatcher'], + $c['ControllerMethodReflector'], + $c['Request'] + ); + }); + + + /** + * Middleware + */ + $app = $this; + $this->registerService('SecurityMiddleware', function($c) use ($app){ + return new SecurityMiddleware( + $c['Request'], + $c['ControllerMethodReflector'], + $app->getServer()->getNavigationManager(), + $app->getServer()->getURLGenerator(), + $app->getServer()->getLogger(), + $c['AppName'], + $app->isLoggedIn(), + $app->isAdminUser(), + $app->getServer()->getContentSecurityPolicyManager() + ); + }); + + $this->registerService('CORSMiddleware', function($c) { + return new CORSMiddleware( + $c['Request'], + $c['ControllerMethodReflector'], + $c['OCP\IUserSession'] + ); + }); + + $this->registerService('SessionMiddleware', function($c) use ($app) { + return new SessionMiddleware( + $c['Request'], + $c['ControllerMethodReflector'], + $app->getServer()->getSession() + ); + }); + + $middleWares = &$this->middleWares; + $this->registerService('MiddlewareDispatcher', function($c) use (&$middleWares) { + $dispatcher = new MiddlewareDispatcher(); + $dispatcher->registerMiddleware($c['CORSMiddleware']); + $dispatcher->registerMiddleware($c['SecurityMiddleware']); + + foreach($middleWares as $middleWare) { + $dispatcher->registerMiddleware($c[$middleWare]); + } + + $dispatcher->registerMiddleware($c['SessionMiddleware']); + return $dispatcher; + }); + + } + + + /** + * @deprecated implements only deprecated methods + * @return IApi + */ + function getCoreApi() + { + return $this->query('API'); + } + + /** + * @return \OCP\IServerContainer + */ + function getServer() + { + return OC::$server; + } + + /** + * @param string $middleWare + * @return boolean|null + */ + function registerMiddleWare($middleWare) { + array_push($this->middleWares, $middleWare); + } + + /** + * used to return the appname of the set application + * @return string the name of your application + */ + function getAppName() { + return $this->query('AppName'); + } + + /** + * @deprecated use IUserSession->isLoggedIn() + * @return boolean + */ + function isLoggedIn() { + return \OC_User::isLoggedIn(); + } + + /** + * @deprecated use IGroupManager->isAdmin($userId) + * @return boolean + */ + function isAdminUser() { + $uid = $this->getUserId(); + return \OC_User::isAdminUser($uid); + } + + private function getUserId() { + return $this->getServer()->getSession()->get('user_id'); + } + + /** + * @deprecated use the ILogger instead + * @param string $message + * @param string $level + * @return mixed + */ + function log($message, $level) { + switch($level){ + case 'debug': + $level = \OCP\Util::DEBUG; + break; + case 'info': + $level = \OCP\Util::INFO; + break; + case 'warn': + $level = \OCP\Util::WARN; + break; + case 'fatal': + $level = \OCP\Util::FATAL; + break; + default: + $level = \OCP\Util::ERROR; + break; + } + \OCP\Util::writeLog($this->getAppName(), $message, $level); + } + + /** + * Register a capability + * + * @param string $serviceName e.g. 'OCA\Files\Capabilities' + */ + public function registerCapability($serviceName) { + $this->query('OC\CapabilitiesManager')->registerCapability(function() use ($serviceName) { + return $this->query($serviceName); + }); + + } +} diff --git a/lib/private/AppFramework/Http.php b/lib/private/AppFramework/Http.php new file mode 100644 index 00000000000..a99f7ea437f --- /dev/null +++ b/lib/private/AppFramework/Http.php @@ -0,0 +1,154 @@ +<?php +/** + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework; + +use OCP\AppFramework\Http as BaseHttp; + +class Http extends BaseHttp { + + private $server; + private $protocolVersion; + protected $headers; + + /** + * @param array $server $_SERVER + * @param string $protocolVersion the http version to use defaults to HTTP/1.1 + */ + public function __construct($server, $protocolVersion='HTTP/1.1') { + $this->server = $server; + $this->protocolVersion = $protocolVersion; + + $this->headers = array( + self::STATUS_CONTINUE => 'Continue', + self::STATUS_SWITCHING_PROTOCOLS => 'Switching Protocols', + self::STATUS_PROCESSING => 'Processing', + self::STATUS_OK => 'OK', + self::STATUS_CREATED => 'Created', + self::STATUS_ACCEPTED => 'Accepted', + self::STATUS_NON_AUTHORATIVE_INFORMATION => 'Non-Authorative Information', + self::STATUS_NO_CONTENT => 'No Content', + self::STATUS_RESET_CONTENT => 'Reset Content', + self::STATUS_PARTIAL_CONTENT => 'Partial Content', + self::STATUS_MULTI_STATUS => 'Multi-Status', // RFC 4918 + self::STATUS_ALREADY_REPORTED => 'Already Reported', // RFC 5842 + self::STATUS_IM_USED => 'IM Used', // RFC 3229 + self::STATUS_MULTIPLE_CHOICES => 'Multiple Choices', + self::STATUS_MOVED_PERMANENTLY => 'Moved Permanently', + self::STATUS_FOUND => 'Found', + self::STATUS_SEE_OTHER => 'See Other', + self::STATUS_NOT_MODIFIED => 'Not Modified', + self::STATUS_USE_PROXY => 'Use Proxy', + self::STATUS_RESERVED => 'Reserved', + self::STATUS_TEMPORARY_REDIRECT => 'Temporary Redirect', + self::STATUS_BAD_REQUEST => 'Bad request', + self::STATUS_UNAUTHORIZED => 'Unauthorized', + self::STATUS_PAYMENT_REQUIRED => 'Payment Required', + self::STATUS_FORBIDDEN => 'Forbidden', + self::STATUS_NOT_FOUND => 'Not Found', + self::STATUS_METHOD_NOT_ALLOWED => 'Method Not Allowed', + self::STATUS_NOT_ACCEPTABLE => 'Not Acceptable', + self::STATUS_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', + self::STATUS_REQUEST_TIMEOUT => 'Request Timeout', + self::STATUS_CONFLICT => 'Conflict', + self::STATUS_GONE => 'Gone', + self::STATUS_LENGTH_REQUIRED => 'Length Required', + self::STATUS_PRECONDITION_FAILED => 'Precondition failed', + self::STATUS_REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large', + self::STATUS_REQUEST_URI_TOO_LONG => 'Request-URI Too Long', + self::STATUS_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', + self::STATUS_REQUEST_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable', + self::STATUS_EXPECTATION_FAILED => 'Expectation Failed', + self::STATUS_IM_A_TEAPOT => 'I\'m a teapot', // RFC 2324 + self::STATUS_UNPROCESSABLE_ENTITY => 'Unprocessable Entity', // RFC 4918 + self::STATUS_LOCKED => 'Locked', // RFC 4918 + self::STATUS_FAILED_DEPENDENCY => 'Failed Dependency', // RFC 4918 + self::STATUS_UPGRADE_REQUIRED => 'Upgrade required', + self::STATUS_PRECONDITION_REQUIRED => 'Precondition required', // draft-nottingham-http-new-status + self::STATUS_TOO_MANY_REQUESTS => 'Too Many Requests', // draft-nottingham-http-new-status + self::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', // draft-nottingham-http-new-status + self::STATUS_INTERNAL_SERVER_ERROR => 'Internal Server Error', + self::STATUS_NOT_IMPLEMENTED => 'Not Implemented', + self::STATUS_BAD_GATEWAY => 'Bad Gateway', + self::STATUS_SERVICE_UNAVAILABLE => 'Service Unavailable', + self::STATUS_GATEWAY_TIMEOUT => 'Gateway Timeout', + self::STATUS_HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version not supported', + self::STATUS_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates', + self::STATUS_INSUFFICIENT_STORAGE => 'Insufficient Storage', // RFC 4918 + self::STATUS_LOOP_DETECTED => 'Loop Detected', // RFC 5842 + self::STATUS_BANDWIDTH_LIMIT_EXCEEDED => 'Bandwidth Limit Exceeded', // non-standard + self::STATUS_NOT_EXTENDED => 'Not extended', + self::STATUS_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', // draft-nottingham-http-new-status + ); + } + + + /** + * Gets the correct header + * @param Http::CONSTANT $status the constant from the Http class + * @param \DateTime $lastModified formatted last modified date + * @param string $ETag the etag + * @return string + */ + public function getStatusHeader($status, \DateTime $lastModified=null, + $ETag=null) { + + if(!is_null($lastModified)) { + $lastModified = $lastModified->format(\DateTime::RFC2822); + } + + // if etag or lastmodified have not changed, return a not modified + if ((isset($this->server['HTTP_IF_NONE_MATCH']) + && trim(trim($this->server['HTTP_IF_NONE_MATCH']), '"') === (string)$ETag) + + || + + (isset($this->server['HTTP_IF_MODIFIED_SINCE']) + && trim($this->server['HTTP_IF_MODIFIED_SINCE']) === + $lastModified)) { + + $status = self::STATUS_NOT_MODIFIED; + } + + // we have one change currently for the http 1.0 header that differs + // from 1.1: STATUS_TEMPORARY_REDIRECT should be STATUS_FOUND + // if this differs any more, we want to create childclasses for this + if($status === self::STATUS_TEMPORARY_REDIRECT + && $this->protocolVersion === 'HTTP/1.0') { + + $status = self::STATUS_FOUND; + } + + return $this->protocolVersion . ' ' . $status . ' ' . + $this->headers[$status]; + } + + +} + + diff --git a/lib/private/AppFramework/Http/Dispatcher.php b/lib/private/AppFramework/Http/Dispatcher.php new file mode 100644 index 00000000000..641339c0d94 --- /dev/null +++ b/lib/private/AppFramework/Http/Dispatcher.php @@ -0,0 +1,179 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Georg Ehrke <georg@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework\Http; + +use \OC\AppFramework\Middleware\MiddlewareDispatcher; +use \OC\AppFramework\Http; +use \OC\AppFramework\Utility\ControllerMethodReflector; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\DataResponse; +use OCP\IRequest; + + +/** + * Class to dispatch the request to the middleware dispatcher + */ +class Dispatcher { + + private $middlewareDispatcher; + private $protocol; + private $reflector; + private $request; + + /** + * @param Http $protocol the http protocol with contains all status headers + * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which + * runs the middleware + * @param ControllerMethodReflector $reflector the reflector that is used to inject + * the arguments for the controller + * @param IRequest $request the incoming request + */ + public function __construct(Http $protocol, + MiddlewareDispatcher $middlewareDispatcher, + ControllerMethodReflector $reflector, + IRequest $request) { + $this->protocol = $protocol; + $this->middlewareDispatcher = $middlewareDispatcher; + $this->reflector = $reflector; + $this->request = $request; + } + + + /** + * Handles a request and calls the dispatcher on the controller + * @param Controller $controller the controller which will be called + * @param string $methodName the method name which will be called on + * the controller + * @return array $array[0] contains a string with the http main header, + * $array[1] contains headers in the form: $key => value, $array[2] contains + * the response output + * @throws \Exception + */ + public function dispatch(Controller $controller, $methodName) { + $out = array(null, array(), null); + + try { + // prefill reflector with everything thats needed for the + // middlewares + $this->reflector->reflect($controller, $methodName); + + $this->middlewareDispatcher->beforeController($controller, + $methodName); + $response = $this->executeController($controller, $methodName); + + // if an exception appears, the middleware checks if it can handle the + // exception and creates a response. If no response is created, it is + // assumed that theres no middleware who can handle it and the error is + // thrown again + } catch(\Exception $exception){ + $response = $this->middlewareDispatcher->afterException( + $controller, $methodName, $exception); + if (is_null($response)) { + throw $exception; + } + } + + $response = $this->middlewareDispatcher->afterController( + $controller, $methodName, $response); + + // depending on the cache object the headers need to be changed + $out[0] = $this->protocol->getStatusHeader($response->getStatus(), + $response->getLastModified(), $response->getETag()); + $out[1] = array_merge($response->getHeaders()); + $out[2] = $response->getCookies(); + $out[3] = $this->middlewareDispatcher->beforeOutput( + $controller, $methodName, $response->render() + ); + $out[4] = $response; + + return $out; + } + + + /** + * Uses the reflected parameters, types and request parameters to execute + * the controller + * @param Controller $controller the controller to be executed + * @param string $methodName the method on the controller that should be executed + * @return Response + */ + private function executeController($controller, $methodName) { + $arguments = array(); + + // valid types that will be casted + $types = array('int', 'integer', 'bool', 'boolean', 'float'); + + foreach($this->reflector->getParameters() as $param => $default) { + + // try to get the parameter from the request object and cast + // it to the type annotated in the @param annotation + $value = $this->request->getParam($param, $default); + $type = $this->reflector->getType($param); + + // if this is submitted using GET or a POST form, 'false' should be + // converted to false + if(($type === 'bool' || $type === 'boolean') && + $value === 'false' && + ( + $this->request->method === 'GET' || + strpos($this->request->getHeader('Content-Type'), + 'application/x-www-form-urlencoded') !== false + ) + ) { + $value = false; + + } elseif($value !== null && in_array($type, $types)) { + settype($value, $type); + } + + $arguments[] = $value; + } + + $response = call_user_func_array(array($controller, $methodName), $arguments); + + // format response + if($response instanceof DataResponse || !($response instanceof Response)) { + + // get format from the url format or request format parameter + $format = $this->request->getParam('format'); + + // if none is given try the first Accept header + if($format === null) { + $headers = $this->request->getHeader('Accept'); + $format = $controller->getResponderByHTTPHeader($headers); + } + + $response = $controller->buildResponse($response, $format); + } + + return $response; + } + +} diff --git a/lib/private/AppFramework/Http/Output.php b/lib/private/AppFramework/Http/Output.php new file mode 100644 index 00000000000..469c809c21c --- /dev/null +++ b/lib/private/AppFramework/Http/Output.php @@ -0,0 +1,92 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Lukas Reschke <lukas@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Http; + +use OCP\AppFramework\Http\IOutput; + +/** + * Very thin wrapper class to make output testable + */ +class Output implements IOutput { + /** @var string */ + private $webRoot; + + /** + * @param $webRoot + */ + public function __construct($webRoot) { + $this->webRoot = $webRoot; + } + + /** + * @param string $out + */ + public function setOutput($out) { + print($out); + } + + /** + * @param string $path + * + * @return bool false if an error occurred + */ + public function setReadfile($path) { + return @readfile($path); + } + + /** + * @param string $header + */ + public function setHeader($header) { + header($header); + } + + /** + * @param int $code sets the http status code + */ + public function setHttpResponseCode($code) { + http_response_code($code); + } + + /** + * @return int returns the current http response code + */ + public function getHttpResponseCode() { + return http_response_code(); + } + + /** + * @param string $name + * @param string $value + * @param int $expire + * @param string $path + * @param string $domain + * @param bool $secure + * @param bool $httpOnly + */ + public function setCookie($name, $value, $expire, $path, $domain, $secure, $httpOnly) { + $path = $this->webRoot ? : '/'; + setcookie($name, $value, $expire, $path, $domain, $secure, $httpOnly); + } + +} diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php new file mode 100644 index 00000000000..7cd8cedcfdd --- /dev/null +++ b/lib/private/AppFramework/Http/Request.php @@ -0,0 +1,771 @@ +<?php +/** + * @author Bart Visscher <bartv@thisnet.nl> + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Mitar <mitar.git@tnode.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <rullzer@owncloud.com> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Http; + +use OC\Security\CSRF\CsrfToken; +use OC\Security\CSRF\CsrfTokenManager; +use OC\Security\TrustedDomainHelper; +use OCP\IConfig; +use OCP\IRequest; +use OCP\Security\ICrypto; +use OCP\Security\ISecureRandom; + +/** + * Class for accessing variables in the request. + * This class provides an immutable object with request variables. + * + * @property mixed[] cookies + * @property mixed[] env + * @property mixed[] files + * @property string method + * @property mixed[] parameters + * @property mixed[] server + */ +class Request implements \ArrayAccess, \Countable, IRequest { + + const USER_AGENT_IE = '/(MSIE)|(Trident)/'; + const USER_AGENT_IE_8 = '/MSIE 8.0/'; + // Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx + const USER_AGENT_MS_EDGE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/'; + // Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference + const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/'; + // Chrome User Agent from https://developer.chrome.com/multidevice/user-agent + const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/'; + // Safari User Agent from http://www.useragentstring.com/pages/Safari/ + const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/'; + // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent + const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#'; + const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#'; + const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/'; + const USER_AGENT_OWNCLOUD_ANDROID = '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/'; + const USER_AGENT_OWNCLOUD_DESKTOP = '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/'; + const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost)$/'; + + protected $inputStream; + protected $content; + protected $items = array(); + protected $allowedKeys = array( + 'get', + 'post', + 'files', + 'server', + 'env', + 'cookies', + 'urlParams', + 'parameters', + 'method', + 'requesttoken', + ); + /** @var ISecureRandom */ + protected $secureRandom; + /** @var IConfig */ + protected $config; + /** @var string */ + protected $requestId = ''; + /** @var ICrypto */ + protected $crypto; + /** @var CsrfTokenManager|null */ + protected $csrfTokenManager; + + /** @var bool */ + protected $contentDecoded = false; + + /** + * @param array $vars An associative array with the following optional values: + * - array 'urlParams' the parameters which were matched from the URL + * - array 'get' the $_GET array + * - array|string 'post' the $_POST array or JSON string + * - array 'files' the $_FILES array + * - array 'server' the $_SERVER array + * - array 'env' the $_ENV array + * - array 'cookies' the $_COOKIE array + * - string 'method' the request method (GET, POST etc) + * - string|false 'requesttoken' the requesttoken or false when not available + * @param ISecureRandom $secureRandom + * @param IConfig $config + * @param CsrfTokenManager|null $csrfTokenManager + * @param string $stream + * @see http://www.php.net/manual/en/reserved.variables.php + */ + public function __construct(array $vars=array(), + ISecureRandom $secureRandom = null, + IConfig $config, + CsrfTokenManager $csrfTokenManager = null, + $stream = 'php://input') { + $this->inputStream = $stream; + $this->items['params'] = array(); + $this->secureRandom = $secureRandom; + $this->config = $config; + $this->csrfTokenManager = $csrfTokenManager; + + if(!array_key_exists('method', $vars)) { + $vars['method'] = 'GET'; + } + + foreach($this->allowedKeys as $name) { + $this->items[$name] = isset($vars[$name]) + ? $vars[$name] + : array(); + } + + $this->items['parameters'] = array_merge( + $this->items['get'], + $this->items['post'], + $this->items['urlParams'], + $this->items['params'] + ); + + } + /** + * @param array $parameters + */ + public function setUrlParameters(array $parameters) { + $this->items['urlParams'] = $parameters; + $this->items['parameters'] = array_merge( + $this->items['parameters'], + $this->items['urlParams'] + ); + } + + /** + * Countable method + * @return int + */ + public function count() { + return count(array_keys($this->items['parameters'])); + } + + /** + * ArrayAccess methods + * + * Gives access to the combined GET, POST and urlParams arrays + * + * Examples: + * + * $var = $request['myvar']; + * + * or + * + * if(!isset($request['myvar']) { + * // Do something + * } + * + * $request['myvar'] = 'something'; // This throws an exception. + * + * @param string $offset The key to lookup + * @return boolean + */ + public function offsetExists($offset) { + return isset($this->items['parameters'][$offset]); + } + + /** + * @see offsetExists + */ + public function offsetGet($offset) { + return isset($this->items['parameters'][$offset]) + ? $this->items['parameters'][$offset] + : null; + } + + /** + * @see offsetExists + */ + public function offsetSet($offset, $value) { + throw new \RuntimeException('You cannot change the contents of the request object'); + } + + /** + * @see offsetExists + */ + public function offsetUnset($offset) { + throw new \RuntimeException('You cannot change the contents of the request object'); + } + + /** + * Magic property accessors + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) { + throw new \RuntimeException('You cannot change the contents of the request object'); + } + + /** + * Access request variables by method and name. + * Examples: + * + * $request->post['myvar']; // Only look for POST variables + * $request->myvar; or $request->{'myvar'}; or $request->{$myvar} + * Looks in the combined GET, POST and urlParams array. + * + * If you access e.g. ->post but the current HTTP request method + * is GET a \LogicException will be thrown. + * + * @param string $name The key to look for. + * @throws \LogicException + * @return mixed|null + */ + public function __get($name) { + switch($name) { + case 'put': + case 'patch': + case 'get': + case 'post': + if($this->method !== strtoupper($name)) { + throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method)); + } + return $this->getContent(); + case 'files': + case 'server': + case 'env': + case 'cookies': + case 'urlParams': + case 'method': + return isset($this->items[$name]) + ? $this->items[$name] + : null; + case 'parameters': + case 'params': + return $this->getContent(); + default; + return isset($this[$name]) + ? $this[$name] + : null; + } + } + + /** + * @param string $name + * @return bool + */ + public function __isset($name) { + return isset($this->items['parameters'][$name]); + } + + /** + * @param string $id + */ + public function __unset($id) { + throw new \RuntimeException('You cannot change the contents of the request object'); + } + + /** + * Returns the value for a specific http header. + * + * This method returns null if the header did not exist. + * + * @param string $name + * @return string + */ + public function getHeader($name) { + + $name = strtoupper(str_replace(array('-'),array('_'),$name)); + if (isset($this->server['HTTP_' . $name])) { + return $this->server['HTTP_' . $name]; + } + + // There's a few headers that seem to end up in the top-level + // server array. + switch($name) { + case 'CONTENT_TYPE' : + case 'CONTENT_LENGTH' : + if (isset($this->server[$name])) { + return $this->server[$name]; + } + break; + + } + + return null; + } + + /** + * Lets you access post and get parameters by the index + * In case of json requests the encoded json body is accessed + * + * @param string $key the key which you want to access in the URL Parameter + * placeholder, $_POST or $_GET array. + * The priority how they're returned is the following: + * 1. URL parameters + * 2. POST parameters + * 3. GET parameters + * @param mixed $default If the key is not found, this value will be returned + * @return mixed the content of the array + */ + public function getParam($key, $default = null) { + return isset($this->parameters[$key]) + ? $this->parameters[$key] + : $default; + } + + /** + * Returns all params that were received, be it from the request + * (as GET or POST) or throuh the URL by the route + * @return array the array with all parameters + */ + public function getParams() { + return $this->parameters; + } + + /** + * Returns the method of the request + * @return string the method of the request (POST, GET, etc) + */ + public function getMethod() { + return $this->method; + } + + /** + * Shortcut for accessing an uploaded file through the $_FILES array + * @param string $key the key that will be taken from the $_FILES array + * @return array the file in the $_FILES element + */ + public function getUploadedFile($key) { + return isset($this->files[$key]) ? $this->files[$key] : null; + } + + /** + * Shortcut for getting env variables + * @param string $key the key that will be taken from the $_ENV array + * @return array the value in the $_ENV element + */ + public function getEnv($key) { + return isset($this->env[$key]) ? $this->env[$key] : null; + } + + /** + * Shortcut for getting cookie variables + * @param string $key the key that will be taken from the $_COOKIE array + * @return string the value in the $_COOKIE element + */ + public function getCookie($key) { + return isset($this->cookies[$key]) ? $this->cookies[$key] : null; + } + + /** + * Returns the request body content. + * + * If the HTTP request method is PUT and the body + * not application/x-www-form-urlencoded or application/json a stream + * resource is returned, otherwise an array. + * + * @return array|string|resource The request body content or a resource to read the body stream. + * + * @throws \LogicException + */ + protected function getContent() { + // If the content can't be parsed into an array then return a stream resource. + if ($this->method === 'PUT' + && strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false + && strpos($this->getHeader('Content-Type'), 'application/json') === false + ) { + if ($this->content === false) { + throw new \LogicException( + '"put" can only be accessed once if not ' + . 'application/x-www-form-urlencoded or application/json.' + ); + } + $this->content = false; + return fopen($this->inputStream, 'rb'); + } else { + $this->decodeContent(); + return $this->items['parameters']; + } + } + + /** + * Attempt to decode the content and populate parameters + */ + protected function decodeContent() { + if ($this->contentDecoded) { + return; + } + $params = []; + + // 'application/json' must be decoded manually. + if (strpos($this->getHeader('Content-Type'), 'application/json') !== false) { + $params = json_decode(file_get_contents($this->inputStream), true); + if(count($params) > 0) { + $this->items['params'] = $params; + if($this->method === 'POST') { + $this->items['post'] = $params; + } + } + + // Handle application/x-www-form-urlencoded for methods other than GET + // or post correctly + } elseif($this->method !== 'GET' + && $this->method !== 'POST' + && strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) { + + parse_str(file_get_contents($this->inputStream), $params); + if(is_array($params)) { + $this->items['params'] = $params; + } + } + + if (is_array($params)) { + $this->items['parameters'] = array_merge($this->items['parameters'], $params); + } + $this->contentDecoded = true; + } + + + /** + * Checks if the CSRF check was correct + * @return bool true if CSRF check passed + */ + public function passesCSRFCheck() { + if($this->csrfTokenManager === null) { + return false; + } + + if (isset($this->items['get']['requesttoken'])) { + $token = $this->items['get']['requesttoken']; + } elseif (isset($this->items['post']['requesttoken'])) { + $token = $this->items['post']['requesttoken']; + } elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) { + $token = $this->items['server']['HTTP_REQUESTTOKEN']; + } else { + //no token found. + return false; + } + $token = new CsrfToken($token); + + return $this->csrfTokenManager->isTokenValid($token); + } + + /** + * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging + * If `mod_unique_id` is installed this value will be taken. + * @return string + */ + public function getId() { + if(isset($this->server['UNIQUE_ID'])) { + return $this->server['UNIQUE_ID']; + } + + if(empty($this->requestId)) { + $this->requestId = $this->secureRandom->generate(20); + } + + return $this->requestId; + } + + /** + * Returns the remote address, if the connection came from a trusted proxy + * and `forwarded_for_headers` has been configured then the IP address + * specified in this header will be returned instead. + * Do always use this instead of $_SERVER['REMOTE_ADDR'] + * @return string IP address + */ + public function getRemoteAddress() { + $remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : ''; + $trustedProxies = $this->config->getSystemValue('trusted_proxies', []); + + if(is_array($trustedProxies) && in_array($remoteAddress, $trustedProxies)) { + $forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [ + 'HTTP_X_FORWARDED_FOR' + // only have one default, so we cannot ship an insecure product out of the box + ]); + + foreach($forwardedForHeaders as $header) { + if(isset($this->server[$header])) { + foreach(explode(',', $this->server[$header]) as $IP) { + $IP = trim($IP); + if (filter_var($IP, FILTER_VALIDATE_IP) !== false) { + return $IP; + } + } + } + } + } + + return $remoteAddress; + } + + /** + * Check overwrite condition + * @param string $type + * @return bool + */ + private function isOverwriteCondition($type = '') { + $regex = '/' . $this->config->getSystemValue('overwritecondaddr', '') . '/'; + $remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : ''; + return $regex === '//' || preg_match($regex, $remoteAddr) === 1 + || $type !== 'protocol'; + } + + /** + * Returns the server protocol. It respects one or more reverse proxies servers + * and load balancers + * @return string Server protocol (http or https) + */ + public function getServerProtocol() { + if($this->config->getSystemValue('overwriteprotocol') !== '' + && $this->isOverwriteCondition('protocol')) { + return $this->config->getSystemValue('overwriteprotocol'); + } + + if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) { + if (strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) { + $parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']); + $proto = strtolower(trim($parts[0])); + } else { + $proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']); + } + + // Verify that the protocol is always HTTP or HTTPS + // default to http if an invalid value is provided + return $proto === 'https' ? 'https' : 'http'; + } + + if (isset($this->server['HTTPS']) + && $this->server['HTTPS'] !== null + && $this->server['HTTPS'] !== 'off' + && $this->server['HTTPS'] !== '') { + return 'https'; + } + + return 'http'; + } + + /** + * Returns the used HTTP protocol. + * + * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0. + */ + public function getHttpProtocol() { + $claimedProtocol = strtoupper($this->server['SERVER_PROTOCOL']); + + $validProtocols = [ + 'HTTP/1.0', + 'HTTP/1.1', + 'HTTP/2', + ]; + + if(in_array($claimedProtocol, $validProtocols, true)) { + return $claimedProtocol; + } + + return 'HTTP/1.1'; + } + + /** + * Returns the request uri, even if the website uses one or more + * reverse proxies + * @return string + */ + public function getRequestUri() { + $uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; + if($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) { + $uri = $this->getScriptName() . substr($uri, strlen($this->server['SCRIPT_NAME'])); + } + return $uri; + } + + /** + * Get raw PathInfo from request (not urldecoded) + * @throws \Exception + * @return string Path info + */ + public function getRawPathInfo() { + $requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : ''; + // remove too many leading slashes - can be caused by reverse proxy configuration + if (strpos($requestUri, '/') === 0) { + $requestUri = '/' . ltrim($requestUri, '/'); + } + + $requestUri = preg_replace('%/{2,}%', '/', $requestUri); + + // Remove the query string from REQUEST_URI + if ($pos = strpos($requestUri, '?')) { + $requestUri = substr($requestUri, 0, $pos); + } + + $scriptName = $this->server['SCRIPT_NAME']; + $pathInfo = $requestUri; + + // strip off the script name's dir and file name + // FIXME: Sabre does not really belong here + list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($scriptName); + if (!empty($path)) { + if($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) { + $pathInfo = substr($pathInfo, strlen($path)); + } else { + throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')"); + } + } + if (strpos($pathInfo, '/'.$name) === 0) { + $pathInfo = substr($pathInfo, strlen($name) + 1); + } + if (strpos($pathInfo, $name) === 0) { + $pathInfo = substr($pathInfo, strlen($name)); + } + if($pathInfo === false || $pathInfo === '/'){ + return ''; + } else { + return $pathInfo; + } + } + + /** + * Get PathInfo from request + * @throws \Exception + * @return string|false Path info or false when not found + */ + public function getPathInfo() { + if(isset($this->server['PATH_INFO'])) { + return $this->server['PATH_INFO']; + } + + $pathInfo = $this->getRawPathInfo(); + // following is taken from \Sabre\HTTP\URLUtil::decodePathSegment + $pathInfo = rawurldecode($pathInfo); + $encoding = mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']); + + switch($encoding) { + case 'ISO-8859-1' : + $pathInfo = utf8_encode($pathInfo); + } + // end copy + + return $pathInfo; + } + + /** + * Returns the script name, even if the website uses one or more + * reverse proxies + * @return string the script name + */ + public function getScriptName() { + $name = $this->server['SCRIPT_NAME']; + $overwriteWebRoot = $this->config->getSystemValue('overwritewebroot'); + if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) { + // FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous + $serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -strlen('lib/private/appframework/http/'))); + $suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), strlen($serverRoot))); + $name = '/' . ltrim($overwriteWebRoot . $suburi, '/'); + } + return $name; + } + + /** + * Checks whether the user agent matches a given regex + * @param array $agent array of agent names + * @return bool true if at least one of the given agent matches, false otherwise + */ + public function isUserAgent(array $agent) { + if (!isset($this->server['HTTP_USER_AGENT'])) { + return false; + } + foreach ($agent as $regex) { + if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) { + return true; + } + } + return false; + } + + /** + * Returns the unverified server host from the headers without checking + * whether it is a trusted domain + * @return string Server host + */ + public function getInsecureServerHost() { + $host = 'localhost'; + if (isset($this->server['HTTP_X_FORWARDED_HOST'])) { + if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) { + $parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']); + $host = trim(current($parts)); + } else { + $host = $this->server['HTTP_X_FORWARDED_HOST']; + } + } else { + if (isset($this->server['HTTP_HOST'])) { + $host = $this->server['HTTP_HOST']; + } else if (isset($this->server['SERVER_NAME'])) { + $host = $this->server['SERVER_NAME']; + } + } + return $host; + } + + + /** + * Returns the server host from the headers, or the first configured + * trusted domain if the host isn't in the trusted list + * @return string Server host + */ + public function getServerHost() { + // overwritehost is always trusted + $host = $this->getOverwriteHost(); + if ($host !== null) { + return $host; + } + + // get the host from the headers + $host = $this->getInsecureServerHost(); + + // Verify that the host is a trusted domain if the trusted domains + // are defined + // If no trusted domain is provided the first trusted domain is returned + $trustedDomainHelper = new TrustedDomainHelper($this->config); + if ($trustedDomainHelper->isTrustedDomain($host)) { + return $host; + } else { + $trustedList = $this->config->getSystemValue('trusted_domains', []); + if(!empty($trustedList)) { + return $trustedList[0]; + } else { + return ''; + } + } + } + + /** + * Returns the overwritehost setting from the config if set and + * if the overwrite condition is met + * @return string|null overwritehost value or null if not defined or the defined condition + * isn't met + */ + private function getOverwriteHost() { + if($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) { + return $this->config->getSystemValue('overwritehost'); + } + return null; + } + +} diff --git a/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php new file mode 100644 index 00000000000..4bd25f79bba --- /dev/null +++ b/lib/private/AppFramework/Middleware/MiddlewareDispatcher.php @@ -0,0 +1,162 @@ +<?php +/** + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework\Middleware; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\MiddleWare; + +/** + * This class is used to store and run all the middleware in correct order + */ +class MiddlewareDispatcher { + + /** + * @var array array containing all the middlewares + */ + private $middlewares; + + /** + * @var int counter which tells us what middlware was executed once an + * exception occurs + */ + private $middlewareCounter; + + + /** + * Constructor + */ + public function __construct(){ + $this->middlewares = array(); + $this->middlewareCounter = 0; + } + + + /** + * Adds a new middleware + * @param Middleware $middleWare the middleware which will be added + */ + public function registerMiddleware(Middleware $middleWare){ + array_push($this->middlewares, $middleWare); + } + + + /** + * returns an array with all middleware elements + * @return array the middlewares + */ + public function getMiddlewares(){ + return $this->middlewares; + } + + + /** + * This is being run in normal order before the controller is being + * called which allows several modifications and checks + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + */ + public function beforeController(Controller $controller, $methodName){ + // we need to count so that we know which middlewares we have to ask in + // case there is an exception + $middlewareCount = count($this->middlewares); + for($i = 0; $i < $middlewareCount; $i++){ + $this->middlewareCounter++; + $middleware = $this->middlewares[$i]; + $middleware->beforeController($controller, $methodName); + } + } + + + /** + * This is being run when either the beforeController method or the + * controller method itself is throwing an exception. The middleware is asked + * in reverse order to handle the exception and to return a response. + * If the response is null, it is assumed that the exception could not be + * handled and the error will be thrown again + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param \Exception $exception the thrown exception + * @return Response a Response object if the middleware can handle the + * exception + * @throws \Exception the passed in exception if it can't handle it + */ + public function afterException(Controller $controller, $methodName, \Exception $exception){ + for($i=$this->middlewareCounter-1; $i>=0; $i--){ + $middleware = $this->middlewares[$i]; + try { + return $middleware->afterException($controller, $methodName, $exception); + } catch(\Exception $exception){ + continue; + } + } + throw $exception; + } + + + /** + * This is being run after a successful controllermethod call and allows + * the manipulation of a Response object. The middleware is run in reverse order + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param Response $response the generated response from the controller + * @return Response a Response object + */ + public function afterController(Controller $controller, $methodName, Response $response){ + for($i=count($this->middlewares)-1; $i>=0; $i--){ + $middleware = $this->middlewares[$i]; + $response = $middleware->afterController($controller, $methodName, $response); + } + return $response; + } + + + /** + * This is being run after the response object has been rendered and + * allows the manipulation of the output. The middleware is run in reverse order + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param string $output the generated output from a response + * @return string the output that should be printed + */ + public function beforeOutput(Controller $controller, $methodName, $output){ + for($i=count($this->middlewares)-1; $i>=0; $i--){ + $middleware = $this->middlewares[$i]; + $output = $middleware->beforeOutput($controller, $methodName, $output); + } + return $output; + } + +} diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php new file mode 100644 index 00000000000..258119b326a --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -0,0 +1,155 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Middleware\Security; + +use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; + +/** + * This middleware sets the correct CORS headers on a response if the + * controller has the @CORS annotation. This is needed for webapps that want + * to access an API and don't run on the same domain, see + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS + */ +class CORSMiddleware extends Middleware { + + /** + * @var IRequest + */ + private $request; + + /** + * @var ControllerMethodReflector + */ + private $reflector; + + /** + * @var IUserSession + */ + private $session; + + /** + * @param IRequest $request + * @param ControllerMethodReflector $reflector + * @param IUserSession $session + */ + public function __construct(IRequest $request, + ControllerMethodReflector $reflector, + IUserSession $session) { + $this->request = $request; + $this->reflector = $reflector; + $this->session = $session; + } + + /** + * This is being run in normal order before the controller is being + * called which allows several modifications and checks + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @throws SecurityException + * @since 6.0.0 + */ + public function beforeController($controller, $methodName){ + // ensure that @CORS annotated API routes are not used in conjunction + // with session authentication since this enables CSRF attack vectors + if ($this->reflector->hasAnnotation('CORS') && + !$this->reflector->hasAnnotation('PublicPage')) { + $user = $this->request->server['PHP_AUTH_USER']; + $pass = $this->request->server['PHP_AUTH_PW']; + + $this->session->logout(); + if(!$this->session->login($user, $pass)) { + throw new SecurityException('CORS requires basic auth', Http::STATUS_UNAUTHORIZED); + } + } + } + + /** + * This is being run after a successful controllermethod call and allows + * the manipulation of a Response object. The middleware is run in reverse order + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param Response $response the generated response from the controller + * @return Response a Response object + * @throws SecurityException + */ + public function afterController($controller, $methodName, Response $response){ + // only react if its a CORS request and if the request sends origin and + + if(isset($this->request->server['HTTP_ORIGIN']) && + $this->reflector->hasAnnotation('CORS')) { + + // allow credentials headers must not be true or CSRF is possible + // otherwise + foreach($response->getHeaders() as $header => $value) { + if(strtolower($header) === 'access-control-allow-credentials' && + strtolower(trim($value)) === 'true') { + $msg = 'Access-Control-Allow-Credentials must not be '. + 'set to true in order to prevent CSRF'; + throw new SecurityException($msg); + } + } + + $origin = $this->request->server['HTTP_ORIGIN']; + $response->addHeader('Access-Control-Allow-Origin', $origin); + } + return $response; + } + + /** + * If an SecurityException is being caught return a JSON error response + * + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param \Exception $exception the thrown exception + * @throws \Exception the passed in exception if it can't handle it + * @return Response a Response object or null in case that the exception could not be handled + */ + public function afterException($controller, $methodName, \Exception $exception){ + if($exception instanceof SecurityException){ + $response = new JSONResponse(['message' => $exception->getMessage()]); + if($exception->getCode() !== 0) { + $response->setStatus($exception->getCode()); + } else { + $response->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR); + } + return $response; + } + + throw $exception; + } + +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php new file mode 100644 index 00000000000..59e247f3307 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/AppNotEnabledException.php @@ -0,0 +1,38 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\Appframework\Middleware\Security\Exceptions; + +use OCP\AppFramework\Http; + +/** + * Class AppNotEnabledException is thrown when a resource for an application is + * requested that is not enabled. + * + * @package OC\Appframework\Middleware\Security\Exceptions + */ +class AppNotEnabledException extends SecurityException { + public function __construct() { + parent::__construct('App is not enabled', Http::STATUS_PRECONDITION_FAILED); + } +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/CrossSiteRequestForgeryException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/CrossSiteRequestForgeryException.php new file mode 100644 index 00000000000..0eeb81730d4 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/CrossSiteRequestForgeryException.php @@ -0,0 +1,38 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\Appframework\Middleware\Security\Exceptions; + +use OCP\AppFramework\Http; + +/** + * Class CrossSiteRequestForgeryException is thrown when a CSRF exception has + * been encountered. + * + * @package OC\Appframework\Middleware\Security\Exceptions + */ +class CrossSiteRequestForgeryException extends SecurityException { + public function __construct() { + parent::__construct('CSRF check failed', Http::STATUS_PRECONDITION_FAILED); + } +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/NotAdminException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/NotAdminException.php new file mode 100644 index 00000000000..be0f2f9d2a9 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/NotAdminException.php @@ -0,0 +1,38 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\Appframework\Middleware\Security\Exceptions; + +use OCP\AppFramework\Http; + +/** + * Class NotAdminException is thrown when a resource has been requested by a + * non-admin user that is not accessible to non-admin users. + * + * @package OC\Appframework\Middleware\Security\Exceptions + */ +class NotAdminException extends SecurityException { + public function __construct() { + parent::__construct('Logged in user must be an admin', Http::STATUS_FORBIDDEN); + } +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/NotLoggedInException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/NotLoggedInException.php new file mode 100644 index 00000000000..f5b2e032032 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/NotLoggedInException.php @@ -0,0 +1,38 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\Appframework\Middleware\Security\Exceptions; + +use OCP\AppFramework\Http; + +/** + * Class NotLoggedInException is thrown when a resource has been requested by a + * guest user that is not accessible to the public. + * + * @package OC\Appframework\Middleware\Security\Exceptions + */ +class NotLoggedInException extends SecurityException { + public function __construct() { + parent::__construct('Current user is not logged in', Http::STATUS_UNAUTHORIZED); + } +} diff --git a/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php b/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php new file mode 100644 index 00000000000..c86614ec477 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/Exceptions/SecurityException.php @@ -0,0 +1,32 @@ +<?php +/** + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Middleware\Security\Exceptions; + +/** + * Class SecurityException is the base class for security exceptions thrown by + * the security middleware. + * + * @package OC\AppFramework\Middleware\Security\Exceptions + */ +class SecurityException extends \Exception {} diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php new file mode 100644 index 00000000000..4afd29cd060 --- /dev/null +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -0,0 +1,215 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Thomas Tanghus <thomas@tanghus.net> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework\Middleware\Security; + +use OC\Appframework\Middleware\Security\Exceptions\AppNotEnabledException; +use OC\Appframework\Middleware\Security\Exceptions\CrossSiteRequestForgeryException; +use OC\Appframework\Middleware\Security\Exceptions\NotAdminException; +use OC\Appframework\Middleware\Security\Exceptions\NotLoggedInException; +use OC\AppFramework\Utility\ControllerMethodReflector; +use OC\Security\CSP\ContentSecurityPolicyManager; +use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Middleware; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\JSONResponse; +use OCP\INavigationManager; +use OCP\IURLGenerator; +use OCP\IRequest; +use OCP\ILogger; +use OCP\AppFramework\Controller; +use OCP\Util; +use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; + +/** + * Used to do all the authentication and checking stuff for a controller method + * It reads out the annotations of a controller method and checks which if + * security things should be checked and also handles errors in case a security + * check fails + */ +class SecurityMiddleware extends Middleware { + /** @var INavigationManager */ + private $navigationManager; + /** @var IRequest */ + private $request; + /** @var ControllerMethodReflector */ + private $reflector; + /** @var string */ + private $appName; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var ILogger */ + private $logger; + /** @var bool */ + private $isLoggedIn; + /** @var bool */ + private $isAdminUser; + /** @var ContentSecurityPolicyManager */ + private $contentSecurityPolicyManager; + + /** + * @param IRequest $request + * @param ControllerMethodReflector $reflector + * @param INavigationManager $navigationManager + * @param IURLGenerator $urlGenerator + * @param ILogger $logger + * @param string $appName + * @param bool $isLoggedIn + * @param bool $isAdminUser + * @param ContentSecurityPolicyManager $contentSecurityPolicyManager + */ + public function __construct(IRequest $request, + ControllerMethodReflector $reflector, + INavigationManager $navigationManager, + IURLGenerator $urlGenerator, + ILogger $logger, + $appName, + $isLoggedIn, + $isAdminUser, + ContentSecurityPolicyManager $contentSecurityPolicyManager) { + $this->navigationManager = $navigationManager; + $this->request = $request; + $this->reflector = $reflector; + $this->appName = $appName; + $this->urlGenerator = $urlGenerator; + $this->logger = $logger; + $this->isLoggedIn = $isLoggedIn; + $this->isAdminUser = $isAdminUser; + $this->contentSecurityPolicyManager = $contentSecurityPolicyManager; + } + + + /** + * This runs all the security checks before a method call. The + * security checks are determined by inspecting the controller method + * annotations + * @param string $controller the controllername or string + * @param string $methodName the name of the method + * @throws SecurityException when a security check fails + */ + public function beforeController($controller, $methodName) { + + // this will set the current navigation entry of the app, use this only + // for normal HTML requests and not for AJAX requests + $this->navigationManager->setActiveEntry($this->appName); + + // security checks + $isPublicPage = $this->reflector->hasAnnotation('PublicPage'); + if(!$isPublicPage) { + if(!$this->isLoggedIn) { + throw new NotLoggedInException(); + } + + if(!$this->reflector->hasAnnotation('NoAdminRequired')) { + if(!$this->isAdminUser) { + throw new NotAdminException(); + } + } + } + + // CSRF check - also registers the CSRF token since the session may be closed later + Util::callRegister(); + if(!$this->reflector->hasAnnotation('NoCSRFRequired')) { + if(!$this->request->passesCSRFCheck()) { + throw new CrossSiteRequestForgeryException(); + } + } + + /** + * FIXME: Use DI once available + * Checks if app is enabled (also includes a check whether user is allowed to access the resource) + * The getAppPath() check is here since components such as settings also use the AppFramework and + * therefore won't pass this check. + */ + if(\OC_App::getAppPath($this->appName) !== false && !\OC_App::isEnabled($this->appName)) { + throw new AppNotEnabledException(); + } + + } + + /** + * Performs the default CSP modifications that may be injected by other + * applications + * + * @param Controller $controller + * @param string $methodName + * @param Response $response + * @return Response + */ + public function afterController($controller, $methodName, Response $response) { + $policy = !is_null($response->getContentSecurityPolicy()) ? $response->getContentSecurityPolicy() : new ContentSecurityPolicy(); + + $defaultPolicy = $this->contentSecurityPolicyManager->getDefaultPolicy(); + $defaultPolicy = $this->contentSecurityPolicyManager->mergePolicies($defaultPolicy, $policy); + + $response->setContentSecurityPolicy($defaultPolicy); + + return $response; + } + + /** + * If an SecurityException is being caught, ajax requests return a JSON error + * response and non ajax requests redirect to the index + * @param Controller $controller the controller that is being called + * @param string $methodName the name of the method that will be called on + * the controller + * @param \Exception $exception the thrown exception + * @throws \Exception the passed in exception if it can't handle it + * @return Response a Response object or null in case that the exception could not be handled + */ + public function afterException($controller, $methodName, \Exception $exception) { + if($exception instanceof SecurityException) { + + if (stripos($this->request->getHeader('Accept'),'html') === false) { + $response = new JSONResponse( + array('message' => $exception->getMessage()), + $exception->getCode() + ); + } else { + if($exception instanceof NotLoggedInException) { + $url = $this->urlGenerator->linkToRoute( + 'core.login.showLoginForm', + [ + 'redirect_url' => urlencode($this->request->server['REQUEST_URI']), + ] + ); + $response = new RedirectResponse($url); + } else { + $response = new TemplateResponse('core', '403', ['file' => $exception->getMessage()], 'guest'); + $response->setStatus($exception->getCode()); + } + } + + $this->logger->debug($exception->getMessage()); + return $response; + } + + throw $exception; + } + +} diff --git a/lib/private/AppFramework/Middleware/SessionMiddleware.php b/lib/private/AppFramework/Middleware/SessionMiddleware.php new file mode 100644 index 00000000000..b218b48ea11 --- /dev/null +++ b/lib/private/AppFramework/Middleware/SessionMiddleware.php @@ -0,0 +1,81 @@ +<?php +/** + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Middleware; + +use OC\AppFramework\Utility\ControllerMethodReflector; +use OCP\IRequest; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Middleware; +use OCP\ISession; + +class SessionMiddleware extends Middleware { + + /** + * @var IRequest + */ + private $request; + + /** + * @var ControllerMethodReflector + */ + private $reflector; + + /** + * @param IRequest $request + * @param ControllerMethodReflector $reflector + */ + public function __construct(IRequest $request, + ControllerMethodReflector $reflector, + ISession $session +) { + $this->request = $request; + $this->reflector = $reflector; + $this->session = $session; + } + + /** + * @param \OCP\AppFramework\Controller $controller + * @param string $methodName + */ + public function beforeController($controller, $methodName) { + $useSession = $this->reflector->hasAnnotation('UseSession'); + if (!$useSession) { + $this->session->close(); + } + } + + /** + * @param \OCP\AppFramework\Controller $controller + * @param string $methodName + * @param Response $response + * @return Response + */ + public function afterController($controller, $methodName, Response $response){ + $useSession = $this->reflector->hasAnnotation('UseSession'); + if ($useSession) { + $this->session->close(); + } + return $response; + } + +} diff --git a/lib/private/AppFramework/Routing/RouteActionHandler.php b/lib/private/AppFramework/Routing/RouteActionHandler.php new file mode 100644 index 00000000000..e8a7b8a1c61 --- /dev/null +++ b/lib/private/AppFramework/Routing/RouteActionHandler.php @@ -0,0 +1,47 @@ +<?php +/** + * @author Jörn Friedrich Dreyer <jfd@butonic.de> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Routing; + +use \OC\AppFramework\App; +use \OC\AppFramework\DependencyInjection\DIContainer; + +class RouteActionHandler { + private $controllerName; + private $actionName; + private $container; + + /** + * @param string $controllerName + * @param string $actionName + */ + public function __construct(DIContainer $container, $controllerName, $actionName) { + $this->controllerName = $controllerName; + $this->actionName = $actionName; + $this->container = $container; + } + + public function __invoke($params) { + App::main($this->controllerName, $this->actionName, $this->container, $params); + } +} diff --git a/lib/private/AppFramework/Routing/RouteConfig.php b/lib/private/AppFramework/Routing/RouteConfig.php new file mode 100644 index 00000000000..f2ec5c0c881 --- /dev/null +++ b/lib/private/AppFramework/Routing/RouteConfig.php @@ -0,0 +1,200 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Patrick Paysant <ppaysant@linagora.com> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Routing; + +use OC\AppFramework\DependencyInjection\DIContainer; +use OCP\Route\IRouter; + +/** + * Class RouteConfig + * @package OC\AppFramework\routing + */ +class RouteConfig { + private $container; + private $router; + private $routes; + private $appName; + + /** + * @param \OC\AppFramework\DependencyInjection\DIContainer $container + * @param \OCP\Route\IRouter $router + * @internal param $appName + */ + public function __construct(DIContainer $container, IRouter $router, $routes) { + $this->routes = $routes; + $this->container = $container; + $this->router = $router; + $this->appName = $container['AppName']; + } + + /** + * The routes and resource will be registered to the \OCP\Route\IRouter + */ + public function register() { + + // parse simple + $this->processSimpleRoutes($this->routes); + + // parse resources + $this->processResources($this->routes); + } + + /** + * Creates one route base on the give configuration + * @param array $routes + * @throws \UnexpectedValueException + */ + private function processSimpleRoutes($routes) + { + $simpleRoutes = isset($routes['routes']) ? $routes['routes'] : array(); + foreach ($simpleRoutes as $simpleRoute) { + $name = $simpleRoute['name']; + $postfix = ''; + + if (isset($simpleRoute['postfix'])) { + $postfix = $simpleRoute['postfix']; + } + + $url = $simpleRoute['url']; + $verb = isset($simpleRoute['verb']) ? strtoupper($simpleRoute['verb']) : 'GET'; + + $split = explode('#', $name, 2); + if (count($split) != 2) { + throw new \UnexpectedValueException('Invalid route name'); + } + $controller = $split[0]; + $action = $split[1]; + + $controllerName = $this->buildControllerName($controller); + $actionName = $this->buildActionName($action); + + // register the route + $handler = new RouteActionHandler($this->container, $controllerName, $actionName); + $router = $this->router->create($this->appName.'.'.$controller.'.'.$action . $postfix, $url) + ->method($verb) + ->action($handler); + + // optionally register requirements for route. This is used to + // tell the route parser how url parameters should be matched + if(array_key_exists('requirements', $simpleRoute)) { + $router->requirements($simpleRoute['requirements']); + } + + // optionally register defaults for route. This is used to + // tell the route parser how url parameters should be default valued + if(array_key_exists('defaults', $simpleRoute)) { + $router->defaults($simpleRoute['defaults']); + } + } + } + + /** + * For a given name and url restful routes are created: + * - index + * - show + * - new + * - create + * - update + * - destroy + * + * @param array $routes + */ + private function processResources($routes) + { + // declaration of all restful actions + $actions = array( + array('name' => 'index', 'verb' => 'GET', 'on-collection' => true), + array('name' => 'show', 'verb' => 'GET'), + array('name' => 'create', 'verb' => 'POST', 'on-collection' => true), + array('name' => 'update', 'verb' => 'PUT'), + array('name' => 'destroy', 'verb' => 'DELETE'), + ); + + $resources = isset($routes['resources']) ? $routes['resources'] : array(); + foreach ($resources as $resource => $config) { + + // the url parameter used as id to the resource + foreach($actions as $action) { + $url = $config['url']; + $method = $action['name']; + $verb = isset($action['verb']) ? strtoupper($action['verb']) : 'GET'; + $collectionAction = isset($action['on-collection']) ? $action['on-collection'] : false; + if (!$collectionAction) { + $url = $url . '/{id}'; + } + if (isset($action['url-postfix'])) { + $url = $url . '/' . $action['url-postfix']; + } + + $controller = $resource; + + $controllerName = $this->buildControllerName($controller); + $actionName = $this->buildActionName($method); + + $routeName = $this->appName . '.' . strtolower($resource) . '.' . strtolower($method); + + $this->router->create($routeName, $url)->method($verb)->action( + new RouteActionHandler($this->container, $controllerName, $actionName) + ); + } + } + } + + /** + * Based on a given route name the controller name is generated + * @param string $controller + * @return string + */ + private function buildControllerName($controller) + { + return $this->underScoreToCamelCase(ucfirst($controller)) . 'Controller'; + } + + /** + * Based on the action part of the route name the controller method name is generated + * @param string $action + * @return string + */ + private function buildActionName($action) { + return $this->underScoreToCamelCase($action); + } + + /** + * Underscored strings are converted to camel case strings + * @param string $str + * @return string + */ + private function underScoreToCamelCase($str) { + $pattern = "/_[a-z]?/"; + return preg_replace_callback( + $pattern, + function ($matches) { + return strtoupper(ltrim($matches[0], "_")); + }, + $str); + } +} diff --git a/lib/private/AppFramework/Utility/ControllerMethodReflector.php b/lib/private/AppFramework/Utility/ControllerMethodReflector.php new file mode 100644 index 00000000000..de83749fbaf --- /dev/null +++ b/lib/private/AppFramework/Utility/ControllerMethodReflector.php @@ -0,0 +1,118 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Olivier Paroz <github@oparoz.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework\Utility; + +use \OCP\AppFramework\Utility\IControllerMethodReflector; + + +/** + * Reads and parses annotations from doc comments + */ +class ControllerMethodReflector implements IControllerMethodReflector{ + + private $annotations; + private $types; + private $parameters; + + public function __construct() { + $this->types = array(); + $this->parameters = array(); + $this->annotations = array(); + } + + + /** + * @param object $object an object or classname + * @param string $method the method which we want to inspect + */ + public function reflect($object, $method){ + $reflection = new \ReflectionMethod($object, $method); + $docs = $reflection->getDocComment(); + + // extract everything prefixed by @ and first letter uppercase + preg_match_all('/@([A-Z]\w+)/', $docs, $matches); + $this->annotations = $matches[1]; + + // extract type parameter information + preg_match_all('/@param\h+(?P<type>\w+)\h+\$(?P<var>\w+)/', $docs, $matches); + $this->types = array_combine($matches['var'], $matches['type']); + + foreach ($reflection->getParameters() as $param) { + // extract type information from PHP 7 scalar types and prefer them + // over phpdoc annotations + if (method_exists($param, 'getType')) { + $type = $param->getType(); + if ($type !== null) { + $this->types[$param->getName()] = (string) $type; + } + } + + if($param->isOptional()) { + $default = $param->getDefaultValue(); + } else { + $default = null; + } + $this->parameters[$param->name] = $default; + } + } + + + /** + * Inspects the PHPDoc parameters for types + * @param string $parameter the parameter whose type comments should be + * parsed + * @return string|null type in the type parameters (@param int $something) + * would return int or null if not existing + */ + public function getType($parameter) { + if(array_key_exists($parameter, $this->types)) { + return $this->types[$parameter]; + } else { + return null; + } + } + + + /** + * @return array the arguments of the method with key => default value + */ + public function getParameters() { + return $this->parameters; + } + + + /** + * Check if a method contains an annotation + * @param string $name the name of the annotation + * @return bool true if the annotation is found + */ + public function hasAnnotation($name){ + return in_array($name, $this->annotations); + } + + +} diff --git a/lib/private/AppFramework/Utility/SimpleContainer.php b/lib/private/AppFramework/Utility/SimpleContainer.php new file mode 100644 index 00000000000..78ded39735e --- /dev/null +++ b/lib/private/AppFramework/Utility/SimpleContainer.php @@ -0,0 +1,162 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + +namespace OC\AppFramework\Utility; + +use ReflectionClass; +use ReflectionException; +use Closure; +use Pimple\Container; +use OCP\AppFramework\QueryException; +use OCP\IContainer; + +/** + * Class SimpleContainer + * + * SimpleContainer is a simple implementation of IContainer on basis of Pimple + */ +class SimpleContainer extends Container implements IContainer { + + + /** + * @param ReflectionClass $class the class to instantiate + * @return \stdClass the created class + */ + private function buildClass(ReflectionClass $class) { + $constructor = $class->getConstructor(); + if ($constructor === null) { + return $class->newInstance(); + } else { + $parameters = []; + foreach ($constructor->getParameters() as $parameter) { + $parameterClass = $parameter->getClass(); + + // try to find out if it is a class or a simple parameter + if ($parameterClass === null) { + $resolveName = $parameter->getName(); + } else { + $resolveName = $parameterClass->name; + } + + $parameters[] = $this->query($resolveName); + } + return $class->newInstanceArgs($parameters); + } + } + + + /** + * If a parameter is not registered in the container try to instantiate it + * by using reflection to find out how to build the class + * @param string $name the class name to resolve + * @return \stdClass + * @throws QueryException if the class could not be found or instantiated + */ + public function resolve($name) { + $baseMsg = 'Could not resolve ' . $name . '!'; + try { + $class = new ReflectionClass($name); + if ($class->isInstantiable()) { + return $this->buildClass($class); + } else { + throw new QueryException($baseMsg . + ' Class can not be instantiated'); + } + } catch(ReflectionException $e) { + throw new QueryException($baseMsg . ' ' . $e->getMessage()); + } + } + + + /** + * @param string $name name of the service to query for + * @return mixed registered service for the given $name + * @throws QueryException if the query could not be resolved + */ + public function query($name) { + $name = $this->sanitizeName($name); + if ($this->offsetExists($name)) { + return $this->offsetGet($name); + } else { + $object = $this->resolve($name); + $this->registerService($name, function () use ($object) { + return $object; + }); + return $object; + } + } + + /** + * @param string $name + * @param mixed $value + */ + public function registerParameter($name, $value) { + $this[$name] = $value; + } + + /** + * The given closure is call the first time the given service is queried. + * The closure has to return the instance for the given service. + * Created instance will be cached in case $shared is true. + * + * @param string $name name of the service to register another backend for + * @param Closure $closure the closure to be called on service creation + * @param bool $shared + */ + public function registerService($name, Closure $closure, $shared = true) { + $name = $this->sanitizeName($name); + if (isset($this[$name])) { + unset($this[$name]); + } + if ($shared) { + $this[$name] = $closure; + } else { + $this[$name] = parent::factory($closure); + } + } + + /** + * Shortcut for returning a service from a service under a different key, + * e.g. to tell the container to return a class when queried for an + * interface + * @param string $alias the alias that should be registered + * @param string $target the target that should be resolved instead + */ + public function registerAlias($alias, $target) { + $this->registerService($alias, function (IContainer $container) use ($target) { + return $container->query($target); + }, false); + } + + /* + * @param string $name + * @return string + */ + protected function sanitizeName($name) { + return ltrim($name, '\\'); + } + +} diff --git a/lib/private/AppFramework/Utility/TimeFactory.php b/lib/private/AppFramework/Utility/TimeFactory.php new file mode 100644 index 00000000000..5241b367069 --- /dev/null +++ b/lib/private/AppFramework/Utility/TimeFactory.php @@ -0,0 +1,44 @@ +<?php +/** + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @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/> + * + */ + + +namespace OC\AppFramework\Utility; + +use OCP\AppFramework\Utility\ITimeFactory; + + +/** + * Needed to mock calls to time() + */ +class TimeFactory implements ITimeFactory { + + + /** + * @return int the result of a call to time() + */ + public function getTime() { + return time(); + } + + +} |