cms_content_sync-3.0.x-dev/src/Controller/Embed.php
src/Controller/Embed.php
<?php
namespace Drupal\cms_content_sync\Controller;
use Drupal\block_content\BlockContentInterface;
use Drupal\cms_content_sync\Entity\EntityStatus;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Form\JsonForm;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\SyncCoreInterface\DrupalApplication;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\media\MediaInterface;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\node\NodeInterface;
use EdgeBox\SyncCore\Exception\UnauthorizedException;
use EdgeBox\SyncCore\Interfaces\Embed\IEmbedService;
use EdgeBox\SyncCore\Interfaces\ISyncCore;
use EdgeBox\SyncCore\V2\Raw\Model\RemoteSiteConfigRequestMode;
use Firebase\JWT\JWT;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Class Embed provides helpers to embed Sync Core functionality into the site.
*/
class Embed extends ControllerBase {
/**
* SyncCoreFactory.
*
* @var \Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory
*/
protected $core;
/**
* SyncCoreFactory.
*
* @var ContentSyncSettings
*/
protected $settings;
/**
* Entity Type parameters.
*
* @var array
*/
protected $params;
/**
* The Sync Core embed service.
*/
protected $embedService;
/**
* Check if the site has been migrated.
*/
public static function didMigrate() {
return !empty(\Drupal::service('config.factory')
->get('cms_content_sync.migration')
->get('cms_content_sync_v2_pool_statuses'));
}
/**
* Embed the syndication dashboard.
*/
public function syndicationDashboard() {
$this->init();
if (!$this->settings->getSiteUuid()) {
\Drupal::messenger()->addWarning($this->t('Please register your site first.'));
return new RedirectResponse(Url::fromRoute('cms_content_sync.site', ['absolute' => TRUE])->toString());
}
$embed = $this->embedService->syndicationDashboard([
'selectedFlowMachineName' => $this->params['flow'] ?? NULL,
'startNew' => isset($this->params['startNew']) ? 'true' === $this->params['startNew'] : FALSE,
'forbidStartNew' => FALSE,
'query' => $this->params,
]);
return $this->run($embed, []);
}
/**
* Embed the site tab.
*/
public function site() {
$this->init(TRUE);
// Already registered if this exists.
$uuid = $this->settings->getSiteUuid();
$force = empty($this->params['force']) ? NULL : $this->params['force'];
$this->params['migrated'] = self::didMigrate();
$this->params['domains'] = _cms_content_sync_get_domains();
$is_registered = $uuid && (empty($this->params['force']) || IEmbedService::REGISTER_SITE !== $this->params['force']);
if ($is_registered) {
$this->params['createFlowUrl'] = Url::fromRoute('entity.cms_content_sync_flow.add_form', [], ['absolute' => TRUE])->toString();
// TODO: Provide a route to export all Flows (like drush cse)
// $this->params['exportAllFlowsUrl'] = '';.
$this->params['existingFlows'] = [];
foreach (Flow::getAll() as $flow) {
$this->params['existingFlows'][] = [
'machineName' => $flow->id(),
'name' => $flow->label(),
'exportUrl' => Url::fromRoute('entity.cms_content_sync_flow.export', ['cms_content_sync_flow' => $flow->id()], ['absolute' => TRUE])->toString(),
'requiresExport' => $flow->getController()->needsEntityTypeUpdate(),
];
}
}
$embed = $is_registered
? $this->embedService->siteRegistered($this->params)
: $this->embedService->registerSite($this->params);
try {
$result = $this->run($embed);
// Site was just registered.
if (!empty($this->params['uuid'])) {
// Clear feature flag cache.
SyncCoreFactory::clearCache();
// Automatically enable the private environment submodule and set the request polling feature flag
// if this is a localhost environment that requires it anyway.
if ($this->params['enableRequestPolling'] === 'true') {
$module_installed = \Drupal::moduleHandler()->moduleExists('cms_content_sync_private_environment');
if (!$module_installed) {
\Drupal::service('module_installer')->install(['cms_content_sync_private_environment']);
}
$client = SyncCoreFactory::getSyncCoreV2();
$enabled = $client->featureEnabled(ISyncCore::FEATURE_REQUEST_POLLING);
if (!$enabled) {
$client->enableFeature(ISyncCore::FEATURE_REQUEST_POLLING);
}
}
// Export configuration immediately if it's available as an asynchronous operation.
SyncCoreFactory::export(RemoteSiteConfigRequestMode::ALL, FALSE);
}
return $result;
}
// The site registration JWT expires after only 5 minutes, then the user must get a new one.
catch (UnauthorizedException $e) {
\Drupal::messenger()->addMessage('Your registration token expired. Please try again.', 'warning');
$url = \Drupal::request()->getRequestUri();
// Remove invalid query params so the user can try again.
$url = explode('?', $url)[0];
return new RedirectResponse($url);
}
}
/**
* Embed the site settings / advanced settings tab.
*/
public function siteSettings() {
$this->init();
if (!$this->settings->getSiteUuid()) {
\Drupal::messenger()->addWarning($this->t('Please register your site first.'));
return new RedirectResponse(Url::fromRoute('cms_content_sync.site', ['absolute' => TRUE])->toString());
}
$force = empty($this->params['force']) ? NULL : $this->params['force'];
$embed = $this->embedService->siteSettings($this->params);
$result = $this->run($embed);
return $result;
}
/**
* Embed the flow edit form.
*/
public function flowEditForm(Flow $cms_content_sync_flow) {
if (Flow::VARIANT_PER_BUNDLE === $cms_content_sync_flow->variant) {
$request = \Drupal::request();
// Drupal will ignore whatever you pass to the redirect response and
// instead use the destination query parameter if given. So we have to remove it.
if ($request->query->get('destination')) {
$destination = $request->query->get('destination');
$request->query->remove('destination');
}
return new RedirectResponse(Url::fromRoute('entity.cms_content_sync_flow.edit_form_advanced', [
'cms_content_sync_flow' => $cms_content_sync_flow->id
], [
'absolute' => TRUE,
'query' => empty($destination) ? [] : ['destination' => $destination]
])->toString());
}
return $this->flowForm($cms_content_sync_flow);
}
/**
* Embed the flow creation form.
*/
public function flowForm(?Flow $flow) {
$this->init();
if (!$this->settings->getSiteUuid()) {
\Drupal::messenger()->addWarning($this->t('Please register your site first.'));
return new RedirectResponse(Url::fromRoute('cms_content_sync.site', ['absolute' => TRUE])->toString());
}
$controller = $flow ? $flow->getController() : NULL;
$embed = $this->embedService->flowForm($controller && $controller instanceof FlowControllerSimple ? $controller->getFormValues() : FlowControllerSimple::getFormValuesForNewFlow($flow));
return $this->run($embed, [
'form' => \Drupal::formBuilder()->getForm(JsonForm::class),
]);
}
/**
* Embed the pull dashboard.
*/
public function pullDashboard() {
$this->init();
$user = \Drupal::currentUser();
$embed = $this->embedService->pullDashboard([
'configurationAccess' => $user->hasPermission('administer cms content sync'),
'query' => $this->params,
]);
return $this->run($embed);
}
/**
* Emebd the node status. Can't use general entityStatus because too many
* modules simply expect routes at /node/:id to use a parameter named node,
* so Drupal dies from exceptions if you name the parameter $entity.
*/
public function nodeStatus(NodeInterface $node) {
return $this->entityStatus($node);
}
/**
* Emebd the media status.
*/
public function mediaStatus(MediaInterface $media) {
return $this->entityStatus($media);
}
/**
* Emebd the block status.
*/
public function blockContentStatus(BlockContentInterface $block_content) {
return $this->entityStatus($block_content);
}
/**
* Emebd the term status.
*/
public function taxonomyTermStatus(EntityInterface $taxonomy_term) {
return $this->entityStatus($taxonomy_term);
}
/**
* Emebd the menu item status.
*/
public function menuLinkContentStatus(MenuLinkContentInterface $menu_link_content) {
return $this->entityStatus($menu_link_content);
}
/**
* Emebd the paragraphs library item status.
*/
public function paragraphsLibraryItemStatus(EntityInterface $paragraphs_library_item) {
return $this->entityStatus($paragraphs_library_item);
}
/**
* Emebd the commerce product status.
*/
public function commerceProductStatus(EntityInterface $commerce_product) {
return $this->entityStatus($commerce_product);
}
/**
* Emebd the commerce product status.
*/
public function commerceProductVariationStatus(EntityInterface $commerce_product_variation) {
return $this->entityStatus($commerce_product_variation);
}
/**
*
*/
protected function serializeReference(EntityInterface $entity) {
$params = [
'namespaceMachineName' => $entity->getEntityTypeId(),
'machineName' => $entity->bundle(),
];
if (EntityHandlerPluginManager::mapById($entity->getEntityType())) {
$params['remoteUniqueId'] = $entity->id();
}
else {
$params['remoteUuid'] = $entity->uuid();
}
return $params;
}
/**
*
*/
protected function getRootEntities(EntityInterface $entity, array &$result) {
$status_entities = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
foreach ($status_entities as $status_entity) {
if (!$status_entity->wasPushedEmbedded() && !$status_entity->wasPulledEmbedded()) {
foreach ($result as $candidate) {
if ($candidate->getEntityTypeId() === $entity->getEntityTypeId() && $candidate->uuid() === $entity->uuid()) {
continue 2;
}
}
$result[] = $entity;
continue;
}
$parent = $status_entity->getParentEntity();
if (!$parent) {
continue;
}
$this->getRootEntities($parent, $result);
}
}
/**
*
*/
protected function getSerializedRootEntities(EntityInterface $entity) {
$root_entities = [];
$this->getRootEntities($entity, $root_entities);
$root_entities_serialized = [];
foreach ($root_entities as $root_entity) {
$root_entities_serialized[] = $this->serializeReference($root_entity);
}
return $root_entities_serialized;
}
/**
* Emebd the entity status.
*/
public function entityStatus(EntityInterface $entity) {
$this->init();
$user = \Drupal::currentUser();
$params = $this->serializeReference($entity);
$params['configurationAccess'] = $user->hasPermission('administer cms content sync');
$params['rootEntities'] = $this->getSerializedRootEntities($entity);
$embed = $this->embedService->entityStatus($params);
return $this->run($embed);
}
/**
* Embed the udpate status box.
*
* @param \Drupal\Core\Entity\EntityInterface|string $entityOrId
* @param bool from_recent_activity
*/
public function updateStatusBox(mixed $entityOrId, ?bool $from_recent_activity = NULL) {
$this->init();
if (is_string($entityOrId)) {
$params = [
'id' => $entityOrId,
];
}
else {
/**
* @var \Drupal\Core\Entity\EntityInterface $entityOrId
*/
$params = [
'namespaceMachineName' => $entityOrId->getEntityTypeId(),
'machineName' => $entityOrId->bundle(),
];
if (EntityHandlerPluginManager::mapById($entityOrId->getEntityType())) {
$params['remoteUniqueId'] = $entityOrId->id();
}
else {
$params['remoteUuid'] = $entityOrId->uuid();
}
$params['rootEntities'] = $this->getSerializedRootEntities($entityOrId);
}
if ($from_recent_activity) {
$params['fromRecentActivity'] = TRUE;
$params['after'] = \Drupal::time()->getRequestTime() * 1_000;
}
$embed = $this->embedService->updateStatusBox($params);
return $this->run($embed, [], 'line');
}
/**
* Initialise the service.
*/
protected function init($expectJwtWithSyncCoreUrl = FALSE) {
$this->params = \Drupal::request()->query->all();
if ($expectJwtWithSyncCoreUrl) {
if (isset($this->params['uuid'], $this->params['jwt'])) {
$tks = explode('.', $this->params['jwt']);
[, $bodyb64] = $tks;
$payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64));
if (!empty($payload->syncCoreBaseUrl)) {
DrupalApplication::get()->setSyncCoreUrl($payload->syncCoreBaseUrl);
}
}
}
$this->core = SyncCoreFactory::getDummySyncCoreV2();
$this->settings = ContentSyncSettings::getInstance();
$this->embedService = $this->core->getEmbedService();
}
/**
* Run the service.
*/
protected function run($embed, $extras = [], $size = 'page') {
$result = $embed->run();
$redirect = $result->getRedirectUrl();
if ($redirect) {
return new RedirectResponse($redirect);
}
$html = $result->getRenderedHtml();
// As the are issuing JWTs that are included in the render array, we don't
// want to cache anything.
\Drupal::service('page_cache_kill_switch')->trigger();
$style = 'line' === $size ? 'margin-top: 3px;' : 'margin-top: 20px;';
// @todo Put this in a theme file.
return [
'#type' => 'inline_template',
'#template' => '<div className="content-sync-embed-wrapper" style="' . $style . '">{{ html|raw }}</div>',
'#context' => [
'html' => $html,
],
'#cache' => [
'max-age' => 0,
],
'#attached' => [
'library' => [
'core/jquery',
],
],
] + $extras;
}
}
