gatsby-8.x-1.x-dev/gatsby.module
gatsby.module
<?php
/**
* @file
* Primary hook implementations for the Gatsby module.
*/
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityInterface;
use Drupal\node\NodeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\node\Entity\NodeType;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\RevisionableInterface;
/**
* Implements hook_help().
*/
function gatsby_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the gatsby module.
case 'help.page.gatsby':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Enables Live Preview for Gatsby') . '</p>';
return $output;
default:
}
}
/**
* Implements hook_form_alter().
*/
function gatsby_form_alter(&$form, FormStateInterface $form_state, $form_id): void {
// Add Gatsby Preview options to the content type form.
if ($form_id == 'node_type_add_form' || $form_id == 'node_type_edit_form') {
$entity = $form_state->getFormObject()->getEntity();
// Make sure the node integration is actually enabled before doing anything.
$entity_types = \Drupal::config('gatsby.settings')
->get('supported_entity_types');
if (empty($entity_types) || !in_array('node', $entity_types)) {
return;
}
// Get settings.
$preview_settings = $entity->getThirdPartySetting('gatsby', 'preview');
$form['gatsby'] = [
'#title' => t('Gatsby Preview'),
'#type' => 'details',
'#group' => 'additional_settings',
];
$form['gatsby']['gatsby_preview'] = [
'#type' => 'checkbox',
'#title' => t('Enable Gatsby Preview Button'),
'#default_value' => !empty($preview_settings),
'#description' => t('This will add a Gatsby Preview button to node pages.'),
];
$form['#entity_builders'][] = 'gatsby_preview_node_entity_builder';
}
// Override node edit form.
elseif (preg_match('/node_(\w*)_edit_form/', $form_id, $matches)) {
$moduleHandler = \Drupal::service('module_handler');
if ($moduleHandler->moduleExists('gatsby_endpoints')) {
return;
}
$settings = \Drupal::config('gatsby.settings');
$entity_types = $settings->get('supported_entity_types');
$preview_url = $settings->get('preview_callback_url');
// Make sure the node integration is actually enabled before doing anything.
if (!$preview_url || empty($entity_types) || !in_array('node', $entity_types)) {
return;
}
// Load settings.
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $form_state->getFormObject()->getEntity();
$entity_type = NodeType::load($entity->bundle());
$preview_settings = $entity_type->getThirdPartySetting('gatsby', 'preview');
$server_urls = array_map('trim', explode(',', \Drupal::config('gatsby.settings')->get('server_url')));
$server_url = reset($server_urls);
if (!empty($preview_settings) && !empty($server_url)) {
$moderation_info = \Drupal::service('content_moderation.moderation_information');
$is_moderated = $moderation_info->isModeratedEntity($entity);
// Alert the user that content moderation is disabled for the node type.
if (!$is_moderated) {
\Drupal::messenger()->addWarning('Gatsby Preview requires content moderation enabled for this node type.');
}
/** @var \Drupal\Core\TempStore\PrivateTempStore $tempstore */
$tempstore = \Drupal::service('tempstore.private')->get('gatsby');
$preview_url = $tempstore->get('preview_url');
// Add Gatsby Preview button.
$form['actions']['gatsby_preview'] = [
'#type' => 'submit',
'#weight' => 5,
'#value' => 'Open Gatsby Preview',
'#name' => 'gatsby_preview',
'#attributes' => [
'class' => [
'gatsby-preview',
],
'disabled' => !$is_moderated,
],
'#submit' => [
'::submitForm',
'::save',
'gatsby_module_preview_submit_handler',
],
'#attached' => [
'library' => [
'gatsby/open_preview_window',
],
'drupalSettings' => [
'gatsby' => [
'entity_type' => 'node',
'entity_id' => $entity->id(),
],
],
],
];
if ($preview_url) {
$form['actions']['gatsby_preview']['#attached']['drupalSettings']['gatsby']['preview_url'] = $preview_url;
// Prevent temporary storage key from persisting.
try {
$tempstore->delete('preview_url');
}
catch (\Exception $error) {
}
}
array_unshift($form['#validate'], 'gatsby_node_validate');
}
}
}
/**
* Custom validation callback.
*/
function gatsby_node_validate(array &$form, FormStateInterface $form_state): void {
$trigger = $form_state->getTriggeringElement();
if ($trigger && $trigger['#name'] === 'gatsby_preview') {
$form_state->setValue(['moderation_state', '0', 'value'], 'draft');
}
}
/**
* Custom submit handler to handle Content Sync preview actions.
*
* This is needed because Content Sync needs to wait until the page is fully
* saved before opening the preview window.
*/
function gatsby_module_preview_submit_handler($form, FormStateInterface $form_state): void {
/** @var \Drupal\Core\TempStore\PrivateTempStore $tempstore */
$tempstore = \Drupal::service('tempstore.private')->get('gatsby');
/** @var \Drupal\node\NodeInterface $node */
$node = $form_state->getFormObject()->getEntity();
$server_urls = array_map('trim', explode(',', \Drupal::config('gatsby.settings')->get('server_url')));
$server_url = reset($server_urls);
$path = \Drupal::service('gatsby.path_mapping')->getPath($node);
$preview_url = rtrim($server_url, '/') . $path;
$contentsync_url = \Drupal::config('gatsby.settings')->get('contentsync_url');
$redirect_url = Url::fromRoute('<current>');
$url_params = [];
if ($destination = \Drupal::request()->query->get('destination')) {
$url_params['destination'] = $destination;
}
\Drupal::request()->query->remove('destination');
$redirect_url->setOptions(['query' => $url_params]);
if ($contentsync_url) {
$date = DrupalDateTime::createFromTimestamp($node->getRevisionCreationTime());
$date->setTimezone(new \DateTimeZone('UTC'));
// Generate the correct content sync url.
$preview_url = $contentsync_url . '/gatsby-source-drupal/' . $node->id() . '-' . $date->format(\DateTime::RFC3339) . '-' . $node->language()->getId();
}
// Set $preview_url to temporary storage for form to use after redirect.
try {
$tempstore->set('preview_url', $preview_url);
}
catch (\Exception $error) {
}
$form_state->setRedirectUrl($redirect_url);
}
/**
* Custom handler for Gatsby preview option.
*/
function gatsby_preview_node_entity_builder($entity_type, ConfigEntityInterface $config_entity, &$form, FormStateInterface $form_state): void {
// Save Preview setting.
$config_entity->setThirdPartySetting('gatsby', 'preview', $form_state->getValue('gatsby_preview'));
}
/**
* Implements hook_entity_update().
*/
function gatsby_entity_update(EntityInterface $entity): void {
/** @var \Drupal\gatsby\GatsbyPreview $gatsbyPreview */
$gatsbyPreview = \Drupal::service('gatsby.preview');
$settings = \Drupal::config('gatsby.settings');
$build_published = $settings->get('build_published');
$special_types = ['redirect'];
// Verify this is a supported entity type.
if (!$gatsbyPreview->isSupportedEntity($entity)) {
return;
}
// Return early if we are only acting on nodes.
if ($build_published && !$entity instanceof NodeInterface) {
return;
}
// Extra checks for files, which have their own peculiarities.
if (!_gatsby_will_process_file($entity)) {
return;
}
$published = method_exists($entity, 'isPublished') && $entity->isPublished();
// Log preview update.
$gatsbyPreview->gatsbyPreparePreviewData($entity, 'update');
// Always send an update for special types.
if (in_array($entity->getEntityTypeId(), $special_types)) {
$gatsbyPreview->gatsbyPrepareBuildData($entity, 'update');
}
// We're only concerned with logging builds for published entities.
if ($published) {
if ($entity instanceof RevisionableInterface) {
// If entity is revisionable, only trigger a build if we are saving a
// published version.
if ($entity->isDefaultRevision()) {
$gatsbyPreview->gatsbyPrepareBuildData($entity, 'update');
}
}
// Assume anything else not handled through content moderation that's
// published triggers a build.
else {
$gatsbyPreview->gatsbyPrepareBuildData($entity, 'update');
}
}
else {
if ($entity instanceof RevisionableInterface && method_exists($entity, 'isPublished') && $entity->original->isPublished()) {
// If this is not a draft version, trigger a delete log.
if ($entity->isDefaultRevision()) {
$gatsbyPreview->gatsbyPrepareBuildData($entity, 'delete');
}
}
}
drupal_register_shutdown_function('_gatsby_update');
}
/**
* Implements hook_entity_insert().
*/
function gatsby_entity_insert(EntityInterface $entity): void {
/** @var \Drupal\gatsby\GatsbyPreview $gatsbyPreview */
$gatsbyPreview = \Drupal::service('gatsby.preview');
$settings = \Drupal::config('gatsby.settings');
$build_published = $settings->get('build_published');
$special_types = ['redirect'];
// Verify this is a supported entity type.
if (!$gatsbyPreview->isSupportedEntity($entity)) {
return;
}
// Return early if we are only acting on nodes.
if ($build_published && !$entity instanceof NodeInterface) {
return;
}
// Extra checks for files, which have their own peculiarities.
if (!_gatsby_will_process_file($entity)) {
return;
}
// Always send content to preview server.
$gatsbyPreview->gatsbyPreparePreviewData($entity, 'insert');
$published = method_exists($entity, 'isPublished') && $entity->isPublished();
// Check to see if we are only triggering builds for published content.
if ($published || in_array($entity->getEntityTypeId(), $special_types)) {
$gatsbyPreview->gatsbyPrepareBuildData($entity, 'insert');
}
drupal_register_shutdown_function('_gatsby_update');
}
/**
* Implements hook_entity_delete().
*/
function gatsby_entity_delete(EntityInterface $entity): void {
/** @var \Drupal\gatsby\GatsbyPreview $gatsbyPreview */
$gatsbyPreview = \Drupal::service('gatsby.preview');
$settings = \Drupal::config('gatsby.settings');
$build_published = $settings->get('build_published');
// Verify this is a supported entity type.
if (!$gatsbyPreview->isSupportedEntity($entity)) {
return;
}
// Return early if we are only acting on nodes.
if ($build_published && !$entity instanceof NodeInterface) {
return;
}
// Extra checks for files, which have their own peculiarities.
if (!_gatsby_will_process_file($entity)) {
return;
}
// Notify Gatsby of the change.
$gatsbyPreview->gatsbyPreparePreviewData($entity, 'delete');
$gatsbyPreview->gatsbyPrepareBuildData($entity, 'delete');
drupal_register_shutdown_function('_gatsby_update');
}
/**
* Implements hook_cron().
*/
function gatsby_cron(): void {
// Do not delete entities if delete setting is not enabled.
if (!\Drupal::config('gatsby.settings')->get('delete_log_entities')) {
return;
}
// Make sure a valid expiration setting is set.
$expiration = \Drupal::config('gatsby.settings')->get('log_expiration');
if (!$expiration) {
return;
}
\Drupal::service('gatsby.logger')->deleteExpiredLoggedEntities(time() - $expiration);
$last_logtime = \Drupal::service('gatsby.logger')->getOldestLoggedEntityTimestamp();
// Set last logtime as current time if there are no log entries.
if (!$last_logtime) {
$last_logtime = time();
}
// Store the log time of the last log entry in order to validate future syncs.
\Drupal::state()->set('gatsby.last_logtime', $last_logtime);
}
/**
* Determine whether the given file entity should be processed.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Whether the entity should be processed.
*
* @return bool
* Whether the file should be processed.
*/
function _gatsby_will_process_file(EntityInterface $entity): bool {
// Extra logic for files, which have their own peculiarities.
if ($entity->getEntityTypeId() == 'file') {
// Ignore files before they are marked "permanent".
if ($entity->isTemporary()) {
return FALSE;
}
// Private files are ignored by default, but may be published if an option
// is enabled.
if (substr($entity->getFileUri(), 0, 7) == 'private') {
$gatsby_settings = \Drupal::config('gatsby.settings');
if (!$gatsby_settings->get('publish_private_files')) {
return FALSE;
}
}
}
return TRUE;
}
/**
* Implements hook_entity_extra_field_info().
*/
function gatsby_entity_extra_field_info(): array {
$extra = [];
$entity_types = \Drupal::config('gatsby.settings')->get('supported_entity_types');
if (!empty($entity_types)) {
foreach ($entity_types as $entity_type) {
$bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type);
$entity_type_definition = \Drupal::entityTypeManager()->getDefinition($entity_type);
if (count($bundle_info) > 1 || $entity_type_definition->getKey('bundle')) {
foreach ($bundle_info as $bundle => $info) {
$extra[$entity_type][$bundle]['display']['gatsby_iframe_preview'] = [
'label' => t('Gatsby iframe preview'),
'description' => t('A preview of this content using the GatsbyJS system, displayed using an iframe.'),
'weight' => 100,
'visible' => FALSE,
];
}
}
else {
$extra[$entity_type][$entity_type_definition->getKey('bundle')]['display']['gatsby_iframe_preview'] = [
'label' => t('Gatsby iframe preview'),
'description' => t('A preview of this content using the GatsbyJS system, displayed using an iframe.'),
'weight' => 100,
'visible' => FALSE,
];
}
}
}
return $extra;
}
/**
* Implements hook_entity_view().
*/
function gatsby_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode): void {
if ($display->getComponent('gatsby_iframe_preview')) {
// Don't run hook_entity_view if gatsby_endpoints is enabled.
$server_urls = array_map('trim', explode(',', \Drupal::config('gatsby.settings')->get('server_url')));
$server_url = reset($server_urls);
// Skip the rest of the logic if no server URL is defined.
if (empty($server_url)) {
return;
}
// Render the preview for this entity.
$gatsby_url = preg_replace('/\/$/', '', $server_url) . \Drupal::service('gatsby.path_mapping')->getPath($entity);
$build['gatsby_iframe_preview'] = [
'#type' => 'inline_template',
'#template' => '<div class="gatsby-iframe-container"><iframe class="gatsby-iframe" src="{{ url }}"></iframe></div>',
'#context' => [
'url' => $gatsby_url,
],
'#attached' => [
'library' => [
'gatsby/iframe_preview',
],
],
];
}
}
/**
* Triggers the update to the Gatsby Preview and Incremental Builds servers.
*
* @see gatsby_entity_insert()
* @see gatsby_entity_update()
* @see gatsby_entity_delete()
*/
function _gatsby_update(): void {
$gatsbyPreview = \Drupal::service('gatsby.preview');
$gatsbyPreview->gatsbyUpdate();
}
