├── 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 |
--------------------------------------------------------------------------------