update modules
This commit is contained in:
parent
240bb618f3
commit
8d0209b367
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "drupal/back_to_top",
|
||||
"description": "Back To Top adds a button that hovers in the bottom of your screen and allow users to smoothly scroll up the page using jQuery.",
|
||||
"type": "drupal-module",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"homepage": "https://www.drupal.org/project/back_top_top",
|
||||
"support": {
|
||||
"issues": "https://www.drupal.org/project/issues/back_top_top",
|
||||
"source": "https://git.drupalcode.org/project/back_to_top"
|
||||
},
|
||||
"require": {
|
||||
"drupal/jquery_ui": "*",
|
||||
"drupal/jquery_ui_effects": "*"
|
||||
}
|
||||
}
|
@ -1,17 +1,14 @@
|
||||
name: Content Access
|
||||
type: module
|
||||
description: 'Provides flexible content access control.'
|
||||
core: 8.x
|
||||
core_version_requirement: ^8 || ^9
|
||||
core_version_requirement: ^8.8 || ^9
|
||||
package: 'Access control'
|
||||
|
||||
dependencies:
|
||||
- drupal:node
|
||||
|
||||
test_dependencies:
|
||||
- drupal:acl
|
||||
- acl:acl
|
||||
|
||||
# Information added by Drupal.org packaging script on 2020-09-17
|
||||
version: '8.x-1.0-alpha3'
|
||||
# Information added by Drupal.org packaging script on 2022-06-01
|
||||
version: '8.x-1.0-alpha4'
|
||||
project: 'content_access'
|
||||
datestamp: 1600327759
|
||||
datestamp: 1654070353
|
||||
|
@ -549,7 +549,7 @@ function content_access_delete_per_node_settings(NodeInterface $node) {
|
||||
->execute();
|
||||
|
||||
// Clear the cache.
|
||||
content_access_per_node_setting(NULL, $node, FALSE);
|
||||
content_access_per_node_setting(NULL, $node);
|
||||
|
||||
// Delete possible acl settings.
|
||||
if (\Drupal::moduleHandler()->moduleExists('acl')) {
|
||||
@ -718,6 +718,9 @@ function _content_access_get_node_permissions($type) {
|
||||
function content_access_get_role_gid($role) {
|
||||
$config = \Drupal::configFactory()->getEditable('content_access.settings');
|
||||
$roles_gids = $config->get('content_access_roles_gids');
|
||||
if (empty($roles_gids) || !array_key_exists($role, $roles_gids)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $roles_gids[$role];
|
||||
}
|
||||
|
@ -9,4 +9,4 @@ content_access_per_node:
|
||||
content_access_user_acl:
|
||||
label: 'User was added to ACL'
|
||||
category: 'Content Access'
|
||||
deriver: '\Drupal\content_access\Plugin\Deriver\RulesEventUserAclDeriver'
|
||||
provider: 'acl'
|
||||
|
@ -11,7 +11,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form.content-access-page,
|
||||
form.content-access-admin-settings {
|
||||
.content-access-page,
|
||||
.content-access-admin-settings {
|
||||
position: static;
|
||||
}
|
||||
|
@ -178,10 +178,10 @@ class ContentAccessAdminSettingsForm extends FormBase {
|
||||
if (content_access_mass_update([$node_type])) {
|
||||
$node_types = node_type_get_names();
|
||||
// This does not gurantee a rebuild.
|
||||
$this->messenger()->addMessage($this->t('Permissions have been changed for the content type @types.<br />You may have to <a href=":rebuild">rebuild permisions</a> for your changes to take effect.',
|
||||
$this->messenger()->addMessage($this->t('Permissions have been changed for the content type @types.<br />You may have to <a href=":rebuild">rebuild permissions</a> for your changes to take effect.',
|
||||
[
|
||||
'@types' => $node_types[$node_type],
|
||||
':rebuild' => Url::FromRoute('node.configure_rebuild_confirm')->ToString(),
|
||||
':rebuild' => Url::fromRoute('node.configure_rebuild_confirm')->toString(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
@ -172,11 +172,11 @@ class ContentAccessPageForm extends FormBase {
|
||||
content_access_save_per_node_settings($node, $settings);
|
||||
|
||||
if ($this->moduleHandler->moduleExists('acl')) {
|
||||
$values = $form_state->getValues();
|
||||
foreach (['view', 'update', 'delete'] as $op) {
|
||||
$values = $form_state->getValues();
|
||||
acl_save_form($values['acl'][$op]);
|
||||
$this->moduleHandler->invokeAll('user_acl', $settings);
|
||||
}
|
||||
$this->moduleHandler->invokeAll('user_acl', $settings);
|
||||
}
|
||||
|
||||
// Apply new settings.
|
||||
@ -190,8 +190,8 @@ class ContentAccessPageForm extends FormBase {
|
||||
// xxxx
|
||||
// route: node.configure_rebuild_confirm:
|
||||
// path: '/admin/reports/status/rebuild'.
|
||||
$this->messenger()->addMessage($this->t('Your changes have been saved. You may have to <a href=":rebuild">rebuild permisions</a> for your changes to take effect.',
|
||||
[':rebuild' => Url::FromRoute('node.configure_rebuild_confirm')->ToString()]));
|
||||
$this->messenger()->addMessage($this->t('Your changes have been saved. You may have to <a href=":rebuild">rebuild permissions</a> for your changes to take effect.',
|
||||
[':rebuild' => Url::fromRoute('node.configure_rebuild_confirm')->toString()]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_access\Plugin\Deriver;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Derives Content Access User plugin definitions.
|
||||
*/
|
||||
class RulesActionUserAclDeriver extends DeriverBase implements ContainerDeriverInterface {
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* Creates a new RulesActionUserAclDeriver object.
|
||||
*
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module_handler.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The string translation service.
|
||||
*/
|
||||
public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation) {
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->stringTranslation = $string_translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static($container->get('module_handler'), $container->get('string_translation'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
if ($this->moduleHandler->moduleExists('acl')) {
|
||||
$id = $base_plugin_definition['id'];
|
||||
$this->derivatives[$id] = $base_plugin_definition;
|
||||
}
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_access\Plugin\Deriver;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Derives 'User was added to ACL' plugin definition.
|
||||
*/
|
||||
class RulesEventUserAclDeriver extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* Creates a new RulesEventUserAclDeriver object.
|
||||
*
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
*/
|
||||
public function __construct(ModuleHandlerInterface $module_handler) {
|
||||
$this->moduleHandler = $module_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static($container->get('module_handler'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
if ($this->moduleHandler->moduleExists('acl')) {
|
||||
$this->derivatives['content_access_user_acl'] = $base_plugin_definition;
|
||||
}
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
@ -10,19 +10,11 @@ use Psr\Log\LogLevel;
|
||||
*/
|
||||
trait ActionCommonTrait {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
* The entity type manager.
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Verifies that per content settings are activated for the given node.
|
||||
*/
|
||||
protected function checkSetting(NodeInterface $node) {
|
||||
$config = $this->configFactory->getEditable('content_access.settings');
|
||||
$config = \Drupal::configFactory()->getEditable('content_access.settings');
|
||||
|
||||
$type = $node->getType();
|
||||
$settings = unserialize($config->get('content_access_node_type.' . $type));
|
||||
@ -56,7 +48,7 @@ trait ActionCommonTrait {
|
||||
* Apply the new grants to the affected node.
|
||||
*/
|
||||
protected function aquireGrants(NodeInterface $node) {
|
||||
$this->entityTypeManager->getAccessControlHandler('node')->writeGrants($node);
|
||||
\Drupal::entityTypeManager()->getAccessControlHandler('node')->writeGrants($node);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
* id = "content_access_action_grant_node_permissions",
|
||||
* label = @Translation("Grant access by role"),
|
||||
* category = @Translation("Content Access"),
|
||||
* context = {
|
||||
* context_definitions = {
|
||||
* "node" = @ContextDefinition("entity:node",
|
||||
* label = @Translation("Content"),
|
||||
* description = @Translation("Grant access to the following content.")
|
||||
@ -26,7 +26,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @todo: Add option_list parameter to permissions after it becomes available.
|
||||
* @todo Add option_list parameter to permissions after it becomes available.
|
||||
*/
|
||||
class ActionGrantNodePermissions extends RulesActionBase implements ContainerFactoryPluginInterface {
|
||||
use ActionCommonTrait;
|
||||
|
@ -11,7 +11,7 @@ use Drupal\rules\Core\RulesActionBase;
|
||||
* id = "content_access_action_reset_node_permissions",
|
||||
* label = @Translation("Reset access to content type defaults"),
|
||||
* category = @Translation("Content Access"),
|
||||
* context = {
|
||||
* context_definitions = {
|
||||
* "node" = @ContextDefinition("entity:node",
|
||||
* label = @Translation("Content"),
|
||||
* description = @Translation("Reset node permissions to default permissions.")
|
||||
|
@ -14,7 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
* id = "content_access_action_revoke_node_permissions",
|
||||
* label = @Translation("Revoke access by role"),
|
||||
* category = @Translation("Content Access"),
|
||||
* context = {
|
||||
* context_definitions = {
|
||||
* "node" = @ContextDefinition("entity:node",
|
||||
* label = @Translation("Content"),
|
||||
* description = @Translation("Revoke access from the following content.")
|
||||
@ -26,7 +26,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @todo: Add option_list parameter to permissions after it becomes available.
|
||||
* @todo Add option_list parameter to permissions after it becomes available.
|
||||
*/
|
||||
class ActionRevokeNodePermissions extends RulesActionBase implements ContainerFactoryPluginInterface {
|
||||
use ActionCommonTrait;
|
||||
|
@ -9,7 +9,8 @@ namespace Drupal\content_access\Plugin\RulesAction;
|
||||
* id = "content_access_action_user_grant",
|
||||
* label = @Translation("Grant access by user"),
|
||||
* category = @Translation("Content Access User"),
|
||||
* context = {
|
||||
* provider = "acl",
|
||||
* context_definitions = {
|
||||
* "node" = @ContextDefinition("entity:node",
|
||||
* label = @Translation("Content"),
|
||||
* description = @Translation("Grant access to the following content.")
|
||||
@ -28,9 +29,8 @@ namespace Drupal\content_access\Plugin\RulesAction;
|
||||
* label = @Translation("Grant delete access"),
|
||||
* description = @Translation("Grant delete access to the following user."),
|
||||
* required = FALSE
|
||||
* )
|
||||
* },
|
||||
* deriver = "\Drupal\content_access\Plugin\Deriver\RulesActionUserAclDeriver"
|
||||
* ),
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ActionUserGrant extends ContentAccessUserRulesActionBase {
|
||||
|
@ -9,7 +9,8 @@ namespace Drupal\content_access\Plugin\RulesAction;
|
||||
* id = "content_access_action_user_revoke",
|
||||
* label = @Translation("Revoke access by user"),
|
||||
* category = @Translation("Content Access User"),
|
||||
* context = {
|
||||
* provider = "acl",
|
||||
* context_definitions = {
|
||||
* "node" = @ContextDefinition("entity:node",
|
||||
* label = @Translation("Content"),
|
||||
* description = @Translation("Revoke access to the following content."),
|
||||
@ -28,9 +29,8 @@ namespace Drupal\content_access\Plugin\RulesAction;
|
||||
* label = @Translation("Revoke delete access"),
|
||||
* description = @Translation("Revoke delete access to the following user."),
|
||||
* required = FALSE
|
||||
* )
|
||||
* },
|
||||
* deriver = "\Drupal\content_access\Plugin\Deriver\RulesActionUserAclDeriver"
|
||||
* ),
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ActionUserRevoke extends ContentAccessUserRulesActionBase {
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace Drupal\Tests\content_access\Functional;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
@ -13,14 +12,11 @@ use Drupal\Tests\BrowserTestBase;
|
||||
*/
|
||||
class ContentAccessAclTest extends BrowserTestBase {
|
||||
use ContentAccessTestHelperTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['content_access', 'acl'];
|
||||
protected static $modules = ['content_access', 'acl'];
|
||||
|
||||
/**
|
||||
* A user with permission to non administer.
|
||||
@ -56,7 +52,7 @@ class ContentAccessAclTest extends BrowserTestBase {
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Setup configuration before each test.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
@ -123,18 +119,19 @@ class ContentAccessAclTest extends BrowserTestBase {
|
||||
$edit = [
|
||||
'acl[view][add]' => $this->testUser->getAccountName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $this->node1->id() . '/access', $edit, $this->t('edit-acl-view-add-button'));
|
||||
$this->drupalPostForm(NULL, [], $this->t('Submit'));
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/access');
|
||||
$this->submitForm($edit, 'edit-acl-view-add-button');
|
||||
$this->submitForm([], 'Submit');
|
||||
|
||||
// Logout admin, try to access the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view access should be allowed now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and disable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -143,12 +140,12 @@ class ContentAccessAclTest extends BrowserTestBase {
|
||||
// Logout admin, try to access the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view access should be denied now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,18 +165,19 @@ class ContentAccessAclTest extends BrowserTestBase {
|
||||
$edit = [
|
||||
'acl[update][add]' => $this->testUser->getAccountName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $this->node1->id() . '/access', $edit, $this->t('edit-acl-update-add-button'));
|
||||
$this->drupalPostForm(NULL, [], $this->t('Submit'));
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/access');
|
||||
$this->submitForm($edit, 'edit-acl-update-add-button');
|
||||
$this->submitForm([], 'Submit');
|
||||
|
||||
// Logout admin, try to edit the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit access should be allowed now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and disable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -188,12 +186,12 @@ class ContentAccessAclTest extends BrowserTestBase {
|
||||
// Logout admin, try to edit the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit access should be denied now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,18 +211,19 @@ class ContentAccessAclTest extends BrowserTestBase {
|
||||
$edit = [
|
||||
'acl[delete][add]' => $this->testUser->getAccountName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $this->node1->id() . '/access', $edit, $this->t('edit-acl-delete-add-button'));
|
||||
$this->drupalPostForm(NULL, [], $this->t('Submit'));
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/access');
|
||||
$this->submitForm($edit, 'edit-acl-delete-add-button');
|
||||
$this->submitForm([], 'Submit');
|
||||
|
||||
// Logout admin, try to delete the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete access should be allowed now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and disable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -233,12 +232,12 @@ class ContentAccessAclTest extends BrowserTestBase {
|
||||
// Logout admin, try to delete the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete access should be denied now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Drupal\Tests\content_access\Functional;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
@ -12,14 +11,11 @@ use Drupal\Tests\BrowserTestBase;
|
||||
*/
|
||||
class ContentAccessModuleTest extends BrowserTestBase {
|
||||
use ContentAccessTestHelperTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['content_access'];
|
||||
protected static $modules = ['content_access'];
|
||||
|
||||
/**
|
||||
* A user with permission to non administer.
|
||||
@ -64,7 +60,7 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create test user with separate role.
|
||||
@ -106,12 +102,12 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Logout admin and try to access the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view node, access must be denied.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login admin and grant access for viewing to the test user.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -121,12 +117,12 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// access must be denied again.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view node, access must be granted.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and enable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -138,18 +134,18 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Logout admin and try to access both nodes anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
$this->drupalGet('node/' . $this->node2->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view node1, access must be granted.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// View node2, access must be denied.
|
||||
$this->drupalGet('node/' . $this->node2->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login admin, swap permissions between content type and node2.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -163,18 +159,18 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Logout admin and try to access both nodes anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
$this->drupalGet('node/' . $this->node2->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view node1, access must be denied.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// View node2, access must be granted.
|
||||
$this->drupalGet('node/' . $this->node2->id());
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,12 +180,12 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Logout admin and try to edit the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit node, access must be denied.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login admin and grant access for editing to the test user.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -199,12 +195,12 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// access must be denied again.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit node, access must be granted.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and enable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -213,25 +209,25 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Restrict access for this content type for the test user.
|
||||
$this->changeAccessContentTypeKeyword('update', FALSE);
|
||||
|
||||
// Allow acces for node1 only.
|
||||
// Allow access for node1 only.
|
||||
$this->changeAccessNodeKeyword($this->node1, 'update');
|
||||
$this->changeAccessNodeKeyword($this->node2, 'update', FALSE);
|
||||
|
||||
// Logout admin and try to edit both nodes anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit node1, access must be granted.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Edit node2, access must be denied.
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login admin, swap permissions between node1 and node2.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -244,18 +240,18 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Logout admin and try to edit both nodes anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit node1, access must be denied.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Edit node2, access must be granted.
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/edit');
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -265,12 +261,12 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Logout admin and try to delete the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete node, access must be denied.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login admin and grant access for deleting to the test user.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -281,19 +277,16 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// access must be denied again.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete node, access must be granted.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalPostForm(
|
||||
'node/' . $this->node1->id() . '/delete',
|
||||
[],
|
||||
'Delete'
|
||||
);
|
||||
$this->assertRaw(
|
||||
$this->t('%node has been deleted', ['%node' => $this->node1->getTitle()]),
|
||||
'Test node was deleted successfully by test user'
|
||||
);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->submitForm([], 'Delete');
|
||||
|
||||
// Check that the test node was deleted successfully by testUser.
|
||||
$title = $this->node1->getTitle();
|
||||
$this->assertSession()->pageTextContains("$title has been deleted");
|
||||
|
||||
// Login admin and recreate test node1.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -307,49 +300,49 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Restrict access for this content type for the test user.
|
||||
$this->changeAccessContentTypeKeyword('delete', FALSE);
|
||||
|
||||
// Allow acces for node1 only.
|
||||
// Allow access for node1 only.
|
||||
$this->changeAccessNodeKeyword($this->node1, 'delete');
|
||||
$this->changeAccessNodeKeyword($this->node2, 'delete', FALSE);
|
||||
|
||||
// Logout admin and try to delete both nodes anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete node1, access must be granted.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Delete node2, access must be denied.
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login admin, swap permissions between node1 and node2.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Grant delete access to node2.
|
||||
$this->changeAccessNodeKeyword($this->node2, 'delete');
|
||||
// Restrict delete acces to node1.
|
||||
// Restrict delete access to node1.
|
||||
$this->changeAccessNodeKeyword($this->node1, 'delete', FALSE);
|
||||
|
||||
// Logout admin and try to delete both nodes anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete node1, access must be denied.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Delete node2, access must be granted.
|
||||
$this->drupalGet('node/' . $this->node2->id() . '/delete');
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -383,27 +376,27 @@ class ContentAccessModuleTest extends BrowserTestBase {
|
||||
// Logout admin and try to access both nodes anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
$this->drupalGet('node/' . $this->node2->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user 1, view node1, access must be granted.
|
||||
$this->drupalLogin($testUser1);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// View node2, access must be denied.
|
||||
$this->drupalGet('node/' . $this->node2->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user 2, view node1, access must be denied.
|
||||
$this->drupalLogin($testUser2);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// View node2, access must be granted.
|
||||
$this->drupalGet('node/' . $this->node2->id());
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,14 +22,11 @@ trait ContentAccessTestHelperTrait {
|
||||
* Change access permissions for a content type.
|
||||
*/
|
||||
public function changeAccessContentType($accessSettings) {
|
||||
$this->drupalPostForm(
|
||||
'admin/structure/types/manage/' . $this->contentType->id() . '/access',
|
||||
$accessSettings,
|
||||
t('Submit')
|
||||
);
|
||||
$this->drupalGet('admin/structure/types/manage/' . $this->contentType->id() . '/access');
|
||||
$this->submitForm($accessSettings, 'Submit');
|
||||
// Both these may be printed:
|
||||
// 'Permissions have been changed' || 'No change' => 'change'.
|
||||
$this->assertSession()->pageTextContains(t('change'));
|
||||
$this->assertSession()->pageTextContains('change');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,8 +92,9 @@ trait ContentAccessTestHelperTrait {
|
||||
* Change access permission for a node.
|
||||
*/
|
||||
public function changeAccessNode(NodeInterface $node, $accessSettings) {
|
||||
$this->drupalPostForm('node/' . $node->id() . '/access', $accessSettings, t('Submit'));
|
||||
$this->assertSession()->pageTextContains(t('Your changes have been saved.'));
|
||||
$this->drupalGet('node/' . $node->id() . '/access');
|
||||
$this->submitForm($accessSettings, 'Submit');
|
||||
$this->assertSession()->pageTextContains('Your changes have been saved.');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace Drupal\Tests\content_access\Functional;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
@ -13,14 +12,11 @@ use Drupal\Tests\BrowserTestBase;
|
||||
*/
|
||||
class ContentAccessTinyTest extends BrowserTestBase {
|
||||
use ContentAccessTestHelperTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['content_access', 'acl'];
|
||||
protected static $modules = ['content_access', 'acl'];
|
||||
|
||||
/**
|
||||
* A user with permission to non administer.
|
||||
@ -56,7 +52,7 @@ class ContentAccessTinyTest extends BrowserTestBase {
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Setup configuration before each test.
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
@ -123,18 +119,19 @@ class ContentAccessTinyTest extends BrowserTestBase {
|
||||
$edit = [
|
||||
'acl[view][add]' => $this->testUser->getAccountName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $this->node1->id() . '/access', $edit, $this->t('Add User'));
|
||||
$this->drupalPostForm(NULL, [], $this->t('Submit'));
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/access');
|
||||
$this->submitForm($edit, 'Add User');
|
||||
$this->submitForm([], 'Submit');
|
||||
|
||||
// Logout admin, try to access the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view access should be allowed now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextNotContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and disable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -143,12 +140,12 @@ class ContentAccessTinyTest extends BrowserTestBase {
|
||||
// Logout admin, try to access the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, view access should be denied now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id());
|
||||
$this->assertSession()->pageTextContains($this->t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
}
|
||||
|
||||
/*
|
||||
@ -169,19 +166,19 @@ class ContentAccessTinyTest extends BrowserTestBase {
|
||||
$edit = [
|
||||
'acl[update][add]' => $this->testUser->getAccountName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $this->node1->id() . '/access', $edit,
|
||||
t('Add User'));
|
||||
$this->drupalPostForm(NULL, [], t('Submit'));
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/access');
|
||||
$this->submitForm($edit, 'Add User');
|
||||
$this->submitForm([], 'Submit');
|
||||
|
||||
// Logout admin, try to edit the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit access should be allowed now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextNotContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and disable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -190,12 +187,12 @@ class ContentAccessTinyTest extends BrowserTestBase {
|
||||
// Logout admin, try to edit the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, edit access should be denied now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/edit');
|
||||
$this->assertSession()->pageTextContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
}
|
||||
*/
|
||||
|
||||
@ -217,19 +214,19 @@ class ContentAccessTinyTest extends BrowserTestBase {
|
||||
$edit = [
|
||||
'acl[delete][add]' => $this->testUser->getAccountName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $this->node1->id() . '/access', $edit,
|
||||
t('Add User'));
|
||||
$this->drupalPostForm(NULL, [], t('Submit'));
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/access');
|
||||
$this->submitForm($edit, 'Add User');
|
||||
$this->submitForm([], 'Submit');
|
||||
|
||||
// Logout admin, try to delete the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete access should be allowed now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextNotContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextNotContains('Access denied');
|
||||
|
||||
// Login admin and disable per node access.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
@ -238,12 +235,12 @@ class ContentAccessTinyTest extends BrowserTestBase {
|
||||
// Logout admin, try to delete the node anonymously.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
|
||||
// Login test user, delete access should be denied now.
|
||||
$this->drupalLogin($this->testUser);
|
||||
$this->drupalGet('node/' . $this->node1->id() . '/delete');
|
||||
$this->assertSession()->pageTextContains(t('Access denied'));
|
||||
$this->assertSession()->pageTextContains('Access denied');
|
||||
}
|
||||
*/
|
||||
|
||||
|
339
frontend/drupal9/web/modules/contrib/entity_clone/LICENSE.txt
Normal file
339
frontend/drupal9/web/modules/contrib/entity_clone/LICENSE.txt
Normal file
@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
14
frontend/drupal9/web/modules/contrib/entity_clone/README.txt
Normal file
14
frontend/drupal9/web/modules/contrib/entity_clone/README.txt
Normal file
@ -0,0 +1,14 @@
|
||||
README.txt
|
||||
==========
|
||||
|
||||
What is this module?
|
||||
--------------------
|
||||
|
||||
This module add a new entity operation which allows to clone many
|
||||
of the entities (config & content) provided by the Drupal core.
|
||||
|
||||
How does it work?
|
||||
-----------------
|
||||
|
||||
The module add a new operation on each entity in all manage lists.
|
||||
Just click on the clone button to process the clone operation.
|
@ -0,0 +1,88 @@
|
||||
entity_clone.settings:
|
||||
type: config_object
|
||||
label: 'Module entity_clone settings'
|
||||
mapping:
|
||||
form_settings:
|
||||
type: mapping
|
||||
mapping:
|
||||
taxonomy_term:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
block_content:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
comment:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
contact_message:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
file:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
node:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
shortcut:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
user:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
||||
menu_link_content:
|
||||
type: mapping
|
||||
mapping:
|
||||
default_value:
|
||||
type: boolean
|
||||
disable:
|
||||
type: boolean
|
||||
hidden:
|
||||
type: boolean
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Entity Clone hooks and events.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event subscribers for Entity Clone.
|
||||
*
|
||||
* Service definition for my_module.services.yml:
|
||||
* <code>
|
||||
* ```yaml
|
||||
* my_module.my_event_subscriber:
|
||||
* class: Drupal\my_module\EventSubscriber\MyEntityCloneEventSubscriber
|
||||
* tags:
|
||||
* - { name: event_subscriber }
|
||||
* ```
|
||||
* </code>
|
||||
*
|
||||
* Code for src/EventSubscriber/MyEntityCloneEventSubscriber.php
|
||||
* <code>
|
||||
* <?php
|
||||
* namespace Drupal\my_module\EventSubscriber;
|
||||
* ?>
|
||||
* </code>
|
||||
*/
|
||||
class MyEntityCloneEventSubscriber implements \Symfony\Component\EventDispatcher\EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* An example event subscriber.
|
||||
*
|
||||
* Dispatched before an entity is cloned and saved.
|
||||
*
|
||||
* @see \Drupal\entity_clone\Event\EntityCloneEvents::PRE_CLONE
|
||||
*/
|
||||
public function myPreClone(\Drupal\entity_clone\Event\EntityCloneEvent $event): void {
|
||||
$original = $event->getEntity();
|
||||
$newEntity = $event->getClonedEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* An example event subscriber.
|
||||
*
|
||||
* Dispatched after an entity is cloned and saved.
|
||||
*
|
||||
* @see \Drupal\entity_clone\Event\EntityCloneEvents::POST_CLONE
|
||||
*/
|
||||
public function myPostClone(\Drupal\entity_clone\Event\EntityCloneEvent $event): void {
|
||||
$original = $event->getEntity();
|
||||
$newEntity = $event->getClonedEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents(): array {
|
||||
$events[\Drupal\entity_clone\Event\EntityCloneEvents::PRE_CLONE][] = ['myPreClone'];
|
||||
$events[\Drupal\entity_clone\Event\EntityCloneEvents::POST_CLONE][] = ['myPostClone'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
name: Entity Clone
|
||||
description: Add a clone action for all entities
|
||||
core: "8.x"
|
||||
core_version_requirement: ^8 || ^9
|
||||
type: module
|
||||
configure: entity_clone.settings
|
||||
|
||||
# Information added by Drupal.org packaging script on 2021-03-31
|
||||
version: '8.x-1.0-beta6'
|
||||
project: 'entity_clone'
|
||||
datestamp: 1617210000
|
@ -0,0 +1,5 @@
|
||||
entity_clone.settings:
|
||||
title: 'Entity clone settings'
|
||||
description: 'Entity clone settings.'
|
||||
route_name: entity_clone.settings
|
||||
parent: 'system.admin_config_content'
|
@ -0,0 +1,8 @@
|
||||
entity_clone.clone:
|
||||
deriver: 'Drupal\entity_clone\Plugin\Derivative\DynamicLocalTasks'
|
||||
weight: 100
|
||||
entity_clone.settings:
|
||||
title: 'Entity clone settings'
|
||||
route_name: entity_clone.settings
|
||||
base_route: entity_clone.settings
|
||||
weight: 0
|
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains entity_clone.module.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\entity_clone\EntityClone\Config\ConfigEntityCloneBase;
|
||||
use Drupal\entity_clone\EntityClone\Config\ConfigEntityCloneFormBase;
|
||||
use Drupal\entity_clone\EntityClone\Config\ConfigWithFieldEntityClone;
|
||||
use Drupal\entity_clone\EntityClone\Config\FieldConfigEntityClone;
|
||||
use Drupal\entity_clone\EntityClone\Config\MenuEntityClone;
|
||||
use Drupal\entity_clone\EntityClone\Config\MenuEntityCloneForm;
|
||||
use Drupal\entity_clone\EntityClone\Content\ContentEntityCloneBase;
|
||||
use Drupal\entity_clone\EntityClone\Content\ContentEntityCloneFormBase;
|
||||
use Drupal\entity_clone\EntityClone\Content\FileEntityClone;
|
||||
use Drupal\entity_clone\EntityClone\Content\TaxonomyTermEntityClone;
|
||||
use Drupal\entity_clone\EntityClone\Content\UserEntityClone;
|
||||
use Drupal\entity_clone\EntityClone\Config\LayoutBuilderEntityClone;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function entity_clone_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
// Main module help for the entity_clone module.
|
||||
case 'help.page.entity_clone':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('Provides a new operation to clone all Entities.') . '</p>';
|
||||
return $output;
|
||||
|
||||
default:
|
||||
return;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_type_build().
|
||||
*/
|
||||
function entity_clone_entity_type_build(array &$entity_types) {
|
||||
$specific_handler = [
|
||||
'file' => [
|
||||
'entity_clone' => FileEntityClone::class,
|
||||
],
|
||||
'user' => [
|
||||
'entity_clone' => UserEntityClone::class,
|
||||
],
|
||||
'field_config' => [
|
||||
'entity_clone' => FieldConfigEntityClone::class,
|
||||
],
|
||||
'node_type' => [
|
||||
'entity_clone' => ConfigWithFieldEntityClone::class,
|
||||
],
|
||||
'comment_type' => [
|
||||
'entity_clone' => ConfigWithFieldEntityClone::class,
|
||||
],
|
||||
'block_content_type' => [
|
||||
'entity_clone' => ConfigWithFieldEntityClone::class,
|
||||
],
|
||||
'contact_form' => [
|
||||
'entity_clone' => ConfigWithFieldEntityClone::class,
|
||||
],
|
||||
'taxonomy_term' => [
|
||||
'entity_clone' => TaxonomyTermEntityClone::class,
|
||||
],
|
||||
'menu' => [
|
||||
'entity_clone_form' => MenuEntityCloneForm::class,
|
||||
'entity_clone' => MenuEntityClone::class,
|
||||
],
|
||||
'taxonomy_vocabulary' => [
|
||||
'entity_clone' => ConfigWithFieldEntityClone::class,
|
||||
],
|
||||
'entity_view_display' => [
|
||||
'entity_clone' => \Drupal::moduleHandler()->moduleExists('layout_builder') ? LayoutBuilderEntityClone::class : NULL,
|
||||
],
|
||||
];
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
|
||||
foreach ($entity_types as $entity_type_id => $entity_type) {
|
||||
$has_entity_clone_handler = $entity_type->getHandlerClass('entity_clone');
|
||||
if (!$has_entity_clone_handler) {
|
||||
if ($entity_type instanceof ContentEntityTypeInterface) {
|
||||
$entity_type->setHandlerClass('entity_clone', ContentEntityCloneBase::class);
|
||||
$entity_type->setHandlerClass('entity_clone_form', ContentEntityCloneFormBase::class);
|
||||
}
|
||||
elseif ($entity_type instanceof ConfigEntityTypeInterface) {
|
||||
$entity_type->setHandlerClass('entity_clone', ConfigEntityCloneBase::class);
|
||||
$entity_type->setHandlerClass('entity_clone_form', ConfigEntityCloneFormBase::class);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($specific_handler[$entity_type->id()]['entity_clone'])) {
|
||||
$entity_type->setHandlerClass('entity_clone', $specific_handler[$entity_type->id()]['entity_clone']);
|
||||
}
|
||||
if (isset($specific_handler[$entity_type->id()]['entity_clone_form'])) {
|
||||
$entity_type->setHandlerClass('entity_clone_form', $specific_handler[$entity_type->id()]['entity_clone_form']);
|
||||
}
|
||||
|
||||
$entity_type->setLinkTemplate('clone-form', "/entity_clone/$entity_type_id/{{$entity_type_id}}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares entity operations.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity on which the linked operations will be performed.
|
||||
*
|
||||
* @return array
|
||||
* An operations array as returned by
|
||||
* EntityListBuilderInterface::getOperations().
|
||||
*
|
||||
* @see \Drupal\Core\Entity\EntityListBuilderInterface::getOperations()
|
||||
*/
|
||||
function entity_clone_entity_operation(EntityInterface $entity) {
|
||||
if ($entity->hasLinkTemplate('clone-form') && $entity->access('clone')) {
|
||||
return [
|
||||
'clone' => [
|
||||
'title' => t('Clone'),
|
||||
'weight' => 50,
|
||||
'url' => $entity->toUrl('clone-form')
|
||||
->mergeOptions(['query' => \Drupal::destination()->getAsArray()]),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_access().
|
||||
*/
|
||||
function entity_clone_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
if ($operation === 'clone') {
|
||||
return AccessResult::allowedIfHasPermission($account, 'clone ' . $entity->getEntityTypeId() . ' entity');
|
||||
}
|
||||
return AccessResult::neutral();
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
administer entity clone:
|
||||
title: 'Administer entity clone'
|
||||
|
||||
permission_callbacks:
|
||||
- Drupal\entity_clone\EntityClonePermissions::permissions
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains entity_clone.post_update.php.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Populates new entity_clone form settings.
|
||||
*/
|
||||
function entity_clone_post_update_populate_form_settings3() {
|
||||
$form_settings = \Drupal::configFactory()->get('entity_clone.settings')->get('form_settings');
|
||||
if (!$form_settings) {
|
||||
/** @var \Drupal\entity_clone\EntityCloneSettingsManager $entity_clone_settings_manager */
|
||||
$entity_clone_settings_manager = \Drupal::service('entity_clone.settings.manager');
|
||||
$form_settings = [];
|
||||
foreach (array_keys($entity_clone_settings_manager->getContentEntityTypes()) as $entity_type_id) {
|
||||
$form_settings[$entity_type_id] = [
|
||||
'default_value' => FALSE,
|
||||
'disable' => FALSE,
|
||||
'hidden' => FALSE,
|
||||
];
|
||||
}
|
||||
|
||||
\Drupal::configFactory()->getEditable('entity_clone.settings')->set('form_settings', $form_settings)->save();
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
entity_clone.settings:
|
||||
path: '/admin/config/system/entity-clone'
|
||||
defaults:
|
||||
_form: 'Drupal\entity_clone\Form\EntityCloneSettingsForm'
|
||||
_title: 'Entity clone settings'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'administer entity clone'
|
@ -0,0 +1,12 @@
|
||||
services:
|
||||
entity_clone.settings.manager:
|
||||
class: Drupal\entity_clone\EntityCloneSettingsManager
|
||||
arguments: ['@entity_type.manager', '@entity_type.bundle.info', '@config.factory']
|
||||
entity_clone.route_subscriber:
|
||||
class: Drupal\entity_clone\Routing\RouteSubscriber
|
||||
arguments: ['@entity_type.manager']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
entity_clone.service_provider:
|
||||
class: Drupal\entity_clone\Services\EntityCloneServiceProvider
|
||||
arguments: [ ]
|
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Config;
|
||||
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\entity_clone\EntityClone\EntityCloneInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class ConfigEntityCloneBase.
|
||||
*/
|
||||
class ConfigEntityCloneBase implements EntityHandlerInterface, EntityCloneInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity type ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId;
|
||||
|
||||
/**
|
||||
* Constructs a new ConfigEntityCloneBase.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityTypeId = $entity_type_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$entity_type->id()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = []) {
|
||||
/** @var \Drupal\core\Config\Entity\ConfigEntityInterface $cloned_entity */
|
||||
$id_key = $this->entityTypeManager->getDefinition($this->entityTypeId)->getKey('id');
|
||||
$label_key = $this->entityTypeManager->getDefinition($this->entityTypeId)->getKey('label');
|
||||
|
||||
// Set new entity properties.
|
||||
if (isset($properties['id'])) {
|
||||
if ($id_key) {
|
||||
$cloned_entity->set($id_key, $properties['id']);
|
||||
}
|
||||
unset($properties['id']);
|
||||
}
|
||||
|
||||
if (isset($properties['label'])) {
|
||||
if ($label_key) {
|
||||
$cloned_entity->set($label_key, $properties['label']);
|
||||
}
|
||||
unset($properties['label']);
|
||||
}
|
||||
|
||||
foreach ($properties as $key => $property) {
|
||||
$cloned_entity->set($key, $property);
|
||||
}
|
||||
|
||||
$cloned_entity->save();
|
||||
return $cloned_entity;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Config;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityStorage;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\StringTranslation\TranslationManager;
|
||||
use Drupal\entity_clone\EntityClone\EntityCloneFormInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class ConfigEntityCloneFormBase.
|
||||
*/
|
||||
class ConfigEntityCloneFormBase implements EntityHandlerInterface, EntityCloneFormInterface {
|
||||
|
||||
/**
|
||||
* The string translation.
|
||||
*
|
||||
* @var \Drupal\Core\StringTranslation\TranslationManager
|
||||
*/
|
||||
protected $translationManager;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a new ConfigEntityCloneFormBase.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationManager $translation_manager
|
||||
* The string translation manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationManager $translation_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->translationManager = $translation_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('string_translation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(EntityInterface $entity, $parent = TRUE) {
|
||||
$form = [];
|
||||
|
||||
if ($this->entityTypeManager->getDefinition($entity->getEntityTypeId())->getKey('label')) {
|
||||
$form['label'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->translationManager->translate('New Label'),
|
||||
'#maxlength' => 255,
|
||||
'#required' => TRUE,
|
||||
];
|
||||
}
|
||||
|
||||
// In common casse, config entities IDs are limited to 64 characters ...
|
||||
$max_length = 64;
|
||||
if ($entity->getEntityType()->getBundleOf()) {
|
||||
// ... Except for bundle definition, that are limited to 32 characters.
|
||||
$max_length = EntityTypeInterface::BUNDLE_MAX_LENGTH;
|
||||
}
|
||||
|
||||
$form['id'] = [
|
||||
'#type' => 'machine_name',
|
||||
'#title' => $this->translationManager->translate('New Id'),
|
||||
'#maxlength' => $max_length,
|
||||
'#required' => TRUE,
|
||||
];
|
||||
|
||||
// If entity must have a prefix
|
||||
// (e.g. entity_form_mode, entity_view_mode, ...).
|
||||
if (method_exists($entity, 'getTargetType')) {
|
||||
$form['id']['#field_prefix'] = $entity->getTargetType() . '.';
|
||||
}
|
||||
|
||||
if (method_exists($entity, 'load')) {
|
||||
$form['id']['#machine_name'] = [
|
||||
'exists' => get_class($entity) . '::load',
|
||||
];
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValues(FormStateInterface $form_state) {
|
||||
// If entity must have a prefix
|
||||
// (e.g. entity_form_mode, entity_view_mode, ...).
|
||||
$field_prefix = '';
|
||||
if (isset($form_state->getCompleteForm()['id']['#field_prefix'])) {
|
||||
$field_prefix = $form_state->getCompleteForm()['id']['#field_prefix'];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $field_prefix . $form_state->getValue('id'),
|
||||
'label' => $form_state->getValue('label'),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Config;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Field\FieldConfigInterface;
|
||||
|
||||
/**
|
||||
* Class ContentEntityCloneBase.
|
||||
*/
|
||||
class ConfigWithFieldEntityClone extends ConfigEntityCloneBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = []) {
|
||||
$cloned_entity = parent::cloneEntity($entity, $cloned_entity, $properties);
|
||||
$bundle_of = $cloned_entity->getEntityType()->getBundleOf();
|
||||
if ($bundle_of) {
|
||||
$this->cloneFields($entity->id(), $cloned_entity->id(), $bundle_of);
|
||||
}
|
||||
|
||||
$view_displays = \Drupal::service('entity_display.repository')->getFormModes($bundle_of);
|
||||
$view_displays = array_merge($view_displays, ['default' => 'default']);
|
||||
if (!empty($view_displays)) {
|
||||
$this->cloneDisplays('form', $entity->id(), $cloned_entity->id(), $view_displays, $bundle_of);
|
||||
}
|
||||
|
||||
$view_displays = \Drupal::service('entity_display.repository')->getViewModes($bundle_of);
|
||||
$view_displays = array_merge($view_displays, ['default' => 'default']);
|
||||
if (!empty($view_displays)) {
|
||||
$this->cloneDisplays('view', $entity->id(), $cloned_entity->id(), $view_displays, $bundle_of);
|
||||
}
|
||||
|
||||
return $cloned_entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone all fields. Each field re-use existing field storage.
|
||||
*
|
||||
* @param string $entity_id
|
||||
* The base entity ID.
|
||||
* @param string $cloned_entity_id
|
||||
* The cloned entity ID.
|
||||
* @param string $bundle_of
|
||||
* The bundle of the cloned entity.
|
||||
*/
|
||||
protected function cloneFields($entity_id, $cloned_entity_id, $bundle_of) {
|
||||
/** @var \Drupal\Core\Entity\EntityFieldManager $field_manager */
|
||||
$field_manager = \Drupal::service('entity_field.manager');
|
||||
$fields = $field_manager->getFieldDefinitions($bundle_of, $entity_id);
|
||||
foreach ($fields as $field_definition) {
|
||||
if ($field_definition instanceof FieldConfigInterface) {
|
||||
if ($this->entityTypeManager->hasHandler($this->entityTypeManager->getDefinition($field_definition->getEntityTypeId())
|
||||
->id(), 'entity_clone')
|
||||
) {
|
||||
/** @var \Drupal\entity_clone\EntityClone\EntityCloneInterface $field_config_clone_handler */
|
||||
$field_config_clone_handler = $this->entityTypeManager->getHandler($this->entityTypeManager->getDefinition($field_definition->getEntityTypeId())
|
||||
->id(), 'entity_clone');
|
||||
$field_config_properties = [
|
||||
'id' => $field_definition->getName(),
|
||||
'label' => $field_definition->label(),
|
||||
'skip_storage' => TRUE,
|
||||
];
|
||||
$cloned_field_definition = $field_definition->createDuplicate();
|
||||
$cloned_field_definition->set('bundle', $cloned_entity_id);
|
||||
$field_config_clone_handler->cloneEntity($field_definition, $cloned_field_definition, $field_config_properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone all fields. Each field re-use existing field storage.
|
||||
*
|
||||
* @param string $type
|
||||
* The type of display (view or form).
|
||||
* @param string $entity_id
|
||||
* The base entity ID.
|
||||
* @param string $cloned_entity_id
|
||||
* The cloned entity ID.
|
||||
* @param array $view_displays
|
||||
* All view available display for this type.
|
||||
* @param string $bundle_of
|
||||
* The bundle of the cloned entity.
|
||||
*/
|
||||
protected function cloneDisplays($type, $entity_id, $cloned_entity_id, array $view_displays, $bundle_of) {
|
||||
foreach ($view_displays as $view_display_id => $view_display) {
|
||||
/** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $display */
|
||||
$display = $this->entityTypeManager->getStorage('entity_' . $type . '_display')->load($bundle_of . '.' . $entity_id . '.' . $view_display_id);
|
||||
if ($display) {
|
||||
/** @var \Drupal\entity_clone\EntityClone\EntityCloneInterface $view_display_clone_handler */
|
||||
$view_display_clone_handler = $this->entityTypeManager->getHandler($this->entityTypeManager->getDefinition($display->getEntityTypeId())
|
||||
->id(), 'entity_clone');
|
||||
$view_display_properties = [
|
||||
'id' => $bundle_of . '.' . $cloned_entity_id . '.' . $view_display_id,
|
||||
];
|
||||
$cloned_view_display = $display->createDuplicate();
|
||||
$cloned_view_display->set('bundle', $cloned_entity_id);
|
||||
$view_display_clone_handler->cloneEntity($display, $cloned_view_display, $view_display_properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Config;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Class FieldConfigEntityClone.
|
||||
*/
|
||||
class FieldConfigEntityClone extends ConfigEntityCloneBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $field_config, EntityInterface $cloned_field_config, array $properties = []) {
|
||||
/** @var \Drupal\field\Entity\FieldConfig $field_config */
|
||||
/** @var \Drupal\field\Entity\FieldConfig $cloned_field_config */
|
||||
/** @var \Drupal\field\Entity\FieldStorageConfig $cloned_field_storage */
|
||||
|
||||
if ((!isset($properties['skip_storage']) || !$properties['skip_storage'])) {
|
||||
$cloned_field_storage = $field_config->getFieldStorageDefinition()->createDuplicate();
|
||||
$cloned_field_storage->set('field_name', $properties['id']);
|
||||
$cloned_field_storage->set('id', $properties['id'] . '.' . $cloned_field_storage->getTargetEntityTypeId());
|
||||
$cloned_field_storage->save();
|
||||
}
|
||||
unset($properties['skip_storage']);
|
||||
|
||||
$properties['field_name'] = $properties['id'];
|
||||
$properties['id'] = $cloned_field_config->getTargetEntityTypeId() . '.' . $cloned_field_config->getTargetBundle() . '.' . $properties['id'];
|
||||
return parent::cloneEntity($field_config, $cloned_field_config, $properties);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Config;
|
||||
|
||||
use Drupal\Component\Plugin\DerivativeInspectionInterface;
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
use Drupal\Component\Uuid\UuidInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class LayoutBuilderEntityView.
|
||||
*/
|
||||
class LayoutBuilderEntityClone extends ConfigEntityCloneBase {
|
||||
|
||||
/**
|
||||
* Uuid generator service.
|
||||
*
|
||||
* @var \Drupal\Component\Uuid\UuidInterface
|
||||
*/
|
||||
protected $uuidGenerator;
|
||||
|
||||
/**
|
||||
* LayoutBuilderEntityClone constructor.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id, UuidInterface $uuid) {
|
||||
parent::__construct($entity_type_manager, $entity_type_id);
|
||||
$this->uuidGenerator = $uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$entity_type->id(),
|
||||
$container->get('uuid')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = []) {
|
||||
/** @var $cloned_entity \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface */
|
||||
/** @var $entity \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface */
|
||||
// We need to create an entity, save it, then adjust layout builder settings
|
||||
// and save it again, because for new entities layout_builder module stacks
|
||||
// all fields into display.
|
||||
// @see \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::preSave()
|
||||
$cloned_entity = parent::cloneEntity($entity, $cloned_entity, $properties);
|
||||
if ($cloned_entity->isLayoutBuilderEnabled()) {
|
||||
$cloned_entity->removeAllSections();
|
||||
foreach ($entity->getSections() as $section) {
|
||||
$cloned_components = [];
|
||||
foreach ($section->getComponents() as $section_component) {
|
||||
$plugin = $section_component->getPlugin();
|
||||
$component_array = $section_component->toArray();
|
||||
$deriver_id = $plugin->getPluginDefinition()['id'];
|
||||
switch ($deriver_id) {
|
||||
case 'field_block':
|
||||
case 'extra_field_block':
|
||||
$full_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin->getPluginId());
|
||||
$field_name = end($full_id);
|
||||
$derivative_id = $cloned_entity->getTargetEntityTypeId() . PluginBase::DERIVATIVE_SEPARATOR . $cloned_entity->getTargetBundle() . PluginBase::DERIVATIVE_SEPARATOR . $field_name;
|
||||
break;
|
||||
|
||||
case 'inline_block':
|
||||
$derivative_id = $plugin->getDerivativeId();
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($plugin instanceof DerivativeInspectionInterface) {
|
||||
$derivative_id = $plugin->getDerivativeId();
|
||||
}
|
||||
else {
|
||||
$derivative_id = '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
$cloned_plugin_id = $deriver_id . (!empty($derivative_id) ? PluginBase::DERIVATIVE_SEPARATOR . $derivative_id : '');
|
||||
$component_array['uuid'] = $this->uuidGenerator->generate();
|
||||
$component_array['configuration']['id'] = $cloned_plugin_id;
|
||||
$cloned_components[] = SectionComponent::fromArray($component_array);
|
||||
}
|
||||
// We don't expect here third-party settings, but just in case.
|
||||
$third_party_settings = [];
|
||||
foreach ($section->getThirdPartyProviders() as $third_party_provider) {
|
||||
$third_party_settings[$third_party_provider] = $section->getThirdPartySettings($third_party_provider);
|
||||
}
|
||||
$cloned_section = new Section($section->getLayoutId(), $section->getLayoutSettings(), $cloned_components, $third_party_settings);
|
||||
$cloned_entity->appendSection($cloned_section);
|
||||
}
|
||||
$cloned_entity->save();
|
||||
}
|
||||
return $cloned_entity;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Config;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Class MenuEntityClone.
|
||||
*/
|
||||
class MenuEntityClone extends ConfigEntityCloneBase {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = []) {
|
||||
/** @var \Drupal\system\Entity\Menu */
|
||||
$cloned_entity->set('locked', FALSE);
|
||||
return parent::cloneEntity($entity, $cloned_entity, $properties);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Config;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Class MenuEntityCloneForm.
|
||||
*/
|
||||
class MenuEntityCloneForm extends ConfigEntityCloneFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(EntityInterface $entity, $parent = TRUE) {
|
||||
$form = parent::formElement($entity, $parent);
|
||||
|
||||
// Menu entities require special replace pattern.
|
||||
$form['id']['#machine_name'] += [
|
||||
'replace_pattern' => '[^a-z0-9-]+',
|
||||
'replace' => '-',
|
||||
];
|
||||
return $form;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Content;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldConfigInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\entity_clone\EntityClone\EntityCloneInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class ContentEntityCloneBase.
|
||||
*/
|
||||
class ContentEntityCloneBase implements EntityHandlerInterface, EntityCloneInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity type ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountProxyInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* A service for obtaining the system's time.
|
||||
*
|
||||
* @var \Drupal\Component\Datetime\TimeInterface
|
||||
*/
|
||||
protected $timeService;
|
||||
|
||||
/**
|
||||
* Constructs a new ContentEntityCloneBase.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
|
||||
* The current user.
|
||||
* @param \Drupal\Component\Datetime\TimeInterface $time_service
|
||||
* A service for obtaining the system's time.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, $entity_type_id, TimeInterface $time_service, AccountProxyInterface $currentUser) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityTypeId = $entity_type_id;
|
||||
$this->timeService = $time_service;
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$entity_type->id(),
|
||||
$container->get('datetime.time'),
|
||||
$container->get('current_user')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) {
|
||||
if (isset($properties['take_ownership']) && $properties['take_ownership'] === 1) {
|
||||
$cloned_entity->setOwnerId($this->currentUser->id());
|
||||
}
|
||||
// Clone referenced entities.
|
||||
$cloned_entity->save();
|
||||
$already_cloned[$entity->getEntityTypeId()][$entity->id()] = $cloned_entity;
|
||||
if ($cloned_entity instanceof FieldableEntityInterface && $entity instanceof FieldableEntityInterface) {
|
||||
foreach ($cloned_entity->getFieldDefinitions() as $field_id => $field_definition) {
|
||||
if ($this->fieldIsClonable($field_definition)) {
|
||||
$field = $entity->get($field_id);
|
||||
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $value */
|
||||
if ($field->count() > 0) {
|
||||
$cloned_entity->set($field_id, $this->cloneReferencedEntities($field, $field_definition, $properties, $already_cloned));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->setClonedEntityLabel($entity, $cloned_entity);
|
||||
|
||||
// For now, check that the cloned entity has a 'setCreatedTime' method, and
|
||||
// if so, try to call it. This condition can be replaced with a more-robust
|
||||
// check whether $cloned_entity is an instance of
|
||||
// Drupal\Core\Entity\EntityCreatedInterface once
|
||||
// https://www.drupal.org/project/drupal/issues/2833378 lands.
|
||||
if (method_exists($cloned_entity, 'setCreatedTime')) {
|
||||
$cloned_entity->setCreatedTime($this->timeService->getRequestTime());
|
||||
}
|
||||
|
||||
$cloned_entity->save();
|
||||
return $cloned_entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a field is clonable.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* The field definition.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the field is clonable; FALSE otherwise.
|
||||
*/
|
||||
protected function fieldIsClonable(FieldDefinitionInterface $field_definition) {
|
||||
$clonable_field_types = [
|
||||
'entity_reference',
|
||||
'entity_reference_revisions',
|
||||
];
|
||||
|
||||
$type_is_clonable = in_array($field_definition->getType(), $clonable_field_types, TRUE);
|
||||
if (($field_definition instanceof FieldConfigInterface) && $type_is_clonable) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cloned entity's label.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $original_entity
|
||||
* The original entity.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $cloned_entity
|
||||
* The entity cloned from the original.
|
||||
*/
|
||||
protected function setClonedEntityLabel(EntityInterface $original_entity, EntityInterface $cloned_entity) {
|
||||
$label_key = $this->entityTypeManager->getDefinition($this->entityTypeId)->getKey('label');
|
||||
if ($label_key && $cloned_entity->hasField($label_key)) {
|
||||
$cloned_entity->set($label_key, $original_entity->label() . ' - Cloned');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones referenced entities.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldItemListInterface $field
|
||||
* The field item.
|
||||
* @param \Drupal\Core\Field\FieldConfigInterface $field_definition
|
||||
* The field definition.
|
||||
* @param array $properties
|
||||
* All new properties to replace old.
|
||||
* @param array $already_cloned
|
||||
* List of all already cloned entities, used for circular references.
|
||||
*
|
||||
* @return array
|
||||
* Referenced entities.
|
||||
*/
|
||||
protected function cloneReferencedEntities(FieldItemListInterface $field, FieldConfigInterface $field_definition, array $properties, array &$already_cloned) {
|
||||
$referenced_entities = [];
|
||||
foreach ($field as $value) {
|
||||
// Check if we're not dealing with an entity
|
||||
// that has been deleted in the meantime.
|
||||
if (!$referenced_entity = $value->get('entity')->getTarget()) {
|
||||
continue;
|
||||
}
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $referenced_entity */
|
||||
$referenced_entity = $value->get('entity')->getTarget()->getValue();
|
||||
$child_properties = $this->getChildProperties($properties, $field_definition, $referenced_entity);
|
||||
if (!empty($child_properties['clone'])) {
|
||||
|
||||
$cloned_reference = $referenced_entity->createDuplicate();
|
||||
/** @var \Drupal\entity_clone\EntityClone\EntityCloneInterface $entity_clone_handler */
|
||||
$entity_clone_handler = $this->entityTypeManager->getHandler($referenced_entity->getEntityTypeId(), 'entity_clone');
|
||||
$entity_clone_handler->cloneEntity($referenced_entity, $cloned_reference, $child_properties['children'], $already_cloned);
|
||||
|
||||
$referenced_entities[] = $cloned_reference;
|
||||
}
|
||||
elseif (!empty($child_properties['is_circular'])) {
|
||||
if (!empty($already_cloned[$referenced_entity->getEntityTypeId()][$referenced_entity->id()])) {
|
||||
$referenced_entities[] = $already_cloned[$referenced_entity->getEntityTypeId()][$referenced_entity->id()];
|
||||
}
|
||||
else {
|
||||
$referenced_entities[] = $referenced_entity;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$referenced_entities[] = $referenced_entity;
|
||||
}
|
||||
}
|
||||
return $referenced_entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the properties of a child entity.
|
||||
*
|
||||
* @param array $properties
|
||||
* Properties of the clone operation.
|
||||
* @param \Drupal\Core\Field\FieldConfigInterface $field_definition
|
||||
* The field definition.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $referenced_entity
|
||||
* The field's target entity.
|
||||
*
|
||||
* @return array
|
||||
* Child properties.
|
||||
*/
|
||||
protected function getChildProperties(array $properties, FieldConfigInterface $field_definition, EntityInterface $referenced_entity) {
|
||||
$child_properties = [];
|
||||
if (isset($properties['recursive'][$field_definition->id()]['references'][$referenced_entity->id()])) {
|
||||
$child_properties = $properties['recursive'][$field_definition->id()]['references'][$referenced_entity->id()];
|
||||
}
|
||||
if (!isset($child_properties['children'])) {
|
||||
$child_properties['children'] = [];
|
||||
}
|
||||
return $child_properties;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Content;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\FieldConfigInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\StringTranslation\TranslationManager;
|
||||
use Drupal\entity_clone\EntityClone\EntityCloneFormInterface;
|
||||
use Drupal\entity_clone\EntityCloneSettingsManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class ContentEntityCloneFormBase.
|
||||
*/
|
||||
class ContentEntityCloneFormBase implements EntityHandlerInterface, EntityCloneFormInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\StringTranslation\TranslationManager
|
||||
*/
|
||||
protected $translationManager;
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManager
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity clone settings manager service.
|
||||
*
|
||||
* @var \Drupal\entity_clone\EntityCloneSettingsManager
|
||||
*/
|
||||
protected $entityCloneSettingsManager;
|
||||
|
||||
/**
|
||||
* Entities we've found while cloning.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityInterface[]
|
||||
*/
|
||||
protected $discoveredEntities = [];
|
||||
|
||||
/**
|
||||
* Constructs a new ContentEntityCloneFormBase.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationManager $translation_manager
|
||||
* The string translation manager.
|
||||
* @param \Drupal\entity_clone\EntityCloneSettingsManager $entity_clone_settings_manager
|
||||
* The entity clone settings manager.
|
||||
*/
|
||||
public function __construct(
|
||||
EntityTypeManagerInterface $entity_type_manager,
|
||||
TranslationManager $translation_manager,
|
||||
EntityCloneSettingsManager $entity_clone_settings_manager
|
||||
) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->translationManager = $translation_manager;
|
||||
$this->entityCloneSettingsManager = $entity_clone_settings_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('string_translation'),
|
||||
$container->get('entity_clone.settings.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(EntityInterface $entity, $parent = TRUE, &$discovered_entities = []) {
|
||||
$form = [
|
||||
'recursive' => [],
|
||||
];
|
||||
|
||||
if ($entity instanceof FieldableEntityInterface) {
|
||||
$discovered_entities[$entity->getEntityTypeId()][$entity->id()] = $entity;
|
||||
foreach ($entity->getFieldDefinitions() as $field_id => $field_definition) {
|
||||
if ($field_definition instanceof FieldConfigInterface && in_array($field_definition->getType(), ['entity_reference', 'entity_reference_revisions'], TRUE)) {
|
||||
$field = $entity->get($field_id);
|
||||
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $value */
|
||||
if ($field->count() > 0
|
||||
&& $this->entityTypeManager->getStorage($field->getSetting('target_type')) instanceof ContentEntityStorageInterface) {
|
||||
$form['recursive'] = array_merge($form['recursive'], $this->getRecursiveFormElement($field_definition, $field_id, $field, $discovered_entities));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($parent) {
|
||||
$form = array_merge([
|
||||
'description' => [
|
||||
'#markup' => $this->getFormDescription($form, $entity),
|
||||
'#access' => TRUE,
|
||||
],
|
||||
], $form);
|
||||
}
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recursive form element.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldConfigInterface $field_definition
|
||||
* The field definition.
|
||||
* @param string $field_id
|
||||
* The field ID.
|
||||
* @param \Drupal\Core\Field\FieldItemListInterface $field
|
||||
* The field item.
|
||||
* @param array $discovered_entities
|
||||
* List of all entities already discovered.
|
||||
*
|
||||
* @return array
|
||||
* The form element for a recursive clone.
|
||||
*/
|
||||
protected function getRecursiveFormElement(FieldConfigInterface $field_definition, $field_id, FieldItemListInterface $field, array &$discovered_entities) {
|
||||
$form_element = [
|
||||
'#tree' => TRUE,
|
||||
];
|
||||
|
||||
$fieldset_access = !$this->entityCloneSettingsManager->getHiddenValue($field_definition->getFieldStorageDefinition()->getSetting('target_type'));
|
||||
$form_element[$field_definition->id()] = [
|
||||
'#type' => 'fieldset',
|
||||
'#title' => $this->translationManager->translate('Entities referenced by field <em>@label (@field_id)</em>.', [
|
||||
'@label' => $field_definition->label(),
|
||||
'@field_id' => $field_id,
|
||||
]),
|
||||
'#access' => $fieldset_access,
|
||||
'#description_should_be_shown' => $fieldset_access,
|
||||
];
|
||||
|
||||
foreach ($field as $value) {
|
||||
// Check if we're not dealing with an entity
|
||||
// That has been deleted in the meantime.
|
||||
if (!$referenced_entity = $value->get('entity')->getTarget()) {
|
||||
continue;
|
||||
}
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $referenced_entity */
|
||||
$referenced_entity = $value->get('entity')->getTarget()->getValue();
|
||||
|
||||
if (isset($discovered_entities[$referenced_entity->getEntityTypeId()]) && array_key_exists($referenced_entity->id(), $discovered_entities[$referenced_entity->getEntityTypeId()])) {
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['is_circular'] = [
|
||||
'#type' => 'hidden',
|
||||
'#value' => TRUE,
|
||||
];
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['circular'] = [
|
||||
'#type' => 'item',
|
||||
'#markup' => $this->translationManager->translate('Circular reference detected'),
|
||||
];
|
||||
}
|
||||
else {
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['clone'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->translationManager->translate('Clone entity <strong>ID:</strong> <em>@entity_id</em>, <strong>Type:</strong> <em>@entity_type - @bundle</em>, <strong>Label:</strong> <em>@entity_label</em>', [
|
||||
'@entity_id' => $referenced_entity->id(),
|
||||
'@entity_type' => $referenced_entity->getEntityTypeId(),
|
||||
'@bundle' => $referenced_entity->bundle(),
|
||||
'@entity_label' => $referenced_entity->label(),
|
||||
]),
|
||||
'#default_value' => $this->entityCloneSettingsManager->getDefaultValue($referenced_entity->getEntityTypeId()),
|
||||
'#access' => $referenced_entity->access('view label'),
|
||||
];
|
||||
|
||||
if ($this->entityCloneSettingsManager->getDisableValue($referenced_entity->getEntityTypeId())) {
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['clone']['#attributes'] = [
|
||||
'disabled' => TRUE,
|
||||
];
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['clone']['#value'] = $form_element[$field_definition->id()]['references'][$referenced_entity->id()]['clone']['#default_value'];
|
||||
}
|
||||
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['target_entity_type_id'] = [
|
||||
'#type' => 'hidden',
|
||||
'#value' => $referenced_entity->getEntityTypeId(),
|
||||
];
|
||||
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['target_bundle'] = [
|
||||
'#type' => 'hidden',
|
||||
'#value' => $referenced_entity->bundle(),
|
||||
];
|
||||
if ($referenced_entity instanceof ContentEntityInterface) {
|
||||
$form_element[$field_definition->id()]['references'][$referenced_entity->id()]['children'] = $this->getChildren($referenced_entity, $discovered_entities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $form_element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches clonable children from a field.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $referenced_entity
|
||||
* The field item list.
|
||||
* @param array $discovered_entities
|
||||
* List of all entities already discovered.
|
||||
*
|
||||
* @return array
|
||||
* The list of children.
|
||||
*/
|
||||
protected function getChildren(ContentEntityInterface $referenced_entity, array &$discovered_entities) {
|
||||
/** @var \Drupal\entity_clone\EntityClone\EntityCloneFormInterface $entity_clone_handler */
|
||||
if ($this->entityTypeManager->hasHandler($referenced_entity->getEntityTypeId(), 'entity_clone_form')) {
|
||||
// Record that we've found this entity.
|
||||
$discovered_entities[$referenced_entity->getEntityTypeId()][$referenced_entity->id()] = $referenced_entity;
|
||||
|
||||
$entity_clone_form_handler = $this->entityTypeManager->getHandler($referenced_entity->getEntityTypeId(), 'entity_clone_form');
|
||||
return $entity_clone_form_handler->formElement($referenced_entity, FALSE, $discovered_entities);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValues(FormStateInterface $form_state) {
|
||||
return $form_state->getValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the clone form confirmation page description.
|
||||
*
|
||||
* If there are no recursive elements visible, the default description should
|
||||
* be shown.
|
||||
*
|
||||
* @param array $form
|
||||
* The clone form.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity.
|
||||
*
|
||||
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
|
||||
* Description to be shown
|
||||
*/
|
||||
protected function getFormDescription(array $form, EntityInterface $entity) {
|
||||
$has_recursive = FALSE;
|
||||
$elements_visible = FALSE;
|
||||
|
||||
if (isset($form['recursive'])) {
|
||||
$has_recursive = TRUE;
|
||||
}
|
||||
|
||||
array_walk_recursive($form['recursive'], function ($item, $key) use (&$elements_visible) {
|
||||
if ($key === '#description_should_be_shown') {
|
||||
$elements_visible = ($elements_visible || $item);
|
||||
}
|
||||
});
|
||||
|
||||
if ($has_recursive && $elements_visible) {
|
||||
return $this->translationManager->translate("
|
||||
<p>Specify the child entities (the entities referenced by this entity) that should also be cloned as part of
|
||||
the cloning process. If they're not included, these fields' referenced entities will be the same as in the
|
||||
original. In other words, fields in both the original entity and the cloned entity will refer to the same
|
||||
referenced entity. Examples:</p>
|
||||
|
||||
<p>If you have a Paragraph field in your entity, and you choose not to clone it here, deleting the original
|
||||
or cloned entity will also delete the Paragraph field from the other one. So you probably want to clone
|
||||
Paragraph fields.</p>
|
||||
|
||||
<p>However, if you have a User reference field, you probably don't want to clone it here because a new User
|
||||
will be created for referencing by the clone.</p>
|
||||
|
||||
<p>Some options may be disabled here, preventing you from changing them, as set by your administrator. Some
|
||||
options may also be missing, hidden by your administrator, forcing you to clone with the default settings.
|
||||
It's possible that there are no options here for you at all, or none need to be set, in which case you may
|
||||
simply hit the <em>Clone</em> button.</p>
|
||||
");
|
||||
}
|
||||
else {
|
||||
return $this->translationManager->translate("<p>Do you want to clone the <em>@type</em> entity named <em>@title</em>?</p>", [
|
||||
"@type" => $entity->getEntityType()->getLabel(),
|
||||
"@title" => $entity->label(),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Content;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
|
||||
/**
|
||||
* Class ContentEntityCloneBase.
|
||||
*/
|
||||
class FileEntityClone extends ContentEntityCloneBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) {
|
||||
/** @var \Drupal\file\FileInterface $cloned_entity */
|
||||
$cloned_file = file_copy($cloned_entity, $cloned_entity->getFileUri(), FileSystemInterface::EXISTS_RENAME);
|
||||
if (isset($properties['take_ownership']) && $properties['take_ownership'] === 1) {
|
||||
$cloned_file->setOwnerId(\Drupal::currentUser()->id());
|
||||
}
|
||||
return parent::cloneEntity($entity, $cloned_file, $properties);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Content;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Class TaxonomyTermEntityClone.
|
||||
*/
|
||||
class TaxonomyTermEntityClone extends ContentEntityCloneBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) {
|
||||
/** @var \Drupal\core\Entity\ContentEntityInterface $cloned_entity */
|
||||
|
||||
// Enforce a parent if the cloned term doesn't have a parent.
|
||||
// (First level of a taxonomy tree).
|
||||
if (!isset($cloned_entity->parent->target_id)) {
|
||||
$cloned_entity->set('parent', 0);
|
||||
}
|
||||
return parent::cloneEntity($entity, $cloned_entity, $properties);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone\Content;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Class ContentEntityCloneBase.
|
||||
*/
|
||||
class UserEntityClone extends ContentEntityCloneBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = [], array &$already_cloned = []) {
|
||||
/** @var \Drupal\user\UserInterface $cloned_entity */
|
||||
$cloned_entity->set('name', $cloned_entity->getAccountName() . '_cloned');
|
||||
return parent::cloneEntity($entity, $cloned_entity, $properties);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Defines a common interface for all entity clone form objects.
|
||||
*/
|
||||
interface EntityCloneFormInterface {
|
||||
|
||||
/**
|
||||
* Get all specific form element.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity.
|
||||
* @param bool $parent
|
||||
* Is the parent form element.
|
||||
*
|
||||
* @return array
|
||||
* The form elements.
|
||||
*/
|
||||
public function formElement(EntityInterface $entity, $parent = TRUE);
|
||||
|
||||
/**
|
||||
* Get all new values provided by the specific form element.
|
||||
*
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The form state.
|
||||
*
|
||||
* @return array
|
||||
* An array containing all new values.
|
||||
*/
|
||||
public function getValues(FormStateInterface $form_state);
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\EntityClone;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Defines a common interface for all entity clone objects.
|
||||
*/
|
||||
interface EntityCloneInterface {
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $cloned_entity
|
||||
* The cloned entity.
|
||||
* @param array $properties
|
||||
* All new properties to replace old.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The new saved entity.
|
||||
*/
|
||||
public function cloneEntity(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = []);
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\TranslationManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\entity_clone\Services\EntityCloneServiceProvider;
|
||||
|
||||
/**
|
||||
* Provides dynamic permissions of the entity_clone module.
|
||||
*/
|
||||
class EntityClonePermissions implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The entoty type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The string translation manager.
|
||||
*
|
||||
* @var \Drupal\Core\StringTranslation\TranslationManager
|
||||
*/
|
||||
protected $translationManager;
|
||||
|
||||
/**
|
||||
* The Service Provider that verifies if entity has ownership.
|
||||
*
|
||||
* @var \Drupal\entity_clone\Services\EntityCloneServiceProvider
|
||||
*/
|
||||
protected $serviceProvider;
|
||||
|
||||
/**
|
||||
* Constructs a new EntityClonePermissions instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationManager $string_translation
|
||||
* The string translation manager.
|
||||
* @param \\Drupal\entity_clone\Services\EntityCloneServiceProvider $service_provider
|
||||
* The Service Provider that verifies if entity has ownership.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_manager, TranslationManager $string_translation, EntityCloneServiceProvider $service_provider) {
|
||||
$this->entityTypeManager = $entity_manager;
|
||||
$this->translationManager = $string_translation;
|
||||
$this->serviceProvider = $service_provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('string_translation'),
|
||||
$container->get('entity_clone.service_provider')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of entity_clone permissions.
|
||||
*
|
||||
* @return array
|
||||
* The permission list.
|
||||
*/
|
||||
public function permissions() {
|
||||
$permissions = [];
|
||||
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
$permissions['clone ' . $entity_type_id . ' entity'] = $this->translationManager->translate('Clone all <em>@label</em> entities.', [
|
||||
'@label' => $entity_type->getLabel(),
|
||||
]);
|
||||
|
||||
if ($this->serviceProvider->entityTypeHasOwnerTrait($entity_type)) {
|
||||
$permissions['take_ownership_on_clone ' . $entity_type_id . ' entity'] = $this->translationManager->translate('Allow user to take ownership of <em>@label</em> cloned entities', [
|
||||
'@label' => $entity_type->getLabel(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* Manage entity clone configuration.
|
||||
*/
|
||||
class EntityCloneSettingsManager {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity type bundle info service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The immutable entity clone settings configuration entity.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* The editable entity clone settings configuration entity.
|
||||
*
|
||||
* @var \Drupal\Core\Config\Config
|
||||
*/
|
||||
protected $editableConfig;
|
||||
|
||||
/**
|
||||
* EntityCloneSettingsManager constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
|
||||
* The entity type bundle info service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, ConfigFactoryInterface $config_factory) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityTypeBundleInfo = $entity_type_bundle_info;
|
||||
$this->config = $config_factory->get('entity_clone.settings');
|
||||
$this->editableConfig = $config_factory->getEditable('entity_clone.settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all content entity types.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityTypeInterface[]
|
||||
* An array containing all content entity types.
|
||||
*/
|
||||
public function getContentEntityTypes() {
|
||||
$definitions = $this->entityTypeManager->getDefinitions();
|
||||
$ret = [];
|
||||
foreach ($definitions as $machine => $type) {
|
||||
if ($type instanceof ContentEntityTypeInterface) {
|
||||
$ret[$machine] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the entity clone settings.
|
||||
*
|
||||
* @param array $settings
|
||||
* The settings from the form.
|
||||
*/
|
||||
public function setFormSettings(array $settings) {
|
||||
if (isset($settings['table'])) {
|
||||
array_walk_recursive($settings['table'], function (&$item) {
|
||||
if ($item == '1') {
|
||||
$item = TRUE;
|
||||
}
|
||||
else {
|
||||
$item = FALSE;
|
||||
}
|
||||
});
|
||||
$this->editableConfig->set('form_settings', $settings['table'])->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkbox default value for a given entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
*
|
||||
* @return bool
|
||||
* The default value.
|
||||
*/
|
||||
public function getDefaultValue($entity_type_id) {
|
||||
$form_settings = $this->config->get('form_settings');
|
||||
if (isset($form_settings[$entity_type_id]['default_value'])) {
|
||||
return $form_settings[$entity_type_id]['default_value'];
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkbox disable value for a given entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
*
|
||||
* @return bool
|
||||
* The disable value.
|
||||
*/
|
||||
public function getDisableValue($entity_type_id) {
|
||||
$form_settings = $this->config->get('form_settings');
|
||||
if (isset($form_settings[$entity_type_id]['disable'])) {
|
||||
return $form_settings[$entity_type_id]['disable'];
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkbox hidden value for a given entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
*
|
||||
* @return bool
|
||||
* The hidden value.
|
||||
*/
|
||||
public function getHiddenValue($entity_type_id) {
|
||||
$form_settings = $this->config->get('form_settings');
|
||||
if (isset($form_settings[$entity_type_id]['hidden'])) {
|
||||
return $form_settings[$entity_type_id]['hidden'];
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the take ownership setting.
|
||||
*
|
||||
* @param int $setting
|
||||
* The settings from the form.
|
||||
*/
|
||||
public function setTakeOwnershipSettings(int $setting) {
|
||||
$this->editableConfig->set('take_ownership', $setting)->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the take ownership settings.
|
||||
*/
|
||||
public function getTakeOwnershipSetting() {
|
||||
return $this->config->get('take_ownership') ?? FALSE;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\Event;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Represents entity selection as event.
|
||||
*/
|
||||
class EntityCloneEvent extends Event {
|
||||
|
||||
/**
|
||||
* Entity being cloned.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* New cloned entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $clonedEntity;
|
||||
|
||||
/**
|
||||
* Properties.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $properties;
|
||||
|
||||
/**
|
||||
* Constructs an EntityCloneEvent object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The original entity that was cloned.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $cloned_entity
|
||||
* The clone of the original entity.
|
||||
* @param array $properties
|
||||
* The entity's properties.
|
||||
*/
|
||||
public function __construct(EntityInterface $entity, EntityInterface $cloned_entity, array $properties = []) {
|
||||
$this->entity = $entity;
|
||||
$this->clonedEntity = $cloned_entity;
|
||||
$this->properties = $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets entity being cloned.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The original entity.
|
||||
*/
|
||||
public function getEntity() {
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets new cloned entity.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The cloned entity.
|
||||
*/
|
||||
public function getClonedEntity() {
|
||||
return $this->clonedEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets entity properties.
|
||||
*
|
||||
* @return array
|
||||
* The list of properties.
|
||||
*/
|
||||
public function getProperties() {
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\Event;
|
||||
|
||||
/**
|
||||
* Contains all events thrown by entity clone.
|
||||
*/
|
||||
final class EntityCloneEvents {
|
||||
|
||||
/**
|
||||
* The PRE_CLONE event occurs before the entity was cloned.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PRE_CLONE = 'entity_clone.pre_clone';
|
||||
|
||||
/**
|
||||
* The POST_CLONE event occurs when an entity has been cloned and saved.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const POST_CLONE = 'entity_clone.post_clone';
|
||||
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\Messenger;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\Core\StringTranslation\TranslationManager;
|
||||
use Drupal\entity_clone\Services\EntityCloneServiceProvider;
|
||||
use Drupal\entity_clone\EntityCloneSettingsManager;
|
||||
use Drupal\entity_clone\Event\EntityCloneEvent;
|
||||
use Drupal\entity_clone\Event\EntityCloneEvents;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Implements an entity Clone form.
|
||||
*/
|
||||
class EntityCloneForm extends FormBase {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity ready to clone.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The entity type définition.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeInterface
|
||||
*/
|
||||
protected $entityTypeDefinition;
|
||||
|
||||
/**
|
||||
* The string translation manager.
|
||||
*
|
||||
* @var \Drupal\Core\StringTranslation\TranslationManager
|
||||
*/
|
||||
protected $stringTranslationManager;
|
||||
|
||||
/**
|
||||
* Event dispatcher service.
|
||||
*
|
||||
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\Messenger
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* The entity clone settings manager service.
|
||||
*
|
||||
* @var \Drupal\entity_clone\EntityCloneSettingsManager
|
||||
*/
|
||||
protected $entityCloneSettingsManager;
|
||||
|
||||
/**
|
||||
* The Service Provider that verifies if entity has ownership.
|
||||
*
|
||||
* @var \Drupal\entity_clone\Services\EntityCloneServiceProvider
|
||||
*/
|
||||
protected $serviceProvider;
|
||||
|
||||
/**
|
||||
* Constructs a new Entity Clone form.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The route match service.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationManager $string_translation
|
||||
* The string translation manager.
|
||||
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
|
||||
* The event dispatcher service.
|
||||
* @param \Drupal\Core\Messenger\Messenger $messenger
|
||||
* The messenger service.
|
||||
* @param \Drupal\Core\Session\AccountProxyInterface $currentUser
|
||||
* The current user.
|
||||
* @param \Drupal\entity_clone\EntityCloneSettingsManager $entity_clone_settings_manager
|
||||
* The entity clone settings manager.
|
||||
* @param \Drupal\entity_clone\Services\EntityCloneServiceProvider $service_provider
|
||||
* The Service Provider that verifies if entity has ownership.
|
||||
*
|
||||
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match, TranslationManager $string_translation, EventDispatcherInterface $eventDispatcher, Messenger $messenger, AccountProxyInterface $currentUser, EntityCloneSettingsManager $entity_clone_settings_manager, EntityCloneServiceProvider $service_provider) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->stringTranslationManager = $string_translation;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->messenger = $messenger;
|
||||
|
||||
$parameter_name = $route_match->getRouteObject()->getOption('_entity_clone_entity_type_id');
|
||||
$this->entity = $route_match->getParameter($parameter_name);
|
||||
|
||||
$this->entityTypeDefinition = $entity_type_manager->getDefinition($this->entity->getEntityTypeId());
|
||||
$this->currentUser = $currentUser;
|
||||
$this->entityCloneSettingsManager = $entity_clone_settings_manager;
|
||||
$this->serviceProvider = $service_provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('current_route_match'),
|
||||
$container->get('string_translation'),
|
||||
$container->get('event_dispatcher'),
|
||||
$container->get('messenger'),
|
||||
$container->get('current_user'),
|
||||
$container->get('entity_clone.settings.manager'),
|
||||
$container->get('entity_clone.service_provider')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'entity_clone_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
if ($this->entity && $this->entityTypeDefinition->hasHandlerClass('entity_clone')) {
|
||||
|
||||
/** @var \Drupal\entity_clone\EntityClone\EntityCloneFormInterface $entity_clone_handler */
|
||||
if ($this->entityTypeManager->hasHandler($this->entityTypeDefinition->id(), 'entity_clone_form')) {
|
||||
$entity_clone_form_handler = $this->entityTypeManager->getHandler($this->entityTypeDefinition->id(), 'entity_clone_form');
|
||||
$form = array_merge($form, $entity_clone_form_handler->formElement($this->entity));
|
||||
}
|
||||
$entityType = $this->getEntity()->getEntityTypeId();
|
||||
if ($this->serviceProvider->entityTypeHasOwnerTrait($this->getEntity()->getEntityType()) && $this->currentUser->hasPermission('take_ownership_on_clone ' . $entityType . ' entity')) {
|
||||
$form['take_ownership'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->stringTranslationManager->translate('Take ownership'),
|
||||
'#default_value' => $this->entityCloneSettingsManager->getTakeOwnershipSetting(),
|
||||
'#description' => $this->stringTranslationManager->translate('Take ownership of the newly created cloned entity.'),
|
||||
];
|
||||
}
|
||||
|
||||
$form['actions'] = ['#type' => 'actions'];
|
||||
$form['actions']['clone'] = [
|
||||
'#type' => 'submit',
|
||||
'#button_type' => 'primary',
|
||||
'#value' => $this->stringTranslationManager->translate('Clone'),
|
||||
];
|
||||
|
||||
$form['actions']['abort'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->stringTranslationManager->translate('Cancel'),
|
||||
'#submit' => ['::cancelForm'],
|
||||
];
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
/** @var \Drupal\entity_clone\EntityClone\EntityCloneInterface $entity_clone_handler */
|
||||
$entity_clone_handler = $this->entityTypeManager->getHandler($this->entityTypeDefinition->id(), 'entity_clone');
|
||||
if ($this->entityTypeManager->hasHandler($this->entityTypeDefinition->id(), 'entity_clone_form')) {
|
||||
$entity_clone_form_handler = $this->entityTypeManager->getHandler($this->entityTypeDefinition->id(), 'entity_clone_form');
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
if (isset($entity_clone_form_handler) && $entity_clone_form_handler) {
|
||||
$properties = $entity_clone_form_handler->getValues($form_state);
|
||||
}
|
||||
|
||||
$duplicate = $this->entity->createDuplicate();
|
||||
|
||||
$this->eventDispatcher->dispatch(EntityCloneEvents::PRE_CLONE, new EntityCloneEvent($this->entity, $duplicate, $properties));
|
||||
$cloned_entity = $entity_clone_handler->cloneEntity($this->entity, $duplicate, $properties);
|
||||
$this->eventDispatcher->dispatch(EntityCloneEvents::POST_CLONE, new EntityCloneEvent($this->entity, $duplicate, $properties));
|
||||
|
||||
$this->messenger->addMessage($this->stringTranslationManager->translate('The entity <em>@entity (@entity_id)</em> of type <em>@type</em> was cloned.', [
|
||||
'@entity' => $this->entity->label(),
|
||||
'@entity_id' => $this->entity->id(),
|
||||
'@type' => $this->entity->getEntityTypeId(),
|
||||
]));
|
||||
|
||||
$this->formSetRedirect($form_state, $cloned_entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel form handler.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public function cancelForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->formSetRedirect($form_state, $this->entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a redirect on form state.
|
||||
*
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The cloned entity.
|
||||
*/
|
||||
protected function formSetRedirect(FormStateInterface $form_state, EntityInterface $entity) {
|
||||
if ($entity && $entity->hasLinkTemplate('canonical')) {
|
||||
$form_state->setRedirect($entity->toUrl()->getRouteName(), $entity->toUrl()->getRouteParameters());
|
||||
}
|
||||
else {
|
||||
$form_state->setRedirect('<front>');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity of this form.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The entity.
|
||||
*/
|
||||
public function getEntity() {
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\Form;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Form\ConfigFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\entity_clone\EntityCloneSettingsManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provide the settings form for entity clone.
|
||||
*/
|
||||
class EntityCloneSettingsForm extends ConfigFormBase implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The entity clone settings manager.
|
||||
*
|
||||
* @var \Drupal\entity_clone\EntityCloneSettingsManager
|
||||
*/
|
||||
protected $entityCloneSettingsManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\entity_clone\EntityCloneSettingsManager $entity_clone_settings_manager
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, EntityCloneSettingsManager $entity_clone_settings_manager) {
|
||||
parent::__construct($config_factory);
|
||||
$this->entityCloneSettingsManager = $entity_clone_settings_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('config.factory'),
|
||||
$container->get('entity_clone.settings.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEditableConfigNames() {
|
||||
return ['entity_clone.settings'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'entity_clone_settings_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$form['#tree'] = TRUE;
|
||||
|
||||
$form['form_settings'] = [
|
||||
'#tree' => TRUE,
|
||||
'#type' => 'fieldset',
|
||||
'#title' => $this->t('Clone form settings'),
|
||||
'#description' => $this->t("
|
||||
For each type of child entity (the entity that's referenced by the entity being
|
||||
cloned), please set your cloning preferences. This will affect the clone form presented to users when they
|
||||
clone entities. Default behaviour for whether or not the child entities should be cloned is specified in
|
||||
the left-most column. To prevent users from altering behaviour for each type when they're actually cloning
|
||||
(but still allowing them to see what will happen), use the middle column. The right-most column can be used
|
||||
to hide the form options from users completely. This will run the clone operation with the defaults set here
|
||||
(in the left-most column). See the clone form (by cloning an entity) for more information.
|
||||
"),
|
||||
'#open' => TRUE,
|
||||
'#collapsible' => FALSE,
|
||||
];
|
||||
|
||||
$form['form_settings']['table'] = [
|
||||
'#type' => 'table',
|
||||
'#header' => [
|
||||
'label' => $this->t('Label'),
|
||||
'default_value' => $this->t('Checkboxes default value'),
|
||||
'disable' => $this->t('Disable checkboxes'),
|
||||
'hidden' => $this->t('Hide checkboxes'),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($this->entityCloneSettingsManager->getContentEntityTypes() as $type_id => $type) {
|
||||
$form['form_settings']['table'][$type_id] = [
|
||||
'label' => [
|
||||
'#type' => 'label',
|
||||
'#title' => $this->t('@type', [
|
||||
'@type' => $type->getLabel(),
|
||||
]),
|
||||
],
|
||||
'default_value' => [
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $this->entityCloneSettingsManager->getDefaultValue($type_id),
|
||||
],
|
||||
'disable' => [
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $this->entityCloneSettingsManager->getDisableValue($type_id),
|
||||
],
|
||||
'hidden' => [
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $this->entityCloneSettingsManager->getHiddenValue($type_id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$form['take_ownership'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Take ownership'),
|
||||
'#description' => $this->t('Whether the "Take ownership" option should be checked by default on the entity clone form.'),
|
||||
'#default_value' => $this->entityCloneSettingsManager->getTakeOwnershipSetting(),
|
||||
];
|
||||
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->entityCloneSettingsManager->setFormSettings($form_state->getValue('form_settings'));
|
||||
$this->entityCloneSettingsManager->setTakeOwnershipSettings($form_state->getValue('take_ownership'));
|
||||
parent::submitForm($form, $form_state);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\TranslationManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines dynamic local tasks.
|
||||
*/
|
||||
class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The translation manager.
|
||||
*
|
||||
* @var \Drupal\Core\StringTranslation\TranslationManager
|
||||
*/
|
||||
protected $translationManager;
|
||||
|
||||
/**
|
||||
* Constructs a new DynamicLocalTasks.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationManager $string_translation
|
||||
* The translation manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationManager $string_translation) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->translationManager = $string_translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('string_translation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
$this->derivatives = [];
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
$has_clone_path = $entity_type->hasLinkTemplate('clone-form');
|
||||
$has_canonical_path = $entity_type->hasLinkTemplate('canonical');
|
||||
|
||||
if ($has_clone_path) {
|
||||
$this->derivatives["$entity_type_id.clone_tab"] = [
|
||||
'route_name' => "entity.$entity_type_id.clone_form",
|
||||
'title' => $this->translationManager->translate('Clone'),
|
||||
'base_route' => "entity.$entity_type_id." . ($has_canonical_path ? "canonical" : "edit_form"),
|
||||
'weight' => 100,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\Routing;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Routing\RouteSubscriberBase;
|
||||
use Drupal\Core\Routing\RoutingEvents;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Subscriber for Entity Clone routes.
|
||||
*/
|
||||
class RouteSubscriber extends RouteSubscriberBase {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a new RouteSubscriber object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_manager) {
|
||||
$this->entityTypeManager = $entity_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function alterRoutes(RouteCollection $collection) {
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
if ($route = $this->getEntityCloneRoute($entity_type)) {
|
||||
$collection->add("entity.$entity_type_id.clone_form", $route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity_clone route.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\Route|null
|
||||
* The generated route, if available.
|
||||
*/
|
||||
protected function getEntityCloneRoute(EntityTypeInterface $entity_type) {
|
||||
if ($clone_form = $entity_type->getLinkTemplate('clone-form')) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
$route = new Route($clone_form);
|
||||
$route
|
||||
->addDefaults([
|
||||
'_form' => '\Drupal\entity_clone\Form\EntityCloneForm',
|
||||
'_title' => 'Clone ' . $entity_type->getLabel(),
|
||||
])
|
||||
->addRequirements([
|
||||
'_entity_access' => $entity_type_id . '.clone',
|
||||
])
|
||||
->setOption('_entity_clone_entity_type_id', $entity_type_id)
|
||||
->setOption('_admin_route', TRUE)
|
||||
->setOption('parameters', [
|
||||
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
|
||||
]);
|
||||
|
||||
// Special case for menu link content.
|
||||
// Menu link content does not work properly with custom operation.
|
||||
// This case must be removed when issue #3016038
|
||||
// (https://www.drupal.org/project/drupal/issues/3016038) was closed.
|
||||
if ($entity_type_id === 'menu_link_content') {
|
||||
$route->setRequirements([
|
||||
'_permission' => 'clone ' . $entity_type_id . ' entity',
|
||||
]);
|
||||
}
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events = parent::getSubscribedEvents();
|
||||
$events[RoutingEvents::ALTER] = 'onAlterRoutes';
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\entity_clone\Services;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\user\EntityOwnerTrait;
|
||||
|
||||
/**
|
||||
* Service Provider Class.
|
||||
*/
|
||||
class EntityCloneServiceProvider {
|
||||
|
||||
/**
|
||||
* Constructs a new ServiceProvider object.
|
||||
*/
|
||||
public function __construct() {}
|
||||
|
||||
/**
|
||||
* Checks if the given entity implements has owner trait.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity
|
||||
* Entity to be tested.
|
||||
*
|
||||
* @return bool
|
||||
* Returns boolean for the owner trait test.
|
||||
*/
|
||||
public function entityTypeHasOwnerTrait(EntityTypeInterface $entityType) {
|
||||
try {
|
||||
$reflectionClass = new \ReflectionClass($entityType->getOriginalClass());
|
||||
} catch (\ReflectionException $e) {
|
||||
return FALSE;
|
||||
}
|
||||
return in_array(
|
||||
EntityOwnerTrait::class,
|
||||
array_keys($reflectionClass->getTraits())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\system\Entity\Action;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create an action and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneActionTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'action'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer actions',
|
||||
'clone action entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure actions settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test action entity clone.
|
||||
*/
|
||||
public function testActionEntityClone() {
|
||||
foreach (\Drupal::service('plugin.manager.action')->getDefinitions() as $id => $definition) {
|
||||
if (is_subclass_of($definition['class'], '\Drupal\Core\Plugin\PluginFormInterface') && $definition['label'] == 'Send email') {
|
||||
$action_key = $id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$edit = [
|
||||
'label' => 'Test send email action for clone',
|
||||
'id' => 'test_send_email_for_clone',
|
||||
'recipient' => 'test@recipient.com',
|
||||
'subject' => 'test subject',
|
||||
'message' => 'test message',
|
||||
];
|
||||
$this->drupalPostForm("admin/config/system/actions/add/$action_key", $edit, t('Save'));
|
||||
|
||||
$actions = \Drupal::entityTypeManager()
|
||||
->getStorage('action')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$action = reset($actions);
|
||||
|
||||
$edit = [
|
||||
'label' => 'Test send email action cloned',
|
||||
'id' => 'test_send_email_cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/action/' . $action->id(), $edit, t('Clone'));
|
||||
|
||||
$actions = \Drupal::entityTypeManager()
|
||||
->getStorage('action')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$action = reset($actions);
|
||||
$this->assertInstanceOf(Action::class, $action, 'Test action cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\block\Entity\Block;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create an block and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneBlockTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'block'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer blocks',
|
||||
'clone block entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure blocks settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test block entity clone.
|
||||
*/
|
||||
public function testBlockEntityClone() {
|
||||
$config = \Drupal::configFactory();
|
||||
$block = Block::create([
|
||||
'plugin' => 'test_block',
|
||||
'region' => 'sidebar_first',
|
||||
'id' => 'test_block',
|
||||
'theme' => $config->get('system.theme')->get('default'),
|
||||
'label' => $this->randomMachineName(8),
|
||||
'visibility' => [],
|
||||
'weight' => 0,
|
||||
]);
|
||||
$block->save();
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_block_cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/block/' . $block->id(), $edit, t('Clone'));
|
||||
|
||||
$blocks = \Drupal::entityTypeManager()
|
||||
->getStorage('block')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$block = reset($blocks);
|
||||
$this->assertInstanceOf(Block::class, $block, 'Test block cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\Tests\comment\Functional\CommentTestBase;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Create a comment and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneCommentTest extends CommentTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'entity_clone',
|
||||
'block',
|
||||
'comment',
|
||||
'node',
|
||||
'history',
|
||||
'field_ui',
|
||||
'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer content types',
|
||||
'administer comments',
|
||||
'administer comment types',
|
||||
'administer comment fields',
|
||||
'administer comment display',
|
||||
'skip comment approval',
|
||||
'post comments',
|
||||
'access comments',
|
||||
'access user profiles',
|
||||
'access content',
|
||||
'clone comment entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test comment entity clone.
|
||||
*/
|
||||
public function testCommentEntityClone() {
|
||||
$subject = 'Test comment for clone';
|
||||
$body = $this->randomMachineName();
|
||||
$comment = $this->postComment($this->node, $body, $subject, TRUE);
|
||||
|
||||
$this->drupalPostForm('entity_clone/comment/' . $comment->id(), [], t('Clone'));
|
||||
|
||||
$comments = \Drupal::entityTypeManager()
|
||||
->getStorage('comment')
|
||||
->loadByProperties([
|
||||
'subject' => $subject . ' - Cloned',
|
||||
'comment_body' => $body,
|
||||
]);
|
||||
$comments = reset($comments);
|
||||
$this->assertInstanceOf(Comment::class, $comments, 'Test comment cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\contact\Entity\ContactForm;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create an contact form and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneContactTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'contact'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer contact forms',
|
||||
'clone contact_form entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure contact forms settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test contact form entity clone.
|
||||
*/
|
||||
public function testContactFormsEntityClone() {
|
||||
|
||||
$edit = [
|
||||
'label' => 'Test contact form for clone',
|
||||
'id' => 'test_contact_form_for_clone',
|
||||
'recipients' => 'test@recipient.com',
|
||||
];
|
||||
$this->drupalPostForm('admin/structure/contact/add', $edit, t('Save'));
|
||||
|
||||
$contact_forms = \Drupal::entityTypeManager()
|
||||
->getStorage('contact_form')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$contact_form = reset($contact_forms);
|
||||
|
||||
$edit = [
|
||||
'label' => 'Test contact form cloned',
|
||||
'id' => 'test_contact_form_cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/contact_form/' . $contact_form->id(), $edit, t('Clone'));
|
||||
|
||||
$contact_forms = \Drupal::entityTypeManager()
|
||||
->getStorage('contact_form')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$contact_form = reset($contact_forms);
|
||||
$this->assertInstanceOf(ContactForm::class, $contact_form, 'Test contact form cloned found in database.');
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_contact_form_clone_with_a_really_long_name_that_is_longer_than_the_bundle_max_length',
|
||||
'label' => 'Test contact form clone with a really long name that is longer than the bundle max length',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/contact_form/' . $contact_form->id(), $edit, t('Clone'));
|
||||
$this->assertText('New Id cannot be longer than 32 characters');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\node\Functional\NodeTestBase;
|
||||
|
||||
/**
|
||||
* Test whether cloning an entity also clones its created date.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneContentCreatedDate extends NodeTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'node'];
|
||||
|
||||
/**
|
||||
* The user that we will use to execute the functional test.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $sutUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->sutUser = $this->drupalCreateUser([
|
||||
'bypass node access',
|
||||
'administer nodes',
|
||||
'clone node entity',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an entity's created date is not cloned.
|
||||
*/
|
||||
public function testCreatedDateIsNotCloned() {
|
||||
// Log in.
|
||||
$this->drupalLogin($this->sutUser);
|
||||
|
||||
// Create the original node.
|
||||
$originalNodeCreatedDate = new \DateTimeImmutable('1 year 1 month 1 day ago');
|
||||
$originalNode = $this->drupalCreateNode([
|
||||
'created' => $originalNodeCreatedDate->getTimestamp(),
|
||||
]);
|
||||
$this->assertEquals($originalNodeCreatedDate->getTimestamp(), $originalNode->getCreatedTime());
|
||||
|
||||
// Clone the node.
|
||||
$this->drupalGet(Url::fromRoute('entity.node.clone_form', [
|
||||
'node' => $originalNode->id(),
|
||||
])->toString());
|
||||
$this->getSession()->getPage()->pressButton('Clone');
|
||||
|
||||
// Find the cloned node.
|
||||
$originalNodeClones = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => sprintf('%s - Cloned', $originalNode->label()),
|
||||
]);
|
||||
$this->assertGreaterThanOrEqual(1, count($originalNodeClones));
|
||||
$clonedNode = reset($originalNodeClones);
|
||||
|
||||
// Validate the cloned node's created time is more recent than the original
|
||||
// node.
|
||||
$this->assertNotEquals($originalNode->getCreatedTime(), $clonedNode->getCreatedTime());
|
||||
$this->assertGreaterThanOrEqual($originalNode->getCreatedTime(), $clonedNode->getCreatedTime());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\node\Functional\NodeTestBase;
|
||||
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
|
||||
|
||||
/**
|
||||
* Create a content and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneContentRecursiveCircularTest extends NodeTestBase {
|
||||
|
||||
use EntityReferenceTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'block', 'node', 'datetime'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Profile to install.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $profile = 'standard';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'bypass node access',
|
||||
'administer nodes',
|
||||
'clone node entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* A user with permission to bypass content access checks.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalCreateContentType([
|
||||
'type' => 'test_content_type',
|
||||
'name' => 'Test content type',
|
||||
'display_submitted' => FALSE,
|
||||
]);
|
||||
|
||||
$this->createEntityReferenceField('node', 'test_content_type', 'test_field_reference', 'Test field reference', 'node');
|
||||
$this->createEntityReferenceField('node', 'test_content_type', 'test_another_field_reference', 'Test another field reference', 'node');
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test clone a content entity with another entities attached.
|
||||
*/
|
||||
public function testContentEntityClone() {
|
||||
|
||||
$node1_title = $this->randomMachineName(8);
|
||||
$node1 = Node::create([
|
||||
'type' => 'test_content_type',
|
||||
'title' => $node1_title,
|
||||
]);
|
||||
$node1->save();
|
||||
|
||||
$node2_title = $this->randomMachineName(8);
|
||||
$node2 = Node::create([
|
||||
'type' => 'test_content_type',
|
||||
'title' => $node2_title,
|
||||
'test_field_reference' => $node1,
|
||||
]);
|
||||
$node2->save();
|
||||
|
||||
$node1->set('test_field_reference', $node2->id());
|
||||
$node1->save();
|
||||
|
||||
$settings = [
|
||||
'node' => [
|
||||
'default_value' => 1,
|
||||
'disable' => 0,
|
||||
'hidden' => 0,
|
||||
],
|
||||
];
|
||||
\Drupal::service('config.factory')->getEditable('entity_clone.settings')->set('form_settings', $settings)->save();
|
||||
|
||||
$this->drupalPostForm('entity_clone/node/' . $node1->id(), [], t('Clone'));
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $node1_title . ' - Cloned',
|
||||
]);
|
||||
/** @var \Drupal\node\Entity\Node $node1cloned */
|
||||
$node1cloned = reset($nodes);
|
||||
$this->assertInstanceOf(Node::class, $node1cloned, 'Node 1 cloned found in database.');
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $node2_title . ' - Cloned',
|
||||
]);
|
||||
/** @var \Drupal\node\Entity\Node $node2cloned */
|
||||
$node2cloned = reset($nodes);
|
||||
$this->assertInstanceOf(Node::class, $node2cloned, 'Node 2 cloned found in database.');
|
||||
|
||||
$reference = $node2cloned->get('test_field_reference')->first()->get('entity')->getTarget()->getValue();
|
||||
$this->assertEquals($node1cloned->id(), $reference->id(), "Node 1 reference, from circular reference, is correctly referenced.");
|
||||
|
||||
$node1cloned->delete();
|
||||
$node2cloned->delete();
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $node1_title,
|
||||
]);
|
||||
$node = reset($nodes);
|
||||
$this->assertInstanceOf(Node::class, $node, 'Test original node 1 found in database.');
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $node2_title,
|
||||
]);
|
||||
$node = reset($nodes);
|
||||
$this->assertInstanceOf(Node::class, $node, 'Test original node 2 found in database.');
|
||||
}
|
||||
|
||||
public function testContentWithTwoSameEntityReference() {
|
||||
$child_node1_title = $this->randomMachineName(8);
|
||||
$child_node1 = Node::create([
|
||||
'type' => 'test_content_type',
|
||||
'title' => $child_node1_title,
|
||||
]);
|
||||
$child_node1->save();
|
||||
|
||||
$parent_node_title = $this->randomMachineName(8);
|
||||
$parent_node = Node::create([
|
||||
'type' => 'test_content_type',
|
||||
'title' => $parent_node_title,
|
||||
'test_field_reference' => $child_node1,
|
||||
'test_another_field_reference' => $child_node1,
|
||||
]);
|
||||
$parent_node->save();
|
||||
|
||||
$settings = [
|
||||
'node' => [
|
||||
'default_value' => 0,
|
||||
'disable' => 0,
|
||||
'hidden' => 0,
|
||||
],
|
||||
];
|
||||
\Drupal::service('config.factory')->getEditable('entity_clone.settings')->set('form_settings', $settings)->save();
|
||||
|
||||
$this->drupalPostForm('entity_clone/node/' . $parent_node->id(), [], t('Clone'));
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $parent_node_title . ' - Cloned',
|
||||
]);
|
||||
/** @var \Drupal\node\Entity\Node $parent_node_cloned */
|
||||
$parent_node_cloned = reset($nodes);
|
||||
|
||||
$this->drupalGet('node/' . $parent_node_cloned->id());
|
||||
|
||||
$first_reference = $parent_node_cloned->get('test_field_reference')->first()->get('entity')->getTarget()->getValue();
|
||||
$second_reference = $parent_node_cloned->get('test_another_field_reference')->first()->get('entity')->getTarget()->getValue();
|
||||
$this->assertEquals($child_node1->id(), $first_reference->id(), "Entity referenced twice time is correctly reused.");
|
||||
$this->assertEquals($child_node1->id(), $second_reference->id(), "Entity referenced twice time is correctly reused.");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\node\Functional\NodeTestBase;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
|
||||
/**
|
||||
* Create a content and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneContentRecursiveTest extends NodeTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'block', 'node', 'datetime'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Profile to install.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $profile = 'standard';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'bypass node access',
|
||||
'administer nodes',
|
||||
'clone node entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* A user with permission to bypass content access checks.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test clone a content entity with another entities attached.
|
||||
*/
|
||||
public function testContentEntityClone() {
|
||||
|
||||
$term_title = $this->randomMachineName(8);
|
||||
$term = Term::create([
|
||||
'vid' => 'tags',
|
||||
'name' => $term_title,
|
||||
]);
|
||||
$term->save();
|
||||
|
||||
$node_title = $this->randomMachineName(8);
|
||||
$node = Node::create([
|
||||
'type' => 'article',
|
||||
'title' => $node_title,
|
||||
'field_tags' => [
|
||||
'target_id' => $term->id(),
|
||||
],
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$settings = [
|
||||
'taxonomy_term' => [
|
||||
'default_value' => 1,
|
||||
'disable' => 0,
|
||||
'hidden' => 0,
|
||||
],
|
||||
];
|
||||
\Drupal::service('config.factory')->getEditable('entity_clone.settings')->set('form_settings', $settings)->save();
|
||||
|
||||
$this->drupalPostForm('entity_clone/node/' . $node->id(), [
|
||||
'recursive[node.article.field_tags][references][' . $term->id() . '][clone]' => 1,
|
||||
], t('Clone'));
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $node_title . ' - Cloned',
|
||||
]);
|
||||
/** @var \Drupal\node\Entity\Node $node */
|
||||
$node = reset($nodes);
|
||||
$this->assertInstanceOf(Node::class, $node, 'Test node cloned found in database.');
|
||||
|
||||
$terms = \Drupal::entityTypeManager()
|
||||
->getStorage('taxonomy_term')
|
||||
->loadByProperties([
|
||||
'name' => $term_title . ' - Cloned',
|
||||
]);
|
||||
/** @var \Drupal\taxonomy\Entity\Term $term */
|
||||
$term = reset($terms);
|
||||
$this->assertInstanceOf(Term::class, $term, 'Test term referenced by node cloned too found in database.');
|
||||
|
||||
$node->delete();
|
||||
$term->delete();
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $node_title,
|
||||
]);
|
||||
$node = reset($nodes);
|
||||
$this->assertInstanceOf(Node::class, $node, 'Test original node found in database.');
|
||||
|
||||
$terms = \Drupal::entityTypeManager()
|
||||
->getStorage('taxonomy_term')
|
||||
->loadByProperties([
|
||||
'name' => $term_title,
|
||||
]);
|
||||
$term = reset($terms);
|
||||
$this->assertInstanceOf(Term::class, $term, 'Test original term found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
|
||||
use Drupal\Tests\node\Functional\NodeTestBase;
|
||||
|
||||
/**
|
||||
* Create a content and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneContentTest extends NodeTestBase {
|
||||
|
||||
use EntityReferenceTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'block', 'node', 'datetime', 'taxonomy'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'bypass node access',
|
||||
'administer nodes',
|
||||
'clone node entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* A user with permission to bypass content access checks.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test content entity clone.
|
||||
*/
|
||||
public function testContentEntityClone() {
|
||||
$node_title = $this->randomMachineName(8);
|
||||
$node = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => $node_title,
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$this->drupalPostForm('entity_clone/node/' . $node->id(), [], t('Clone'));
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => $node_title . ' - Cloned',
|
||||
]);
|
||||
$node = reset($nodes);
|
||||
$this->assertInstanceOf(Node::class, $node, 'Test node cloned found in database.');
|
||||
}
|
||||
|
||||
public function testContentReferenceConfigEntity() {
|
||||
$this->createEntityReferenceField('node', 'page', 'config_field_reference', 'Config field reference', 'taxonomy_vocabulary');
|
||||
|
||||
$node_title = $this->randomMachineName(8);
|
||||
$node = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => $node_title,
|
||||
'config_field_reference' => 'tags'
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$this->drupalGet('entity_clone/node/' . $node->id());
|
||||
$this->assertSession()->elementNotExists('css', '#edit-recursive-nodepageconfig-field-reference');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\block_content\Entity\BlockContent;
|
||||
use Drupal\Tests\block_content\Functional\BlockContentTestBase;
|
||||
|
||||
/**
|
||||
* Creat ea block and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneCustomBlockTest extends BlockContentTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* Enable dummy module that implements hook_block_insert() for exceptions and
|
||||
* field_ui to edit display settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'block', 'block_content'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = ['administer blocks', 'clone block_content entity'];
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test custom block entity clone.
|
||||
*/
|
||||
public function testCustomBlockEntityClone() {
|
||||
|
||||
$edit = [];
|
||||
$edit['info[0][value]'] = 'Test block ready to clone';
|
||||
$edit['body[0][value]'] = $this->randomMachineName(16);
|
||||
$this->drupalPostForm('block/add/basic', $edit, t('Save'));
|
||||
|
||||
$blocks = \Drupal::entityTypeManager()
|
||||
->getStorage('block_content')
|
||||
->loadByProperties([
|
||||
'info' => $edit['info[0][value]'],
|
||||
]);
|
||||
$block = reset($blocks);
|
||||
$this->assertInstanceOf(BlockContent::class, $block, 'Test Block for clone found in database.');
|
||||
|
||||
$this->drupalPostForm('entity_clone/block_content/' . $block->id(), [], t('Clone'));
|
||||
|
||||
$blocks = \Drupal::entityTypeManager()
|
||||
->getStorage('block_content')
|
||||
->loadByProperties([
|
||||
'info' => $edit['info[0][value]'] . ' - Cloned',
|
||||
'body' => $edit['body[0][value]'],
|
||||
]);
|
||||
$block = reset($blocks);
|
||||
$this->assertInstanceOf(BlockContent::class, $block, 'Test Block cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\Core\Datetime\Entity\DateFormat;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create a date format and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneDateFormatTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone date_format entity',
|
||||
'administer site configuration',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure date formats settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test date format entity clone.
|
||||
*/
|
||||
public function testDateFormatEntityClone() {
|
||||
$edit = [
|
||||
'label' => 'Test date format for clone',
|
||||
'id' => 'test_date_format_for_clone',
|
||||
'date_format_pattern' => 'Y m d',
|
||||
];
|
||||
$this->drupalPostForm("admin/config/regional/date-time/formats/add", $edit, t('Add format'));
|
||||
|
||||
$date_formats = \Drupal::entityTypeManager()
|
||||
->getStorage('date_format')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$date_format = reset($date_formats);
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_date_format_cloned',
|
||||
'label' => 'Test date format cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/date_format/' . $date_format->id(), $edit, t('Clone'));
|
||||
|
||||
$date_formats = \Drupal::entityTypeManager()
|
||||
->getStorage('date_format')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$date_format = reset($date_formats);
|
||||
$this->assertInstanceOf(DateFormat::class, $date_format, 'Test date format cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityFormMode;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Test an entity form mode clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneEntityFormModeTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone entity_form_mode entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user.
|
||||
*
|
||||
* With permission to configure entity form modes settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test entity form mode entity clone.
|
||||
*/
|
||||
public function testEntityFormModeEntityClone() {
|
||||
$entity_form_modes = \Drupal::entityTypeManager()
|
||||
->getStorage('entity_form_mode')
|
||||
->loadByProperties([
|
||||
'id' => 'user.register',
|
||||
]);
|
||||
$entity_form_mode = reset($entity_form_modes);
|
||||
|
||||
$edit = [
|
||||
'label' => 'User register cloned form mode',
|
||||
'id' => 'register_clone',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/entity_form_mode/' . $entity_form_mode->id(), $edit, t('Clone'));
|
||||
|
||||
$entity_form_modes = \Drupal::entityTypeManager()
|
||||
->getStorage('entity_form_mode')
|
||||
->loadByProperties([
|
||||
'id' => 'user.' . $edit['id'],
|
||||
]);
|
||||
$entity_form_mode = reset($entity_form_modes);
|
||||
$this->assertInstanceOf(EntityFormMode::class, $entity_form_mode, 'Test entity form mode cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityViewMode;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Test an entity view mode clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneEntityViewModeTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone entity_view_mode entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user.
|
||||
*
|
||||
* With permission to configure entity view modes settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test entity view mode entity clone.
|
||||
*/
|
||||
public function testEntityViewModeEntityClone() {
|
||||
$entity_view_modes = \Drupal::entityTypeManager()
|
||||
->getStorage('entity_view_mode')
|
||||
->loadByProperties([
|
||||
'id' => 'user.full',
|
||||
]);
|
||||
$entity_view_mode = reset($entity_view_modes);
|
||||
|
||||
$edit = [
|
||||
'label' => 'User full cloned view mode',
|
||||
'id' => 'register_clone',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/entity_view_mode/' . $entity_view_mode->id(), $edit, t('Clone'));
|
||||
|
||||
$entity_view_modes = \Drupal::entityTypeManager()
|
||||
->getStorage('entity_view_mode')
|
||||
->loadByProperties([
|
||||
'id' => 'user.' . $edit['id'],
|
||||
]);
|
||||
$entity_view_mode = reset($entity_view_modes);
|
||||
$this->assertInstanceOf(EntityViewMode::class, $entity_view_mode, 'Test entity view mode cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create a filer and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneFileTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'file'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone file entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure files settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test file entity clone.
|
||||
*/
|
||||
public function testFileEntityClone() {
|
||||
/** @var \Drupal\file\FileInterface $file */
|
||||
$file = File::create([
|
||||
'uid' => 1,
|
||||
'filename' => 'druplicon.txt',
|
||||
'uri' => 'public://druplicon.txt',
|
||||
'filemime' => 'text/plain',
|
||||
'status' => FILE_STATUS_PERMANENT,
|
||||
]);
|
||||
file_put_contents($file->getFileUri(), 'hello world');
|
||||
$file->save();
|
||||
|
||||
$this->drupalPostForm('entity_clone/file/' . $file->id(), [], t('Clone'));
|
||||
|
||||
$files = \Drupal::entityTypeManager()
|
||||
->getStorage('file')
|
||||
->loadByProperties([
|
||||
'filename' => 'druplicon.txt - Cloned',
|
||||
]);
|
||||
$file = reset($files);
|
||||
$this->assertInstanceOf(File::class, $file, 'Test file cloned found in database.');
|
||||
|
||||
$this->assertEqual($file->getFileUri(), 'public://druplicon_0.txt', 'The stored file is also cloned.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create a filter format and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneFilterFormatTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'filter'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone filter_format entity',
|
||||
'administer filters',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user.
|
||||
*
|
||||
* With permission to configure filter formats settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test filter format entity clone.
|
||||
*/
|
||||
public function testFilterFormatEntityClone() {
|
||||
$edit = [
|
||||
'name' => 'Test filter format for clone',
|
||||
'format' => 'test_filter_format_for_clone',
|
||||
];
|
||||
$this->drupalPostForm("admin/config/content/formats/add", $edit, t('Save configuration'));
|
||||
|
||||
$filter_formats = \Drupal::entityTypeManager()
|
||||
->getStorage('filter_format')
|
||||
->loadByProperties([
|
||||
'format' => $edit['format'],
|
||||
]);
|
||||
$filter_format = reset($filter_formats);
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_filter_format_cloned',
|
||||
'label' => 'Test filter format cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/filter_format/' . $filter_format->id(), $edit, t('Clone'));
|
||||
|
||||
$filter_formats = \Drupal::entityTypeManager()
|
||||
->getStorage('filter_format')
|
||||
->loadByProperties([
|
||||
'format' => $edit['id'],
|
||||
]);
|
||||
$filter_format = reset($filter_formats);
|
||||
$this->assertInstanceOf(FilterFormat::class, $filter_format, 'Test filter format cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\image\Entity\ImageStyle;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create an image style and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneImageStyleTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'image'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone image_style entity',
|
||||
'administer image styles',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure image styles settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test image style entity clone.
|
||||
*/
|
||||
public function testImageStyleEntityClone() {
|
||||
$edit = [
|
||||
'label' => 'Test image style for clone',
|
||||
'name' => 'test_image_style_for_clone',
|
||||
];
|
||||
$this->drupalPostForm("admin/config/media/image-styles/add", $edit, t('Create new style'));
|
||||
|
||||
$image_styles = \Drupal::entityTypeManager()
|
||||
->getStorage('image_style')
|
||||
->loadByProperties([
|
||||
'name' => $edit['name'],
|
||||
]);
|
||||
$image_style = reset($image_styles);
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_iamge_style_cloned',
|
||||
'label' => 'Test image_style cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/image_style/' . $image_style->id(), $edit, t('Clone'));
|
||||
|
||||
$image_styles = \Drupal::entityTypeManager()
|
||||
->getStorage('image_style')
|
||||
->loadByProperties([
|
||||
'name' => $edit['id'],
|
||||
]);
|
||||
$image_style = reset($image_styles);
|
||||
$this->assertInstanceOf(ImageStyle::class, $image_style, 'Test image style cloned found in database.');
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_image_style_clone_with_a_really_long_name_that_is_longer_than_the_max_length',
|
||||
'label' => 'Test image style clone with a really long name that is longer than the max length',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/image_style/' . $image_style->id(), $edit, t('Clone'));
|
||||
$this->assertText('New Id cannot be longer than 64 characters');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create an language and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneLanguageTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'language'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer languages',
|
||||
'clone configurable_language entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure languages settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test language entity clone.
|
||||
*/
|
||||
public function testLanguageEntityClone() {
|
||||
$edit = [
|
||||
'predefined_langcode' => 'fr',
|
||||
];
|
||||
$this->drupalPostForm("/admin/config/regional/language/add", $edit, t('Add language'));
|
||||
|
||||
$languages = \Drupal::entityTypeManager()
|
||||
->getStorage('configurable_language')
|
||||
->loadByProperties([
|
||||
'id' => 'fr',
|
||||
]);
|
||||
$language = reset($languages);
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_language_cloned',
|
||||
'label' => 'French language cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/configurable_language/' . $language->id(), $edit, t('Clone'));
|
||||
|
||||
$languages = \Drupal::entityTypeManager()
|
||||
->getStorage('configurable_language')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$language = reset($languages);
|
||||
$this->assertInstanceOf(ConfigurableLanguage::class, $language, 'Test language cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\system\Entity\Menu;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create a menu and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneMenuTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'menu_ui'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone menu entity',
|
||||
'administer menu',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure menus settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test menu entity clone.
|
||||
*/
|
||||
public function testMenuEntityClone() {
|
||||
|
||||
$menus = \Drupal::entityTypeManager()
|
||||
->getStorage('menu')
|
||||
->loadByProperties([
|
||||
'id' => 'account',
|
||||
]);
|
||||
$menu = reset($menus);
|
||||
|
||||
$edit = [
|
||||
'label' => 'Test menu cloned',
|
||||
'id' => 'test-menu-cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/menu/' . $menu->id(), $edit, t('Clone'));
|
||||
|
||||
$menus = \Drupal::entityTypeManager()
|
||||
->getStorage('menu')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$menu = reset($menus);
|
||||
$this->assertInstanceOf(Menu::class, $menu, 'Test menu cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create a responsive image style and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneResponsiveImageStyleTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'responsive_image'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone responsive_image_style entity',
|
||||
'administer responsive images',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure image styles settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test responsive image style entity clone.
|
||||
*/
|
||||
public function testResponsiveImageStyleEntityClone() {
|
||||
$edit = [
|
||||
'label' => 'Test responsive image style for clone',
|
||||
'id' => 'test_responsive_image_style_for_clone',
|
||||
'breakpoint_group' => 'responsive_image',
|
||||
'fallback_image_style' => 'large',
|
||||
];
|
||||
$this->drupalPostForm("admin/config/media/responsive-image-style/add", $edit, t('Save'));
|
||||
|
||||
$responsive_image_styles = \Drupal::entityTypeManager()
|
||||
->getStorage('responsive_image_style')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$responsive_image_style = reset($responsive_image_styles);
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_responsive_image_style_cloned',
|
||||
'label' => 'Test responsive image style cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/responsive_image_style/' . $responsive_image_style->id(), $edit, t('Clone'));
|
||||
|
||||
$responsive_image_styles = \Drupal::entityTypeManager()
|
||||
->getStorage('responsive_image_style')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$responsive_image_style = reset($responsive_image_styles);
|
||||
$this->assertInstanceOf(ResponsiveImageStyle::class, $responsive_image_style, 'Test responsive image style cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
|
||||
/**
|
||||
* Create a role and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneRoleTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'user'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer permissions',
|
||||
'clone user_role entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure roles settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test role entity clone.
|
||||
*/
|
||||
public function testRoleEntityClone() {
|
||||
$edit = [
|
||||
'label' => 'Test role for clone',
|
||||
'id' => 'test_role_for_clone',
|
||||
];
|
||||
$this->drupalPostForm("/admin/people/roles/add", $edit, t('Save'));
|
||||
|
||||
$roles = \Drupal::entityTypeManager()
|
||||
->getStorage('user_role')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$role = reset($roles);
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_role_cloned',
|
||||
'label' => 'Test role cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/user_role/' . $role->id(), $edit, t('Clone'));
|
||||
|
||||
$roles = \Drupal::entityTypeManager()
|
||||
->getStorage('user_role')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$role = reset($roles);
|
||||
$this->assertInstanceOf(Role::class, $role, 'Test role cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\search\Entity\SearchPage;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create a search page and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneSearchPageTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'search', 'node'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer search',
|
||||
'clone search_page entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure search pages settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test search page entity clone.
|
||||
*/
|
||||
public function testSearchPageEntityClone() {
|
||||
$edit = [
|
||||
'label' => 'Test search page for clone',
|
||||
'id' => 'test_search_page_for_clone',
|
||||
'path' => 'test_search_page_for_clone_url',
|
||||
];
|
||||
$this->drupalPostForm("/admin/config/search/pages/add/node_search", $edit, t('Save'));
|
||||
|
||||
$search_pages = \Drupal::entityTypeManager()
|
||||
->getStorage('search_page')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$search_page = reset($search_pages);
|
||||
|
||||
$edit = [
|
||||
'id' => 'test_search_page_cloned',
|
||||
'label' => 'Test search page cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/search_page/' . $search_page->id(), $edit, t('Clone'));
|
||||
|
||||
$search_pages = \Drupal::entityTypeManager()
|
||||
->getStorage('search_page')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$search_page = reset($search_pages);
|
||||
$this->assertInstanceOf(SearchPage::class, $search_page, 'Test search page cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\shortcut\Entity\ShortcutSet;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Create a shortcut set and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneShortcutSetTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'shortcut'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone shortcut_set entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure shortcuts settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test shortcut set entity clone.
|
||||
*/
|
||||
public function testShortcutSetEntityClone() {
|
||||
$edit = [
|
||||
'id' => 'test_shortcut_set_cloned',
|
||||
'label' => 'Test shortcut set cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/shortcut_set/default', $edit, t('Clone'));
|
||||
|
||||
$shortcut_sets = \Drupal::entityTypeManager()
|
||||
->getStorage('shortcut_set')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$shortcut_set = reset($shortcut_sets);
|
||||
$this->assertInstanceOf(ShortcutSet::class, $shortcut_set, 'Test default shortcut set cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\User;
|
||||
|
||||
/**
|
||||
* Create a user and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneUserTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'user'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone user entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure users settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions, 'test_user');
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user entity clone.
|
||||
*/
|
||||
public function testUserEntityClone() {
|
||||
$this->drupalPostForm('entity_clone/user/' . $this->adminUser->id(), [], t('Clone'));
|
||||
|
||||
$users = \Drupal::entityTypeManager()
|
||||
->getStorage('user')
|
||||
->loadByProperties([
|
||||
'name' => 'test_user_cloned',
|
||||
]);
|
||||
$user = reset($users);
|
||||
$this->assertInstanceOf(User::class, $user, 'Test user cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\entity_clone\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\views\Entity\View;
|
||||
|
||||
/**
|
||||
* Create a view and test a clone.
|
||||
*
|
||||
* @group entity_clone
|
||||
*/
|
||||
class EntityCloneViewTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['entity_clone', 'views'];
|
||||
|
||||
/**
|
||||
* Theme to enable by default
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'clone view entity',
|
||||
];
|
||||
|
||||
/**
|
||||
* An administrative user with permission to configure views settings.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test view entity clone.
|
||||
*/
|
||||
public function testViewEntityClone() {
|
||||
$edit = [
|
||||
'id' => 'test_view_cloned',
|
||||
'label' => 'Test view cloned',
|
||||
];
|
||||
$this->drupalPostForm('entity_clone/view/who_s_new', $edit, t('Clone'));
|
||||
|
||||
$views = \Drupal::entityTypeManager()
|
||||
->getStorage('view')
|
||||
->loadByProperties([
|
||||
'id' => $edit['id'],
|
||||
]);
|
||||
$view = reset($views);
|
||||
$this->assertInstanceOf(View::class, $view, 'Test default view cloned found in database.');
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
services:
|
||||
google_analytics.visibility:
|
||||
class: Drupal\google_analytics\Helpers\VisiblityTracker
|
||||
arguments: [ '@config.factory', '@path_alias.manager', '@path.matcher', '@user.data', '@path.current' ]
|
||||
google_analytics.accounts:
|
||||
class: Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts
|
||||
arguments: [ '@config.factory', '@private_key' ]
|
||||
google_analytics.javascript_cache:
|
||||
class: Drupal\google_analytics\JavascriptLocalCache
|
||||
arguments: [ '@http_client', '@file_system', '@config.factory', '@logger.factory', '@state' ]
|
||||
|
||||
# Google Analytics Event Subscribers
|
||||
google_analytics.events.messages:
|
||||
class: '\Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents\DrupalMessage'
|
||||
arguments: [ '@config.factory', '@google_analytics.accounts', '@messenger' ]
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
google_analytics.pagepath.content_translation:
|
||||
class: '\Drupal\google_analytics\EventSubscriber\PagePath\ContentTranslation'
|
||||
arguments: [ '@config.factory', '@request_stack', '@module_handler', '@entity.repository' ]
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
google_analytics.pagepath.http_status:
|
||||
class: '\Drupal\google_analytics\EventSubscriber\PagePath\HttpStatus'
|
||||
arguments: [ '@config.factory', '@request_stack' ]
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
google_analytics.pagepath.invalid_user_login:
|
||||
class: '\Drupal\google_analytics\EventSubscriber\PagePath\InvalidUserLogin'
|
||||
arguments: [ '@request_stack', '@current_route_match' ]
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
google_analytics.pagepath.search:
|
||||
class: '\Drupal\google_analytics\EventSubscriber\PagePath\Search'
|
||||
arguments: [ '@config.factory', '@request_stack', '@module_handler', '@current_route_match' ]
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
google_analytics.config.default_config:
|
||||
class: '\Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig\DefaultConfig'
|
||||
arguments: [ '@config.factory', '@google_analytics.accounts', '@current_user' ]
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
||||
google_analytics.config.custom_config:
|
||||
class: '\Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig\CustomConfig'
|
||||
arguments: [ '@config.factory', '@current_user', '@request_stack', '@token' ]
|
||||
tags:
|
||||
- { name: 'event_subscriber' }
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\Constants;
|
||||
|
||||
/**
|
||||
* Defines events for the google_analytics module.
|
||||
*/
|
||||
final class GoogleAnalyticsEvents {
|
||||
|
||||
/**
|
||||
* The event fired to build the Google Analytics javascript.
|
||||
*
|
||||
* Each action in Drupal that is tracked with google Analytics should have its
|
||||
* own event subscriber to compile into the final javascript.
|
||||
*
|
||||
* @Event
|
||||
*
|
||||
* @see \Drupal\google_analytics\Event\BuildGaJavascriptEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const BUILD_JAVASCRIPT = 'google_analytics_build_javascript';
|
||||
|
||||
/**
|
||||
* The event fired to build the Google Analytics javascript.
|
||||
*
|
||||
* Each action in Drupal that is tracked with google Analytics should have its
|
||||
* own event subscriber to compile into the final javascript.
|
||||
*
|
||||
* @Event
|
||||
*
|
||||
* @see \Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const ADD_EVENT = 'google_analytics_add_event';
|
||||
|
||||
/**
|
||||
* The event fired to build the Google Analytics javascript.
|
||||
*
|
||||
* Each action in Drupal that is tracked with google Analytics should have its
|
||||
* own event subscriber to compile into the final javascript.
|
||||
*
|
||||
* @Event
|
||||
*
|
||||
* @see \Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const ADD_CONFIG = 'google_analytics_add_config';
|
||||
|
||||
/**
|
||||
* The event fired to set custom page paths.
|
||||
*
|
||||
* Each built in page path should stop propigation once it is found.
|
||||
* This will then set the custom page path in analytics.
|
||||
*
|
||||
* @Event
|
||||
*
|
||||
* @see \Drupal\google_analytics\Event\PagePathEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const PAGE_PATH = 'google_analytics_page_path';
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\Constants;
|
||||
|
||||
/**
|
||||
* Defines regex patterns for matching Google Analytics variables.
|
||||
*/
|
||||
final class GoogleAnalyticsPatterns {
|
||||
|
||||
/**
|
||||
* Define the default file extension list that should be tracked as download.
|
||||
*/
|
||||
const GOOGLE_ANALYTICS_TRACKFILES_EXTENSIONS = '7z|aac|arc|arj|asf|asx|avi|bin|csv|doc(x|m)?|dot(x|m)?|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|msi|msp|pdf|phps|png|ppt(x|m)?|pot(x|m)?|pps(x|m)?|ppam|sld(x|m)?|thmx|qtm?|ra(m|r)?|sea|sit|tar|tgz|torrent|txt|wav|wma|wmv|wpd|xls(x|m|b)?|xlt(x|m)|xlam|xml|z|zip';
|
||||
|
||||
/**
|
||||
* Define the Acceptable GA ID Patterns
|
||||
*/
|
||||
const GOOGLE_ANALYTICS_GTAG_MATCH = '/(?:UA|G|AW|DC)-[0-9a-zA-Z]{5,}(?:-[0-9]{1,})?/';
|
||||
|
||||
/**
|
||||
* Define the Acceptable tracking ID patterns
|
||||
*/
|
||||
const GOOGLE_ANALYTICS_TRACKING_MATCH = '/(?:UA|G)-[0-9a-zA-Z]{5,}(?:-[0-9]{1,})?/';
|
||||
|
||||
/**
|
||||
* Define the pattern matching a universal analytics account.
|
||||
*/
|
||||
const GOOGLE_ANALYTICS_UA_MATCH = '/(?:UA)-[0-9a-zA-Z]{5,}(?:-[0-9]{1,})?/';
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\Event;
|
||||
|
||||
use Drupal\google_analytics\GaAccount;
|
||||
use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Drupal\google_analytics\GaJavascriptObject;
|
||||
|
||||
/**
|
||||
* Event that gathers all the config settings for a GA account.
|
||||
*/
|
||||
class GoogleAnalyticsConfigEvent extends Event {
|
||||
|
||||
/**
|
||||
* The GA Javascript Object for which to store config.
|
||||
*
|
||||
* @var \Drupal\google_analytics\GaJavascriptObject
|
||||
*/
|
||||
protected $javascript;
|
||||
|
||||
/**
|
||||
* Array representing the config to pass to GA.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Array representing the config to pass to GA.
|
||||
*
|
||||
* @var \Drupal\google_analytics\GaAccount
|
||||
*/
|
||||
protected $gaAccount;
|
||||
|
||||
/**
|
||||
* GoogleAnalyticsConfigEvent constructor.
|
||||
*
|
||||
* @param \Drupal\google_analytics\GaJavascriptObject $javascript
|
||||
* The GA Javascript Object.
|
||||
*/
|
||||
public function __construct(GaJavascriptObject $javascript, GaAccount $ga_account) {
|
||||
$this->javascript = $javascript;
|
||||
$this->gaAccount = $ga_account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GA Javascript Object.
|
||||
*
|
||||
* @return \Drupal\google_analytics\GaJavascriptObject
|
||||
* The GA Javascript
|
||||
*/
|
||||
public function getJavascript() {
|
||||
return $this->javascript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the specific Google Analytics account associated with this config.
|
||||
*
|
||||
* @return \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts
|
||||
*/
|
||||
public function getGaAccount() {
|
||||
return $this->gaAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GA Javascript Object being created.
|
||||
*
|
||||
* @return array
|
||||
* Config to be set in the GA javascript
|
||||
*/
|
||||
public function getConfig() {
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a config key.
|
||||
*
|
||||
*/
|
||||
public function addConfig($config_key, $value) {
|
||||
$this->config[$config_key] = $value;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
use Drupal\google_analytics\GaJavascriptObject;
|
||||
|
||||
/**
|
||||
* Event that is fired when a user logs in.
|
||||
*/
|
||||
class GoogleAnalyticsEventsEvent extends Event {
|
||||
|
||||
/**
|
||||
* The GA Javascript Object for which to create events.
|
||||
*
|
||||
* @var \Drupal\google_analytics\GaJavascriptObject
|
||||
*/
|
||||
protected $javascript;
|
||||
|
||||
/**
|
||||
* GoogleAnalyticsEventsEvent constructor.
|
||||
*
|
||||
* @param \Drupal\google_analytics\GaJavascriptObject $javascript
|
||||
* The GA Javascript object.
|
||||
*/
|
||||
public function __construct(GaJavascriptObject $javascript) {
|
||||
$this->javascript = $javascript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GA Javascript Object being created.
|
||||
*
|
||||
* @return array
|
||||
* Events in the javascript.
|
||||
*/
|
||||
public function getEvents() {
|
||||
return $this->javascript->getEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GA Javascript Object being created.
|
||||
*/
|
||||
public function addEvent($event) {
|
||||
$this->javascript->addEvent($event);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Event that is fired when a user logs in.
|
||||
*/
|
||||
class PagePathEvent extends Event {
|
||||
|
||||
/**
|
||||
* The Custom URL to be attached to the GA javascript.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $page_path = '';
|
||||
|
||||
/**
|
||||
* Get the current page path
|
||||
*
|
||||
* @return string
|
||||
* The currently set custom url in the javascript.
|
||||
*/
|
||||
public function getPagePath() {
|
||||
return $this->page_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GA Javascript Object being created.
|
||||
*/
|
||||
public function setPagePath($url) {
|
||||
$this->page_path = $url;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\Core\Utility\Token;
|
||||
use Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent;
|
||||
use Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Adds custom Dimensions and Metrics to config and events.
|
||||
*/
|
||||
class CustomConfig implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Drupal Config Factory
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Current Drupal User Account.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountProxyInterface
|
||||
*/
|
||||
protected $currentAccount;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Utility\Token
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
/**
|
||||
* @var \Symfony\Component\HttpFoundation\Request|null
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* Custom Mapping of Vars.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $custom_map = [];
|
||||
|
||||
/**
|
||||
* Custom Variables passed to GA.
|
||||
* @var array
|
||||
*/
|
||||
protected $custom_vars = [];
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, AccountProxyInterface $account, RequestStack $request, Token $token) {
|
||||
$this->config = $config_factory->get('google_analytics.settings');
|
||||
$this->currentAccount = $account;
|
||||
$this->request = $request->getCurrentRequest();
|
||||
$this->token = $token;
|
||||
|
||||
// Populate custom map/vars
|
||||
$this->populateCustomConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[GoogleAnalyticsEvents::ADD_CONFIG][] = ['onAddConfig'];
|
||||
$events[GoogleAnalyticsEvents::ADD_EVENT][] = ['onAddEvent'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new event to the Ga Javascript
|
||||
*
|
||||
* @param \Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent $event
|
||||
* The event being dispatched.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function onAddConfig(GoogleAnalyticsConfigEvent $event) {
|
||||
// Don't execute event if there is nothing in the mapping fields.
|
||||
if (empty($this->custom_map)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only populate the config on UA accounts.
|
||||
if ($event->getGaAccount()->isUniversalAnalyticsAccount()) {
|
||||
$event->addConfig('custom_map', $this->custom_map['custom_map']);
|
||||
}
|
||||
}
|
||||
|
||||
public function onAddEvent(GoogleAnalyticsEventsEvent $event) {
|
||||
// Don't execute event if there is nothing in the mapping fields.
|
||||
if (empty($this->custom_vars)) {
|
||||
return;
|
||||
}
|
||||
$event->addEvent(['custom' => $this->custom_vars]);
|
||||
}
|
||||
|
||||
protected function populateCustomConfig() {
|
||||
// Add custom dimensions and metrics.
|
||||
$custom_parameters = $this->config->get('custom.parameters');
|
||||
if (!empty($custom_parameters)) {
|
||||
// Add all the configured variables to the content.
|
||||
foreach ($custom_parameters as $index => $custom_parameter) {
|
||||
// Replace tokens in values.
|
||||
$types = [];
|
||||
if ($this->request->attributes->has('node')) {
|
||||
$node = $this->request->attributes->get('node');
|
||||
if ($node instanceof NodeInterface) {
|
||||
$types += ['node' => $node];
|
||||
}
|
||||
}
|
||||
$custom_parameter['value'] = $this->token->replace($custom_parameter['value'], $types, ['clear' => TRUE]);
|
||||
|
||||
// Suppress empty values.
|
||||
if ((isset($custom_parameter['name']) && !mb_strlen(trim($custom_parameter['name']))) || !mb_strlen(trim($custom_parameter['value']))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Per documentation the max length of a dimension is 150 bytes.
|
||||
// A metric has no length limitation. It's not documented if this
|
||||
// limit means 150 bytes after url encoding or before.
|
||||
// See https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#customs
|
||||
if ($custom_parameter['type'] == 'dimension' && mb_strlen($custom_parameter['value']) > 150) {
|
||||
$custom_parameter['value'] = substr($custom_parameter['value'], 0, 150);
|
||||
}
|
||||
|
||||
// Cast metric values for json_encode to data type numeric.
|
||||
if ($custom_parameter['type'] == 'metric') {
|
||||
settype($custom_parameter['value'], 'float');
|
||||
};
|
||||
|
||||
// Build the arrays of values.
|
||||
$this->custom_map['custom_map'][$index] = ($custom_parameter['name'] ?? "");
|
||||
if (isset($custom_parameter['name'])) {
|
||||
$this->custom_vars[$custom_parameter['name']] = $custom_parameter['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsConfig;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent;
|
||||
use Drupal\google_analytics\Event\PagePathEvent;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Adds default config to Google Analytics.
|
||||
*/
|
||||
class DefaultConfig implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Drupal Config Factory
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Current Drupal User Account.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountProxyInterface
|
||||
*/
|
||||
protected $currentAccount;
|
||||
|
||||
/**
|
||||
* The Global Google Analytics Accounts Service
|
||||
*
|
||||
* @var \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts
|
||||
*/
|
||||
protected $gaAccounts;
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, GoogleAnalyticsAccounts $ga_accounts, AccountProxyInterface $account) {
|
||||
$this->config = $config_factory->get('google_analytics.settings');
|
||||
$this->gaAccounts = $ga_accounts;
|
||||
$this->currentAccount = $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[GoogleAnalyticsEvents::ADD_CONFIG][] = ['onAddConfig'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new event to the Ga Javascript
|
||||
*
|
||||
* @param \Drupal\google_analytics\Event\GoogleAnalyticsConfigEvent $event
|
||||
* The event being dispatched.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function onAddConfig(GoogleAnalyticsConfigEvent $event) {
|
||||
$javascript = $event->getJavascript();
|
||||
$ga_account = $event->getGaAccount();
|
||||
|
||||
// Custom Code Snippets that aren't created programmatically.
|
||||
$codesnippet_parameters = $this->config->get('codesnippet.create') ?? [];
|
||||
|
||||
// Build the arguments fields list.
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/sending-data
|
||||
$arguments = ['groups' => 'default'];
|
||||
$arguments = array_merge($arguments, $codesnippet_parameters);
|
||||
|
||||
// Domain tracking type.
|
||||
global $cookie_domain;
|
||||
$domain_mode = $this->config->get('domain_mode');
|
||||
|
||||
// Per RFC 2109, cookie domains must contain at least one dot other than the
|
||||
// first. For hosts such as 'localhost' or IP Addresses we don't set a
|
||||
// cookie domain.
|
||||
if ($domain_mode == 1 && count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
|
||||
$arguments = array_merge($arguments, ['cookie_domain' => $cookie_domain]);
|
||||
$javascript->setAdsenseScript($cookie_domain);
|
||||
}
|
||||
elseif ($domain_mode == 2) {
|
||||
// Cross Domain tracking
|
||||
// https://developers.google.com/analytics/devguides/collection/gtagjs/cross-domain
|
||||
$arguments['linker'] = [
|
||||
'domains' => preg_split('/(\r\n?|\n)/', $this->config->get('cross_domains')),
|
||||
];
|
||||
$javascript->setAdsenseScript();
|
||||
}
|
||||
|
||||
// Track logged in users across all devices.
|
||||
if ($this->currentAccount->isAuthenticated()) {
|
||||
$arguments['user_id'] = $this->gaAccounts->getUserIdHash($this->currentAccount->id());
|
||||
}
|
||||
|
||||
// Eliminate for GA 4.x
|
||||
if ($this->config->get('privacy.anonymizeip') && $ga_account->isUniversalAnalyticsAccount()) {
|
||||
$arguments['anonymize_ip'] = TRUE;
|
||||
}
|
||||
|
||||
$page_path = new PagePathEvent();
|
||||
// Get the event_dispatcher service and dispatch the event.
|
||||
$event_dispatcher = \Drupal::service('event_dispatcher');
|
||||
$event_dispatcher->dispatch(GoogleAnalyticsEvents::PAGE_PATH, $page_path);
|
||||
|
||||
$path_type = $ga_account->isUniversalAnalyticsAccount() ? 'page_path' : 'page_location';
|
||||
$arguments['page_placeholder'] = 'PLACEHOLDER_' . $path_type;
|
||||
|
||||
// TODO: Rewrite this into the PagePath event that executes first.
|
||||
if ($this->config->get('track.urlfragments')) {
|
||||
$arguments['page'] = 'location.pathname + location.search + location.hash';
|
||||
}
|
||||
|
||||
if (!empty($page_path->getPagePath())) {
|
||||
$arguments['page'] = $page_path->getPagePath();
|
||||
}
|
||||
|
||||
// Add enhanced link attribution after 'create', but before 'pageview' send.
|
||||
// @see https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-link-attribution
|
||||
if ($this->config->get('track.linkid')) {
|
||||
$arguments['link_attribution'] = TRUE;
|
||||
}
|
||||
|
||||
// Disabling display features.
|
||||
// @see https://developers.google.com/analytics/devguides/collection/gtagjs/display-features
|
||||
if (!$this->config->get('track.displayfeatures')) {
|
||||
$arguments['allow_ad_personalization_signals'] = FALSE;
|
||||
}
|
||||
|
||||
foreach ($arguments as $config_key => $value) {
|
||||
$event->addConfig($config_key, $value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
|
||||
|
||||
/**
|
||||
* Adds Drupal Messages to GA Javascript.
|
||||
*/
|
||||
class DrupalMessage extends GoogleAnalyticsEventBase {
|
||||
|
||||
/**
|
||||
* Drupal Messenger Service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
* @param \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts $ga_accounts
|
||||
* The Google Analytics Account Service.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* Messenger Factory.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, GoogleAnalyticsAccounts $ga_accounts, MessengerInterface $messenger) {
|
||||
parent::__construct($config_factory, $ga_accounts);
|
||||
$this->messenger = $messenger;
|
||||
}
|
||||
|
||||
public function addGaEvent(): array {
|
||||
$events = [];
|
||||
if ($message_types = $this->ga_config->get('track.messages')) {
|
||||
$message_types = array_values(array_filter($message_types));
|
||||
$status_heading = [
|
||||
'status' => t('Status message'),
|
||||
'warning' => t('Warning message'),
|
||||
'error' => t('Error message'),
|
||||
];
|
||||
|
||||
foreach ($this->messenger->all() as $type => $messages) {
|
||||
// Track only the selected message types.
|
||||
if (in_array($type, $message_types)) {
|
||||
foreach ($messages as $message) {
|
||||
// Compatibility with 3.x and UA format.
|
||||
if ($this->isLegacy) {
|
||||
$events[] = [(string)$status_heading[$type] =>
|
||||
['event_category' => (string)t('Messages'),
|
||||
'event_label' => strip_tags((string) $message)
|
||||
]
|
||||
];
|
||||
}
|
||||
else {
|
||||
$events[] = [(string)$status_heading[$type] =>
|
||||
['value' => strip_tags((string) $message)]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent;
|
||||
use Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Class GoogleAnalyticsEventBase.
|
||||
*
|
||||
* Base class create events for Google Analytics.
|
||||
*
|
||||
* @package Drupal\google_analytics\EventSubscriber\GoogleAnalyticsEvents
|
||||
*/
|
||||
abstract class GoogleAnalyticsEventBase implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Google Analytics Config
|
||||
*
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
protected $ga_config;
|
||||
|
||||
/**
|
||||
* Priority of the subscriber.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $priority = 0;
|
||||
|
||||
/**
|
||||
* Detect Legacy Universal Analytics Accounts
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isLegacy;
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
* @param \Drupal\google_analytics\Helpers\GoogleAnalyticsAccounts $ga_accounts
|
||||
* The Google Analytics Account Service.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, GoogleAnalyticsAccounts $ga_accounts) {
|
||||
$this->ga_config = $config_factory->get('google_analytics.settings');
|
||||
$this->isLegacy = $ga_accounts->getDefaultMeasurementId()->isUniversalAnalyticsAccount();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[GoogleAnalyticsEvents::ADD_EVENT][] =
|
||||
['onAddEvent', self::$priority];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new event in an array format for UA or GA4.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function addGaEvent(): array;
|
||||
|
||||
/**
|
||||
* Adds a new event to the Ga Javascript
|
||||
*
|
||||
* @param \Drupal\google_analytics\Event\GoogleAnalyticsEventsEvent $event
|
||||
* The event being dispatched.
|
||||
*/
|
||||
public function onAddEvent(GoogleAnalyticsEventsEvent $event) {
|
||||
$ga_events = $this->addGaEvent();
|
||||
if (!empty($ga_events)) {
|
||||
foreach($ga_events AS $ga_event) {
|
||||
$event->addEvent($ga_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\PagePath;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Extension\ModuleHandler;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\google_analytics\Event\PagePathEvent;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Adds Content Translation to custom URL
|
||||
*/
|
||||
class ContentTranslation implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Drupal Config Factory
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Drupal Messenger Service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\Psr7\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Extension\ModuleHandler
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Routing\CurrentRouteMatch
|
||||
*/
|
||||
protected $currentRoute;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityRepositoryInterface
|
||||
*/
|
||||
protected $entityRepository;
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, RequestStack $request, ModuleHandler $module_handler, EntityRepositoryInterface $entity_repsoitory) {
|
||||
$this->config = $config_factory->get('google_analytics.settings');
|
||||
$this->request = $request->getCurrentRequest();
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->entityRepository = $entity_repsoitory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onPagePath'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new event to the Ga Javascript
|
||||
*
|
||||
* @param \Drupal\google_analytics\Event\PagePathEvent $event
|
||||
* The event being dispatched.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function onPagePath(PagePathEvent $event) {
|
||||
// Site search tracking support.
|
||||
// If this node is a translation of another node, pass the original
|
||||
// node instead.
|
||||
if ($this->moduleHandler->moduleExists('content_translation') && $this->config->get('translation_set')) {
|
||||
// Check if we have a node object, it has translation enabled, and its
|
||||
// language code does not match its source language code.
|
||||
if ($this->request->attributes->has('node')) {
|
||||
$node = $this->request->attributes->get('node');
|
||||
if ($node instanceof NodeInterface && $this->entityRepository->getTranslationFromContext($node) !== $node->getUntranslated()) {
|
||||
$url_custom = Json::encode(Url::fromRoute('entity.node.canonical', ['node' => $node->id()], ['language' => $node->getUntranslated()->language()])->toString());
|
||||
$event->setPagePath($url_custom);
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\PagePath;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\google_analytics\Event\PagePathEvent;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Adds Content Translation to custom URL
|
||||
*/
|
||||
class HttpStatus implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Drupal Config Factory
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\Psr7\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, RequestStack $request) {
|
||||
$this->config = $config_factory->get('google_analytics.settings');
|
||||
$this->request = $request->getCurrentRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onPagePath'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds error pages to the page path.
|
||||
*
|
||||
* @param \Drupal\google_analytics\Event\PagePathEvent $event
|
||||
* The event being dispatched.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function onPagePath(PagePathEvent $event) {
|
||||
// Get page http status code for visibility filtering.
|
||||
$status = NULL;
|
||||
if ($exception = $this->request->attributes->get('exception')) {
|
||||
$status = $exception->getStatusCode();
|
||||
}
|
||||
// TODO: Make configurable
|
||||
$trackable_status_codes = [
|
||||
// "Forbidden" status code.
|
||||
'403',
|
||||
// "Not Found" status code.
|
||||
'404',
|
||||
];
|
||||
if (in_array($status, $trackable_status_codes)) {
|
||||
$base_path = base_path();
|
||||
|
||||
// Track access denied (403) and file not found (404) pages.
|
||||
$event->setPagePath('"' . $base_path . $status . '.html?page=" + document.location.pathname + document.location.search + "&from=" + document.referrer');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\PagePath;
|
||||
|
||||
use Drupal\Core\Routing\CurrentRouteMatch;
|
||||
use Drupal\google_analytics\Event\PagePathEvent;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Adds Content Translation to custom URL
|
||||
*/
|
||||
class InvalidUserLogin implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\Psr7\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Routing\CurrentRouteMatch
|
||||
*/
|
||||
protected $currentRoute;
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
*/
|
||||
public function __construct(RequestStack $request, CurrentRouteMatch $current_route) {
|
||||
$this->request = $request->getCurrentRequest();
|
||||
$this->currentRoute = $current_route;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onPagePath', 100];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds error pages to the page path.
|
||||
*
|
||||
* @param \Drupal\google_analytics\Event\PagePathEvent $event
|
||||
* The event being dispatched.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function onPagePath(PagePathEvent $event) {
|
||||
// #2693595: User has entered an invalid login and clicked on forgot
|
||||
// password link. This link contains the username or email address and may
|
||||
// get send to Google if we do not override it. Override only if 'name'
|
||||
// query param exists. Last custom url condition, this need to win.
|
||||
//
|
||||
// URLs to protect are:
|
||||
// - user/password?name=username
|
||||
// - user/password?name=foo@example.com
|
||||
$base_path = base_path();
|
||||
if ($this->currentRoute->getRouteName() == 'user.pass' && $this->request->query->has('name')) {
|
||||
$event->setPagePath('"' . $base_path . 'user/password"');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\EventSubscriber\PagePath;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Extension\ModuleHandler;
|
||||
use Drupal\Core\Routing\CurrentRouteMatch;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\google_analytics\Event\PagePathEvent;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Adds Drupal Messages to GA Javascript.
|
||||
*/
|
||||
class Search implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Drupal Config Factory
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\Psr7\Request
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Extension\ModuleHandler
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Routing\CurrentRouteMatch
|
||||
*/
|
||||
protected $currentRoute;
|
||||
|
||||
/**
|
||||
* DrupalMessage constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* Config Factory for Google Analytics Settings.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, RequestStack $request, ModuleHandler $module_handler, CurrentRouteMatch $current_route) {
|
||||
$this->config = $config_factory->get('google_analytics.settings');
|
||||
$this->request = $request->getCurrentRequest();
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->currentRoute = $current_route;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[GoogleAnalyticsEvents::PAGE_PATH][] = ['onCustomPagePath'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new event to the Ga Javascript
|
||||
*
|
||||
* @param \Drupal\google_analytics\Event\PagePathEvent $event
|
||||
* The event being dispatched.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function onCustomPagePath(PagePathEvent $event) {
|
||||
// Site search tracking support.
|
||||
if ($this->moduleHandler->moduleExists('search') && $this->config->get('track.site_search') && (strpos($this->currentRoute->getRouteName(), 'search.view') === 0) && $keys = ($this->request->query->has('keys') ? trim($this->request->get('keys')) : '')) {
|
||||
// hook_item_list__search_results() is not executed if search result is
|
||||
// empty. Make sure the counter is set to 0 if there are no results.
|
||||
$entity = $this->currentRoute->getParameter('entity');
|
||||
if (isset($entity)) {
|
||||
$entity_id = $entity->id();
|
||||
$url_custom = '(window.google_analytics_search_results) ? ' . Json::encode(Url::fromRoute('search.view_' . $entity_id, [], ['query' => ['search' => $keys]])
|
||||
->toString()) . ' : ' . Json::encode(Url::fromRoute('search.view_' . $entity_id, [], [
|
||||
'query' => [
|
||||
'search' => 'no-results:' . $keys,
|
||||
'cat' => 'no-results'
|
||||
]
|
||||
])->toString());
|
||||
$event->setPagePath($url_custom);
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics;
|
||||
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsPatterns;
|
||||
|
||||
/**
|
||||
* Decorator class for Google Analytics accounts
|
||||
*/
|
||||
class GaAccount {
|
||||
|
||||
/**
|
||||
* The Google Analytics Account.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
public function __construct(string $account) {
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the account as a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if there is a universal analytics account.
|
||||
*
|
||||
* If any account is UA, then this will return true.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isUniversalAnalyticsAccount() {
|
||||
if (preg_match(GoogleAnalyticsPatterns::GOOGLE_ANALYTICS_UA_MATCH, $this->account)) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics;
|
||||
|
||||
/**
|
||||
* Interface GaJavascriptInterface.
|
||||
*
|
||||
* @package Drupal\google_analytics
|
||||
*/
|
||||
interface GaJavascriptInterface {
|
||||
|
||||
/**
|
||||
* Returns the Primary GA measurement ID.
|
||||
*
|
||||
* @return string
|
||||
* Object measurement ID.
|
||||
*/
|
||||
public function getMeasurementId();
|
||||
|
||||
/**
|
||||
* Returns object's config.
|
||||
*
|
||||
* @param string $measurement_id
|
||||
* The config's measurement_id.
|
||||
*
|
||||
* @return array
|
||||
* Object's config.
|
||||
*/
|
||||
public function getConfig($measurement_id);
|
||||
|
||||
/**
|
||||
* Metadata setter.
|
||||
*
|
||||
* @param array $config
|
||||
* Metadata array.
|
||||
*/
|
||||
public function setConfig($measurement_id, array $config);
|
||||
|
||||
/**
|
||||
* Returns all stored GA Events.
|
||||
*
|
||||
* @return array
|
||||
* Object's events.
|
||||
*/
|
||||
public function getEvents();
|
||||
|
||||
/**
|
||||
* Appends an event to the Javascript object.
|
||||
*
|
||||
* @param array $event
|
||||
* The event.
|
||||
*/
|
||||
public function addEvent(array $event);
|
||||
|
||||
/**
|
||||
* Converts object to array.
|
||||
*
|
||||
* @return mixed
|
||||
* Array representation.
|
||||
*/
|
||||
public function toArray();
|
||||
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
|
||||
/**
|
||||
* Class GaJavascript Object.
|
||||
*
|
||||
* @package Drupal\google_analytics
|
||||
*/
|
||||
class GaJavascriptObject implements GaJavascriptInterface {
|
||||
|
||||
/**
|
||||
* Default measurement_id for the object.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $measurement_id;
|
||||
|
||||
/**
|
||||
* Object config.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
/**
|
||||
* Events list.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $events = [];
|
||||
|
||||
/**
|
||||
* Custom URL.
|
||||
*/
|
||||
protected $custom_url = '';
|
||||
|
||||
/**
|
||||
* Adsense Script
|
||||
*/
|
||||
protected $adsense = '';
|
||||
|
||||
/**
|
||||
* GaJavascriptObject constructor.
|
||||
*
|
||||
* @param string $measurement_id
|
||||
* Object default measurement_id.
|
||||
* @param array $config
|
||||
* Object config.
|
||||
*/
|
||||
public function __construct($measurement_id, array $config = []) {
|
||||
$this->measurement_id = $measurement_id;
|
||||
$this->setConfig($measurement_id, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static Factory method to allow GaJavascript to interpret their own data.
|
||||
*
|
||||
* @param array $data
|
||||
* Initial data.
|
||||
*
|
||||
* @return \Drupal\google_analytics\GaJavascriptObject
|
||||
* GaJavascriptObject.
|
||||
*
|
||||
*/
|
||||
public static function fromArray(array $data) {
|
||||
$object = new static($data['measurement_id'], $data['config']['measurement_id']);
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static Factory method to format data from JSON into the Javascript Object.
|
||||
*
|
||||
* @param string $json
|
||||
* Data in JSON format.
|
||||
*
|
||||
* @return \Drupal\google_analytics\GaJavascriptObject
|
||||
* GA Javascript Object.
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public static function fromJson(string $json) {
|
||||
return self::fromArray(json_decode($json, TRUE));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toArray() {
|
||||
$output = [
|
||||
'measurement_id' => $this->getMeasurementId(),
|
||||
'config' => $this->config,
|
||||
'events' => $this->events,
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function getMeasurementId() {
|
||||
return $this->measurement_id;
|
||||
}
|
||||
|
||||
public function getConfig($measurement_id = NULL) {
|
||||
if (isset($this->config[$measurement_id ?? $this->measurement_id])) {
|
||||
return $this->config[$measurement_id ?? $this->measurement_id];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setConfig($measurement_id, array $config) {
|
||||
$this->config[(string)$measurement_id] = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEvents() {
|
||||
return $this->events;
|
||||
}
|
||||
|
||||
public function addEvent(array $event) {
|
||||
$this->events[] = $event;
|
||||
}
|
||||
|
||||
public function getCustomUrl() {
|
||||
return $this->custom_url;
|
||||
}
|
||||
|
||||
public function setCustomUrl($url) {
|
||||
$this->custom_url = $url;
|
||||
}
|
||||
|
||||
public function setAdsenseScript($domain = 'none') {
|
||||
$this->adsense = 'window.google_analytics_domain_name = ' . Json::encode($domain) . ';
|
||||
window.google_analytics_uacct = ' . Json::encode($this->measurement_id) . ';';
|
||||
}
|
||||
|
||||
public function getAdsenseScript() {
|
||||
return $this->adsense;
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\Helpers;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\PrivateKey;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\google_analytics\Constants\GoogleAnalyticsPatterns;
|
||||
use Drupal\google_analytics\GaAccount;
|
||||
|
||||
class GoogleAnalyticsAccounts {
|
||||
|
||||
/**
|
||||
* Private Key Service for generating user id hash.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $privateKey;
|
||||
|
||||
/**
|
||||
* The loaded config for the GA Module.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* The Google Analytics Accounts storage array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $accounts;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\Core\PrivateKey $private_key
|
||||
* The private key service.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, PrivateKey $private_key) {
|
||||
$this->config = $config_factory->get('google_analytics.settings');
|
||||
|
||||
$accounts = $this->config->get('account');
|
||||
// Create the accounts array from either a single gtag id or multiple ones.
|
||||
if (strpos($accounts, ',') === FALSE) {
|
||||
$this->accounts[] = new GaAccount($accounts);
|
||||
}
|
||||
else {
|
||||
$accounts_array = explode(',', $accounts);
|
||||
foreach($accounts_array as $account) {
|
||||
$this->accounts[] = new GaAccount($account);
|
||||
}
|
||||
}
|
||||
|
||||
$this->privateKey = $private_key->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate user id hash to implement USER_ID.
|
||||
*
|
||||
* The USER_ID value should be a unique, persistent, and non-personally
|
||||
* identifiable string identifier that represents a user or signed-in
|
||||
* account across devices.
|
||||
*
|
||||
* @param int $uid
|
||||
* User id.
|
||||
*
|
||||
* @return string
|
||||
* User id hash.
|
||||
*/
|
||||
public function getUserIdHash($uid) {
|
||||
return Crypt::hmacBase64($uid, $this->privateKey . Settings::getHashSalt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default measurement ID. Defaults to the first account in config.
|
||||
*
|
||||
* @return false|mixed|string
|
||||
*/
|
||||
public function getDefaultMeasurementId() {
|
||||
// The top UA- or G- Account is the default measurement ID.
|
||||
foreach ($this->accounts as $account) {
|
||||
if (preg_match(GoogleAnalyticsPatterns::GOOGLE_ANALYTICS_TRACKING_MATCH, $account)) {
|
||||
return $account;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accounts that aren't the default measurement ID.
|
||||
*
|
||||
* @return array|false|string[]
|
||||
*/
|
||||
public function getAdditionalAccounts() {
|
||||
return array_filter($this->accounts, function($v) {
|
||||
return $v !== $this->getDefaultMeasurementId();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the GA accounts stored.
|
||||
*
|
||||
* @return array|false|string[]
|
||||
*/
|
||||
public function getAccounts() {
|
||||
return $this->accounts;
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics\Helpers;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Path\CurrentPathStack;
|
||||
use Drupal\Core\Path\PathMatcherInterface;
|
||||
use Drupal\path_alias\AliasManagerInterface;
|
||||
use Drupal\user\UserDataInterface;
|
||||
|
||||
/**
|
||||
* Defines the Path Matcher class.
|
||||
*/
|
||||
class VisiblityTracker {
|
||||
|
||||
/**
|
||||
* @var \Drupal\path_alias\AliasManagerInterface
|
||||
*/
|
||||
private $aliasManager;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Path\PathMatcherInterface
|
||||
*/
|
||||
private $pathMatcher;
|
||||
|
||||
/**
|
||||
* @var \Drupal\user\UserDataInterface
|
||||
*/
|
||||
private $userData;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Path\CurrentPathStack
|
||||
*/
|
||||
private $currentPath;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\path_alias\AliasManagerInterface $alias_manager
|
||||
* The alias manager service.
|
||||
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
|
||||
* The path matcher service.
|
||||
* @param \Drupal\user\UserDataInterface $user_data
|
||||
* The user data service.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, AliasManagerInterface $alias_manager, PathMatcherInterface $path_matcher, UserDataInterface $user_data, CurrentPathStack $current_path) {
|
||||
$this->config = $config_factory->get('google_analytics.settings');
|
||||
$this->aliasManager = $alias_manager;
|
||||
$this->pathMatcher = $path_matcher;
|
||||
$this->userData = $user_data;
|
||||
$this->currentPath = $current_path;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tracking visibility check for an user object.
|
||||
*
|
||||
* @param object $account
|
||||
* A user object containing an array of roles to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the current user is being tracked by Google Analytics,
|
||||
* otherwise FALSE.
|
||||
*/
|
||||
public function getUserVisibilty($account) {
|
||||
$enabled = FALSE;
|
||||
|
||||
// Is current user a member of a role that should be tracked?
|
||||
if ($this->getVisibilityRoles($account)) {
|
||||
|
||||
// Use the user's block visibility setting, if necessary.
|
||||
if (($visibility_user_account_mode = $this->config->get('visibility.user_account_mode')) != 0) {
|
||||
$user_data_google_analytics = $this->userData->get('google_analytics', $account->id());
|
||||
if ($account->id() && isset($user_data_google_analytics['user_account_users'])) {
|
||||
$enabled = $user_data_google_analytics['user_account_users'];
|
||||
}
|
||||
else {
|
||||
$enabled = ($visibility_user_account_mode == 1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$enabled = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking visibility check for user roles.
|
||||
*
|
||||
* Based on visibility setting this function returns TRUE if JS code should
|
||||
* be added for the current role and otherwise FALSE.
|
||||
*
|
||||
* @param object $account
|
||||
* A user object containing an array of roles to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if JS code should be added for the current role and otherwise FALSE.
|
||||
*/
|
||||
public function getVisibilityRoles($account) {
|
||||
$enabled = $visibility_user_role_mode = $this->config->get('visibility.user_role_mode');
|
||||
$visibility_user_role_roles = $this->config->get('visibility.user_role_roles');
|
||||
|
||||
if (count($visibility_user_role_roles) > 0) {
|
||||
// One or more roles are selected.
|
||||
foreach (array_values($account->getRoles()) as $user_role) {
|
||||
// Is the current user a member of one of these roles?
|
||||
if (in_array($user_role, $visibility_user_role_roles)) {
|
||||
// Current user is a member of a role that should be tracked/excluded
|
||||
// from tracking.
|
||||
$enabled = !$visibility_user_role_mode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No role is selected for tracking, therefore all roles should be tracked.
|
||||
$enabled = TRUE;
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracking visibility check for pages.
|
||||
*
|
||||
* Based on visibility setting this function returns TRUE if JS code should
|
||||
* be added to the current page and otherwise FALSE.
|
||||
*/
|
||||
public function getVisibilityPages() {
|
||||
static $page_match;
|
||||
|
||||
// Cache visibility result if function is called more than once.
|
||||
if (!isset($page_match)) {
|
||||
$visibility_request_path_mode = $this->config->get('visibility.request_path_mode');
|
||||
$visibility_request_path_pages = $this->config->get('visibility.request_path_pages');
|
||||
|
||||
// Match path if necessary.
|
||||
if (!empty($visibility_request_path_pages)) {
|
||||
// Convert path to lowercase. This allows comparison of the same path
|
||||
// with different case. Ex: /Page, /page, /PAGE.
|
||||
$pages = mb_strtolower($visibility_request_path_pages);
|
||||
if ($visibility_request_path_mode < 2) {
|
||||
// Compare the lowercase path alias (if any) and internal path.
|
||||
$path = $this->currentPath->getPath();
|
||||
$path_alias = mb_strtolower($this->aliasManager->getAliasByPath($path));
|
||||
$page_match = $this->pathMatcher->matchPath($path_alias, $pages) || (($path != $path_alias) && $this->pathMatcher->matchPath($path, $pages));
|
||||
// When $visibility_request_path_mode has a value of 0, the tracking
|
||||
// code is displayed on all pages except those listed in $pages. When
|
||||
// set to 1, it is displayed only on those pages listed in $pages.
|
||||
$page_match = !($visibility_request_path_mode xor $page_match);
|
||||
}
|
||||
else {
|
||||
$page_match = FALSE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$page_match = TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
return $page_match;
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\google_analytics;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
class JavascriptLocalCache {
|
||||
|
||||
/**
|
||||
* Google Analytics Javascript URL.
|
||||
*/
|
||||
const GOOGLE_ANALYTICS_JAVASCRIPT_URL = 'https://www.googletagmanager.com/gtag/js';
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\File\FileSystem
|
||||
*/
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Logger\LoggerChannelInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* @var \GuzzleHttp\ClientInterface
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
public function __construct(ClientInterface $http_client, FileSystemInterface $file_system, ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory, StateInterface $state) {
|
||||
$this->httpClient = $http_client;
|
||||
$this->fileSystem = $file_system;
|
||||
$this->configFactory = $config_factory;
|
||||
$this->state = $state;
|
||||
$this->logger = $logger_factory->get('google_analytics');
|
||||
}
|
||||
/**
|
||||
* Download/Synchronize/Cache tracking code file locally.
|
||||
*
|
||||
* @param string $tracking_id
|
||||
* The GA Tracking ID
|
||||
* @param bool $synchronize
|
||||
* Synchronize to local cache if remote file has changed.
|
||||
*
|
||||
* @return string
|
||||
* The path to the local or remote tracking file.
|
||||
*/
|
||||
public function fetchGoogleAnalyticsJavascript(string $tracking_id, bool $synchronize = FALSE) {
|
||||
$path = 'public://google_analytics';
|
||||
$remote_url = self::GOOGLE_ANALYTICS_JAVASCRIPT_URL . '?id=' . $tracking_id;
|
||||
$file_destination = $path . '/gtag.js';
|
||||
|
||||
// If cache is disabled, just return the URL for GA
|
||||
if (!$this->configFactory->get('google_analytics.settings')->get('cache')) {
|
||||
return $remote_url;
|
||||
}
|
||||
|
||||
if (!file_exists($file_destination) || $synchronize) {
|
||||
// Download the latest tracking code.
|
||||
try {
|
||||
$data = (string) $this->httpClient
|
||||
->get($remote_url)
|
||||
->getBody();
|
||||
|
||||
if (file_exists($file_destination)) {
|
||||
// Synchronize tracking code and replace local file if outdated.
|
||||
$data_hash_local = Crypt::hashBase64(file_get_contents($file_destination));
|
||||
$data_hash_remote = Crypt::hashBase64($data);
|
||||
// Check that the files directory is writable.
|
||||
if ($data_hash_local != $data_hash_remote && $this->fileSystem->prepareDirectory($path)) {
|
||||
// Save updated tracking code file to disk.
|
||||
$this->fileSystem->saveData($data, $file_destination, FileSystemInterface::EXISTS_REPLACE);
|
||||
// Based on Drupal Core class AssetDumper.
|
||||
if (extension_loaded('zlib') && $this->configFactory->get('system.performance')->get('js.gzip')) {
|
||||
$this->fileSystem->saveData(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FileSystemInterface::EXISTS_REPLACE);
|
||||
}
|
||||
$this->logger->info('Locally cached tracking code file has been updated.');
|
||||
|
||||
// Change query-strings on css/js files to enforce reload for all
|
||||
// users.
|
||||
_drupal_flush_css_js();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Check that the files directory is writable.
|
||||
if ($this->fileSystem->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY)) {
|
||||
// There is no need to flush JS here as core refreshes JS caches
|
||||
// automatically, if new files are added.
|
||||
$this->fileSystem->saveData($data, $file_destination, FileSystemInterface::EXISTS_REPLACE);
|
||||
// Based on Drupal Core class AssetDumper.
|
||||
if (extension_loaded('zlib') && $this->configFactory->get('system.performance')->get('js.gzip')) {
|
||||
$this->fileSystem->saveData(gzencode($data, 9, FORCE_GZIP), $file_destination . '.gz', FileSystemInterface::EXISTS_REPLACE);
|
||||
}
|
||||
$this->logger->info('Locally cached tracking code file has been saved.');
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (RequestException $exception) {
|
||||
watchdog_exception('google_analytics', $exception);
|
||||
return $remote_url;
|
||||
}
|
||||
}
|
||||
// Return the local JS file path.
|
||||
$query_string = '?' . (\Drupal::state()->get('system.css_js_query_string') ?: '0');
|
||||
return file_url_transform_relative(file_create_url($file_destination)) . $query_string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cached files and directory.
|
||||
*/
|
||||
public function clearGoogleAnalyticsJsCache() {
|
||||
$path = 'public://google_analytics';
|
||||
if (is_dir($path)) {
|
||||
$this->fileSystem->deleteRecursive($path);
|
||||
|
||||
// Change query-strings on css/js files to enforce reload for all users.
|
||||
_drupal_flush_css_js();
|
||||
|
||||
$this->logger->info('Local Google Analytics file cache has been purged.');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\google_analytics\FunctionalJavascript;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
|
||||
/**
|
||||
* Tests add more behavior for a multiple value field.
|
||||
*
|
||||
* @group google_analytics
|
||||
*/
|
||||
class GoogleAnalyticsFormValidationTest extends WebDriverTestBase {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['google_analytics', 'token', 'node'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Admin user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User|bool
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer google analytics',
|
||||
'administer nodes',
|
||||
'create article content',
|
||||
];
|
||||
|
||||
// Create node type.
|
||||
$this->drupalCreateContentType([
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
]);
|
||||
|
||||
// User to set up google_analytics.
|
||||
$this->adminUser = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if Custom Dimensions token form validation works.
|
||||
*/
|
||||
public function testGoogleAnalyticsCustomDimensionsTokenFormValidation() {
|
||||
$this->drupalGet('admin/config/services/google-analytics');
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
// Set the UA Code
|
||||
$user_name = $assert_session->waitForField('accounts[0][value]');
|
||||
$account_field = $page->findField('accounts[0][value]');
|
||||
$account_field->setValue('UA-123456-1');
|
||||
|
||||
$dms = $assert_session->waitForLink('Dimensions and Metrics');
|
||||
$page->clickLink('Dimensions and Metrics');
|
||||
|
||||
// First set a value on the first input field.
|
||||
$field_0_name = $page->findField('custom_parameters[0][name]');
|
||||
$field_0_name->setValue('current_user_name');
|
||||
$field_0_value = $page->findField('custom_parameters[0][value]');
|
||||
$field_0_value->setValue('[current-user:name]');
|
||||
|
||||
// Validate the value of the first field exists.
|
||||
$this->assertEquals('current_user_name', $field_0_name->getValue(), 'Name for the first item has not changed.');
|
||||
$this->assertEquals('[current-user:name]', $field_0_value->getValue(), 'Value for the first item has not changed.');
|
||||
|
||||
/** TODO: Fix tests in Issue #3243622
|
||||
$add_more_button = $page->findButton('Add another Parameter');
|
||||
// Add another item
|
||||
$add_more_button->click();
|
||||
$field_1 = $assert_session->waitForField('custom_parameters[1][name]');
|
||||
$this->assertNotEmpty($field_1, 'Successfully added another item.');
|
||||
|
||||
// Validate the value of the first field has not changed.
|
||||
$this->assertEquals('current_user_name', $field_0_name->getValue(), 'Name for the first item has not changed.');
|
||||
$this->assertEquals('[current-user:name]', $field_0_value->getValue(), 'Value for the first item has not changed.');
|
||||
|
||||
// Validate the value of the second item is empty.
|
||||
$this->assertEmpty($field_1->getValue(), 'Value for the second item is currently empty.');
|
||||
|
||||
$field_1_name = $page->findField('custom_parameters[1][name]');
|
||||
$field_1_name->setValue('current_user_edit_url');
|
||||
$field_1_value = $page->findField('custom_parameters[1][value]');
|
||||
$field_1_value->setValue('[current-user:edit-url]');
|
||||
|
||||
// Add third item
|
||||
$add_more_button->click();
|
||||
$field_2 = $assert_session->waitForField('custom_parameters[2][name]');
|
||||
$this->assertNotEmpty($field_2, 'Successfully added another item.');
|
||||
|
||||
$field_2_name = $page->findField('custom_parameters[2][name]');
|
||||
$field_2_name->setValue('user_name');
|
||||
$field_2_value = $page->findField('custom_parameters[2][value]');
|
||||
$field_2_value->setValue('[user:name]');
|
||||
|
||||
// Add forth item
|
||||
$add_more_button->click();
|
||||
$field_3 = $assert_session->waitForField('custom_parameters[3][name]');
|
||||
$this->assertNotEmpty($field_3, 'Successfully added another item.');
|
||||
|
||||
$field_3_name = $page->findField('custom_parameters[3][name]');
|
||||
$field_3_name->setValue('term_name');
|
||||
$field_3_value = $page->findField('custom_parameters[3][value]');
|
||||
$field_3_value->setValue('[term:name]');
|
||||
|
||||
// Add fifth item
|
||||
$add_more_button->click();
|
||||
$field_4 = $assert_session->waitForField('custom_parameters[4][name]');
|
||||
$this->assertNotEmpty($field_4, 'Successfully added another item.');
|
||||
|
||||
$field_4_name = $page->findField('custom_parameters[4][name]');
|
||||
$field_4_name->setValue('term_tid');
|
||||
$field_4_value = $page->findField('custom_parameters[4][value]');
|
||||
$field_4_value->setValue('[term:tid]');
|
||||
|
||||
$page->pressButton('op');
|
||||
|
||||
// Check form validation.
|
||||
$this->assertSession()->responseContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => $this->t('Custom dimension value #@index', ['@index' => 0]), '@invalid-tokens' => implode(', ', ['[current-user:name]'])]));
|
||||
$this->assertSession()->responseContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => $this->t('Custom dimension value #@index', ['@index' => 1]), '@invalid-tokens' => implode(', ', ['[current-user:edit-url]'])]));
|
||||
$this->assertSession()->responseContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => $this->t('Custom dimension value #@index', ['@index' => 2]), '@invalid-tokens' => implode(', ', ['[user:name]'])]));
|
||||
// BUG #2037595
|
||||
//$this->assertSession()->responseNotContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 4]), '@invalid-tokens' => implode(', ', ['[term:name]'])]));
|
||||
//$this->assertSession()->responseNotContains($this->t('The %element-title is using the following forbidden tokens with personal identifying information: @invalid-tokens.', ['%element-title' => t('Custom dimension value #@index', ['@index' => 5]), '@invalid-tokens' => implode(', ', ['[term:tid]'])]));
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\google_analytics\Kernel\Form;
|
||||
|
||||
use Drupal\Core\Form\FormInterface;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\google_analytics\Form\GoogleAnalyticsAdminSettingsForm;
|
||||
|
||||
/**
|
||||
* Tests the google_analytics settings form.
|
||||
*
|
||||
* @group google_analytics
|
||||
*/
|
||||
class GoogleAnalyticsAdminSettingsFormTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* The google_analytics form object under test.
|
||||
*
|
||||
* @var \Drupal\google_analytics\Form\GoogleAnalyticsAdminSettingsForm
|
||||
*/
|
||||
protected $googleAnalyticsSettingsForm;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'system',
|
||||
'path_alias',
|
||||
'user',
|
||||
'google_analytics',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @covers ::__construct
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installConfig(static::$modules);
|
||||
$this->googleAnalyticsSettingsForm = new GoogleAnalyticsAdminSettingsForm(
|
||||
$this->container->get('config.factory'),
|
||||
$this->container->get('current_user'),
|
||||
$this->container->get('module_handler'),
|
||||
$this->container->get('google_analytics.accounts'),
|
||||
$this->container->get('google_analytics.javascript_cache')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for \Drupal\google_analytics\Form\GoogleAnalyticsAdminSettingsForm.
|
||||
*/
|
||||
public function testGoogleAnalyticsAdminSettingsForm() {
|
||||
$this->assertInstanceOf(FormInterface::class, $this->googleAnalyticsSettingsForm);
|
||||
|
||||
$this->assertEquals('google_analytics_admin_settings', $this->googleAnalyticsSettingsForm->getFormId());
|
||||
|
||||
$method = new \ReflectionMethod(GoogleAnalyticsAdminSettingsForm::class, 'getEditableConfigNames');
|
||||
$method->setAccessible(TRUE);
|
||||
|
||||
$name = $method->invoke($this->googleAnalyticsSettingsForm);
|
||||
$this->assertEquals(['google_analytics.settings'], $name);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
services:
|
||||
php:
|
||||
image: q0rban/tugboat-drupal:9.3
|
||||
default: true
|
||||
http: false
|
||||
depends: mysql
|
||||
commands:
|
||||
update: |
|
||||
set -eux
|
||||
# Check out a branch using the unique Tugboat ID for this repository, to
|
||||
# ensure we don't clobber an existing branch.
|
||||
git checkout -b $TUGBOAT_REPO_ID
|
||||
# Composer is hungry. You need a Tugboat project with a pretty sizeable
|
||||
# chunk of memory.
|
||||
export COMPOSER_MEMORY_LIMIT=-1
|
||||
# This is an environment variable we added in the Dockerfile that
|
||||
# provides the path to Drupal composer root (not the web root).
|
||||
cd $DRUPAL_COMPOSER_ROOT
|
||||
# We configure the Drupal project to use the checkout of the module as a
|
||||
# Composer package repository.
|
||||
composer config repositories.tugboat vcs $TUGBOAT_ROOT
|
||||
# Now we can require this module, specifing the branch name we created
|
||||
# above that uses the $TUGBOAT_REPO_ID environment variable.
|
||||
composer require drupal/google_cse:dev-$TUGBOAT_REPO_ID
|
||||
# Install Drupal on the site.
|
||||
vendor/bin/drush \
|
||||
--yes \
|
||||
--db-url=mysql://tugboat:tugboat@mysql:3306/tugboat \
|
||||
--site-name="Live preview for ${TUGBOAT_PREVIEW_NAME}" \
|
||||
--account-pass=admin \
|
||||
site:install standard
|
||||
# Set up the files directory permissions.
|
||||
mkdir -p $DRUPAL_DOCROOT/sites/default/files
|
||||
chgrp -R www-data $DRUPAL_DOCROOT/sites/default/files
|
||||
chmod 2775 $DRUPAL_DOCROOT/sites/default/files
|
||||
chmod -R g+w $DRUPAL_DOCROOT/sites/default/files
|
||||
# Enable the module.
|
||||
vendor/bin/drush --yes pm:enable google_cse
|
||||
build: |
|
||||
set -eux
|
||||
# Delete and re-check out this branch in case this is built from a Base Preview.
|
||||
git branch -D $TUGBOAT_REPO_ID && git checkout -b $TUGBOAT_REPO_ID || true
|
||||
export COMPOSER_MEMORY_LIMIT=-1
|
||||
cd $DRUPAL_COMPOSER_ROOT
|
||||
composer install --optimize-autoloader
|
||||
# Update this module, including all dependencies.
|
||||
composer update drupal/google_cse --with-all-dependencies
|
||||
vendor/bin/drush --yes updb
|
||||
vendor/bin/drush cache:rebuild
|
||||
mysql:
|
||||
image: tugboatqa/mariadb
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user