├── Makefile ├── behat.yml ├── composer.json ├── phpunit.xml.dist ├── src ├── CmfResourceRestBundle.php ├── Controller │ └── ResourceController.php ├── DependencyInjection │ ├── CmfResourceRestExtension.php │ └── Configuration.php ├── Registry │ └── PayloadAliasRegistry.php ├── Resources │ ├── config │ │ ├── resource-rest.xml │ │ ├── routing.yml │ │ ├── schema │ │ │ └── resource-rest.xsd │ │ ├── security.xml │ │ └── serializer.xml │ └── meta │ │ └── LICENSE ├── Security │ └── ResourcePathVoter.php └── Serializer │ └── Jms │ ├── EventSubscriber │ ├── PhpcrNodeSubscriber.php │ └── ResourceSubscriber.php │ └── Handler │ ├── PhpcrNodeHandler.php │ └── ResourceHandler.php └── tests ├── Features ├── Context │ └── ResourceContext.php ├── nesting.feature ├── resource_api_filesystem.feature ├── resource_api_phpcr.feature ├── resource_api_phpcr_odm.feature └── security.feature ├── Fixtures └── App │ ├── Description │ └── DummyEnhancer.php │ ├── Document │ └── Article.php │ ├── Kernel.php │ ├── Resources │ └── views │ │ └── snippets │ │ └── snippet1.html │ ├── Security │ └── ResourceVoter.php │ └── config │ ├── bundles.php │ ├── config.php │ └── routing.php └── Unit ├── DependencyInjection ├── CmfResourceRestExtensionTest.php ├── ConfigurationTest.php └── fixtures │ ├── config.xml │ └── config.yml ├── Registry └── PayloadAliasRegistryTest.php ├── Security └── ResourcePathVoterTest.php └── Serializer └── Jms ├── EventSubscriber └── PhpcrNodeSubscriberTest.php └── Handler ├── PhpcrNodeHandlerTest.php └── ResourceHandlerTest.php /Makefile: -------------------------------------------------------------------------------- 1 | ####################################################### 2 | # DO NOT EDIT THIS FILE! # 3 | # # 4 | # It's auto-generated by symfony-cmf/dev-kit package. # 5 | ####################################################### 6 | 7 | ############################################################################ 8 | # This file is part of the Symfony CMF package. # 9 | # # 10 | # (c) 2011-2017 Symfony CMF # 11 | # # 12 | # For the full copyright and license information, please view the LICENSE # 13 | # file that was distributed with this source code. # 14 | ############################################################################ 15 | 16 | TESTING_SCRIPTS_DIR=vendor/symfony-cmf/testing/bin 17 | CONSOLE=${TESTING_SCRIPTS_DIR}/console 18 | VERSION=dev-master 19 | ifdef BRANCH 20 | VERSION=dev-${BRANCH} 21 | endif 22 | PACKAGE=symfony-cmf/resource-rest-bundle 23 | export KERNEL_CLASS=Symfony\Cmf\Bundle\ResourceRestBundle\Tests\Fixtures\App\Kernel 24 | list: 25 | @echo 'test: will run all tests' 26 | @echo 'unit_tests: will run unit tests only' 27 | 28 | 29 | @echo 'test_installation: will run installation test' 30 | include ${TESTING_SCRIPTS_DIR}/make/unit_tests.mk 31 | include ${TESTING_SCRIPTS_DIR}/make/test_installation.mk 32 | 33 | .PHONY: test 34 | test: unit_tests 35 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - Symfony\Cmf\Bundle\ResourceRestBundle\Tests\Features\Context\ResourceContext 6 | - Behat\WebApiExtension\Context\WebApiContext 7 | paths: 8 | - tests/Features 9 | extensions: 10 | Behat\WebApiExtension: 11 | base_url: http://localhost:8000/ 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony-cmf/resource-rest-bundle", 3 | "description": "Bundle which provides a REST API for resources", 4 | "homepage": "http://cmf.symfony.com", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Symfony CMF Community", 9 | "homepage": "https://github.com/symfony-cmf/symfony-cmf/contributors" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.2", 14 | "symfony-cmf/resource-bundle": "^1.0", 15 | "jms/serializer-bundle": "^2.0 || ^3.0" 16 | }, 17 | "require-dev": { 18 | "symfony-cmf/testing": "^2.1@dev", 19 | "symfony/phpunit-bridge": "^5", 20 | "doctrine/phpcr-odm": "^1.4|^2.0", 21 | "behat/behat": "^3.0.6", 22 | "imbo/behat-api-extension": "^2.1", 23 | "matthiasnoback/symfony-dependency-injection-test": "^4", 24 | "matthiasnoback/symfony-config-test": "^4", 25 | "symfony/twig-bundle": "^2.8 || ^3.3 || ^4.0", 26 | "symfony/validator": "^2.8 || ^3.3 || ^4.0", 27 | "symfony/security-bundle": "^2.8 || ^3.3 || ^4.0", 28 | "symfony/asset": "^2.8 || ^3.3 || ^4.0", 29 | "symfony/templating": "^2.8 || ^3.3 || ^4.0", 30 | "symfony/form": "^2.8 || ^3.3 || ^4.0", 31 | "symfony/web-server-bundle": "^2.8 || ^3.3 || ^4.0", 32 | "phpspec/prophecy": "^1.12" 33 | }, 34 | "suggest": { 35 | "doctrine/phpcr-odm": "To enable support for the PHPCR ODM documents (^1.2)", 36 | "doctrine/phpcr-bundle": "To enable support for the PHPCR ODM documents" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\": "tests" 46 | } 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-master": "1.1-dev" 51 | } 52 | }, 53 | "conflict": { 54 | "jms/serializer": "<2.2.0", 55 | "sebastian/environment": "<1.3.4", 56 | "sebastian/exporter": "<2.0.0" 57 | }, 58 | "type": "symfony-bundle" 59 | } 60 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | ./tests/Unit 11 | 12 | 13 | 14 | 15 | 16 | src 17 | 18 | *Bundle.php 19 | Resources/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/CmfResourceRestBundle.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 54 | $this->registry = $registry; 55 | $this->authorizationChecker = $authorizationChecker; 56 | $this->resourceHandler = $resourceHandler; 57 | } 58 | 59 | /** 60 | * Provides resource information. 61 | * 62 | * @param string $repositoryName 63 | * @param string $path 64 | */ 65 | public function getResourceAction(Request $request, $repositoryName, $path) 66 | { 67 | if ($request->query->has('depth')) { 68 | $this->resourceHandler->setMaxDepth($request->query->getInt('depth')); 69 | } 70 | 71 | $path = '/'.ltrim($path, '/'); 72 | 73 | try { 74 | $repository = $this->registry->get($repositoryName); 75 | 76 | $fullPath = method_exists($repository, 'resolvePath') ? $repository->resolvePath($path) : $path; 77 | $this->guardAccess('read', $repositoryName, $fullPath); 78 | 79 | $resource = $repository->get($path); 80 | 81 | return $this->createResponse($resource); 82 | } catch (ResourceNotFoundException $e) { 83 | throw new NotFoundHttpException(sprintf('No resource found at path "%s" for repository "%s"', $path, $repositoryName), $e); 84 | } 85 | } 86 | 87 | /** 88 | * Changes the current resource. 89 | * 90 | * The request body should contain a JSON list of operations 91 | * like: 92 | * 93 | * [{"operation": "move", "target": "/cms/new/id"}] 94 | * 95 | * Currently supported operations: 96 | * 97 | * - move (options: target) 98 | * 99 | * changing payload properties isn't supported yet. 100 | * 101 | * @param string $repositoryName 102 | * @param string $path 103 | * 104 | * @return Response 105 | */ 106 | public function patchResourceAction($repositoryName, $path, Request $request) 107 | { 108 | $path = '/'.ltrim($path, '/'); 109 | $repository = $this->registry->get($repositoryName); 110 | 111 | $fullPath = method_exists($repository, 'resolvePath') ? $repository->resolvePath($path) : $path; 112 | $this->guardAccess('write', $repositoryName, $fullPath); 113 | 114 | $requestContent = json_decode($request->getContent(), true); 115 | if (!$requestContent) { 116 | return $this->badRequestResponse('Only JSON request bodies are supported.'); 117 | } 118 | 119 | foreach ($requestContent as $action) { 120 | if (!isset($action['operation'])) { 121 | return $this->badRequestResponse('Malformed request body. It should contain a list of operations.'); 122 | } 123 | 124 | switch ($action['operation']) { 125 | case 'move': 126 | $targetPath = $action['target']; 127 | $repository->move($path, $targetPath); 128 | 129 | $resource = $repository->get($targetPath); 130 | 131 | break; 132 | default: 133 | return $this->badRequestResponse(sprintf('Operation "%s" is not supported, supported operations: move.', $action['operation'])); 134 | } 135 | } 136 | 137 | $this->resourceHandler->setMaxDepth(0); 138 | 139 | return $this->createResponse($resource, Response::HTTP_OK); 140 | } 141 | 142 | /** 143 | * Deletes the resource. 144 | * 145 | * @param string $repositoryName 146 | * @param string $path 147 | * 148 | * @return Response 149 | */ 150 | public function deleteResourceAction($repositoryName, $path) 151 | { 152 | $path = '/'.ltrim($path, '/'); 153 | $repository = $this->registry->get($repositoryName); 154 | 155 | $fullPath = method_exists($repository, 'resolvePath') ? $repository->resolvePath($path) : $path; 156 | $this->guardAccess('write', $repositoryName, $fullPath); 157 | 158 | $repository->remove($path); 159 | 160 | return $this->createResponse('', Response::HTTP_NO_CONTENT); 161 | } 162 | 163 | /** 164 | * @param string $message 165 | * 166 | * @return Response 167 | */ 168 | private function badRequestResponse($message) 169 | { 170 | return $this->createResponse(['message' => $message], Response::HTTP_BAD_REQUEST); 171 | } 172 | 173 | private function guardAccess($attribute, $repository, $path) 174 | { 175 | if (null !== $this->authorizationChecker 176 | && !$this->authorizationChecker->isGranted( 177 | 'CMF_RESOURCE_'.strtoupper($attribute), 178 | ['repository_name' => $repository, 'path' => $path] 179 | ) 180 | ) { 181 | throw new AccessDeniedException(sprintf('%s access denied for "%s".', ucfirst($attribute), $path)); 182 | } 183 | } 184 | 185 | /** 186 | * @param mixed $resource 187 | * @param int $httpStatusCode 188 | * 189 | * @return Response 190 | */ 191 | private function createResponse($resource, $httpStatusCode = Response::HTTP_OK) 192 | { 193 | $context = SerializationContext::create(); 194 | $context->enableMaxDepthChecks(); 195 | $context->setSerializeNull(true); 196 | $json = $this->serializer->serialize( 197 | $resource, 198 | 'json', 199 | $context 200 | ); 201 | 202 | $response = new Response($json, $httpStatusCode); 203 | $response->headers->set('Content-Type', 'application/json'); 204 | 205 | return $response; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/DependencyInjection/CmfResourceRestExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 31 | 32 | $bundles = $container->getParameter('kernel.bundles'); 33 | if (!\array_key_exists('JMSSerializerBundle', $bundles)) { 34 | throw new \LogicException('The JMSSerializerBundle must be registered in order to use the CmfResourceRestBundle.'); 35 | } 36 | 37 | $container->setParameter('cmf_resource_rest.max_depth', $config['max_depth']); 38 | $container->setParameter('cmf_resource_rest.expose_payload', $config['expose_payload']); 39 | 40 | $loader->load('serializer.xml'); 41 | $loader->load('resource-rest.xml'); 42 | 43 | $this->configurePayloadAliasRegistry($container, $config['payload_alias_map']); 44 | $this->configureSecurityVoter($loader, $container, $config['security']); 45 | } 46 | 47 | private function configureSecurityVoter(XmlFileLoader $loader, ContainerBuilder $container, array $config) 48 | { 49 | if ([] === $config['access_control']) { 50 | return; 51 | } 52 | 53 | $container->setParameter('cmf_resource_rest.security.access_map', $config['access_control']); 54 | 55 | $loader->load('security.xml'); 56 | } 57 | 58 | public function getNamespace() 59 | { 60 | return 'http://cmf.symfony.com/schema/dic/'.$this->getAlias(); 61 | } 62 | 63 | public function getXsdValidationBasePath() 64 | { 65 | return __DIR__.'/../Resources/config/schema'; 66 | } 67 | 68 | private function configurePayloadAliasRegistry(ContainerBuilder $container, $aliasMap) 69 | { 70 | $registry = $container->getDefinition('cmf_resource_rest.registry.payload_alias'); 71 | $registry->replaceArgument(1, $aliasMap); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('cmf_resource_rest') 29 | ->fixXmlConfig('payload_alias', 'payload_alias_map') 30 | ->children() 31 | ->integerNode('max_depth')->defaultValue(2)->end() 32 | ->booleanNode('expose_payload')->defaultFalse()->end() 33 | 34 | ->arrayNode('security') 35 | ->fixXmlConfig('rule', 'access_control') 36 | ->addDefaultsIfNotSet() 37 | ->children() 38 | ->arrayNode('access_control') 39 | ->defaultValue([]) 40 | ->prototype('array') 41 | ->fixXmlConfig('attribute') 42 | ->children() 43 | ->scalarNode('pattern')->defaultValue('^/')->end() 44 | ->scalarNode('repository')->defaultNull()->end() 45 | ->arrayNode('attributes') 46 | ->defaultValue([ResourceController::ROLE_RESOURCE_READ, ResourceController::ROLE_RESOURCE_WRITE]) 47 | ->prototype('scalar')->end() 48 | ->end() 49 | ->arrayNode('require') 50 | ->isRequired() 51 | ->requiresAtLeastOneElement() 52 | ->beforeNormalization() 53 | ->ifString() 54 | ->then(function ($v) { 55 | return [$v]; 56 | }) 57 | ->end() 58 | ->prototype('scalar')->end() 59 | ->end() // roles 60 | ->end() 61 | ->end() 62 | ->end() // access_control 63 | ->end() 64 | ->end() // security 65 | 66 | ->arrayNode('payload_alias_map') 67 | ->useAttributeAsKey('name') 68 | ->prototype('array') 69 | ->children() 70 | ->scalarNode('repository')->end() 71 | ->scalarNode('type')->end() 72 | ->end() 73 | ->end() 74 | ->end() // payload_alias_map 75 | ->end(); 76 | 77 | return $treeBuilder; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Registry/PayloadAliasRegistry.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class PayloadAliasRegistry 24 | { 25 | /** 26 | * @var array 27 | */ 28 | private $aliasesByRepository = []; 29 | 30 | /** 31 | * @var RepositoryRegistryInterface 32 | */ 33 | private $repositoryRegistry; 34 | 35 | public function __construct(RepositoryRegistryInterface $repositoryRegistry, array $aliases = []) 36 | { 37 | $this->repositoryRegistry = $repositoryRegistry; 38 | 39 | foreach ($aliases as $alias => $config) { 40 | if (!isset($this->aliasesByRepository[$config['repository']])) { 41 | $this->aliasesByRepository[$config['repository']] = []; 42 | } 43 | 44 | $this->aliasesByRepository[$config['repository']][$config['type']] = $alias; 45 | } 46 | } 47 | 48 | /** 49 | * Return the alias for the given PHPCR resource. 50 | * 51 | * @return string 52 | */ 53 | public function getPayloadAlias(PuliResource $resource) 54 | { 55 | $repositoryType = $this->repositoryRegistry->getRepositoryType( 56 | $resource->getRepository() 57 | ); 58 | 59 | $type = null; 60 | if ($resource instanceof CmfResource) { 61 | $type = $resource->getPayloadType(); 62 | } 63 | 64 | if (null === $type) { 65 | return; 66 | } 67 | 68 | if (!isset($this->aliasesByRepository[$repositoryType])) { 69 | return; 70 | } 71 | 72 | if (!isset($this->aliasesByRepository[$repositoryType][$type])) { 73 | return; 74 | } 75 | 76 | return $this->aliasesByRepository[$repositoryType][$type]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Resources/config/resource-rest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/config/routing.yml: -------------------------------------------------------------------------------- 1 | _cmf_delete_resource: 2 | path: /api/{repositoryName}/{path} 3 | methods: ['delete'] 4 | requirements: 5 | path: .* 6 | defaults: 7 | _controller: cmf_resource_rest.controller.resource:deleteResourceAction 8 | 9 | _cmf_patch_resource: 10 | path: /api/{repositoryName}/{path} 11 | methods: ['patch'] 12 | requirements: 13 | path: .* 14 | defaults: 15 | _controller: cmf_resource_rest.controller.resource:patchResourceAction 16 | 17 | _cmf_get_resource: 18 | path: /api/{repositoryName}/{path} 19 | methods: ['get'] 20 | requirements: 21 | path: .* 22 | defaults: 23 | _controller: cmf_resource_rest.controller.resource:getResourceAction 24 | -------------------------------------------------------------------------------- /src/Resources/config/schema/resource-rest.xsd: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Resources/config/security.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | %cmf_resource_rest.security.access_map% 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Resources/config/serializer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | %cmf_resource_rest.max_depth% 13 | %cmf_resource_rest.expose_payload% 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Symfony Cmf Resource Rest Bundle 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2011-2017 Symfony CMF 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/Security/ResourcePathVoter.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ResourcePathVoter extends Voter 23 | { 24 | private $accessDecisionManager; 25 | 26 | private $accessMap; 27 | 28 | public function __construct(AccessDecisionManagerInterface $accessDecisionManager, array $accessMap) 29 | { 30 | $this->accessDecisionManager = $accessDecisionManager; 31 | $this->accessMap = $accessMap; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function supports($attribute, $subject) 38 | { 39 | return \in_array($attribute, [ResourceController::ROLE_RESOURCE_READ, ResourceController::ROLE_RESOURCE_WRITE]) 40 | && \is_array($subject) && isset($subject['repository_name']) && isset($subject['path']); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | protected function voteOnAttribute($attribute, $subject, TokenInterface $token) 47 | { 48 | foreach ($this->accessMap as $rule) { 49 | if (!$this->ruleMatches($rule, $attribute, $subject)) { 50 | continue; 51 | } 52 | 53 | if ($this->accessDecisionManager->decide($token, $rule['require'])) { 54 | return true; 55 | } 56 | } 57 | 58 | return false; 59 | } 60 | 61 | private function ruleMatches($rule, $attribute, $subject) 62 | { 63 | if (!\in_array($attribute, $rule['attributes'])) { 64 | return false; 65 | } 66 | 67 | if (null !== $rule['repository'] && $rule['repository'] !== $subject['repository_name']) { 68 | return false; 69 | } 70 | 71 | if (!preg_match('{'.$rule['pattern'].'}', $subject['path'])) { 72 | return false; 73 | } 74 | 75 | return true; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Serializer/Jms/EventSubscriber/PhpcrNodeSubscriber.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class PhpcrNodeSubscriber implements EventSubscriberInterface 25 | { 26 | public static function getSubscribedEvents() 27 | { 28 | return [ 29 | [ 30 | 'event' => Events::PRE_SERIALIZE, 31 | 'method' => 'onPreSerialize', 32 | ], 33 | ]; 34 | } 35 | 36 | public function onPreSerialize(PreSerializeEvent $event) 37 | { 38 | $object = $event->getObject(); 39 | 40 | if ($object instanceof NodeInterface) { 41 | $event->setType('PHPCR\NodeInterface'); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Serializer/Jms/EventSubscriber/ResourceSubscriber.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ResourceSubscriber implements EventSubscriberInterface 25 | { 26 | public static function getSubscribedEvents() 27 | { 28 | return [ 29 | [ 30 | 'event' => Events::PRE_SERIALIZE, 31 | 'method' => 'onPreSerialize', 32 | ], 33 | ]; 34 | } 35 | 36 | public function onPreSerialize(PreSerializeEvent $event) 37 | { 38 | $object = $event->getObject(); 39 | 40 | if ($object instanceof PuliResource) { 41 | $event->setType('Puli\Repository\Api\Resource\PuliResource'); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Serializer/Jms/Handler/PhpcrNodeHandler.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class PhpcrNodeHandler implements SubscribingHandlerInterface 26 | { 27 | public static function getSubscribingMethods() 28 | { 29 | return [ 30 | [ 31 | 'event' => GraphNavigator::DIRECTION_SERIALIZATION, 32 | 'format' => 'json', 33 | 'type' => 'PHPCR\NodeInterface', 34 | 'method' => 'serializePhpcrNode', 35 | ], 36 | ]; 37 | } 38 | 39 | /** 40 | * @param NodeInterface $nodeInterface 41 | */ 42 | public function serializePhpcrNode( 43 | SerializationVisitorInterface $visitor, 44 | NodeInterface $node, 45 | array $type, 46 | Context $context 47 | ) { 48 | $res = []; 49 | 50 | foreach ($node->getProperties() as $name => $property) { 51 | $res[$name] = $property->getValue(); 52 | } 53 | 54 | return $res; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Serializer/Jms/Handler/ResourceHandler.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class ResourceHandler implements SubscribingHandlerInterface 32 | { 33 | private $registry; 34 | 35 | private $payloadAliasRegistry; 36 | 37 | private $descriptionFactory; 38 | 39 | private $maxDepth; 40 | 41 | private $exposePayload; 42 | 43 | public function __construct( 44 | RepositoryRegistryInterface $registry, 45 | PayloadAliasRegistry $payloadAliasRegistry, 46 | DescriptionFactory $descriptionFactory, 47 | $maxDepth = 2, 48 | $exposePayload = false 49 | ) { 50 | $this->registry = $registry; 51 | $this->payloadAliasRegistry = $payloadAliasRegistry; 52 | $this->descriptionFactory = $descriptionFactory; 53 | $this->maxDepth = $maxDepth; 54 | $this->exposePayload = $exposePayload; 55 | } 56 | 57 | public static function getSubscribingMethods() 58 | { 59 | return [ 60 | [ 61 | 'event' => GraphNavigator::DIRECTION_SERIALIZATION, 62 | 'format' => 'json', 63 | 'type' => 'Puli\Repository\Api\Resource\PuliResource', 64 | 'method' => 'serializeResource', 65 | ], 66 | ]; 67 | } 68 | 69 | /** 70 | * @param NodeInterface $resourceInterface 71 | */ 72 | public function serializeResource( 73 | SerializationVisitorInterface $visitor, 74 | PuliResource $resource, 75 | array $type, 76 | Context $context 77 | ) { 78 | $data = $this->doSerializeResource($resource); 79 | $context->getNavigator()->accept($data); 80 | 81 | return $data; 82 | } 83 | 84 | public function setMaxDepth($maxDepth) 85 | { 86 | $this->maxDepth = $maxDepth; 87 | } 88 | 89 | private function doSerializeResource(PuliResource $resource, $depth = 0) 90 | { 91 | $data = []; 92 | $repositoryAlias = $this->registry->getRepositoryName($resource->getRepository()); 93 | 94 | $data['repository_alias'] = $repositoryAlias; 95 | $data['repository_type'] = $this->registry->getRepositoryType($resource->getRepository()); 96 | $data['payload_alias'] = $this->payloadAliasRegistry->getPayloadAlias($resource); 97 | $data['payload_type'] = null; 98 | 99 | if ($resource instanceof CmfResource) { 100 | $data['payload_type'] = $resource->getPayloadType(); 101 | 102 | if ($this->exposePayload && null !== $resource->getPayload()) { 103 | $data['payload'] = $resource->getPayload(); 104 | } 105 | } 106 | 107 | $data['path'] = $resource->getPath(); 108 | $data['label'] = $data['node_name'] = PathHelper::getNodeName($data['path']); 109 | $data['repository_path'] = $resource->getRepositoryPath(); 110 | 111 | $children = []; 112 | foreach ($resource->listChildren() as $name => $childResource) { 113 | $children[$name] = []; 114 | 115 | if ($depth < $this->maxDepth) { 116 | $children[$name] = $this->doSerializeResource($childResource, $depth + 1); 117 | } 118 | } 119 | $data['children'] = $children; 120 | 121 | $description = $this->descriptionFactory->getPayloadDescriptionFor($resource); 122 | $data['descriptors'] = $description->all(); 123 | 124 | return $data; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Features/Context/ResourceContext.php: -------------------------------------------------------------------------------- 1 | kernel = new Kernel('test', true); 43 | } 44 | 45 | /** 46 | * Return the path of the configuration file used by the AppKernel. 47 | * 48 | * @static 49 | * 50 | * @return string 51 | */ 52 | public static function getConfigurationFile() 53 | { 54 | return __DIR__.'/../../Fixtures/App/var/cache/resource.yml'; 55 | } 56 | 57 | /** 58 | * @BeforeScenario 59 | */ 60 | public function beforeScenario(BeforeScenarioScope $scope) 61 | { 62 | if (file_exists(self::getConfigurationFile())) { 63 | unlink(self::getConfigurationFile()); 64 | } 65 | 66 | $this->clearDiCache(); 67 | 68 | $this->kernel->boot(); 69 | 70 | $this->manager = $this->kernel->getContainer()->get('doctrine_phpcr.odm.document_manager'); 71 | $this->session = $this->manager->getPhpcrSession(); 72 | 73 | if ($this->session->getRootNode()->hasNode('tests')) { 74 | $this->session->removeItem('/tests'); 75 | $this->session->save(); 76 | } 77 | } 78 | 79 | /** 80 | * @AfterScenario 81 | */ 82 | public function refreshSession() 83 | { 84 | $this->session->refresh(true); 85 | $this->kernel->shutdown(); 86 | } 87 | 88 | /** 89 | * @Given the test application has the following configuration: 90 | */ 91 | public function setApplicationConfig(PyStringNode $config) 92 | { 93 | file_put_contents(self::getConfigurationFile(), $config->getRaw()); 94 | } 95 | 96 | /** 97 | * @Given there is a file named :filename with: 98 | */ 99 | public function createFile($filename, PyStringNode $content) 100 | { 101 | $filesytem = new Filesystem(); 102 | $file = str_replace('%kernel.root_dir%', $this->kernel->getRootDir(), $filename); 103 | $filesytem->mkdir(\dirname($file)); 104 | 105 | file_put_contents($file, (string) $content); 106 | } 107 | 108 | /** 109 | * @Given there exists a/an :class document at :path: 110 | */ 111 | public function createDocument($class, $path, TableNode $fields) 112 | { 113 | $class = 'Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\'.$class; 114 | $path = '/tests'.$path; 115 | 116 | $parentPath = PathHelper::getParentPath($path); 117 | 118 | if (!$this->session->nodeExists($parentPath)) { 119 | NodeHelper::createPath($this->session, $parentPath); 120 | } 121 | 122 | if (!class_exists($class)) { 123 | throw new \InvalidArgumentException(sprintf('Class "%s" does not exist', $class)); 124 | } 125 | 126 | $document = new $class(); 127 | $document->id = $path; 128 | 129 | foreach ($fields->getRowsHash() as $field => $value) { 130 | $document->$field = $value; 131 | } 132 | 133 | $this->manager->persist($document); 134 | $this->manager->flush(); 135 | $this->manager->clear(); 136 | } 137 | 138 | /** 139 | * @Then there is a/an :class document at :path 140 | * @Then there is a/an :class document at :path: 141 | */ 142 | public function thereIsADocumentAt($class, $path, TableNode $fields = null) 143 | { 144 | $class = 'Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\'.$class; 145 | $path = '/tests'.$path; 146 | 147 | if (!class_exists($class)) { 148 | throw new \InvalidArgumentException(sprintf('Class "%s" does not exist', $class)); 149 | } 150 | 151 | $document = $this->manager->find($class, $path); 152 | 153 | Assert::notNull($document, sprintf('No "%s" document exists at "%s"', $class, $path)); 154 | 155 | if (null === $fields) { 156 | return; 157 | } 158 | 159 | foreach ($fields->getRowsHash() as $field => $value) { 160 | Assert::eq($document->$field, $value); 161 | } 162 | } 163 | 164 | /** 165 | * @Then there is no :class document at :path 166 | */ 167 | public function thereIsNoDocumentAt($class, $path) 168 | { 169 | $class = 'Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\'.$class; 170 | $path = '/tests'.$path; 171 | 172 | if (!class_exists($class)) { 173 | throw new \InvalidArgumentException(sprintf('Class "%s" does not exist', $class)); 174 | } 175 | 176 | $this->session->refresh(true); 177 | $this->manager->clear(); 178 | 179 | $document = $this->manager->find($class, $path); 180 | 181 | Assert::null($document, sprintf('A "%s" document does exist at "%s".', $class, $path)); 182 | } 183 | 184 | private function clearDiCache() 185 | { 186 | $finder = new Finder(); 187 | $dirs = $this->kernel->getCacheDir(); 188 | if (!is_dir($dirs)) { 189 | return; 190 | } 191 | $finder->in($dirs); 192 | $finder->name('*.php'); 193 | $finder->name('*.php.meta'); 194 | $filesystem = new Filesystem(); 195 | $filesystem->remove($finder); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/Features/nesting.feature: -------------------------------------------------------------------------------- 1 | Feature: Nesting resources 2 | In order to retrieve a tree of data 3 | As a webservice user 4 | I need to be able to get nested resources 5 | 6 | Background: 7 | Given the test application has the following configuration: 8 | """ 9 | cmf_resource: 10 | repositories: 11 | default: 12 | type: doctrine/phpcr-odm 13 | basepath: /tests/cmf/articles 14 | 15 | cmf_resource_rest: 16 | security: 17 | access_control: 18 | - { pattern: '^/', require: IS_AUTHENTICATED_ANONYMOUSLY } 19 | """ 20 | And there exists an "Article" document at "/cmf/articles/foo": 21 | | title | Article 1 | 22 | | body | This is my article | 23 | And there exists an "Article" document at "/cmf/articles/foo/sub": 24 | | title | Sub-article 1 | 25 | | body | This is my article | 26 | 27 | Scenario: Retrieving nested resources 28 | When I send a GET request to "/api/default/foo" 29 | Then the response should contain json: 30 | """ 31 | { 32 | "repository_alias": "default", 33 | "repository_type": "doctrine/phpcr-odm", 34 | "payload_alias": null, 35 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 36 | "path": "\/foo", 37 | "node_name": "foo", 38 | "label": "foo", 39 | "repository_path": "\/foo", 40 | "children": { 41 | "sub": { 42 | "repository_alias": "default", 43 | "repository_type": "doctrine/phpcr-odm", 44 | "payload_alias": null, 45 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 46 | "path": "\/foo\/sub", 47 | "node_name": "sub", 48 | "label": "sub", 49 | "repository_path": "\/foo\/sub", 50 | "children": [], 51 | "descriptors": [] 52 | } 53 | }, 54 | "descriptors": [] 55 | } 56 | """ 57 | 58 | Scenario: Specifying a depth 59 | When I send a GET request to "/api/default/foo?depth=0" 60 | Then the response should contain json: 61 | """ 62 | { 63 | "repository_alias": "default", 64 | "repository_type": "doctrine/phpcr-odm", 65 | "payload_alias": null, 66 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 67 | "path": "\/foo", 68 | "node_name": "foo", 69 | "label": "foo", 70 | "repository_path": "\/foo", 71 | "children": { 72 | "sub": [] 73 | }, 74 | "descriptors": [] 75 | } 76 | """ 77 | -------------------------------------------------------------------------------- /tests/Features/resource_api_filesystem.feature: -------------------------------------------------------------------------------- 1 | Feature: Filesystem resource repository 2 | In order to retrieve data from the resource webservice 3 | As a webservice user 4 | I need to be able to query the webservice 5 | 6 | # Background: 7 | # Given the test application has the following configuration: 8 | # """ 9 | # cmf_resource: 10 | # repositories: 11 | # default: 12 | # type: puli/filesystem 13 | # base_dir: "%kernel.root_dir%/Resources/views/snippets" 14 | # """ 15 | # And there is a file named "%kernel.root_dir%/Resources/views/snippets/snippet1.html" with: 16 | # """ 17 | #

