cms_content_sync-3.0.x-dev/cms_content_sync.module
cms_content_sync.module
<?php
/**
* @file
* Module file for cms_content_sync.
*
* @author Edge Box GmbH
*/
use Drupal\user\Entity\User;
use Drupal\cms_content_sync\Controller\ContentSyncSettings;
use Drupal\cms_content_sync\Controller\Embed;
use Drupal\cms_content_sync\Controller\ShowUsage;
use Drupal\cms_content_sync\Entity\EntityStatus;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\EntityStatusProxy;
use Drupal\cms_content_sync\Exception\SyncException;
use Drupal\cms_content_sync\Plugin\cms_content_sync\field_handler\DefaultEntityReferenceHandler;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\PullIntent;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncCoreInterface\DrupalApplication;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\cms_content_sync_developer\Cli\CliService;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\encrypt\Entity\EncryptionProfile;
use Drupal\group\Entity\Group;
use Drupal\group\Entity\GroupContent;
use Drupal\layout_builder\Form\OverridesEntityForm;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\node\Form\DeleteMultiple;
use Drupal\paragraphs\Entity\Paragraph;
use Drupal\webform_ui\WebformUiEntityElementsForm;
use EdgeBox\SyncCore\Exception\SyncCoreException;
use EdgeBox\SyncCore\Interfaces\ISyncCore;
/**
* @return int The ID of the user to perform updates with.
*/
function _cms_content_sync_user_id() {
return Drupal::service('keyvalue.database')->get('cms_content_sync_user')->get('uid');
}
/**
* Stores the Content Sync encryption profile name into a constant.
*
* @var string cms_content_sync_PROFILE_NAME The encryption profile name
*/
define('CMS_CONTENT_SYNC_ENCRYPTION_PROFILE_NAME', 'cms_content_sync');
/**
* Installation check.
*
* Check whether the module has been installed properly. If another module
* creates entities *during* the installation of this module for example,
* the installation will throw a fatal error and the user can't continue
* using this module. This can happen when you're using an audit module that
* logs all site interactions for example.
*
* @returns bool
*/
function _cms_content_sync_is_installed() {
static $installed = FALSE;
if ($installed) {
return TRUE;
}
try {
Drupal::entityTypeManager()->getStorage('cms_content_sync_flow');
Drupal::entityTypeManager()->getStorage('cms_content_sync_pool');
Drupal::entityTypeManager()->getStorage('cms_content_sync_entity_status');
return $installed = TRUE;
}
catch (Exception $e) {
return FALSE;
}
}
/**
* Add a submit handler to the form in case paragraphs are embedded within it.
*
* @param array $form
* The array Drupal form object.
* @param array $element
* The entity submit handler element.
*
* @return bool
* Returns true if the entity could be embedded.
*/
function _cms_content_sync_add_embedded_entity_submit_handler(array &$form, array &$element) {
if (!empty($element['cms_content_sync_edit_override']) && $element !== $form) {
// Submit button is not available yet, so we temporarily store the handler
// in the form array and set it later when the buttons are available.
$form['actions']['submit']['#submit'][] = '_cms_content_sync_override_embedded_entity_submit';
return TRUE;
}
foreach ($element as &$item) {
if (!is_array($item)) {
continue;
}
if (_cms_content_sync_add_embedded_entity_submit_handler($form, $item)) {
return TRUE;
}
}
return FALSE;
}
/**
* Get HTML for a list of entity type differences.
*
* @param string $entity_type
* The Drupal entity type.
* @param string $bundle
* The Drupal entity type bundle.
*
* @throws Exception
*
* @return string
* Returns the entity type differences.
*/
function _cms_content_sync_display_entity_type_differences($entity_type, $bundle) {
$all_diffs = Pool::getAllSitesWithDifferentEntityTypeVersion($entity_type, $bundle);
$result = '';
foreach ($all_diffs as $pool_id => $pool_diff) {
if (empty($pool_diff)) {
continue;
}
$pool = Pool::getAll()[$pool_id];
foreach ($pool_diff as $site_id => $diff) {
$name = $pool->getClient()->getSiteName($site_id);
if (!$name) {
$name = $site_id;
}
$result .= '<li>' . $name . ' (' . $pool_id . ')<ul>';
if (isset($diff['local_missing'])) {
foreach ($diff['local_missing'] as $field) {
$result .= '<li>' . t('Missing locally:') . ' ' . $field . '</li>';
}
}
if (isset($diff['remote_missing'])) {
foreach ($diff['remote_missing'] as $field) {
$result .= '<li>' . t('Missing remotely:') . ' ' . $field . '</li>';
}
}
$result .= '</ul></li>';
}
}
if (empty($result)) {
return NULL;
}
return '<ul>' . $result . '</ul>';
}
/**
* Display Entity Type differences.
*
* Get HTML for a list of entity type differences and include all referenced
* entity types.
*
* @param array $result
* A storage to save information per entity type + bundle
* in.
* @param string $entity_type
* The Drupal entity type.
* @param string $bundle
* The Drupal entity type bundle.
*
* @throws Exception
*/
function _cms_content_sync_display_entity_type_differences_recursively(array &$result, $entity_type, $bundle) {
if (isset($result[$entity_type][$bundle])) {
return;
}
if (!EntityHandlerPluginManager::isEntityTypeFieldable($entity_type)) {
return;
}
$self = _cms_content_sync_display_entity_type_differences($entity_type, $bundle);
$result[$entity_type][$bundle] = empty($self) ? '' : $self;
/**
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundleInfoService
*/
$bundleInfoService = Drupal::service('entity_type.bundle.info');
/**
* @var \Drupal\Core\Entity\EntityFieldManager $entityFieldManager
*/
$entityFieldManager = Drupal::service('entity_field.manager');
/**
* @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields
*/
$fields = $entityFieldManager->getFieldDefinitions($entity_type, $bundle);
foreach ($fields as $key => $field) {
if (!in_array($field->getType(), [
'entity_reference',
'entity_reference_revisions',
'cohesion_entity_reference_revisions',
])) {
continue;
}
$any_handler = FALSE;
foreach (Flow::getAll() as $id => $flow) {
$config = $flow->getController()->getPropertyConfig($entity_type, $bundle, $key);
if (empty($config) || Flow::HANDLER_IGNORE == $config['handler']) {
continue;
}
$any_handler = TRUE;
break;
}
if (!$any_handler) {
continue;
}
$type = $field->getSetting('target_type');
/** @var \Drupal\cms_content_sync\Helper\FieldHelper $field_helper */
$field_helper = \Drupal::service('cms_content_sync.field_helper');
$bundles = $field_helper->getEntityReferenceFieldAllowedTargetBundles($field);
if (empty($bundles)) {
$bundles = array_keys($bundleInfoService->getBundleInfo($type));
}
foreach ($bundles as $name) {
$config = $flow->getController()->getEntityTypeConfig($type, $name, TRUE);
if (empty($config)) {
continue;
}
_cms_content_sync_display_entity_type_differences_recursively($result, $type, $name);
}
}
}
/**
* Get HTML for a list of the usage for the given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The Entity the pool usage should be shown for.
*
* @throws Exception
*
* @return string
* The pool usage of the entity.
*/
function _cms_content_sync_display_pool_usage(EntityInterface $entity) {
$usages = Pool::getAllExternalUsages($entity);
if (empty($usages)) {
return '';
}
$result = '';
foreach ($usages as $pool_id => $usage) {
if (empty($usage)) {
continue;
}
$pool = Pool::getAll()[$pool_id];
$result .= '<br><b>Pool: ' . $pool->label . '</b><ul>';
foreach ($usage as $site_id => $url) {
$name = $pool->getClient()->getSiteName($site_id);
if (!$name) {
$name = $site_id;
}
if ($url) {
$text = '<a href="' . $url . '">' . $name . '</a>';
}
else {
$text = $name;
}
$result .= '<li>' . $text . '</li>';
}
$result .= '</ul>';
}
if (empty($result)) {
return $result;
}
return '</ul>' . $result . '</ul>';
}
/**
* Temp. save static values for taxonomy tree changes.
*
* @param null|bool $set
* The value to be set.
* @param null|mixed $entity
* The Drupal entity.
*
* @return array|bool
* The updated Drupal entities.
*/
function _cms_content_sync_update_taxonomy_tree_static($set = NULL, $entity = NULL) {
static $value = FALSE;
static $entities = [];
if (NULL !== $set) {
$value = $set;
}
if (NULL !== $entity) {
$entities[] = $entity;
}
if (FALSE === $set) {
return $entities;
}
return $value;
}
/**
* React on changes within taxonomy trees.
*
* @param array $form
* The Drupal form object.
*/
function cms_content_sync_update_taxonomy_tree_validate(array $form, FormStateInterface $form_state) {
_cms_content_sync_update_taxonomy_tree_static(TRUE);
}
/**
* React on changes within taxonomy trees.
*
* @param array $form
* The Drupal form object.
*/
function cms_content_sync_update_taxonomy_tree_submit(array $form, FormStateInterface $form_state) {
$entities = _cms_content_sync_update_taxonomy_tree_static(FALSE);
foreach ($entities as $entity) {
PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_AUTOMATICALLY, SyncIntent::ACTION_UPDATE);
}
}
/**
* React on changes within taxonomy trees.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state interface.
* @param bool $ignore_step
* Option to ignore the current step.
*
* @return \Drupal\Core\Entity\EntityInterface|\Drupal\group\Entity\GroupContent
* Returns either a entity or the group content entity.
*/
function _cms_content_sync_get_group_wizard_entity(FormStateInterface $form_state, $ignore_step = FALSE) {
$wizard_id = $form_state->get('group_wizard_id');
if ('group_entity' !== $wizard_id && 'group_creator' !== $wizard_id) {
return NULL;
}
$store_id = $form_state->get('store_id');
$store = \Drupal::service('tempstore.private')->get($wizard_id);
if ($store && $store_id) {
$step = $store->get("{$store_id}:step");
// We ignore step 1 and only show the "Save and Push" button at the second step.
if (!$ignore_step && 2 !== $step) {
return NULL;
}
$entity = $store->get("{$store_id}:entity");
if ($entity) {
return $entity;
}
// When it's already saved, the group module will remove the entity from the store,
// so we have to load it manually.
if ('group_entity' === $wizard_id) {
$unknown_entity = _cms_content_sync_get_entity_from_form($form_state);
if ($unknown_entity instanceof GroupContent) {
return $unknown_entity->getEntity();
}
return $unknown_entity;
}
if ('group_creator' === $wizard_id) {
$unknown_entity = _cms_content_sync_get_entity_from_form($form_state);
if ($unknown_entity instanceof Group) {
return $unknown_entity;
}
if ($unknown_entity instanceof GroupContent) {
return $unknown_entity->getGroup();
}
}
}
return NULL;
}
/**
* Content Sync Form alter.
*
* 1) Make sure the user is informed that content will not only be deleted on
* this * instance but also on all connected instances if configured that way.
*
* 2) Make sure Sync Core knows about password changes at the
* Content Sync user and can still authenticate to perform updates.
*
* 3) Disabled node forms if the content has been pulled and the
* synchronization is configured to disable pulled content.
*
* @param array $form
* The form definition.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $form_id
* The Drupal form id.
*
* @throws Exception
*
* @see _cms_content_sync_form_alter_disabled_fields
*/
function cms_content_sync_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
if (!_cms_content_sync_is_installed()) {
return;
}
if ('taxonomy_overview_terms' == $form_id) {
$form['#validate'][] = 'cms_content_sync_update_taxonomy_tree_validate';
$form['#submit'][] = 'cms_content_sync_update_taxonomy_tree_submit';
}
$form_object = $form_state->getFormObject();
// Avoid function nesting error for conditional fields.
// @todo Find a way to limit this function call in a useful way.
if ('conditional_field_edit_form' != $form_id) {
_cms_content_sync_add_embedded_entity_submit_handler($form, $form);
}
$entity = _cms_content_sync_get_group_wizard_entity($form_state);
if ($entity) {
if (!_cms_content_sync_prevent_entity_export($entity)) {
_cms_content_sync_form_alter_for_entity($form, $form_state, $entity);
}
return;
}
if (_cms_content_sync_get_group_wizard_entity($form_state, TRUE)) {
return;
}
if ('user_form' === $form_id) {
$form['actions']['submit']['#submit'][] = 'cms_content_sync_user_password_submit';
}
$webform = FALSE;
$moduleHandler = Drupal::service('module_handler');
if ($moduleHandler->moduleExists('webform')) {
if ($form_object instanceof WebformUiEntityElementsForm) {
$webform = TRUE;
}
}
if ($form_id === 'file_management') {
$file_entity = \Drupal::routeMatch()->getParameters()->get('file');
}
else {
$file_entity = NULL;
}
if ($form_object instanceof DeleteMultiple || $form_object instanceof ContentEntityDeleteForm) {
if (!empty($form_state->getUserInput()['confirm'])) {
return;
}
if ($form_object instanceof DeleteMultiple) {
$temp_store_factory = Drupal::service('tempstore.private');
$entity_type_manager = Drupal::service('entity_type.manager');
$tempstore = $temp_store_factory->get('entity_delete_multiple_confirm');
$user = Drupal::currentUser();
// @todo Extend this that it is also working with other entity types.
$entity_type_id = 'node';
$selection = $tempstore->get($user->id() . ':' . $entity_type_id);
$entities = $entity_type_manager->getStorage($entity_type_id)->loadMultiple(array_keys($selection));
}
else {
$entities[] = _cms_content_sync_get_entity_from_form($form_state);
}
foreach ($entities as $entity) {
if (!EntityHandlerPluginManager::isSupported($entity->getEntityTypeId(), $entity->bundle())) {
continue;
}
if (!Flow::isLocalDeletionAllowed($entity)) {
// ['actions']['submit'].
$form['cms_content_sync'] = [
'#prefix' => '<div class="messages messages--warning">',
'#markup' => t(
'%label cannot be deleted as it has been pulled.',
['%label' => $entity->label()]
),
'#suffix' => '</div>',
];
$form['#disabled'] = TRUE;
}
else {
$is_translation = $entity instanceof TranslatableInterface && !$entity->isDefaultTranslation();
$flows = Flow::getFlowsForPushing($entity, SyncIntent::ACTION_DELETE, !$is_translation);
if (count($flows)) {
$usage = _cms_content_sync_display_pool_usage($entity);
$message = $usage ? t(
'This will delete %label from all sites using it: @sites',
[
'%label' => $entity->label(),
'@sites' => Markup::create($usage)
]
) : t(
'This will delete %label from all sites using it.',
['%label' => $entity->label()]
);
$form['cms_content_sync'] = [
'#prefix' => '<div class="messages messages--warning">',
'#markup' => $message,
'#suffix' => '</div>',
];
$form['actions']['submit']['#value'] = t('Delete and Push');
}
// FIXME: Drupal doesn't call our #submit handlers at all, so we don't have the "Delete and Push"
// functionality yet for manually pushed deletions where we need another redirect to make it work.
/*elseif ($is_translation) {
$flows = Flow::getFlowsForPushing($entity, SyncIntent::ACTION_DELETE, TRUE);
if (count($flows)) {
$form_state->set('flow_id', reset($flows)->id());
$form_state->set('entity_type', $entity->getEntityTypeId());
$form_state->set('entity_id', $entity->id());
$form['actions']['delete_push'] = $form['actions']['submit'];
$form['actions']['delete_push']['#weight'] = $form['actions']['submit']['#weight'] - 1;
$form['actions']['delete_push']['#value'] = t('Delete and Push');
if (empty($form['actions']['delete_push']['#submit'])) {
$form['actions']['delete_push']['#submit'] = [];
}
array_push($form['actions']['delete_push']['#submit'], '_cms_content_sync_delete_push_action_submit');
}
}*/
}
}
}
// Add pool selection functionality to entity types.
elseif ($form_object instanceof ContentEntityForm || $webform || $file_entity) {
$entity = _cms_content_sync_get_entity_from_form($form_state);
if (!EntityHandlerPluginManager::isSupported($entity->getEntityTypeId(), $entity->bundle())) {
return;
}
$form['#attached']['library'][] = 'cms_content_sync/entity-form';
_cms_content_sync_form_alter_disabled_fields($form, $form_state, $entity);
$registered = !!DrupalApplication::get()->getSiteUuid();
if (!$registered) {
_cms_content_sync_add_form_group($form);
$form['cms_content_sync_group']['message'] = [
'#prefix' => '<div class="messages messages--warning">',
'#suffix' => '</div>',
'#markup' => t('This site is not registered.'),
];
}
$bundle = $entity->bundle();
$selectable_pushing_flows = Pool::getSelectablePools($entity->getEntityTypeId(), $bundle);
$flows = Flow::getAll();
if ($registered) {
if (!empty($selectable_pushing_flows)) {
_cms_content_sync_add_push_pool_form($form, $form_state, $selectable_pushing_flows, $entity);
_cms_content_sync_add_usage_form($form, $entity);
}
else {
$flows = Flow::getFlowsForPushing($entity, SyncIntent::ACTION_DELETE);
if (count($flows)) {
_cms_content_sync_add_usage_form($form, $entity);
}
else {
$flows = Flow::getFlowsForPushing($entity, SyncIntent::ACTION_UPDATE);
if (count($flows)) {
_cms_content_sync_add_usage_form($form, $entity);
}
}
}
}
foreach ($flows as $flow) {
if ($flow->getController()->supportsEntity($entity)) {
if ($registered) {
_cms_content_sync_add_version_mismatches_form($form, $form_state);
}
_cms_content_sync_add_form_value_cache($form);
break;
}
}
if (!_cms_content_sync_prevent_entity_export($entity)) {
_cms_content_sync_form_alter_for_entity($form, $form_state, $entity);
}
}
}
/**
* Save and push submit handler.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*/
function _cms_content_sync_delete_push_action_submit($form, $form_state) {
$options = [];
$destination = \Drupal::request()->query->get('destination');
if (isset($destination)) {
$options = [
'query' => [
'destination' => $destination,
],
];
}
\Drupal::request()->query->remove('destination');
$form_state->setRedirect('cms_content_sync.publish_changes_confirm_deleted_translation', [
'flow_id' => $form_state->get('flow_id'),
'entity_type' => $form_state->get('entity_type'),
'entity_id' => $form_state->get('entity_id'),
], $options);
}
/**
* Alters the form for a given entity.
*
* @param array $form
* The form definition.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The Drupal entity.
*/
function _cms_content_sync_form_alter_for_entity(array &$form, FormStateInterface $form_state, EntityInterface $entity) {
$user = Drupal::currentUser();
if ($user->hasPermission('publish cms content sync changes')) {
foreach (Flow::getAll() as $flow_id => $flow) {
// Add "Save and push" button to entity types which are configured to
// be pushed manually.
if ($flow->getController()->canPushEntity($entity, PushIntent::PUSH_MANUALLY)) {
// Adding groups is a two-step process, so we can't just add a "Save and Push" or we'll get an error.
// So instead, we hide the button when adding a group and users can then push it later.
// if ($entity->getEntityTypeId() === 'group' && !$entity->id()) {
// continue;
// }.
_cms_content_sync_add_save_push_action($form, $form_state, $flow_id);
break;
}
// Adjust save button label if the entity will be pushed
// automatically after saving it.
if ($flow->getController()->canPushEntity($entity, PushIntent::PUSH_AUTOMATICALLY)) {
if (!empty($form['new_file_details']['actions'])) {
$form['new_file_details']['actions']['submit']['#value'] = _cms_content_sync_push_label(TRUE);
}
else {
$form['actions']['submit']['#value'] = _cms_content_sync_push_label(TRUE);
}
break;
}
}
}
}
/**
* Get the label to Push Changes or to Save and Push.
*
* If the site is a staging site pushing to prod the label changes to include
* "push [...] to live".
*/
function _cms_content_sync_push_label($save = FALSE) {
try {
$client = SyncCoreFactory::getSyncCoreV2();
if ($client->isStagingSite(TRUE) && !$client->isProductionSite()) {
return $save ? t('Save and push to live') : t('Push changes to live');
}
}
catch (\Exception $e) {
}
return $save ? t('Save and push') : t('Push changes');
}
/**
* Prevent Export.
*
* Only allow pushing of entities which have not been pulled before and
* do not have been configured as "forbid updating" or "allow overwrite".
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The current entity.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
function _cms_content_sync_prevent_entity_export(EntityInterface $entity) {
// UUIDs are required to at least query for the status entities. Without a UUID,
// we cannot save status information, so we also can't push or pull the item.
if (!$entity->uuid()) {
return TRUE;
}
$infos = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
foreach ($infos as $info) {
$flow = $info->getFlow();
if (!$flow) {
continue;
}
$entity_type_configuration = $flow->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle());
$language = $entity instanceof TranslatableInterface ? $entity->language()->getId() : NULL;
if ($info->getLastPull($language, TRUE) && isset($entity_type_configuration['import_updates'])) {
if (PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN == $entity_type_configuration['import_updates']
|| PullIntent::PULL_UPDATE_FORCE_AND_FORBID_EDITING == $entity_type_configuration['import_updates']) {
return TRUE;
}
if (empty($entity_type_configuration['allow_cross_sync'])) {
return TRUE;
}
}
}
return FALSE;
}
/**
* Add "Save and push" action.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
* @param string $flow_id
* The content sync flow id.
*/
function _cms_content_sync_add_save_push_action(array &$form, FormStateInterface $form_state, $flow_id) {
$form_state->setFormState([
'flow_id' => $flow_id,
]);
if (!empty($form['new_file_details']['actions'])) {
$actions = &$form['new_file_details']['actions'];
}
else {
$actions = &$form['actions'];
}
$actions['save_push'] = $actions['submit'];
$actions['save_push']['#value'] = _cms_content_sync_push_label(TRUE);
if (empty($actions['save_push']['#submit'])) {
$actions['save_push']['#submit'] = [];
}
array_push($actions['save_push']['#submit'], '_cms_content_sync_add_save_push_action_submit');
}
/**
*
*/
function _cms_content_sync_get_entity_from_form(FormStateInterface $form_state) {
if ($form_state->getFormObject()->getFormId() === 'file_management') {
return \Drupal::routeMatch()->getParameters()->get('file');
}
else {
return $form_state->getFormObject()->getEntity();
}
}
/**
* Save and push submit handler.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*/
function _cms_content_sync_add_save_push_action_submit($form, $form_state) {
$entity = _cms_content_sync_get_group_wizard_entity($form_state, TRUE);
if (!$entity) {
$entity = $form_state->getformObject()->getEntity();
}
// When using the groups module, the node is not saved immediately. In this
// case we ignore the operation as the entity won't have an ID.
if (!$entity->id()) {
return;
}
$options = [];
$destination = \Drupal::request()->query->get('destination');
if (isset($destination)) {
$options = [
'query' => [
'destination' => $destination,
],
];
}
$translatable = $entity instanceof TranslatableInterface;
\Drupal::request()->query->remove('destination');
$form_state->setRedirect($translatable ? 'cms_content_sync.publish_changes_confirm_translation' : 'cms_content_sync.publish_changes_confirm', [
'flow_id' => $form_state->get('flow_id'),
'entity_type' => $entity->getEntityTypeId(),
'entity_id' => $entity->id(),
] + ($translatable ? ['language' => $entity->language()->getId()] : []), $options);
}
/**
* Add additional entity status fields to paragraph items.
*
* @param mixed $element
* The element the entity status field should be added to.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
* @param mixed $context
* The widget plugin instance.
*/
function cms_content_sync_field_widget_single_element_form_alter(&$element, FormStateInterface $form_state, $context) {
if (!_cms_content_sync_is_installed()) {
return;
}
$field_types = $context['widget']->getPluginDefinition()['field_types'];
if (isset($field_types)) {
if (in_array('entity_reference_revisions', $field_types)) {
_cms_content_sync_paragraphs_push_settings_form($element, $form_state, $context);
}
}
}
/**
* Add the Push settings for to the several Paragraph widget types.
*
* @param mixed $element
* The element the entity status field should be added to.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
* @param mixed $context
* The widget plugin instance.
*/
function _cms_content_sync_paragraphs_push_settings_form(&$element, FormStateInterface &$form_state, &$context) {
// The parent entity of the paragraph.
$parent = $context['items']->getParent()->getValue();
// This code is based on:
// https://www.drupal.org/project/paragraphs/issues/2868155#comment-12610258
$entity_type = 'paragraph';
if (isset($element['#paragraph_type'])) {
$bundle = $element['#paragraph_type'];
$delta = $context['delta'];
if (!empty($context['items'])) {
if (isset($context['items']->get($delta)->target_id)) {
$entity = Paragraph::load($context['items']->get($delta)->target_id);
}
}
if (!empty($entity)) {
_cms_content_sync_form_alter_disabled_fields($element, $form_state, $entity, TRUE);
}
// If no bundle is given, the previous mentioned commit is
// not added to the project.
if (!is_null($bundle)) {
// If the parent entity isn't pushed, there's no need to handle these
// paragraphs at all.
$push_any = (bool) count(PushIntent::getFlowsForEntity($parent, PushIntent::PUSH_ANY));
if (!$push_any && !EntityStatus::getLastPushForEntity($parent)) {
return;
}
$selectable_push_flows = Pool::getSelectablePools($entity_type, $bundle, $parent, $context['items']->getName());
if (!empty($selectable_push_flows)) {
if (isset($entity)) {
_cms_content_sync_add_push_pool_form($element['subform'], $form_state, $selectable_push_flows, $entity);
}
else {
_cms_content_sync_add_push_pool_form($element['subform'], $form_state, $selectable_push_flows, NULL, $parent);
}
}
}
}
}
/**
* Add Content Sync form group.
*
* Display the push group either to select pools or to display the usage on
* other sites.
* You can use $form['cms_content_sync_group'] afterwards to access it.
*
* @param array $form
* The form array.
*/
function _cms_content_sync_add_form_group(array &$form) {
if (isset($form['cms_content_sync_group'])) {
return;
}
// Try to show the group right above the status checkbox if it exists.
if (isset($form['status']['#weight'])) {
$weight = $form['status']['#weight'] - 1;
}
else {
$weight = 99;
}
$form['cms_content_sync_group'] = [
'#type' => 'details',
'#open' => FALSE,
'#title' => _cms_content_sync_get_repository_name(),
'#weight' => $weight,
];
// If we got a advanced group we use it.
if (isset($form['advanced'])) {
$form['cms_content_sync_group']['#type'] = 'details';
$form['cms_content_sync_group']['#group'] = 'advanced';
}
}
/**
* Content Sync value cache.
*
* Cache all form values on submission.
* This is required for sub modules like the sitemap to get values statically
* from cache per entity type.
*
* @param array $form
* The Drupal form.
*/
function _cms_content_sync_add_form_value_cache(array &$form) {
// Entity form submit handler.
if (isset($form['actions']['submit'])) {
if (!empty($form['actions']['submit']['#submit'])) {
array_unshift($form['actions']['submit']['#submit'], '_cms_content_sync_cache_submit_values');
}
else {
$form['actions']['submit']['#submit'][] = '_cms_content_sync_cache_submit_values';
}
}
}
/**
* Content Sync cache submit callback.
*
* @param string $entity_type
* The Drupal entity type.
* @param string $entity_uuid
* The entity uuid.
* @param array $values
* The cache values to be submitted.
*
* @return array
* The values to be cached.
*/
function _cms_content_sync_submit_cache($entity_type, $entity_uuid, ?array $values = NULL) {
static $cache = [];
if (!empty($values)) {
$cache[$entity_type][$entity_uuid] = $values;
}
if (empty($cache[$entity_type][$entity_uuid])) {
return NULL;
}
return $cache[$entity_type][$entity_uuid];
}
/**
* Get the values to be cached.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*/
function _cms_content_sync_cache_submit_values(array $form, FormStateInterface $form_state) {
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
*/
$entity = _cms_content_sync_get_entity_from_form($form_state);
_cms_content_sync_submit_cache(
$entity->getEntityTypeId(),
$entity->uuid(),
$form_state->getValues()
);
}
/**
* The version mismatch form.
*
* Add a button "Show version mismatches" to show all sites using a different
* entity type version.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*/
function _cms_content_sync_add_version_mismatches_form(array &$form, FormStateInterface $form_state) {
_cms_content_sync_add_form_group($form);
// Only add the button for users having the permission
// "show entity type differences".
$user = Drupal::currentUser();
if ($user->hasPermission('show entity type differences')) {
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
*/
$entity = _cms_content_sync_get_entity_from_form($form_state);
$form['cms_content_sync_group']['cms_content_sync_version_mismatches'] = [
'#type' => 'button',
'#prefix' => '<span id="cms-content-sync-version-mismatches">',
'#suffix' => '</span>',
'#value' => t('Show version mismatches'),
'#entity_type' => $entity->getEntityTypeId(),
'#bundle' => $entity->bundle(),
'#recursive' => TRUE,
'#ajax' => [
'callback' => '_cms_content_sync_display_version_mismatches',
'wrapper' => 'cms-content-sync-version-mismatches',
'effect' => 'fade',
],
];
}
}
/**
* Display entity type differences recursively.
*
* @param array $mismatches
* The mismatches.
*
* @return string
* The mismatches.
*/
function _cms_content_sync_display_entity_type_differences_recursively_render(array $mismatches) {
$result = '';
foreach ($mismatches as $entity_type => $bundles) {
$title_set = FALSE;
foreach ($bundles as $bundle => $html) {
if (empty($html)) {
continue;
}
if (!$title_set) {
$result .= '<li>' . $entity_type . '<ul>';
$title_set = TRUE;
}
$result .= '<li>' . $bundle . ' ' . print_r($html, 1) . '</li>';
}
if ($title_set) {
$result .= '</ul></li>';
}
}
return $result;
}
/**
* Replace the "Show version mismatches" button with the actual information.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*
* @throws Exception
*
* @return array
* The version mismatches.
*/
function _cms_content_sync_display_version_mismatches(array $form, FormStateInterface &$form_state) {
$trigger = $form_state->getTriggeringElement();
if ($trigger['#recursive']) {
$mismatches = [];
_cms_content_sync_display_entity_type_differences_recursively($mismatches, $trigger['#entity_type'], $trigger['#bundle']);
$result = _cms_content_sync_display_entity_type_differences_recursively_render($mismatches);
}
else {
$result = _cms_content_sync_display_entity_type_differences($trigger['#entity_type'], $trigger['#bundle']);
}
if (empty($result)) {
$result = '<div class="messages messages--status">' . t('No differences.') . '</div>';
}
return [
'#type' => 'fieldset',
'#title' => t('Version mismatches'),
'#markup' => $result,
];
}
/**
* Add a button "Show usage" to show all sites using this content.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The Drupal entity interface.
*/
function _cms_content_sync_add_usage_form(array &$form, EntityInterface $entity) {
_cms_content_sync_add_form_group($form);
$used = EntityStatus::getLastPushForEntity($entity);
if (!$used) {
$used = EntityStatus::getLastPullForEntity($entity);
}
if ($used) {
$form['cms_content_sync_group']['cms_content_sync_usage'] = [
'#type' => 'button',
'#prefix' => '<span id="cms-content-sync-usage">',
'#suffix' => '</span>',
'#value' => t('Show usage'),
'#ajax' => [
'callback' => '_cms_content_sync_display_usage',
'wrapper' => 'cms-content-sync-usage',
'effect' => 'fade',
],
];
}
}
/**
* Replace the "Show usage" button with the actual usage information.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*
* @throws Exception
*
* @return array
* Returns the rendered usage of an entity.
*/
function _cms_content_sync_display_usage(array $form, FormStateInterface &$form_state) {
$entity = _cms_content_sync_get_entity_from_form($form_state);
$result = _cms_content_sync_display_pool_usage($entity);
if (!$result) {
$result = '<div class="messages messages--status">' . t('No usage yet.') . '</div>';
}
return [
'#type' => 'fieldset',
'#title' => t('Usage'),
'#markup' => $result,
];
}
/**
* Add the push widgets to the form, providing flow and pool selection.
*
* @param array $form
* The Drupal form.
* @param mixed $selectable_push_flows
* The flows that can be selected for a push.
* @param null|mixed $entity
* The entity the push form should be added to.
* @param null|mixed $parentEntity
* The parent entity.
*/
function _cms_content_sync_add_push_pool_form(array &$form, FormStateInterface $form_state, $selectable_push_flows, $entity = NULL, $parentEntity = NULL) {
_cms_content_sync_add_form_group($form);
$selected_flow = NULL;
// FIXME: we always use the same Flow for parent + child entities. So usiing the root cms_content_sync_flow value here is correct
// but when the parent entity's flow is changed, we have to reload all affected cms_content_sync_group in the whole form.
$selected_flow_id = $form_state->getValue('cms_content_sync_flow') ?? $form_state->getValue([
'cms_content_sync_group',
'cms_content_sync_flow'
]);
if ($selected_flow_id) {
$selected_flow = Flow::getAll()[$selected_flow_id];
}
// Flow selection.
if (1 === count($selectable_push_flows) || $parentEntity) {
$id = array_keys($selectable_push_flows)[0];
$form['cms_content_sync_group']['cms_content_sync_flow'] = [
'#title' => t('Push flow selection'),
'#type' => 'hidden',
'#value' => $id,
];
$selected_flow = Flow::getAll()[$id];
}
else {
$flow_options = [];
foreach ($selectable_push_flows as $flow_id => $selectable_push_flow) {
if (!$selected_flow) {
$selected_flow = Flow::getAll()[$flow_id];
}
$flow_options[$flow_id] = $selectable_push_flow['flow_label'];
}
$form['cms_content_sync_group']['cms_content_sync_flow'] = [
'#title' => t('Push flow selection'),
'#type' => 'select',
'#default_value' => $selected_flow->id,
'#options' => $flow_options,
'#ajax' => [
'callback' => '_cms_content_sync_update_pool_selector',
'event' => 'change',
'wrapper' => 'ajax-pool-selector-wrapper',
],
];
}
// Pool selection.
$options = $selectable_push_flows[$selected_flow->id];
// Get configured widget type for the current active flow.
if ('single_select' == $options['widget_type'] || 'multi_select' == $options['widget_type']) {
$widget_type = 'select';
}
else {
$widget_type = $options['widget_type'];
}
$pushed_to_pools = [];
$selected_pools = [];
if ($entity) {
$infos = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
foreach ($infos as $info) {
if ($info->getLastPull()) {
$pushed_to_pools[] = $info->getPool()->id();
}
else {
foreach ($selected_flow->getController()->getPoolsToPushTo($entity, PushIntent::PUSH_ANY, SyncIntent::ACTION_CREATE, FALSE) as $pool) {
$pushed_to_pools[] = $pool->id;
}
}
$selected_pools = $pushed_to_pools;
}
}
elseif ($parentEntity) {
foreach ($selected_flow->getController()->getPoolsToPushTo($parentEntity, PushIntent::PUSH_ANY, SyncIntent::ACTION_UPDATE, FALSE) as $pool) {
if (!isset($options['pools'][$pool->id])) {
continue;
}
$selected_pools[] = $pool->id;
}
}
$single = 'single_select' == $options['widget_type'] || 'radios' == $options['widget_type'];
$pool_list = [];
if ($single) {
$pool_list['ignore'] = t('None');
$default_value = empty($selected_pools) ? 'ignore' : $selected_pools[0];
}
else {
$default_value = $selected_pools;
}
$pool_list = array_merge($pool_list, $options['pools']);
$disabled = !empty($pushed_to_pools) && ContentSyncSettings::getInstance()->isDynamicPoolAssignmentDisabled();
$warning = '';
if (!$disabled && !empty($pushed_to_pools)) {
try {
$deletion_enabled = SyncCoreFactory::featureEnabled(ISyncCore::FEATURE_DYNAMIC_POOL_ASSIGNMENT, TRUE);
}
// If in doubt (e.g. Sync Core temporarily not available or slow) better display the warning.
catch (\Exception $e) {
$deletion_enabled = TRUE;
}
if ($deletion_enabled) {
$warning = '<div class="messages messages--warning">' . t('Unassigning a Pool may remove this content from other sites!')->__toString() . '</div>';
}
}
$form['cms_content_sync_group']['cms_content_sync_pool'] = [
'#title' => t('Push to pool'),
'#prefix' => '<div id="ajax-pool-selector-wrapper">',
'#suffix' => '</div>' . $warning,
'#type' => $widget_type,
'#default_value' => $default_value,
'#options' => $pool_list,
'#disabled' => $disabled,
];
if ($entity) {
$form['cms_content_sync_group']['cms_content_sync_uuid'] = [
'#type' => 'hidden',
'#value' => $entity->uuid(),
];
}
if ('multi_select' == $options['widget_type']) {
$form['cms_content_sync_group']['cms_content_sync_pool']['#multiple'] = TRUE;
}
// Entity form submit handler.
if (isset($form['actions']['submit'])) {
if (!empty($form['actions']['submit']['#submit'])) {
array_unshift($form['actions']['submit']['#submit'], '_cms_content_sync_set_entity_push_pools');
}
else {
$form['actions']['submit']['#submit'][] = '_cms_content_sync_set_entity_push_pools';
}
}
// Add validation.
$form['#validate'][] = '_cms_content_sync_pool_switch_validation';
}
/**
* Ensure that at least one pool is set, if an entity has been pushed before.
*
* @param mixed $form
* The Drupal form.
*/
function _cms_content_sync_pool_switch_validation(&$form, FormStateInterface $form_state) {
if ($form_state->getFormObject() instanceof EntityFormInterface) {
$entity = _cms_content_sync_get_entity_from_form($form_state);
if ($entity) {
// Check if the entity has been pushed before.
$last_push = EntityStatus::getLastPushForEntity($entity);
if (!is_null($last_push)) {
// Check if at least one pool has been selected.
$pool_selected = FALSE;
$pools = $form_state->getvalue('cms_content_sync_pool');
// Handle single- and multiselects.
if (is_array($pools)) {
foreach ($pools as $pool) {
if ($pool) {
$pool_selected = TRUE;
break;
}
}
}
else {
if ('ignore' != $pools) {
$pool_selected = TRUE;
}
}
if (!$pool_selected) {
$message = t('As this content item has already been pushed, at least one pool has to be selected.
To hide / remove this content item from other sites you have to unpublish it
or delete it completely.');
$form_state->setErrorByName('cms_content_sync_pool', $message);
}
}
}
}
}
/**
* Entity status update.
*
* Update the EntityStatus for the given entity, setting
* the EntityStatus::FLAG_EDIT_OVERRIDE flag accordingly.
*/
function _cms_content_sync_set_entity_push_pools(array $form, FormStateInterface $form_state) {
$flow_id = $form_state->getValue('cms_content_sync_flow');
if (!$flow_id) {
return;
}
$values = $form_state->getValue('cms_content_sync_pool');
$processed = [];
if (is_array($values)) {
foreach ($values as $id => $selected) {
if ($selected && 'ignore' !== $id) {
$processed[] = $id;
}
}
}
else {
if ('ignore' !== $values) {
$processed[] = $values;
}
}
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
*/
$entity = _cms_content_sync_get_entity_from_form($form_state);
EntityStatus::saveSelectedPoolsToPushTo($entity, $flow_id, $processed);
if ($entity instanceof FieldableEntityInterface) {
$entityFieldManager = Drupal::service('entity_field.manager');
/** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */
$fields = $entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
_cms_content_sync_set_entity_push_subform($entity, $form, $form_state, $fields);
}
}
/**
* Set entity push subform.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The Drupal field fieldable entity interface.
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
* @param array $fields
* The fields to be pushed.
* @param array $tree_position
* The tree position.
*/
function _cms_content_sync_set_entity_push_subform(FieldableEntityInterface $entity, array $form, FormStateInterface $form_state, array $fields, array $tree_position = []) {
$entityFieldManager = Drupal::service('entity_field.manager');
foreach ($fields as $name => $definition) {
if ('entity_reference_revisions' == $definition->getType()) {
if (!isset($form[$name]['widget']['#max_delta'])) {
continue;
}
$subform = &$form[$name]['widget'];
$count = $subform['#max_delta'];
for ($i = 0; $i <= $count; ++$i) {
$refflow = $form_state->getValue(array_merge($tree_position, [
$name,
$i,
'subform',
'cms_content_sync_group',
'cms_content_sync_flow'
]));
$refvalues = $form_state->getValue(array_merge($tree_position, [
$name,
$i,
'subform',
'cms_content_sync_group',
'cms_content_sync_pool'
]));
$refuuid = $form_state->getValue(array_merge($tree_position, [
$name,
$i,
'subform',
'cms_content_sync_group',
'cms_content_sync_uuid'
]));
if (!empty($refflow) && !empty($refvalues)) {
EntityStatus::accessTemporaryPushToPoolInfoForField($entity->getEntityTypeId(), $entity->uuid(), $name, $i, $tree_position, $refflow, $refvalues, $refuuid);
}
if (!empty($subform[$i]['subform'])) {
$entity_type = $definition->getSetting('target_type');
$bundle = $subform[$i]['#paragraph_type'];
/** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */
$fields = $entityFieldManager->getFieldDefinitions($entity_type, $bundle);
_cms_content_sync_set_entity_push_subform($entity, $subform[$i]['subform'], $form_state, $fields, array_merge($tree_position, [
$name,
$i,
'subform'
]));
}
}
}
}
}
/**
* Ajax callback to render the pools after flow selection.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*/
function _cms_content_sync_update_pool_selector(array $form, FormStateInterface $form_state) {
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
*/
$entity = _cms_content_sync_get_entity_from_form($form_state);
$bundle = $entity->bundle();
$selectable_push_flows = Pool::getSelectablePools($entity->getEntityTypeId(), $bundle);
$options = $selectable_push_flows[$form_state->getValue('cms_content_sync_flow')]['pools'];
$form['cms_content_sync_group']['cms_content_sync_pool']['#options'] = $options;
return $form['cms_content_sync_group']['cms_content_sync_pool'];
}
/**
* Get domains from the Domains module.
*
* Get the site's domains if the site is using the domain module. Will ignore
* inactive domains and duplicates, e.g. where only the path is different.
* Returns an empty array if the domain module doesn't exist.
*
* @param null|EntityInterface $deleted
* Ignore this domain entity in the list.
*
* @return array
* The domains.
*/
function _cms_content_sync_get_domains(?EntityInterface $deleted = NULL) {
// Ignore if the domain contrib module doesn't exist.
if (!Drupal::service('module_handler')->moduleExists('domain')) {
return [];
}
$all_domains = \Drupal::entityTypeManager()->getStorage('domain')->loadMultiple();
$active_domains = [];
foreach ($all_domains as $domain) {
if ((bool) $domain->get('status') && (!$deleted || $domain->id() != $deleted->id())) {
if (!in_array($domain->get('hostname'), $active_domains)) {
$active_domains[] = $domain->get('hostname');
}
}
}
return $active_domains;
}
/**
* Report the site's domains to the Sync Core whenever they change.
*
* @param null|\Drupal\Core\Entity\EntityInterface $deleted
* Ignore this domain entity in the list.
*/
function _cms_content_sync_report_domains(?EntityInterface $deleted = NULL) {
// Ignore if the site wasn't registered yet.
if (!ContentSyncSettings::getInstance()->getSiteUuid()) {
return;
}
$active_domains = _cms_content_sync_get_domains($deleted);
SyncCoreFactory::getSyncCoreV2()->setDomains($active_domains);
}
/**
* Push the entity automatically if configured to do so.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be pushed.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
function cms_content_sync_entity_insert(EntityInterface $entity) {
if (!_cms_content_sync_is_installed()) {
return;
}
if ('domain' === $entity->getEntityTypeId()) {
_cms_content_sync_report_domains();
}
if (!EntityHandlerPluginManager::isSupported($entity->getEntityTypeId(), $entity->bundle())) {
return;
}
if ($entity instanceof FieldableEntityInterface) {
DefaultEntityReferenceHandler::saveEmbeddedPushToPools($entity);
}
PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_AUTOMATICALLY, SyncIntent::ACTION_CREATE);
}
/**
* Push the entity automatically if configured to do so.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be updated.
*/
function cms_content_sync_entity_update(EntityInterface $entity) {
if (!_cms_content_sync_is_installed()) {
return;
}
if ('domain' === $entity->getEntityTypeId()) {
_cms_content_sync_report_domains();
}
if (!EntityHandlerPluginManager::isSupported($entity->getEntityTypeId(), $entity->bundle())) {
return;
}
// When updating the tree, Drupal Core does NOT update the parent and weight of the tern when saving it.
// Instead, they manipulate the tree afterwards WITHOUT triggering the save again.
// The values provided by the form submit are also WRONG as Drupal keeps the parent that was set but changes the
// weight unpredictably.
// So we need to skip pushing these and instead push them all at once after the full save routing from Drupal
// is done.
if (_cms_content_sync_update_taxonomy_tree_static()) {
_cms_content_sync_update_taxonomy_tree_static(TRUE, $entity);
return;
}
if ($entity instanceof FieldableEntityInterface) {
DefaultEntityReferenceHandler::saveEmbeddedPushToPools($entity);
}
// This is actually an update, but for the case this entity existed
// before the synchronization was created or the entity could not be
// pushed before for any reason, using ::ACTION_UPDATE would lead to
// errors. Thus we're just using ::ACTION_CREATE which always works.
if (!_cms_content_sync_prevent_entity_export($entity)) {
PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_AUTOMATICALLY, SyncIntent::ACTION_UPDATE);
}
$moduleHandler = Drupal::service('module_handler');
// Limit execution to nodes.
// Push manually as well if the entity was scheduled.
if ($moduleHandler->moduleExists('scheduler') && 'node' == $entity->getEntityTypeId()) {
if ($entity->isPublished() && !$entity->get('publish_on')->getValue()) {
$original = $entity->original;
if ($original && !$original->isPublished() && $original->get('publish_on')->getValue()) {
PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_MANUALLY, SyncIntent::ACTION_UPDATE);
}
}
if (!$entity->isPublished() && !$entity->get('unpublish_on')->getValue()) {
$original = $entity->original;
if ($original && $original->isPublished() && $original->get('unpublish_on')->getValue()) {
PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_MANUALLY, SyncIntent::ACTION_UPDATE);
}
}
}
}
/**
* Push the entity deletion automatically if configured to do so.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be deleted.
*
* @throws Exception
* If this entity has been pulled and local deletion is forbidden, this will throw an error.
*/
function cms_content_sync_entity_delete(EntityInterface $entity) {
if (!_cms_content_sync_is_installed()) {
return;
}
if ('domain' === $entity->getEntityTypeId()) {
_cms_content_sync_report_domains($entity);
}
// Check if deletion has been called by the developer submodule force_deletion
// drush command.
if (Drupal::moduleHandler()->moduleExists('cms_content_sync_developer')) {
if (CliService::$forceEntityDeletion) {
return;
}
}
if (!EntityHandlerPluginManager::isSupported($entity->getEntityTypeId(), $entity->bundle())) {
return;
}
if (PullIntent::entityHasBeenPulledFromRemoteSite($entity->getEntityTypeId(), $entity->uuid())) {
return;
}
if (!Flow::isLocalDeletionAllowed($entity)
&& !PullIntent::entityHasBeenPulledFromRemoteSite()) {
throw new Exception($entity->label() . ' cannot be deleted as it has been pulled.');
}
$infos = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
// Can't propagate deletion if the entity was embedded.
$proxy = new EntityStatusProxy($infos);
if ($proxy->wasPushedEmbedded()) {
$allow_embedded_deletion = FALSE;
// Do not push paragraph deletions at all.
if ($entity->getEntityTypeId() !== 'paragraph') {
foreach ($infos as $info) {
$allow_embedded_deletion |= $info->getFlow()->getController()->canPushEmbeddedDeletion();
}
}
if (!$allow_embedded_deletion) {
// Still save the deleted status at the status entity.
foreach ($infos as $info) {
$info->isDeleted(TRUE);
$info->save();
}
return;
}
}
$pushed = PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_AUTOMATICALLY, SyncIntent::ACTION_DELETE);
// If the entity has been deleted, there will be no "push changes" button, so this content has to be deleted automatically as well.
$pushed |= PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_MANUALLY, SyncIntent::ACTION_DELETE);
// If the entity has been deleted as a dependency, it's deletion also has to be pushed.
$pushed |= PushIntent::pushEntityFromUi($entity, PushIntent::PUSH_AS_DEPENDENCY, SyncIntent::ACTION_DELETE);
$not_pushed = PushIntent::getNoPushReason($entity);
if (!empty($not_pushed) && $not_pushed instanceof SyncException) {
/**
* @var \Drupal\cms_content_sync\Exception\SyncException $not_pushed
*/
if (SyncException::CODE_INTERNAL_ERROR === $not_pushed->errorCode
|| SyncException::CODE_ENTITY_API_FAILURE === $not_pushed->errorCode
|| SyncException::CODE_PUSH_REQUEST_FAILED === $not_pushed->errorCode
|| SyncException::CODE_UNEXPECTED_EXCEPTION === $not_pushed->errorCode) {
Drupal::logger('cms_content_sync')->error($not_pushed->getMessage());
throw new Exception($entity->label() . ' cannot be deleted as the deletion could not be propagated to the Sync Core. If you need to delete this item right now, edit the Flow and disable "Export deletions" temporarily.');
}
}
$infos = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
// Entity was pulled, so we inform the Sync Core that it has been deleted on this site.
if (!empty($not_pushed) && !($not_pushed instanceof \Exception) && !empty($infos)) {
foreach ($infos as $info) {
if (!$info->wasPulledEmbedded() && $info->getLastPull()) {
try {
$info->getPool()->getClient()->getSyndicationService()->deletedLocally(
$info->getFlow()->id,
$entity->getEntityTypeId(),
$entity->bundle(),
$entity->language()->getId(),
$entity->uuid(),
EntityHandlerPluginManager::getIdOrNull($entity)
);
}
catch (\Exception $e) {
Drupal::logger('cms_content_sync')->error($e->getMessage());
\Drupal::messenger(t('Could not update your @repository to mark the entity as deleted. The entity status may be displayed incorrectly or the content may be re-pulled again by accident.', ['@repository' => _cms_content_sync_get_repository_name()]));
}
break;
}
}
}
foreach ($infos as $info) {
$info->isDeleted(TRUE);
$info->save();
}
}
/**
* Implements hook_entity_translation_delete().
*
* @param \Drupal\Core\Entity\EntityInterface $translation
* The translation to be updated.
*/
function cms_content_sync_entity_translation_delete(EntityInterface $translation) {
if (!_cms_content_sync_is_installed()) {
return;
}
if (!EntityHandlerPluginManager::isSupported($translation->getEntityTypeId(), $translation->bundle())) {
return;
}
$statuses = EntityStatus::getInfosForEntity($translation->getEntityTypeId(), $translation->uuid());
$now = time();
$use_flows = [];
foreach ($statuses as $status) {
$status->setTranslationDeletedAt($now, $translation->language()->getId());
$status->save();
if (!$status->getLastPush($translation->language()->getId())) {
continue;
}
$flow = $status->getFlow();
if (!$flow) {
continue;
}
if (!$flow->getController()->canPushEntity($translation, PushIntent::PUSH_AUTOMATICALLY, SyncIntent::ACTION_DELETE) || !$flow->getController()->canPushEntity($translation, PushIntent::PUSH_AUTOMATICALLY, SyncIntent::ACTION_UPDATE)) {
continue;
}
if (in_array($flow->id(), $use_flows)) {
continue;
}
$use_flows[] = $flow->id();
}
foreach ($use_flows as $flow_id) {
$flow = Flow::getAll()[$flow_id] ?? NULL;
if (!$flow) {
continue;
}
PushIntent::pushEntitiesFromUi([], PushIntent::PUSH_AUTOMATICALLY, SyncIntent::ACTION_UPDATE, $flow, FALSE, NULL, [
[$translation, $translation->language()->getId()]
]);
}
}
/**
* Update the password at Sync Core if it's necessary for authentication.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*/
function cms_content_sync_user_password_submit(array &$form, FormStateInterface $form_state) {
if (!_cms_content_sync_is_installed()) {
return;
}
$uid = $form_state->getValue('uid');
if (_cms_content_sync_user_id() == $uid) {
$new_data = [
'userName' => $form_state->getValue('name'),
'userPass' => $form_state->getValue('pass'),
];
// If password wasn't changed then value will be empty and we don't need it.
$new_data = array_filter($new_data);
$new_data = cms_content_sync_encrypt_values($new_data);
$userId = $form_state->getValue('uid');
$userData = Drupal::service('user.data');
$old_data = $userData->get('cms_content_sync', $userId, 'sync_data');
if ($old_data) {
$new_data = array_replace($old_data, $new_data);
}
$userData->set('cms_content_sync', $userId, 'sync_data', $new_data);
$flows = Flow::getAll();
foreach ($flows as $flow) {
$flow->save();
}
}
}
/**
* Encrypt the provided values.
*
* This is used to securely store the authentication password necessary for
* Sync Core to make changes.
*
* @param array $values
* The values to encrypt.
*
* @return array
* The input array, but with encrypted values
*/
function cms_content_sync_encrypt_values(array $values) {
$encryption_profile = EncryptionProfile::load(CMS_CONTENT_SYNC_ENCRYPTION_PROFILE_NAME);
foreach ($values as $key => $value) {
$values[$key] = Drupal::service('encryption')
->encrypt($value, $encryption_profile);
}
return $values;
}
/**
* Check whether any special behavior for pulled entities should be applied.
* This is true if the entity was pulled AND all parent entities have been
* pulled OR it doesn't have a parent entity.
* This is important to inherit e.g. resets of the "last pulled" date so that
* when a node's "last pulled" timestamp is reset, the paragraphs are not
* disabled / overridden anymore.
*
* @return bool
*/
function _cms_content_sync_enable_pull_behavior(EntityInterface $entity) {
if (!EntityStatus::getLastPullForEntity($entity)) {
return FALSE;
}
if ('paragraph' !== $entity->getEntityTypeId()) {
return TRUE;
}
/**
* @var \Drupal\paragraphs\Entity\Paragraph
*/
$parent = $entity->getParentEntity();
if (!$parent) {
return TRUE;
}
return _cms_content_sync_enable_pull_behavior($parent);
}
/**
* Disable fields.
*
* Disable all form elements if the content has been pulled and the user
* should not be able to alter pulled content.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state to get default values from.
*
* @see \cms_content_sync_form_alter()
*/
function _cms_content_sync_form_alter_disabled_fields(array &$form, FormStateInterface $form_state, EntityInterface $entity, $nested = FALSE) {
if (!_cms_content_sync_enable_pull_behavior($entity)) {
return;
}
$value_path = [];
if (!empty($form['#field_parents'])) {
$value_path = $form['#field_parents'];
}
if ('paragraph' == $entity->getEntityTypeId()) {
$value_path[] = $entity->get('parent_field_name')->value;
$value_path[] = $form['#delta'];
}
$value_path[] = 'cms_content_sync_edit_override';
if ($form_state->hasValue($value_path)) {
$value = boolval($form_state->getValue($value_path));
}
else {
$input = $form_state->getUserInput();
foreach ($value_path as $key) {
if (empty($input[$key])) {
$input = NULL;
break;
}
$input = $input[$key];
}
$value = boolval($input);
}
$entity_status = EntityStatus::getInfosForEntity(
$entity->getEntityTypeId(),
$entity->uuid()
);
$behavior = NULL;
$overridden = FALSE;
$pull_deletion = FALSE;
$merged_fields = [];
$source_url = NULL;
$language = $entity instanceof TranslatableInterface ? $entity->language()->getId() : NULL;
foreach ($entity_status as $info) {
if (!$info || !$info->getLastPull($language, TRUE)) {
continue;
}
if ($info->isSourceEntity()) {
continue;
}
if (!$info->getFlow()) {
continue;
}
if (!$source_url) {
$source_url = $info->getSourceUrl();
}
$config = $info->getFlow()
->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle());
if (isset($config['import_updates']) && (PullIntent::PULL_UPDATE_FORCE_AND_FORBID_EDITING == $config['import_updates']
|| PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN == $config['import_updates'])) {
$behavior = $config['import_updates'];
$overridden = $info->isOverriddenLocally() || $value;
$pull_deletion = boolval($config['import_deletion_settings']['import_deletion']);
if (EntityHandlerPluginManager::isEntityTypeFieldable($entity->getEntityTypeId())) {
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager */
$entityFieldManager = Drupal::service('entity_field.manager');
$type = $entity->getEntityTypeId();
$bundle = $entity->bundle();
$field_definitions = $entityFieldManager->getFieldDefinitions($type, $bundle);
foreach ($field_definitions as $key => $definition) {
$field_config = $info->getFlow()
->getController()
->getPropertyConfig($entity->getEntityTypeId(), $entity->bundle(), $key);
if (!empty($field_config['handler_settings']['merge_local_changes'])) {
$merged_fields[] = $definition->getLabel();
}
}
}
break;
}
}
if (!$behavior) {
if (!$nested) {
_cms_content_sync_warn_about_missing_references($entity, "", $entity_status);
}
return;
}
$id = bin2hex(random_bytes(4));
$allow_overrides = PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN == $behavior;
// $hide_elements = ['container', 'vertical_tabs', 'details'];.
foreach ($form as $key => $form_item) {
if (!is_array($form_item)) {
continue;
}
if (!isset($form_item['#type'])) {
continue;
}
if ('actions' != $key) {
if ($allow_overrides) {
// If we used the DISABLED attribute, we couldn't reliably remove it
// from all elements, as some should still have the attribute from other
// circumstances and we would also have to apply it nested.
// Otherwise we'd have to either submit the form and redirect to the
// edit page or reload the whole form via AJAX, conflicting with
// embedded forms.
// So instead we hide and show the elements via JavaScript, leading
// to the best usability and overall simplest / most reliable
// implementation from the options available.
$form[$key]['#attributes']['class'][] = 'cms-content-sync-edit-override-id-' . $id;
if (!$overridden) {
$form[$key]['#attributes']['class'][] = 'cms-content-sync-edit-override-hide';
}
}
else {
if ('hidden' != $form[$key]['#type'] && 'token' != $form[$key]['#type'] && empty($form[$key]['#disabled'])) {
if ('menu' == $key) {
$allow = TRUE;
$menu_link_manager = Drupal::service('plugin.manager.menu.link');
/**
* @var \Drupal\Core\Menu\MenuLinkManager $menu_link_manager
*/
$menu_items = $menu_link_manager->loadLinksByRoute('entity.' . $entity->getEntityTypeId() . '.canonical', [$entity->getEntityTypeId() => $entity->id()]);
foreach ($menu_items as $menu_item) {
if (!($menu_item instanceof MenuLinkContent)) {
continue;
}
/**
* @var \Drupal\menu_link_content\Entity\MenuLinkContent $item
*/
$item = Drupal::service('entity.repository')
->loadEntityByUuid('menu_link_content', $menu_item->getDerivativeId());
if (!$item) {
continue;
}
$menu_entity_status = EntityStatus::getInfosForEntity(
$item->getEntityTypeId(),
$item->uuid()
);
foreach ($menu_entity_status as $info) {
if (!$info || !$info->getLastPull()) {
continue;
}
if ($info->isSourceEntity()) {
continue;
}
if (!$info->getFlow()) {
continue;
}
$config = $info->getFlow()
->getController()->getEntityTypeConfig($item->getEntityTypeId(), $item->bundle());
if (PullIntent::PULL_UPDATE_FORCE_AND_FORBID_EDITING == $config['import_updates']
|| (PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN == $config['import_updates'] && !$info->isOverriddenLocally())) {
$allow = FALSE;
}
}
}
if ($allow) {
continue;
}
}
// This will transform the field from being disabled to being readonly instead. This will interfere with
// Drupal's default behavior however, so we leave it out by default.
// $form[$key]['#attributes']['class'][] = 'cms-content-sync-edit-override-disabled';.
$form[$key]['#disabled'] = TRUE;
}
}
}
// Disable entity actions for the core layout builder.
$form_object = $form_state->getFormObject();
if ('actions' == $key && $form_object instanceof OverridesEntityForm) {
if (PullIntent::PULL_UPDATE_FORCE_AND_FORBID_EDITING == $behavior) {
$form['actions']['submit']['#attributes']['disabled'] = 'disabled';
$form['actions']['discard_changes']['#attributes']['disabled'] = 'disabled';
$form['actions']['revert']['#attributes']['disabled'] = 'disabled';
}
}
// Disable the submit button when the editing of the entity is forbidden.
if ('actions' == $key && PullIntent::PULL_UPDATE_FORCE_AND_FORBID_EDITING == $behavior) {
$form['actions']['submit']['#attributes']['disabled'] = 'disabled';
}
}
$is_embedded = 'paragraph' == $entity->getEntityTypeId();
if ($allow_overrides) {
$form['cms_content_sync_edit_override'] = [
'#type' => 'checkbox',
'#default_value' => $overridden,
'#weight' => -10000,
'#title' => t('Overwrite locally and ignore future remote updates'),
'#description' => t('%label has been pulled and future remote updates would overwrite local changes.<br>You can override this to make sure that future remote updates will be ignored so your local changes persist.<br>Resetting this later will immediately reset all local changes.', [
'%label' => $is_embedded ? t('This content') : $entity->label(),
]) .
(count($merged_fields) ? '<br>' . t('Changes to @name will still be merged.', ['@name' => implode(', ', $merged_fields)]) : '') .
($pull_deletion ? '<br><strong>' . t('If the remote content is deleted, this content will also be deleted locally and your local changes will be lost.') . '</strong>' : ''),
'#attributes' => [
'class' => ['cms-content-sync-edit-override'],
'data-cms-content-sync-edit-override-id' => $id,
],
];
$form['cms_content_sync_edit_override__entity_type'] = [
'#type' => 'hidden',
'#value' => $entity->getEntityTypeId(),
];
$form['cms_content_sync_edit_override__entity_uuid'] = [
'#type' => 'hidden',
'#value' => $entity->uuid(),
];
$form['actions']['submit']['#submit'][] = '_cms_content_sync_override_entity_submit';
}
elseif (!$is_embedded) {
if ($source_url) {
\Drupal::messenger()->addWarning(t('%label cannot be edited as it was published by another site. The content can only be edited at the source site @source.', [
'%label' => $entity->label(),
'@source' => Link::fromTextAndUrl(t('here'), Url::fromUri($source_url))->toString(),
]));
}
else {
\Drupal::messenger()->addWarning(t('%label cannot be edited as it was published by another site.', [
'%label' => $entity->label(),
]));
}
}
if (!$nested) {
_cms_content_sync_warn_about_missing_references($entity, "", $entity_status);
}
}
/**
*
*/
function _cms_content_sync_warn_about_missing_references(EntityInterface $entity, string $entity_label = "", ?array $entity_status = NULL) {
if (!$entity_status) {
$entity_status = EntityStatus::getInfosForEntity(
$entity->getEntityTypeId(),
$entity->uuid()
);
}
$field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
$language = $entity instanceof TranslatableInterface ? $entity->language()->getId() : 'und';
$entity_status_instance = count($entity_status) > 0 ? new EntityStatusProxy($entity_status) : $entity_status[0];
if (!empty($entity_status_instance)) {
$missing = $entity_status_instance->getMissingReferences($language);
if ($missing) {
foreach ($missing as $property => $values) {
foreach ($values as $value) {
$property_name = $property;
if ($entity instanceof FieldableEntityInterface) {
$field_definition = $field_definitions[$property] ?? NULL;
if ($field_definition) {
// If an editor provided a value in the meantime, we ignore this.
if (!$field_definition->getFieldStorageDefinition()->isMultiple()) {
if (!empty($entity->get($property)->value) && !in_array($field_definition->getType(), ['text_with_summary', 'text_long'])) {
continue;
}
}
if (method_exists($field_definition, 'getLabel')) {
$property_name = $field_definition->getLabel();
}
elseif (method_exists($field_definition, 'label')) {
$property_name = $field_definition->label();
}
}
}
$name = empty($value['name']) ? 'an unnamed entity' : $value['name'];
if ($entity_label) {
$property_name = $entity_label . " > " . $property_name;
}
if (!empty($value['viewUrl'])) {
$link = Link::fromTextAndUrl($name, Url::fromUri($value['viewUrl']))->toString();
\Drupal::messenger()->addWarning(t('%property is missing a link to @link.', [
'%property' => $property_name,
'@link' => $link,
]));
}
else {
\Drupal::messenger()->addWarning(t('%property is missing a link to %name.', [
'%property' => $property_name,
'%name' => $name,
]));
}
}
}
}
}
// Look for nested missing references for paragraphs.
if ($field_definitions) {
foreach ($field_definitions as $key => $field_definition) {
if ('entity_reference_revisions' == $field_definition->getType()) {
if ($field_definition->getSetting('target_type') === 'paragraph') {
foreach ($entity->get($key)->referencedEntities() as $paragraph) {
_cms_content_sync_warn_about_missing_references($paragraph, $paragraph->label());
}
}
}
}
}
}
/**
* Entity status update.
*
* Update the EntityStatus for the given entity, setting
* the EntityStatus::FLAG_EDIT_OVERRIDE flag accordingly.
*/
function _cms_content_sync_override_entity_submit(array $form, FormStateInterface $form_state) {
$value = boolval($form_state->getValue('cms_content_sync_edit_override'));
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
*/
$entity = _cms_content_sync_get_entity_from_form($form_state);
$entity_status = EntityStatus::getInfosForEntity(
$entity->getEntityTypeId(),
$entity->uuid()
);
foreach ($entity_status as $info) {
if (!$info || !$info->getLastPull() || !$info->getFlow()) {
continue;
}
$config = $info->getFlow()
->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle());
if (PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN == $config['import_updates']) {
if ($value != $info->isOverriddenLocally()) {
$info->isOverriddenLocally($value);
$info->save();
if (!$value) {
_cms_content_sync_reset_entity($entity, $info);
}
}
break;
}
}
}
/**
* Reset entity.
*
* Gets the latest version of the entity from the sync core and override to
* local version.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be resetted.
* @param \Drupal\cms_content_sync\Entity\EntityStatus $status
* The related status entity of the entity.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
function _cms_content_sync_reset_entity(EntityInterface $entity, EntityStatus $status) {
if ($status->wasPulledEmbedded()) {
$parent = $status->getParentEntity();
if (!$parent) {
Drupal::messenger()
->addWarning(t("Overwrite changed but @entity_type_id @bundle %label could not be reset to it's original values as it was embedded and the parent entity doesn't exist.", [
'@entity_type_id' => $entity->getEntityTypeId(),
'@bundle' => $entity->bundle(),
'%label' => $entity->label(),
'@uuid' => $entity->uuid(),
]));
return;
}
$parent_statuses = EntityStatus::getInfosForEntity($parent->getEntityTypeId(), $parent->uuid(), [
'flow' => $status->getFlow()->id(),
]);
if (empty($parent_statuses)) {
$parent_statuses = EntityStatus::getInfosForEntity($parent->getEntityTypeId(), $parent->uuid());
}
if (empty($parent_statuses)) {
Drupal::messenger()
->addWarning(t("Overwrite changed but @entity_type_id @bundle %label could not be reset to it's original values as it was embedded and the parent entity doesn't have a pull status.", [
'@entity_type_id' => $entity->getEntityTypeId(),
'@bundle' => $entity->bundle(),
'%label' => $entity->label(),
'@uuid' => $entity->uuid(),
]));
return;
}
// We just take the first match as it's the same entity in the Sync Core.
$parent_status = reset($parent_statuses);
_cms_content_sync_reset_entity($parent, $parent_status);
return;
}
$shared_entity_id = EntityHandlerPluginManager::getSharedId($entity);
try {
$flow = $status->getFlow();
$manually = $flow->getController()->canPullEntity($entity->getEntityTypeId(), $entity->bundle(), PullIntent::PULL_MANUALLY, SyncIntent::ACTION_CREATE, TRUE);
$dependency = $flow->getController()->canPullEntity($entity->getEntityTypeId(), $entity->bundle(), PullIntent::PULL_AS_DEPENDENCY, SyncIntent::ACTION_CREATE, TRUE);
$update_id = $status
->getPool()
->getClient()
->getSyndicationService()
->pullSingle($flow->id, $entity->getEntityTypeId(), $entity->bundle(), $shared_entity_id)
->fromPool($status->getPool()->id)
->manually($manually)
->asDependency($dependency)
->execute()
->getId();
$message = t('Overwrite changed; @entity_type_id @bundle %label will be pulled again within a few seconds, you may want to <a href="javascript:window.location.href=window.location.href">reload</a> the page to see the changes.', [
'@entity_type_id' => $entity->getEntityTypeId(),
'@bundle' => $entity->bundle(),
'%label' => $entity->label(),
'@uuid' => $entity->uuid(),
]);
if ($update_id) {
$embed = Embed::create(\Drupal::getContainer());
$update_progress = $embed->updateStatusBox($update_id, TRUE);
Drupal::messenger()->addMessage([
'message' => ['#markup' => $message],
'update_progress' => $update_progress,
]);
}
else {
Drupal::messenger()->addMessage($message);
}
}
catch (SyncCoreException $e) {
Drupal::messenger()->addWarning(t('Overwrite changed, but failed to pull entity @entity_type_id @bundle %label (@uuid): @message', [
'@entity_type_id' => $entity->getEntityTypeId(),
'@bundle' => $entity->bundle(),
'%label' => $entity->label(),
'@uuid' => $entity->uuid(),
'@message' => $e->getMessage(),
]));
return;
}
}
/**
* Entity status update.
*
* Update the EntityStatus for the given entity, setting
* the EntityStatus::FLAG_EDIT_OVERRIDE flag accordingly.
*
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
*/
function _cms_content_sync_override_embedded_entity_submit(array $form, FormStateInterface $form_state) {
$value = boolval($form_state->getValue('cms_content_sync_edit_override'));
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
*/
$entity = _cms_content_sync_get_entity_from_form($form_state);
if ($entity instanceof FieldableEntityInterface) {
_cms_content_sync_override_embedded_entity_save_status_entity($entity, $form, $form_state, [], !$value);
}
}
/**
* Override the embedded entities status entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity to be overridden.
* @param array $form
* The Drupal form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The Drupal form state.
* @param array $tree_position
* The current tree position.
* @param bool $force_disable
* Check for the force disable option.
*/
function _cms_content_sync_override_embedded_entity_save_status_entity(FieldableEntityInterface $entity, array $form, FormStateInterface $form_state, array $tree_position = [], $force_disable = FALSE) {
$entityFieldManager = Drupal::service('entity_field.manager');
/** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */
$fields = $entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
foreach ($fields as $name => $definition) {
if ('entity_reference_revisions' == $definition->getType()) {
$subform = &$form[$name]['widget'];
$count = $subform['#max_delta'];
for ($i = 0; $i <= $count; ++$i) {
$value = $force_disable ? FALSE : boolval($form_state->getValue(array_merge($tree_position, [
$name,
$i,
'cms_content_sync_edit_override'
])));
$embedded_entity_type = $form_state->getValue(array_merge($tree_position, [
$name,
$i,
'cms_content_sync_edit_override__entity_type'
]));
$embedded_entity_uuid = $form_state->getValue(array_merge($tree_position, [
$name,
$i,
'cms_content_sync_edit_override__entity_uuid'
]));
// In case editing has been restricted by other code, we have to
// ignore this item.
if (!$embedded_entity_type || !$embedded_entity_uuid) {
continue;
}
$embedded_entity = Drupal::service('entity.repository')
->loadEntityByUuid(
$embedded_entity_type,
$embedded_entity_uuid
);
if (!$embedded_entity) {
continue;
}
if (!empty($subform[$i]['subform'])) {
_cms_content_sync_override_embedded_entity_save_status_entity($embedded_entity, $subform[$i]['subform'], $form_state, array_merge($tree_position, [
$name,
$i,
'subform'
]), !$value);
}
$entity_status = EntityStatus::getInfosForEntity(
$embedded_entity->getEntityTypeId(),
$embedded_entity->uuid()
);
foreach ($entity_status as $info) {
if (!$info || !$info->getLastPull() || !$info->getFlow()) {
continue;
}
$config = $info->getFlow()
->getController()->getEntityTypeConfig($embedded_entity->getEntityTypeId(), $embedded_entity->bundle());
if (PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN == $config['import_updates']) {
if ($value != $info->isOverriddenLocally()) {
$info->isOverriddenLocally($value);
$info->save();
if (!$value && !$force_disable) {
_cms_content_sync_reset_entity($embedded_entity, $info);
}
}
break;
}
}
}
}
}
}
/**
* Implements hook_theme().
*/
function cms_content_sync_theme() {
$theme['cms_content_sync_content_dashboard'] = [
'variables' => ['configuration' => NULL],
'template' => 'cms_content_sync_content_dashboard',
];
$theme['cms_content_sync_compatibility'] = [
'variables' => ['supported_entity_types' => NULL],
'template' => 'cms_content_sync_compatibility',
];
$theme['cms_content_sync_show_usage'] = [
'variables' => ['usage' => NULL],
'template' => 'cms_content_sync_show_usage',
];
return $theme;
}
/**
* Implements hook_action_info_alter().
*
* Optionally rename "push changes" action to "push changes to live".
*/
// Function cms_content_sync_action_info_alter(array &$actions)
// {
// if (!_cms_content_sync_is_installed()) {
// return;
// }
//
// if(!empty($actions['node_cms_content_sync_export_action'])) {
// $actions['node_cms_content_sync_export_action']['label'] = _cms_content_sync_push_label();
// }
// }
/**
* Implements hook_entity_operation_alter().
*
* Provide "push changes" option.
*/
function cms_content_sync_entity_operation_alter(array &$operations, EntityInterface $entity) {
if (!_cms_content_sync_is_installed()) {
return;
}
$operations += cms_content_sync_get_publish_changes_operations($entity);
$operations += cms_content_sync_show_usage_operation($entity);
}
/**
* Returns operations for "push changes" action.
*/
function cms_content_sync_get_publish_changes_operations(EntityInterface $entity) {
if (!Drupal::currentUser()->hasPermission('publish cms content sync changes')) {
return [];
}
if (!EntityHandlerPluginManager::isSupported($entity->getEntityTypeId(), $entity->bundle())) {
return [];
}
$operations = [];
/** @var \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination */
$redirect_destination = Drupal::service('redirect.destination');
$flows = PushIntent::getFlowsForEntity($entity, PushIntent::PUSH_MANUALLY);
if (!count($flows)) {
return [];
}
if (_cms_content_sync_prevent_entity_export($entity)) {
return [];
}
$translatable = $entity instanceof TranslatableInterface;
foreach ($flows as $flow) {
$route_parameters = [
'flow_id' => $flow->id(),
'entity_type' => $entity->getEntityTypeId(),
'entity_id' => $entity->id(),
] + ($translatable ? ['language' => $entity->language()->getId()] : []);
$operations['publish_changes_' . $flow->id()] = [
'title' => count($flows) > 1 ? t('Push changes - %name', ['%name' => $flow->name]) : _cms_content_sync_push_label(),
'weight' => 150,
'url' => Url::fromRoute($translatable ? 'cms_content_sync.publish_changes_confirm_translation' : 'cms_content_sync.publish_changes_confirm', $route_parameters),
'query' => $redirect_destination->getAsArray(),
];
}
return $operations;
}
/**
* Callback function for the show operation entity action.
*/
function cms_content_sync_show_usage_operation(EntityInterface $entity) {
if (!\Drupal::currentUser()->hasPermission('view cms content sync syndication status')) {
return [];
}
if (!EntityHandlerPluginManager::isSupported($entity->getEntityTypeId(), $entity->bundle())) {
return [];
}
$flows = PushIntent::getFlowsForEntity($entity, PushIntent::PUSH_ANY);
if (!count($flows)) {
return [];
}
$status_entities = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
$is_pushed = FALSE;
foreach ($status_entities as $status_entity) {
$last_push = $status_entity->get('last_export')->value;
if (!is_null($last_push)) {
$is_pushed = TRUE;
}
}
// Only show the operation for entities which have been pushed.
if (!$is_pushed) {
return [];
}
$operations = [];
if (in_array($entity->getEntityTypeId(), ['node', 'taxonomy_term', 'menu_link_content', 'block_content', 'media', 'paragraphs_library_item'])) {
$operations['show_usage'] = [
'title' => t('Show usage'),
'weight' => 151,
'url' => Url::fromRoute('cms_content_sync.' . ($entity->getEntityTypeId() === 'node' ? 'content' : $entity->getEntityTypeId()) . '_sync_status', [
$entity->getEntityTypeId() => $entity->id(),
]),
];
}
else {
$operations['show_usage'] = [
'title' => t('Show usage'),
'weight' => 151,
'attributes' => [
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => ShowUsage::DIALOG_WIDTH,
]),
],
'url' => Url::fromRoute('cms_content_sync.show_usage', [
'entity' => $entity->id(),
'entity_type' => $entity->getEntityTypeId(),
]),
];
}
return $operations;
}
/**
* Implements hook_entity_operation().
*/
function cms_content_sync_entity_operation(EntityInterface $entity) {
$operations = [];
try {
$async_export_enabled = SyncCoreFactory::getSyncCoreV2()->featureEnabled(ISyncCore::FEATURE_ASYNC_SITE_CONFIG_AVAILABLE);
}
catch (\Exception $e) {
$async_export_enabled = FALSE;
}
if ('cms_content_sync_flow' == $entity->getEntityTypeId()) {
$enabled = !empty(Flow::getAll()[$entity->id()]);
if ($enabled) {
if (!$async_export_enabled) {
$operations['export'] = [
'title' => t('Export to Sync Core'),
'weight' => 10,
'url' => Url::fromRoute('entity.cms_content_sync_flow.export', ['cms_content_sync_flow' => $entity->id()]),
];
}
if (Flow::TYPE_PUSH !== $entity->type) {
$operations['pull_all'] = [
'title' => t('Pull all content'),
'weight' => 10,
'url' => Flow::VARIANT_SIMPLE === $entity->variant ? Url::fromRoute('cms_content_sync.syndication', [], [
'query' => [
'flow' => $entity->id(),
'startNew' => 'true'
]
]) : Url::fromRoute('entity.cms_content_sync_flow.pull_confirmation', [
'cms_content_sync_flow' => $entity->id()
]),
];
}
if (Flow::TYPE_PULL !== $entity->type) {
$operations['push_all'] = [
'title' => t('Push all content'),
'weight' => 10,
'url' => Flow::VARIANT_SIMPLE === $entity->variant ? Url::fromRoute('cms_content_sync.syndication', [], [
'query' => [
'flow' => $entity->id(),
'startNew' => 'true'
]
]) : Url::fromRoute('entity.cms_content_sync_flow.push_confirmation', [
'cms_content_sync_flow' => $entity->id()
]),
];
}
}
$set_status_title = $enabled ? t('Set inactive') : t('Set active');
$operations['set_status'] = [
'title' => $set_status_title,
'weight' => 10,
'url' => Url::fromRoute('entity.cms_content_sync_flow.set_status', ['cms_content_sync_flow' => $entity->id()]),
];
}
elseif ('cms_content_sync_pool' == $entity->getEntityTypeId()) {
$operations['export'] = [
'title' => t('Export'),
'weight' => 10,
'url' => Url::fromRoute('entity.cms_content_sync_pool.export', ['cms_content_sync_pool' => $entity->id()]),
];
$operations['reset_status'] = [
'title' => t('Reset status entities'),
'weight' => 10,
'url' => Url::fromRoute('entity.cms_content_sync_pool.reset_status_entity_confirmation', ['cms_content_sync_pool' => $entity->id()]),
];
}
return $operations;
}
/**
* Implements hook_form_menu_edit_form_alter().
*
* Provide "push changes" action link.
*
* @param array $form
* The Drupal form.
*/
function cms_content_sync_form_menu_edit_form_alter(array &$form) {
$links = [];
if (!empty($form['links']['links'])) {
$links = Element::children($form['links']['links']);
}
foreach ($links as $link_key) {
$link = $form['links']['links'][$link_key];
/** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $menu_link */
$menu_link = $link['#item']->link;
if (!method_exists($menu_link, 'getEntity')) {
continue;
}
// We need to get an Entity at this point,
// but 'getEntity' is protected for some reason.
// So we don't have other choice here but use a reflection.
$menu_link_reflection = new ReflectionMethod('\Drupal\menu_link_content\Plugin\Menu\MenuLinkContent', 'getEntity');
$menu_link_reflection->setAccessible(TRUE);
$menu_link_entity = $menu_link_reflection->invoke($menu_link, 'getEntity');
$form['links']['links'][$link_key]['operations']['#links'] += cms_content_sync_get_publish_changes_operations($menu_link_entity);
}
}
/**
* Implements hook_local_tasks_alter().
*
* @param array $local_tasks
* Definitions to alter.
*/
function cms_content_sync_local_tasks_alter(array &$local_tasks) {
// Change tab title based on whether the subscriber is using the cloud or self-hosted version.
if (isset($local_tasks['entity.cms_content_sync.pull'])) {
$local_tasks['entity.cms_content_sync.pull']['title'] = _cms_content_sync_get_repository_name();
}
}
/**
* Name the manual pull tab and entity edit settings tab.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The manual pull tab naming.
*/
function _cms_content_sync_get_repository_name() {
return t('Content Repository');
}
/**
* Check whether the Sync Cores used are Cloud based. Default if none exist is YES.
*
* @return bool
* Returns true or false if the content sync cloud version is used.
*/
function _cms_content_sync_is_cloud_version() {
static $result = NULL;
if (NULL !== $result) {
return $result;
}
$result = TRUE;
foreach (Pool::getAll() as $pool) {
try {
if (FALSE === strpos($pool->getSyncCoreUrl(), 'content-sync.io')) {
$result = FALSE;
break;
}
}
// If the site isn't registered yet, we have no Sync Core URL, so we assume
// this is going to use our cloud version by default.
catch (\Exception $e) {
}
}
return $result;
}
/**
* Implements hook_preprocess_html().
*/
function cms_content_sync_preprocess_html(&$vars) {
// Add the possiblity to hide the admin toolbar for the import dashboard
// live previews.
$isPreview = \Drupal::request()->query->get('content_sync_preview');
if (isset($isPreview)) {
$vars['attributes']['class'][] = 'content-sync-preview';
$vars['#attached']['library'][] = 'cms_content_sync/preview';
}
}
/**
* Implements hook_entity_presave().
*
* Overwrite password expiration for the content sync user.
*/
function cms_content_sync_entity_presave(EntityInterface $entity) {
$moduleHandler = \Drupal::service('module_handler');
if ($moduleHandler->moduleExists('password_policy')) {
if ($entity instanceof User && $entity->id() == _cms_content_sync_user_id() && $entity->hasField('field_password_expiration')) {
$field_password_expiration = $entity->get('field_password_expiration')->value;
if ($field_password_expiration == 1) {
$entity->set('field_password_expiration', 0);
}
}
}
}
/**
* Implements hook_form_FORM_ID_alter() for user_form.
*/
function cms_content_sync_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$moduleHandler = \Drupal::service('module_handler');
if ($moduleHandler->moduleExists('password_policy')) {
$entity = _cms_content_sync_get_entity_from_form($form_state);
if ($entity instanceof User && $entity->id() == _cms_content_sync_user_id() && $entity->hasField('field_password_expiration')) {
$form['field_password_expiration']['#disabled'] = TRUE;
$form['field_password_expiration']['widget']['value']['#description'] .= '<br>' . t('Disabled - Never let the Content Sync user password expire to avoid any unwanted interruptions');
$form['field_last_password_reset']['#disabled'] = TRUE;
}
}
}
/**
* Implements hook_form_FORM_ID_alter() for node_form.
*
* Show a warning message on node form for menu items that are pulled.
*/
function cms_content_sync_form_node_form_alter(&$form, FormStateInterface &$form_state, $form_id) {
if (!empty($form['menu'])) {
$menu =& $form['menu'];
$menu_item_id = $menu['link']['entity_id']['#value'] ?? '';
if (empty($menu_item_id)) {
return NULL;
}
// Load the menu entity and get the status entity.
$menu_item = Drupal::entityTypeManager()->getStorage('menu_link_content')->load($menu_item_id);
if (empty($menu_item)) {
return NULL;
}
// Check if menu item is pulled from upstream.
/** @var \Drupal\cms_content_sync\Entity\EntityStatus[] $entity_status */
$entity_status = EntityStatus::getInfosForEntity('menu_link_content', $menu_item->uuid());
$show_warning_message = FALSE;
foreach ($entity_status as $info) {
if (!$info) {
continue;
}
// Check if amy of the flows allow local overrides.
if ($info->getFlow()->getController()->getEntityTypeConfig('menu_link_content', $menu_item->bundle())['import_updates'] !== PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN) {
continue;
}
// Check if the menu item is pulled from upstream and not overridden
// locally to show thee warning message.
if (!empty($info->getLastPull()) && !$info->isOverriddenLocally()) {
$show_warning_message = TRUE;
break;
}
}
// If the menu item is not pulled, do not show the warning message.
if (!$show_warning_message) {
return NULL;
}
// Build the link to the edit page.
$menu_edit_link = Link::fromTextAndUrl(t('here'), Url::fromRoute('entity.menu_link_content.canonical', [
'menu_link_content' => $menu_item->id(),
['destination' => \Drupal::service('path.current')->getPath()],
])
);
// Add the warning message to the menu form on node edit.
$menu['message'] = [
'#weight' => -10,
'#theme' => 'status_messages',
'#message_list' => [
'warning' => [
t('This menu item is pulled independently and changes may be lost during the next import. Please edit the menu item @link instead!', [
'@link' => $menu_edit_link->toString(),
]),
],
],
'#status_headings' => [
'warning' => t('Warning'),
],
];
}
}
