tome-8.x-1.x-dev/modules/tome_static/src/StaticGenerator.php

modules/tome_static/src/StaticGenerator.php
<?php

namespace Drupal\tome_static;

use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\Exception\FileWriteException;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\Site\Settings;
use Drupal\tome_base\PathTrait;
use Drupal\tome_static\Event\CollectPathsEvent;
use Drupal\tome_static\Event\FileSavedEvent;
use Drupal\tome_static\Event\ModifyDestinationEvent;
use Drupal\tome_static\Event\ModifyHtmlEvent;
use Drupal\tome_static\Event\PathPlaceholderEvent;
use Drupal\tome_static\Event\TomeStaticEvents;
use Drupal\tome_static\EventSubscriber\ExcludePathSubscriber;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Drupal\Core\File\FileSystemInterface;

/**
 * Handles static site generation.
 *
 * @internal
 */
class StaticGenerator implements StaticGeneratorInterface {

  use PathTrait;

  /**
   * The HTTP kernel.
   *
   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
   */
  protected $httpKernel;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $currentRequest;

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * The static cache.
   *
   * @var \Drupal\tome_static\StaticCacheInterface
   */
  protected $cache;

  /**
   * The account switcher.
   *
   * @var \Drupal\Core\Session\AccountSwitcherInterface
   */
  protected $accountSwitcher;

  /**
   * The file system.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * Creates a StaticGenerator object.
   *
   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
   *   The HTTP kernel.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\tome_static\StaticCacheInterface $cache
   *   The static cache.
   * @param \Drupal\Core\Session\AccountSwitcherInterface $account_switcher
   *   The account switcher.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system.
   */
  public function __construct(HttpKernelInterface $http_kernel, RequestStack $request_stack, EventDispatcherInterface $event_dispatcher, StaticCacheInterface $cache, AccountSwitcherInterface $account_switcher, FileSystemInterface $file_system) {
    $this->httpKernel = $http_kernel;
    $this->currentRequest = $request_stack->getCurrentRequest();
    $this->eventDispatcher = $event_dispatcher;
    $this->cache = $cache;
    $this->accountSwitcher = $account_switcher;
    $this->requestStack = $request_stack;
    $this->fileSystem = $file_system;

  }

  /**
   * {@inheritdoc}
   */
  public function getPaths() {
    $this->accountSwitcher->switchTo(new AnonymousUserSession());
    $event = new CollectPathsEvent([]);
    $this->eventDispatcher->dispatch($event, TomeStaticEvents::COLLECT_PATHS);
    $paths = $event->getPaths();

    $paths = $this->cache->filterUncachedPaths($this->currentRequest->getSchemeAndHttpHost(), $paths);
    $this->accountSwitcher->switchBack();
    return array_values($paths);
  }

