diff --git a/lib/private/appframework/app.php b/lib/private/appframework/app.php index f56ba4af87..b94c7bd995 100644 --- a/lib/private/appframework/app.php +++ b/lib/private/appframework/app.php @@ -24,8 +24,9 @@ namespace OC\AppFramework; -use OC\AppFramework\DependencyInjection\DIContainer; - +use \OC_App; +use \OC\AppFramework\DependencyInjection\DIContainer; +use \OCP\AppFramework\QueryException; /** * Entry point for every request in your app. You can consider this as your @@ -36,6 +37,34 @@ use OC\AppFramework\DependencyInjection\DIContainer; 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 tag + $filePath = OC_App::getAppPath($appId) . '/appinfo/info.xml'; + $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 @@ -48,7 +77,16 @@ class App { if (!is_null($urlParams)) { $container['urlParams'] = $urlParams; } - $controller = $container[$controllerName]; + $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 $dispatcher = $container['Dispatcher']; diff --git a/lib/private/appframework/dependencyinjection/dicontainer.php b/lib/private/appframework/dependencyinjection/dicontainer.php index dc57ef4c16..2c5089865a 100644 --- a/lib/private/appframework/dependencyinjection/dicontainer.php +++ b/lib/private/appframework/dependencyinjection/dicontainer.php @@ -36,12 +36,13 @@ use OC\AppFramework\Utility\SimpleContainer; use OC\AppFramework\Utility\TimeFactory; use OC\AppFramework\Utility\ControllerMethodReflector; use OCP\AppFramework\IApi; +use OCP\AppFramework\QueryException; use OCP\AppFramework\IAppContainer; use OCP\AppFramework\Middleware; use OCP\IServerContainer; -class DIContainer extends SimpleContainer implements IAppContainer{ +class DIContainer extends SimpleContainer implements IAppContainer { /** * @var array @@ -53,19 +54,181 @@ class DIContainer extends SimpleContainer implements IAppContainer{ * @param string $appName the name of the app */ public function __construct($appName, $urlParams = array()){ - $this['AppName'] = $appName; $this['urlParams'] = $urlParams; - $this->registerParameter('ServerContainer', \OC::$server); + /** + * Core services + */ + $this->registerService('OCP\\IAppConfig', function($c) { + return \OC::$server->getAppConfig(); + }); + $this->registerService('OCP\\IAppManager', function($c) { + return \OC::$server->getAppManager(); + }); + + $this->registerService('OCP\\IAvatarManager', function($c) { + return \OC::$server->getAvatarManager(); + }); + + $this->registerService('OCP\\Activity\\IManager', function($c) { + return \OC::$server->getActivityManager(); + }); + + $this->registerService('OCP\\ICache', function($c) { + return \OC::$server->getCache(); + }); + + $this->registerService('OCP\\ICacheFactory', function($c) { + return \OC::$server->getMemCacheFactory(); + }); + + $this->registerService('OCP\\IConfig', function($c) { + return \OC::$server->getConfig(); + }); + + $this->registerService('OCP\\Contacts\\IManager', function($c) { + return \OC::$server->getContactsManager(); + }); + + $this->registerService('OCP\\IDateTimeZone', function($c) { + return \OC::$server->getDateTimeZone(); + }); + + $this->registerService('OCP\\IDb', function($c) { + return \OC::$server->getDb(); + }); + + $this->registerService('OCP\\IDBConnection', function($c) { + return \OC::$server->getDatabaseConnection(); + }); + + $this->registerService('OCP\\Diagnostics\\IEventLogger', function($c) { + return \OC::$server->getEventLogger(); + }); + + $this->registerService('OCP\\Diagnostics\\IQueryLogger', function($c) { + return \OC::$server->getQueryLogger(); + }); + + $this->registerService('OCP\\Files\\Config\\IMountProviderCollection', function($c) { + return \OC::$server->getMountProviderCollection(); + }); + + $this->registerService('OCP\\Files\\IRootFolder', function($c) { + return \OC::$server->getRootFolder(); + }); + + $this->registerService('OCP\\IGroupManager', function($c) { + return \OC::$server->getGroupManager(); + }); + + $this->registerService('OCP\\IL10N', function($c) { + return \OC::$server->getL10N($c->query('AppName')); + }); + + $this->registerService('OCP\\ILogger', function($c) { + return \OC::$server->getLogger(); + }); + + $this->registerService('OCP\\BackgroundJob\\IJobList', function($c) { + return \OC::$server->getJobList(); + }); + + $this->registerService('OCP\\AppFramework\\Utility\\IControllerMethodReflector', function($c) { + return $c->query('ControllerMethodReflector'); + }); + + $this->registerService('OCP\\INavigationManager', function($c) { + return \OC::$server->getNavigationManager(); + }); + + $this->registerService('OCP\\IPreview', function($c) { + return \OC::$server->getPreviewManager(); + }); + + $this->registerService('OCP\\IRequest', function($c) { + return $c->query('Request'); + }); + + $this->registerService('OCP\\ITagManager', function($c) { + return \OC::$server->getTagManager(); + }); + + $this->registerService('OCP\\ITempManager', function($c) { + return \OC::$server->getTempManager(); + }); + + $this->registerService('OCP\\AppFramework\\Utility\\ITimeFactory', function($c) { + return $c->query('TimeFactory'); + }); + + $this->registerService('OCP\\Route\\IRouter', function($c) { + return \OC::$server->getRouter(); + }); + + $this->registerService('OCP\\ISearch', function($c) { + return \OC::$server->getSearch(); + }); + + $this->registerService('OCP\\ISearch', function($c) { + return \OC::$server->getSearch(); + }); + + $this->registerService('OCP\\Security\\ICrypto', function($c) { + return \OC::$server->getCrypto(); + }); + + $this->registerService('OCP\\Security\\IHasher', function($c) { + return \OC::$server->getHasher(); + }); + + $this->registerService('OCP\\Security\\ISecureRandom', function($c) { + return \OC::$server->getSecureRandom(); + }); + + $this->registerService('OCP\\IURLGenerator', function($c) { + return \OC::$server->getURLGenerator(); + }); + + $this->registerService('OCP\\IUserManager', function($c) { + return \OC::$server->getUserManager(); + }); + + $this->registerService('OCP\\IUserSession', function($c) { + return \OC::$server->getUserSession(); + }); + + $this->registerService('ServerContainer', function ($c) { + $c->query('OCP\\ILogger')->warning( + 'Accessing the server container is deprecated. Use type ' . + 'annotations to inject core services instead!' + ); + return \OC::$server; + }); + + // 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')->warning( + 'Accessing the API class is deprecated! Use the appropriate ' . + 'services instead!' + ); return new API($c['AppName']); }); - /** - * Http - */ $this->registerService('Request', function($c) { /** @var $c SimpleContainer */ /** @var $server SimpleContainer */ @@ -234,4 +397,6 @@ class DIContainer extends SimpleContainer implements IAppContainer{ } \OCP\Util::writeLog($this->getAppName(), $message, $level); } + + } diff --git a/lib/private/appframework/utility/simplecontainer.php b/lib/private/appframework/utility/simplecontainer.php index 55b9cf7a97..68d52d759e 100644 --- a/lib/private/appframework/utility/simplecontainer.php +++ b/lib/private/appframework/utility/simplecontainer.php @@ -1,7 +1,29 @@ + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + * + */ namespace OC\AppFramework\Utility; +use \OCP\AppFramework\QueryException; + /** * Class SimpleContainer * @@ -9,12 +31,71 @@ namespace OC\AppFramework\Utility; */ class SimpleContainer extends \Pimple\Container implements \OCP\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 + * @throws QueryException if the class could not be found or instantiated + */ + private 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 QueryExcpetion if the query could not be resolved */ public function query($name) { - return $this->offsetGet($name); + if ($this->offsetExists($name)) { + return $this->offsetGet($name); + } else { + $object = $this->resolve($name); + $this->registerService($name, function () use ($object) { + return $object; + }); + return $object; + } } /** @@ -44,4 +125,6 @@ class SimpleContainer extends \Pimple\Container implements \OCP\IContainer { $this[$name] = parent::factory($closure); } } + + } diff --git a/lib/private/route/router.php b/lib/private/route/router.php index 5d6f621dc3..3559b84192 100644 --- a/lib/private/route/router.php +++ b/lib/private/route/router.php @@ -9,6 +9,7 @@ namespace OC\Route; use OCP\Route\IRouter; +use OCP\AppFramework\App; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Routing\RequestContext; @@ -129,7 +130,7 @@ class Router implements IRouter { if (!isset($this->loadedApps[$app])) { $this->loadedApps[$app] = true; $this->useCollection($app); - $this->requireRouteFile($file); + $this->requireRouteFile($file, $app); $collection = $this->getCollection($app); $collection->addPrefix('/apps/' . $app); $this->root->addCollection($collection); @@ -283,10 +284,39 @@ class Router implements IRouter { /** * To isolate the variable scope used inside the $file it is required in it's own method - * @param string $file + * @param string $file the route file location to include + * @param string $appName */ - private function requireRouteFile($file) { - require_once $file; + private function requireRouteFile($file, $appName) { + $this->setupRoutes(include_once $file, $appName); } + + /** + * If a routes.php file returns an array, try to set up the application and + * register the routes for the app. The application class will be chosen by + * camelcasing the appname, e.g.: my_app will be turned into + * \OCA\MyApp\AppInfo\Application. If that class does not exist, a default + * App will be intialized. This makes it optional to ship an + * appinfo/application.php by using the built in query resolver + * @param array $routes the application routes + * @param string $appName the name of the app. + */ + private function setupRoutes($routes, $appName) { + if (is_array($routes)) { + $appNameSpace = App::buildAppNamespace($appName); + + $applicationClassName = $appNameSpace . '\\AppInfo\\Application'; + + if (class_exists($applicationClassName)) { + $application = new $applicationClassName(); + } else { + $application = new App($appName); + } + + $application->registerRoutes($this, $routes); + } + } + + } diff --git a/lib/public/appframework/app.php b/lib/public/appframework/app.php index 2161232787..da405262ae 100644 --- a/lib/public/appframework/app.php +++ b/lib/public/appframework/app.php @@ -37,6 +37,22 @@ use OC\AppFramework\routing\RouteConfig; * to be registered using IContainer::registerService */ class App { + + + /** + * Turns an app id into a namespace by convetion. The id is split at the + * underscores, all parts are camelcased and reassembled. e.g.: + * some_app_id -> OCA\SomeAppId + * @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\\') { + return \OC\AppFramework\App::buildAppNamespace($appId, $topNamespace); + } + + /** * @param array $urlParams an array with variables extracted from the routes */ diff --git a/lib/public/appframework/queryexception.php b/lib/public/appframework/queryexception.php new file mode 100644 index 0000000000..f08d5b9a12 --- /dev/null +++ b/lib/public/appframework/queryexception.php @@ -0,0 +1,28 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + * + */ + +namespace OCP\AppFramework; + +use Exception; + + +class QueryException extends Exception {} diff --git a/tests/lib/appframework/AppTest.php b/tests/lib/appframework/AppTest.php index 86128db118..e60f3439f2 100644 --- a/tests/lib/appframework/AppTest.php +++ b/tests/lib/appframework/AppTest.php @@ -24,6 +24,17 @@ namespace OC\AppFramework; +function rrmdir($directory) { + $files = array_diff(scandir($directory), array('.','..')); + foreach ($files as $file) { + if (is_dir($directory . '/' . $file)) { + rrmdir($directory . '/' . $file); + } else { + unlink($directory . '/' . $file); + } + } + return rmdir($directory); +} class AppTest extends \Test\TestCase { @@ -36,6 +47,7 @@ class AppTest extends \Test\TestCase { private $output; private $controllerName; private $controllerMethod; + private $appPath; protected function setUp() { parent::setUp(); @@ -59,6 +71,17 @@ class AppTest extends \Test\TestCase { $this->container[$this->controllerName] = $this->controller; $this->container['Dispatcher'] = $this->dispatcher; $this->container['urlParams'] = array(); + + $this->appPath = __DIR__ . '/../../../apps/namespacetestapp/appinfo'; + $infoXmlPath = $this->appPath . '/info.xml'; + mkdir($this->appPath, 0777, true); + + $xml = '' . + '' . + 'namespacetestapp' . + 'NameSpaceTestApp' . + ''; + file_put_contents($infoXmlPath, $xml); } @@ -77,6 +100,28 @@ class AppTest extends \Test\TestCase { } + public function testBuildAppNamespace() { + $ns = App::buildAppNamespace('someapp'); + $this->assertEquals('OCA\Someapp', $ns); + } + + + public function testBuildAppNamespaceCore() { + $ns = App::buildAppNamespace('someapp', 'OC\\'); + $this->assertEquals('OC\Someapp', $ns); + } + + + public function testBuildAppNamespaceInfoXml() { + $ns = App::buildAppNamespace('namespacetestapp', 'OCA\\'); + $this->assertEquals('OCA\NameSpaceTestApp', $ns); + } + + + protected function tearDown() { + rrmdir($this->appPath); + } + /* FIXME: this complains about shit headers which are already sent because of the content length. Would be cool if someone could fix this diff --git a/tests/lib/appframework/utility/SimpleContainerTest.php b/tests/lib/appframework/utility/SimpleContainerTest.php new file mode 100644 index 0000000000..c5c522d911 --- /dev/null +++ b/tests/lib/appframework/utility/SimpleContainerTest.php @@ -0,0 +1,167 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + * + */ +namespace Test\AppFramework\Utility; + +use OC\AppFramework\Utility\SimpleContainer; + + +interface TestInterface {} + +class ClassEmptyConstructor implements IInterfaceConstructor {} + +class ClassSimpleConstructor implements IInterfaceConstructor { + public $test; + public function __construct($test) { + $this->test = $test; + } +} + +class ClassComplexConstructor { + public $class; + public $test; + public function __construct(ClassSimpleConstructor $class, $test) { + $this->class = $class; + $this->test = $test; + } +} + +interface IInterfaceConstructor {} +class ClassInterfaceConstructor { + public $class; + public $test; + public function __construct(IInterfaceConstructor $class, $test) { + $this->class = $class; + $this->test = $test; + } +} + + +class SimpleContainerTest extends \Test\TestCase { + + + private $container; + + public function setUp() { + $this->container = new SimpleContainer(); + } + + + public function testRegister() { + $this->container->registerParameter('test', 'abc'); + $this->assertEquals('abc', $this->container->query('test')); + } + + + /** + * @expectedException \OCP\AppFramework\QueryException + */ + public function testNothingRegistered() { + $this->container->query('something really hard'); + } + + + /** + * @expectedException \OCP\AppFramework\QueryException + */ + public function testNotAClass() { + $this->container->query('Test\AppFramework\Utility\TestInterface'); + } + + + public function testNoConstructorClass() { + $object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + $this->assertTrue($object instanceof ClassEmptyConstructor); + } + + + public function testInstancesOnlyOnce() { + $object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + $object2 = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + $this->assertSame($object, $object2); + } + + public function testConstructorSimple() { + $this->container->registerParameter('test', 'abc'); + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassSimpleConstructor' + ); + $this->assertTrue($object instanceof ClassSimpleConstructor); + $this->assertEquals('abc', $object->test); + } + + + public function testConstructorComplex() { + $this->container->registerParameter('test', 'abc'); + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassComplexConstructor' + ); + $this->assertTrue($object instanceof ClassComplexConstructor); + $this->assertEquals('abc', $object->class->test); + $this->assertEquals('abc', $object->test); + } + + + public function testConstructorComplexInterface() { + $this->container->registerParameter('test', 'abc'); + $this->container->registerService( + 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { + return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor'); + }); + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassInterfaceConstructor' + ); + $this->assertTrue($object instanceof ClassInterfaceConstructor); + $this->assertEquals('abc', $object->class->test); + $this->assertEquals('abc', $object->test); + } + + + public function tesOverrideService() { + $this->container->registerParameter('test', 'abc'); + $this->container->registerService( + 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { + return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor'); + }); + $this->container->registerService( + 'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) { + return $c->query('Test\AppFramework\Utility\ClassEmptyConstructor'); + }); + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassInterfaceConstructor' + ); + $this->assertTrue($object instanceof ClassEmptyConstructor); + $this->assertEquals('abc', $object->test); + } + + + /** + * @expectedException \OCP\AppFramework\QueryException + */ + public function testConstructorComplexNoTestParameterFound() { + $object = $this->container->query( + 'Test\AppFramework\Utility\ClassComplexConstructor' + ); + } + + +}