Issue #3208390 by amateescu, s_leu, Fabianx, tim.plunkett, xjm, smustgrave, alexpott, catch, joachim, EclipseGc: Add an API for allowing modules to mark their forms as workspace-safe

merge-requests/7012/head^2
catch 2024-04-21 09:19:06 +01:00
parent cbad32ab34
commit 51f471d8fb
23 changed files with 102 additions and 50 deletions

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\Core\Form;
/**
* Defines an interface for forms that can be workspace-safe.
*
* This interface should be used by forms that have to determine whether they're
* workspace-safe based on dynamic criteria.
*
* @see \Drupal\Core\Form\WorkspaceSafeFormInterface
*/
interface WorkspaceDynamicSafeFormInterface extends FormInterface {
/**
* Determines whether the form is safe to be submitted in a workspace.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return bool
* TRUE if the form is workspace-safe, FALSE otherwise.
*/
public function isWorkspaceSafeForm(array $form, FormStateInterface $form_state): bool;
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Drupal\Core\Form;
/**
* Defines an interface for forms that are safe to be submitted in a workspace.
*
* A form is considered workspace-safe if its submission has no impact on the
* Live site.
*
* @see \Drupal\Core\Form\WorkspaceDynamicSafeFormInterface
*/
interface WorkspaceSafeFormInterface extends FormInterface {}

View File

@ -11,6 +11,7 @@ use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
@ -29,7 +30,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
abstract class ConfigureBlockFormBase extends FormBase implements BaseFormIdInterface {
abstract class ConfigureBlockFormBase extends FormBase implements BaseFormIdInterface, WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use ContextAwarePluginAssignmentTrait;
@ -164,7 +165,6 @@ abstract class ConfigureBlockFormBase extends FormBase implements BaseFormIdInte
$this->delta = $delta;
$this->uuid = $component->getUuid();
$this->block = $component->getPlugin();
$this->markWorkspaceSafe($form_state);
$form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($section_storage));

View File

@ -7,6 +7,7 @@ use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Layout\LayoutInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
@ -26,7 +27,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class ConfigureSectionForm extends FormBase {
class ConfigureSectionForm extends FormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderContextTrait;
@ -128,7 +129,6 @@ class ConfigureSectionForm extends FormBase {
$this->delta = $delta;
$this->isUpdate = is_null($plugin_id);
$this->pluginId = $plugin_id;
$this->markWorkspaceSafe($form_state);
$section = $this->getCurrentSection();

View File

@ -4,6 +4,7 @@ namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
@ -15,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class DiscardLayoutChangesForm extends ConfirmFormBase {
class DiscardLayoutChangesForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
@ -89,7 +90,6 @@ class DiscardLayoutChangesForm extends ConfirmFormBase {
*/
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
$this->markWorkspaceSafe($form_state);
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);

View File

@ -4,6 +4,7 @@ namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
@ -16,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class LayoutBuilderDisableForm extends ConfirmFormBase {
class LayoutBuilderDisableForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
@ -94,7 +95,6 @@ class LayoutBuilderDisableForm extends ConfirmFormBase {
}
$this->sectionStorage = $section_storage;
$this->markWorkspaceSafe($form_state);
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);

View File

@ -5,6 +5,7 @@ namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
@ -17,7 +18,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase {
abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderHighlightTrait;
@ -78,7 +79,6 @@ abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->markWorkspaceSafe($form_state);
$form = parent::buildForm($form, $form_state);
if ($this->isAjax()) {

View File

@ -5,6 +5,7 @@ namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
@ -18,7 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class MoveBlockForm extends FormBase {
class MoveBlockForm extends FormBase implements WorkspaceDynamicSafeFormInterface {
use AjaxFormHelperTrait;
use LayoutBuilderContextTrait;
@ -118,7 +119,6 @@ class MoveBlockForm extends FormBase {
$this->delta = $delta;
$this->uuid = $uuid;
$this->region = $region;
$this->markWorkspaceSafe($form_state);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid);

View File

@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
@ -21,7 +22,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class OverridesEntityForm extends ContentEntityForm {
class OverridesEntityForm extends ContentEntityForm implements WorkspaceDynamicSafeFormInterface {
use PreviewToggleTrait;
use LayoutBuilderEntityFormTrait;
@ -91,7 +92,6 @@ class OverridesEntityForm extends ContentEntityForm {
*/
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
$this->markWorkspaceSafe($form_state);
$form = parent::buildForm($form, $form_state);
$form['#attributes']['class'][] = 'layout-builder-form';

View File

@ -4,6 +4,7 @@ namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
@ -16,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* @internal
* Form classes are internal.
*/
class RevertOverridesForm extends ConfirmFormBase {
class RevertOverridesForm extends ConfirmFormBase implements WorkspaceDynamicSafeFormInterface {
use WorkspaceSafeFormTrait;
@ -101,7 +102,6 @@ class RevertOverridesForm extends ConfirmFormBase {
}
$this->sectionStorage = $section_storage;
$this->markWorkspaceSafe($form_state);
// Mark this as an administrative page for JavaScript ("Back to site" link).
$form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE;
return parent::buildForm($form, $form_state);

View File

@ -19,16 +19,17 @@ trait WorkspaceSafeFormTrait {
protected ?WorkspaceInformationInterface $workspaceInfo = NULL;
/**
* Marks a form as workspace-safe, if possible.
* Determines whether the current form is safe to be submitted in a workspace.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
* The current state of the form.
*
* @return bool
* TRUE if the form is workspace-safe, FALSE otherwise.
*/
protected function markWorkspaceSafe(FormStateInterface $form_state): void {
if (!\Drupal::hasService('workspaces.information')) {
return;
}
public function isWorkspaceSafeForm(array $form, FormStateInterface $form_state): bool {
$section_storage = $this->sectionStorage ?: $this->getSectionStorageFromFormState($form_state);
if ($section_storage) {
$context_definitions = $section_storage->getContextDefinitions();
@ -39,10 +40,12 @@ trait WorkspaceSafeFormTrait {
$ignored = $entity && $this->getWorkspaceInfo()->isEntityIgnored($entity);
if ($supported || $ignored) {
$form_state->set('workspace_safe', TRUE);
return TRUE;
}
}
}
return FALSE;
}
/**

View File

@ -5,6 +5,7 @@ namespace Drupal\search\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\search\SearchPageRepositoryInterface;
@ -15,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @internal
*/
class SearchBlockForm extends FormBase {
class SearchBlockForm extends FormBase implements WorkspaceSafeFormInterface {
/**
* The search page repository.

View File

@ -4,6 +4,7 @@ namespace Drupal\search\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Url;
use Drupal\search\SearchPageInterface;
@ -18,7 +19,7 @@ use Drupal\search\SearchPageInterface;
*
* @internal
*/
class SearchPageForm extends FormBase {
class SearchPageForm extends FormBase implements WorkspaceSafeFormInterface {
/**
* The search page entity.

View File

@ -5,6 +5,7 @@ namespace Drupal\views\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Render\Element\Checkboxes;
use Drupal\Core\Url;
@ -16,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @internal
*/
class ViewsExposedForm extends FormBase {
class ViewsExposedForm extends FormBase implements WorkspaceSafeFormInterface {
/**
* The exposed form cache.

View File

@ -5,6 +5,7 @@ namespace Drupal\workspaces\Form;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Url;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -12,7 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that switches to the live version of the site.
*/
class SwitchToLiveForm extends ConfirmFormBase implements WorkspaceFormInterface, ContainerInjectionInterface {
class SwitchToLiveForm extends ConfirmFormBase implements ContainerInjectionInterface, WorkspaceSafeFormInterface {
/**
* The workspace manager.

View File

@ -5,6 +5,7 @@ namespace Drupal\workspaces\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\workspaces\WorkspaceAccessException;
@ -14,7 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Handle activation of a workspace on administrative pages.
*/
class WorkspaceActivateForm extends EntityConfirmFormBase implements WorkspaceFormInterface {
class WorkspaceActivateForm extends EntityConfirmFormBase implements WorkspaceSafeFormInterface {
/**
* The workspace entity.

View File

@ -16,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @internal
*/
class WorkspaceDeleteForm extends ContentEntityDeleteForm implements WorkspaceFormInterface {
class WorkspaceDeleteForm extends ContentEntityDeleteForm {
/**
* The workspace entity.

View File

@ -12,7 +12,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the workspace edit forms.
*/
class WorkspaceForm extends ContentEntityForm implements WorkspaceFormInterface {
class WorkspaceForm extends ContentEntityForm {
/**
* The workspace entity.

View File

@ -8,5 +8,11 @@ use Drupal\Core\Form\FormInterface;
* Defines interface for workspace forms so they can be easily distinguished.
*
* @internal
*
* @deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use
* \Drupal\Core\Form\WorkspaceSafeFormInterface or
* \Drupal\Core\Form\WorkspaceDynamicSafeFormInterface instead.
*
* @see https://www.drupal.org/node/3229111
*/
interface WorkspaceFormInterface extends FormInterface {}

View File

@ -6,6 +6,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Url;
use Drupal\workspaces\WorkspaceInterface;
use Drupal\workspaces\WorkspaceOperationFactory;
@ -14,7 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that merges the contents for a workspace into another one.
*/
class WorkspaceMergeForm extends ConfirmFormBase implements WorkspaceFormInterface, ContainerInjectionInterface {
class WorkspaceMergeForm extends ConfirmFormBase implements ContainerInjectionInterface, WorkspaceSafeFormInterface {
/**
* The source workspace entity.

View File

@ -6,6 +6,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Url;
use Drupal\workspaces\WorkspaceAccessException;
use Drupal\workspaces\WorkspaceInterface;
@ -15,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the workspace publishing form.
*/
class WorkspacePublishForm extends ConfirmFormBase implements WorkspaceFormInterface, ContainerInjectionInterface {
class WorkspacePublishForm extends ConfirmFormBase implements ContainerInjectionInterface, WorkspaceSafeFormInterface {
/**
* The workspace that will be published.

View File

@ -5,6 +5,7 @@ namespace Drupal\workspaces\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\workspaces\WorkspaceAccessException;
use Drupal\workspaces\WorkspaceManagerInterface;
@ -13,7 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that activates a different workspace.
*/
class WorkspaceSwitcherForm extends FormBase implements WorkspaceFormInterface {
class WorkspaceSwitcherForm extends FormBase implements WorkspaceSafeFormInterface {
/**
* The workspace manager.

View File

@ -4,10 +4,10 @@ namespace Drupal\workspaces;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface;
use Drupal\Core\Form\WorkspaceSafeFormInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Form\ViewsExposedForm;
use Drupal\workspaces\Form\WorkspaceFormInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -61,28 +61,18 @@ class FormOperations implements ContainerInjectionInterface {
return;
}
// Add an additional validation step for every form if we are in a
// non-default workspace.
// Add a validation step for every form if we are in a workspace.
$this->addWorkspaceValidation($form);
// If a form has already been marked as safe or not to submit in a
// non-default workspace, we don't have anything else to do.
// workspace, we don't have anything else to do.
if ($form_state->has('workspace_safe')) {
return;
}
// No forms are safe to submit in a non-default workspace by default, except
// for the whitelisted ones defined below.
$workspace_safe = FALSE;
// Whitelist a few forms that we know are safe to submit.
$form_object = $form_state->getFormObject();
$is_workspace_form = $form_object instanceof WorkspaceFormInterface;
$is_search_form = in_array($form_object->getFormId(), ['search_block_form', 'search_form'], TRUE);
$is_views_exposed_form = $form_object instanceof ViewsExposedForm;
if ($is_workspace_form || $is_search_form || $is_views_exposed_form) {
$workspace_safe = TRUE;
}
$workspace_safe = $form_object instanceof WorkspaceSafeFormInterface
|| ($form_object instanceof WorkspaceDynamicSafeFormInterface && $form_object->isWorkspaceSafeForm($form, $form_state));
$form_state->set('workspace_safe', $workspace_safe);
}