Including security review as a submodule - with patched for Yaffs.
[yaffs-website] / web / modules / contrib / blazy / src / BlazyManager.php
1 <?php
2
3 namespace Drupal\blazy;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Entity\EntityInterface;
7
8 /**
9  * Implements a public facing blazy manager.
10  *
11  * A few modules re-use this: GridStack, Mason, Slick...
12  */
13 class BlazyManager extends BlazyManagerBase {
14
15   /**
16    * Cleans up empty breakpoints.
17    *
18    * @param array $settings
19    *   The settings being modified.
20    */
21   public function cleanUpBreakpoints(array &$settings = []) {
22     if (!empty($settings['breakpoints'])) {
23       foreach ($settings['breakpoints'] as $key => &$breakpoint) {
24         $breakpoint = array_filter($breakpoint);
25
26         if (empty($breakpoint['width']) && empty($breakpoint['image_style'])) {
27           unset($settings['breakpoints'][$key]);
28         }
29       }
30     }
31
32     // Identify that Blazy can be activated only by breakpoints.
33     if (empty($settings['blazy'])) {
34       $settings['blazy'] = !empty($settings['breakpoints']);
35     }
36   }
37
38   /**
39    * Checks if an image style contains crop effect.
40    */
41   public function isCrop($style = NULL) {
42     foreach ($style->getEffects() as $effect) {
43       if (strpos($effect->getPluginId(), 'crop') !== FALSE) {
44         return TRUE;
45       }
46     }
47
48     return FALSE;
49   }
50
51   /**
52    * Sets dimensions once to reduce method calls, if image style contains crop.
53    *
54    * The implementor should only call this if not using Responsive image style.
55    *
56    * @param array $settings
57    *   The settings being modified.
58    */
59   public function setDimensionsOnce(array &$settings = []) {
60     $item                 = isset($settings['item']) ? $settings['item'] : NULL;
61     $dimensions['width']  = $settings['original_width'] = isset($item->width) ? $item->width : NULL;
62     $dimensions['height'] = $settings['original_height'] = isset($item->height) ? $item->height : NULL;
63
64     // If image style contains crop, sets dimension once, and let all inherit.
65     if (!empty($settings['image_style']) && ($style = $this->entityLoad($settings['image_style']))) {
66       if ($this->isCrop($style)) {
67         $style->transformDimensions($dimensions, $settings['uri']);
68
69         $settings['height'] = $dimensions['height'];
70         $settings['width']  = $dimensions['width'];
71
72         // Informs individual images that dimensions are already set once.
73         $settings['_dimensions'] = TRUE;
74       }
75     }
76
77     // Also sets breakpoint dimensions once, if cropped.
78     if (!empty($settings['breakpoints'])) {
79       $this->buildDataBlazy($settings, $item);
80     }
81
82     // Remove these since this method is meant for top-level container.
83     unset($settings['uri'], $settings['item']);
84   }
85
86   /**
87    * Checks for Blazy formatter such as from within a Views style plugin.
88    *
89    * Ensures the settings traverse up to the container where Blazy is clueless.
90    * The supported plugins can add [data-blazy] attribute into its container
91    * containing $settings['blazy_data'] converted into [data-blazy] JSON.
92    *
93    * @param array $settings
94    *   The settings being modified.
95    * @param array $item
96    *   The item containing settings or item keys.
97    */
98   public function isBlazy(array &$settings, array $item = []) {
99     // Retrieves Blazy formatter related settings from within Views style.
100     $content = !empty($settings['item_id']) && isset($item[$settings['item_id']]) ? $item[$settings['item_id']] : $item;
101
102     // 1. Blazy formatter within Views fields by supported modules.
103     if (isset($item['settings'])) {
104       // Prevents edge case with unexpected flattened Views results which is
105       // normally triggered by checking "Use field template" option.
106       $blazy = is_array($content) && isset($content['#build']['settings']) ? $content['#build']['settings'] : [];
107
108       // Allows breakpoints overrides such as multi-styled images by GridStack.
109       if (empty($settings['breakpoints']) && isset($blazy['breakpoints'])) {
110         $settings['breakpoints'] = $blazy['breakpoints'];
111       }
112
113       $cherries = [
114         'blazy',
115         'box_style',
116         'image_style',
117         'lazy',
118         'media_switch',
119         'ratio',
120         'uri',
121       ];
122
123       foreach ($cherries as $key) {
124         $fallback = isset($settings[$key]) ? $settings[$key] : '';
125         $settings[$key] = isset($blazy[$key]) && empty($fallback) ? $blazy[$key] : $fallback;
126       }
127     }
128
129     // 2. Blazy Views fields by supported modules.
130     if (is_array($content) && isset($content['#view']) && ($view = $content['#view'])) {
131       if ($blazy_field = BlazyViews::viewsField($view)) {
132         $settings = array_merge(array_filter($blazy_field->mergedViewsSettings()), array_filter($settings));
133       }
134     }
135
136     // Provides data for the [data-blazy] attribute at the containing element.
137     $this->cleanUpBreakpoints($settings);
138     if (!empty($settings['breakpoints'])) {
139       $image = isset($item['item']) ? $item['item'] : NULL;
140       $this->buildDataBlazy($settings, $image);
141     }
142     unset($settings['uri']);
143   }
144
145   /**
146    * Builds breakpoints suitable for top-level [data-blazy] wrapper attributes.
147    *
148    * The hustle is because we need to define dimensions once, if applicable, and
149    * let all images inherit. Each breakpoint image may be cropped, or scaled
150    * without a crop. To set dimensions once requires all breakpoint images
151    * uniformly cropped. But that is not always the case.
152    *
153    * @param array $settings
154    *   The settings being modified.
155    * @param object|mixed $item
156    *   The \Drupal\image\Plugin\Field\FieldType\ImageItem item, or array when
157    *   dealing with Video Embed Field.
158    *
159    * @todo: Refine this like everything else.
160    */
161   public function buildDataBlazy(array &$settings, $item = NULL) {
162     // Early opt-out if blazy_data has already been defined.
163     // Blazy doesn't always deal with image directly.
164     if (!empty($settings['blazy_data'])) {
165       return;
166     }
167
168     if (empty($settings['original_width'])) {
169       $settings['original_width'] = isset($item->width) ? $item->width : NULL;
170       $settings['original_height'] = isset($item->height) ? $item->height : NULL;
171     }
172
173     $json = $sources = [];
174     $end = end($settings['breakpoints']);
175     foreach ($settings['breakpoints'] as $key => $breakpoint) {
176       if (empty($breakpoint['image_style']) || empty($breakpoint['width'])) {
177         continue;
178       }
179
180       if ($width = Blazy::widthFromDescriptors($breakpoint['width'])) {
181         // If contains crop, sets dimension once, and let all images inherit.
182         if (!empty($settings['uri']) && !empty($settings['ratio'])) {
183           $dimensions['width'] = $settings['original_width'];
184           $dimensions['height'] = $settings['original_height'];
185
186           if (!empty($breakpoint['image_style']) && ($style = $this->entityLoad($breakpoint['image_style']))) {
187             if ($this->isCrop($style)) {
188               $style->transformDimensions($dimensions, $settings['uri']);
189               $padding = round((($dimensions['height'] / $dimensions['width']) * 100), 2);
190               $json['dimensions'][$width] = $padding;
191
192               // Only set padding-bottom for the last breakpoint to avoid FOUC.
193               if ($end['width'] == $breakpoint['width']) {
194                 $settings['padding_bottom'] = $padding;
195               }
196             }
197           }
198         }
199
200         // If BG, provide [data-src-BREAKPOINT].
201         if (!empty($settings['background'])) {
202           $sources[] = ['width' => (int) $width, 'src' => 'data-src-' . $key];
203         }
204       }
205     }
206
207     // As of Blazy v1.6.0 applied to BG only.
208     if ($sources) {
209       $json['breakpoints'] = $sources;
210     }
211
212     // @todo: A more efficient way not to do this in the first place.
213     // ATM, this is okay as this method is run once on the top-level container.
214     if (isset($json['dimensions']) && (count($settings['breakpoints']) != count($json['dimensions']))) {
215       unset($json['dimensions'], $settings['padding_bottom']);
216     }
217
218     // Supported modules can add blazy_data as [data-blazy] to the container.
219     // This also informs individual images to not work with dimensions any more
220     // if the image style contains 'crop'.
221     if ($json) {
222       $settings['blazy_data'] = $json;
223     }
224
225     // Identify that Blazy can be activated only by breakpoints.
226     $settings['blazy'] = TRUE;
227   }
228
229   /**
230    * Returns the enforced content, or image using theme_blazy().
231    *
232    * @param array $build
233    *   The array containing: item, content, settings, or optional captions.
234    *
235    * @return array
236    *   The alterable and renderable array of enforced content, or theme_blazy().
237    */
238   public function getImage(array $build = []) {
239     if (empty($build['item'])) {
240       return [];
241     }
242
243     /** @var Drupal\image\Plugin\Field\FieldType\ImageItem $item */
244     $item                    = $build['item'];
245     $settings                = &$build['settings'];
246     $settings['delta']       = isset($settings['delta']) ? $settings['delta'] : 0;
247     $settings['image_style'] = isset($settings['image_style']) ? $settings['image_style'] : '';
248
249     if (empty($settings['uri'])) {
250       $settings['uri'] = ($entity = $item->entity) && empty($item->uri) ? $entity->getFileUri() : $item->uri;
251     }
252
253     // Respects content not handled by theme_blazy(), but passed through.
254     if (empty($build['content'])) {
255       $image = [
256         '#theme'       => isset($settings['theme_hook_image']) ? $settings['theme_hook_image'] : 'blazy',
257         '#delta'       => $settings['delta'],
258         '#item'        => [],
259         '#image_style' => $settings['image_style'],
260         '#build'       => $build,
261         '#pre_render'  => [[$this, 'preRenderImage']],
262       ];
263     }
264     else {
265       $image = $build['content'];
266     }
267
268     $this->getModuleHandler()->alter('blazy', $image, $settings);
269
270     return $image;
271   }
272
273   /**
274    * Builds the Blazy image as a structured array ready for ::renderer().
275    *
276    * @param array $element
277    *   The pre-rendered element.
278    *
279    * @return array
280    *   The renderable array of pre-rendered element.
281    */
282   public function preRenderImage(array $element) {
283     $build = $element['#build'];
284     $item  = $build['item'];
285     unset($element['#build']);
286
287     $settings = $build['settings'];
288     if (empty($item)) {
289       return [];
290     }
291
292     // Extract field item attributes for the theme function, and unset them
293     // from the $item so that the field template does not re-render them.
294     $item_attributes = [];
295     if (isset($item->_attributes)) {
296       $item_attributes = $item->_attributes;
297       unset($item->_attributes);
298     }
299
300     // Responsive image integration.
301     $settings['responsive_image_style_id'] = '';
302     if (!empty($settings['resimage']) && !empty($settings['responsive_image_style'])) {
303       $responsive_image_style = $this->entityLoad($settings['responsive_image_style'], 'responsive_image_style');
304       $settings['lazy'] = '';
305       if (!empty($responsive_image_style)) {
306         $settings['responsive_image_style_id'] = $responsive_image_style->id();
307         if ($this->configLoad('responsive_image')) {
308           $item_attributes['data-srcset'] = TRUE;
309           $settings['lazy'] = 'responsive';
310         }
311         $element['#cache']['tags'] = $this->getResponsiveImageCacheTags($responsive_image_style);
312       }
313     }
314     else {
315       if (!isset($settings['_no_cache'])) {
316         $file_tags = isset($settings['file_tags']) ? $settings['file_tags'] : [];
317         $settings['cache_tags'] = empty($settings['cache_tags']) ? $file_tags : Cache::mergeTags($settings['cache_tags'], $file_tags);
318
319         $element['#cache']['max-age'] = -1;
320         foreach (['contexts', 'keys', 'tags'] as $key) {
321           if (!empty($settings['cache_' . $key])) {
322             $element['#cache'][$key] = $settings['cache_' . $key];
323           }
324         }
325       }
326     }
327
328     $element['#item']            = $item;
329     $element['#captions']        = empty($build['captions']) ? [] : ['inline' => $build['captions']];
330     $element['#item_attributes'] = $item_attributes;
331     $element['#url']             = '';
332     $element['#settings']        = $settings;
333
334     foreach (['caption', 'media', 'wrapper'] as $key) {
335       if (!empty($settings[$key . '_attributes'])) {
336         $element["#$key" . '_attributes'] = $settings[$key . '_attributes'];
337       }
338     }
339
340     if (!empty($settings['media_switch']) && $settings['media_switch'] != 'media') {
341       if ($settings['media_switch'] == 'content' && !empty($settings['content_url'])) {
342         $element['#url'] = $settings['content_url'];
343       }
344       elseif (!empty($settings['lightbox'])) {
345         BlazyLightbox::build($element);
346       }
347     }
348
349     return $element;
350   }
351
352   /**
353    * Returns the entity view, if available.
354    *
355    * @param object $entity
356    *   The entity being rendered.
357    * @param array $settings
358    *   The settings containing view_mode.
359    * @param string $fallback
360    *   The fallback content when all fails, probably just entity label.
361    *
362    * @return array|bool
363    *   The renderable array of the view builder, or false if not applicable.
364    */
365   public function getEntityView($entity = NULL, array $settings = [], $fallback = '') {
366     if ($entity instanceof EntityInterface) {
367       $entity_type_id = $entity->getEntityTypeId();
368       $view_hook      = $entity_type_id . '_view';
369       $view_mode      = empty($settings['view_mode']) ? 'default' : $settings['view_mode'];
370       $langcode       = $entity->language()->getId();
371
372       // If module implements own {entity_type}_view.
373       if (function_exists($view_hook)) {
374         return $view_hook($entity, $view_mode, $langcode);
375       }
376       // If entity has view_builder handler.
377       elseif ($this->getEntityTypeManager()->hasHandler($entity_type_id, 'view_builder')) {
378         return $this->getEntityTypeManager()->getViewBuilder($entity_type_id)->view($entity, $view_mode, $langcode);
379       }
380       elseif ($fallback) {
381         return ['#markup' => $fallback];
382       }
383     }
384
385     return FALSE;
386   }
387
388   /**
389    * Returns the Responsive image cache tags.
390    *
391    * @param object $responsive
392    *   The responsive image style entity.
393    *
394    * @return array
395    *   The responsive image cache tags, or empty array.
396    */
397   public function getResponsiveImageCacheTags($responsive = NULL) {
398     $cache_tags = [];
399     $image_styles_to_load = [];
400     if ($responsive) {
401       $cache_tags = Cache::mergeTags($cache_tags, $responsive->getCacheTags());
402       $image_styles_to_load = $responsive->getImageStyleIds();
403     }
404
405     $image_styles = $this->entityLoadMultiple('image_style', $image_styles_to_load);
406     foreach ($image_styles as $image_style) {
407       $cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags());
408     }
409     return $cache_tags;
410   }
411
412 }