diff --git a/modules/entity_reference_integrity_enforce/entity_reference_integrity_enforce.module b/modules/entity_reference_integrity_enforce/entity_reference_integrity_enforce.module index 51929f9..7105163 100644 --- a/modules/entity_reference_integrity_enforce/entity_reference_integrity_enforce.module +++ b/modules/entity_reference_integrity_enforce/entity_reference_integrity_enforce.module @@ -94,3 +94,71 @@ function entity_reference_integrity_enforce_action_info_alter(&$definitions) { } } } + +/** + * FieldAPI form validation callback. + * + * @see \Drupal\entity_reference_integrity_enforce\FormAlter::formAlter() + */ +function entity_reference_integrity_enforce_state_validation(&$form, FormStateInterface $form_state) { + // Look to see if any entities were defined as possibly blocking this edit. + $entities = $form['referencing_entities_list']['#value']; + + // Nothing to worry about, so skip all further logic. + if (empty($entities)) { + return; + } + + // Support the Content Moderation workflow system. + $new_state = $form_state->getValue('moderation_state'); + if (isset($new_state)) { + // Identify what the actual moderation state is, and if it is both changing + // the publication status to "unpublished" and changing the default revision + // to point to the new revision, block the change. + if (!empty($new_state[0]['value'])) { + // Get the entity that was being edited. + $form_object = $form_state->getFormObject(); + $entity = $form_object->getEntity(); + + /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */ + $moderation_info = \Drupal::service('content_moderation.moderation_information'); + if (!$moderation_info->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) { + return; + } + + // Load the moderation state. + $workflow = $moderation_info->getWorkflowForEntity($entity); + + // Check the new moderation status. Don't allow the page to be archived, + // which means that the new revision is unpublished and the new revision + // is made the default revision. + $new_moderation_state = $workflow->getTypePlugin()->getState($new_state[0]['value']); + if (!$new_moderation_state->isPublishedState()) { + if ($new_moderation_state->isDefaultRevisionState()) { + $form_state->setErrorByName('moderation_state', t('You can not archive this as it is being referenced by another entity.')); + + // Render the list of entities; no point in doing it earlier if it + // isn't actually needed. + // @see \Drupal\entity_reference_integrity_enforce\FormAlter::buildReferencingEntitiesList() + $form_state->setErrorByName('referencing_entities_list', \Drupal::service('renderer')->render($entities)); + } + } + } + } + + // Standard/legacy node publication states. + else { + $complete_form = $form_state->getCompleteForm(); + $new_status = $form_state->getValue('status'); + + // The node is being unpublished. + if (empty($new_status['value']) && isset($complete_form['status'])) { + $form_state->setErrorByName('status', t('You can not unpublish this as it is being referenced by another entity.')); + + // Render the list of entities; no point in doing it earlier if it isn't + // actually needed. + // @see \Drupal\entity_reference_integrity_enforce\FormAlter::buildReferencingEntitiesList() + $form_state->setErrorByName('referencing_entities_list', \Drupal::service('renderer')->render($entities)); + } + } +} diff --git a/modules/entity_reference_integrity_enforce/src/Form/SettingsForm.php b/modules/entity_reference_integrity_enforce/src/Form/SettingsForm.php index 215a2f3..742c3f0 100644 --- a/modules/entity_reference_integrity_enforce/src/Form/SettingsForm.php +++ b/modules/entity_reference_integrity_enforce/src/Form/SettingsForm.php @@ -72,6 +72,12 @@ class SettingsForm extends ConfigFormBase { '#options' => $options, '#default_value' => $this->config('entity_reference_integrity_enforce.settings')->get('enabled_entity_type_ids'), ]; + $form['show_node_edit_link'] = array( + '#type' => 'checkbox', + '#title' => t('Show node edit link'), + '#description' => t('Show node edit link instead of node link in error message.'), + '#default_value' => $this->config('entity_reference_integrity_enforce.settings')->get('show_node_edit_link'), + ); return parent::buildForm($form, $form_state); } @@ -82,6 +88,7 @@ class SettingsForm extends ConfigFormBase { $this ->config('entity_reference_integrity_enforce.settings') ->set('enabled_entity_type_ids', $form_state->getValue('enabled_entity_type_ids')) + ->set('show_node_edit_link', $form_state->getValue('show_node_edit_link')) ->save(); parent::submitForm($form, $form_state); } diff --git a/modules/entity_reference_integrity_enforce/src/FormAlter.php b/modules/entity_reference_integrity_enforce/src/FormAlter.php index 430944e..9add744 100644 --- a/modules/entity_reference_integrity_enforce/src/FormAlter.php +++ b/modules/entity_reference_integrity_enforce/src/FormAlter.php @@ -8,6 +8,11 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\entity_reference_integrity\EntityReferenceDependencyManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Url; +use Drupal\Core\Link; +use Drupal\path_alias\AliasManagerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; + /** * Alter entity delete forms to provide some warning deletes will fail. @@ -30,12 +35,29 @@ class FormAlter implements ContainerInjectionInterface { */ protected $enabledEntityTypeIds; + /** + * The path alias manager. + * + * @var \Drupal\path_alias\AliasManagerInterface + */ + protected $aliasManager; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** * Create a DeleteFormAlter object. */ - public function __construct(EntityReferenceDependencyManagerInterface $calculator, $enabled_entity_type_ids) { + public function __construct(EntityReferenceDependencyManagerInterface $calculator, $enabled_entity_type_ids, AliasManagerInterface $alias_manager, ConfigFactoryInterface $config_factory) { $this->dependencyManager = $calculator; $this->enabledEntityTypeIds = $enabled_entity_type_ids; + $this->aliasManager = $alias_manager; + $this->configFactory = $config_factory; } /** @@ -44,7 +66,9 @@ class FormAlter implements ContainerInjectionInterface { public static function create(ContainerInterface $container) { return new static( $container->get('entity_reference_integrity.dependency_manager'), - $container->get('config.factory')->get('entity_reference_integrity_enforce.settings')->get('enabled_entity_type_ids') + $container->get('config.factory')->get('entity_reference_integrity_enforce.settings')->get('enabled_entity_type_ids'), + $container->get('path_alias.manager'), + $container->get('config.factory') ); } @@ -54,7 +78,7 @@ class FormAlter implements ContainerInjectionInterface { public function formAlter(&$form, FormStateInterface $form_state, $form_id) { /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ $form_object = $form_state->getFormObject(); - if (!$this->isDeleteForm($form_object)) { + if (!$this->isDeleteForm($form_object) && !$this->isEditForm($form_object)) { return; } @@ -62,20 +86,28 @@ class FormAlter implements ContainerInjectionInterface { if (in_array($entity->getEntityTypeId(), $this->enabledEntityTypeIds, TRUE) && $this->dependencyManager->hasDependents($entity)) { $referencing_entities = $this->dependencyManager->getDependentEntities($entity); - - unset($form['actions']['submit']); - unset($form['description']); - - $form['referencing_entities_list'] = [ - '#weight' => -10, - 'explanation' => [ - '#prefix' => '
', - '#markup' => $this->t('You can not delete this as it is being referenced by another entity.'), - '#suffix' => '
', - ], - 'entities' => $this->buildReferencingEntitiesList($referencing_entities), - '#suffix' => '
', - ]; + if ($this->isDeleteForm($form_object)) { + unset($form['actions']['submit']); + unset($form['description']); + + $form['referencing_entities_list'] = [ + '#weight' => -10, + 'explanation' => [ + '#prefix' => '
', + '#markup' => $this->t('You can not delete this as it is being referenced by another entity.'), + '#suffix' => '
', + ], + 'entities' => $this->buildReferencingEntitiesList($referencing_entities), + '#suffix' => '
', + ]; + } + if ($this->isEditForm($form_object)) { + $form['referencing_entities_list'] = [ + '#weight' => -10, + '#value' => $this->buildReferencingEntitiesList($referencing_entities), + ]; + array_unshift($form['#validate'], 'entity_reference_integrity_enforce_state_validation'); + } } } @@ -89,6 +121,7 @@ class FormAlter implements ContainerInjectionInterface { * A renderable array of referencing entities. */ protected function buildReferencingEntitiesList(array $referencing_entities) { + $is_show_edit_link = $this->configFactory->get('entity_reference_integrity_enforce.settings')->get('show_node_edit_link'); $build = []; /** @var \Drupal\Core\Entity\EntityInterface[] $entities */ foreach ($referencing_entities as $entity_type_id => $entities) { @@ -102,14 +135,57 @@ class FormAlter implements ContainerInjectionInterface { '#items' => [], ]; foreach ($entities as $entity) { - $build[$entity_type_id]['list']['#items'][] = $entity->hasLinkTemplate('canonical') ? $entity->toLink() : $entity->label(); + if (!$is_show_edit_link) { + $build[$entity_type_id]['list']['#items'][] = $entity->hasLinkTemplate('canonical') ? $entity->toLink() : $entity->label(); + } + else { + if ($entity->hasLinkTemplate('canonical')) { + $node_edit_link = $this->build_node_edit_link($entity->id(), $entity->label()); + $build[$entity_type_id]['list']['#items'][] = $node_edit_link; + } + else { + $parent_entity = $entity->getParentEntity(); + + checkPE: + if (!empty($parent_entity)) { + $parent_entity_type = $parent_entity->getEntityTypeId(); + // In the case of nested relationships we need to find the base + // entity. + if ($parent_entity_type != 'node') { + // If child is only nested one level deep. + if (method_exists($parent_entity, 'getParentEntity') && !is_null($parent_entity->getParentEntity()) && !is_null($parent_entity->getParentEntity()->getEntityTypeId())) { + if ($parent_entity->getParentEntity()->getEntityTypeId() == 'node') { + $parent_entity = $parent_entity->getParentEntity(); + } + // Two or more levels of nesting. + else { + if (!is_null($parent_entity->getEntityTypeId())) { + while (1 == 1) { + if (is_null($parent_entity)) break; + $tempA = $parent_entity->getEntityTypeId(); + if (is_null($tempA)) break; + if ($tempA == 'node') break; + $parent_entity = $parent_entity->getParentEntity(); + } + } + } + } + } + if (is_null($parent_entity)) goto checkPE; + + $node_edit_link = $this->build_node_edit_link($parent_entity->id(), $entity->label()); + $build[$entity_type_id]['list']['#items'][] = $node_edit_link; + } + } + } } } return $build; } /** - * Check if a given generic form is applicable to be altered. + * Check if a given generic form is applicable to be altered + * and an delete form. * * @param mixed $form_object * The form object. @@ -121,4 +197,35 @@ class FormAlter implements ContainerInjectionInterface { return $form_object instanceof EntityFormInterface && $form_object->getOperation() === 'delete'; } + /** + * Check if a given generic form is applicable to be altered + * and an edit form. + * + * @param mixed $form_object + * The form object. + * + * @return bool + * If alteration applies. + */ + protected function isEditForm($form_object) { + return $form_object instanceof EntityFormInterface && ($form_object->getOperation() === 'edit' || $form_object->getOperation() === 'default'); + } + + /** + * Return the node edit url. + * + * @param int $id + * The node id. + * + * @param string $label + * The entity label. + * + * @return object + * Node Edit Link. + */ + protected function build_node_edit_link(int $id, string $label) { + $url = Url::fromRoute('entity.node.edit_form', ['node' => $id], ['attributes' => ['target' => '_blank']]); + return Link::fromTextAndUrl($label, $url); + } + }