423 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			423 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace ShortPixel\Controller;
 | |
| 
 | |
| if ( ! defined( 'ABSPATH' ) ) {
 | |
|  exit; // Exit if accessed directly.
 | |
| }
 | |
| 
 | |
| use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
 | |
| use ShortPixel\Notices\NoticeController as Notices;
 | |
| use ShortPixel\Helper\UtilHelper as UtilHelper;
 | |
| use ShortPixel\Model\FrontImage as FrontImage;
 | |
| 
 | |
| use ShortPixel\ShortPixelImgToPictureWebp as ShortPixelImgToPictureWebp;
 | |
| 
 | |
| /** Handle everything that SP is doing front-wise */
 | |
| class FrontController extends \ShortPixel\Controller
 | |
| {
 | |
|   // DeliverWebp option settings for front-end delivery of webp
 | |
|   const WEBP_GLOBAL = 1;
 | |
|   const WEBP_WP = 2;
 | |
|   const WEBP_NOCHANGE = 3;
 | |
| 
 | |
|   public function __construct()
 | |
|   {
 | |
| 
 | |
|     if (\wpSPIO()->env()->is_front) // if is front.
 | |
|     {
 | |
|       $this->initWebpHooks();
 | |
| 
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected function initWebpHooks()
 | |
|   {
 | |
|     $webp_option = \wpSPIO()->settings()->deliverWebp;
 | |
| 
 | |
|     if ( $webp_option ) {  // @tood Replace this function with the one in ENV.
 | |
|         if(UtilHelper::shortPixelIsPluginActive('shortpixel-adaptive-images/short-pixel-ai.php')) {
 | |
|             Notices::addWarning(__('Please deactivate the ShortPixel Image Optimizer\'s
 | |
|                 <a href="options-general.php?page=wp-shortpixel-settings&part=adv-settings">Deliver the next generation versions of the images in the front-end</a>
 | |
|                 option when the ShortPixel Adaptive Images plugin is active.','shortpixel-image-optimiser'), true);
 | |
|         }
 | |
|         elseif( $webp_option == self::WEBP_GLOBAL ){
 | |
|             //add_action( 'wp_head', array($this, 'addPictureJs') ); // adds polyfill JS to the header || Removed. Browsers without picture support?
 | |
|             add_action( 'init',  array($this, 'startOutputBuffer'), 1 ); // start output buffer to capture content
 | |
|         } elseif ($webp_option == self::WEBP_WP){
 | |
|             add_filter( 'the_content', array($this, 'convertImgToPictureAddWebp'), 10000 ); // priority big, so it will be executed last
 | |
|             add_filter( 'the_excerpt', array($this, 'convertImgToPictureAddWebp'), 10000 );
 | |
|             add_filter( 'post_thumbnail_html', array($this,'convertImgToPictureAddWebp') );
 | |
|         }
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /* Picture generation, hooked on the_content filter
 | |
|   * @param $content String The content to check and convert
 | |
|   * @return String Converted content
 | |
|   */
 | |
|   public function convertImgToPictureAddWebp($content) {
 | |
| 
 | |
|       if(function_exists('is_amp_endpoint') && is_amp_endpoint()) {
 | |
|           //for AMP pages the <picture> tag is not allowed
 | |
| 					// phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form
 | |
|           return $content . (isset($_GET['SHORTPIXEL_DEBUG']) ? '<!-- SPDBG is AMP -->' : '');
 | |
|       }
 | |
| 
 | |
|       $content = $this->convert($content);
 | |
|       return $content;
 | |
|   }
 | |
| 
 | |
|   public function startOutputBuffer() {
 | |
|       $env = wpSPIO()->env();
 | |
|       if ($env->is_admin || $env->is_ajaxcall)
 | |
|         return;
 | |
| 
 | |
|       $call = array($this, 'convertImgToPictureAddWebp');
 | |
|       ob_start( $call );
 | |
|   }
 | |
| 
 | |
| 
 | |
|   protected function convert($content)
 | |
|   {
 | |
|       // Don't do anything with the RSS feed.
 | |
|       if (is_feed() || is_admin()) {
 | |
|           Log::addInfo('SPDBG convert is_feed or is_admin');
 | |
|           return $content; // . (isset($_GET['SHORTPIXEL_DEBUG']) ? '<!--  -->' : '');
 | |
|       }
 | |
| 
 | |
|       $new_content = $this->testPictures($content);
 | |
| 
 | |
|       if ($new_content !== false)
 | |
|       {
 | |
|         $content = $new_content;
 | |
|       }
 | |
|       else
 | |
|       {
 | |
|         Log::addDebug('Test Pictures returned empty.');
 | |
|       }
 | |
| 
 | |
|       if (! class_exists('DOMDocument'))
 | |
|       {
 | |
|         Log::addWarn('Webp Active, but DomDocument class not found ( missing xmldom library )');
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|     //	preg_match_all
 | |
|       $content = preg_replace_callback('/<img[^>]*>/i', array($this, 'convertImage'), $content);
 | |
| 
 | |
|       // [BS] No callback because we need preg_match_all
 | |
|       $content = $this->testInlineStyle($content);
 | |
| 
 | |
|       return $content;
 | |
|   }
 | |
| 
 | |
|   /** If lazy loading is happening, get source (src) from those values
 | |
|   * Otherwise pass back image data in a regular way.
 | |
|   */
 | |
|   private function lazyGet($img, $type)
 | |
|   {
 | |
| 
 | |
|     $value = false;
 | |
|     $prefix = false;
 | |
| 
 | |
|      if (isset($img['data-lazy-' . $type]) && strlen($img['data-lazy-' . $type]) > 0)
 | |
|      {
 | |
|          $value = $img['data-lazy-' . $type];
 | |
|          $prefix = 'data-lazy-';
 | |
|      }
 | |
|      elseif( isset($img['data-' . $type]) && strlen($img['data-' . $type]) > 0)
 | |
|      {
 | |
|         $value = $img['data-' . $type];
 | |
|         $prefix = 'data-';
 | |
|      }
 | |
|      elseif(isset($img[$type]) && strlen($img[$type]) > 0)
 | |
|      {
 | |
|         $value = $img[$type];
 | |
|         $prefix = '';
 | |
|      }
 | |
| 
 | |
|     return array(
 | |
|       'value' => $value,
 | |
|       'prefix' => $prefix,
 | |
|      );
 | |
|   }
 | |
| 
 | |
|   /* Find image tags within picture definitions and make sure they are converted only by block, */
 | |
|   private function testPictures($content)
 | |
|   {
 | |
|     // [BS] Escape when DOM Module not installed
 | |
|     //if (! class_exists('DOMDocument'))
 | |
|     //  return false;
 | |
|   //$pattern =''
 | |
|   //$pattern ='/(?<=(<picture>))(.*)(?=(<\/picture>))/mi';
 | |
|   $pattern = '/<picture.*?>.*?(<img.*?>).*?<\/picture>/is';
 | |
|   $count = preg_match_all($pattern, $content, $matches);
 | |
| 
 | |
|   if ($matches === false)
 | |
|     return false;
 | |
| 
 | |
|   if ( is_array($matches) && count($matches) > 0)
 | |
|   {
 | |
|     foreach($matches[1] as $match)
 | |
|     {
 | |
|          $imgtag = $match;
 | |
| 
 | |
|          if (strpos($imgtag, 'class=') !== false) // test for class, if there, insert ours in there.
 | |
|          {
 | |
|           $pos = strpos($imgtag, 'class=');
 | |
|           $pos = $pos + 7;
 | |
| 
 | |
|           $newimg = substr($imgtag, 0, $pos) . 'sp-no-webp ' . substr($imgtag, $pos);
 | |
| 
 | |
|          }
 | |
|          else {
 | |
|             $pos = 4;
 | |
|             $newimg = substr($imgtag, 0, $pos) . ' class="sp-no-webp" ' . substr($imgtag, $pos);
 | |
|          }
 | |
| 
 | |
|          $content = str_replace($imgtag, $newimg, $content);
 | |
| 
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return $content;
 | |
|   }
 | |
| 
 | |
|   /* This might be a future solution for regex callbacks.
 | |
|   public static function processImageNode($node, $type)
 | |
|   {
 | |
|     $srcsets = $node->getElementsByTagName('srcset');
 | |
|     $srcs = $node->getElementsByTagName('src');
 | |
|     $imgs = $node->getElementsByTagName('img');
 | |
|   } */
 | |
| 
 | |
|   /** Callback function with received an <img> tag match
 | |
|   * @param $match Image declaration block
 | |
|   * @return String Replacement image declaration block
 | |
|   */
 | |
|   protected function convertImage($match)
 | |
|   {
 | |
|       $fs = \wpSPIO()->filesystem();
 | |
| 
 | |
|       $raw_image = $match[0];
 | |
| 
 | |
|       // Raw Image HTML
 | |
|       $image = new FrontImage($raw_image);
 | |
| 
 | |
|       if (false === $image->isParseable())
 | |
|       {
 | |
|          return $raw_image;
 | |
|       }
 | |
| 
 | |
|       $srcsetWebP = array();
 | |
|       $srcsetAvif = array();
 | |
|       // Count real instances of either of them, without fillers.
 | |
|       $webpCount = $avifCount = 0;
 | |
| 
 | |
|       $imagePaths = array();
 | |
| 
 | |
|       $definitions = $image->getImageData();
 | |
|       $imageBase = $image->getImageBase();
 | |
| 
 | |
|       foreach ($definitions as $definition) {
 | |
| 
 | |
|               // Split the URL from the size definition ( eg 800w )
 | |
|               $parts = preg_split('/\s+/', trim($definition));
 | |
|               $image_url = $parts[0];
 | |
| 
 | |
|               // The space if not set is required, otherwise it will not work.
 | |
|               $image_condition = isset($parts[1]) ? ' ' . $parts[1] : ' ';
 | |
| 
 | |
|               // A source that starts with data:, will not need processing.
 | |
|               if (strpos($image_url, 'data:') === 0)
 | |
|               {
 | |
|                 continue;
 | |
|               }
 | |
| 
 | |
|               $fsFile = $fs->getFile($image_url);
 | |
|               $extension = $fsFile->getExtension(); // trigger setFileinfo, which will resolve URL -> Path
 | |
|               $mime = $fsFile->getMime();
 | |
| 
 | |
|               // Can happen when file is virtual, or other cases. Just assume this type.
 | |
|               if ($mime === false)
 | |
|               {
 | |
|                  $mime = 'image/' .  $extension;
 | |
|               }
 | |
| 
 | |
|               $fileWebp = $fs->getFile($imageBase . $fsFile->getFileBase() . '.webp');
 | |
|               $fileWebpCompat = $fs->getFile($imageBase . $fsFile->getFileName() . '.webp');
 | |
| 
 | |
|               // The URL of the image without the filename
 | |
|               $image_url_base = str_replace($fsFile->getFileName(), '', $image_url);
 | |
| 
 | |
|               $files = array($fileWebp, $fileWebpCompat);
 | |
| 
 | |
|               $fileAvif = $fs->getFile($imageBase . $fsFile->getFileBase() . '.avif');
 | |
| 
 | |
|               $lastwebp = false;
 | |
| 
 | |
|               foreach($files as $index => $thisfile)
 | |
|               {
 | |
|                 if (! $thisfile->exists())
 | |
|                 {
 | |
|                   // FILTER: boolean, object, string, filedir
 | |
|                   $thisfile = $fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $thisfile, $image_url, $imageBase);
 | |
|                 }
 | |
| 
 | |
|                 if ($thisfile !== false)
 | |
|                 {
 | |
|                     // base url + found filename + optional condition ( in case of sourceset, as in 1400w or similar)
 | |
|                     $webpCount++;
 | |
| 
 | |
|                      $lastwebp = $image_url_base . $thisfile->getFileName() . $image_condition;
 | |
|                      $srcsetWebP[] = $lastwebp;
 | |
|                      break;
 | |
|                 }
 | |
|                 elseif ($index+1 !== count($files)) // Don't write the else on the first file, because then the srcset will be written twice ( if file exists on the first fails)
 | |
|                 {
 | |
|                   continue;
 | |
|                 }
 | |
|                 else {
 | |
|                     $lastwebp = $definition;
 | |
|                     $srcsetWebP[] = $lastwebp;
 | |
|                 }
 | |
|               }
 | |
| 
 | |
|               if (false === $fileAvif->exists())
 | |
|               {
 | |
|                 $fileAvif = apply_filters('shortpixel/front/webp_notfound', false, $fileAvif, $image_url, $imageBase);
 | |
|               }
 | |
| 
 | |
|               if ($fileAvif !== false)
 | |
|               {
 | |
|                  $srcsetAvif[] = $image_url_base . $fileAvif->getFileName() . $image_condition;
 | |
|                  $avifCount++;
 | |
|               }
 | |
|               else { //fallback to jpg
 | |
|                 if (false !== $lastwebp) // fallback to webp if there is a variant in this run. or jpg if none
 | |
|                 {
 | |
|                    $srcsetAvif[] = $lastwebp;
 | |
|                 }
 | |
|                 else {
 | |
|                   $srcsetAvif[] = $definition;
 | |
|                 }
 | |
|               }
 | |
|       }
 | |
| 
 | |
|       if ($webpCount == 0 && $avifCount == 0) {
 | |
|           return $raw_image;
 | |
|       }
 | |
| 
 | |
|       $args = array();
 | |
| 
 | |
|       if ($webpCount > 0)
 | |
|         $args['webp'] = $srcsetWebP;
 | |
| 
 | |
|       if ($avifCount > 0)
 | |
|         $args['avif']  = $srcsetAvif;
 | |
| 
 | |
|       $output = $image->parseReplacement($args);
 | |
| 
 | |
|       return $output;
 | |
| 
 | |
|   }
 | |
| 
 | |
|   protected function testInlineStyle($content)
 | |
|   {
 | |
|     //preg_match_all('/background.*[^:](url\(.*\))[;]/isU', $content, $matches);
 | |
|     preg_match_all('/url\(.*\)/isU', $content, $matches);
 | |
| 
 | |
|     if (count($matches) == 0)
 | |
|       return $content;
 | |
| 
 | |
|     $content = $this->convertInlineStyle($matches, $content);
 | |
|     return $content;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /** Function to convert inline CSS backgrounds to webp
 | |
|   * @param $match Regex match for inline style
 | |
|   * @return String Replaced (or not) content for webp.
 | |
|   * @author Bas Schuiling
 | |
|   */
 | |
|   protected function convertInlineStyle($matches, $content)
 | |
|   {
 | |
| 
 | |
|     $fs = \wpSPIO()->filesystem();
 | |
|     $allowed_exts = array('jpg', 'jpeg', 'gif', 'png');
 | |
|     $converted = array();
 | |
| 
 | |
|     for($i = 0; $i < count($matches[0]); $i++)
 | |
|     {
 | |
|       $item = $matches[0][$i];
 | |
| 
 | |
|       preg_match('/url\(\'(.*)\'\)/imU', $item, $match);
 | |
|       if (! isset($match[1]))
 | |
|         continue;
 | |
| 
 | |
|       $url = $match[1];
 | |
|       //$parsed_url = parse_url($url);
 | |
|       $filename = basename($url);
 | |
| 
 | |
|       $fileonly = pathinfo($url, PATHINFO_FILENAME);
 | |
|       $ext = pathinfo($url, PATHINFO_EXTENSION);
 | |
| 
 | |
|       if (! in_array($ext, $allowed_exts))
 | |
|         continue;
 | |
| 
 | |
|       $image_base_url = str_replace($filename, '', $url);
 | |
|       $fsFile = $fs->getFile($url);
 | |
|       $dir = $fsFile->getFileDir();
 | |
|       $imageBase = is_object($dir) ? $dir->getPath() : false;
 | |
| 
 | |
|       if (false === $imageBase) // returns false if URL is external, do nothing with that.
 | |
|         continue;
 | |
| 
 | |
|       $checkedFile = false;
 | |
|       $fileWebp = $fs->getFile($imageBase . $fsFile->getFileBase() . '.webp');
 | |
|       $fileWebpCompat = $fs->getFile($imageBase . $fsFile->getFileName() . '.webp');
 | |
| 
 | |
|       if (true === $fileWebp->exists())
 | |
|       {
 | |
|         $checkedFile = $image_base_url . $fsFile->getFileBase()  . '.webp';
 | |
|       }
 | |
|       elseif (true === $fileWebpCompat->exists())
 | |
|       {
 | |
|         $checkedFile = $image_base_url . $fsFile->getFileName() . '.webp';
 | |
|       }
 | |
|       else
 | |
|       {
 | |
|         $fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $fileWebp, $url, $imageBase);
 | |
|         if (false !== $fileWebp_exists)
 | |
|         {
 | |
|            $checkedFile = $image_base_url . $fsFile->getFileBase()  . '.webp';
 | |
|         }
 | |
|         else {
 | |
|           $fileWebp_exists = apply_filters('shortpixel/front/webp_notfound', false, $fileWebpCompat, $url, $imageBase);
 | |
|           if (false !== $fileWebp_exists)
 | |
|           {
 | |
|              $checkedFile = $image_base_url . $fsFile->getFileName()  . '.webp';
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if ($checkedFile)
 | |
|       {
 | |
|           // if webp, then add another URL() def after the targeted one.  (str_replace old full URL def, with new one on main match?
 | |
|           $target_urldef = $matches[0][$i];
 | |
|           if (! isset($converted[$target_urldef])) // if the same image is on multiple elements, this replace might go double. prevent.
 | |
|           {
 | |
|             $converted[] = $target_urldef;
 | |
|             $new_urldef = "url('" . $checkedFile . "'), " . $target_urldef;
 | |
|             $content = str_replace($target_urldef, $new_urldef, $content);
 | |
|           }
 | |
|       }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     return $content;
 | |
|   }
 | |
| 
 | |
| } // class
 |