mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 01:08:26 +00:00 
			
		
		
		
	Dav: implements JWT extraction from the URL, and add the access_token in dav urls
This commit is contained in:
		| @@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Dav\Request\PropfindRequestAnalyzer; | ||||
| use Chill\DocStoreBundle\Dav\Response\DavResponse; | ||||
| use Chill\DocStoreBundle\Entity\StoredObject; | ||||
| use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; | ||||
| use DateTimeInterface; | ||||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| @@ -30,41 +31,44 @@ final readonly class WebdavController | ||||
|     public function __construct( | ||||
|         private \Twig\Environment $engine, | ||||
|         private StoredObjectManagerInterface $storedObjectManager, | ||||
|         private Security $security, | ||||
|         private ?JWTTokenManagerInterface $JWTTokenManager = null, | ||||
|     ) { | ||||
|         $this->requestAnalyzer = new PropfindRequestAnalyzer(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/open/{uuid}") | ||||
|      * @Route("/chdoc/open/{uuid}") | ||||
|      */ | ||||
|     public function open(StoredObject $storedObject): Response | ||||
|     { | ||||
|         /*$accessToken = $this->JWTTokenManager->createFromPayload($this->security->getUser(), [ | ||||
|         $accessToken = $this->JWTTokenManager?->createFromPayload($this->security->getUser(), [ | ||||
|             'UserCanWrite' => true, | ||||
|             'UserCanAttend' => true, | ||||
|             'UserCanPresent' => true, | ||||
|             'fileId' => $storedObject->getUuid(), | ||||
|         ]);*/ | ||||
|         ]); | ||||
|  | ||||
|         return new DavResponse($this->engine->render('@ChillDocStore/Webdav/open_in_browser.html.twig', [ | ||||
|             'stored_object' => $storedObject, 'access_token' => '', | ||||
|             'stored_object' => $storedObject, 'access_token' => $accessToken, | ||||
|         ])); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get") | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get") | ||||
|      */ | ||||
|     public function getDirectory(StoredObject $storedObject): Response | ||||
|     public function getDirectory(StoredObject $storedObject, string $access_token): Response | ||||
|     { | ||||
|         return new DavResponse( | ||||
|             $this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [ | ||||
|                 'stored_object' => $storedObject | ||||
|                 'stored_object' => $storedObject, | ||||
|                 'access_token' => $access_token, | ||||
|             ]) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/", methods={"OPTIONS"}) | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/", methods={"OPTIONS"}) | ||||
|      */ | ||||
|     public function optionsDirectory(StoredObject $storedObject): Response | ||||
|     { | ||||
| @@ -78,9 +82,9 @@ final readonly class WebdavController | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/", methods={"PROPFIND"}) | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/", methods={"PROPFIND"}) | ||||
|      */ | ||||
|     public function propfindDirectory(StoredObject $storedObject, Request $request): Response | ||||
|     public function propfindDirectory(StoredObject $storedObject, string $access_token, Request $request): Response | ||||
|     { | ||||
|         $depth = $request->headers->get('depth'); | ||||
|  | ||||
| @@ -111,10 +115,11 @@ final readonly class WebdavController | ||||
|             $this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [ | ||||
|                 'stored_object' => $storedObject, | ||||
|                 'properties' => $properties, | ||||
|                 'last_modified' => $lastModified ?? null, | ||||
|                 'etag' => $etag ?? null, | ||||
|                 'content_length' => $length ?? null, | ||||
|                 'depth' => (int) $depth | ||||
|                 'last_modified' => $lastModified , | ||||
|                 'etag' => $etag, | ||||
|                 'content_length' => $length, | ||||
|                 'depth' => (int) $depth, | ||||
|                 'access_token' => $access_token, | ||||
|             ]), | ||||
|             207 | ||||
|         ); | ||||
| @@ -127,7 +132,7 @@ final readonly class WebdavController | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"}) | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"}) | ||||
|      */ | ||||
|     public function getDocument(StoredObject $storedObject): Response | ||||
|     { | ||||
| @@ -136,7 +141,7 @@ final readonly class WebdavController | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/d", methods={"HEAD"}) | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/d", methods={"HEAD"}) | ||||
|      */ | ||||
|     public function headDocument(StoredObject $storedObject): Response | ||||
|     { | ||||
| @@ -154,7 +159,7 @@ final readonly class WebdavController | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/d", methods={"OPTIONS"}) | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/d", methods={"OPTIONS"}) | ||||
|      */ | ||||
|     public function optionsDocument(StoredObject $storedObject): Response | ||||
|     { | ||||
| @@ -176,9 +181,9 @@ final readonly class WebdavController | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/d", methods={"PROPFIND"}) | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/d", methods={"PROPFIND"}) | ||||
|      */ | ||||
|     public function propfindDocument(StoredObject $storedObject, Request $request): Response | ||||
|     public function propfindDocument(StoredObject $storedObject, string $access_token, Request $request): Response | ||||
|     { | ||||
|         $content = $request->getContent(); | ||||
|         $xml = new \DOMDocument(); | ||||
| @@ -204,9 +209,10 @@ final readonly class WebdavController | ||||
|                 [ | ||||
|                     'stored_object' => $storedObject, | ||||
|                     'properties' => $properties, | ||||
|                     'etag' => $etag ?? null, | ||||
|                     'last_modified' => $lastModified ?? null, | ||||
|                     'content_length' => $length ?? null, | ||||
|                     'etag' => $etag, | ||||
|                     'last_modified' => $lastModified, | ||||
|                     'content_length' => $length, | ||||
|                     'access_token' => $access_token, | ||||
|                 ] | ||||
|             ), | ||||
|             207 | ||||
| @@ -221,7 +227,7 @@ final readonly class WebdavController | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @Route("/dav/get/{uuid}/d", methods={"PUT"}) | ||||
|      * @Route("/dav/{access_token}/get/{uuid}/d", methods={"PUT"}) | ||||
|      */ | ||||
|     public function putDocument(StoredObject $storedObject, Request $request): Response | ||||
|     { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| </head> | ||||
| <body> | ||||
| <ul> | ||||
|     <li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">d</a></li> | ||||
|     <li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">d</a></li> | ||||
| </ul> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" ?> | ||||
| <d:multistatus xmlns:d="DAV:"> | ||||
|     <d:response> | ||||
|         <d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid } ) }}</d:href> | ||||
|         <d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid, 'access_token': access_token } ) }}</d:href> | ||||
|         {% if properties.resourceType or properties.contentType %} | ||||
|             <d:propstat> | ||||
|                 <d:prop> | ||||
| @@ -28,7 +28,7 @@ | ||||
|     </d:response> | ||||
|     {% if depth == 1 %} | ||||
|         <d:response> | ||||
|             <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href> | ||||
|             <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token':access_token}) }}</d:href> | ||||
|             {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %} | ||||
|                 <d:propstat> | ||||
|                     <d:prop> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" ?> | ||||
| <d:multistatus xmlns:d="DAV:"> | ||||
|     <d:response> | ||||
|         <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href> | ||||
|         <d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token}) }}</d:href> | ||||
|         {% if properties.lastModified or properties.contentLength or properties.resourceType or properties.etag or properties.contentType or properties.creationDate %} | ||||
|             <d:propstat> | ||||
|                 <d:prop> | ||||
|   | ||||
| @@ -2,6 +2,6 @@ | ||||
|  | ||||
| {% block content %} | ||||
|     <p>document uuid: {{ stored_object.uuid }}</p> | ||||
|     <p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid  })) }}</p> | ||||
| <a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">Open document</a> | ||||
|     <p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token  })) }}</p> | ||||
| <a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid, 'access_token': access_token })) }}">Open document</a> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -0,0 +1,49 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Security\Guard; | ||||
|  | ||||
| use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface; | ||||
| use Psr\Log\LoggerInterface; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| final readonly class DavOnUrlTokenExtractor implements TokenExtractorInterface | ||||
| { | ||||
|     public function __construct( | ||||
|         private LoggerInterface $logger, | ||||
|     ) {} | ||||
|  | ||||
|     public function extract(Request $request): string|false | ||||
|     { | ||||
|         $uri = $request->getRequestUri(); | ||||
|  | ||||
|         $segments = array_values( | ||||
|             array_filter( | ||||
|                 explode('/', $uri), | ||||
|                 fn ($item) => '' !== trim($item) | ||||
|             ) | ||||
|         ); | ||||
|  | ||||
|         if (2 > count($segments)) { | ||||
|             $this->logger->info("not enough segment for parsing URL"); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if ('dav' !== $segments[0]) { | ||||
|             $this->logger->info("the first segment of the url must be DAV"); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return $segments[1]; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,38 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Security\Guard; | ||||
|  | ||||
| use Lexik\Bundle\JWTAuthenticationBundle\Security\Guard\JWTTokenAuthenticator; | ||||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | ||||
| use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; | ||||
| use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; | ||||
| use Symfony\Contracts\Translation\TranslatorInterface; | ||||
|  | ||||
| class JWTOnDavUrlAuthenticator extends JWTTokenAuthenticator | ||||
| { | ||||
|     public function __construct( | ||||
|         JWTTokenManagerInterface $jwtManager, | ||||
|         EventDispatcherInterface $dispatcher, | ||||
|         TokenExtractorInterface $tokenExtractor, | ||||
|         TokenStorageInterface $preAuthenticationTokenStorage, | ||||
|         TranslatorInterface $translator = null, | ||||
|         private readonly DavOnUrlTokenExtractor $davOnUrlTokenExtractor, | ||||
|     ) { | ||||
|         parent::__construct($jwtManager, $dispatcher, $tokenExtractor, $preAuthenticationTokenStorage, $translator); | ||||
|     } | ||||
|  | ||||
|     protected function getTokenExtractor() | ||||
|     { | ||||
|         return $this->davOnUrlTokenExtractor; | ||||
|     } | ||||
| } | ||||
| @@ -21,6 +21,7 @@ use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Ramsey\Uuid\Uuid; | ||||
| use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Symfony\Component\Security\Core\Security; | ||||
| use Symfony\Component\Templating\EngineInterface; | ||||
|  | ||||
| /** | ||||
| @@ -43,8 +44,9 @@ class WebdavControllerTest extends KernelTestCase | ||||
|     private function buildController(): WebdavController | ||||
|     { | ||||
|         $storedObjectManager = new MockedStoredObjectManager(); | ||||
|         $security = $this->prophesize(Security::class); | ||||
|  | ||||
|         return new WebdavController($this->engine, $storedObjectManager); | ||||
|         return new WebdavController($this->engine, $storedObjectManager, $security->reveal()); | ||||
|     } | ||||
|  | ||||
|     private function buildDocument(): StoredObject | ||||
| @@ -115,7 +117,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|  | ||||
|         $request = new Request([], [], [], [], [], [], $requestContent); | ||||
|         $request->setMethod('PROPFIND'); | ||||
|         $response = $controller->propfindDocument($this->buildDocument(), $request); | ||||
|         $response = $controller->propfindDocument($this->buildDocument(), '1234', $request); | ||||
|  | ||||
|         self::assertEquals($expectedStatusCode, $response->getStatusCode()); | ||||
|         self::assertContains('content-type', $response->headers->keys()); | ||||
| @@ -134,7 +136,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|         $request = new Request([], [], [], [], [], [], $requestContent); | ||||
|         $request->setMethod('PROPFIND'); | ||||
|         $request->headers->add(["Depth" => "0"]); | ||||
|         $response = $controller->propfindDirectory($this->buildDocument(), $request); | ||||
|         $response = $controller->propfindDirectory($this->buildDocument(), '1234', $request); | ||||
|  | ||||
|         self::assertEquals($expectedStatusCode, $response->getStatusCode()); | ||||
|         self::assertContains('content-type', $response->headers->keys()); | ||||
| @@ -170,7 +172,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|             <d:multistatus xmlns:d="DAV:" > | ||||
|             <d:response> | ||||
|               <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|               <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|               <d:propstat> | ||||
|                 <d:prop> | ||||
|                   <d:resourcetype/> | ||||
| @@ -205,7 +207,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/"> | ||||
|                       <ns0:IsReadOnly/> | ||||
| @@ -232,7 +234,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/"> | ||||
|                       <ns0:BaseURI/> | ||||
| @@ -259,7 +261,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop> | ||||
|                     <!-- the date scraped from a webserver is >Sun, 10 Sep 2023 14:10:23 GMT --> | ||||
| @@ -285,7 +287,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|               <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                   <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/d</d:href> | ||||
|                   <d:propstat> | ||||
|                     <d:prop> | ||||
|                       <d:resourcetype/> | ||||
| @@ -323,7 +325,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|             <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                     <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href> | ||||
|                     <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href> | ||||
|                     <d:propstat> | ||||
|                         <d:prop> | ||||
|                             <d:resourcetype><d:collection/></d:resourcetype> | ||||
| @@ -365,7 +367,7 @@ class WebdavControllerTest extends KernelTestCase | ||||
|             <?xml version="1.0" encoding="utf-8"?> | ||||
|             <d:multistatus xmlns:d="DAV:"> | ||||
|                 <d:response> | ||||
|                     <d:href>/dav/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href> | ||||
|                     <d:href>/dav/1234/get/716e6688-4579-4938-acf3-c4ab5856803b/</d:href> | ||||
|                     <d:propstat> | ||||
|                         <d:prop xmlns:ns0="http://ucb.openoffice.org/dav/props/" > | ||||
|                             <ns0:CreatableContentsInfo/> | ||||
|   | ||||
| @@ -0,0 +1,54 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\DocStoreBundle\Tests\Security\Guard; | ||||
|  | ||||
| use Chill\DocStoreBundle\Security\Guard\DavOnUrlTokenExtractor; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Psr\Log\NullLogger; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class DavOnUrlTokenExtractorTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     /** | ||||
|      * @dataProvider provideDataUri | ||||
|      */ | ||||
|     public function testExtract(string $uri, string|false $expected): void | ||||
|     { | ||||
|         $request = $this->prophesize(Request::class); | ||||
|         $request->getRequestUri()->willReturn($uri); | ||||
|  | ||||
|         $extractor = new DavOnUrlTokenExtractor(new NullLogger()); | ||||
|  | ||||
|         $actual = $extractor->extract($request->reveal()); | ||||
|  | ||||
|         self::assertEquals($expected, $actual); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @phpstan-pure | ||||
|      */ | ||||
|     public static function provideDataUri(): iterable | ||||
|     { | ||||
|         yield ['/dav/123456789/get/d07d2230-5326-11ee-8fd4-93696acf5ea1/d', '123456789']; | ||||
|         yield ['/dav/123456789', '123456789']; | ||||
|         yield ['/not-dav/123456978', false]; | ||||
|         yield ['/dav', false]; | ||||
|         yield ['/', false]; | ||||
|     } | ||||
| } | ||||
| @@ -34,6 +34,11 @@ services: | ||||
|         autoconfigure: true | ||||
|         autowire: true | ||||
|  | ||||
|     Chill\DocStoreBundle\Security\: | ||||
|         resource: './../Security' | ||||
|         autoconfigure: true | ||||
|         autowire: true | ||||
|  | ||||
|     Chill\DocStoreBundle\Serializer\Normalizer\: | ||||
|         autowire: true | ||||
|         resource: '../Serializer/Normalizer/' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user