1<?php 2 3namespace Drupal\layout_builder\Plugin\SectionStorage; 4 5use Drupal\Core\Access\AccessResult; 6use Drupal\Core\Cache\RefinableCacheableDependencyInterface; 7use Drupal\Core\Entity\EntityFieldManagerInterface; 8use Drupal\Core\Entity\EntityRepositoryInterface; 9use Drupal\Core\Entity\EntityTypeInterface; 10use Drupal\Core\Entity\EntityTypeManagerInterface; 11use Drupal\Core\Entity\FieldableEntityInterface; 12use Drupal\Core\Entity\TranslatableInterface; 13use Drupal\Core\Plugin\ContainerFactoryPluginInterface; 14use Drupal\Core\Plugin\Context\Context; 15use Drupal\Core\Plugin\Context\ContextDefinition; 16use Drupal\Core\Plugin\Context\EntityContext; 17use Drupal\Core\Session\AccountInterface; 18use Drupal\Core\Url; 19use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; 20use Drupal\layout_builder\OverridesSectionStorageInterface; 21use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; 22use Symfony\Component\DependencyInjection\ContainerInterface; 23use Symfony\Component\Routing\RouteCollection; 24 25/** 26 * Defines the 'overrides' section storage type. 27 * 28 * OverridesSectionStorage uses a negative weight because: 29 * - It must be picked before 30 * \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage. 31 * - The default weight is 0, so custom implementations will not take 32 * precedence unless otherwise specified. 33 * 34 * @SectionStorage( 35 * id = "overrides", 36 * weight = -20, 37 * handles_permission_check = TRUE, 38 * context_definitions = { 39 * "entity" = @ContextDefinition("entity", constraints = { 40 * "EntityHasField" = \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::FIELD_NAME, 41 * }), 42 * "view_mode" = @ContextDefinition("string", default_value = "default"), 43 * } 44 * ) 45 * 46 * @internal 47 * Plugin classes are internal. 48 */ 49class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface { 50 51 /** 52 * The field name used by this storage. 53 * 54 * @var string 55 */ 56 const FIELD_NAME = 'layout_builder__layout'; 57 58 /** 59 * The entity type manager. 60 * 61 * @var \Drupal\Core\Entity\EntityTypeManagerInterface 62 */ 63 protected $entityTypeManager; 64 65 /** 66 * The entity field manager. 67 * 68 * @var \Drupal\Core\Entity\EntityFieldManagerInterface 69 */ 70 protected $entityFieldManager; 71 72 /** 73 * The section storage manager. 74 * 75 * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface 76 */ 77 protected $sectionStorageManager; 78 79 /** 80 * The entity repository. 81 * 82 * @var \Drupal\Core\Entity\EntityRepositoryInterface 83 */ 84 protected $entityRepository; 85 86 /** 87 * The current user. 88 * 89 * @var \Drupal\Core\Session\AccountInterface 90 */ 91 protected $currentUser; 92 93 /** 94 * {@inheritdoc} 95 */ 96 public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, SectionStorageManagerInterface $section_storage_manager, EntityRepositoryInterface $entity_repository, AccountInterface $current_user = NULL) { 97 parent::__construct($configuration, $plugin_id, $plugin_definition); 98 99 $this->entityTypeManager = $entity_type_manager; 100 $this->entityFieldManager = $entity_field_manager; 101 $this->sectionStorageManager = $section_storage_manager; 102 $this->entityRepository = $entity_repository; 103 if (!$current_user) { 104 @trigger_error('The current_user service must be passed to OverridesSectionStorage::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED); 105 $current_user = \Drupal::currentUser(); 106 } 107 $this->currentUser = $current_user; 108 } 109 110 /** 111 * {@inheritdoc} 112 */ 113 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 114 return new static( 115 $configuration, 116 $plugin_id, 117 $plugin_definition, 118 $container->get('entity_type.manager'), 119 $container->get('entity_field.manager'), 120 $container->get('plugin.manager.layout_builder.section_storage'), 121 $container->get('entity.repository'), 122 $container->get('current_user') 123 ); 124 } 125 126 /** 127 * {@inheritdoc} 128 */ 129 protected function getSectionList() { 130 return $this->getEntity()->get(static::FIELD_NAME); 131 } 132 133 /** 134 * Gets the entity storing the overrides. 135 * 136 * @return \Drupal\Core\Entity\FieldableEntityInterface 137 * The entity storing the overrides. 138 */ 139 protected function getEntity() { 140 return $this->getContextValue('entity'); 141 } 142 143 /** 144 * {@inheritdoc} 145 */ 146 public function getStorageId() { 147 $entity = $this->getEntity(); 148 return $entity->getEntityTypeId() . '.' . $entity->id(); 149 } 150 151 /** 152 * {@inheritdoc} 153 */ 154 public function getTempstoreKey() { 155 $key = parent::getTempstoreKey(); 156 $key .= '.' . $this->getContextValue('view_mode'); 157 158 $entity = $this->getEntity(); 159 // @todo Allow entities to provide this contextual information in 160 // https://www.drupal.org/project/drupal/issues/3026957. 161 if ($entity instanceof TranslatableInterface) { 162 $key .= '.' . $entity->language()->getId(); 163 } 164 return $key; 165 } 166 167 /** 168 * {@inheritdoc} 169 */ 170 public function extractIdFromRoute($value, $definition, $name, array $defaults) { 171 @trigger_error('\Drupal\layout_builder\SectionStorageInterface::extractIdFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() should be used instead. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED); 172 if (strpos($value, '.') !== FALSE) { 173 return $value; 174 } 175 176 if (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) { 177 $entity_type_id = $defaults['entity_type_id']; 178 $entity_id = $defaults[$entity_type_id]; 179 return $entity_type_id . '.' . $entity_id; 180 } 181 } 182 183 /** 184 * {@inheritdoc} 185 */ 186 public function getSectionListFromId($id) { 187 @trigger_error('\Drupal\layout_builder\SectionStorageInterface::getSectionListFromId() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The section list should be derived from context. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED); 188 if (strpos($id, '.') !== FALSE) { 189 list($entity_type_id, $entity_id) = explode('.', $id, 2); 190 $entity = $this->entityRepository->getActive($entity_type_id, $entity_id); 191 if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) { 192 return $entity->get(static::FIELD_NAME); 193 } 194 } 195 throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType())); 196 } 197 198 /** 199 * {@inheritdoc} 200 */ 201 public function deriveContextsFromRoute($value, $definition, $name, array $defaults) { 202 $contexts = []; 203 204 if ($entity = $this->extractEntityFromRoute($value, $defaults)) { 205 $contexts['entity'] = EntityContext::fromEntity($entity); 206 // @todo Expand to work for all view modes in 207 // https://www.drupal.org/node/2907413. 208 $view_mode = 'full'; 209 // Retrieve the actual view mode from the returned view display as the 210 // requested view mode may not exist and a fallback will be used. 211 $view_mode = LayoutBuilderEntityViewDisplay::collectRenderDisplay($entity, $view_mode)->getMode(); 212 $contexts['view_mode'] = new Context(new ContextDefinition('string'), $view_mode); 213 } 214 return $contexts; 215 } 216 217 /** 218 * Extracts an entity from the route values. 219 * 220 * @param mixed $value 221 * The raw value from the route. 222 * @param array $defaults 223 * The route defaults array. 224 * 225 * @return \Drupal\Core\Entity\EntityInterface|null 226 * The entity for the route, or NULL if none exist. 227 * 228 * @see \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() 229 * @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert() 230 */ 231 private function extractEntityFromRoute($value, array $defaults) { 232 if (strpos($value, '.') !== FALSE) { 233 list($entity_type_id, $entity_id) = explode('.', $value, 2); 234 } 235 elseif (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) { 236 $entity_type_id = $defaults['entity_type_id']; 237 $entity_id = $defaults[$entity_type_id]; 238 } 239 else { 240 return NULL; 241 } 242 243 $entity = $this->entityRepository->getActive($entity_type_id, $entity_id); 244 if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) { 245 return $entity; 246 } 247 } 248 249 /** 250 * {@inheritdoc} 251 */ 252 public function buildRoutes(RouteCollection $collection) { 253 foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { 254 // If the canonical route does not exist, do not provide any Layout 255 // Builder UI routes for this entity type. 256 if (!$collection->get("entity.$entity_type_id.canonical")) { 257 continue; 258 } 259 260 $defaults = []; 261 $defaults['entity_type_id'] = $entity_type_id; 262 263 // Retrieve the requirements from the canonical route. 264 $requirements = $collection->get("entity.$entity_type_id.canonical")->getRequirements(); 265 266 $options = []; 267 // Ensure that upcasting is run in the correct order. 268 $options['parameters']['section_storage'] = []; 269 $options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id; 270 271 $template = $entity_type->getLinkTemplate('canonical') . '/layout'; 272 $this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id, $entity_type_id); 273 } 274 } 275 276 /** 277 * {@inheritdoc} 278 */ 279 public function buildLocalTasks($base_plugin_definition) { 280 $local_tasks = []; 281 foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { 282 $local_tasks["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [ 283 'route_name' => "layout_builder.overrides.$entity_type_id.view", 284 'weight' => 15, 285 'title' => $this->t('Layout'), 286 'base_route' => "entity.$entity_type_id.canonical", 287 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], 288 ]; 289 } 290 return $local_tasks; 291 } 292 293 /** 294 * Determines if this entity type's ID is stored as an integer. 295 * 296 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type 297 * An entity type. 298 * 299 * @return bool 300 * TRUE if this entity type's ID key is always an integer, FALSE otherwise. 301 */ 302 protected function hasIntegerId(EntityTypeInterface $entity_type) { 303 $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); 304 return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer'; 305 } 306 307 /** 308 * Returns an array of relevant entity types. 309 * 310 * @return \Drupal\Core\Entity\EntityTypeInterface[] 311 * An array of entity types. 312 */ 313 protected function getEntityTypes() { 314 return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { 315 return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasHandlerClass('form', 'layout_builder') && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical'); 316 }); 317 } 318 319 /** 320 * {@inheritdoc} 321 */ 322 public function getDefaultSectionStorage() { 323 $display = LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), $this->getContextValue('view_mode')); 324 return $this->sectionStorageManager->load('defaults', ['display' => EntityContext::fromEntity($display)]); 325 } 326 327 /** 328 * {@inheritdoc} 329 */ 330 public function getRedirectUrl() { 331 return $this->getEntity()->toUrl('canonical'); 332 } 333 334 /** 335 * {@inheritdoc} 336 */ 337 public function getLayoutBuilderUrl($rel = 'view') { 338 $entity = $this->getEntity(); 339 $route_parameters[$entity->getEntityTypeId()] = $entity->id(); 340 return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.$rel", $route_parameters); 341 } 342 343 /** 344 * {@inheritdoc} 345 */ 346 public function getContextsDuringPreview() { 347 $contexts = parent::getContextsDuringPreview(); 348 349 // @todo Remove this in https://www.drupal.org/node/3018782. 350 if (isset($contexts['entity'])) { 351 $contexts['layout_builder.entity'] = $contexts['entity']; 352 unset($contexts['entity']); 353 } 354 return $contexts; 355 } 356 357 /** 358 * {@inheritdoc} 359 */ 360 public function label() { 361 return $this->getEntity()->label(); 362 } 363 364 /** 365 * {@inheritdoc} 366 */ 367 public function save() { 368 return $this->getEntity()->save(); 369 } 370 371 /** 372 * {@inheritdoc} 373 */ 374 public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { 375 if ($account === NULL) { 376 $account = $this->currentUser; 377 } 378 379 $entity = $this->getEntity(); 380 381 // Create an access result that will allow access to the layout if one of 382 // these conditions applies: 383 // 1. The user can configure any layouts. 384 $any_access = AccessResult::allowedIfHasPermission($account, 'configure any layout'); 385 // 2. The user can configure layouts on all items of the bundle type. 386 $bundle_access = AccessResult::allowedIfHasPermission($account, "configure all {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides"); 387 // 3. The user can configure layouts items of this bundle type they can edit 388 // AND the user has access to edit this entity. 389 $edit_only_bundle_access = AccessResult::allowedIfHasPermission($account, "configure editable {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides"); 390 $edit_only_bundle_access = $edit_only_bundle_access->andIf($entity->access('update', $account, TRUE)); 391 392 $result = $any_access 393 ->orIf($bundle_access) 394 ->orIf($edit_only_bundle_access); 395 396 // Access also depends on the default being enabled. 397 $result = $result->andIf($this->getDefaultSectionStorage()->access($operation, $account, TRUE)); 398 $result = $this->handleTranslationAccess($result, $operation, $account); 399 return $return_as_object ? $result : $result->isAllowed(); 400 } 401 402 /** 403 * Handles access checks related to translations. 404 * 405 * @param \Drupal\Core\Access\AccessResult $result 406 * The access result. 407 * @param string $operation 408 * The operation to be performed. 409 * @param \Drupal\Core\Session\AccountInterface $account 410 * The user for which to check access. 411 * 412 * @return \Drupal\Core\Access\AccessResultInterface 413 * The access result. 414 */ 415 protected function handleTranslationAccess(AccessResult $result, $operation, AccountInterface $account) { 416 $entity = $this->getEntity(); 417 // Access is always denied on non-default translations. 418 return $result->andIf(AccessResult::allowedIf(!($entity instanceof TranslatableInterface && !$entity->isDefaultTranslation())))->addCacheableDependency($entity); 419 } 420 421 /** 422 * {@inheritdoc} 423 */ 424 public function isApplicable(RefinableCacheableDependencyInterface $cacheability) { 425 $default_section_storage = $this->getDefaultSectionStorage(); 426 $cacheability->addCacheableDependency($default_section_storage)->addCacheableDependency($this); 427 // Check that overrides are enabled and have at least one section. 428 return $default_section_storage->isOverridable() && $this->isOverridden(); 429 } 430 431 /** 432 * {@inheritdoc} 433 */ 434 public function isOverridden() { 435 // If there are any sections at all, including a blank one, this section 436 // storage has been overridden. Do not use count() as it does not include 437 // blank sections. 438 return !empty($this->getSections()); 439 } 440 441} 442