  /**
   * {@inheritdoc}
   */
  public function cleanupStaticDirectory() {
    foreach ($this->cache->getExpiredFiles() as $file) {
      $this->fileSystem->delete($file);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function requestPath($path) {
    $this->accountSwitcher->switchTo(new AnonymousUserSession());
    $invoke_paths = [];

    $original_path = $path;

    $event = new PathPlaceholderEvent($path);
    $this->eventDispatcher->dispatch($event, TomeStaticEvents::PATH_PLACEHOLDER);

    if ($event->isInvalid()) {
      $this->accountSwitcher->switchBack();
      return [];
    }

    $path = $event->getPath();

    $request = Request::create($path, 'GET', [], [], [], $this->currentRequest->server->all());

    $request->attributes->set(static::REQUEST_KEY, static::REQUEST_KEY);

    $previous_stack = $this->replaceRequestStack($request);

    try {
      $response = $this->httpKernel->handle($request, HttpKernelInterface::MAIN_REQUEST);
    }
    catch (\Exception $e) {
      $this->accountSwitcher->switchBack();
      $this->restoreRequestStack($previous_stack);
      throw $e;
    }

    $destination = $this->getDestination($path);
    if ($response->isRedirection() || $response->isOk()) {
      $directory = dirname($destination);
      $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
      // This is probably an image style derivative.
      if ($response instanceof BinaryFileResponse) {
        $file_path = $response->getFile()->getPathname();
        $this->copyPath($file_path, $destination);
      }
      else {
        $content = $response->getContent();
        if (strpos($response->headers->get('Content-Type', ''), 'text/html') === 0) {
          $event = new ModifyHtmlEvent($content, $path);
          $this->eventDispatcher->dispatch($event, TomeStaticEvents::MODIFY_HTML);
          $content = $event->getHtml();
          $invoke_paths = array_merge($invoke_paths, $this->getHtmlAssets($content, $path), $event->getInvokePaths());
          $invoke_paths = array_diff($invoke_paths, $event->getExcludePaths());
        }
        if (strpos($response->headers->get('Content-Type', ''), 'text/css') === 0) {
          $invoke_paths = array_merge($invoke_paths, $this->getCssAssets($content, $path));
        }
        if (strpos($response->headers->get('Content-Type', ''), 'text/javascript') === 0) {
          $invoke_paths = array_merge($invoke_paths, $this->getJavascriptModules($content, $path));
        }
        file_put_contents($destination, $content);
      }
      $this->eventDispatcher->dispatch(new FileSavedEvent($destination), TomeStaticEvents::FILE_SAVED);

      if ($response instanceof RedirectResponse) {
        $target_url = $this->makeExternalUrlLocal($response->getTargetUrl());
        if (!UrlHelper::isExternal($target_url)) {
          $invoke_paths[] = $target_url;
        }
      }

      $this->cache->setCache($request, $response, $original_path, $destination);
    }

    $this->restoreRequestStack($previous_stack);
    $this->accountSwitcher->switchBack();
    return $this->filterInvokePaths($invoke_paths, $request);
  }

  /**
   * {@inheritdoc}
   */
  public function exportPaths(array $paths) {
    $paths = array_diff($paths, $this->getExcludedPaths());
    $paths = array_values(array_unique($paths));

    $invoke_paths = [];

    foreach ($paths as $path) {
      $path = $this->makeExternalUrlLocal($path);
      if (UrlHelper::isExternal($path)) {
        continue;
      }
      $destination = $this->getDestination($path);

      $sanitized_path = $this->sanitizePath($path);
      if ($this->copyPath($sanitized_path, $destination)) {
        $destination_noparams = preg_replace("/\?.*/", "", $destination);
        if (pathinfo($destination_noparams, PATHINFO_EXTENSION) === 'css') {
          $css_assets = $this->getCssAssets(file_get_contents($destination_noparams), $sanitized_path);
          $invoke_paths = array_merge($invoke_paths, $this->exportPaths($css_assets));
        }
        if (pathinfo($destination_noparams, PATHINFO_EXTENSION) === 'js') {
          $js_modules = $this->getJavascriptModules(file_get_contents($destination_noparams), $sanitized_path);
          $invoke_paths = array_merge($invoke_paths, $this->exportPaths($js_modules));
        }
      }
      else {
        $invoke_paths[] = $path;
      }
    }

    return $this->filterInvokePaths($invoke_paths, $this->currentRequest);
  }

  /**
   * {@inheritdoc}
   */
  public function getStaticDirectory() {
    return Settings::get('tome_static_directory', '../html');
  }

  /**
   * {@inheritdoc}
   */
  public function prepareStaticDirectory() {
    $directory = $this->getStaticDirectory();
    if ($this->cache->isCacheEmpty()) {
      if (file_exists($directory)) {
        try {
          $this->fileSystem->deleteRecursive($directory);
        }
        catch (FileException $e) {
          return FALSE;
        }
      }
    }
    try {
      $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
    }
    catch (FileException $e) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Filters invoke paths to remove any external or cached paths.
   *
   * @param array $invoke_paths
   *   An array of paths returned by requestPath or exportPaths.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   A request object.
   *
   * @return array
   *   An array of paths to invoke.
   */
  protected function filterInvokePaths(array $invoke_paths, Request $request) {
    // Also filter invoke_paths array to prevent loop redirect on front page.
    $invoke_paths = preg_replace(['/^#.*/', '/#.*/'], ['/', ''], array_filter($invoke_paths));

    foreach ($invoke_paths as $i => &$invoke_path) {
      $invoke_path = $this->makeExternalUrlLocal($invoke_path);
      if (UrlHelper::isExternal($invoke_path) || strpos($invoke_path, 'data:') === 0) {
        unset($invoke_paths[$i]);
        continue;
      }
      $components = parse_url($invoke_path);
      if (isset($components['query'])) {
        parse_str($components['query'], $query);
        if (isset($query['destination'])) {
          unset($query['destination']);
        }
        $query = http_build_query($query);
        $invoke_path = $components['path'];
        if (!empty($query)) {
          $invoke_path .= '?' . $query;
        }
      }
    }

    $invoke_paths = $this->cache->filterUncachedPaths($request->getSchemeAndHttpHost(), $invoke_paths);

    return array_values(array_unique($invoke_paths));
  }

  /**
   * Makes external URLs local if their hostname is the current hostname.
   *
   * @param string $path
   *   A path.
   *
   * @return string
   *   A possibly transformed path.
   */
  protected function makeExternalUrlLocal($path) {
    $components = parse_url($path);
    if (UrlHelper::isExternal($path) && isset($components['host']) && UrlHelper::externalIsLocal($path, $this->currentRequest->getSchemeAndHttpHost())) {
      $path = $components['path'];
      if (!empty($components['query'])) {
        $path .= '?' . $components['query'];
      }
    }
    return $path;
  }

  /**
   * Finds assets for the given CSS content.
   *
   * @param string $content
   *   A CSS string.
   * @param string $root
   *   A root path to resolve relative paths.
   *
   * @return array
   *   An array of paths found in the given CSS string.
   */
  protected function getCssAssets($content, $root) {
    $paths = [];
    // Regex copied from the Static module from Drupal 7.
    // Credit to Randall Knutson and Michael Vanetta.
    $matches = [];
    preg_match_all('/url\(\s*[\'"]?(?!(?:data)+:)([^\'")]+)[\'"]?\s*\)/i', $content, $matches);
    if (isset($matches[1])) {
      $paths = $matches[1];
    }
    $paths = $this->getRealPaths($paths, $root);
    return $paths;
  }

  /**
   * Finds JavaScript modules for the given JavaScript content.
   *
   * @param string $content
   *   A JavaScript string.
   * @param string $root
   *   A root path to resolve relative paths.
   *
   * @return array
   *   An array of paths found in the given JavaScript string.
   */
  protected function getJavascriptModules(string $content, string $root) {
    $matches = [];
    preg_match_all('/import(?:{[^}]+}from)?"(?<file>[a-z0-9-_\/.]+.js)"/i', $content, $matches);
    $paths = $matches['file'] ?? [];
    $root_dir = rtrim(dirname($this->sanitizePath($root)), '/');
    return $this->getRealPaths(array_map(function (string $path) use ($root_dir): string {
      if (strpos($path, '.') === 0) {
        // Relative path, append the root dir.
        return $root_dir . '/' . $path;
      }
      return $path;
    }, $paths), $root);
  }

  /**
   * Turns relative paths into absolute paths.
   *
   * Useful specifically for CSS's url().
   *
   * @param array $paths
   *   An array of paths to convert.
   * @param string $root
   *   A root path to resolve relative paths.
   *
   * @return array
   *   An array of converted paths.
   */
  protected function getRealPaths(array $paths, $root) {
    $root_dir = dirname($this->sanitizePath($root));
    foreach ($paths as &$path) {
      if (strpos($path, '../') !== FALSE) {
        $path = $this->joinPaths($root_dir, $path);
      }
    }
    return $paths;
  }

  /**
   * Finds and exports assets for the given HTML content.
   *
   * @param string $content
   *   An HTML string.
   * @param string $root
   *   A root path to resolve relative paths.
   *
   * @return array
   *   An array of paths found in the given HTML string.
   */
  protected function getHtmlAssets($content, $root) {
    $paths = [];
    $document = new \DOMDocument();
    @$document->loadHTML($content);
    $xpath = new \DOMXPath($document);
    /** @var \DOMElement $image */
    foreach ($xpath->query('//img | //source | //video') as $image) {
      if ($image->hasAttribute('src')) {
        $paths[] = $image->getAttribute('src');
      }
      if ($image->hasAttribute('poster')) {
        $paths[] = $image->getAttribute('poster');
      }
      if ($image->hasAttribute('srcset')) {
        $srcset = $image->getAttribute('srcset');
        $sources = explode(' ', preg_replace('/ [^ ]+(,|$)/', '', $srcset));
        foreach ($sources as $src) {
          $paths[] = $src;
        }
      }
    }
    /** @var \DOMElement $node */
    foreach ($xpath->query('//svg/use') as $node) {
      if ($node->hasAttribute('xlink:href')) {
        $paths[] = $node->getAttribute('xlink:href');
      }
    }
    $rels = [
      'stylesheet',
      'shortcut icon',
      'icon',
      'image_src',
      'manifest',
    ];
    /** @var \DOMElement $node */
    foreach ($document->getElementsByTagName('link') as $node) {
      if (in_array($node->getAttribute('rel'), $rels, TRUE) && $node->hasAttribute('href')) {
        $paths[] = $node->getAttribute('href');
      }
    }
    $meta_files = [
      'twitter:image',
      'twitter:player:stream',
      'og:image',
      'og:video',
      'og:audio',
      'og:image:url',
      'og:image:secure_url',
    ];
    /** @var \DOMElement $node */
    foreach ($document->getElementsByTagName('meta') as $node) {
      if ((in_array($node->getAttribute('property'), $meta_files, TRUE) || in_array($node->getAttribute('name'), $meta_files, TRUE)) && $node->hasAttribute('content')) {
        $paths[] = $node->getAttribute('content');
      }
    }
    /** @var \DOMElement $node */
    foreach ($document->getElementsByTagName('a') as $node) {
      if ($node->hasAttribute('href')) {
        $paths[] = $node->getAttribute('href');
      }
    }
    /** @var \DOMElement $node */
    foreach ($document->getElementsByTagName('script') as $node) {
      if ($node->hasAttribute('src')) {
        $paths[] = $node->getAttribute('src');
      }
    }
    /** @var \DOMElement $node */
    foreach ($document->getElementsByTagName('style') as $node) {
      $paths = array_merge($paths, $this->getCssAssets($node->textContent, $root));
    }
    foreach ($xpath->query('//*[@style]') as $node) {
      $paths = array_merge($paths, $this->getCssAssets($node->getAttribute('style'), $root));
    }
    /** @var \DOMElement $node */
    foreach ($document->getElementsByTagName('iframe') as $node) {
      if ($node->hasAttribute('src')) {
        $paths[] = $node->getAttribute('src');
      }
    }

    // Recursive call in HTML comments in order to retrieve conditional assets.
    /** @var \DOMElement $node */
    foreach ($xpath->query('//comment()') as $node) {
      $paths = array_merge($paths, $this->getHtmlAssets($node->nodeValue, $root));
    }

    return $paths;
  }

  /**
   * Returns a destination for saving a given path.
   *
   * @param string $path
   *   The path.
   *
   * @return string
   *   The destination.
   */
  protected function getDestination($path) {
    $event = new ModifyDestinationEvent($path);
    $this->eventDispatcher->dispatch($event, TomeStaticEvents::MODIFY_DESTINATION);
    $path = $event->getDestination();
    $path = urldecode($path);
    $path = $this->sanitizePath($path);
    if (empty(pathinfo($path, PATHINFO_EXTENSION))) {
      $path .= '/index.html';
    }
    return $this->joinPaths($this->getStaticDirectory(), $path);
  }

  /**
   * Sanitizes a given path by removing hashes, get params, and extra slashes.
   *
   * @param string $path
   *   The path.
   *
   * @return string
   *   The sanitized path.
   */
  protected function sanitizePath($path) {
    $path = preg_replace(['/\?.*/', '/#.*/'], '', $path);
    return ltrim($path, '/');
  }

  /**
   * Attempts to copy a path from the file system.
   *
   * @param string $path
   *   The path.
   * @param string $destination
   *   The destination.
   *
   * @return bool
   *   TRUE if $path exists and was copied to $destination, FALSE otherwise.
   */
  protected function copyPath($path, $destination) {
    $path = urldecode($path);

    $base_path = base_path();
    if ($base_path !== '/') {
      $base_path = ltrim($base_path, '/');
      $pattern = '|^' . preg_quote($base_path, '|') . '|';
      $path = preg_replace($pattern, '', $path);
    }
    if (file_exists($path)) {
      $directory = dirname($destination);
      $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
      try {
        $this->fileSystem->copy($path, $destination, FileSystemInterface::CREATE_DIRECTORY);
      }
      catch (FileWriteException $exception) {
        return FALSE;
      }
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Returns paths excluded globally and per site.
   *
   * @return array
   *   An array of excluded paths.
   */
  protected function getExcludedPaths() {
    $paths = ExcludePathSubscriber::getExcludedPaths();
    foreach ($paths as &$path) {
      $path = $this->joinPaths(base_path(), $path);
    }
    return $paths;
  }

  /**
   * Replaces the request stack with a static request.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The new static request.
   *
   * @return \Symfony\Component\HttpFoundation\Request[]
   *   An array of previous stack requests.
   */
  protected function replaceRequestStack(Request $request) {
    $previous_stack = [];
    while ($pop = $this->requestStack->pop()) {
      $previous_stack[] = $pop;
    };
    $this->requestStack->push($request);
    return array_reverse($previous_stack);
  }

  /**
   * Restores the request stack to its previous state.
   *
   * @param \Symfony\Component\HttpFoundation\Request[] $stack
   *   An array of previous stack requests.
   */
  protected function restoreRequestStack(array $stack) {
    while ($this->requestStack->pop()) {
    };
    foreach ($stack as $request) {
      $this->requestStack->push($request);
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc