Issue #2487600 by dawehner, Wim Leers, fgm: #access should support AccessResultInterface objects or better has to always use it
parent
40ae780a3f
commit
7f420bd908
|
@ -1036,7 +1036,7 @@ function drupal_pre_render_links($element) {
|
|||
$child = &$element[$key];
|
||||
// If the child has links which have not been printed yet and the user has
|
||||
// access to it, merge its links in to the parent.
|
||||
if (isset($child['#links']) && empty($child['#printed']) && (!isset($child['#access']) || $child['#access'])) {
|
||||
if (isset($child['#links']) && empty($child['#printed']) && Element::isVisibleElement($child)) {
|
||||
$element['#links'] += $child['#links'];
|
||||
// Mark the child as having been printed already (so that its links
|
||||
// cannot be mistakenly rendered twice).
|
||||
|
|
|
@ -12,6 +12,7 @@ use Drupal\Component\Utility\NestedArray;
|
|||
use Drupal\Component\Utility\SortArray;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
|
||||
|
@ -415,8 +416,8 @@ abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface
|
|||
}
|
||||
}
|
||||
|
||||
// Only set errors if the element is accessible.
|
||||
if (!isset($element['#access']) || $element['#access']) {
|
||||
// Only set errors if the element is visible.
|
||||
if (Element::isVisibleElement($element)) {
|
||||
$handles_multiple = $this->handlesMultipleValues();
|
||||
|
||||
$violations_by_delta = array();
|
||||
|
|
|
@ -12,6 +12,7 @@ use Drupal\Component\Utility\Html;
|
|||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
use Drupal\Core\Access\CsrfTokenGenerator;
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||
|
@ -829,6 +830,13 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
|
|||
|
||||
// Recurse through all child elements.
|
||||
$count = 0;
|
||||
if (isset($element['#access'])) {
|
||||
$access = $element['#access'];
|
||||
$inherited_access = NULL;
|
||||
if (($access instanceof AccessResultInterface && !$access->isAllowed()) || $access === FALSE) {
|
||||
$inherited_access = $access;
|
||||
}
|
||||
}
|
||||
foreach (Element::children($element) as $key) {
|
||||
// Prior to checking properties of child elements, their default
|
||||
// properties need to be loaded.
|
||||
|
@ -842,9 +850,9 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
|
|||
$element[$key]['#tree'] = $element['#tree'];
|
||||
}
|
||||
|
||||
// Deny access to child elements if parent is denied.
|
||||
if (isset($element['#access']) && !$element['#access']) {
|
||||
$element[$key]['#access'] = FALSE;
|
||||
// Children inherit #access from parent.
|
||||
if (isset($inherited_access)) {
|
||||
$element[$key]['#access'] = $inherited_access;
|
||||
}
|
||||
|
||||
// Make child elements inherit their parent's #disabled and #allow_focus
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
namespace Drupal\Core\Render;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
|
||||
/**
|
||||
* Provides helper methods for Drupal render elements.
|
||||
|
@ -136,11 +137,6 @@ class Element {
|
|||
foreach (static::children($elements) as $key) {
|
||||
$child = $elements[$key];
|
||||
|
||||
// Skip un-accessible children.
|
||||
if (isset($child['#access']) && !$child['#access']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip value and hidden elements, since they are not rendered.
|
||||
if (!static::isVisibleElement($child)) {
|
||||
continue;
|
||||
|
@ -162,7 +158,9 @@ class Element {
|
|||
* TRUE if the element is visible, otherwise FALSE.
|
||||
*/
|
||||
public static function isVisibleElement($element) {
|
||||
return (!isset($element['#type']) || !in_array($element['#type'], ['value', 'hidden', 'token'])) && (!isset($element['#access']) || $element['#access']);
|
||||
return (!isset($element['#type']) || !in_array($element['#type'], ['value', 'hidden', 'token']))
|
||||
&& (!isset($element['#access'])
|
||||
|| (($element['#access'] instanceof AccessResultInterface && $element['#access']->isAllowed()) || ($element['#access'] === TRUE)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace Drupal\Core\Render;
|
|||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Controller\ControllerResolverInterface;
|
||||
|
@ -168,6 +169,10 @@ class Renderer implements RendererInterface {
|
|||
* See the docs for ::render().
|
||||
*/
|
||||
protected function doRender(&$elements, $is_root_call = FALSE) {
|
||||
if (empty($elements)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
|
||||
if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
|
||||
$elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
|
||||
|
@ -176,8 +181,18 @@ class Renderer implements RendererInterface {
|
|||
}
|
||||
|
||||
// Early-return nothing if user does not have access.
|
||||
if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
|
||||
return '';
|
||||
if (isset($elements['#access'])) {
|
||||
// If #access is an AccessResultInterface object, we must apply it's
|
||||
// cacheability metadata to the render array.
|
||||
if ($elements['#access'] instanceof AccessResultInterface) {
|
||||
$this->addCacheableDependency($elements, $elements['#access']);
|
||||
if (!$elements['#access']->isAllowed()) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
elseif ($elements['#access'] === FALSE) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Do not print elements twice.
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
namespace Drupal\Tests\Core\Form;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Access\AccessResultForbidden;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Form\EnforcedResponseException;
|
||||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
|
@ -449,6 +452,171 @@ class FormBuilderTest extends FormTestBase {
|
|||
$this->formBuilder->buildForm($form_arg, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::buildForm
|
||||
*
|
||||
* @dataProvider providerTestChildAccessInheritance
|
||||
*/
|
||||
public function testChildAccessInheritance($element, $access_checks) {
|
||||
$form_arg = new TestFormWithPredefinedForm();
|
||||
$form_arg->setForm($element);
|
||||
|
||||
$form_state = new FormState();
|
||||
|
||||
$form = $this->formBuilder->buildForm($form_arg, $form_state);
|
||||
|
||||
$actual_access_structure = [];
|
||||
$expected_access_structure = [];
|
||||
|
||||
// Ensure that the expected access checks are set.
|
||||
foreach ($access_checks as $access_check) {
|
||||
$parents = $access_check[0];
|
||||
$parents[] = '#access';
|
||||
|
||||
$actual_access = NestedArray::getValue($form, $parents);
|
||||
$actual_access_structure[] = [$parents, $actual_access];
|
||||
$expected_access_structure[] = [$parents, $access_check[1]];
|
||||
}
|
||||
|
||||
$this->assertEquals($expected_access_structure, $actual_access_structure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testChildAccessInheritance.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function providerTestChildAccessInheritance() {
|
||||
$data = [];
|
||||
|
||||
$element = [
|
||||
'child0' => [
|
||||
'#type' => 'checkbox',
|
||||
],
|
||||
'child1' => [
|
||||
'#type' => 'checkbox',
|
||||
],
|
||||
'child2' => [
|
||||
'#type' => 'fieldset',
|
||||
'child2.0' => [
|
||||
'#type' => 'checkbox',
|
||||
],
|
||||
'child2.1' => [
|
||||
'#type' => 'checkbox',
|
||||
],
|
||||
'child2.2' => [
|
||||
'#type' => 'checkbox',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Sets access FALSE on the root level, this should be inherited completely.
|
||||
$clone = $element;
|
||||
$clone['#access'] = FALSE;
|
||||
|
||||
$expected_access = [];
|
||||
$expected_access[] = [[], FALSE];
|
||||
$expected_access[] = [['child0'], FALSE];
|
||||
$expected_access[] = [['child1'], FALSE];
|
||||
$expected_access[] = [['child2'], FALSE];
|
||||
$expected_access[] = [['child2', 'child2.0'], FALSE];
|
||||
$expected_access[] = [['child2', 'child2.1'], FALSE];
|
||||
$expected_access[] = [['child2', 'child2.2'], FALSE];
|
||||
|
||||
$data['access-false-root'] = [$clone, $expected_access];
|
||||
|
||||
$clone = $element;
|
||||
$access_result = AccessResult::forbidden()->addCacheContexts(['user']);
|
||||
$clone['#access'] = $access_result;
|
||||
|
||||
$expected_access = [];
|
||||
$expected_access[] = [[], $access_result];
|
||||
$expected_access[] = [['child0'], $access_result];
|
||||
$expected_access[] = [['child1'], $access_result];
|
||||
$expected_access[] = [['child2'], $access_result];
|
||||
$expected_access[] = [['child2', 'child2.0'], $access_result];
|
||||
$expected_access[] = [['child2', 'child2.1'], $access_result];
|
||||
$expected_access[] = [['child2', 'child2.2'], $access_result];
|
||||
|
||||
$data['access-forbidden-root'] = [$clone, $expected_access];
|
||||
|
||||
// Allow access on the most outer level but set FALSE otherwise.
|
||||
$clone = $element;
|
||||
$clone['#access'] = TRUE;
|
||||
$clone['child0']['#access'] = FALSE;
|
||||
|
||||
$expected_access = [];
|
||||
$expected_access[] = [[], TRUE];
|
||||
$expected_access[] = [['child0'], FALSE];
|
||||
$expected_access[] = [['child1'], NULL];
|
||||
$expected_access[] = [['child2'], NULL];
|
||||
$expected_access[] = [['child2', 'child2.0'], NULL];
|
||||
$expected_access[] = [['child2', 'child2.1'], NULL];
|
||||
$expected_access[] = [['child2', 'child2.2'], NULL];
|
||||
|
||||
$data['access-true-root'] = [$clone, $expected_access];
|
||||
|
||||
// Allow access on the most outer level but forbid otherwise.
|
||||
$clone = $element;
|
||||
$access_result_allowed = AccessResult::allowed()
|
||||
->addCacheContexts(['user']);
|
||||
$clone['#access'] = $access_result_allowed;
|
||||
$access_result_forbidden = AccessResult::forbidden()
|
||||
->addCacheContexts(['user']);
|
||||
$clone['child0']['#access'] = $access_result_forbidden;
|
||||
|
||||
$expected_access = [];
|
||||
$expected_access[] = [[], $access_result_allowed];
|
||||
$expected_access[] = [['child0'], $access_result_forbidden];
|
||||
$expected_access[] = [['child1'], NULL];
|
||||
$expected_access[] = [['child2'], NULL];
|
||||
$expected_access[] = [['child2', 'child2.0'], NULL];
|
||||
$expected_access[] = [['child2', 'child2.1'], NULL];
|
||||
$expected_access[] = [['child2', 'child2.2'], NULL];
|
||||
|
||||
$data['access-allowed-root'] = [$clone, $expected_access];
|
||||
|
||||
// Allow access on the most outer level, deny access on a parent, and allow
|
||||
// on a child. The denying should be inherited.
|
||||
$clone = $element;
|
||||
$clone['#access'] = TRUE;
|
||||
$clone['child2']['#access'] = FALSE;
|
||||
$clone['child2.0']['#access'] = TRUE;
|
||||
$clone['child2.1']['#access'] = TRUE;
|
||||
$clone['child2.2']['#access'] = TRUE;
|
||||
|
||||
$expected_access = [];
|
||||
$expected_access[] = [[], TRUE];
|
||||
$expected_access[] = [['child0'], NULL];
|
||||
$expected_access[] = [['child1'], NULL];
|
||||
$expected_access[] = [['child2'], FALSE];
|
||||
$expected_access[] = [['child2', 'child2.0'], FALSE];
|
||||
$expected_access[] = [['child2', 'child2.1'], FALSE];
|
||||
$expected_access[] = [['child2', 'child2.2'], FALSE];
|
||||
|
||||
$data['access-mixed-parents'] = [$clone, $expected_access];
|
||||
|
||||
$clone = $element;
|
||||
$clone['#access'] = $access_result_allowed;
|
||||
$clone['child2']['#access'] = $access_result_forbidden;
|
||||
$clone['child2.0']['#access'] = $access_result_allowed;
|
||||
$clone['child2.1']['#access'] = $access_result_allowed;
|
||||
$clone['child2.2']['#access'] = $access_result_allowed;
|
||||
|
||||
$expected_access = [];
|
||||
$expected_access[] = [[], $access_result_allowed];
|
||||
$expected_access[] = [['child0'], NULL];
|
||||
$expected_access[] = [['child1'], NULL];
|
||||
$expected_access[] = [['child2'], $access_result_forbidden];
|
||||
$expected_access[] = [['child2', 'child2.0'], $access_result_forbidden];
|
||||
$expected_access[] = [['child2', 'child2.1'], $access_result_forbidden];
|
||||
$expected_access[] = [['child2', 'child2.2'], $access_result_forbidden];
|
||||
|
||||
$data['access-mixed-parents-object'] = [$clone, $expected_access];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TestForm implements FormInterface {
|
||||
|
@ -467,3 +635,21 @@ class TestFormInjected extends TestForm implements ContainerInjectionInterface {
|
|||
return new static();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestFormWithPredefinedForm extends TestForm {
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $form;
|
||||
|
||||
public function setForm($form) {
|
||||
$this->form = $form;
|
||||
}
|
||||
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
return $this->form;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
namespace Drupal\Tests\Core\Render;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\Core\Render\Element;
|
||||
|
||||
|
@ -151,6 +152,8 @@ class ElementTest extends UnitTestCase {
|
|||
array(array('#property1' => '', 'child1' => array()), array('child1')),
|
||||
array(array('#property1' => '', 'child1' => array(), 'child2' => array('#access' => TRUE)), array('child1', 'child2')),
|
||||
array(array('#property1' => '', 'child1' => array(), 'child2' => array('#access' => FALSE)), array('child1')),
|
||||
'access_result_object_allowed' => array(array('#property1' => '', 'child1' => array(), 'child2' => array('#access' => AccessResult::allowed())), array('child1', 'child2')),
|
||||
'access_result_object_forbidden' => array(array('#property1' => '', 'child1' => array(), 'child2' => array('#access' => AccessResult::forbidden())), array('child1')),
|
||||
array(array('#property1' => '', 'child1' => array(), 'child2' => array('#type' => 'textfield')), array('child1', 'child2')),
|
||||
array(array('#property1' => '', 'child1' => array(), 'child2' => array('#type' => 'value')), array('child1')),
|
||||
array(array('#property1' => '', 'child1' => array(), 'child2' => array('#type' => 'hidden')), array('child1')),
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
namespace Drupal\Tests\Core\Render;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Access\AccessResultInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
|
@ -367,7 +369,7 @@ class RendererTest extends RendererTestBase {
|
|||
* @covers ::render
|
||||
* @covers ::doRender
|
||||
*
|
||||
* @dataProvider providerBoolean
|
||||
* @dataProvider providerAccessValues
|
||||
*/
|
||||
public function testRenderWithPresetAccess($access) {
|
||||
$build = [
|
||||
|
@ -381,7 +383,7 @@ class RendererTest extends RendererTestBase {
|
|||
* @covers ::render
|
||||
* @covers ::doRender
|
||||
*
|
||||
* @dataProvider providerBoolean
|
||||
* @dataProvider providerAccessValues
|
||||
*/
|
||||
public function testRenderWithAccessCallbackCallable($access) {
|
||||
$build = [
|
||||
|
@ -399,7 +401,7 @@ class RendererTest extends RendererTestBase {
|
|||
* @covers ::render
|
||||
* @covers ::doRender
|
||||
*
|
||||
* @dataProvider providerBoolean
|
||||
* @dataProvider providerAccessValues
|
||||
*/
|
||||
public function testRenderWithAccessPropertyAndCallback($access) {
|
||||
$build = [
|
||||
|
@ -416,16 +418,49 @@ class RendererTest extends RendererTestBase {
|
|||
* @covers ::render
|
||||
* @covers ::doRender
|
||||
*
|
||||
* @dataProvider providerBoolean
|
||||
* @dataProvider providerAccessValues
|
||||
*/
|
||||
public function testRenderWithAccessControllerResolved($access) {
|
||||
|
||||
switch ($access) {
|
||||
case AccessResult::allowed():
|
||||
$method = 'accessResultAllowed';
|
||||
break;
|
||||
|
||||
case AccessResult::forbidden():
|
||||
$method = 'accessResultForbidden';
|
||||
break;
|
||||
|
||||
case FALSE:
|
||||
$method = 'accessFalse';
|
||||
break;
|
||||
|
||||
case TRUE:
|
||||
$method = 'accessTrue';
|
||||
break;
|
||||
}
|
||||
|
||||
$build = [
|
||||
'#access_callback' => 'Drupal\Tests\Core\Render\TestAccessClass::' . ($access ? 'accessTrue' : 'accessFalse'),
|
||||
'#access_callback' => 'Drupal\Tests\Core\Render\TestAccessClass::' . $method,
|
||||
];
|
||||
|
||||
$this->assertAccess($build, $access);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::render
|
||||
* @covers ::doRender
|
||||
*/
|
||||
public function testRenderAccessCacheablityDependencyInheritance() {
|
||||
$build = [
|
||||
'#access' => AccessResult::allowed()->addCacheContexts(['user']),
|
||||
];
|
||||
|
||||
$this->renderer->renderPlain($build);
|
||||
|
||||
$this->assertEquals(['languages:language_interface', 'theme', 'user'], $build['#cache']['contexts']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a first render returns the rendered output and a second doesn't.
|
||||
*
|
||||
|
@ -451,10 +486,12 @@ class RendererTest extends RendererTestBase {
|
|||
*
|
||||
* @return array
|
||||
*/
|
||||
public function providerBoolean() {
|
||||
public function providerAccessValues() {
|
||||
return [
|
||||
[FALSE],
|
||||
[TRUE]
|
||||
[TRUE],
|
||||
[AccessResult::forbidden()],
|
||||
[AccessResult::allowed()],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -469,7 +506,7 @@ class RendererTest extends RendererTestBase {
|
|||
protected function assertAccess($build, $access) {
|
||||
$sensitive_content = $this->randomContextValue();
|
||||
$build['#markup'] = $sensitive_content;
|
||||
if ($access) {
|
||||
if (($access instanceof AccessResultInterface && $access->isAllowed()) || $access === TRUE) {
|
||||
$this->assertSame($sensitive_content, $this->renderer->renderRoot($build));
|
||||
}
|
||||
else {
|
||||
|
@ -784,6 +821,14 @@ class TestAccessClass {
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
public static function accessResultAllowed() {
|
||||
return AccessResult::allowed();
|
||||
}
|
||||
|
||||
public static function accessResultForbidden() {
|
||||
return AccessResult::forbidden();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TestCallables {
|
||||
|
|
Loading…
Reference in New Issue