From 11261e0dbc460322856fbec8c466c1203bba1108 Mon Sep 17 00:00:00 2001 From: gthomas2 Date: Sun, 24 Dec 2017 00:42:04 +0000 Subject: [PATCH] Issue #7: Security fix using sub dir in original file area --- .gitignore | 1 + amd/build/appear.min.js | 2 +- amd/build/imageopt.min.js | 2 +- amd/src/appear.js | 1 + amd/src/imageopt.js | 23 ++- classes/componentsupport/base_component.php | 51 ++++++ classes/componentsupport/question.php | 79 ++++++++ classes/image.php | 88 +++++---- classes/local.php | 189 ++++++++++++++++++++ classes/test_util.php | 51 ------ filter.php | 174 ++++++++++-------- lang/en/filter_imageopt.php | 2 +- lib.php | 79 +++++--- settings.php | 4 +- tests/behat/behat_filter_imageopt.php | 178 ++++++++++++++++++ tests/behat/image_processing.feature | 63 +++++++ tests/filter_test.php | 74 +++++--- tests/fixtures/testpng_400x250.png | Bin 0 -> 2301 bytes version.php | 4 +- 19 files changed, 843 insertions(+), 222 deletions(-) create mode 100644 .gitignore create mode 100644 classes/componentsupport/base_component.php create mode 100644 classes/componentsupport/question.php create mode 100644 classes/local.php delete mode 100644 classes/test_util.php create mode 100644 tests/behat/behat_filter_imageopt.php create mode 100644 tests/behat/image_processing.feature create mode 100644 tests/fixtures/testpng_400x250.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23ce61a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.DS_store diff --git a/amd/build/appear.min.js b/amd/build/appear.min.js index f24ffaf..b73bf1f 100644 --- a/amd/build/appear.min.js +++ b/amd/build/appear.min.js @@ -1 +1 @@ -define(["jquery"],function(a){return function(a){function b(b){return a(b).filter(function(){return a(this).is(":appeared")})}function c(){g=!1;for(var a=0,c=e.length;a=e&&h-k<=e+j.height()&&g+c.width()+l>=d&&g-l<=d+j.width()},a.fn.extend({appear:function(b){i=a.extend({},h,b||{});var e=this.selector||this;if(!f){var j=function(){g||(g=!0,setTimeout(c,i.interval))};a(window).scroll(j).resize(j),f=!0}return i.force_process&&setTimeout(c,i.interval),d(e),a(e)}}),a.extend({force_appear:function(){return!!f&&(c(),!0)}})}(function(){return"undefined"!=typeof module?require("jquery"):a}()),a}); \ No newline at end of file +define(["jquery"],function(a){var b=a;return b(),function(a){function b(b){return a(b).filter(function(){return a(this).is(":appeared")})}function c(){g=!1;for(var a=0,c=e.length;a=e&&h-k<=e+j.height()&&g+c.width()+l>=d&&g-l<=d+j.width()},a.fn.extend({appear:function(b){i=a.extend({},h,b||{});var e=this.selector||this;if(!f){var j=function(){g||(g=!0,setTimeout(c,i.interval))};a(window).scroll(j).resize(j),f=!0}return i.force_process&&setTimeout(c,i.interval),d(e),a(e)}}),a.extend({force_appear:function(){return!!f&&(c(),!0)}})}(function(){return"undefined"!=typeof module?require("jquery"):a}()),a}); \ No newline at end of file diff --git a/amd/build/imageopt.min.js b/amd/build/imageopt.min.js index 923c4e9..cf3c342 100644 --- a/amd/build/imageopt.min.js +++ b/amd/build/imageopt.min.js @@ -1 +1 @@ -define(["filter_imageopt/appear"],function(a){return{init:function(){a(document).ready(function(){a(document.body).on("appear","img[data-loadonvisible]",function(b,c){c.each(function(){var b=a(this).data("loadonvisible");a(this).attr("src",b),a(this).removeAttr("data-loadonvisible"),a(this).addClass("imageopt_loading"),a(this).on("load",function(){a(this).removeClass("imageopt_loading")})})});var b={appeartopoffset:100,appearleftoffset:100};a("img[data-loadonvisible]").appear(b),a.force_appear(),function(){var b=location.hash;a(window).on("popstate hashchange",function(){var c=location.hash;c!==b&&window.setTimeout(function(){a.force_appear()},200),b=c})}()})}}}); \ No newline at end of file +define(["filter_imageopt/appear"],function(a){return{init:function(){var b=function(b,c){a(b).attr("src",c),a(b).removeAttr("data-loadonvisible"),a(b).addClass("imageopt_loading"),a(b).on("load",function(){a(b).removeClass("imageopt_loading")})};a(document).ready(function(){a(document.body).on("appear","img[data-loadonvisible]",function(c,d){d.each(function(){var c=a(this).data("loadonvisible");b(this,c)})});var c={appeartopoffset:100,appearleftoffset:100};a("img[data-loadonvisible]").appear(c),a.force_appear(),function(){var b=location.hash;a(window).on("popstate hashchange",function(){var c=location.hash;c!==b&&window.setTimeout(function(){a.force_appear()},200),b=c})}()})}}}); \ No newline at end of file diff --git a/amd/src/appear.js b/amd/src/appear.js index dcbaf91..16f912f 100644 --- a/amd/src/appear.js +++ b/amd/src/appear.js @@ -2,6 +2,7 @@ define(['jquery'], function(jQuery) { var $ = jQuery; + $(); // This is here to stop linter moaning about unuse. /* * jQuery appear plugin diff --git a/amd/src/imageopt.js b/amd/src/imageopt.js index 0dcb896..241a34c 100644 --- a/amd/src/imageopt.js +++ b/amd/src/imageopt.js @@ -27,16 +27,27 @@ define(['filter_imageopt/appear'], function($) { return { init:function() { + + /** + * Load optimised image. + * @param {Element} el + * @param {string} imgurl + * @returns {void} + */ + var loadOptimisedImg = function(el, imgurl) { + $(el).attr('src', imgurl); + $(el).removeAttr('data-loadonvisible'); + $(el).addClass('imageopt_loading'); + $(el).on('load', function() { + $(el).removeClass('imageopt_loading'); + }); + }; + $(document).ready(function() { $(document.body).on('appear', 'img[data-loadonvisible]', function(e, appeared) { appeared.each(function() { var imgurl = $(this).data('loadonvisible'); - $(this).attr('src', imgurl); - $(this).removeAttr('data-loadonvisible'); - $(this).addClass('imageopt_loading'); - $(this).on('load', function() { - $(this).removeClass('imageopt_loading'); - }); + loadOptimisedImg(this, imgurl); }); }); // Appear configuration - start loading images when they are out of the view port by 400px. diff --git a/classes/componentsupport/base_component.php b/classes/componentsupport/base_component.php new file mode 100644 index 0000000..6a998ba --- /dev/null +++ b/classes/componentsupport/base_component.php @@ -0,0 +1,51 @@ +. +namespace filter_imageopt\componentsupport; + +defined('MOODLE_INTERNAL') || die; + +abstract class base_component { + /** + * Get the image file for the specified file path components. + * @param array $pathcomponents - path split by /. + * @return \stored_file | null + */ + public static function get_img_file(array $pathcomponents) { + return null; + } + + /** + * Get the optimised path for specified file path. + * @param array $pathcomponents - path split by /. + * @param int $maxwidth + * @return string | null; + */ + public static function get_optimised_path(array $pathcomponents, $maxwidth) { + return null; + } + + /** + * Return the optimised url for the specfied file and original src. + * @param \filter_imageopt\componentsupport\stored_file $file + * @param type $originalsrc + * @return \moodle_url + */ + public static function get_optimised_src(\stored_file $file, $originalsrc) { + return null; + } +} + + diff --git a/classes/componentsupport/question.php b/classes/componentsupport/question.php new file mode 100644 index 0000000..dd79d60 --- /dev/null +++ b/classes/componentsupport/question.php @@ -0,0 +1,79 @@ +. + +/** + * Image optimiser support for question component. + * @package filter_imageopt + * @author Guy Thomas + * @copyright Copyright (c) 2018 Guy Thomas. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace filter_imageopt\componentsupport; + +defined('MOODLE_INTERNAL') || die; + +use filter_imageopt\local; + +class question extends base_component { + + public static function get_img_file(array $pathcomponents) { + if ($pathcomponents[1] !== 'question') { + throw new \coding_exception('Component is not a question ('.$pathcomponents[2].')'); + } + if (count($pathcomponents) === 7) { + array_splice($pathcomponents, 3, 2); + } else { + return null; + } + + $path = '/'.implode('/', $pathcomponents); + + $fs = get_file_storage(); + return $fs->get_file_by_hash(sha1($path)); + } + + public static function get_optimised_path(array $pathcomponents, $maxwidth) { + if ($pathcomponents[1] !== 'question') { + throw new \coding_exception('Component is not a question ('.$pathcomponents[2].')'); + } + if (count($pathcomponents) === 7) { + array_splice($pathcomponents, 3, 2); + } else { + return null; + } + $pathcomponents[count($pathcomponents)-1] = 'imageopt/'.$maxwidth.'/'.$pathcomponents[count($pathcomponents)-1]; + $optimisedpath = implode('/', $pathcomponents); + if (substr($optimisedpath, 0, 1) !== '/') { + $optimisedpath = '/'.$optimisedpath; + } + return $optimisedpath; + } + + public static function get_optimised_src(\stored_file $file, $originalsrc) { + global $CFG; + + $maxwidth = get_config('filter_imageopt', 'maxwidth'); + + $urlpath = local::get_img_path_from_src($originalsrc); + $urlpathcomponents = local::explode_img_path($urlpath); + + array_splice($urlpathcomponents, 6, 0, ['imageopt', $maxwidth]); + + $opturl = new \moodle_url($CFG->wwwroot.'/pluginfile.php/'.implode('/', $urlpathcomponents)); + + return $opturl; + } +} \ No newline at end of file diff --git a/classes/image.php b/classes/image.php index 187cc93..0ae5b36 100644 --- a/classes/image.php +++ b/classes/image.php @@ -58,18 +58,27 @@ class image { * Creates a resized version of image and stores copy in file area * * @param stored_file $originalfile + * @param string $resizefilepath + * @param bool | string $resizefilename * @param int $newwidth; * @param int $newheight; * @return stored_file */ public static function resize ( stored_file $originalfile, + $resizefilepath, $resizefilename = false, $newwidth = false, $newheight = false, $jpgquality = 90 ) { + raise_memory_limit(MEMORY_EXTRA); + + if (substr($resizefilepath, -1) !== '/') { + $resizefilepath .= '/'; + } + if ($resizefilename === false) { $resizefilename = $originalfile->get_filename(); } @@ -91,7 +100,7 @@ public static function resize ( // Create temporary image for processing. $tmpimage = tempnam(sys_get_temp_dir(), 'tmpimg'); - \file_put_contents($tmpimage, $originalfile->get_content()); + file_put_contents($tmpimage, $originalfile->get_content()); if (!$newheight) { $m = $imageinfo->height / $imageinfo->width; // Multiplier to work out $newheight. @@ -103,47 +112,47 @@ public static function resize ( $t = null; switch ($imageinfo->mimetype) { case 'image/gif': - if (\function_exists('imagecreatefromgif')) { - $im = \imagecreatefromgif($tmpimage); + if (function_exists('imagecreatefromgif')) { + $im = imagecreatefromgif($tmpimage); } else { - \debugging('GIF not supported on this server'); + debugging('GIF not supported on this server'); unlink ($tmpimage); return false; } // Guess transparent colour from GIF. - $transparent = \imagecolortransparent($im); + $transparent = imagecolortransparent($im); if ($transparent != -1) { - $t = \imagecolorsforindex($im, $transparent); + $t = imagecolorsforindex($im, $transparent); } break; case 'image/jpeg': - if (\function_exists('imagecreatefromjpeg')) { - $im = \imagecreatefromjpeg($tmpimage); + if (function_exists('imagecreatefromjpeg')) { + $im = imagecreatefromjpeg($tmpimage); } else { - \debugging('JPEG not supported on this server'); + debugging('JPEG not supported on this server'); unlink ($tmpimage); return false; } // If the user uploads a jpeg them we should process as a jpeg if possible. - if (\function_exists('imagejpeg')) { + if (function_exists('imagejpeg')) { $imagefnc = 'imagejpeg'; $filters = null; // Not used. $quality = $jpgquality; - } else if (\function_exists('imagepng')) { + } else if (function_exists('imagepng')) { $imagefnc = 'imagepng'; $filters = PNG_NO_FILTER; $quality = 1; } else { - \debugging('Jpeg and png not supported on this server, please fix server configuration'); + debugging('Jpeg and png not supported on this server, please fix server configuration'); unlink ($tmpimage); return false; } break; case 'image/png': - if (\function_exists('imagecreatefrompng')) { - $im = \imagecreatefrompng($tmpimage); + if (function_exists('imagecreatefrompng')) { + $im = imagecreatefrompng($tmpimage); } else { - \debugging('PNG not supported on this server'); + debugging('PNG not supported on this server'); unlink ($tmpimage); return false; } @@ -156,59 +165,70 @@ public static function resize ( // The default for all images other than jpegs is to try imagepng first. if (empty($imagefnc)) { - if (\function_exists('imagepng')) { + if (function_exists('imagepng')) { $imagefnc = 'imagepng'; $filters = PNG_NO_FILTER; $quality = 1; - } else if (\function_exists('imagejpeg')) { + } else if (function_exists('imagejpeg')) { $imagefnc = 'imagejpeg'; $filters = null; // Not used. $quality = $jpgquality; } else { - \debugging('Jpeg and png not supported on this server, please fix server configuration'); + debugging('Jpeg and png not supported on this server, please fix server configuration'); return false; } } - if (\function_exists('imagecreatetruecolor')) { - $newimage = \imagecreatetruecolor($newwidth, $newheight); + if (function_exists('imagecreatetruecolor')) { + $newimage = imagecreatetruecolor($newwidth, $newheight); if ($imageinfo->mimetype != 'image/jpeg' and $imagefnc === 'imagepng') { if ($t) { // Transparent GIF hacking... - $transparentcolour = \imagecolorallocate($newimage , $t['red'] , $t['green'] , $t['blue']); - \imagecolortransparent($newimage , $transparentcolour); + $transparentcolour = imagecolorallocate($newimage , $t['red'] , $t['green'] , $t['blue']); + imagecolortransparent($newimage , $transparentcolour); } - \imagealphablending($newimage, false); - $color = \imagecolorallocatealpha($newimage, 0, 0, 0, 127); - \imagefill($newimage, 0, 0, $color); - \imagesavealpha($newimage, true); + imagealphablending($newimage, false); + $color = imagecolorallocatealpha($newimage, 0, 0, 0, 127); + imagefill($newimage, 0, 0, $color); + imagesavealpha($newimage, true); } } else { - $newimage = \imagecreate($newwidth, $newheight); + $newimage = imagecreate($newwidth, $newheight); } - \imagecopybicubic($newimage, $im, 0, 0, 0, 0, $newwidth, $newheight, $imageinfo->width, $imageinfo->height); + imagecopybicubic($newimage, $im, 0, 0, 0, 0, $newwidth, $newheight, $imageinfo->width, $imageinfo->height); - $fs = \get_file_storage(); + $fs = get_file_storage(); $newimageparams = array( 'contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid, - 'filepath' => '/' + 'filepath' => $resizefilepath ); - \ob_start(); + $dirs = explode('/', $resizefilepath); + $dirpath = '/'; + + foreach ($dirs as $dir) { + if (empty($dir)) { + continue; + } + $dirpath .= $dir.'/'; + $fs->create_directory($contextid, $component, $filearea, $itemid, $dirpath); + } + + ob_start(); if (!$imagefnc($newimage, null, $quality, $filters)) { return false; } - $data = \ob_get_clean(); - \imagedestroy($newimage); + $data = ob_get_clean(); + imagedestroy($newimage); $newimageparams['filename'] = $resizefilename; - if ($resizefilename == $originalfile->get_filename()) { + if ($resizefilename === $originalfile->get_filename() && $resizefilepath === $originalfile->get_filepath()) { $originalfile->delete(); } $file1 = $fs->create_file_from_string($newimageparams, $data); diff --git a/classes/local.php b/classes/local.php new file mode 100644 index 0000000..b82e2ec --- /dev/null +++ b/classes/local.php @@ -0,0 +1,189 @@ +. + +/** + * Local class for local people (we'll have no trouble here). + * + * @package filter_imageopt + * @author Guy Thomas + * @copyright Copyright (c) 2017 Guy Thomas. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace filter_imageopt; + +defined('MOODLE_INTERNAL') || die(); + +use stored_file; + +class local { + + const REGEXP_IMGSRC = '/]*(src=["|\']((?:.*)(pluginfile.php(?:.*)))["|\'])(?:.*)>/isU'; + + const REGEXP_SRC = '/(?:.*)(pluginfile.php(?:.*))/'; + + /** + * Get the optimised path for a file path - this is the path that get's written to the db as a hash. + * @param string $filepath + * @param bool $asfilepath - if true will return the path for use with the file storage system, not urls. + * @return string + */ + public static function get_optimised_path($filepath, $asfilepath = true) { + $maxwidth = get_config('filter_imageopt', 'maxwidth'); + if (empty($maxwidth)) { + $maxwidth = 480; + } + + $pathcomps = self::explode_img_path($filepath); + self::url_decode_path_components($pathcomps); + if (count($pathcomps) > 5 && $asfilepath) { + $component = $pathcomps[1]; + + // See if we have component support for this component. + $classname = '\\filter_imageopt\\componentsupport\\'.$component; + if (class_exists($classname) && method_exists($classname, 'get_optimised_path')) { + $optimisedpath = $classname::get_optimised_path($pathcomps, $maxwidth); + if ($optimisedpath !== null) { + return $optimisedpath; + } + } + } + + $pathcomps[count($pathcomps)-1] = 'imageopt/'.$maxwidth.'/'.$pathcomps[count($pathcomps)-1]; + $optimisedpath = implode('/', $pathcomps); + if (substr($optimisedpath, 0, 1) !== '/') { + $optimisedpath = '/'.$optimisedpath; + } + return $optimisedpath; + } + + /** + * Get optimised src. + * @param stored_file $file + * @param string $originalsrc + * @param string $optimisedpath + * @return string + */ + public static function get_optimised_src(\stored_file $file, $originalsrc, $optimisedpath) { + global $CFG; + $classname = '\\filter_imageopt\\componentsupport\\'.$file->get_component(); + $optimisedsrc = null; + if (class_exists($classname) && method_exists($classname, 'get_optimised_src')) { + $optimisedsrc = $classname::get_optimised_src($file, $originalsrc); + } + if (empty($optimisedsrc)) { + $optimisedsrc = new \moodle_url($CFG->wwwroot.'/pluginfile.php'.$optimisedpath); + } + $optimisedsrc = $optimisedsrc->out(); + return $optimisedsrc; + } + + /** + * Gets an img path from image src attribute. + * @param type string $src + * @return array + */ + public static function get_img_path_from_src($src) { + $matches = []; + + preg_match(self::REGEXP_SRC, $src, $matches); + + return $matches[1]; + } + + /** + * Explode an image path. + * @param string $pluginfilepath + * @return array + */ + public static function explode_img_path($pluginfilepath) { + $tmparr = explode('/', $pluginfilepath); + if ($tmparr[0] === 'pluginfile.php') { + array_splice($tmparr, 0, 1); + } else if ($tmparr[0] === '') { + array_splice($tmparr, 0, 1); + } + return $tmparr; + } + + /** + * URL decode each component of a path. + * @param array $pathcomponents + */ + public static function url_decode_path_components(array &$pathcomponents) { + array_walk($pathcomponents, function(&$item, $key) { + $item = urldecode($item); + }); + } + + /** + * URL decode file path. + * @param string $pluginfilepath + * @return string + */ + public static function url_decode_path($pluginfilepath) { + $tmparr = self::explode_img_path($pluginfilepath); + self::url_decode_path($tmparr); + return implode('/', $tmparr); + } + + /** + * Get's an image file from the plugin file path. + * + * @param str $pluginfilepath pluginfile.php/ + * @return \stored_file + */ + public static function get_img_file($pluginfilepath) { + + $fs = get_file_storage(); + + $pathcomps = self::explode_img_path($pluginfilepath); + self::url_decode_path_components($pathcomps); + + if (count($pathcomps) > 5) { + $component = $pathcomps[1]; + + // See if we have component support for this component. + $classname = '\\filter_imageopt\\componentsupport\\'.$component; + if (class_exists($classname) && method_exists($classname, 'get_img_file')) { + $file = $classname::get_img_file($pathcomps); + if ($file instanceof stored_file) { + return $file; + } + } + } + + // If no item id then put one in. + if (!is_number($pathcomps[3])) { + array_splice($pathcomps, 3, 0, [0]); + } + + $path = '/'.implode('/', $pathcomps); + + $file = $fs->get_file_by_hash(sha1($path)); + + return $file; + } + + public static function file_pluginfile($relativepath) { + $forcedownload = optional_param('forcedownload', 0, PARAM_BOOL); + $preview = optional_param('preview', null, PARAM_ALPHANUM); + // Offline means download the file from the repository and serve it, even if it was an external link. + // The repository may have to export the file to an offline format. + $offline = optional_param('offline', 0, PARAM_BOOL); + $embed = optional_param('embed', 0, PARAM_BOOL); + file_pluginfile($relativepath, $forcedownload, $preview, $offline, $embed); + } +} \ No newline at end of file diff --git a/classes/test_util.php b/classes/test_util.php deleted file mode 100644 index a3998e9..0000000 --- a/classes/test_util.php +++ /dev/null @@ -1,51 +0,0 @@ -. - -/** - * Testing utility. - * Allows for private / protected properties to be called against an object for php unit testing purposes. - * @package filter_imageopt - * @author Guy Thomas - * @copyright Copyright (c) 2017 Blackboard Inc. - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace filter_imageopt; - -defined('MOODLE_INTERNAL') || die(); - -/** - * Testing utility class. - * @package filter_imageopt - * @author Guy Thomas - * @copyright Copyright (c) 2017 Blackboard Inc. - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class test_util { - - /** - * Call a private / protected method against an object. - * @param string|mixed $object - * @param string $method - * @param array $args - * @return mixed - */ - public static function call_restricted_method($object, $method, array $args) { - $method = new \ReflectionMethod($object, $method); - $method->setAccessible(true); - return $method->invokeArgs($object, $args); - } -} diff --git a/filter.php b/filter.php index 1a35579..f174ac1 100644 --- a/filter.php +++ b/filter.php @@ -17,7 +17,7 @@ /** * Image optimiser * @package filter_imageopt - * @author Guy Thomas + * @author Guy Thomas * @copyright Copyright (c) Guy Thomas. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die(); use filter_imageopt\image; +use filter_imageopt\local; /** * Image optimiser - main filter class. @@ -40,11 +41,6 @@ class filter_imageopt extends moodle_text_filter { */ private $config; - /** - * Regex to extract and process img. - */ - const REGEXP_IMGSRC = '/]*(src=["|\']((?:.*)(pluginfile.php(?:.*)))["|\'])(?:.*)>/isU'; - public function __construct(context $context, array $localconfig) { global $CFG; @@ -55,35 +51,11 @@ public function __construct(context $context, array $localconfig) { $this->config->widthattribute = image::WIDTHATTPRSERVELTMAX; } $this->config->widthattribute = intval($this->config->widthattribute); - - parent::__construct($context, $localconfig); - } - - /** - * Get's an image file from the plugin file path. - * - * @param str $pluginfilepath pluginfile.php/ - * @return bool|stored_file - */ - private function get_img_file($pluginfilepath) { - $tmparr = explode('/', $pluginfilepath); - - $contextid = urldecode($tmparr[1]); - $component = urldecode($tmparr[2]); - - if (count($tmparr) === 5) { - $area = urldecode($tmparr[3]); - $item = 0; - $filename = urldecode($tmparr[4]); - } else if (count($tmparr) === 6) { - $area = urldecode($tmparr[3]); - $item = urldecode($tmparr[4]); - $filename = urldecode($tmparr[5]); + if (!isset($this->config->maxwidth)) { + $this->config->maxwidth = 480; } - $fs = get_file_storage(); - $file = $fs->get_file($contextid, $component, $area, $item, '/', $filename); - return $file; + parent::__construct($context, $localconfig); } private function empty_image($width, $height) { @@ -99,25 +71,6 @@ private function empty_image($width, $height) { return $svg; } - /** - * Create image optimised url for image file. - * @param stored_file $file original file - * @return moodle_url - */ - private function imageopturl($file) { - global $CFG; - $maxwidth = $this->config->maxwidth; - $filename = $file->get_filename(); - $contextid = $file->get_contextid(); - $component = $file->get_component(); - $area = $file->get_filearea(); - $item = $file->get_itemid(); - return new moodle_url( - $CFG->wwwroot.'/pluginfile.php/'.$contextid.'/filter_imageopt/'.$area.'/'.$item.'/'.$component.'/'.$maxwidth. - '/'.$filename - ); - } - /** * Add width and height to img tag and return modified tag with width and height * @param string $img @@ -169,12 +122,87 @@ private function img_add_width_height($img, $width, $height) { return $img; } + /** + * Create image optimiser url - will take an original file and resize it then forward on. + * @param stored_file $file + * @param string $originalsrc + * @return moodle_url + */ + public function image_opt_url(stored_file $file, $originalsrc) { + global $CFG; + + $maxwidth = $this->config->maxwidth; + $filename = $file->get_filename(); + $contextid = $file->get_contextid(); + + $url = $CFG->wwwroot.'/pluginfile.php/'.$contextid.'/filter_imageopt/'.$maxwidth.'/'. + base64_encode($originalsrc).'/'.$filename; + + return new moodle_url($url); + } + + private function process_img_tag(array $match) { + global $CFG; + + $fs = get_file_storage(); + + $maxwidth = $this->config->maxwidth; + + $optimisedavailable = false; + + // Don't process images that aren't in this site or don't have a relative path. + if (stripos($match[2], $CFG->wwwroot) === false && substr($match[2], 0, 1) != '/') { + return $match[0]; + } + + $file = local::get_img_file($match[3]); + + if (!$file) { + return $match[0]; + } + + // Generally, if anything is being exported then we don't want to mess with it. + if ($file->get_filearea() === 'export') { + return $match[0]; + } + + if (stripos($match[3], 'imageopt/'.$maxwidth.'/') !== false) { + return $match[0]; + } + + $imageinfo = (object) $file->get_imageinfo(); + if ($imageinfo->width <= $maxwidth) { + return $match[0]; + } + + $optimisedpath = local::get_optimised_path($match[3]); + $optimisedavailable = local::get_img_file($optimisedpath); + + $originalsrc = $match[2]; + + if ($optimisedavailable) { + $optimisedsrc = local::get_optimised_src($file, $originalsrc, $optimisedpath); + } else { + $optimisedsrc = $this->image_opt_url($file, $originalsrc); + } + + if (empty($this->config->loadonvisible) || $this->config->loadonvisible < 999) { + return $this->apply_loadonvisible($match, $file, $originalsrc, $optimisedsrc, $optimisedavailable); + } else { + return $this->apply_img_tag($match, $file, $originalsrc, $optimisedsrc); + } + } + /** * Place hold images so that they are loaded when visible. * @param array $match (0 - full img tag, 1 src tag and contents, 2 - contents of src, 3 - pluginfile.php/) + * @param stored_file $file + * @param string $originalsrc + * @param string $optimisedsrc + * @param bool $optimisedavailable * @return string */ - private function apply_loadonvisible(array $match) { + private function apply_loadonvisible(array $match, stored_file $file, $originalsrc, $optimisedsrc, $optimisedavailable = false) { global $PAGE; static $jsloaded = false; @@ -184,7 +212,7 @@ private function apply_loadonvisible(array $match) { // This is so we can make the first couple of images load immediately without placeholding. if ($imgcount <= $this->config->loadonvisible) { - return $this->process_image_tag($match); + return $this->apply_img_tag($match, $file, $originalsrc, $optimisedsrc); } if (!$jsloaded) { @@ -196,14 +224,13 @@ private function apply_loadonvisible(array $match) { // Full image tag + attributes, etc. $img = $match[0]; + // If this text already has load on visible applied then just return it. if (stripos('data-loadonvisible', $match[0]) !== false) { return ($img); } $maxwidth = $this->config->maxwidth; - $file = $this->get_img_file($match[3]); - if (!$file) { return $img; } @@ -219,11 +246,12 @@ private function apply_loadonvisible(array $match) { if (!$file) { $loadonvisible = $match[2]; } else { - - $loadonvisible = $this->imageopturl($file); + $loadonvisible = $optimisedsrc; } - $img = str_ireplace('get_img_file($match[3]); + $file = local::get_img_file($match[3]); if (!$file) { return $match[0]; } @@ -262,11 +293,15 @@ private function process_image_tag($match) { return $match[0]; } - $newsrc = $this->imageopturl($file); + $newsrc = $optimisedsrc; $img = $this->img_add_width_height($match[0], $width, $height); - return str_replace($match[2], $newsrc, $img); + $img = str_replace($match[2], $newsrc, $img); + + $img = str_ireplace('config->loadonvisible) || $this->config->loadonvisible < 999) { - $search = self::REGEXP_IMGSRC; - $filtered = preg_replace_callback($search, 'self::apply_loadonvisible', $filtered); - } else { - $search = self::REGEXP_IMGSRC; - $filtered = preg_replace_callback($search, 'self::process_image_tag', $filtered); - } + $search = local::REGEXP_IMGSRC; + $filtered = preg_replace_callback($search, 'self::process_img_tag', $filtered); if (empty($filtered)) { return $text; diff --git a/lang/en/filter_imageopt.php b/lang/en/filter_imageopt.php index a77b0c6..4bf8d97 100644 --- a/lang/en/filter_imageopt.php +++ b/lang/en/filter_imageopt.php @@ -16,7 +16,7 @@ /** * Image optimiser filter. - * @author Guy Thomas + * @author Guy Thomas * @copyright Copyright (c) Guy Thomas. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/lib.php b/lib.php index a272a52..0c6a90e 100644 --- a/lib.php +++ b/lib.php @@ -23,6 +23,7 @@ */ use filter_imageopt\image; +use filter_imageopt\local; defined('MOODLE_INTERNAL') || die(); @@ -39,43 +40,71 @@ * @return bool */ function filter_imageopt_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { - if (count($args) < 2) { - throw new coding_exception('Bad image url, args should contain item id and original component'); + global $CFG; + + $originalsrc = base64_decode(clean_param($args[0], PARAM_ALPHANUMEXT)); // PARAM_BASE64 did not work for me. + $imgpath = local::get_img_path_from_src($originalsrc); + $originalfile = local::get_img_file($imgpath); + $optimisedpath = local::get_optimised_path($imgpath); + $optimisedurlpath = local::get_optimised_path($imgpath, false); + + $fs = get_file_storage(); + + $optimisedfile = local::get_img_file($optimisedpath); + + if ($optimisedfile) { + local::file_pluginfile($optimisedurlpath); + die; } - $item = $args[0]; - $component = $args[1]; - $maxwidth = $args[2]; - $filename = $args[3]; + $regex = '/imageopt\/(\d*)/'; + $matches = []; + preg_match($regex, $optimisedpath, $matches); + $maxwidth = ($matches[1]); + $item = $originalfile->get_itemid(); + $component = $originalfile->get_component(); + $filename = $originalfile->get_filename(); + $filearea = $originalfile->get_filearea(); $pathinfo = pathinfo($filename); - $resizename = $pathinfo['filename'].'_opt_'.$maxwidth.'.'.$pathinfo['extension']; - - $fs = get_file_storage(); - $file = $fs->get_file($context->id, $component, $filearea, $item, '/', $filename); - $originalts = $file->get_timemodified(); + $originalts = $originalfile->get_timemodified(); - $imageinfo = (object) $file->get_imageinfo(); + $imageinfo = (object) $originalfile->get_imageinfo(); if ($imageinfo->width <= $maxwidth) { - send_stored_file($file, null, 0, false); - return true; + local::file_pluginfile(local::url_decode_path($imgpath)); + die; } - $resizedfile = $fs->get_file($context->id, $component, $filearea, $item, '/', $resizename); // Make sure resized file is fresh. - if ($resizedfile && ($resizedfile->get_timemodified() < $originalts)) { - $resizedfile->delete(); - $resizedfile = false; + if ($optimisedfile && ($optimisedfile->get_timemodified() < $originalts)) { + $optimisedfile->delete(); + $optimisedfile = false; } - if (!$resizedfile) { - $resizedfile = image::resize($file, $resizename, $maxwidth); + + if (!$optimisedfile) { + + $pathcomps = local::explode_img_path($optimisedpath); + local::url_decode_path_components($pathcomps); + + $imageoptpos = array_search('imageopt', $pathcomps, true); + if ($imageoptpos === false) { + local::file_pluginfile(local::url_decode_path($imgpath)); + die; + } + + $filepos = array_search($filename, $pathcomps, true); + $length = $filepos - $imageoptpos; + + $optimiseddirpath = '/'.implode('/', array_slice($pathcomps, $imageoptpos, $length)).'/'; + + $optimisedfile = image::resize($originalfile, $optimiseddirpath, $filename, $maxwidth); } - if (!$resizedfile) { - send_stored_file($file, null, 0, false); - return true; + if (!$optimisedfile) { + local::file_pluginfile(local::url_decode_path($imgpath)); + die; } else { - send_stored_file($resizedfile, null, 0, false); - return true; + local::file_pluginfile($optimisedurlpath); + die; } } diff --git a/settings.php b/settings.php index b2025dd..bbbe756 100644 --- a/settings.php +++ b/settings.php @@ -16,7 +16,7 @@ /** * Image optimiser settings. - * @author Guy Thomas + * @author Guy Thomas * @copyright Copyright (c) 2017 Guy Thomas. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -36,7 +36,7 @@ 2048 => '2048' ]; $settings->add(new admin_setting_configselect('filter_imageopt/maxwidth', get_string('maxwidth', 'filter_imageopt'), - get_string('maxwidthdesc', 'filter_imageopt'), 2, $choices)); + get_string('maxwidthdesc', 'filter_imageopt'), 480, $choices)); $choices = [ 0 => get_string('loadonvisibilityall', 'filter_imageopt') diff --git a/tests/behat/behat_filter_imageopt.php b/tests/behat/behat_filter_imageopt.php new file mode 100644 index 0000000..efc3fb3 --- /dev/null +++ b/tests/behat/behat_filter_imageopt.php @@ -0,0 +1,178 @@ +. + +/** + * Imageopt filter context + * @author Guy Thomas + * @copyright Copyright (c) 2017 Guy Thomas. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +use Behat\Mink\Exception\ExpectationException; + +/** + * Imageopt filter context + * @author Guy Thomas + * @copyright Copyright (c) 2017 Guy Thomas. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_filter_imageopt extends behat_base { + /** + * @Given /^the image optimiser filter is enabled$/ + */ + public function the_imageopt_filter_is_enabled() { + filter_set_global_state('imageopt', TEXTFILTER_ON); + } + + /** + * This function is copied from filter_ally, copyright Blackboard Inc 2017. + * Get current course; + * @return stdClass | false + * @throws \Behat\Mink\Exception\ExpectationException + * @throws coding_exception + */ + protected function get_current_course() { + global $DB; + + $bodynode = $this->find('xpath', 'body'); + $bodyclass = $bodynode->getAttribute('class'); + $matches = []; + if (preg_match('/(?<=^course-|\scourse-)(?:\d*)/', $bodyclass, $matches) && !empty($matches)) { + $courseid = intval($matches[0]); + } else { + $courseid = SITEID; + } + $course = $DB->get_record('course', ['id' => $courseid]); + if (!$course) { + throw new coding_exception('Failed to get course by id '.$courseid. ' '.$bodyclass); + } + return ($course); + } + + /** + * @Given /^the image "(?P[^"]*)" has been optimised$/ + * @param string $imgfile + */ + public function image_optimised($imgfile) { + $this->ensure_element_exists('//img[contains(@data-originalsrc, "'.$imgfile.'")]', 'xpath_element'); + } + + /** + * @Given /^the image "(?P[^"]*)" has not been optimised$/ + * @param string $imgfile + */ + public function image_not_optimised($imgfile) { + $this->ensure_element_does_not_exist('//img[contains(@data-originalsrc, "'.$imgfile.'")]', 'xpath_element'); + } + + /** + * @Given /^I directly open the image "(?P[^"]*)" in the test label in course "(?P[^"]*)"$/ + * @param string $imgfile + * @param string $courseshortname + * @throws ExpectationException + */ + public function open_label_image_directly($imgfile, $courseshortname) { + global $DB, $CFG; + + $course = $DB->get_record('course', ['shortname' => $courseshortname]); + + $rs = $DB->get_records('label'); + + $row = $DB->get_record('label', ['name' => 'test label', 'course' => $course->id]); + $text = $row->intro; + + list($course, $cm) = get_course_and_cm_from_instance($row->id, 'label'); + $context = $cm->context; + + $text = file_rewrite_pluginfile_urls($text, 'pluginfile.php', $context->id, 'mod_label', 'intro', 0); + $text = format_text($text, FORMAT_HTML); + + $regex = '/getSession()); + } + + $src = $matches[2][0]; + // TODO - bodge fix, don't know why but we are getting back a URL that doesn't work with labels because it shouldn't have + // a 0 item in path. This is only happening in this behat step for some reason (works fine in real use). + $src = str_replace('/0/', '/', $src); + + $this->getSession()->visit($this->locate_path($src)); + } + + /** + * This function is copied from filter_ally, copyright Blackboard Inc 2017. + * @Given /^I create a label resource with fixture images "(?P[^"]*)"$/ + * @param string $images (csv) + */ + public function i_create_label_with_sample_images($images) { + global $CFG, $DB; + + $gen = testing_util::get_data_generator(); + + $fixturedir = $CFG->dirroot.'/filter/imageopt/tests/fixtures/'; + $images = explode(',', $images); + + $labeltext = '

