View source
<?php
namespace Drupal\blazy\Plugin\Filter;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Drupal\blazy\Blazy;
use Drupal\blazy\BlazyDefault;
use Drupal\blazy\BlazyOEmbedInterface;
use Drupal\blazy\BlazyUtil;
use Drupal\blazy\Form\BlazyAdminInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BlazyFilter extends FilterBase implements BlazyFilterInterface, ContainerFactoryPluginInterface {
protected $root;
protected $entityFieldManager;
protected $blazyAdmin;
protected $blazyOembed;
protected $blazyManager;
public function __construct(array $configuration, $plugin_id, $plugin_definition, $root, EntityFieldManagerInterface $entity_field_manager, BlazyAdminInterface $blazy_admin, BlazyOEmbedInterface $blazy_oembed) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->root = $root;
$this->entityFieldManager = $entity_field_manager;
$this->blazyAdmin = $blazy_admin;
$this->blazyOembed = $blazy_oembed;
$this->blazyManager = $blazy_oembed
->blazyManager();
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container
->get('app.root'), $container
->get('entity_field.manager'), $container
->get('blazy.admin'), $container
->get('blazy.oembed'));
}
public function process($text, $langcode) {
$result = new FilterProcessResult($text);
$allowed_tags = array_values((array) $this->settings['filter_tags']);
if (empty($allowed_tags)) {
return $result;
}
$dom = Html::load($text);
$attachments = [];
$settings = $this
->buildSettings($text);
$valid_nodes = $this
->validNodes($dom, $allowed_tags);
if (count($valid_nodes) > 0) {
$elements = $grid_nodes = [];
foreach ($valid_nodes as $delta => $node) {
$item_settings = $settings;
$item_settings['uri'] = $item_settings['image_url'] = '';
$item_settings['delta'] = $delta;
if ($image_style = $node
->getAttribute('data-image-style')) {
$item_settings['image_style'] = $image_style;
}
if ($responsive_image_style = $node
->getAttribute('data-responsive-image-style')) {
$item_settings['responsive_image_style'] = $responsive_image_style;
}
if (!empty($settings['_resimage']) && !empty($item_settings['responsive_image_style'])) {
$item_settings['resimage'] = $this->blazyManager
->entityLoad($item_settings['responsive_image_style'], 'responsive_image_style');
}
$this
->buildItemSettings($item_settings, $node);
$build = [
'settings' => $item_settings,
];
$this
->buildImageItem($build, $node);
$this
->buildImageCaption($build, $node);
$uri = $build['settings']['uri'];
$missing = BlazyUtil::isValidUri($uri) && !is_file($uri);
if (empty($uri) || $missing) {
$node
->setAttribute('class', 'blazy-removed');
continue;
}
$output = $this->blazyManager
->getBlazy($build);
if ($settings['_grid']) {
$elements[] = $output;
$grid_nodes[] = $node;
}
else {
$altered_html = $this->blazyManager
->getRenderer()
->render($output);
$updated_nodes = Html::load($altered_html)
->getElementsByTagName('body')
->item(0)->childNodes;
foreach ($updated_nodes as $updated_node) {
$updated_node = $dom
->importNode($updated_node, TRUE);
$node->parentNode
->insertBefore($updated_node, $node);
}
if ($node->parentNode) {
$node->parentNode
->removeChild($node);
}
}
}
$all = [
'blazy' => TRUE,
'filter' => TRUE,
'ratio' => TRUE,
];
$all['media_switch'] = $switch = $settings['media_switch'];
if (!empty($settings[$switch])) {
$all[$switch] = $settings[$switch];
}
if ($settings['_grid'] && !empty($elements[0])) {
$all['grid'] = $settings['grid'];
$all['column'] = $settings['column'];
$settings['_uri'] = isset($elements[0]['#build'], $elements[0]['#build']['settings']['uri']) ? $elements[0]['#build']['settings']['uri'] : '';
$this
->buildGrid($dom, $settings, $elements, $grid_nodes);
}
$attachments = $this->blazyManager
->attach($all);
$this
->cleanupNodes($dom);
}
$result
->setProcessedText(Html::serialize($dom))
->addAttachments($attachments);
return $result;
}
public function buildSettings($text) {
$settings = $this->settings + BlazyDefault::lazySettings();
$settings['_check_protocol'] = TRUE;
$settings['grid'] = stristr($text, 'data-grid') !== FALSE;
$settings['column'] = stristr($text, 'data-column') !== FALSE;
$settings['id'] = $settings['gallery_id'] = Blazy::getHtmlId('blazy-filter-' . Crypt::randomBytesBase64(8));
$settings['plugin_id'] = 'blazy_filter';
$settings['_grid'] = $settings['column'] || $settings['grid'];
$definitions = $this->entityFieldManager
->getFieldDefinitions('media', 'remote_video');
$settings['is_media_library'] = $definitions && isset($definitions['field_media_oembed_video']);
$settings['_resimage'] = $this->blazyManager
->getModuleHandler()
->moduleExists('responsive_image');
if (isset($settings['hybrid_style']) && ($style = $settings['hybrid_style'])) {
if ($settings['_resimage'] && ($box_style = $this->blazyManager
->entityLoad($style, 'responsive_image_style'))) {
$settings['responsive_image_style'] = $style;
}
else {
$settings['image_style'] = $style;
}
}
$this->blazyManager
->getCommonSettings($settings);
$build = [
'settings' => $settings,
];
$this->blazyManager
->getModuleHandler()
->alter('blazy_settings', $build, $this->settings);
return array_merge($settings, $build['settings']);
}
public function cleanupNodes(\DOMDocument &$dom) {
$xpath = new \DOMXPath($dom);
$nodes = $xpath
->query("//*[contains(@class, 'blazy-removed')]");
if ($nodes->length > 0) {
$this
->removeNodes($nodes);
}
}
public function buildGrid(\DOMDocument &$dom, array &$settings, array $elements = [], array $grid_nodes = []) {
$xpath = new \DOMXPath($dom);
$query = $settings['style'] = $settings['column'] ? 'column' : 'grid';
$grid = FALSE;
$node = $query == 'column' ? $xpath
->query('//*[@data-column]') : $xpath
->query('//*[@data-grid]');
if ($node->length > 0 && $node
->item(0) && $node
->item(0)
->hasAttribute('data-' . $query)) {
$grid = $node
->item(0)
->getAttribute('data-' . $query);
}
if ($grid) {
$grids = array_map('trim', explode(' ', $grid));
foreach ([
'small',
'medium',
'large',
] as $key => $item) {
if (isset($grids[$key])) {
$settings['grid_' . $item] = $grids[$key];
$settings['grid'] = $grids[$key];
}
}
$build = [
'items' => $elements,
'settings' => $settings,
];
$output = $this->blazyManager
->build($build);
$altered_html = $this->blazyManager
->getRenderer()
->render($output);
if ($first = $grid_nodes[0]) {
if ($first->parentNode && $first->parentNode->tagName == 'figure') {
$first = $first->parentNode;
}
$parent = $first->parentNode ? $first->parentNode : $first;
$container = $parent
->insertBefore($dom
->createElement('div'), $first);
$container
->setAttribute('class', 'blazy-wrapper blazy-wrapper--filter');
$updated_nodes = Html::load($altered_html)
->getElementsByTagName('body')
->item(0)->childNodes;
foreach ($updated_nodes as $updated_node) {
$updated_node = $dom
->importNode($updated_node, TRUE);
$container
->appendChild($updated_node);
}
$this
->removeNodes($grid_nodes);
}
}
}
public function buildImageItem(array &$build, &$node) {
$settings =& $build['settings'];
$item = NULL;
if ($src = $node
->getAttribute('src')) {
$data_uri = mb_substr($src, 0, 10) === 'data:image';
if (!$data_uri) {
if (mb_substr($src, 0, 2) === '//') {
$src = 'https:' . $src;
}
if ($node->tagName == 'img') {
$item = $this
->getImageItemFromImageSrc($settings, $node, $src);
}
elseif ($node->tagName == 'iframe') {
try {
$item = $this
->getImageItemFromIframeSrc($settings, $node, $src);
} catch (\Exception $ignore) {
}
}
}
}
if ($item) {
$item->alt = $node
->getAttribute('alt') ?: (isset($item->alt) ? $item->alt : '');
$item->title = $node
->getAttribute('title') ?: (isset($item->title) ? $item->title : '');
if (!empty($item->uri) && empty($item->width)) {
if ($data = @getimagesize($item->uri)) {
list($item->width, $item->height) = $data;
}
}
}
$build['media_attributes']['class'] = [
'media-wrapper',
'media-wrapper--blazy',
];
if ($node->attributes->length) {
foreach ($node->attributes as $attribute) {
if ($attribute->nodeName == 'src') {
continue;
}
if ($attribute->nodeName == 'class') {
$build['media_attributes']['class'][] = $attribute->nodeValue;
}
elseif (!isset($item->target_id)) {
$build['item_attributes'][$attribute->nodeName] = $attribute->nodeValue;
}
}
$build['media_attributes']['class'] = array_unique($build['media_attributes']['class']);
}
$build['item'] = $item;
}
public function buildImageCaption(array &$build, &$node) {
if ($node->parentNode && $node->parentNode->tagName === 'figure') {
$caption = $node->parentNode
->getElementsByTagName('figcaption');
$item = $caption->length > 0 && $caption
->item(0) ? $caption
->item(0) : NULL;
if ($item && ($text = $item->ownerDocument
->saveXML($item))) {
$settings =& $build['settings'];
$markup = Xss::filter($text, BlazyDefault::TAGS);
$build['captions']['alt'] = [
'#markup' => $markup,
];
if (isset($settings['box_caption']) && $settings['box_caption'] == 'inline') {
$settings['box_caption'] = $markup;
}
$item
->setAttribute('class', 'blazy-removed');
if ($settings['_grid']) {
$node->parentNode
->setAttribute('class', 'blazy-removed');
}
}
}
}
public function getImageItemFromImageSrc(array &$settings, $node, $src) {
$data['item'] = NULL;
$uuid = $node
->hasAttribute('data-entity-uuid') ? $node
->getAttribute('data-entity-uuid') : '';
if ($uuid && ($file = $this->blazyManager
->getEntityRepository()
->loadEntityByUuid('file', $uuid))) {
$data = $this->blazyOembed
->getImageItem($file);
if (isset($data['settings'])) {
$settings = array_merge($settings, $data['settings']);
}
}
else {
$settings['uri'] = $src;
if ($uri = BlazyUtil::buildUri($src)) {
$settings['uri'] = $uri;
$data['item'] = Blazy::image($settings);
}
else {
$settings['uri_root'] = mb_substr($src, 0, 4) === 'http' ? $src : $this->root . $src;
}
}
return $data['item'];
}
public function getImageItemFromIframeSrc(array &$settings, &$node, $src) {
$settings['input_url'] = $src;
$this->blazyOembed
->checkInputUrl($settings);
$data['item'] = NULL;
if (!empty($settings['is_media_library'])) {
$media = $this->blazyManager
->getEntityTypeManager()
->getStorage('media')
->loadByProperties([
'field_media_oembed_video' => $settings['input_url'],
]);
$media = reset($media);
}
if (isset($media) && $media) {
$data['settings'] = $settings;
$this->blazyOembed
->getMediaItem($data, $media);
$settings = array_merge($settings, $data['settings']);
}
else {
$data['item'] = $this->blazyOembed
->getExternalImageItem($settings);
$this->blazyOembed
->build($settings);
}
return $data['item'];
}
public function buildItemSettings(array &$settings, $node) {
$settings['width'] = $node
->getAttribute('width');
$settings['height'] = $node
->getAttribute('height');
$settings['media_switch'] = $this->settings['media_switch'];
}
public function tips($long = FALSE) {
if ($long) {
return $this
->t('
<p><strong>Blazy</strong>: Image or iframe is lazyloaded. To disable, add attribute <code>data-unblazy</code>:</p>
<ul>
<li><code><img data-unblazy /></code></li>
<li><code><iframe data-unblazy /></code></li>
</ul>
<p>To build a grid of images/ videos, add attribute <code>data-grid</code> or <code>data-column</code> (only to the first item):</p>
<ul>
<li><code><img data-grid="1 3 4" /></code></li>
<li><code><iframe data-column="1 3 4" /></code></li>
</ul>
<p>The numbers represent the amount of grids/ columns for small, medium and large devices respectively, space delimited. Be aware! All media items will be grouped regardless of their placements, unless those given a <code>data-unblazy</code>. Manually add width and height for SVG, and other images without image styles.</p>');
}
else {
return $this
->t('To disable lazyload, add attribute <code>data-unblazy</code> to <code><img></code> or <code><iframe></code> elements. Examples: <code><img data-unblazy</code> or <code><iframe data-unblazy</code>. Manually add width and height for SVG, and other images without image styles.');
}
}
public function settingsForm(array $form, FormStateInterface $form_state) {
$lightboxes = $this->blazyManager
->getLightboxes();
$form['filter_tags'] = [
'#type' => 'checkboxes',
'#title' => $this
->t('Enable HTML tags'),
'#options' => [
'img' => $this
->t('Image'),
'iframe' => $this
->t('Video iframe'),
],
'#default_value' => empty($this->settings['filter_tags']) ? [] : array_values((array) $this->settings['filter_tags']),
'#description' => $this
->t('Recommended placement after Align / Caption images. To disable Blazy per individual item, add attribute <code>data-unblazy</code>.'),
'#prefix' => '<p>' . $this
->t('<b>Warning!</b> Blazy Filter is useless and broken when you enable <b>Media embed</b> or <b>Display embedded entities</b>. You can disable Blazy Filter in favor of Blazy formatter embedded inside <b>Media embed</b> or <b>Display embedded entities</b> instead. However it might be useful for User Generated Contents (UGC) where Entity/Media Embed are likely more for privileged users, authors, editors, admins, alike. Or when Entity/Media Embed is disabled. Or when editors prefer pasting embed codes from video providers rather than creating media entities.') . '</p>',
];
$form['media_switch'] = [
'#type' => 'select',
'#title' => $this
->t('Media switcher'),
'#options' => [
'media' => $this
->t('Image to iframe'),
],
'#empty_option' => $this
->t('- None -'),
'#default_value' => isset($this->settings['media_switch']) ? $this->settings['media_switch'] : '',
'#description' => $this
->t('<ul><li><b>Image to iframe</b> will play video when toggled.</li><li><b>Image to lightbox</b> (Colorbox, Photobox, PhotoSwipe, Slick Lightbox, Zooming, Intense, etc.) will display media in lightbox.</li></ul>Both can stand alone or grouped as a gallery. To build a gallery, add <code>data-column="1 3 4"</code> or <code>data-grid="1 3 4"</code> to the first image/ iframe only.'),
];
if (!empty($lightboxes)) {
foreach ($lightboxes as $lightbox) {
$name = Unicode::ucwords(str_replace('_', ' ', $lightbox));
$form['media_switch']['#options'][$lightbox] = $this
->t('Image to @lightbox', [
'@lightbox' => $name,
]);
}
}
$styles = $this->blazyAdmin
->getResponsiveImageOptions() + $this->blazyAdmin
->getEntityAsOptions('image_style');
$form['hybrid_style'] = [
'#type' => 'select',
'#title' => $this
->t('(Responsive) image style'),
'#options' => $styles,
'#empty_option' => $this
->t('- None -'),
'#default_value' => isset($this->settings['hybrid_style']) ? $this->settings['hybrid_style'] : '',
'#description' => $this
->t('Fallback (Responsive) image style when <code>[data-image-style]</code> or <code>[data-responsive-image-style]</code> attributes are not present, see https://drupal.org/node/2061377.'),
];
$form['box_style'] = [
'#type' => 'select',
'#title' => $this
->t('Lightbox (Responsive) image style'),
'#options' => $styles,
'#empty_option' => $this
->t('- None -'),
'#default_value' => isset($this->settings['box_style']) ? $this->settings['box_style'] : '',
];
$captions = $this->blazyAdmin
->getLightboxCaptionOptions();
unset($captions['entity_title'], $captions['custom']);
$form['box_caption'] = [
'#type' => 'select',
'#title' => $this
->t('Lightbox caption'),
'#options' => $captions + [
'inline' => $this
->t('Caption filter'),
],
'#empty_option' => $this
->t('- None -'),
'#default_value' => isset($this->settings['box_caption']) ? $this->settings['box_caption'] : '',
'#description' => $this
->t('Automatic will search for Alt text first, then Title text. <br>Image styles only work for uploaded images, not hand-coded ones.'),
];
$form['use_data_uri'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Trust data URI'),
'#default_value' => isset($this->settings['use_data_uri']) ? $this->settings['use_data_uri'] : FALSE,
'#description' => $this
->t('Enable to support the use of data URI. Leave it unchecked if unsure, or never use data URI.'),
];
return $form;
}
protected function removeNodes($nodes) {
foreach ($nodes as $node) {
if ($node->parentNode) {
$node->parentNode
->removeChild($node);
}
}
}
private function validNodes(\DOMDocument $dom, array $allowed_tags = []) {
$valid_nodes = [];
foreach ($allowed_tags as $allowed_tag) {
$nodes = $dom
->getElementsByTagName($allowed_tag);
if ($nodes->length > 0) {
foreach ($nodes as $node) {
if ($node
->hasAttribute('data-unblazy')) {
continue;
}
$valid_nodes[] = $node;
}
}
}
return $valid_nodes;
}
}