{ $script .= '};'; } $script .= ' window.addEventListener(\'LazyLoad::Initialized\', function (e) { var lazyLoadInstance = e.detail.instance; if (window.MutationObserver) { var observer = new MutationObserver(function(mutations) { var image_count = 0; var iframe_count = 0; var rocketlazy_count = 0; mutations.forEach(function(mutation) { for (var i = 0; i < mutation.addedNodes.length; i++) { if (typeof mutation.addedNodes[i].getElementsByTagName !== \'function\') { continue; } if (typeof mutation.addedNodes[i].getElementsByClassName !== \'function\') { continue; } images = mutation.addedNodes[i].getElementsByTagName(\'img\'); is_image = mutation.addedNodes[i].tagName == "IMG"; iframes = mutation.addedNodes[i].getElementsByTagName(\'iframe\'); is_iframe = mutation.addedNodes[i].tagName == "IFRAME"; rocket_lazy = mutation.addedNodes[i].getElementsByClassName(\'rocket-lazyload\'); image_count += images.length; iframe_count += iframes.length; rocketlazy_count += rocket_lazy.length; if(is_image){ image_count += 1; } if(is_iframe){ iframe_count += 1; } } } ); if(image_count > 0 || iframe_count > 0 || rocketlazy_count > 0){ lazyLoadInstance.update(); } } ); var b = document.getElementsByTagName("body")[0]; var config = { childList: true, subtree: true }; observer.observe(b, config); } }, false);'; return $script; } /** * CL: Gets the JavaScript literal for a lazyload option. * * @param string $option Lazyload option name. * @param mixed $value Lazyload option value. * @return string|null */ private function getLazyloadOptionValue( $option, $value ) { if ( 'container' === $option ) { return $this->getLazyloadContainerOptionValue( $value ); } if ( 0 === strpos( $option, 'callback_' ) ) { return $this->getLazyloadCallbackOptionValue( $value ); } if ( in_array( $option, [ 'cancel_on_exit', 'unobserve_entered', 'unobserve_completed', 'use_native' ], true ) ) { return $this->getBooleanOptionValue( $value ); } $encoded = wp_json_encode( $value ); return false === $encoded ? null : $encoded; } /** * CL: Gets a safe JavaScript expression for the lazyload container option. * * @param mixed $value Container option value. * @return string|null */ private function getLazyloadContainerOptionValue( $value ) { if ( ! is_string( $value ) ) { return null; } $value = trim( $value ); $allowed_containers = [ 'document' => true, 'window.document' => true, 'document.body' => true, 'document.documentElement' => true, ]; return isset( $allowed_containers[ $value ] ) ? $value : null; } /** * CL: Gets a safe JavaScript expression for a lazyload callback option. * * @param mixed $value Callback option value. * @return string|null */ private function getLazyloadCallbackOptionValue( $value ) { if ( ! is_string( $value ) ) { return null; } $value = trim( $value ); if ( '' === $value ) { return null; } return preg_match( '/^(?:window\.)?[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/', $value ) ? $value : null; } /** * CL: Gets a JavaScript boolean literal for a lazyload option. * * @param mixed $value Boolean-ish option value. * @return string|null */ private function getBooleanOptionValue( $value ) { if ( is_array( $value ) || is_object( $value ) ) { return null; } $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); if ( null === $value ) { return null; } return $value ? 'true' : 'false'; } /** * Returns the lazyload inline script * * @param array $args Array of arguments to populate the lazyload script options. * @return string */ public function getLazyloadScript( $args = [] ) { $defaults = [ 'base_url' => '', 'version' => '', ]; $args = wp_parse_args( $args, $defaults ); $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min'; /** * Filters the script tag for the lazyload script * * @since 2.2.6 * * @param string $script_tag HTML tag for the lazyload script. */ return apply_filters( 'rocket_lazyload_script_tag', '' ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript } /** * Inserts in the HTML the script to replace the Youtube thumbnail by the iframe. * * @param array $args Array of arguments to populate the script options. * @return void */ public function insertYoutubeThumbnailScript( $args = [] ) { echo $this->getYoutubeThumbnailScript( $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Returns the Youtube Thumbnail inline script * * @param array $args Array of arguments to populate the script options. * @return string */ public function getYoutubeThumbnailScript( $args = [] ) { $defaults = [ 'resolution' => 'hqdefault', 'lazy_image' => false, 'native' => true, 'extension' => 'jpg', 'button_aria_label' => 'play Youtube video', ]; $allowed_resolutions = [ 'default' => [ 'width' => 120, 'height' => 90, ], 'mqdefault' => [ 'width' => 320, 'height' => 180, ], 'hqdefault' => [ 'width' => 480, 'height' => 360, ], 'sddefault' => [ 'width' => 640, 'height' => 480, ], 'maxresdefault' => [ 'width' => 1280, 'height' => 720, ], ]; $args['resolution'] = ( isset( $args['resolution'] ) && isset( $allowed_resolutions[ $args['resolution'] ] ) ) ? $args['resolution'] : 'hqdefault'; $args = wp_parse_args( $args, $defaults ); $extension_uri = 'webp' === $args['extension'] ? 'vi_webp' : 'vi'; $image_url = esc_url( 'https://i.ytimg.com/' . $extension_uri . '/ID/' . $args['resolution'] . '.' . $args['extension'] ); // CL: escape thumbnail URL before inline-script output (XSS). $image = ''; if ( isset( $args['lazy_image'] ) && $args['lazy_image'] ) { $attributes = 'alt="" width="' . $allowed_resolutions[ $args['resolution'] ]['width'] . '" height="' . $allowed_resolutions[ $args['resolution'] ]['height'] . '"'; $image = ''; if ( $args['native'] ) { $image = ''; } } $button_aria_label = esc_attr( $args['button_aria_label'] ); // CL: escape aria-label before inline-script output (XSS). /** * Filters the patterns excluded from lazyload for youtube thumbnails. * * @param array $excluded_patterns Array of excluded patterns. */ $excluded_patterns = apply_filters( 'rocket_lazyload_exclude_youtube_thumbnail', [] ); if ( ! is_array( $excluded_patterns ) ) { $excluded_patterns = []; } $excluded_patterns = wp_json_encode( $excluded_patterns ); // CL: escape dataset values (data-id, data-alt) before they are concatenated into the innerHTML'd thumbnail markup; browser dataset access HTML-decodes the esc_attr'd attribute, so a literal " or < would otherwise break out of alt="..."/src="..." -> DOM XSS. The escaped value is inserted via a String.replace REPLACEMENT FUNCTION (not a replacement string) so JS $-patterns ($`, $', $&, $n) in a dataset value are inserted literally instead of re-injecting raw pre/post-match template markup -> attribute breakout. Upstream this to the RocketLazyload package on re-vendor. return ""; } /** * Inserts the CSS to style the Youtube thumbnail container * * @param array $args Array of arguments to populate the CSS. * @return void */ public function insertYoutubeThumbnailCSS( $args = [] ) { wp_register_style( 'rocket-lazyload', false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_enqueue_style( 'rocket-lazyload' ); wp_add_inline_style( 'rocket-lazyload', $this->getYoutubeThumbnailCSS( $args ) ); } /** * Returns the CSS for the Youtube Thumbnail * * @param array $args Array of arguments to populate the CSS. * @return string */ public function getYoutubeThumbnailCSS( $args = [] ) { $defaults = [ 'base_url' => '', 'responsive_embeds' => true, ]; $args = wp_parse_args( $args, $defaults ); $css = '.rll-youtube-player{position:relative;padding-bottom:56.23%;height:0;overflow:hidden;max-width:100%;}.rll-youtube-player:focus-within{outline: 2px solid currentColor;outline-offset: 5px;}.rll-youtube-player iframe{position:absolute;top:0;left:0;width:100%;height:100%;z-index:100;background:0 0}.rll-youtube-player img{bottom:0;display:block;left:0;margin:auto;max-width:100%;width:100%;position:absolute;right:0;top:0;border:none;height:auto;-webkit-transition:.4s all;-moz-transition:.4s all;transition:.4s all}.rll-youtube-player img:hover{-webkit-filter:brightness(75%)}.rll-youtube-player .play{height:100%;width:100%;left:0;top:0;position:absolute;background:url(' . $args['base_url'] . 'img/youtube.png) no-repeat center;background-color: transparent !important;cursor:pointer;border:none;}'; if ( $args['responsive_embeds'] ) { $css .= '.wp-embed-responsive .wp-has-aspect-ratio .rll-youtube-player{position:absolute;padding-bottom:0;width:100%;height:100%;top:0;bottom:0;left:0;right:0}'; } return $css; } /** * Inserts the CSS needed when Javascript is not enabled to keep the display correct * * @return void */ public function insertNoJSCSS() { echo $this->getNoJSCSS(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Returns the CSS to correctly display images when JavaScript is disabled * * @return string */ public function getNoJSCSS() { return ''; } }