Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.24% covered (success)
93.24%
69 / 74
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageUrlReplacer
93.24% covered (success)
93.24%
69 / 74
91.67% covered (success)
91.67%
11 / 12
38.45
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replaceUrl
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 replaceUrlOr
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 handleSrc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handleSrcSet
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 looksLikeSrcSet
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 handleAttribute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 attributeFilter
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 processCSSRegExCallback
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 processCSS
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 replaceHtml
82.14% covered (warning)
82.14%
23 / 28
0.00% covered (danger)
0.00%
0 / 1
13.96
 replace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace DOMUtilForWebP;
4
5//use Sunra\PhpSimple\HtmlDomParser;
6use KubAT\PhpSimple\HtmlDomParser;
7
8/**
9 *  Highly configurable class for replacing image URLs in HTML (both src and srcset syntax)
10 *
11 *  Uses http://simplehtmldom.sourceforge.net/ - a library for easily manipulating HTML by means of a DOM.
12 *  The great thing about this library is that it supports working on invalid HTML and it only applies the changes you
13 *  make - very gently (however, not as gently as we do in PictureTags).
14 *  PS: The library is a bit old, so perhaps we should look for another.
15 *  ie https://packagist.org/packages/masterminds/html5 ??
16 *
17 *  Behaviour can be customized by overriding the public methods (replaceUrl, $searchInTags, etc)
18 *
19 *  Default behaviour:
20 *  - The modified URL is the same as the original, with ".webp" appended                   (replaceUrl)
21 *  - Limits to these tags: <img>, <source>, <input> and <iframe>                           ($searchInTags)
22 *  - Limits to these attributes: "src", "src-set" and any attribute starting with "data-"  (attributeFilter)
23 *  - Only replaces URLs that ends with "png", "jpg" or "jpeg" (no query strings either)    (replaceUrl)
24 *
25 *
26 */
27class ImageUrlReplacer
28{
29
30    // define tags to be searched.
31    // The div and li are on the list because these are often used with lazy loading
32    // should we add <meta> ?
33    // Probably not for open graph images or twitter
34    // so not these:
35    // - <meta property="og:image" content="[url]">
36    // - <meta property="og:image:secure_url" content="[url]">
37    // - <meta name="twitter:image" content="[url]">
38    // Meta can also be used in schema.org micro-formatting, ie:
39    // - <meta itemprop="image" content="[url]">
40    //
41    // How about preloaded images? - yes, suppose we should replace those
42    // - <link rel="prefetch" href="[url]">
43    // - <link rel="preload" as="image" href="[url]">
44    public static $searchInTags = ['img', 'source', 'input', 'iframe', 'div', 'li', 'link', 'a', 'section', 'video'];
45
46    /**
47     * Empty constructor for preventing child classes from creating constructors.
48     *
49     * We do this because otherwise the "new static()" call inside the ::replace() method
50     * would be unsafe. See #21
51     * @return  void
52     */
53    final public function __construct()
54    {
55    }
56
57    /**
58     *
59     * @return string|null webp url or, if URL should not be changed, return nothing
60     **/
61    public function replaceUrl($url)
62    {
63        if (!preg_match('#(png|jpe?g)$#', $url)) {
64            return null;
65        }
66        return $url . '.webp';
67    }
68
69    public function replaceUrlOr($url, $returnValueIfDenied)
70    {
71        $url = $this->replaceUrl($url);
72        return (isset($url) ? $url : $returnValueIfDenied);
73    }
74
75    /*
76    public function isValidUrl($url)
77    {
78        return preg_match('#(png|jpe?g)$#', $url);
79    }*/
80
81    public function handleSrc($attrValue)
82    {
83        return $this->replaceUrlOr($attrValue, $attrValue);
84    }
85
86    public function handleSrcSet($attrValue)
87    {
88        // $attrValue is ie: <img data-x="1.jpg 1000w, 2.jpg">
89        $srcsetArr = explode(',', $attrValue);
90        foreach ($srcsetArr as $i => $srcSetEntry) {
91            // $srcSetEntry is ie "image.jpg 520w", but can also lack width, ie just "image.jpg"
92            // it can also be ie "image.jpg 2x"
93            $srcSetEntry = trim($srcSetEntry);
94            $entryParts = preg_split('/\s+/', $srcSetEntry, 2);
95            if (count($entryParts) == 2) {
96                list($src, $descriptors) = $entryParts;
97            } else {
98                $src = $srcSetEntry;
99                $descriptors = null;
100            }
101
102            $webpUrl = $this->replaceUrlOr($src, false);
103            if ($webpUrl !== false) {
104                $srcsetArr[$i] = $webpUrl . (isset($descriptors) ? ' ' . $descriptors : '');
105            }
106        }
107        return implode(', ', $srcsetArr);
108    }
109
110    /**
111     *  Test if attribute value looks like it has srcset syntax.
112     *  "image.jpg 100w" does for example. And "image.jpg 1x". Also "image1.jpg, image2.jpg 1x"
113     *  Mixing x and w is invalid (according to
114     *         https://stackoverflow.com/questions/26928828/html5-srcset-mixing-x-and-w-syntax)
115     *  But we accept it anyway
116     *  It is not the job of this function to see if the first part is an image URL
117     *  That will be done in handleSrcSet.
118     *
119     */
120    public function looksLikeSrcSet($value)
121    {
122        if (preg_match('#\s\d*(w|x)#', $value)) {
123            return true;
124        }
125        return false;
126    }
127
128    public function handleAttribute($value)
129    {
130        if (self::looksLikeSrcSet($value)) {
131            return self::handleSrcSet($value);
132        }
133        return self::handleSrc($value);
134    }
135
136    public function attributeFilter($attrName)
137    {
138        $attrName = strtolower($attrName);
139        if (($attrName == 'src') || ($attrName == 'srcset') || (strpos($attrName, 'data-') === 0)) {
140            return true;
141        }
142        return false;
143    }
144
145    public function processCSSRegExCallback($matches)
146    {
147        list($all, $pre, $quote, $url, $post) = $matches;
148        return $pre . $this->replaceUrlOr($url, $url) . $post;
149    }
150
151    public function processCSS($css)
152    {
153        $declarations = explode(';', $css);
154        foreach ($declarations as $i => &$declaration) {
155            if (preg_match('#(background(-image)?)\\s*:#', $declaration)) {
156                // https://regexr.com/46qdg
157                //$regex = '#(url\s*\(([\"\']?))([^\'\";\)]*)(\2\s*\))#';
158                $parts = explode(',', $declaration);
159                //print_r($parts);
160                foreach ($parts as &$part) {
161                    //echo 'part:' . $part . "\n";
162                    $regex = '#(url\\s*\\(([\\"\\\']?))([^\\\'\\";\\)]*)(\\2\\s*\\))#';
163                    $part = preg_replace_callback(
164                        $regex,
165                        '\DOMUtilForWebP\ImageUrlReplacer::processCSSRegExCallback',
166                        $part
167                    );
168                    //echo 'result:' . $part . "\n";
169                }
170                $declarations[$i] = implode(',', $parts);
171            }
172        }
173        return implode(';', $declarations);
174    }
175
176    public function replaceHtml($html)
177    {
178        if ($html == '') {
179            return '';
180        }
181
182        // https://stackoverflow.com/questions/4812691/preserve-line-breaks-simple-html-dom-parser
183
184        // function str_get_html($str, $lowercase=true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET,
185        //    $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT, $defaultSpanText=DEFAULT_SPAN_TEXT)
186
187        $dom = HtmlDomParser::str_get_html($html, false, true, 'UTF-8', false);
188        //$dom = str_get_html($html, false, false, 'UTF-8', false);
189
190
191        // MAX_FILE_SIZE is defined in simple_html_dom.
192        // For safety sake, we make sure it is defined before using
193        defined('MAX_FILE_SIZE') || define('MAX_FILE_SIZE', 600000);
194
195        if ($dom === false) {
196            if (strlen($html) > MAX_FILE_SIZE) {
197                return '<!-- Alter HTML was skipped because the HTML is too big to process! ' .
198                    '(limit is set to ' . MAX_FILE_SIZE . ' bytes) -->' . "\n" . $html;
199            }
200            return '<!-- Alter HTML was skipped because the helper library refused to process the html -->' .
201                "\n" . $html;
202        }
203
204        // Replace attributes (src, srcset, data-src, etc)
205        foreach (self::$searchInTags as $tagName) {
206            $elems = $dom->find($tagName);
207            foreach ($elems as $index => $elem) {
208                $attributes = $elem->getAllAttributes();
209                foreach ($elem->getAllAttributes() as $attrName => $attrValue) {
210                    if ($this->attributeFilter($attrName)) {
211                        $elem->setAttribute($attrName, $this->handleAttribute($attrValue));
212                    }
213                }
214            }
215        }
216
217        // Replace <style> elements
218        $elems = $dom->find('style');
219        foreach ($elems as $index => $elem) {
220            $css = $this->processCSS($elem->innertext);
221            if ($css != $elem->innertext) {
222                $elem->innertext = $css;
223            }
224        }
225
226        // Replace "style attributes
227        $elems = $dom->find('*[style]');
228        foreach ($elems as $index => $elem) {
229            $css = $this->processCSS($elem->style);
230            if ($css != $elem->style) {
231                $elem->style = $css;
232            }
233        }
234
235        return $dom->save();
236    }
237
238    /* Main replacer function */
239    public static function replace($html)
240    {
241        /*if (!function_exists('str_get_html')) {
242            require_once __DIR__ . '/../src-vendor/simple_html_dom/simple_html_dom.inc';
243        }*/
244        $iur = new static();
245        return $iur->replaceHtml($html);
246    }
247}