A test label

'; + + $voidtype = '/>'; + + $course = $this->get_current_course(); + + $data = (object) [ + 'course' => $course->id, + 'name' => 'test label', + 'intro' => 'pre file inserts', + 'introformat' => FORMAT_HTML + ]; + + $label = $gen->create_module('label', $data); + + $i = 0; + foreach ($images as $image) { + $image = trim($image); + $i ++; + // Alternate the way the image tag is closed. + $voidtype = $voidtype === '/>' ? '>' : '/>'; + $fixturepath = $fixturedir.$image; + if (!file_exists($fixturepath)) { + throw new coding_exception('Fixture image does not exist '.$fixturepath); + } + + // Add actual file there. + $filerecord = ['component' => 'mod_label', 'filearea' => 'intro', + 'contextid' => context_module::instance($label->cmid)->id, 'itemid' => 0, + 'filename' => $image, 'filepath' => '/']; + $fs = get_file_storage(); + $fs->create_file_from_pathname($filerecord, $fixturepath); + $path = '@@PLUGINFILE@@/' . $image; + $labeltext .= 'Some text before the image'; + $labeltext .= 'test file ' . $i . 'get_record('label', ['id' => $label->id]); + $label->intro = $labeltext; + $label->name = 'test label'; + $DB->update_record('label', $label); + } +} \ No newline at end of file diff --git a/tests/behat/image_processing.feature b/tests/behat/image_processing.feature new file mode 100644 index 0000000..1e315ca --- /dev/null +++ b/tests/behat/image_processing.feature @@ -0,0 +1,63 @@ +# This file is part of Moodle - http://moodle.org/ +# +# Moodle is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Moodle is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Moodle. If not, see . +# +# Tests for image optimiser filter. +# +# @package filter_imageopt +# @copyright Copyright (c) 2017 Guy Thomas. +# @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + +@filter @filter_imageopt +Feature: When the image optimiser filter is enabled, images are placeheld until visible and re-sampled to appropriate widths. + + Background: + Given the following "courses" exist: + | fullname | shortname | category | groupmode | + | Course 1 | C1 | 0 | 1 | + And the image optimiser filter is enabled + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + + @javascript + Scenario: Images are optimised as required. + Given I log in as "teacher1" + And I am on "Course 1" course homepage + And I create a label resource with fixture images "testpng_2880x1800.png,testpng_400x250.png" + And I reload the page + And the image "testpng_2880x1800.png" has been optimised + And the image "testpng_400x250.png" has not been optimised + And I directly open the image "testpng_2880x1800.png" in the test label in course "C1" + # If the image was opened successfully then there should not be a html element on the page. + And I wait until "#page-wrapper" "css_element" does not exist + And I am on site homepage + And I log out + And I log in as "student1" + And I directly open the image "testpng_2880x1800.png" in the test label in course "C1" + # The page is reloaded to nuke image cache. + And I reload the page + # If the image could not be opened due to access rights, we should have a html element on a page with appropriate options. + And I wait until "#page-wrapper" "css_element" exists + And I should see "You can not enrol yourself in this course" + And I log out + And I directly open the image "testpng_2880x1800.png" in the test label in course "C1" + # The page is reloaded to nuke image cache. + And I reload the page + And I wait until "#page-wrapper" "css_element" exists + And I should see "Some courses may allow guest access" in the "#page-login-index" "css_element" \ No newline at end of file diff --git a/tests/filter_test.php b/tests/filter_test.php index 0440f23..f4862d5 100644 --- a/tests/filter_test.php +++ b/tests/filter_test.php @@ -17,14 +17,14 @@ /** * Tests for filter class * @package filter_imageopt - * @author Guy Thomas + * @author Guy Thomas * @copyright Guy Thomas 2017. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -use filter_imageopt\test_util; +use filter_imageopt\local; global $CFG; @@ -34,7 +34,7 @@ /** * Tests for filter class * @package filter_imageopt - * @author Guy Thomas + * @author Guy Thomas * @copyright Guy Thomas 2017. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -44,7 +44,7 @@ class filter_imageopt_filter_testcase extends advanced_testcase { * Test regex works with sample img tag + pluginfile.php src. */ public function test_regex() { - $regex = filter_imageopt::REGEXP_IMGSRC; + $regex = local::REGEXP_IMGSRC; $matches = []; $img = 'assertContains('width="'.$size[0].'" height="'.$size[1].'"', $emptyimage); } } @@ -95,7 +95,7 @@ public function test_empty_image() { * Test image opt url is created as expected. * @throws dml_exception */ - public function test_imageopturl() { + public function test_image_opt_url() { global $CFG; $this->resetAfterTest(); @@ -109,11 +109,12 @@ public function test_imageopturl() { $file = $this->std_file_record($context, $fixturefile); $filter = new filter_imageopt($context, []); - $url = test_util::call_restricted_method($filter, 'imageopturl', [$file]); + $originalurl = 'http://somesite/pluginfile.php/somefile.jpg'; + + $url = phpunit_util::call_internal_method($filter, 'image_opt_url', [$file, $originalurl], get_class($filter)); $expected = new moodle_url( - $CFG->wwwroot.'/pluginfile.php/'.$context->id.'/filter_imageopt/'.$file->get_filearea().'/'. - $file->get_itemid().'/'.$file->get_component().'/480'. + $CFG->wwwroot.'/pluginfile.php/'.$context->id.'/filter_imageopt/480'.'/'.base64_encode($originalurl). '/'.$file->get_filename()); $this->assertEquals($expected, $url); } @@ -133,14 +134,14 @@ public function test_img_add_width_height() { $filter = new filter_imageopt($context, []); $img = ''; - $img = test_util::call_restricted_method($filter, 'img_add_width_height', [$img, 800, 400]); + $img = phpunit_util::call_internal_method($filter, 'img_add_width_height', [$img, 800, 400], get_class($filter)); $expected = ''; $this->assertEquals($expected, $img); $img = ''; $expected = ''; - $img = test_util::call_restricted_method($filter, 'img_add_width_height', [$img, 800, 400]); + $img = phpunit_util::call_internal_method($filter, 'img_add_width_height', [$img, 800, 400], get_class($filter)); $this->assertEquals($expected, $img); } @@ -196,7 +197,7 @@ public function test_get_img_file() { $filepath = 'pluginfile.php/'.$context->id.'/mod_label/intro/'.$fixturefile; /** @var stored_file $imgfile */ - $imgfile = test_util::call_restricted_method($filter, 'get_img_file', [$filepath]); + $imgfile = phpunit_util::call_internal_method(null, 'get_img_file', [$filepath], 'filter_imageopt\local'); $this->assertNotEmpty($imgfile); $this->assertEquals($fixturefile, $imgfile->get_filename()); } @@ -224,7 +225,7 @@ private function create_image_file_text($fixturefile) { $labeltxt = file_rewrite_pluginfile_urls($label->intro, 'pluginfile.php', $context->id, $file->get_component(), $file->get_filearea(), 0); $matches = []; - $regex = filter_imageopt::REGEXP_IMGSRC; + $regex = local::REGEXP_IMGSRC; preg_match($regex, $labeltxt, $matches); return [$labeltxt, $matches, $file]; } @@ -251,17 +252,24 @@ public function test_apply_loadonvisible() { /** @var stored_file $file */ $file = $file; - $regex = filter_imageopt::REGEXP_IMGSRC; + $filter = new filter_imageopt(context_helper::instance_by_id($file->get_contextid()), []); + + $regex = local::REGEXP_IMGSRC; preg_match($regex, $labeltxt, $matches); - $filter = new filter_imageopt(context_helper::instance_by_id($file->get_contextid()), []); - $str = test_util::call_restricted_method($filter, 'apply_loadonvisible', [$matches]); + $originalsrc = $matches[2]; + + $optimisedsrc = $filter->image_opt_url($file, $originalsrc); + + $str = phpunit_util::call_internal_method($filter, 'apply_loadonvisible', [$matches, $file, $originalsrc, $optimisedsrc], + get_class($filter)); $loadonvisibleurl = $CFG->wwwroot.'/pluginfile.php/'.$file->get_contextid().'/filter_imageopt/'. - $file->get_filearea().'/0/'.$file->get_component().'/'.$maxwidth.'/'.$fixturefile; + $maxwidth.'/~base64url~/'.$fixturefile; // Test filter plugin img, lazy load. - $this->assertContains('assertRegExp($regex, $str); $this->assertContains('src="data:image/svg+xml;utf8,', $str); } @@ -275,8 +283,8 @@ public function test_apply_loadonvisible() { private function filter_imageopt_url_from_file(stored_file $file, $maxwidth) { global $CFG; - $url = $CFG->wwwroot.'/pluginfile.php/'.$file->get_contextid().'/filter_imageopt/'.$file->get_filearea().'/'. - $file->get_itemid().'/'.$file->get_component().'/'.$maxwidth.'/'.$file->get_filename(); + $url = $CFG->wwwroot.'/pluginfile.php/'.$file->get_contextid().'/filter_imageopt/'.$maxwidth. + '/~base64url~/'.$file->get_filename(); return $url; } @@ -285,7 +293,7 @@ private function filter_imageopt_url_from_file(stored_file $file, $maxwidth) { * Test processing image src. * @throws coding_exception */ - public function test_process_image_tag() { + public function test_apply_img_tag() { $this->resetAfterTest(); $this->setAdminUser(); @@ -302,9 +310,15 @@ public function test_process_image_tag() { $filter = new filter_imageopt($context, []); - $processed = test_util::call_restricted_method($filter, 'process_image_tag', [$matches]); + $originalsrc = $matches[2]; + $optimisedsrc = $filter->image_opt_url($file, $originalsrc); + + $processed = phpunit_util::call_internal_method($filter, 'apply_img_tag', [$matches, $file, $originalsrc, $optimisedsrc], + get_class($filter)); + $postfilterurl = $this->filter_imageopt_url_from_file($file, $maxwidth); - $this->assertContains('src="'.$postfilterurl, $processed); + $regex = '/'.str_replace('~base64url~', '(?:[A-z|0-9|=]*)', preg_quote($postfilterurl, '/')).'/'; + $this->assertRegExp($regex, $processed); } @@ -335,8 +349,11 @@ public function test_filter() { $prefilterurl = $CFG->wwwroot.'/pluginfile.php/'.$context->id.'/mod_label/intro/0/testpng_2880x1800.png'; $this->assertContains($prefilterurl, $labeltxt); $postfilterurl = $this->filter_imageopt_url_from_file($file, $maxwidth); - $this->assertContains('src="'.$postfilterurl, $filtered); - $this->assertNotContains('src="'.$prefilterurl, $filtered); + $regex = '/src="'.str_replace('~base64url~', '(?:[A-z|0-9|=]*)', preg_quote($postfilterurl, '/')).'/'; + $this->assertRegExp($regex, $filtered); + + // We need a space before src so it doesn't trigger on original-src. + $this->assertNotContains(' src="'.$prefilterurl, $filtered); $this->assertNotContains('data-loadonvisible="'.$postfilterurl, $filtered); $this->assertNotContains('data-loadonvisible="'.$prefilterurl, $filtered); @@ -347,9 +364,12 @@ public function test_filter() { $prefilterurl = $CFG->wwwroot.'/pluginfile.php/'.$context->id.'/mod_label/intro/0/testpng_2880x1800.png'; $this->assertContains($prefilterurl, $labeltxt); $postfilterurl = $this->filter_imageopt_url_from_file($file, $maxwidth); - $this->assertContains('data-loadonvisible="'.$postfilterurl, $filtered); + + $regex = '/data-loadonvisible="'.str_replace('~base64url~', '(?:[A-z|0-9|=]*)', preg_quote($postfilterurl, '/')).'/'; + $this->assertRegExp($regex, $filtered); + $this->assertNotContains('data-loadonvisible="'.$prefilterurl, $filtered); $this->assertNotContains('src="'.$postfilterurl, $filtered); $this->assertNotContains('src="'.$prefilterurl, $filtered); } -} +} \ No newline at end of file diff --git a/tests/fixtures/testpng_400x250.png b/tests/fixtures/testpng_400x250.png new file mode 100644 index 0000000000000000000000000000000000000000..72f7ebf71467e1732bd7462f40cc828f51866bac GIT binary patch literal 2301 zcmb7G3s4hx8vj!wAQfvZt+g-Lh!(Zv!6QHv0XK;WM{<^jmr;9no9qG$tn{AXbbYti1y@aTdl=DED?fW{Q3_S)EVhrcQW}u z_WM1)|M%_AZdj>Xkr4ktJVGczqgE6l6y?VAk+^95-|9SDi-*~~QqLD#%6L1)F({w5 z7#KohqRN>fhN7KM9$}Uu6rIGD=y|=iP;RuCL{ub3VtB&9l(Xg> z;ZFxY5)v$(Bm7f_R-(0%Oa-g19s2f(!w!BhA1t?D3MADnM@>2&ydNaj}fU7X{uO)f3g&5 zx;#TBPt7E*UqYsIkK^E458J^HD()6nT?D_kt4($B9^7)sx%3yRH(9*>56ozRGFEj zP^4z1E3(p5sti&k$r#5fEXEoWW9G-P^bKtOq}WI^n5-&Q9(n zb5g$RSbB0VS(9SL*o-(*+M7x}zJ>cIGM#8#yqIWx#*F)&!;M|{!J+l|DH~l`SfT^~ znx^q10^JBA!_aa(%!FP9(2kdb(1rXEHW&(Ps#2+Z5bo{m?dF~7 zeF-qI4}u3_a1cI25bCL{tPJi3VA-cYJ9-{2`vBU|gMcKM$K#m`h}1y`f>UvDB>*k^ z0j~bw@pxeHa3_MFJdg-zE7Uguw`U)WV5wsW!j}^O!Id-E+>?-tMLq@pc>~sc9|rtC zdSKhOZHK@Q!-2~%&>IbKB6;v2Kx1PpMukG(7@!%}PY1++sC_K0##bRJ=KOa2+N{th z@=F}2zdv(=OME;ruj`Y!&&_zKVTr)5X^6!yp0R7V@R1z_FE>|?bf@LK6OMYj-!oWm zUNBEnsb z&W0U9k6CvKFrm94#yQ$*Yj|Sx!+16Shg`d6@7ue?cN`fV z(^J3sx2G@M?L8OkTE~)MNeY_TJVY)1peFW)l?hM3Z_@UceDmQ02U_n{TiyREwohx@ zzoW?|h@JXiBtvf;{ofIPw%7lbVTt?m6ZX=F2fRCX#^zny>OvbTPVTDzxz0CTt#+@O zGkWOawM=4ezS-0N@SgWxa;+20PTfxVWA2(Vb84#dtL=YZpo`fgY4grk-kq{pkVLtg zl?lQ{mGS?}Y<);Ft!>V-*94nN%TLFCci~ZDM_k;DsGCZF8KR;#EpjvMT=%`i`+Tce zxB5=+Ye%{0?rGy?x>bdX_We+G%yFhykd)Nc)c(xZH6!=C-GqJB(DNtX_-EeohQ!L7 zt9;~H_teJKZ|zmy6MrAw;qMRZ*j3lJrD4kE+^VuKj%U~cR~x1*G@aU}>#ywcCEi~0 zmZrJY&F{aZ4iN9nx=*>!Ds!*PFMHvI>KHuwCQ0`HqW4bm)|m6^1<{+AFst7e$z_eVcAp=fmC!0zWI;??@d2Z=_hQ@p$UuMNKd D#oLXF literal 0 HcmV?d00001 diff --git a/version.php b/version.php index 8893146..dd9cc03 100644 --- a/version.php +++ b/version.php @@ -16,13 +16,13 @@ /** * Image Optimiser - * @author Guy Thomas + * @author Guy Thomas * @copyright Copyright (c) 2016 Guy Thomas. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2017110300; +$plugin->version = 2017110301; $plugin->requires = 2011111500; $plugin->component = 'filter_imageopt'; $plugin->maturity = MATURITY_BETA;