فهرست منبع

Add option `target_admin_access_action` to ModelAutocompleteType (#4448)

Andrej Hudec 8 سال پیش
والد
کامیت
a9f2c1335b

+ 11 - 8
Controller/HelperController.php

@@ -352,23 +352,26 @@ class HelperController
             $itemsPerPage = $filterAutocomplete->getFieldOption('items_per_page', 10);
             $reqParamPageNumber = $filterAutocomplete->getFieldOption('req_param_name_page_number', '_page');
             $toStringCallback = $filterAutocomplete->getFieldOption('to_string_callback');
+            $targetAdminAccessAction = $filterAutocomplete->getFieldOption('target_admin_access_action');
         } else {
             // create/edit form
             $fieldDescription = $this->retrieveFormFieldDescription($admin, $request->get('field'));
             $formAutocomplete = $admin->getForm()->get($fieldDescription->getName());
 
-            if ($formAutocomplete->getConfig()->getAttribute('disabled')) {
+            $formAutocompleteConfig = $formAutocomplete->getConfig();
+            if ($formAutocompleteConfig->getAttribute('disabled')) {
                 throw new AccessDeniedException(
                     'Autocomplete list can`t be retrieved because the form element is disabled or read_only.'
                 );
             }
 
-            $property = $formAutocomplete->getConfig()->getAttribute('property');
-            $callback = $formAutocomplete->getConfig()->getAttribute('callback');
-            $minimumInputLength = $formAutocomplete->getConfig()->getAttribute('minimum_input_length');
-            $itemsPerPage = $formAutocomplete->getConfig()->getAttribute('items_per_page');
-            $reqParamPageNumber = $formAutocomplete->getConfig()->getAttribute('req_param_name_page_number');
-            $toStringCallback = $formAutocomplete->getConfig()->getAttribute('to_string_callback');
+            $property = $formAutocompleteConfig->getAttribute('property');
+            $callback = $formAutocompleteConfig->getAttribute('callback');
+            $minimumInputLength = $formAutocompleteConfig->getAttribute('minimum_input_length');
+            $itemsPerPage = $formAutocompleteConfig->getAttribute('items_per_page');
+            $reqParamPageNumber = $formAutocompleteConfig->getAttribute('req_param_name_page_number');
+            $toStringCallback = $formAutocompleteConfig->getAttribute('to_string_callback');
+            $targetAdminAccessAction = $formAutocompleteConfig->getAttribute('target_admin_access_action');
         }
 
         $searchText = $request->get('q');
@@ -376,7 +379,7 @@ class HelperController
         $targetAdmin = $fieldDescription->getAssociationAdmin();
 
         // check user permission
-        $targetAdmin->checkAccess('list');
+        $targetAdmin->checkAccess($targetAdminAccessAction);
 
         if (mb_strlen($searchText, 'UTF-8') < $minimumInputLength) {
             return new JsonResponse(array('status' => 'KO', 'message' => 'Too short search string.'), 403);

+ 4 - 0
Form/Type/ModelAutocompleteType.php

@@ -48,6 +48,7 @@ class ModelAutocompleteType extends AbstractType
             || (array_key_exists('read_only', $options) && $options['read_only'])
         );
         $builder->setAttribute('to_string_callback', $options['to_string_callback']);
+        $builder->setAttribute('target_admin_access_action', $options['target_admin_access_action']);
 
         if ($options['multiple']) {
             $resizeListener = new ResizeFormListener(
@@ -140,6 +141,9 @@ class ModelAutocompleteType extends AbstractType
             'req_param_name_page_number' => '_page',
             'req_param_name_items_per_page' => '_per_page',
 
+            // security
+            'target_admin_access_action' => 'list',
+
             // CSS classes
             'container_css_class' => '',
             'dropdown_css_class' => '',

+ 52 - 0
Resources/doc/reference/form_types.rst

@@ -330,6 +330,58 @@ template
     {# change the default selection format #}
     {% block sonata_type_model_autocomplete_selection_format %}'<b>'+item.label+'</b>'{% endblock %}
 
+target_admin_access_action
+  defaults to ``list``.
+  By default, the user needs the ``LIST`` role (mapped to ``list`` access action)
+  to get the autocomplete items from the target admin's datagrid.
+  If you can't give some users this role because they will then have access to the target
+  admin's datagrid, you have to grant them another role.
+
+  In the example below we changed the ``target_admin_access_action`` from ``list`` to ``autocomplete``,
+  which is mapped in the target admin to ``AUTOCOMPLETE`` role. Please make sure that all valid users
+  have the ``AUTOCOMPLETE`` role.
+
+.. code-block:: php
+
+    <?php
+    // src/AppBundle/Admin/ArticleAdmin.php
+
+    class ArticleAdmin extends AbstractAdmin
+    {
+        protected function configureFormFields(FormMapper $formMapper)
+        {
+            // the dropdown autocomplete list will show only Category
+            // entities that contain specified text in "title" attribute
+            $formMapper
+                ->add('category', 'sonata_type_model_autocomplete', array(
+                    'property' => 'title',
+                    'target_admin_access_action' => 'autocomplete'
+                ))
+            ;
+        }
+    }
+
+.. code-block:: php
+
+    <?php
+    // src/AppBundle/Admin/CategoryAdmin.php
+
+    class CategoryAdmin extends AbstractAdmin
+    {
+        protected $accessMapping = array(
+            'autocomplete' => 'AUTOCOMPLETE',
+        );
+
+        protected function configureDatagridFilters(DatagridMapper $datagridMapper)
+        {
+            // this text filter will be used to retrieve autocomplete fields
+            // only the users with role AUTOCOMPLETE will be able to get the items
+            $datagridMapper
+                ->add('title')
+            ;
+        }
+    }
+
 sonata_choice_field_mask
 ^^^^^^^^^^^^^^^^^^^^^^^^
 

+ 259 - 2
Tests/Controller/HelperControllerTest.php

@@ -575,8 +575,6 @@ class HelperControllerTest extends PHPUnit_Framework_TestCase
             ->with('create')
             ->will($this->returnValue(true));
 
-        $entity = new Foo();
-
         $fieldDescription = $this->createMock('Sonata\AdminBundle\Admin\FieldDescriptionInterface');
 
         $fieldDescription->expects($this->once())
@@ -634,6 +632,265 @@ class HelperControllerTest extends PHPUnit_Framework_TestCase
         $this->controller->retrieveAutocompleteItemsAction($request);
     }
 
+    public function testRetrieveAutocompleteItemsTooShortSearchString()
+    {
+        $this->admin->expects($this->once())
+            ->method('hasAccess')
+            ->with('create')
+            ->will($this->returnValue(true));
+
+        $targetAdmin = $this->createMock('Sonata\AdminBundle\Admin\AbstractAdmin');
+        $targetAdmin->expects($this->once())
+            ->method('checkAccess')
+            ->with('list')
+            ->will($this->returnValue(null));
+
+        $fieldDescription = $this->createMock('Sonata\AdminBundle\Admin\FieldDescriptionInterface');
+
+        $fieldDescription->expects($this->once())
+            ->method('getTargetEntity')
+            ->will($this->returnValue('Sonata\AdminBundle\Tests\Fixtures\Bundle\Entity\Foo'));
+
+        $fieldDescription->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('barField'));
+
+        $fieldDescription->expects($this->once())
+            ->method('getAssociationAdmin')
+            ->will($this->returnValue($targetAdmin));
+
+        $this->admin->expects($this->once())
+            ->method('getFormFieldDescriptions')
+            ->will($this->returnValue(null));
+
+        $this->admin->expects($this->once())
+            ->method('getFormFieldDescription')
+            ->with('barField')
+            ->will($this->returnValue($fieldDescription));
+
+        $form = $this->getMockBuilder('Symfony\Component\Form\Form')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->admin->expects($this->once())
+            ->method('getForm')
+            ->will($this->returnValue($form));
+
+        $formType = $this->getMockBuilder('Symfony\Component\Form\Form')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $form->expects($this->once())
+            ->method('get')
+            ->with('barField')
+            ->will($this->returnValue($formType));
+
+        $formConfig = $this->getMockBuilder('Symfony\Component\Form\FormConfigInterface')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $formType->expects($this->once())
+            ->method('getConfig')
+            ->will($this->returnValue($formConfig));
+
+        $formConfig->expects($this->any())
+            ->method('getAttribute')
+            ->will($this->returnCallback(function ($name, $default = null) {
+                switch ($name) {
+                    case 'property':
+                        return 'foo';
+                    case 'callback':
+                        return;
+                    case 'minimum_input_length':
+                        return 3;
+                    case 'items_per_page':
+                        return 10;
+                    case 'req_param_name_page_number':
+                        return '_page';
+                    case 'to_string_callback':
+                        return;
+                    case 'disabled':
+                        return false;
+                    case 'target_admin_access_action':
+                        return 'list';
+                    default:
+                        throw new \RuntimeException(sprintf('Unkown parameter "%s" called.', $name));
+                }
+            }));
+
+        $request = new Request(array(
+            'admin_code' => 'foo.admin',
+            'field' => 'barField',
+            'q' => 'so',
+        ), array(), array(), array(), array(), array('REQUEST_METHOD' => 'GET', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'));
+
+        $response = $this->controller->retrieveAutocompleteItemsAction($request);
+        $this->isInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+        $this->assertSame('application/json', $response->headers->get('Content-Type'));
+        $this->assertSame('{"status":"KO","message":"Too short search string."}', $response->getContent());
+    }
+
+    public function testRetrieveAutocompleteItems()
+    {
+        $entity = new Foo();
+        $this->admin->expects($this->once())
+            ->method('hasAccess')
+            ->with('create')
+            ->will($this->returnValue(true));
+
+        $this->admin->expects($this->once())
+            ->method('id')
+            ->with($entity)
+            ->will($this->returnValue(123));
+
+        $targetAdmin = $this->createMock('Sonata\AdminBundle\Admin\AbstractAdmin');
+        $targetAdmin->expects($this->once())
+            ->method('checkAccess')
+            ->with('list')
+            ->will($this->returnValue(null));
+
+        $targetAdmin->expects($this->once())
+            ->method('setPersistFilters')
+            ->with(false)
+            ->will($this->returnValue(null));
+
+        $datagrid = $this->createMock('Sonata\AdminBundle\Datagrid\DatagridInterface');
+        $targetAdmin->expects($this->once())
+            ->method('getDatagrid')
+            ->with()
+            ->will($this->returnValue($datagrid));
+
+        $metadata = $this->createMock('Sonata\CoreBundle\Model\Metadata');
+        $metadata->expects($this->once())
+            ->method('getTitle')
+            ->with()
+            ->will($this->returnValue('FOO'));
+
+        $targetAdmin->expects($this->once())
+            ->method('getObjectMetadata')
+            ->with($entity)
+            ->will($this->returnValue($metadata));
+
+        $datagrid->expects($this->once())
+            ->method('hasFilter')
+            ->with('foo')
+            ->will($this->returnValue(true));
+
+        $datagrid->expects($this->exactly(3))
+            ->method('setValue')
+            ->withConsecutive(
+                array($this->equalTo('foo'), $this->equalTo(null), $this->equalTo('sonata')),
+                array($this->equalTo('_per_page'), $this->equalTo(null), $this->equalTo(10)),
+                array($this->equalTo('_page'), $this->equalTo(null), $this->equalTo(1))
+               )
+            ->will($this->returnValue(null));
+
+        $datagrid->expects($this->once())
+            ->method('buildPager')
+            ->with()
+            ->will($this->returnValue(null));
+
+        $pager = $this->createMock('Sonata\AdminBundle\Datagrid\Pager');
+        $datagrid->expects($this->once())
+            ->method('getPager')
+            ->with()
+            ->will($this->returnValue($pager));
+
+        $pager->expects($this->once())
+            ->method('getResults')
+            ->with()
+            ->will($this->returnValue(array($entity)));
+
+        $pager->expects($this->once())
+            ->method('isLastPage')
+            ->with()
+            ->will($this->returnValue(true));
+
+        $fieldDescription = $this->createMock('Sonata\AdminBundle\Admin\FieldDescriptionInterface');
+
+        $fieldDescription->expects($this->once())
+            ->method('getTargetEntity')
+            ->will($this->returnValue('Sonata\AdminBundle\Tests\Fixtures\Bundle\Entity\Foo'));
+
+        $fieldDescription->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('barField'));
+
+        $fieldDescription->expects($this->once())
+            ->method('getAssociationAdmin')
+            ->will($this->returnValue($targetAdmin));
+
+        $this->admin->expects($this->once())
+            ->method('getFormFieldDescriptions')
+            ->will($this->returnValue(null));
+
+        $this->admin->expects($this->once())
+            ->method('getFormFieldDescription')
+            ->with('barField')
+            ->will($this->returnValue($fieldDescription));
+
+        $form = $this->getMockBuilder('Symfony\Component\Form\Form')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->admin->expects($this->once())
+            ->method('getForm')
+            ->will($this->returnValue($form));
+
+        $formType = $this->getMockBuilder('Symfony\Component\Form\Form')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $form->expects($this->once())
+            ->method('get')
+            ->with('barField')
+            ->will($this->returnValue($formType));
+
+        $formConfig = $this->getMockBuilder('Symfony\Component\Form\FormConfigInterface')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $formType->expects($this->once())
+            ->method('getConfig')
+            ->will($this->returnValue($formConfig));
+
+        $formConfig->expects($this->any())
+            ->method('getAttribute')
+            ->will($this->returnCallback(function ($name, $default = null) {
+                switch ($name) {
+                    case 'property':
+                        return 'foo';
+                    case 'callback':
+                        return;
+                    case 'minimum_input_length':
+                        return 3;
+                    case 'items_per_page':
+                        return 10;
+                    case 'req_param_name_page_number':
+                        return '_page';
+                    case 'to_string_callback':
+                        return;
+                    case 'disabled':
+                        return false;
+                    case 'target_admin_access_action':
+                        return 'list';
+                    default:
+                        throw new \RuntimeException(sprintf('Unkown parameter "%s" called.', $name));
+                }
+            }));
+
+        $request = new Request(array(
+            'admin_code' => 'foo.admin',
+            'field' => 'barField',
+            'q' => 'sonata',
+        ), array(), array(), array(), array(), array('REQUEST_METHOD' => 'GET', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'));
+
+        $response = $this->controller->retrieveAutocompleteItemsAction($request);
+        $this->isInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+        $this->assertSame('application/json', $response->headers->get('Content-Type'));
+        $this->assertSame('{"status":"OK","more":false,"items":[{"id":123,"label":"FOO"}]}', $response->getContent());
+    }
+
     /**
      * Symfony Validator has 2 API version (2.4 and 2.5)
      * This data provider ensure tests pass on each one.

+ 22 - 3
Tests/Form/Type/ModelAutocompleteTypeTest.php

@@ -17,16 +17,27 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
 
 class ModelAutocompleteTypeTest extends TypeTestCase
 {
+    /**
+     * @var ModelAutocompleteType
+     */
+    private $type;
+
+    protected function setUp()
+    {
+        $this->type = new ModelAutocompleteType();
+
+        parent::setUp();
+    }
+
     public function testGetDefaultOptions()
     {
-        $type = new ModelAutocompleteType();
         $modelManager = $this->getMockForAbstractClass('Sonata\AdminBundle\Model\ModelManagerInterface');
         $optionResolver = new OptionsResolver();
 
         if (!method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')) {
-            $type->setDefaultOptions($optionResolver);
+            $this->type->setDefaultOptions($optionResolver);
         } else {
-            $type->configureOptions($optionResolver);
+            $this->type->configureOptions($optionResolver);
         }
 
         $options = $optionResolver->resolve(array('model_manager' => $modelManager, 'class' => 'Foo', 'property' => 'bar'));
@@ -53,6 +64,9 @@ class ModelAutocompleteTypeTest extends TypeTestCase
         $this->assertSame('q', $options['req_param_name_search']);
         $this->assertSame('_page', $options['req_param_name_page_number']);
         $this->assertSame('_per_page', $options['req_param_name_items_per_page']);
+
+        $this->assertSame('list', $options['target_admin_access_action']);
+
         $this->assertSame('', $options['container_css_class']);
         $this->assertSame('', $options['dropdown_css_class']);
         $this->assertSame('', $options['dropdown_item_css_class']);
@@ -61,4 +75,9 @@ class ModelAutocompleteTypeTest extends TypeTestCase
 
         $this->assertSame('', $options['context']);
     }
+
+    public function testGetBlockPrefix()
+    {
+        $this->assertSame('sonata_type_model_autocomplete', $this->type->getBlockPrefix());
+    }
 }