static_generator-8.x-1.x-dev/src/StaticGenerator.php
src/StaticGenerator.php
<?php
namespace Drupal\static_generator;
use DOMDocument;
use DOMXPath;
use Drupal\Component\Utility\Html;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Menu\MenuActiveTrailInterface;
use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Queue\QueueInterface;
use Drupal\Core\Queue\QueueWorkerInterface;
use Drupal\Core\Queue\SuspendQueueException;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Theme\ThemeInitializationInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\static_generator\Event\ModifyMarkupEvent;
use Drupal\static_generator\Event\ModifyEsiMarkupEvent;
use Drupal\static_generator\Event\StaticGeneratorEvents;
use Drupal\node\NodeInterface;
use Drupal\path_alias\AliasManagerInterface;
use Drupal\Core\Menu\MenuTreeParameters;
/**
* Static Generator Service.
*
* Provides static generation services.
*/
class StaticGenerator {
/**
* The renderer.
*
* @var RendererInterface
*/
private $renderer;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The class resolver.
*
* @var ClassResolverInterface
*/
private $classResolver;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $currentRequest;
/**
* The HTTP kernel.
*
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected $httpKernel;
/**
* The session configuration.
*
* @var \Drupal\Core\Session\SessionConfigurationInterface
*/
protected $sessionConfiguration;
/**
* The webform theme manager.
*
* @var \Drupal\webform\WebformThemeManagerInterface
*/
protected $themeManager;
/**
* The theme initialization.
*
* @var \Drupal\Core\Theme\ThemeInitializationInterface
*/
protected $themeInitialization;
/**
* The configuration object factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* File system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The path matcher.
*
* @var \Drupal\Core\Path\PathMatcherInterface
*/
protected $pathMatcher;
/**
* The menu active trail cache collector.
*
* @var \Drupal\Core\Menu\MenuActiveTrailInterface
*/
protected $menuActiveTrail;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The module_handler service.
*
* @var \Drupal\Core\Extension\ModuleHandler
* The module_handler service.
*/
protected $moduleHandler;
/**
* The path_alias manager.
*
* @var \Drupal\path_alias\AliasManagerInterface
*/
protected $pathAliasManager;
/**
* Constructs a new StaticGenerator object.ClassResolverInterface
* $class_resolver,
*
* @param RendererInterface $renderer
* The renderer.
* @param RouteMatchInterface $route_match
* The route matcher.
* @param ClassResolverInterface $class_resolver
* The class resolver.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
* The HTTP Kernel service.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization
* The theme initialization.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration object factory.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* File system.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
* Path matcher.
* @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail
* The menu active trail cache collector.
* @param \Drupal\Core\Extension\ModuleHandler $moduleHandler
* The module Handler service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\path_alias\AliasManagerInterface $pathManager
* The path_alias manager.
*/
public function __construct(RendererInterface $renderer, RouteMatchInterface $route_match, ClassResolverInterface $class_resolver, RequestStack $request_stack, HttpKernelInterface $http_kernel, ThemeManagerInterface $theme_manager, ThemeInitializationInterface $theme_initialization, ConfigFactoryInterface $config_factory, FileSystemInterface $file_system, EntityTypeManagerInterface $entity_type_manager, PathMatcherInterface $path_matcher, MenuActiveTrailInterface $menu_active_trail, ModuleHandler $moduleHandler, EventDispatcherInterface $event_dispatcher, AliasManagerInterface $pathAliasManager) {
$this->renderer = $renderer;
$this->routeMatch = $route_match;
$this->classResolver = $class_resolver;
$this->requestStack = $request_stack;
$this->currentRequest = $request_stack->getCurrentRequest();
$this->httpKernel = $http_kernel;
$this->themeManager = $theme_manager;
$this->themeInitialization = $theme_initialization;
$this->configFactory = $config_factory;
$this->fileSystem = $file_system;
$this->entityTypeManager = $entity_type_manager;
$this->pathMatcher = $path_matcher;
$this->menuActiveTrail = $menu_active_trail;
$this->moduleHandler = $moduleHandler;
$this->eventDispatcher = $event_dispatcher;
$this->pathAliasManager = $pathAliasManager;
}
/**
* Generate all pages and files.
*
* Limit the number of nodes generated for each bundle.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateAll() {
\Drupal::logger('static_generator')->notice('Begin generateAll()');
$elapsed_time = $this->deleteAll();
$elapsed_time += $this->generatePages();
$elapsed_time += $this->generateFiles();
\Drupal::logger('static_generator')
->notice('End generateAll(), elapsed time: ' . $elapsed_time . ' seconds.');
return $elapsed_time;
}
/**
* Generate pages.
*
* @param bool $delete_pages
*
* @return int
* Execution time in seconds.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Theme\MissingThemeDependencyException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generatePages($delete_pages = FALSE) {
$elapsed_time = 0;
if ($delete_pages) {
$elapsed_time = $this->deletePages();
$elapsed_time += $this->deleteEsi();
}
$elapsed_time += $this->generateNodes();
$elapsed_time += $this->generatePaths();
//$elapsed_time += $this->generateRedirects();
\Drupal::logger('static_generator')
->notice('Generation of all pages complete, elapsed time: ' . $elapsed_time . ' seconds.');
return $elapsed_time;
}
/**
* Generate media entities.
*
* @param bool $esi_only
* @param string $bundle
* @param int $start
* @param int $length
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateMedia($bundle = '', $esi_only = FALSE, $start = 0, $length = 100000) {
$elapsed_time_total = 0;
// Get bundles to generate from config if not specified in $type.
if (empty($bundle)) {
$bundles_string = $this->configFactory->get('static_generator.settings')
->get('gen_media');
$bundles = explode(',', $bundles_string);
} else {
$bundles = [$bundle];
}
// Generate as Anonymous user.
\Drupal::service('account_switcher')
->switchTo(new AnonymousUserSession());
// Switch to default theme
$active_theme = $this->themeManager->getActiveTheme();
$default_theme_name = $this->configFactory->get('system.theme')
->get('default');
$default_theme = $this->themeInitialization->getActiveThemeByName($default_theme_name);
$this->themeManager->setActiveTheme($default_theme);
// Generate each bundle.
$blocks_processed = [];
$sg_esi_processed = [];
$sg_esi_existing = $this->existingSgEsiFiles();
foreach ($bundles as $bundle) {
$start_time = time();
$query = \Drupal::entityQuery('media');
$query->accessCheck(TRUE);
$query->condition('status', 1);
$query->condition('bundle', $bundle);
$count = $query->count()->execute();
$count_gen = 0;
for ($i = $start; $i <= $count; $i = $i + $length) {
$query = \Drupal::entityQuery('media');
$query->accessCheck(TRUE);
$query->condition('status', 1);
$query->condition('bundle', $bundle);
$query->range($i, $length);
$query->sort('mid', 'DESC');
$entity_ids = $query->execute();
// Generate pages for bundle.
foreach ($entity_ids as $entity_id) {
$path_alias = \Drupal::service('path_alias.manager')
->getAliasByPath('/media/' . $entity_id);
$this->generatePage($path_alias, '', $esi_only, FALSE, FALSE, FALSE, $blocks_processed, $sg_esi_processed, $sg_esi_existing);
$count_gen++;
}
// Exit if single run for specified content type.
if (!empty($bundle)) {
break;
}
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
$elapsed_time_total += $elapsed_time;
if ($count_gen > 0) {
$seconds_per_page = round($elapsed_time / $count_gen, 2);
} else {
$seconds_per_page = 'n/a';
}
\Drupal::logger('static_generator_time')
->notice('Gen bundle ' . $bundle . ' ' . $count_gen .
' pages in ' . $elapsed_time . ' seconds, ' . $seconds_per_page . ' seconds per page.');
}
// Switch back from anonymous user.
\Drupal::service('account_switcher')->switchBack();
// Switch back to active theme.
$this->themeManager->setActiveTheme($active_theme);
return $elapsed_time_total;
}
/**
* Generate term entities.
*
* @param bool $esi_only
* @param string $bundle
* @param int $start
* @param int $length
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateVocabulary($bundle = '', $esi_only = FALSE, $start = 0, $length = 100000) {
$elapsed_time_total = 0;
// Get bundles to generate from config if not specified in $type.
if (empty($bundle)) {
// @todo Implement settings page UI for this.
$bundles_string = $this->configFactory->get('static_generator.settings')
->get('gen_taxonomy');
$bundles = explode(',', $bundles_string);
} else {
$bundles = [$bundle];
}
// Generate as Anonymous user.
\Drupal::service('account_switcher')
->switchTo(new AnonymousUserSession());
// Switch to default theme
$active_theme = $this->themeManager->getActiveTheme();
$default_theme_name = $this->configFactory->get('system.theme')
->get('default');
$default_theme = $this->themeInitialization->getActiveThemeByName($default_theme_name);
$this->themeManager->setActiveTheme($default_theme);
// Get vocabulary id.
$vocabulary = $this->entityTypeManager->getStorage('taxonomy_vocabulary')
->load($bundle);
$vid = $vocabulary->id();
// Generate each bundle.
$blocks_processed = [];
$sg_esi_processed = [];
$sg_esi_existing = $this->existingSgEsiFiles();
foreach ($bundles as $bundle) {
$start_time = time();
$query = \Drupal::entityQuery('taxonomy_term');
$query->accessCheck(TRUE);
$query->condition('status', 1);
$query->condition('vid', $vid);
$count = $query->count()->execute();
$count_gen = 0;
for ($i = $start; $i <= $count; $i = $i + $length) {
// Reset memory
// drupal_static_reset();
// $manager = \Drupal::entityManager();
// foreach ($manager->getDefinitions() as $id => $definition) {
// $manager->getStorage($id)->resetCache();
// }
// Run garbage collector to further reduce memory.
// gc_collect_cycles();
// @TODO Can we reset container?
$query = \Drupal::entityQuery('taxonomy_term');
$query->accessCheck(TRUE);
$query->condition('status', 1);
$query->condition('vid', $vid);
$query->sort('weight');
$query->range($i, $length);
$entity_ids = $query->execute();
// Generate pages for bundle.
foreach ($entity_ids as $entity_id) {
// Load only published nodes tagged by each entity_id (term_id).
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([
'status' => 1,
'field_topics' => $entity_id,
]);
// Only generate page if there are nodes tagged by topic.
if (!empty($nodes)) {
$path_alias = \Drupal::service('path_alias.manager')
->getAliasByPath('/taxonomy/term/' . $entity_id);
$this->generatePage($path_alias, '', $esi_only, false, false, false, $blocks_processed, $sg_esi_processed, $sg_esi_existing);
$count_gen++;
}
}
// Exit if single run for specified content type.
if (!empty($bundle)) {
break;
}
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
$elapsed_time_total += $elapsed_time;
if ($count_gen > 0) {
$seconds_per_page = round($elapsed_time / $count_gen, 2);
} else {
$seconds_per_page = 'n/a';
}
\Drupal::logger('static_generator_time')
->notice('Gen bundle ' . $bundle . ' ' . $count_gen .
' pages in ' . $elapsed_time . ' seconds, ' . $seconds_per_page . ' seconds per page.');
}
// Switch back from anonymous user.
\Drupal::service('account_switcher')->switchBack();
// Switch back to active theme.
$this->themeManager->setActiveTheme($active_theme);
return $elapsed_time_total;
}
/**
* Generate nodes.
*
* @param string $bundle
* @param bool $esi_only
* @param int $start
* @param int $length
*
* @return int
* Execution time in seconds.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Theme\MissingThemeDependencyException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateNodes($bundle = '', $esi_only = FALSE, $start = 0, $length = 100000) {
$elapsed_time_total = 0;
// Get bundles to generate from config if not specified in $bundle.
if (empty($bundle)) {
$bundles_string = $this->configFactory->get('static_generator.settings')
->get('gen_node');
$bundles = explode(',', $bundles_string);
} else {
$bundles = [$bundle];
}
// Generate as Anonymous user.
\Drupal::service('account_switcher')
->switchTo(new AnonymousUserSession());
// Switch to default theme
$active_theme = $this->themeManager->getActiveTheme();
$default_theme_name = $this->configFactory->get('system.theme')
->get('default');
$default_theme = $this->themeInitialization->getActiveThemeByName($default_theme_name);
$this->themeManager->setActiveTheme($default_theme);
// Generate each bundle.
$blocks_processed = [];
$sg_esi_processed = [];
$sg_esi_existing = $this->existingSgEsiFiles();
foreach ($bundles as $bundle) {
$start_time = time();
$query = \Drupal::entityQuery('node');
$query->accessCheck(TRUE);
$query->condition('status', 1);
$query->condition('type', $bundle);
$count = $query->count()->execute();
$count_gen = 0;
$query = \Drupal::entityQuery('node');
$query->accessCheck(TRUE);
$query->condition('status', 1);
$query->condition('type', $bundle);
$query->range($start, $length);
$query->sort('nid', 'DESC');
$entity_ids = $query->execute();
// Generate pages for bundle.
foreach ($entity_ids as $entity_id) {
$path_alias = \Drupal::service('path_alias.manager')
->getAliasByPath('/node/' . $entity_id);
$error_time = $this->generatePage($path_alias, '', $esi_only, FALSE, FALSE, FALSE, $blocks_processed, $sg_esi_processed, $sg_esi_existing);
if (!is_null($error_time)) {
$error_times[] = $error_time;
if ($this->errorThresholdExceeded($error_times)) {
\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.1.0', fn() => \Drupal\Core\Utility\Error::logException(\Drupal::logger('static_generator_flood'), new Exception('Static Generator - error log flooding.')), fn() => watchdog_exception('static_generator_flood', new Exception('Static Generator - error log flooding.')));
break;
}
}
$count_gen++;
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
$elapsed_time_total += $elapsed_time;
if ($count_gen > 0) {
$seconds_per_page = round($elapsed_time / $count_gen, 2);
} else {
$seconds_per_page = 'n/a';
}
\Drupal::logger('static_generator_time')
->notice('Gen bundle ' . $bundle . ' ' . $count_gen .
' pages in ' . $elapsed_time . ' seconds, ' . $seconds_per_page . ' seconds per page.');
}
// Switch back from anonymous user.
\Drupal::service('account_switcher')->switchBack();
// Switch back to active theme.
$this->themeManager->setActiveTheme($active_theme);
return $elapsed_time_total;
}
/**
* Examin array of errors to determine if log is being flooded.
*
* @param $errors
*
* @return bool
*/
public function errorThresholdExceeded($errors) {
$threshold_time = 30; // seconds
$threshold_errors = 10; // number of errors
$error_count = 0;
for ($i = count($errors) - 1; $i >= 0; $i--) {
if (time() - $errors[$i] < $threshold_time) {
$error_count++;
if ($error_count > $threshold_errors) {
return TRUE;
}
} else {
return FALSE;
}
}
}
/**
* Generate paths specified in settings.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generatePaths() {
$start_time = time();
$paths_string = $this->configFactory->get('static_generator.settings')
->get('paths_generate');
if (!empty($paths_string)) {
$paths = explode(',', $paths_string);
foreach ($paths as $path) {
$this->generatePage(trim($path));
}
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
\Drupal::logger('static_generator')
->notice('generatePaths() elapsed time: ' . $elapsed_time . ' seconds.');
return $elapsed_time;
}
/**
* Generate markup for a single page.
*
* @param string $path
* The page's path.
*
* @param string $path_generate
* @param bool $esi_only
* Optionally omit generating the page (just generate the blocks).
*
* @param bool $log
* Should a log message be written to dblog.
*
* @param bool $account_switcher
*
* @param bool $theme_switcher
*
* @param array $blocks_processed
*
* @param array $sg_esi_processed
*
* @param array $sg_esi_existing
*
* @param bool $check_published
*
* @return string|void
* Returns null if no errors. If an error occurs, returns the time() of the error.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Theme\MissingThemeDependencyException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generatePage($path, $path_generate = '', $esi_only = FALSE, $log = FALSE, $account_switcher = TRUE, $theme_switcher = TRUE, &$blocks_processed = [], &$sg_esi_processed = [], $sg_esi_existing = [], $check_published = FALSE) {
// Get path alias for path (incase path provided is not an alias itself).
$path_alias = $this->pathAliasManager->getAliasByPath($path);
// Return if path is excluded.
if ($this->excludePath($path)) {
return null;
}
// Ignore if path is xml.
if ($this->endsWith($path_alias, '.xml')) {
return null;
}
// Write JSON files as-is.
// TODO: This assumes the path always ends with .json (no query parameters).
// If query string exists, use generateJsonPath() method.
if ($this->endsWith($path_alias, '.json')) {
// Get the markup.
$markup = $this->markupForPage($path_alias, $account_switcher, $theme_switcher, TRUE);
$web_directory = $this->directoryFromPath($path_alias);
$file_name = $this->filenameFromPath($path_alias);
// Write the page.
$directory = $this->generatorDirectory() . $web_directory;
if (!$esi_only && \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) {
\Drupal::service('file_system')->saveData($markup, $directory . '/' . $file_name, FileSystemInterface::EXISTS_REPLACE);
if ($log) {
\Drupal::logger('static_generator')
->notice('Generate Page: ' . $directory . '/' . $file_name);
}
}
return;
}
// Get node object if path is a node.
$path_canonical = $this->pathAliasManager->getPathByAlias($path);
$nid = substr($path_canonical, strpos($path_canonical, '/', 1) + 1);
$node = $this->entityTypeManager->getStorage('node')->load($nid);
// Check publish state if required.
if ($check_published) {
// Only do this if path is a node.
if ($node instanceof NodeInterface) {
if (!$node->isPublished()) {
return null;
}
}
}
// Get the markup.
$markup = $this->markupForPage($path_alias, $account_switcher, $theme_switcher, FALSE);
// Return if error.
if ($markup == 'error') {
return time();
}
if ($markup == '404') {
return null;
}
// Process ESIs.
$markup = $this->injectESIs($markup, $path, $blocks_processed, $sg_esi_processed, $sg_esi_existing);
// Allow modules to modify the markup (for nodes only).
if ($node instanceof NodeInterface) {
$event = new ModifyMarkupEvent($markup, $node, $path);
$this->eventDispatcher->dispatch($event, StaticGeneratorEvents::MODIFY_MARKUP);
$markup = $event->getMarkup();
}
// Get file name.
if (empty($path_generate)) {
$web_directory = $this->directoryFromPath($path_alias);
$file_name = $this->filenameFromPath($path_alias);
} else {
$web_directory = $this->directoryFromPath($path_generate);
$file_name = $this->filenameFromPath($path_generate);
}
// Return if on index.html and gen index is false.
if ($file_name == "index.html" && !$this->generateIndex()) {
return null;
}
// Generate redirect page for path.
$this->generateRedirectPageForPath($path);
// Write the page.
$directory = $this->generatorDirectory() . $web_directory;
if (!$esi_only && \Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) {
\Drupal::service('file_system')->saveData($markup, $directory . '/' . $file_name, FileSystemInterface::EXISTS_REPLACE);
if ($log) {
\Drupal::logger('static_generator')
->notice('Generate Page: ' . $directory . '/' . $file_name);
}
}
}
/**
* Generate a JSON path.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateJsonPath($path) {
// Define defaults.
$account_switcher = TRUE;
$theme_switcher = TRUE;
$raw_markup = TRUE;
// Get the markup.
$markup = $this->markupForPage($path, $account_switcher, $theme_switcher, $raw_markup);
$web_directory = $this->directoryFromPath($path);
$file_name = substr(strrchr($path, '/'), 1) . '.json';
// Get the directory structure.
$directory = $this->generatorDirectory() . $web_directory;
// Write the file.
if (\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) {
\Drupal::service('file_system')
->saveData($markup, $directory . '/' . $file_name, FileSystemInterface::EXISTS_REPLACE);
\Drupal::logger('static_generator')
->notice('Generate Page: ' . $directory . '/' . $file_name);
}
}
/**
* Generates static pages for redirects.
*/
public function generateRedirectPageForPath($path) {
if (!empty($path) && \Drupal::moduleHandler()->moduleExists('redirect')) {
$path_canonical = \Drupal::service('path_alias.manager')
->getPathByAlias($path);
$nid = substr($path_canonical, strpos($path_canonical, '/', 1) + 1);
$redirect_storage = $this->entityTypeManager->getStorage('redirect');
$redirect_ids = $redirect_storage->getQuery()
->condition('redirect_redirect__uri', '%' . $nid, 'LIKE')
->accessCheck(TRUE)
->execute();
$redirects = $redirect_storage->loadMultiple($redirect_ids);
foreach ($redirects as $redirect) {
$source_url = $redirect->getSourceUrl();
$target_array = $redirect->getRedirect();
$target_uri = $target_array['uri'];
$target_url = \Drupal::service('path_alias.manager')->getAliasByPath(substr($target_uri, 9));
$this->generateRedirect($source_url, $target_url);
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateRedirects() source: ' . $source_url . ' target: ' . $target_url);
}
}
}
}
/**
* @param $menu_link
* @param $path
*
* @throws \Drupal\Core\Theme\MissingThemeDependencyException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generatePagesMenuChildrenSiblings($menu_link, $path) {
if ($path) {
$this->queuePage($path);
}
// Get the menu link's children and generate their pages.
$menu_parameters = new MenuTreeParameters();
$menu_parameters->setMaxDepth(1);
$menu_parameters->setRoot($menu_link->getPluginId());
$menu_parameters->excludeRoot();
$menu_tree_service = \Drupal::service('menu.link_tree');
$tree = $menu_tree_service->load('main', $menu_parameters);
$children = $menu_tree_service->build($tree);
if (isset($child_items)) {
if (array_key_exists('#items', $child_items)) {
$child_items = $children['#items'];
foreach ($child_items as $child_item) {
$url = $child_item['url'];
$path = $url->toString();
if (substr($path, 0, 1) == '/') {
\Drupal::service('static_generator')->queuePage($path);
}
}
}
}
// Get this link's parent and generate its pages.
$menu_parameters_siblings = new MenuTreeParameters();
$menu_parameters_siblings->setMaxDepth(1);
$parent_id = $menu_link->getParentId();
$menu_parameters_siblings->setRoot($parent_id);
$menu_parameters_siblings->excludeRoot();
$menu_tree_service = \Drupal::service('menu.link_tree');
$tree_siblings = $menu_tree_service->load('main', $menu_parameters_siblings);
$siblings = $menu_tree_service->build($tree_siblings);
$child_items = $siblings['#items'];
foreach ($child_items as $child_item) {
$url = $child_item['url'];
$path = $url->toString();
if (substr($path, 0, 1) == '/') {
\Drupal::service('static_generator')->queuePage($path);
}
}
}
/**
* Should a path be excluded by "Paths to not generate setting.
*
* @param $path
*
* @return boolean
* Return true if path is excluded, false otherwise.
*`
*/
public function excludePath($path) {
$path_alias = \Drupal::service('path_alias.manager')
->getAliasByPath($path);
// Get paths to exclude (not generate)
$paths_do_not_generate_string = $this->configFactory->get('static_generator.settings')
->get('paths_do_not_generate');
if (empty($paths_do_not_generate_string)) {
return FALSE;
}
$paths_do_not_generate = explode(',', $paths_do_not_generate_string);
foreach ($paths_do_not_generate as $path_dng) {
if ($this->pathMatcher->matchPath($path_alias, $path_dng)) {
return TRUE;
}
}
return FALSE;
}
/**
* Generate block ESi fragment files.
*
* @param bool $frequent_only
* Generate frequent blocks only. Frequent blocks are defined in settings.
*
* @return int
* Execution time in seconds.
*`
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateBlocks($frequent_only = FALSE) {
if ($frequent_only) {
// Generate frequent blocks only.
$blocks_frequent = $this->configFactory->get('static_generator.settings')
->get('blocks_frequent');
if (!empty($blocks_frequent)) {
$blocks_frequent = explode(',', $blocks_frequent);
foreach ($blocks_frequent as $esi_id) {
//$this->generateBlockById($block_id);
$this->generateEsiById($esi_id);
}
}
} else {
// Generate all blocks.
$this->deleteEsi();
return $this->generateNodes('', TRUE);
}
}
/**
* Get all Block ID's to ESI, or optionally only those that match a patten.
*
* @param string $pattern
* The block id pattern.
*
* @return array|int
*
* @throws \Exception
*/
public function blockIds($pattern = '') {
$controller = $this->entityTypeManager->getStorage('block');
$ids = [];
foreach ($controller->loadMultiple() as $return_block) {
$ids[] = $return_block->id();
}
if (!empty($pattern)) {
$ids_match_pattern = [];
foreach ($ids as $id) {
if (substr($id, 0, strlen($pattern)) === $pattern) {
$ids_match_pattern[] = $id;
}
}
return $ids_match_pattern;
} else {
return 'done';
}
}
/**
*
* Determines if block should be ESI.
*
* @param $block_id
*
* @return bool
*/
public function esiBlock($block_id) {
// Return if block on "no esi" in settings.
$blocks_no_esi = $this->configFactory->get('static_generator.settings')
->get('blocks_no_esi');
if (empty($blocks_no_esi)) {
return TRUE;
}
$blocks_no_esi = explode(',', $blocks_no_esi);
if (in_array($block_id, $blocks_no_esi)) {
return FALSE;
}
// Return if block's pattern on "no esi" in settings.
foreach ($blocks_no_esi as $block_no_esi) {
if (substr($block_no_esi, strlen($block_no_esi) - 1, 1) === '*') {
$block_no_esi = substr($block_no_esi, 0, strlen($block_no_esi) - 1);
if (strpos($block_id, $block_no_esi) === 0) {
return FALSE;
}
}
}
// Did not match id or pattern
return TRUE;
}
/**
* Generate a block fragment file. This approach generates a block directly,
* rather than taking the rendered block markup from the rendered pages, which
* is the approach used when generating all pages.
*
* @param string $block_id
* The block id.
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateBlockById($block_id) {
if (empty($block_id)) {
return;
}
// Return if block id listed in "block no esi" setting.
if (!$this->esiBlock($block_id)) {
return;
}
if (substr($block_id, 0, 8) === 'sg-esi--') {
$generator_directory = $this->generatorDirectory() . '/esi/sg-esi';
} else {
$generator_directory = $this->generatorDirectory() . '/esi/block';
}
$files = \Drupal::service('file_system')->scanDirectory($generator_directory, '/*/', ['recurse' => FALSE]);
foreach ($files as $file) {
$filename = $file->filename;
$block_id_file = substr($filename, 0, strpos($block_id, '__'));
if ($block_id === $block_id_file) {
$path_str = substr($block_id, strpos($block_id, '__'));
$path = '/' . str_replace('-', '/', $path_str);
$this->generatePage($path, '', TRUE);
}
}
}
/**
* Create array of existing sg-esi files.
*
*/
public function existingSgEsiFiles() {
$generator_directory = $this->generatorDirectory();
$directory = $generator_directory . 'esi/sg-esi';
\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
$files = \Drupal::service('file_system')->scanDirectory($directory, '/.*/', ['recurse' => FALSE]);
//$files = scandir($directory);
$existingSgEsiFiles = [];
foreach ($files as $file) {
$filename = $file->filename;
if (strpos($filename, '__') !== FALSE) {
$esi_id = substr($filename, 0, strpos($filename, '__'));
$existingSgEsiFiles[$esi_id] = $filename;
}
}
return $existingSgEsiFiles;
}
/**
* Generate a esi fragment file.
*
* @param string $esi_id
* The esi id.
*
* @throws \Exception
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function generateEsiById($esi_id) {
$generator_directory = $this->generatorDirectory() . '/esi/sg-esi';
$files = \Drupal::service('file_system')->scanDirectory($generator_directory, '/.*/', ['recurse' => FALSE]);
foreach ($files as $file) {
$filename = $file->filename;
$esi_id_file = substr($filename, 0, strpos($filename, '__'));
$generate_page = FALSE;
if ($this->endsWith($esi_id, "*")) {
$esi_id_real = substr($esi_id, 0, strlen($esi_id) - 1);
// Wildcard esi_id ends in *
if (substr($esi_id_file, 0, strlen($esi_id_real)) === $esi_id_real) {
$generate_page = TRUE;
}
} else {
if ($esi_id === $esi_id_file) {
$generate_page = TRUE;
}
}
if ($generate_page) {
$path_str = substr($filename, strpos($filename, '__') + 2);
$path = '/' . str_replace('--', '/', $path_str);
$this->generatePage($path, '', TRUE);
}
}
}
/**
* @param $haystack
* @param $needle
*
* @return bool
*/
public function startsWith($haystack, $needle) {
$length = strlen($needle);
return (substr($haystack, 0, $length) === $needle);
}
/**
* @param $haystack
* @param $needle
*
* @return bool
*/
public function endsWith($haystack, $needle) {
$length = strlen($needle);
if ($length == 0) {
return TRUE;
}
return (substr($haystack, -$length) === $needle);
}
/**
* Generate all files.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function generateFiles() {
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')->notice('Begin generateFiles()');
}
$elapsed_time = $this->generateCodeFiles();
$elapsed_time += $this->generatePublicFiles();
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('End generateFiles(), elapsed time: ' . $elapsed_time . ' seconds.');
}
return $elapsed_time;
}
/**
* Generate public files.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function generatePublicFiles() {
$start_time = time();
// Unpublished files to exclude.
$exclude_media_ids = $this->excludeMediaIdsUnpublished();
$public_files_directory = $this->fileSystem->realpath('public://');
// Exclude list.
$exclude_media_entities = $this->loadMediaEntities($exclude_media_ids);
$this->createExcludeFile($exclude_media_entities, $public_files_directory);
// Get the Draft media.
$exclude_media_rids = $this->excludeMediaIdsDraft();
$exclude_media_entities = $this->loadMediaRevisionEntities($exclude_media_rids);
$this->createExcludeFile($exclude_media_entities, $public_files_directory, 'rsync_public_draft_exclude.tmp', $draft = TRUE);
// Draft List.
// Create files directory if it does not exist.
$generator_directory = $this->generatorDirectory(TRUE);
exec('mkdir -p ' . $generator_directory . '/sites/default/files');
// rSync public.
$rsync_public = $this->configFactory->get('static_generator.settings')
->get('rsync_public');
$rsync_public_command = $rsync_public . ' --filter="merge ' . $public_files_directory . '/rsync_public_draft_exclude.tmp" ' . ' --exclude-from "' . $public_files_directory . '/rsync_public_exclude.tmp" ' . $public_files_directory . '/ ' . $generator_directory . '/sites/default/files';
exec($rsync_public_command);
// rSync CSS.
$css_directory = $this->configFactory->get('static_generator.settings')
->get('css_directory');
$rsync_css = 'rsync -azr ' . $public_files_directory . '/css/ ' . $css_directory;
exec($rsync_css);
// rSync JS.
$js_directory = $this->configFactory->get('static_generator.settings')
->get('js_directory');
$rsync_js = 'rsync -azr ' . $public_files_directory . '/js/ ' . $js_directory;
exec($rsync_js);
// Create symlinks to static files directory from css and js directories.
//symlink($css_directory, $generator_directory . '/sites/default/files/css');
//symlink($js_directory, $generator_directory . '/sites/default/files/js');
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('Generate Public Files elapsed time: ' . $elapsed_time .
' seconds. (' . $rsync_public . ')');
}
return $elapsed_time;
}
/**
* Get the exclude files.
*
* @param array $exclude_media_entities
* Media entities in array.
* @param string $public_files_directory
* Public directory path.
* @param string $exclude_file_tmp
* Exclude or draft file name.
* @param bool $draft
* Is draft or not.
*/
protected function createExcludeFile(array $exclude_media_entities, $public_files_directory, $exclude_file_tmp = 'rsync_public_exclude.tmp', $draft = FALSE) {
if (empty($exclude_media_entities)) {
$exclude_media_entities = [];
}
$exclude_files = '';
$field = '';
foreach ($exclude_media_entities as $media) {
// Get the file id.
$fid = 0;
if ($media->hasField('field_media_image')) {
$field = 'field_media_image';
$fid = $media->get('field_media_image')->getValue()[0]['target_id'];
}
elseif ($media->hasField('field_media_file')) {
$value = $media->get('field_media_file')->getValue();
$field = 'field_media_file';
if (!is_null($value) && is_array($value) && count($value) > 0 && array_key_exists('target_id', $value[0])) {
$fid = $media->get('field_media_file')->getValue()[0]['target_id'];
}
}
elseif ($media->hasField('field_media_audio_file')) {
$field = 'field_media_audio_file';
$value = $media->get('field_media_audio_file')->getValue();
if (!empty($value) && is_array($value)) {
$fid = $media->get('field_media_audio_file')->getValue()[0]['target_id'];
}
}
if ($fid > 0) {
if ($draft) {
$uri = \Drupal::database()
->query('SELECT uri FROM file_managed WHERE fid=:fid', [':fid' => $fid])
->fetchField();
if (!empty($uri)) {
$exclude_file = substr($uri, 9);
$exclude_files .= '- ' . $exclude_file . "\r\n";
$exclude_files .= 'P ' . $exclude_file . "\r\n";
}
}
else {
$this->updateExcludeFilesForUnpublishedMedia($media, $exclude_files, $field, $fid);
}
}
}
// Files to exclude specified in settings.
if (!$draft) {
$rsync_public_exclude = $this->configFactory->get('static_generator.settings')
->get('rsync_public_exclude');
if (!empty($rsync_public_exclude)) {
$rsync_public_exclude_array = explode(',', $rsync_public_exclude);
foreach ($rsync_public_exclude_array as $rsync_public_exclude_file) {
$exclude_files .= $rsync_public_exclude_file . "\r\n";
}
}
}
\Drupal::service('file_system')
->saveData($exclude_files, $public_files_directory . '/' . $exclude_file_tmp, FileSystemInterface::EXISTS_REPLACE);
}
/**
* Update the exclude files.
*
* @param $media
*/
protected function updateExcludeFilesForUnpublishedMedia($media, &$exclude_files, $field, $fid = 0) {
$fid_cond = 0;
if ($media->isPublished()) {
$fid_cond = $fid;
}
$files_path = \Drupal::database()
->query('SELECT fm.fid AS fid, fm.uri AS uri FROM media_revision__' . $field . ' fmf
LEFT JOIN file_managed fm on fm.fid=fmf.' . $field . '_target_id
LEFT JOIN file_usage fu ON fu.fid=fm.fid WHERE fu.id=:media_id AND fu.type=:entity_type AND fm.fid!=:fid', [
':media_id' => $media->id(),
':entity_type' => 'media',
':fid' => $fid_cond,
])
->fetchAllKeyed();
if (!empty($files_path)) {
foreach (array_unique($files_path) as $uri) {
$exclude_file = substr($uri, 9);
$exclude_files .= $exclude_file . "\r\n";
}
}
}
/**
* Load the media revisions.
*
* @param array $media_revision_ids
* Media revision ids.
*
* @return mixed
* Media revisions in the array.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function loadMediaRevisionEntities(array $media_revision_ids) {
return \Drupal::entityTypeManager()
->getStorage('media')
->loadMultipleRevisions($media_revision_ids);
}
/**
* Load the media entities.
*
* @param array $media_ids
* Media ids array.
*
* @return mixed
* Load the media by using media ids.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function loadMediaEntities(array $media_ids) {
return \Drupal::entityTypeManager()
->getStorage('media')
->loadMultipleRevisions(array_keys($media_ids));;
}
/**
* Load the Media draft revisions.
*
* @return array
* Revision array.
*/
protected function excludeMediaIdsDraft() {
$q = \Drupal::entityQuery('media');
$or_cond = $q
->orConditionGroup()
->condition('moderation_state', 'draft')
->condition('moderation_state', 'scheduled_publish');
$q->latestRevision();
$q->accessCheck(TRUE);
$q->condition($or_cond);
$draft_ids = $q->execute();
if (!empty($draft_ids)) {
return array_flip($draft_ids);
}
return [];
}
/**
* Generate files for core, modules, and themes.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function generateCodeFiles() {
$start_time = time();
$rsync_code = $this->configFactory->get('static_generator.settings')
->get('rsync_code');
$generator_directory = $this->generatorDirectory(TRUE);
// rSync core.
$rsync_core = $rsync_code . ' ' . DRUPAL_ROOT . '/core ' . $generator_directory;
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateCodeFiles() Core: ' . $rsync_core);
}
exec($rsync_core);
// rSync modules.
$rsync_modules = $rsync_code . ' ' . DRUPAL_ROOT . '/modules ' . $generator_directory;
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateCodeFiles() Modules: ' . $rsync_modules);
}
exec($rsync_modules);
// rSync profiles.
$rsync_profiles = $rsync_code . ' ' . DRUPAL_ROOT . '/profiles ' . $generator_directory;
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateCodeFiles() profiles: ' . $rsync_profiles);
}
exec($rsync_profiles);
// rSync themes.
$rsync_themes = $rsync_code . ' ' . DRUPAL_ROOT . '/themes ' . $generator_directory;
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateCodeFiles() Themes: ' . $rsync_themes);
}
exec($rsync_themes);
// rSync libraries.
$rsync_libraries = $rsync_code . ' ' . DRUPAL_ROOT . '/libraries ' . $generator_directory;
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateCodeFiles() Libraries: ' . $rsync_libraries);
}
exec($rsync_libraries);
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateCodeFiles() elapsed time: ' . $elapsed_time . ' seconds.');
}
return $elapsed_time;
}
/**
* Generate redirects - requires redirect module.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function generateRedirects() {
$start_time = time();
if (\Drupal::moduleHandler()->moduleExists('redirect')) {
$storage = $this->entityTypeManager->getStorage('redirect');
$ids = $storage->getQuery()
->accessCheck(FALSE)
->execute();
$redirects = $storage->loadMultiple($ids);
foreach ($redirects as $redirect) {
$source_url = $redirect->getSourceUrl();
$target_array = $redirect->getRedirect();
$target_uri = $target_array['uri'];
// Grab alias instead of internal url;
$target_url = \Drupal::service('path_alias.manager')->getAliasByPath(substr($target_uri, 9));
$this->generateRedirect($source_url, $target_url);
if ($this->verboseLogging()) {
\Drupal::logger('static_generator')
->notice('generateRedirects() source: ' . $source_url . ' target: ' . $target_url);
}
}
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
return $elapsed_time;
}
/**
* Generate a redirect page file.
*
* @param string $source_url
* The source url.
* @param string $target_url
* The target url.
*
* @throws \Exception
*
*/
public function generateRedirect($source_url, $target_url) {
if (empty($source_url) || empty($target_url)) {
return;
}
// Get the redirect markup.
$redirect_markup = '<html><head><meta http-equiv="refresh" content="0;URL=' . $target_url . '"></head><body><a href="' . $target_url . '">Page has moved to this location.</a></body></html>';
// Write redirect page files.
$web_directory = $this->directoryFromPath($source_url);
$file_name = $this->filenameFromPath($source_url);
$directory = $this->generatorDirectory() . $web_directory;
if (\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY)) {
\Drupal::service('file_system')->saveData($redirect_markup, $directory . '/' . $file_name, FileSystemInterface::EXISTS_REPLACE);
}
}
/**
* Get filename from path.
*
* @param string $path
* The page's path.
*
* @return string
* The filename.
*
* @throws \Exception
*/
public function filenameFromPath($path) {
$path_alias = \Drupal::service('path_alias.manager')
->getAliasByPath($path);
$front = $this->configFactory->get('system.site')->get('page.front');
$front_alias = \Drupal::service('path_alias.manager')
->getAliasByPath($front);
if ($path_alias == $front_alias) {
$file_name = 'index.html';
} elseif ($this->endsWith($path_alias, '.json')) {
$file_name = strrchr($path_alias, '/');
$file_name = substr($file_name, 1);
} else {
$file_name = strrchr($path_alias, '/') . '.html';
$file_name = substr($file_name, 1);
}
return $file_name;
}
/**
* Get page directory from path.
*
* @param string $path
* The page's path.
*
* @return string
* The directory and filename.
*
* @throws \Exception
*/
public function directoryFromPath($path) {
$directory = '';
$front = $this->configFactory->get('system.site')->get('page.front');
if ($path != $front) {
$alias = \Drupal::service('path_alias.manager')
->getAliasByPath($path);
$occur = substr_count($alias, '/');
if ($occur > 1) {
$last_pos = strrpos($alias, '/');
$directory = substr($alias, 0, $last_pos);
}
}
return $directory;
}
/**
* Returns the rendered markup for a path.
*
* @param string $path
* The path.
*
* @param bool $account_switcher
*
* This allows caller to switch accounts once, that way the account
* is not repeatedly switched, if repeated calls to this function are made.
*
* @param bool $theme_switcher
*
* This allows caller to switch theme once, that way the theme
* is not repeatedly switched, if repeated calls to this function are made.
*
* @return string
* The rendered markup. The string '404' is returned if a 404 error is thrown,
* the strng 'error' is returned if any other error is thrown.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Theme\MissingThemeDependencyException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function markupForPage($path, $account_switcher = TRUE, $theme_switcher = TRUE, $raw_markup = FALSE) {
global $base_url;
$configuration = \Drupal::service('config.factory')->get('static_generator.settings');
// Switch to anonymous user.
if ($account_switcher) {
// Generate as Anonymous user.
\Drupal::service('account_switcher')
->switchTo(new AnonymousUserSession());
}
// Switch to default theme.
if ($theme_switcher) {
$active_theme = $this->themeManager->getActiveTheme();
$default_theme_name = $this->configFactory->get('system.theme')
->get('default');
$default_theme = $this->themeInitialization->getActiveThemeByName($default_theme_name);
$this->themeManager->setActiveTheme($default_theme);
}
// Get render method (core or guzzle).
$render_method = $this->configFactory->get('static_generator.settings')
->get('render_method');
$markup = '';
// Make Request
$configuration = \Drupal::service('config.factory')
->get('static_generator.settings');
$static_url = $configuration->get('static_url');
if ($render_method == 'Core') {
// Internal request using Drupal Core.
$request = Request::create($path, 'GET', [], [], [], $this->currentRequest->server->all());
try {
$response = $this->httpKernel->handle($request, HttpKernelInterface::MAIN_REQUEST);
$markup = $response->getContent();
} catch (\Exception $exception) {
// Switch back to active theme.
if ($theme_switcher) {
$this->themeManager->setActiveTheme($active_theme);
}
// Switch back from anonymous user.
if ($account_switcher) {
\Drupal::service('account_switcher')->switchBack();
}
\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.1.0', fn() => \Drupal\Core\Utility\Error::logException(\Drupal::logger('static_generator'), $exception), fn() => watchdog_exception('static_generator', $exception));
return '';
}
} else {
// Guzzle request.
$client = \Drupal::httpClient(['SERVER_NAME' => $static_url]);
try {
$guzzle_host = $this->configFactory->get('static_generator.settings')
->get('guzzle_host');
$guzzle_options = $this->configFactory->get('static_generator.settings')
->get('guzzle_options');
if (!empty($guzzle_options)) {
$guzzle_array = [];
$eval_cmd = '$guzzle_array=' . $guzzle_options . ';';
eval($eval_cmd);
$response = $client->request('GET', $guzzle_host . $path, $guzzle_array);
} else {
$response = $client->request('GET', $guzzle_host . $path);
}
if ($response) {
$markup = $response->getBody();
}
} catch (RequestException $exception) {
// Switch back to active theme.
if ($theme_switcher) {
$this->themeManager->setActiveTheme($active_theme);
}
// Switch back from anonymous user.
if ($account_switcher) {
\Drupal::service('account_switcher')->switchBack();
}
if (strpos($exception, '404') !== FALSE) {
\Drupal::logger('static_generator_404')->critical($path);
return '404';
} else {
\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.1.0', fn() => \Drupal\Core\Utility\Error::logException(\Drupal::logger('static_generator'), $exception), fn() => watchdog_exception('static_generator', $exception));
return 'error';
}
}
}
// Switch back to active theme.
if ($theme_switcher) {
$this->themeManager->setActiveTheme($active_theme);
}
// Switch back from anonymous user.
if ($account_switcher) {
\Drupal::service('account_switcher')->switchBack();
}
// If the markup does not need any transformation, return raw.
if ($raw_markup) {
return $markup;
}
// @todo need to implement this as a plugin, similar to a migrate process plugin
$dom = new DomDocument();
@$dom->loadHTML($markup);
$finder = new DomXPath($dom);
// Remove elements with class = block-local-task-block
$remove_local_tasks = $finder->query("//*[contains(@class, 'block-local-tasks-block')]");
foreach ($remove_local_tasks as $local_task) {
$local_task->parentNode->removeChild($local_task);
}
// Remove parent of elements with class = node--unpublished
$remove_unpublished = $finder->query("//*[contains(@class, 'node--unpublished')]");
foreach ($remove_unpublished as $unpublished) {
$unpublished->parentNode->parentNode->removeChild($unpublished->parentNode);
}
// Render video iframes.
$iframes = $finder->query("//iframe");
foreach ($iframes as $iframe) {
// iframe Markup looks like:
// '<iframe src="https://www.youtube.com/embed/Z2J_J2DY2-c" frameborder="0" width="480" height="270"></iframe>';
$iframe_src = $iframe->getAttribute('src');
// Generate remote video iframes.
// Currently only works with YouTube and Vimeo iframes.
$this->generateRemoteVideo($iframe_src, $iframe, $dom);
}
// Generation of pages for Views pagers.
if (strpos($path, '?') === FALSE) {
$last_page_lis = $finder->query("//li[contains(@class, 'pager__item--last')]");
foreach ($last_page_lis as $last_page_li) {
$last_page_href = $last_page_li->childNodes->item(1)
->getAttribute('href');
$last_page = intval(substr($last_page_href, 6));
for ($i = 0; $i <= $last_page; $i++) {
//$this->queuePage($path . '?page=' . $i, $path . '/page/' . $i);
$this->generatePage($path . '?page=' . $i, $path . '/page/' . $i);
}
}
}
// Fix pager links in markup.
/** @var \DOMElement $node */
foreach ($finder->query('//a[contains(@href,"?page=")]') as $node) {
$original_href = $node->getAttribute('href');
// If the href is not a relative path, skip it.
if (strpos($node->getAttribute('href'), '/') !== 0) {
continue;
}
if (strpos($path, '?') === FALSE) {
$new_path = $path;
} else {
$new_path = substr($path, 0, strpos($path, '?'));
}
$new_href = $new_path . str_replace('?page=', '/page/', $original_href);
$node->setAttribute('href', $new_href);
}
$markup = $dom->saveHTML();
// Fix canonical link so it has static site url.
if ($render_method == 'Core') {
// If we are using core, the source URL will be http://default.
$markup = str_replace($base_url, '', $markup);
} else {
$static_url = $configuration->get('static_url');
$guzzle_host = $configuration->get('guzzle_host');
$markup = str_replace($guzzle_host, $static_url, $markup);
}
// Convert canonical path to aliased paths.
$nids = [];
$pos = 0;
$i = 0;
while (strpos($markup, 'node/', $pos) !== FALSE) {
$i++;
if ($i > 500) {
$this->log('> 500 looping in page: ' . $path);
return $markup;
}
$pos = strpos($markup, 'node/', $pos) + 5;
$next_char = substr($markup, $pos, 1);
if (!is_numeric($next_char)) {
continue;
}
$nid = $next_char;
$next_char = substr($markup, $pos + 1, 1);
if (is_numeric($next_char)) {
$nid .= $next_char;
} else {
$nids[] = $nid;
continue;
}
$next_char = substr($markup, $pos + 2, 1);
if (is_numeric($next_char)) {
$nid .= $next_char;
} else {
$nids[] = $nid;
continue;
}
$next_char = substr($markup, $pos + 3, 1);
if (is_numeric($next_char)) {
$nid .= $next_char;
} else {
$nids[] = $nid;
continue;
}
$next_char = substr($markup, $pos + 4, 1);
if (is_numeric($next_char)) {
$nid .= $next_char;
} else {
$nids[] = $nid;
continue;
}
$next_char = substr($markup, $pos + 5, 1);
if (is_numeric($next_char)) {
$nid .= $next_char;
$nids[] = $nid;
} else {
$nids[] = $nid;
continue;
}
}
$nids = array_unique($nids);
rsort($nids);
foreach ($nids as $nid) {
$node_path = 'node/' . $nid;
$path_alias = \Drupal::service('path_alias.manager')
->getAliasByPath('/' . $node_path);
$markup = str_replace($node_path, substr($path_alias, 1), $markup);
}
return $markup;
}
/**
* Inject ESI markup/save ESI file.
*
* @param string $markup
* The markup.
*
* @param string $path
*
* @param array $blocks_processed
*
* @param array $sg_esi_processed
*
* @param $sg_esi_existing
*
* @return string
* Markup with ESI's injected.
*/
public function injectESIs($markup, $path, &$blocks_processed, &$sg_esi_processed, $sg_esi_existing) {
// Find all of the blocks in the markup.
// @todo Currently SG does ESI for every block, but specific blocks may
// @todo be excluded in the SG settings. May make more sense to work by
// @todo including blocks instead, or at least have that option.
$dom = new DomDocument();
@$dom->loadHTML($markup);
$finder = new DomXPath($dom);
$esi_blocks = $this->configFactory->get('static_generator.settings')
->get('esi_blocks');
if ($esi_blocks) {
$blocks = $finder->query("//*[contains(@class, 'block')]");
foreach ($blocks as $block) {
// Make sure class = "block".
$block_classes_str = $block->getAttribute('class');
if (!empty($block_classes_str)) {
$block_classes = explode(' ', $block_classes_str);
if (!in_array('block', $block_classes)) {
continue;
}
} else {
continue;
}
// Construct block id.
$block_id = $block->getAttribute('id');
if (empty($block_id)) {
continue;
}
if (substr($block_id, 0, 6) == 'block-') {
$block_id = substr($block_id, 6);
}
$block_id = str_replace('-', '_', $block_id);
// Return if block id or block pattern is listed in "block no esi" setting.
if (!$this->esiBlock($block_id)) {
continue;
}
// Get ESI filename.
$esi_filename = $block_id;
// Create the ESI and then replace the block with the ESI markup.
$esi_markup = '<!--#include virtual="/esi/block/' . Html::escape($esi_filename) . '" -->';
$esi_element = $dom->createElement('span', $esi_markup);
$block->parentNode->replaceChild($esi_element, $block);
// Generate the ESI fragment file.
if (in_array($block_id, $blocks_processed)) {
// Return if block has been processed.
continue;
} else {
$this->generateEsiFileByElement($esi_filename, $block, 'block');
$blocks_processed[] = $block_id;
}
}
}
$esi_sg_esi = $this->configFactory->get('static_generator.settings')
->get('esi_sg_esi');
if ($esi_sg_esi) {
// Remove elements with class=sg--remove.
$remove_elements = $finder->query("//*[contains(@class, 'sg--remove')]");
foreach ($remove_elements as $remove_element) {
$remove_element->parentNode->removeChild($remove_element);
}
// Process ESI for elements which have a class of sg-esi--<some-id>
$sg_esi_elements = $finder->query("//*[contains(@class, 'sg-esi--')]");
foreach ($sg_esi_elements as $element) {
// Get esi class.
$classes = $element->getAttribute('class');
$classes_array = explode(' ', $classes);
$esi_id = '';
foreach ($classes_array as $esi_class) {
if ($this->startsWith($esi_class, 'sg-esi--')) {
// Remove three dashes - hack for site specific issue, will be removed.
if ($this->startsWith($esi_class, 'sg-esi---')) {
continue;
}
$esi_id = substr($esi_class, 8);
}
}
// Must have an sg esi id.
if (empty($esi_id)) {
continue;
}
// Get list of existing sg esi filenames if not provided.
if (count($sg_esi_existing) == 0) {
$sg_esi_existing = $this->existingSgEsiFiles();
}
// Get ESI filename.
if (array_key_exists($esi_id, $sg_esi_processed)) {
// If esi id already processed, use existing file name.
$esi_filename = $sg_esi_processed[$esi_id];
} else {
if (array_key_exists($esi_id, $sg_esi_existing)) {
// Fragment file with esi_id exists, so use that file name.
$esi_filename = $sg_esi_existing[$esi_id];
} else {
// Get new filename.
$path_id = \Drupal::service('path_alias.manager')
->getPathByAlias($path);
$path_id = substr($path_id, 1);
$path_str = str_replace('/', '--', $path_id);
$esi_filename = $esi_id . '__' . $path_str;
}
}
// Replace the original element with the ESI markup.
$esi_markup = '<!--#include virtual="/esi/sg-esi/' . Html::escape($esi_filename) . '" -->';
$esi_element = $dom->createElement('span', $esi_markup);
$element->parentNode->replaceChild($esi_element, $element);
// Generate the ESI fragment file.
if (array_key_exists($esi_id, $sg_esi_processed)) {
// Return if esi_id has been processed.
continue;
} else {
$this->generateEsiFileByElement($esi_filename, $element, 'sg-esi');
$sg_esi_processed[$esi_id] = $esi_filename;
}
}
}
// Return markup with ESI's.
$markup_esi = $dom->saveHTML();
$markup_esi = str_replace('<', '<', $markup_esi);
$markup_esi = str_replace('>', '>', $markup_esi);
return $markup_esi;
}
/**
* Generate a block fragment file using the block_id and DOM block element.
*
* @param $esi_filename
* The filename for the generated ESI file.
*
* @param $element
* The dom element getting ESI.
*
* @param $directory
* The target directory (/esi/<directory>)
*/
public function generateEsiFileByElement($esi_filename, $element, $directory) {
// Make sure directory exists.
$directory = $this->generatorDirectory() . '/esi/' . $directory;
\Drupal::service('file_system')->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
// Generate esi fragment file.
$markup = $element->ownerDocument->saveHTML($element);
// Allow modules to modify ESI markup.
$event = new ModifyEsiMarkupEvent($markup);
$this->eventDispatcher->dispatch($event, StaticGeneratorEvents::MODIFY_ESI_MARKUP);
$markup = $event->getMarkup();
\Drupal::service('file_system')->saveData($markup, $directory . '/' . $esi_filename, FileSystemInterface::EXISTS_REPLACE);
}
/**
* Place page into queue.
*
* @param $path
*
* @param string $path_generate
*
* @return void ;
*/
public function queuePage($path, $action = 'create', $path_generate = '') {
// Get the queue implementation for SG
$queue_factory = \Drupal::service('queue');
$queue = $queue_factory->get('page_generator');
// Create new queue item
$item = new \stdClass();
$item->path = $path;
$item->action = $action;
$item->path_generate = $path_generate;
$queue->createItem($item);
}
/**
* @param $path
*/
public function processQueue() {
// Get the queue implementation for SG
$queue_factory = \Drupal::service('queue');
$queue = $queue_factory->get('page_generator');
$queue_worker = \Drupal::service('plugin.manager.queue_worker')
->createInstance('page_generator');
while ($item = $queue->claimItem()) {
try {
$queue_worker->processItem($item);
$queue->deleteItem($item);
} catch (SuspendQueueException $e) {
$queue->releaseItem($item);
break;
} catch (\Exception $e) {
\Drupal\Component\Utility\DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.1.0', fn() => \Drupal\Core\Utility\Error::logException(\Drupal::logger('static_generator'), $e), fn() => watchdog_exception('static_generator', $e));
}
}
}
public function processQueuesave($path) {
/** @var QueueInterface $queue */
$queue = $this->queueFactory->get('page_generator');
/** @var QueueWorkerInterface $queue_worker */
$queue_worker = $this->queueManager->createInstance('page_generator');
}
/**
* Get verbose logging setting.
*
* @return boolean;
*/
public function verboseLogging() {
$verbose_logging = $this->configFactory->get('static_generator.settings')
->get('verbose_logging');
return $verbose_logging;
}
/**
* Get generate unpublished setting.
*
* @return boolean;
*/
public function generateUnpublished() {
$gen_unpublished = $this->configFactory->get('static_generator.settings')
->get('gen_unpublished');
return $gen_unpublished;
}
/**
* Get generate index.html setting.
*
* @return boolean;
*/
public function generateIndex() {
$generate_index = $this->configFactory->get('static_generator.settings')
->get('generate_index');
return $generate_index;
}
/**
* @param $notice
*/
public function log($notice) {
\Drupal::logger('static_generator')
->notice($notice);
}
/**
* Get generator directory.
*
* @param bool $real_path
* Get the real path.
*
* @return string
*/
public function generatorDirectory($real_path = FALSE) {
$generator_directory = $this->configFactory->get('static_generator.settings')
->get('generator_directory');
if ($real_path) {
$generator_directory = $this->fileSystem->realpath($generator_directory);
}
return $generator_directory;
}
/**
* Delete all generated pages and files. This is done by deleting the top
* level directory and then re-creating it.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function deleteAll() {
$elapsed_time = $this->deletePages();
$elapsed_time += $this->deleteEsi();
$elapsed_time += $this->deleteDrupal();
// Elapsed time.
\Drupal::logger('static_generator')
->notice('Delete all elapsed time: ' . $elapsed_time . ' seconds.');
return $elapsed_time;
}
/**
* Delete all generated pages. Deletes all generated *.html files,
* and ESI include files.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function deletePages() {
$start_time = time();
// Get Drupal dirs setting.
$drupal = $this->configFactory->get('static_generator.settings')
->get('drupal');
if (!empty($drupal)) {
$drupal_array = explode(',', $drupal);
$drupal_array[] = 'esi';
} else {
$drupal_array = ['esi'];
}
// Get Non Drupal dirs setting.
$non_drupal = $this->configFactory->get('static_generator.settings')
->get('non_drupal');
$non_drupal_array = [];
if (!empty($non_drupal)) {
$non_drupal_array = explode(',', $non_drupal);
}
$generator_directory = $this->generatorDirectory(TRUE);
$files = \Drupal::service('file_system')->scanDirectory($generator_directory, '/.*/', ['recurse' => FALSE]);
foreach ($files as $file) {
$filename = $file->filename;
$html_file = substr($filename, -strlen('html')) == 'html';
if ($html_file && !in_array($filename, $non_drupal_array)) {
\Drupal::service('file_system')->deleteRecursive($file->uri, $callback = NULL);
} else {
if (!in_array($filename, $drupal_array) && !in_array($filename, $non_drupal_array)) {
if ($filename == 'node') {
$node_files = \Drupal::service('file_system')->scanDirectory($generator_directory . '/node', '/.*/', ['recurse' => TRUE]);
foreach ($node_files as $node_file) {
\Drupal::service('file_system')->deleteRecursive($node_file->uri, $callback = NULL);
}
\Drupal::service('file_system')->deleteRecursive($file->uri, $callback = NULL);
} else {
\Drupal::service('file_system')->deleteRecursive($file->uri, $callback = NULL);
exec('rm -rf ' . $file->uri);
}
}
}
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
\Drupal::logger('static_generator')
->notice('Delete Page elapsed time: ' . $elapsed_time . ' seconds.');
return $elapsed_time;
}
/**
* Deletes all generated block include files in /esi/blocks.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function deleteEsi() {
$start_time = time();
// Delete Blocks
$dir = $this->generatorDirectory(TRUE) . '/esi/block';
if (is_dir($dir)) {
// Delete ESIs include files and the esi directory.
$esi_files = \Drupal::service('file_system')->scanDirectory($dir, '/.*/', ['recurse' => true]);
foreach ($esi_files as $block_esi_file) {
\Drupal::service('file_system')->deleteRecursive($block_esi_file->uri, $callback = null);
}
\Drupal::service('file_system')->deleteRecursive($dir, $callback = null);
}
// Delete sg_esi tags
$dir = $this->generatorDirectory(true) . '/esi/sg-esi';
if (is_dir($dir)) {
// Delete sg esi include files and the sg-esi directory.
$esi_files = \Drupal::service('file_system')->scanDirectory($dir, '/.*/', ['recurse' => true]);
foreach ($esi_files as $block_esi_file) {
\Drupal::service('file_system')->deleteRecursive($block_esi_file->uri, $callback = null);
}
\Drupal::service('file_system')->deleteRecursive($dir, $callback = null);
}
// Delete /esi directory.
$dir = $this->generatorDirectory(TRUE) . '/esi';
if (is_dir($dir)) {
\Drupal::service('file_system')->deleteRecursive($dir, $callback = NULL);
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
\Drupal::logger('static_generator')
->notice('Delete blocks elapsed time: ' . $elapsed_time . ' seconds.');
return $elapsed_time;
}
/**
* Delete a single page.
*
* @param string $path
* The page's path.
*
* @throws \Exception
*/
public function deletePage($path) {
$web_directory = $this->directoryFromPath($path);
$file_name = $this->filenameFromPath($path);
$full_file_name = $this->generatorDirectory() . $web_directory . '/' . $file_name;
\Drupal::service('file_system')->delete($full_file_name);
\Drupal::logger('static_generator')
->notice('Deleted page: ' . $full_file_name);
}
/**
* Delete Drupal directories.
*
* @return int
* Execution time in seconds.
*
* @throws \Exception
*/
public function deleteDrupal() {
$start_time = time();
// Get Drupal dirs setting.
$drupal = $this->configFactory->get('static_generator.settings')
->get('drupal');
$drupal_array = [];
if (!empty($drupal)) {
$drupal_array = explode(',', $drupal);
}
$generator_directory = $this->generatorDirectory(TRUE);
$files = \Drupal::service('file_system')->scanDirectory($generator_directory, '/.*/', ['recurse' => FALSE]);
foreach ($files as $file) {
$filename = $file->filename;
if (in_array($filename, $drupal_array)) {
\Drupal::service('file_system')->deleteRecursive($file->uri, $callback = NULL);
exec('rm -rf ' . $file->uri);
}
}
// Elapsed time.
$end_time = time();
$elapsed_time = $end_time - $start_time;
\Drupal::logger('static_generator')
->notice('deleteDrupal() elapsed time: ' . $elapsed_time . ' seconds.');
return $elapsed_time;
}
/**
* Exclude media that is not published (e.g. Draft or Archived).
*
* @throws \Exception
*/
public function excludeMediaIdsUnpublished() {
$query = \Drupal::entityQuery('media');
$query->accessCheck(TRUE);
$query->latestRevision();
$exclude_media_ids = $query->execute();
return $exclude_media_ids;
}
/**
* List file name and update time for a path.
*
* @param $path
*
* @return string
* @throws \Exception
*/
public function fileInfo($path) {
$file_name = $this->generatorDirectory(TRUE) . $this->directoryFromPath($path) . '/' .
$this->filenameFromPath($path);
if (file_exists($file_name)) {
$return_string = $file_name . '<br/>' . date("F j, Y, g:i a", filemtime($file_name));
} else {
$return_string = 'Static page file not found.';
}
return $return_string;
}
/**
* Get generation info for a page.
*
* @param $path
*
* @param $entity
* @param array $form
*
* @param bool $details
*
* @return array
*
* @throws \Exception
*/
public function generationInfoForm($path, $entity = NULL, &$form = [], $details = FALSE) {
// Name and date info for static file.
$file_info = $this->fileInfo($path);
// Get path alias for path.
$path_alias = \Drupal::service('path_alias.manager')
->getAliasByPath($path);
// Get static URL setting.
$configuration = \Drupal::service('config.factory')
->get('static_generator.settings');
$static_url = $configuration->get('static_url');
$markup = '<br/>' . $file_info . '<br/><br/>';
if (!is_null($entity) && $entity->isPublished()) {
$markup .= '<a target="_blank" href="' . $path . '/gen' . '">' . t("Generate Static Page") . '</a>';
} else {
$markup .= t('This item is unpublished and may not be generated.');
}
$markup .= '<br/><br/><a target="_blank" href="' . $static_url . $path_alias . '">' . t("View Static Page") . '</a>';
$form['static_generator'] = [
'#title' => t('Static Generation'),
'#description' => t(''),
'#group' => 'advanced',
'#open' => FALSE,
'markup' => [
'#markup' => $markup,
],
'#weight' => 1000,
];
// Create form details.
if ($details) {
$form['static_generator']['#type'] = 'details';
}
return $form;
}
/**
* @param $path
*
* @param $entity
*
* @return \Drupal\Component\Render\MarkupInterface
* @throws \Exception
*/
public function generationInfo($path, $entity) {
$form = $this->generationInfoForm($path, $entity);
$markup = $this->renderer->render($form);
return $markup;
}
/**
* Generates a valid iframe for remote videos.
*
* @param string $iframe_src
* The iframe src URL.
* @param \DOMElement $iframe
* The DOMElement containing the iframe.
* @param \DOMDocument $dom
* The HTML from the iFrame element.
*
* @return void
* Replaces the incoming iframe with a newly generated iframe.
*/
public function generateRemoteVideo($iframe_src, $iframe, $dom) {
$iframe_attributes = [];
// Get the Youtube Attributes from the iFrame.
$youtube_embed_attributes = $this->getYouTubeEmbedAttributes($iframe_src);
if ($youtube_embed_attributes) {
$iframe_attributes['src'] = 'https://www.youtube.com/embed/' . $youtube_embed_attributes['youtube_id'];
if (!empty($youtube_embed_attributes['width'])) {
$iframe_attributes['width'] = $youtube_embed_attributes['width'];
}
if (!empty($youtube_embed_attributes['height'])) {
$iframe_attributes['height'] = $youtube_embed_attributes['height'];
}
}
// Check if the original iframe has allowfullscreen attribute.
$allowfullscreen = $iframe->getAttribute('allowfullscreen');
if (!empty($allowfullscreen)) {
$iframe_attributes['allowfullscreen'] = $allowfullscreen;
}
else {
// Default to allowfullscreen if not specified.
$iframe_attributes['allowfullscreen'] = 'allowfullscreen';
}
// Get the Vimeo Attributes from the iFrame.
$vimeo_embed_attributes = $this->getVimeoEmbedAttributes($iframe_src);
if ($vimeo_embed_attributes) {
$iframe_attributes['src'] = 'https://player.vimeo.com/video/' . $vimeo_embed_attributes['vimeo_id'];
}
// Get out of the function if iframe does not have src attribute.
if (!array_key_exists('src', $iframe_attributes)) {
return;
}
// Set default iframe attributes if not already set.
if (!array_key_exists('width', $iframe_attributes)) {
$iframe_attributes['width'] = '480';
}
if (!array_key_exists('height', $iframe_attributes)) {
$iframe_attributes['height'] = '270';
}
$iframe_element = $this->createRemoteVideoiFrameElement($iframe_attributes, $dom);
$iframe->parentNode->replaceChild($iframe_element, $iframe);
}
/**
* Creates an array of attributes that are pulled out of the YouTube src URL.
*
* @param string $iframe_src
* The iframe src URL.
*
* @return array|void
* Returns the attributes that are pulled out of the src URL.
*/
public function getYouTubeEmbedAttributes($iframe_src) {
$youtube_embed_attributes = [];
// Get Youtube ID.
$start_pos = strpos($iframe_src, 'youtu.be/');
if ($start_pos === FALSE) {
$iframe_src = urldecode($iframe_src);
$start_pos = strpos($iframe_src, 'youtube.com/watch?v=');
if ($start_pos === FALSE) {
$start_pos = strpos($iframe_src, '//www.youtube.com/embed/');
if ($start_pos !== FALSE) {
$start_pos += 24;
}
}
else {
$start_pos += 20;
}
}
else {
$start_pos += 9;
}
if ($start_pos === FALSE) {
return;
}
if (strpos($iframe_src, '//www.youtube.com/embed/') === FALSE) {
$end_pos = strpos($iframe_src, '&', $start_pos);
}
else {
$end_pos = strpos($iframe_src, '?', $start_pos);
}
$youtube_embed_attributes['youtube_id'] = substr($iframe_src, $start_pos, $end_pos - $start_pos);
if (empty($youtube_embed_attributes['youtube_id'])) {
return;
}
// Get the width.
$start_pos = strpos($iframe_src, 'width=');
if ($start_pos !== FALSE) {
$start_pos += 6;
$end_pos = strpos($iframe_src, '&', $start_pos);
if ($end_pos === FALSE) {
$end_pos = strlen($iframe_src);
}
$youtube_embed_attributes['width'] = substr($iframe_src, $start_pos, $end_pos - $start_pos);
}
// Get the height.
$start_pos = strpos($iframe_src, 'height=');
if ($start_pos !== FALSE) {
$start_pos += 7;
$end_pos = strpos($iframe_src, '&', $start_pos);
if ($end_pos === FALSE) {
$end_pos = strlen($iframe_src);
}
$youtube_embed_attributes['height'] = substr($iframe_src, $start_pos, $end_pos - $start_pos);
}
return $youtube_embed_attributes;
}
/**
* Creates an array of attributes that are pulled out of the vimeo src URL.
*
* @param string $iframe_src
* The iframe src URL.
*
* @return array|void
* Returns the attributes that are pulled out of the src URL.
*/
public function getVimeoEmbedAttributes($iframe_src) {
$vimeo_embed_attributes = [];
if (str_contains($iframe_src, 'vimeo') == FALSE) {
return;
}
$vimeo_id_regex = '/(?<=\/\/vimeo\.com\/).*?(?=&max)/';
if (preg_match($vimeo_id_regex, $iframe_src, $matches)) {
$vimeo_embed_attributes['vimeo_id'] = $matches[0];
}
return $vimeo_embed_attributes;
}
/**
* Creates iframe element for iframe embedded videos.
*
* @param array $iframe_attributes
* Attributes relating to the iframe src URL.
* @param \DOMDocument $dom
* The HTML from the iFrame element.
*
* @return \DOMElement
* The new iframe instance.
*/
public function createRemoteVideoiFrameElement($iframe_attributes, $dom) {
$iframe_element = $dom->createElement('iframe');
$iframe_element->setAttribute('src', $iframe_attributes['src']);
$iframe_element->setAttribute('frameborder', '0');
$iframe_element->setAttribute('width', $iframe_attributes['width']);
$iframe_element->setAttribute('height', $iframe_attributes['height']);
if (isset($iframe_attributes['allowfullscreen'])) {
$iframe_element->setAttribute('allowfullscreen', $iframe_attributes['allowfullscreen']);
}
return $iframe_element;
}
}
