', + '#markup' => $this->t('Would you like to activate the %workspace workspace?', ['%workspace' => $workspace->label()]), + '#suffix' => '
', + ]; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => 'Activate', + ]; + + $form['#title'] = $this->t('Activate workspace %label', array('%label' => $workspace->label())); + + return $form; + } + +} diff --git a/core/modules/workspace/src/Form/WorkspaceActivateFormBase.php b/core/modules/workspace/src/Form/WorkspaceActivateFormBase.php new file mode 100644 index 0000000000..dba7776af8 --- /dev/null +++ b/core/modules/workspace/src/Form/WorkspaceActivateFormBase.php @@ -0,0 +1,82 @@ +get('workspace.manager'), + $container->get('entity_type.manager') + ); + } + + public function __construct(WorkspaceManagerInterface $workspace_manager, EntityTypeManagerInterface $entity_type_manager) { + $this->workspaceManager = $workspace_manager; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $id = $form_state->getValue('workspace_id'); + + // Ensure we are given an ID. + if (!$id) { + $form_state->setErrorByName('workspace_id', 'The workspace ID is required.'); + } + + // Ensure the workspace by that id exists. + /** @var WorkspaceInterface $workspace */ + $workspace = $this->entityTypeManager->getStorage('workspace')->load($id); + if (!$workspace) { + $form_state->setErrorByName('workspace_id', 'This workspace no longer exists.'); + } + } + + /** + * @inheritDoc + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $id = $form_state->getValue('workspace_id'); + /** @var WorkspaceInterface $workspace */ + $workspace = $this->entityTypeManager->getStorage('workspace')->load($id); + + try { + $this->workspaceManager->setActiveWorkspace($workspace); + $form_state->setRedirect('' . t('The Workspace module allows workspaces to be defined and switched between. Content is then assigned to the active workspace when created. For more information, see the online documentation for the Workspace module.', [':workspace' => 'https://www.drupal.org/node/2824024']) . '
'; + return $output; + } +} + +/** + * Implements hook_entity_base_field_info(). + */ +function workspace_entity_base_field_info(EntityTypeInterface $entity_type) { + if ($entity_type->isRevisionable() && !in_array($entity_type->id(), ['content_workspace', 'workspace'])) { + return ['workspace' => BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Workspace')) + ->setDescription(t('The Workspace of this piece of content.')) + ->setComputed(TRUE) + ->setClass(WorkspaceFieldItemList::class) + ->setSetting('target_type', 'workspace') + ->setTranslatable(TRUE)]; + } +} + +/** + * Implements hook_query_TAG_alter(). + */ +function workspace_query_entity_query_alter(AlterableInterface $query) { + /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspace.manager'); + $active_workspace = $workspace_manager->getActiveWorkspace(); + $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default'); + if ($active_workspace == $default_workspace_id) { + return; + } + + $entity_type = \Drupal::entityTypeManager()->getDefinition($query->getMetaData('entity_type')); + if (!empty($entity_type) && $workspace_manager->entityTypeCanBelongToWorkspaces($entity_type)) { + $entity_type_id = $entity_type->id(); + $entity_type_id_key = $entity_type->getKey('id'); + $query->leftJoin('content_workspace_field_revision', 'cwfr', 'base_table.' . $entity_type_id_key . ' = cwfr.content_entity_id'); + $query->condition('cwfr.content_entity_type_id', $entity_type_id); + $query->condition('cwfr.workspace', [$active_workspace, $default_workspace_id], 'IN'); + } +} + +/** + * Implements hook_views_query_alter(). + */ +function workspace_views_query_alter(ViewExecutable $view, QueryPluginBase $query) { + /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspace.manager'); + $active_workspace = $workspace_manager->getActiveWorkspace(); + $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default'); + if ($active_workspace == $default_workspace_id) { + return; + } + + $entity_type = $view->getBaseEntityType(); + if (!$workspace_manager->entityTypeCanBelongToWorkspaces($entity_type)) { + return; + } + + $configuration = [ + 'table' => 'content_workspace_field_revision', + 'field' => 'content_entity_revision_id', + 'left_table' => $entity_type->getDataTable(), + 'left_field' => $entity_type->getKey('revision'), + 'operator' => '=', + ]; + /** @var \Drupal\views\Plugin\views\join\JoinPluginBase $join */ + $join = Views::pluginManager('join') + ->createInstance('standard', $configuration); + /** @var \Drupal\views\Plugin\views\query\Sql $query */ + $query->addRelationship('cwrf', $join, 'content_workspace_field_revision'); + $query->addWhere(0, 'cwrf.workspace', [$active_workspace, $default_workspace_id], 'IN'); + foreach ($query->where as $where_id => $where) { + foreach ($where['conditions'] as $condition_id => $condition) { + if ($condition['field'] == $entity_type->getDataTable() . '.' . $entity_type->getKey('published')) { + //$query->where[$where_id]['conditions'][$condition_id]['field'] = 'cwrf.published'; + $value = $query->where[$where_id]['conditions'][$condition_id]['value']; + $query->setWhereGroup('OR', 'published'); + $query->addWhere('published', 'cwrf.published', $value); + $query->addWhere('published', $entity_type->getDataTable() . '.' . $entity_type->getKey('published'), $value); + unset($query->where[$where_id]['conditions'][$condition_id]); + } + } + } +} + + +/** + * Implements hook_entity_load(). + */ +function workspace_entity_load(array &$entities, $entity_type_id) { + /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspace.manager'); + if (!$workspace_manager->entityTypeCanBelongToWorkspaces(\Drupal::entityTypeManager()->getDefinition($entity_type_id))) { + return; + } + + $active_workspace = $workspace_manager->getActiveWorkspace(); + $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default'); + if ($active_workspace == $default_workspace_id) { + return; + } + + $keys = array_keys($entities); + $results = \Drupal::entityTypeManager() + ->getStorage('content_workspace') + ->getQuery() + ->condition('content_entity_type_id', $entity_type_id) + ->condition('content_entity_id', $keys, 'IN') + ->condition('workspace', [$active_workspace, $default_workspace_id], 'IN') + ->execute(); + foreach ($results as $revision_id => $entity_id) { + /** @var \Drupal\workspace\Entity\ContentWorkspaceInterface $content_workspace */ + $content_workspace = \Drupal::entityTypeManager() + ->getStorage('content_workspace') + ->loadRevision($revision_id); + $entity = $entities[$content_workspace->get('content_entity_id')->value]; + if ($content_workspace->get('content_entity_revision_id')->value != $entity->getRevisionId()) { + $new_entity = \Drupal::entityTypeManager() + ->getStorage($entity_type_id) + ->loadRevision($content_workspace->get('content_entity_revision_id')->value); + $entities[$entity->id()] = $new_entity; + } + $content_workspace->isPublished() ? $entities[$entity->id()]->setPublished() : $entities[$entity->id()]->setUnpublished(); + } +} + +/** + * Implements hook_element_info_alter(). + */ +function workspace_element_info_alter(array &$types) { + foreach ($types as &$type) { + if (!isset($type['#pre_render'])) { + $type['#pre_render'] = array(); + } + $type['#pre_render'][] = 'workspace_element_pre_render'; + } +} + +/** + * Element pre-render callback. + */ +function workspace_element_pre_render($element) { + if (isset($element['#cache'])) { + if (!isset($element['#cache']['contexts'])) { + $element['#cache']['contexts'] = []; + } + $element['#cache']['contexts'] = Cache::mergeContexts( + $element['#cache']['contexts'], ['workspace'] + ); + } + return $element; +} + +/** + * Implements hook_entity_presave(). + */ +function workspace_entity_presave(EntityInterface $entity) { + /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspace.manager'); + + // Only modify the entity if the active workspace isn't the default, and + // and the entity can belong to a workspace. + if (!empty($workspace_manager->getActiveWorkspace()) + && $workspace_manager->entityCanBelongToWorkspaces($entity)) { + + if (!$entity->isNew()) { + $original_workspace_id = $entity->original->workspace->target_id; + $workspace_id = $entity->workspace->target_id; + /** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */ + if ($original_workspace_id == $workspace_id) { + // Force a new revision is the entity is not new and the workspace + // is the same as the previous revision. + $entity->setNewRevision(TRUE); + } + } + + // The publishing status can be stored in a property for safe keeping + $entity->initial_published = $entity->isPublished(); + + $default_workspace_id = \Drupal::getContainer()->getParameter('workspace.default'); + if ($default_workspace_id != $workspace_manager->getActiveWorkspace()) { + // As this is the non-default workspace only new entity revisions should be + // made default. + $entity->isNew() || ($entity->original->workspace->target_id != $default_workspace_id) ? $entity->isDefaultRevision(TRUE) : $entity->isDefaultRevision(FALSE); + + // All entities in the non-default workspace get unpublished. + $entity->setUnpublished(); + } + } +} + +/** + * Implements hook_entity_insert(). + */ +function workspace_entity_insert(Drupal\Core\Entity\EntityInterface $entity) { + /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspace.manager'); + $workspace_manager->updateOrCreateFromEntity($entity); + if ($workspace_manager->entityCanBelongToWorkspaces($entity)) { + \Drupal::service('workspace.index.sequence') + ->useWorkspace($workspace_manager->getActiveWorkspace()) + ->add($entity); + } +} + +/** + * Implements hook_entity_update(). + */ +function workspace_entity_update(Drupal\Core\Entity\EntityInterface $entity) { + /** @var \Drupal\workspace\WorkspaceManagerInterface $workspace_manager */ + $workspace_manager = \Drupal::service('workspace.manager'); + $workspace_manager->updateOrCreateFromEntity($entity); + if ($workspace_manager->entityCanBelongToWorkspaces($entity)) { + \Drupal::service('workspace.index.sequence') + ->useWorkspace($workspace_manager->getActiveWorkspace()) + ->add($entity); + } +} + +/** + * Default value callback for 'upstream' base field definition. + * + * @return array + */ +function workspace_active_id() { + return 'workspace:' . \Drupal::service('workspace.manager')->getActiveWorkspace(); +} + +/** + * Implements hook_toolbar(). + * + * @see \Drupal\workspace\Toolbar + */ +function workspace_toolbar() { + $items = []; + + /** @var \Drupal\workspace\Entity\WorkspaceInterface $active_workspace */ + $active_workspace = \Drupal::service('workspace.manager')->getActiveWorkspace(TRUE); + + $items['workspace_switcher'] = [ + // Include the toolbar_tab_wrapper to style the link like a toolbar tab. + // Exclude the theme wrapper if custom styling is desired. + '#type' => 'toolbar_item', + '#weight' => 125, + '#wrapper_attributes' => [ + 'class' => ['workspace-toolbar-tab'], + ], + '#attached' => [ + 'library' => [ + 'workspace/drupal.workspace.toolbar', + ], + ], + ]; + + $items['deploy'] = [ + '#type' => 'toolbar_item', + '#weight' => 130, + '#wrapper_attributes' => [ + 'class' => ['workspace-deploy-toolbar-tab'], + ], + 'tab' => [ + \Drupal::formBuilder()->getForm(\Drupal\workspace\Form\DeploymentForm::class, $active_workspace), + ], + ]; + + $items['workspace_switcher']['tab'] = [ + '#type' => 'link', + '#title' => t('@active', ['@active' => $active_workspace->label()]), + '#url' => Url::fromRoute('entity.workspace.collection'), + '#attributes' => [ + 'title' => t('Switch workspaces'), + 'class' => ['toolbar-icon', 'toolbar-icon-workspace'], + ], + ]; + + $create_link = [ + '#type' => 'link', + '#title' => t('Add workspace'), + '#url' => Url::fromRoute('entity.workspace.add_form'), + '#options' => ['attributes' => ['class' => ['add-workspace']]], + ]; + + $items['workspace_switcher']['tray'] = [ + '#heading' => t('Switch to workspace'), + '#pre_render' => ['workspace_switcher_toolbar_pre_render'], + // This wil get filled in via pre-render. + 'workspace_forms' => [], + 'create_link' => $create_link, + '#cache' => [ + 'contexts' => \Drupal::entityTypeManager()->getDefinition('workspace')->getListCacheContexts(), + 'tags' => \Drupal::entityTypeManager()->getDefinition('workspace')->getListCacheTags(), + ], + '#attributes' => [ + 'class' => ['toolbar-menu'], + ], + ]; + + return $items; +} + +/** + * Prerender callback; Adds the workspace switcher forms to the render array. + * + * @param array $element + * + * @return array + * The modified $element. + */ +function workspace_switcher_toolbar_pre_render(array $element) { + /** @var \Drupal\workspace\Entity\WorkspaceInterface $workspace */ + foreach (\Drupal::entityTypeManager()->getStorage('workspace')->loadMultiple() as $workspace) { + if ($workspace->access('view', \Drupal::currentUser())) { + $element['workspace_forms']['workspace_' . $workspace->getMachineName()] = \Drupal::formBuilder()->getForm(WorkspaceSwitcherForm::class, $workspace); + } + } + + return $element; +} + +/** + * Implements hook_entity_access(). + * + * @see \Drupal\workspace\EntityAccess + */ +function workspace_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityAccess::class) + ->entityAccess($entity, $operation, $account); +} + +/** + * Implements hook_entity_create_access(). + * + * @see \Drupal\workspace\EntityAccess + */ +function workspace_entity_create_access(AccountInterface $account, array $context, $entity_bundle) { + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityAccess::class) + ->entityCreateAccess($account, $context, $entity_bundle); +} + +/** + * Implements hook_ENTITY_TYPE_access(). + * + * @see \Drupal\workspace\EntityAccess + */ +function workspace_workspace_access(EntityInterface $entity, $operation, AccountInterface $account) { + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityAccess::class) + ->workspaceAccess($entity, $operation, $account); +} + +/** + * Implements hook_ENTITY_TYPE_create_access(). + * + * @see \Drupal\workspace\EntityAccess + */ +function workspace_workspace_create_access(AccountInterface $account, array $context, $entity_bundle) { + return \Drupal::service('class_resolver') + ->getInstanceFromDefinition(EntityAccess::class) + ->workspaceCreateAccess($account, $context, $entity_bundle); +} diff --git a/core/modules/workspace/workspace.permissions.yml b/core/modules/workspace/workspace.permissions.yml new file mode 100644 index 0000000000..475d7e541e --- /dev/null +++ b/core/modules/workspace/workspace.permissions.yml @@ -0,0 +1,42 @@ +create_workspace: + title: Create a new workspace + +view_own_workspace: + title: View own workspace + description: View a workspace owned by the user. + +view_any_workspace: + title: View any workspace + description: View any workspace, regardless of ownership. + +edit_own_workspace: + title: Edit own workspace + description: Make changes to workspaces owned by the user. + +edit_any_workspace: + title: Edit any workspace + description: Make changes to any workspace, regardless of ownership. + +delete_own_workspace: + title: Delete own workspace + description: Delete a workspace owned by the user and all content revisions within it. + +delete_any_workspace: + title: Delete any workspace + description: Delete a workspace and all content revisions within it, regardless of ownership. + +bypass_entity_access_own_workspace: + title: Bypass content entity access in own workspace + description: Allow all Edit/Update/Delete permissions for all content entities in a workspace owned by the user. + restrict access: TRUE + +view_revision_trees: + title: View revision trees + description: View the revision tree for any entities. + +update any workspace from upstream: + title: Update any workspace from upstream + description: Update any workspace with the latest changes from its upstream workspace. + +permission_callbacks: + - Drupal\workspace\EntityAccess::workspacePermissions diff --git a/core/modules/workspace/workspace.routing.yml b/core/modules/workspace/workspace.routing.yml new file mode 100644 index 0000000000..da89e77d46 --- /dev/null +++ b/core/modules/workspace/workspace.routing.yml @@ -0,0 +1,25 @@ +entity.workspace.collection: + path: '/admin/structure/workspace' + defaults: + _title: 'Workspaces' + _entity_list: 'workspace' + requirements: + _permission: 'administer workspaces+edit_any_workspace' + +entity.workspace.activate_form: + path: '/admin/structure/workspace/{workspace}/activate' + defaults: + _title: 'Activate Workspace' + _form: '\Drupal\workspace\Form\WorkspaceActivateForm' + options: + _admin_route: TRUE + requirements: + _workspace_view: 'TRUE' + +workspace.deployment: + path: '/admin/config/workflow/deployment' + defaults: + _title: 'Workspace deployment' + _controller: '\Drupal\workspace\Controller\DeploymentController::workspaces' + requirements: + _permission: 'administer workspaces' \ No newline at end of file diff --git a/core/modules/workspace/workspace.services.yml b/core/modules/workspace/workspace.services.yml new file mode 100644 index 0000000000..1143559ee0 --- /dev/null +++ b/core/modules/workspace/workspace.services.yml @@ -0,0 +1,80 @@ +parameters: + workspace.default: live + workspace.factory.keyvalue.sorted_set: + default: keyvalue.sorted_set.database + +services: + workspace.paramconverter.entity_revision: + class: Drupal\workspace\ParamConverter\EntityRevisionConverter + arguments: ['@entity.manager'] + tags: + - { name: paramconverter, priority: 30 } + access_check.workspace_view: + class: Drupal\workspace\Access\WorkspaceViewCheck + tags: + - { name: access_check, applies_to: _workspace_view } + workspace.manager: + class: Drupal\workspace\WorkspaceManager + arguments: ['@request_stack', '@entity_type.manager', '@current_user', '@logger.channel.workspace'] + tags: + - { name: service_collector, tag: workspace_negotiator, call: addNegotiator } + workspace.index.sequence: + class: Drupal\workspace\Index\SequenceIndex + arguments: ['@workspace.keyvalue.sorted_set', '@workspace.manager'] + workspace.changes_factory: + class: Drupal\workspace\Changes\ChangesFactory + arguments: ['@entity_type.manager', '@workspace.index.sequence'] + workspace.keyvalue.sorted_set: + class: Drupal\workspace\KeyValueStore\KeyValueSortedSetFactory + arguments: ['@service_container', '%workspace.factory.keyvalue.sorted_set%'] + workspace.keyvalue.sorted_set.database: + class: Drupal\workspace\KeyValueStore\KeyValueDatabaseSortedSetFactory + arguments: ['@serialization.phpserialize', '@database'] + workspace.upstream_manager: + class: Drupal\workspace\UpstreamManager + parent: default_plugin_manager + + workspace.replication_manager: + class: Drupal\workspace\Replication\ReplicationManager + tags: + - { name: service_collector, tag: workspace_replicator, call: addReplicator } + workspace.default_replicator: + class: Drupal\workspace\Replication\DefaultReplicator + arguments: ['@workspace.manager', '@workspace.changes_factory', '@entity_type.manager', '@workspace.index.sequence'] + tags: + - { name: workspace_replicator, priority: 0 } + + workspace.negotiator.default: + class: Drupal\workspace\Negotiator\DefaultWorkspaceNegotiator + calls: + - [setContainer, ['@service_container']] + - [setCurrentUser, ['@current_user']] + - [setWorkspaceManager, ['@workspace.manager']] + tags: + - { name: workspace_negotiator, priority: 0 } + workspace.negotiator.session: + class: Drupal\workspace\Negotiator\SessionWorkspaceNegotiator + arguments: ['@user.private_tempstore'] + calls: + - [setContainer, ['@service_container']] + - [setCurrentUser, ['@current_user']] + - [setWorkspaceManager, ['@workspace.manager']] + tags: + - { name: workspace_negotiator, priority: 100 } + workspace.negotiator.param: + class: Drupal\workspace\Negotiator\ParamWorkspaceNegotiator + calls: + - [setContainer, ['@service_container']] + - [setCurrentUser, ['@current_user']] + - [setWorkspaceManager, ['@workspace.manager']] + tags: + - { name: workspace_negotiator, priority: 200 } + + cache_context.workspace: + class: Drupal\workspace\WorkspaceCacheContext + arguments: ['@workspace.manager'] + tags: + - { name: cache.context } + logger.channel.workspace: + parent: logger.channel_base + arguments: ['cron']