Snippet 1

18 | # """ 19 | # 20 | # 21 | # Scenario: Retrieve filesystem resource 22 | # When I send a GET request to "/api/default/snippet1.html" 23 | # Then the response should contain json: 24 | # """ 25 | # { 26 | # "repository_alias": "default", 27 | # "repository_type": "puli/filesystem", 28 | # "payload_alias": null, 29 | # "payload_type": null, 30 | # "path": "\/snippet1.html", 31 | # "node_name": "snippet1.html", 32 | # "label": "snippet1.html", 33 | # "repository_path": "\/snippet1.html", 34 | # "children": [], 35 | # "body": "

Snippet 1

" 36 | # } 37 | # """ 38 | -------------------------------------------------------------------------------- /tests/Features/resource_api_phpcr.feature: -------------------------------------------------------------------------------- 1 | Feature: PHPCR resource repository 2 | In order to retrieve data from the resource webservice 3 | As a webservice user 4 | I need to be able to query the webservice 5 | 6 | Background: 7 | Given the test application has the following configuration: 8 | """ 9 | cmf_resource: 10 | description: { enhancers: [dummy] } 11 | repositories: 12 | phpcr_repo: 13 | type: phpcr/phpcr 14 | basepath: /tests/cmf/articles 15 | 16 | cmf_resource_rest: 17 | expose_payload: true 18 | security: 19 | access_control: 20 | - { pattern: '^/', require: IS_AUTHENTICATED_ANONYMOUSLY } 21 | """ 22 | 23 | 24 | Scenario: Retrieve PHPCR resource with children 25 | Given there exists an "Article" document at "/cmf/articles/foo": 26 | | title | Article 1 | 27 | | body | This is my article | 28 | When I send a GET request to "/api/phpcr_repo/foo" 29 | Then the response should contain json: 30 | """ 31 | { 32 | "repository_alias": "phpcr_repo", 33 | "repository_type": "phpcr/phpcr", 34 | "payload_alias": null, 35 | "payload_type": "nt:unstructured", 36 | "path": "\/foo", 37 | "node_name": "foo", 38 | "label": "foo", 39 | "repository_path": "\/foo", 40 | "children": [], 41 | "descriptors": { 42 | "name_reverse": "oof" 43 | } 44 | } 45 | """ 46 | 47 | Scenario: Rename a PHPCR resource 48 | Given there exists an "Article" document at "/cmf/articles/foo": 49 | | title | Article 1 | 50 | | body | This is my article | 51 | When I send a PATCH request to "/api/phpcr_repo/foo" with body: 52 | """ 53 | [{"operation": "move", "target": "/foo-bar"}] 54 | """ 55 | Then the response code should be 200 56 | And there is an "Article" document at "/cmf/articles/foo-bar" 57 | | title | Article 1 | 58 | | body | This is my article | 59 | 60 | Scenario: Move a PHPCR resource 61 | Given there exists an "Article" document at "/cmf/articles/foo": 62 | | title | Article 1 | 63 | | body | This is my article | 64 | And there exists a "Article" document at "/cmf/articles/bar": 65 | | title | Article 2 | 66 | | body | Another one | 67 | When I send a PATCH request to "/api/phpcr_repo/foo" with body: 68 | """ 69 | [{"operation": "move", "target": "/bar/foo"}] 70 | """ 71 | Then the response code should be 200 72 | And there is an "Article" document at "/cmf/articles/bar/foo" 73 | | title | Article 1 | 74 | | body | This is my article | 75 | 76 | Scenario: Remove a PHPCR resource 77 | Given there exists an "Article" document at "/cmf/articles/foo": 78 | | title | Article 1 | 79 | | body | This is my article | 80 | When I send a DELETE request to "/api/phpcr_repo/foo" 81 | Then the response code should be 204 82 | And there is no "Article" document at "/api/phpcr_repo/bar/foo" 83 | -------------------------------------------------------------------------------- /tests/Features/resource_api_phpcr_odm.feature: -------------------------------------------------------------------------------- 1 | Feature: PHPCR-ODM resource repository 2 | In order to retrieve data from the resource webservice 3 | As a webservice user 4 | I need to be able to query the webservice 5 | 6 | Background: 7 | Given the test application has the following configuration: 8 | """ 9 | cmf_resource: 10 | description: { enhancers: [dummy] } 11 | repositories: 12 | phpcrodm_repo: 13 | type: doctrine/phpcr-odm 14 | basepath: /tests/cmf/articles 15 | 16 | cmf_resource_rest: 17 | payload_alias_map: 18 | article: 19 | repository: doctrine/phpcr-odm 20 | type: "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article" 21 | security: 22 | access_control: 23 | - { pattern: '^/', require: IS_AUTHENTICATED_ANONYMOUSLY } 24 | """ 25 | 26 | 27 | Scenario: Retrieve a PHPCR-ODM resource 28 | Given there exists an "Article" document at "/cmf/articles/foo": 29 | | title | Article 1 | 30 | | body | This is my article | 31 | When I send a GET request to "/api/phpcrodm_repo/foo" 32 | Then the response code should be 200 33 | And the response should contain json: 34 | """ 35 | { 36 | "repository_alias": "phpcrodm_repo", 37 | "repository_type": "doctrine/phpcr-odm", 38 | "payload_alias": "article", 39 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 40 | "path": "\/foo", 41 | "node_name": "foo", 42 | "label": "foo", 43 | "repository_path": "\/foo", 44 | "children": [], 45 | "descriptors": { 46 | "name_reverse": "oof" 47 | } 48 | } 49 | """ 50 | 51 | Scenario: Retrieve a PHPCR-ODM resource with children 52 | Given there exists an "Article" document at "/cmf/articles/foo": 53 | | title | Article 1 | 54 | | body | This is my article | 55 | And there exists an "Article" document at "/cmf/articles/foo/bar": 56 | | title | Article child | 57 | | body | There are many like it | 58 | And there exists an "Article" document at "/cmf/articles/foo/boo": 59 | | title | Article child | 60 | | body | But this one is mine | 61 | When I send a GET request to "/api/phpcrodm_repo/foo" 62 | Then the response code should be 200 63 | And the response should contain json: 64 | """ 65 | { 66 | "repository_alias": "phpcrodm_repo", 67 | "repository_type": "doctrine/phpcr-odm", 68 | "payload_alias": "article", 69 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 70 | "path": "\/foo", 71 | "node_name": "foo", 72 | "label": "foo", 73 | "repository_path": "\/foo", 74 | "children": { 75 | "bar": { 76 | "repository_alias": "phpcrodm_repo", 77 | "repository_type": "doctrine/phpcr-odm", 78 | "payload_alias": "article", 79 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 80 | "path": "/foo/bar", 81 | "node_name": "bar", 82 | "label": "bar", 83 | "repository_path": "/foo/bar", 84 | "children": [ ], 85 | "descriptors": { "name_reverse": "rab" } 86 | }, 87 | "boo": { 88 | "repository_alias": "phpcrodm_repo", 89 | "repository_type": "doctrine/phpcr-odm", 90 | "payload_alias": "article", 91 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 92 | "path": "/foo/boo", 93 | "node_name": "boo", 94 | "label": "boo", 95 | "repository_path": "/foo/boo", 96 | "children": [ ], 97 | "descriptors": { "name_reverse": "oob" } 98 | } 99 | }, 100 | "descriptors": { 101 | "name_reverse": "oof" 102 | } 103 | } 104 | """ 105 | 106 | Scenario: Rename a PHPCR-ODM resource 107 | Given there exists an "Article" document at "/cmf/articles/foo": 108 | | title | Article 1 | 109 | | body | This is my article | 110 | When I send a PATCH request to "/api/phpcrodm_repo/foo" with body: 111 | """ 112 | [{"operation": "move", "target": "/foo-bar"}] 113 | """ 114 | Then the response code should be 200 115 | And there is an "Article" document at "/cmf/articles/foo-bar": 116 | | title | Article 1 | 117 | | body | This is my article | 118 | 119 | Scenario: Move a PHPCR-ODM resource 120 | Given there exists an "Article" document at "/cmf/articles/foo": 121 | | title | Article 1 | 122 | | body | This is my article | 123 | And there exists an "Article" document at "/cmf/articles/bar": 124 | | title | Article 2 | 125 | | body | Another one | 126 | When I send a PATCH request to "/api/phpcrodm_repo/foo" with body: 127 | """ 128 | [{"operation": "move", "target": "/bar/foo"}] 129 | """ 130 | Then the response code should be 200 131 | And the response should contain json: 132 | """ 133 | { 134 | "repository_alias": "phpcrodm_repo", 135 | "repository_type": "doctrine/phpcr-odm", 136 | "payload_alias": "article", 137 | "payload_type": "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Fixtures\\App\\Document\\Article", 138 | "path": "\/bar\/foo", 139 | "node_name": "foo", 140 | "label": "foo", 141 | "repository_path": "\/bar\/foo", 142 | "children": [] 143 | } 144 | """ 145 | And there is an "Article" document at "/cmf/articles/bar/foo": 146 | | title | Article 1 | 147 | | body | This is my article | 148 | 149 | Scenario: Remove a PHPCR-ODM resource 150 | Given there exists an "Article" document at "/cmf/articles/foo": 151 | | title | Article 1 | 152 | | body | This is my article | 153 | When I send a DELETE request to "/api/phpcrodm_repo/foo" 154 | Then the response code should be 204 155 | And there is no "Article" document at "/cmf/articles/foo" 156 | -------------------------------------------------------------------------------- /tests/Features/security.feature: -------------------------------------------------------------------------------- 1 | Feature: Security 2 | In order to deny API access to private files 3 | As a developer 4 | I need to be able to write security voters 5 | 6 | Background: 7 | Given the test application has the following configuration: 8 | """ 9 | cmf_resource: 10 | repositories: 11 | security: 12 | type: phpcr/phpcr 13 | basepath: /tests/cmf/articles 14 | 15 | cmf_resource_rest: 16 | security: 17 | access_control: 18 | - { pattern: '^/tests/cmf/articles/private', repository: security, require: ROLE_ADMIN } 19 | """ 20 | And there exists an "Article" document at "/private/foo": 21 | | title | Article 1 | 22 | | body | This is my article | 23 | 24 | Scenario: Retrieve a protected resource 25 | When I send a GET request to "/api/security/private/foo" 26 | Then the response code should be 401 27 | 28 | Scenario: Retrieve a protected non-existent resource 29 | When I send a GET request to "/api/security/private/bar" 30 | Then the response code should be 401 31 | 32 | Scenario: Remove a protected resource 33 | When I send a DELETE request to "/api/security/private/admin/something" 34 | Then the response code should be 401 35 | 36 | Scenario: Edit a resource 37 | When I send a PATCH request to "/api/security/admin/file" 38 | Then the response code should be 401 39 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Description/DummyEnhancer.php: -------------------------------------------------------------------------------- 1 | set('name_reverse', strrev($description->getResource()->getName())); 24 | } 25 | 26 | public function supports(PuliResource $resource) 27 | { 28 | return $resource instanceof CmfResource; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/Article.php: -------------------------------------------------------------------------------- 1 | requireBundleSets([ 26 | 'default', 'phpcr_odm', 27 | ]); 28 | 29 | $this->registerConfiguredBundles(); 30 | } 31 | 32 | public function registerContainerConfiguration(LoaderInterface $loader) 33 | { 34 | $loader->load(__DIR__.'/config/config.php'); 35 | 36 | if ('behat' !== $this->getEnvironment() && file_exists(ResourceContext::getConfigurationFile())) { 37 | $loader->import(ResourceContext::getConfigurationFile()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Resources/views/snippets/snippet1.html: -------------------------------------------------------------------------------- 1 |

Snippet 1

-------------------------------------------------------------------------------- /tests/Fixtures/App/Security/ResourceVoter.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 20 | CmfResourceBundle::class => ['all' => true], 21 | JMSSerializerBundle::class => ['all' => true], 22 | TwigBundle::class => ['all' => true], 23 | WebServerBundle::class => ['all' => true], 24 | ]; 25 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/config.php: -------------------------------------------------------------------------------- 1 | setParameter('cmf_testing.bundle_fqn', 'Symfony\Cmf\Bundle\ResourceRestBundle'); 15 | $loader->import(CMF_TEST_CONFIG_DIR.'/dist/parameters.yml'); 16 | $loader->import(CMF_TEST_CONFIG_DIR.'/dist/framework.php'); 17 | $loader->import(CMF_TEST_CONFIG_DIR.'/dist/monolog.yml'); 18 | $loader->import(CMF_TEST_CONFIG_DIR.'/dist/doctrine.yml'); 19 | $loader->import(CMF_TEST_CONFIG_DIR.'/dist/security.yml'); 20 | $loader->import(CMF_TEST_CONFIG_DIR.'/phpcr_odm.php'); 21 | 22 | $container->register('app.dummy_enhancer', DummyEnhancer::class) 23 | ->addTag('cmf_resource.description.enhancer', ['alias' => 'dummy']); 24 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/routing.php: -------------------------------------------------------------------------------- 1 | addCollection( 16 | $loader->import('@CmfResourceRestBundle/Resources/config/routing.yml') 17 | ); 18 | 19 | return $collection; 20 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/CmfResourceRestExtensionTest.php: -------------------------------------------------------------------------------- 1 | [ 30 | 'article' => [ 31 | 'repository' => 'doctrine_phpcr_odm', 32 | 'type' => 'Article', 33 | ], 34 | ], 35 | ], 36 | ], 37 | ]; 38 | } 39 | 40 | /** 41 | * @dataProvider provideExtension 42 | */ 43 | public function testExtension($config) 44 | { 45 | $this->container->setParameter('kernel.bundles', ['JMSSerializerBundle' => true]); 46 | 47 | $this->load($config); 48 | 49 | $this->compile(); 50 | } 51 | 52 | public function testNoJmsSerializerBundleRegistered() 53 | { 54 | $this->container->setParameter('kernel.bundles', []); 55 | 56 | $this->expectException(\LogicException::class); 57 | $this->expectExceptionMessage('The JMSSerializerBundle must be registered'); 58 | 59 | $this->load([]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | assertProcessedConfigurationEquals([ 46 | 'payload_alias_map' => [ 47 | 'article' => [ 48 | 'repository' => 'doctrine_phpcr_odm', 49 | 'type' => 'Namespace\Article', 50 | ], 51 | ], 52 | 'max_depth' => 2, 53 | 'expose_payload' => false, 54 | 'security' => [ 55 | 'access_control' => [ 56 | ['pattern' => '^/cms/public', 'attributes' => ['CMF_RESOURCE_READ'], 'require' => ['IS_AUTHENTICATED_ANONYMOUSLY'], 'repository' => null], 57 | ['pattern' => '^/cms/members-only', 'attributes' => ['CMF_RESOURCE_READ'], 'require' => ['ROLE_USER'], 'repository' => null], 58 | ['pattern' => '^/', 'attributes' => ['CMF_RESOURCE_WRITE'], 'require' => ['ROLE_ADMIN'], 'repository' => null], 59 | ], 60 | ], 61 | ], [$source]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/fixtures/config.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | CMF_RESOURCE_READ 14 | 15 | 16 | 17 | ROLE_USER 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/fixtures/config.yml: -------------------------------------------------------------------------------- 1 | cmf_resource_rest: 2 | payload_alias_map: 3 | article: 4 | repository: doctrine_phpcr_odm 5 | type: Namespace\Article 6 | 7 | security: 8 | access_control: 9 | - { pattern: ^/cms/public, attributes: [CMF_RESOURCE_READ], require: IS_AUTHENTICATED_ANONYMOUSLY } 10 | - { pattern: ^/cms/members-only, attributes: [CMF_RESOURCE_READ], require: ROLE_USER } 11 | - { pattern: ^/, attributes: [CMF_RESOURCE_WRITE], require: ROLE_ADMIN } 12 | -------------------------------------------------------------------------------- /tests/Unit/Registry/PayloadAliasRegistryTest.php: -------------------------------------------------------------------------------- 1 | repositoryRegistry = $this->prophesize('Symfony\Cmf\Component\Resource\RepositoryRegistryInterface'); 28 | $this->resource = $this->prophesize('Symfony\Cmf\Component\Resource\Repository\Resource\CmfResource'); 29 | $this->repository = $this->prophesize('Symfony\Cmf\Component\Resource\Puli\Api\ResourceRepository'); 30 | } 31 | 32 | public function provideRegistry() 33 | { 34 | return [ 35 | [ 36 | [ 37 | 'article' => [ 38 | 'repository' => 'doctrine_phpcr_odm', 39 | 'type' => 'Article', 40 | ], 41 | ], 42 | [ 43 | 'type' => null, 44 | 'repository' => 'doctrine_phpcr_odm', 45 | ], 46 | null, 47 | ], 48 | ]; 49 | } 50 | 51 | /** 52 | * @dataProvider provideRegistry 53 | */ 54 | public function testRegistry($aliases, $resource, $expectedAlias) 55 | { 56 | $registry = $this->createRegistry($aliases); 57 | 58 | $this->repositoryRegistry->getRepositoryType( 59 | $this->repository 60 | )->willReturn($resource['repository']); 61 | $this->resource->getPayloadType()->willReturn($resource['type']); 62 | $this->resource->getRepository()->willReturn($this->repository); 63 | 64 | $alias = $registry->getPayloadAlias($this->resource->reveal()); 65 | self::assertEquals($expectedAlias, $alias); 66 | } 67 | 68 | private function createRegistry($aliases) 69 | { 70 | return new PayloadAliasRegistry($this->repositoryRegistry->reveal(), $aliases); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Unit/Security/ResourcePathVoterTest.php: -------------------------------------------------------------------------------- 1 | accessDecisionManager = $this->prophesize(AccessDecisionManagerInterface::class); 28 | } 29 | 30 | /** 31 | * @dataProvider provideVoteData 32 | */ 33 | public function testVote($rules, $subject, array $attributes, $result) 34 | { 35 | $token = $this->prophesize(TokenInterface::class)->reveal(); 36 | 37 | $this->accessDecisionManager->decide($token, ['ROLE_USER'])->willReturn(true); 38 | $this->accessDecisionManager->decide($token, ['ROLE_ADMIN'])->willReturn(false); 39 | 40 | $voter = new ResourcePathVoter($this->accessDecisionManager->reveal(), $rules); 41 | 42 | self::assertSame($result, $voter->vote($token, $subject, $attributes)); 43 | } 44 | 45 | public function provideVoteData() 46 | { 47 | $ruleSet1 = [ 48 | $this->buildRule('^/', ['ROLE_USER'], ['CMF_RESOURCE_READ']), 49 | $this->buildRule('^/cms/private', ['ROLE_ADMIN'], ['CMF_RESOURCE_WRITE']), 50 | ]; 51 | 52 | return [ 53 | // Basic behaviour 54 | [[$this->buildRule('^/')], $this->buildSubject('/cms/articles/foo'), ['CMF_RESOURCE_READ'], V::ACCESS_GRANTED], 55 | [[$this->buildRule('^/')], $this->buildSubject('/cms/articles/foo'), ['CMF_RESOURCE_WRITE'], V::ACCESS_GRANTED], 56 | [[$this->buildRule('^/', ['ROLE_ADMIN'])], $this->buildSubject('/cms/articles/foo'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED], 57 | 58 | // Multiple rules 59 | [$ruleSet1, $this->buildSubject('/cms/private/admin'), ['CMF_RESOURCE_READ'], V::ACCESS_GRANTED], 60 | [$ruleSet1, $this->buildSubject('/cms/private/admin'), ['CMF_RESOURCE_WRITE'], V::ACCESS_DENIED], 61 | [$ruleSet1, $this->buildSubject('/cms/public'), ['CMF_RESOURCE_READ', 'CMF_RESOURCE_WRITE'], V::ACCESS_GRANTED], 62 | 63 | // Unsupported attributes or subjects 64 | [[], $this->buildSubject('/cms/articles'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED], 65 | [[$this->buildRule('^/')], $this->buildSubject('/cms/articles'), ['ROLE_USER'], V::ACCESS_ABSTAIN], 66 | [[$this->buildRule('^/')], new stdClass(), ['CMF_RESOURCE_READ'], V::ACCESS_ABSTAIN], 67 | 68 | // Repository name matching 69 | [[$this->buildRule('^/')], $this->buildSubject('/cms/articles', 'other_repo'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED], 70 | [[$this->buildRule('^/', ['ROLE_USER'], ['CMF_RESOURCE_READ'], 'other_repo')], $this->buildSubject('/cms/articles'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED], 71 | ]; 72 | } 73 | 74 | private function buildRule($pattern, $require = ['ROLE_USER'], $attributes = ['CMF_RESOURCE_READ', 'CMF_RESOURCE_WRITE'], $repository = 'default') 75 | { 76 | return ['pattern' => $pattern, 'attributes' => $attributes, 'require' => $require, 'repository' => $repository]; 77 | } 78 | 79 | private function buildSubject($path, $repository = 'default') 80 | { 81 | return ['path' => $path, 'repository_name' => $repository]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Unit/Serializer/Jms/EventSubscriber/PhpcrNodeSubscriberTest.php: -------------------------------------------------------------------------------- 1 | node = $this->prophesize('PHPCR\NodeInterface'); 30 | $this->event = $this->prophesize('JMS\Serializer\EventDispatcher\PreSerializeEvent'); 31 | $this->subscriber = new PhpcrNodeSubscriber(); 32 | } 33 | 34 | public function testPreSerialize() 35 | { 36 | $this->event->getObject()->willReturn($this->node->reveal()); 37 | $this->event->setType('PHPCR\NodeInterface')->shouldBeCalled(); 38 | $this->subscriber->onPreSerialize($this->event->reveal()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/Serializer/Jms/Handler/PhpcrNodeHandlerTest.php: -------------------------------------------------------------------------------- 1 | node = $this->prophesize('PHPCR\NodeInterface'); 31 | $this->property1 = $this->prophesize('PHPCR\PropertyInterface'); 32 | $this->property2 = $this->prophesize('PHPCR\PropertyInterface'); 33 | $this->visitor = $this->prophesize(SerializationVisitorInterface::class); 34 | $this->context = $this->prophesize('JMS\Serializer\Context'); 35 | $this->handler = new PhpcrNodeHandler(); 36 | } 37 | 38 | public function testHandler() 39 | { 40 | $this->property1->getValue()->willReturn('hello'); 41 | $this->property2->getValue()->willReturn('world'); 42 | $this->node->getProperties()->willReturn([ 43 | 'a' => $this->property1, 44 | 'b' => $this->property2, 45 | ]); 46 | 47 | $res = $this->handler->serializePhpcrNode( 48 | $this->visitor->reveal(), 49 | $this->node->reveal(), 50 | [], 51 | $this->context->reveal() 52 | ); 53 | 54 | $this->assertEquals([ 55 | 'a' => 'hello', 56 | 'b' => 'world', 57 | ], $res); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Unit/Serializer/Jms/Handler/ResourceHandlerTest.php: -------------------------------------------------------------------------------- 1 | repositoryRegistry = $this->prophesize(RepositoryRegistryInterface::class); 54 | $this->payloadAliasRegistry = $this->prophesize(PayloadAliasRegistry::class); 55 | $this->visitor = $this->prophesize(SerializationVisitorInterface::class); 56 | $this->resource = $this->prophesize(CmfResource::class); 57 | $this->childResource = $this->prophesize(CmfResource::class); 58 | 59 | $this->repository = $this->prophesize(ResourceRepository::class); 60 | $this->context = $this->prophesize(Context::class); 61 | $this->navigator = $this->prophesize(GraphNavigatorInterface::class); 62 | 63 | $this->description = $this->prophesize(Description::class); 64 | $this->description->all()->willReturn([]); 65 | $this->descriptionFactory = $this->prophesize(DescriptionFactory::class); 66 | $this->descriptionFactory->getPayloadDescriptionFor(Argument::any())->willReturn($this->description->reveal()); 67 | 68 | $this->handler = new ResourceHandler( 69 | $this->repositoryRegistry->reveal(), 70 | $this->payloadAliasRegistry->reveal(), 71 | $this->descriptionFactory->reveal() 72 | ); 73 | 74 | $this->resource->getRepository()->willReturn($this->repository); 75 | } 76 | 77 | public function testHandler() 78 | { 79 | $this->repositoryRegistry->getRepositoryName($this->repository)->willReturn('repo'); 80 | $this->repositoryRegistry->getRepositoryType($this->repository)->willReturn('repo_type'); 81 | $this->payloadAliasRegistry->getPayloadAlias($this->resource->reveal())->willReturn('alias'); 82 | $this->resource->getPayloadType()->willReturn('payload_type'); 83 | $this->resource->getPayload()->willReturn(null); 84 | $this->resource->getPath()->willReturn('/path/to'); 85 | $this->resource->getRepositoryPath()->willReturn('/repository/path'); 86 | $this->resource->listChildren()->willReturn([ 87 | $this->childResource, 88 | ]); 89 | 90 | $this->payloadAliasRegistry->getPayloadAlias($this->childResource->reveal())->willReturn('alias'); 91 | $this->childResource->getPayloadType()->willReturn('payload_type'); 92 | $this->childResource->getPayload()->willReturn(null); 93 | $this->childResource->getPath()->willReturn('/path/to/child'); 94 | $this->childResource->getRepositoryPath()->willReturn('/child/repository/path'); 95 | $this->childResource->getRepository()->willReturn($this->repository->reveal()); 96 | $this->childResource->listChildren()->willReturn([ 97 | ]); 98 | 99 | $expected = [ 100 | 'repository_alias' => 'repo', 101 | 'repository_type' => 'repo_type', 102 | 'payload_alias' => 'alias', 103 | 'payload_type' => 'payload_type', 104 | 'path' => '/path/to', 105 | 'node_name' => 'to', 106 | 'label' => 'to', 107 | 'repository_path' => '/repository/path', 108 | 'children' => [ 109 | [ 110 | 'repository_alias' => 'repo', 111 | 'repository_type' => 'repo_type', 112 | 'payload_alias' => 'alias', 113 | 'payload_type' => 'payload_type', 114 | 'path' => '/path/to/child', 115 | 'label' => 'child', 116 | 'node_name' => 'child', 117 | 'repository_path' => '/child/repository/path', 118 | 'children' => [], 119 | 'descriptors' => [], 120 | ], 121 | ], 122 | 'descriptors' => [], 123 | ]; 124 | 125 | $this->context->getNavigator()->willReturn($this->navigator); 126 | $this->navigator->accept($expected)->willReturn($this->context); 127 | 128 | $this->handler->serializeResource( 129 | $this->visitor->reveal(), 130 | $this->resource->reveal(), 131 | [], 132 | $this->context->reveal() 133 | ); 134 | } 135 | } 136 | --------------------------------------------------------------------------------