diff --git a/src/Css/Stylesheet.php b/src/Css/Stylesheet.php index 18b210fe1..2720388d4 100644 --- a/src/Css/Stylesheet.php +++ b/src/Css/Stylesheet.php @@ -325,46 +325,26 @@ function load_css_file($file, $origin = self::ORIG_AUTHOR) $parsed = Helpers::parse_data_uri($file); $css = $parsed["data"]; } else { - $parsed_url = Helpers::explode_url($file); - - [$this->_protocol, $this->_base_host, $this->_base_path, $filename] = $parsed_url; + $options = $this->_dompdf->getOptions(); - $file = Helpers::build_url($this->_protocol, $this->_base_host, $this->_base_path, $filename); + $parsed_url = Helpers::explode_url($file); + $protocol = $parsed_url["protocol"]; - $options = $this->_dompdf->getOptions(); - // Download the remote file - if (!$options->isRemoteEnabled() && ($this->_protocol !== "" && $this->_protocol !== "file://")) { - Helpers::record_warnings(E_USER_WARNING, "Remote CSS resource '$file' referenced, but remote file download is disabled.", __FILE__, __LINE__); - return; - } - if ($this->_protocol === "" || $this->_protocol === "file://") { - $realfile = realpath($file); - - $rootDir = realpath($options->getRootDir()); - if (strpos($realfile, $rootDir) !== 0) { - $chroot = $options->getChroot(); - $chrootValid = false; - foreach ($chroot as $chrootPath) { - $chrootPath = realpath($chrootPath); - if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) { - $chrootValid = true; - break; - } - } - if ($chrootValid !== true) { - Helpers::record_warnings(E_USER_WARNING, "Permission denied on $file. The file could not be found under the paths specified by Options::chroot.", __FILE__, __LINE__); + if ($file !== $this->getDefaultStylesheet()) { + $allowed_protocols = $options->getAllowedProtocols(); + if (!array_key_exists($protocol, $allowed_protocols)) { + Helpers::record_warnings(E_USER_WARNING, "Permission denied on $file. The communication protocol is not supported.", __FILE__, __LINE__); + return; + } + foreach ($allowed_protocols[$protocol]["rules"] as $rule) { + [$result, $message] = $rule($file); + if (!$result) { + Helpers::record_warnings(E_USER_WARNING, "Error loading $file: $message", __FILE__, __LINE__); return; } } - - if (!$realfile) { - Helpers::record_warnings(E_USER_WARNING, "File '$realfile' not found.", __FILE__, __LINE__); - return; - } - - $file = $realfile; } - + [$css, $http_response_header] = Helpers::getFileContent($file, $this->_dompdf->getHttpContext()); $good_mime_type = true; @@ -379,11 +359,12 @@ function load_css_file($file, $origin = self::ORIG_AUTHOR) } } } - if (!$good_mime_type || $css === null) { Helpers::record_warnings(E_USER_WARNING, "Unable to load css file $file", __FILE__, __LINE__); return; } + + [$this->_protocol, $this->_base_host, $this->_base_path] = $parsed_url; } $this->_parse_css($css); @@ -1421,20 +1402,16 @@ public function resolve_url($val): string $val = preg_replace("/url\(\s*['\"]?([^'\")]+)['\"]?\s*\)/", "\\1", trim($val)); // Resolve the url now in the context of the current stylesheet - $parsed_url = Helpers::explode_url($val); $path = Helpers::build_url($this->_protocol, $this->_base_host, $this->_base_path, $val); - if (($parsed_url["protocol"] === "" || $parsed_url["protocol"] === "file://") && ($this->_protocol === "" || $this->_protocol === "file://")) { - $path = realpath($path); - // If realpath returns FALSE then specifically state that there is no background image - if ($path === false) { - $path = "none"; - } + if ($path === null) { + $path = "none"; } } if ($DEBUGCSS) { + $parsed_url = Helpers::explode_url($path); print "
[_image\n";
             print_r($parsed_url);
             print $this->_protocol . "\n" . $this->_base_path . "\n" . $path . "\n";
@@ -1483,9 +1460,9 @@ private function _parse_import($url)
             // Above does not work for subfolders and absolute urls.
             // Todo: As above, do we need to replace php or file to an empty protocol for local files?
 
-            $url = $this->resolve_url($url);
-
-            $this->load_css_file($url);
+            if (($url = $this->resolve_url($url)) !== "none") {
+                $this->load_css_file($url);
+            }
 
             // Restore the current base url
             $this->_protocol = $protocol;
@@ -1675,7 +1652,7 @@ public function getDefaultStylesheet()
     {
         $options = $this->_dompdf->getOptions();
         $rootDir = realpath($options->getRootDir());
-        return $rootDir . self::DEFAULT_STYLESHEET;
+        return Helpers::build_url("file://", "", $rootDir, $rootDir . self::DEFAULT_STYLESHEET);
     }
 
     /**
diff --git a/src/Dompdf.php b/src/Dompdf.php
index 73b1a39b9..7e657af20 100644
--- a/src/Dompdf.php
+++ b/src/Dompdf.php
@@ -196,16 +196,6 @@ class Dompdf
      */
     private $quirksmode = false;
 
-    /**
-    * Protocol whitelist
-    *
-    * Protocols and PHP wrappers allowed in URLs. Full support is not
-    * guaranteed for the protocols/wrappers contained in this array.
-    *
-    * @var array
-    */
-    private $allowedProtocols = ["", "file://", "http://", "https://"];
-
     /**
     * Local file extension whitelist
     *
@@ -271,8 +261,11 @@ public function __construct($options = null)
         }
 
         $versionFile = realpath(__DIR__ . '/../VERSION');
-        if (file_exists($versionFile) && ($version = trim(file_get_contents($versionFile))) !== false && $version !== '$Format:<%h>$') {
-            $this->version = sprintf('dompdf %s', $version);
+        if (($version = file_get_contents($versionFile)) !== false) {
+            $version = trim($version);
+            if ($version !== '$Format:<%h>$') {
+                $this->version = sprintf('dompdf %s', $version);
+            }
         }
 
         $this->setPhpConfig();
@@ -352,43 +345,25 @@ public function loadHtmlFile($file, $encoding = null)
             [$this->protocol, $this->baseHost, $this->basePath] = Helpers::explode_url($file);
         }
         $protocol = strtolower($this->protocol);
-        
         $uri = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $file);
 
-        if (!in_array($protocol, $this->allowedProtocols, true)) {
+        $allowed_protocols = $this->options->getAllowedProtocols();
+        if (!array_key_exists($protocol, $allowed_protocols)) {
             throw new Exception("Permission denied on $file. The communication protocol is not supported.");
         }
 
-        if (!$this->options->isRemoteEnabled() && ($protocol !== "" && $protocol !== "file://")) {
-            throw new Exception("Remote file requested, but remote file download is disabled.");
-        }
-
-        if ($protocol === "" || $protocol === "file://") {
-            $realfile = realpath($uri);
-
-            $chroot = $this->options->getChroot();
-            $chrootValid = false;
-            foreach ($chroot as $chrootPath) {
-                $chrootPath = realpath($chrootPath);
-                if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
-                    $chrootValid = true;
-                    break;
-                }
-            }
-            if ($chrootValid !== true) {
-                throw new Exception("Permission denied on $file. The file could not be found under the paths specified by Options::chroot.");
-            }
-
-            $ext = strtolower(pathinfo($realfile, PATHINFO_EXTENSION));
+        if ($protocol === "file://") {
+            $ext = strtolower(pathinfo($uri, PATHINFO_EXTENSION));
             if (!in_array($ext, $this->allowedLocalFileExtensions)) {
-                throw new Exception("Permission denied on $file. This file extension is forbidden");
+                throw new Exception("Permission denied on $file: The file extension is forbidden.");
             }
+        }
 
-            if (!$realfile) {
-                throw new Exception("File '$file' not found.");
+        foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
+            [$result, $message] = $rule($uri);
+            if (!$result) {
+                throw new Exception("Error loading $file: $message");
             }
-
-            $uri = $realfile;
         }
 
         [$contents, $http_response_header] = Helpers::getFileContent($uri, $this->options->getHttpContext());
@@ -604,7 +579,9 @@ private function processHtml()
                         $url = $tag->getAttribute("href");
                         $url = Helpers::build_url($this->protocol, $this->baseHost, $this->basePath, $url);
 
-                        $this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
+                        if ($url !== null) {
+                            $this->css->load_css_file($url, Stylesheet::ORIG_AUTHOR);
+                        }
                     }
                     break;
 
diff --git a/src/FontMetrics.php b/src/FontMetrics.php
index 1d0a9c471..83d0c99c7 100644
--- a/src/FontMetrics.php
+++ b/src/FontMetrics.php
@@ -214,37 +214,18 @@ public function registerFont($style, $remoteFile, $context = null)
 
         // Download the remote file
         [$protocol] = Helpers::explode_url($remoteFile);
-        if (!$this->options->isRemoteEnabled() && ($protocol !== "" && $protocol !== "file://")) {
-            Helpers::record_warnings(E_USER_WARNING, "Remote font resource $remoteFile referenced, but remote file download is disabled.", __FILE__, __LINE__);
-            return false;
+        $allowed_protocols = $this->options->getAllowedProtocols();
+        if (!array_key_exists($protocol, $allowed_protocols)) {
+            Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__);
         }
-        if ($protocol === "" || $protocol === "file://") {
-            $realfile = realpath($remoteFile);
-
-            $rootDir = realpath($this->options->getRootDir());
-            if (strpos($realfile, $rootDir) !== 0) {
-                $chroot = $this->options->getChroot();
-                $chrootValid = false;
-                foreach ($chroot as $chrootPath) {
-                    $chrootPath = realpath($chrootPath);
-                    if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
-                        $chrootValid = true;
-                        break;
-                    }
-                }
-                if ($chrootValid !== true) {
-                    Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The file could not be found under the paths specified by Options::chroot.", __FILE__, __LINE__);
-                    return false;
-                }
-            }
 
-            if (!$realfile) {
-                Helpers::record_warnings(E_USER_WARNING, "File '$realfile' not found.", __FILE__, __LINE__);
-                return false;
+        foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
+            [$result, $message] = $rule($remoteFile);
+            if ($result !== true) {
+                Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__);
             }
-
-            $remoteFile = $realfile;
         }
+
         list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
         if ($remoteFileContent === null) {
             return false;
diff --git a/src/FrameDecorator/Image.php b/src/FrameDecorator/Image.php
index 61df2bc8a..c52aae803 100644
--- a/src/FrameDecorator/Image.php
+++ b/src/FrameDecorator/Image.php
@@ -57,7 +57,7 @@ function __construct(Frame $frame, Dompdf $dompdf)
             $dompdf->getProtocol(),
             $dompdf->getBaseHost(),
             $dompdf->getBasePath(),
-            $dompdf
+            $dompdf->getOptions()
         );
 
         if (Cache::is_broken($this->_image_url) &&
diff --git a/src/Helpers.php b/src/Helpers.php
index 6b56c58c9..ece05c567 100644
--- a/src/Helpers.php
+++ b/src/Helpers.php
@@ -57,28 +57,45 @@ public static function pre_r($mixed, $return = false)
     public static function build_url($protocol, $host, $base_path, $url)
     {
         $protocol = mb_strtolower($protocol);
+        if (empty($protocol)) {
+            $protocol = "file://";
+        }
         if ($url === "") {
-            //return $protocol . $host . rtrim($base_path, "/\\") . "/";
-            return $protocol . $host . $base_path;
+            return null;
         }
 
+        $url_lc = mb_strtolower($url);
+
         // Is the url already fully qualified, a Data URI, or a reference to a named anchor?
         // File-protocol URLs may require additional processing (e.g. for URLs with a relative path)
-        if ((mb_strpos($url, "://") !== false && substr($url, 0, 7) !== "file://") || mb_substr($url, 0, 1) === "#" || mb_strpos($url, "data:") === 0 || mb_strpos($url, "mailto:") === 0 || mb_strpos($url, "tel:") === 0) {
+        if (
+            (
+                mb_strpos($url_lc, "://") !== false
+                && !in_array(substr($url_lc, 0, 7), ["file://", "phar://"], true)
+            )
+            || mb_substr($url_lc, 0, 1) === "#"
+            || mb_strpos($url_lc, "data:") === 0
+            || mb_strpos($url_lc, "mailto:") === 0
+            || mb_strpos($url_lc, "tel:") === 0
+        ) {
             return $url;
         }
 
-        if (strpos($url, "file://") === 0) {
+        $res = "";
+        if (strpos($url_lc, "file://") === 0) {
             $url = substr($url, 7);
-            $protocol = "";
+            $protocol = "file://";
+        } elseif (strpos($url_lc, "phar://") === 0) {
+            $res = substr($url, strpos($url_lc, ".phar")+5);
+            $url = substr($url, 7, strpos($url_lc, ".phar")-2);
+            $protocol = "phar://";
         }
 
         $ret = "";
-        if ($protocol !== "file://") {
-            $ret = $protocol;
-        }
 
-        if (!in_array(mb_strtolower($protocol), ["http://", "https://", "ftp://", "ftps://"], true)) {
+        $is_local_path = in_array($protocol, ["file://", "phar://"], true);
+
+        if ($is_local_path) {
             //On Windows local file, an abs path can begin also with a '\' or a drive letter and colon
             //drive: followed by a relative path would be a drive specific default folder.
             //not known in php app code, treat as abs path
@@ -89,9 +106,18 @@ public static function build_url($protocol, $host, $base_path, $url)
             }
             $ret .= $url;
             $ret = preg_replace('/\?(.*)$/', "", $ret);
+
+            $filepath = realpath($ret);
+            if ($filepath === false) {
+                return null;
+            }
+
+            $ret = "$protocol$filepath$res";
+
             return $ret;
         }
 
+        $ret = $protocol;
         // Protocol relative urls (e.g. "//example.org/style.css")
         if (strpos($url, '//') === 0) {
             $ret .= substr($url, 2);
@@ -431,14 +457,14 @@ public static function explode_url($url)
         $host = "";
         $path = "";
         $file = "";
+        $res = "";
 
         $arr = parse_url($url);
         if ( isset($arr["scheme"]) ) {
             $arr["scheme"] = mb_strtolower($arr["scheme"]);
         }
 
-        // Exclude windows drive letters...
-        if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && strlen($arr["scheme"]) > 1) {
+        if (isset($arr["scheme"]) && $arr["scheme"] !== "file" && $arr["scheme"] !== "phar" && strlen($arr["scheme"]) > 1) {
             $protocol = $arr["scheme"] . "://";
 
             if (isset($arr["user"])) {
@@ -480,42 +506,32 @@ public static function explode_url($url)
 
         } else {
 
-            $i = mb_stripos($url, "file://");
-            if ($i !== false) {
-                $url = mb_substr($url, $i + 7);
-            }
-
-            $protocol = ""; // "file://"; ? why doesn't this work... It's because of
-            // network filenames like //COMPU/SHARENAME
-
+            $protocol = "";
             $host = ""; // localhost, really
-            $file = basename($url);
-
-            $path = dirname($url);
-
-            // Check that the path exists
-            if ($path !== false) {
-                $path .= '/';
 
+            $i = mb_stripos($url, "://");
+            if ($i !== false) {
+                $protocol = mb_strtolower(mb_substr($url, 0, $i + 3));
+                $url = mb_substr($url, $i + 3);
             } else {
-                // generate a url to access the file if no real path found.
-                $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://';
-
-                $host = isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : php_uname("n");
+                $protocol = "file://";
+            }
 
-                if (substr($arr["path"], 0, 1) === '/') {
-                    $path = dirname($arr["path"]);
-                } else {
-                    $path = '/' . rtrim(dirname($_SERVER["SCRIPT_NAME"]), '/') . '/' . $arr["path"];
-                }
+            if ($protocol === "phar://") {
+                $res = substr($url, stripos($url, ".phar")+5);
+                $url = substr($url, 7, stripos($url, ".phar")-2);
             }
+
+            $file = basename($url);
+            $path = dirname($url) . "/";
         }
 
         $ret = [$protocol, $host, $path, $file,
             "protocol" => $protocol,
             "host" => $host,
             "path" => $path,
-            "file" => $file];
+            "file" => $file,
+            "resource" => $res];
         return $ret;
     }
 
@@ -878,12 +894,13 @@ public static function getFileContent($uri, $context = null, $offset = 0, $maxle
         $content = null;
         $headers = null;
         [$protocol] = Helpers::explode_url($uri);
-        $is_local_path = ($protocol === "" || $protocol === "file://");
+        $is_local_path = in_array(strtolower($protocol), ["", "file://", "phar://"], true);
+        $can_use_curl = in_array(strtolower($protocol), ["http://", "https://"], true);
 
         set_error_handler([self::class, 'record_warnings']);
 
         try {
-            if ($is_local_path || ini_get('allow_url_fopen')) {
+            if ($is_local_path || ini_get('allow_url_fopen') || !$can_use_curl) {
                 if ($is_local_path === false) {
                     $uri = Helpers::encodeURI($uri);
                 }
@@ -899,7 +916,7 @@ public static function getFileContent($uri, $context = null, $offset = 0, $maxle
                     $headers = $http_response_header;
                 }
 
-            } elseif (function_exists('curl_exec')) {
+            } elseif ($can_use_curl && function_exists('curl_exec')) {
                 $curl = curl_init($uri);
 
                 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
diff --git a/src/Image/Cache.php b/src/Image/Cache.php
index a6ef0fe5d..7da66a1e1 100644
--- a/src/Image/Cache.php
+++ b/src/Image/Cache.php
@@ -9,7 +9,7 @@
  */
 namespace Dompdf\Image;
 
-use Dompdf\Dompdf;
+use Dompdf\Options;
 use Dompdf\Helpers;
 use Dompdf\Exception\ImageException;
 
@@ -43,13 +43,6 @@ class Cache
 
     public static $error_message = "Image not found or type unknown";
     
-    /**
-     * Current dompdf instance
-     *
-     * @var Dompdf
-     */
-    protected static $_dompdf;
-
     /**
      * Resolve and fetch an image for use.
      *
@@ -57,130 +50,91 @@ class Cache
      * @param string $protocol  Default protocol if none specified in $url
      * @param string $host      Default host if none specified in $url
      * @param string $base_path Default path if none specified in $url
-     * @param Dompdf $dompdf    The Dompdf instance
+     * @param Options $options  An instance of Dompdf\Options
      *
-     * @throws ImageException
-     * @return array             An array with two elements: The local path to the image and the image extension
+     * @return array            An array with three elements: The local path to the image, the image
+     *                          extension, and an error message if the image could not be cached
      */
-    static function resolve_url($url, $protocol, $host, $base_path, Dompdf $dompdf)
+    static function resolve_url($url, $protocol, $host, $base_path, Options $options)
     {
-        self::$_dompdf = $dompdf;
-        
-        $protocol = mb_strtolower($protocol);
-        $parsed_url = Helpers::explode_url($url);
+        $tempfile = null;
+        $resolved_url = null;
+        $type = null;
         $message = null;
-
-        $remote = ($protocol && $protocol !== "file://") || ($parsed_url['protocol'] !== "");
-
-        $data_uri = strpos($parsed_url['protocol'], "data:") === 0;
-        $full_url = null;
-        $enable_remote = $dompdf->getOptions()->getIsRemoteEnabled();
-        $tempfile = false;
-
+        
         try {
+            $full_url = Helpers::build_url($protocol, $host, $base_path, $url);
 
-            // Remote not allowed and is not DataURI
-            if (!$enable_remote && $remote && !$data_uri) {
-                throw new ImageException("Remote file access is disabled.", E_WARNING);
+            if ($full_url === null) {
+                throw new ImageException("Unable to parse image URL $url.", E_WARNING);
             }
-            
-            // remote allowed or DataURI
-            if (($enable_remote && $remote) || $data_uri) {
-                // Download remote files to a temporary directory
-                $full_url = Helpers::build_url($protocol, $host, $base_path, $url);
 
-                // From cache
-                if (isset(self::$_cache[$full_url])) {
-                    $resolved_url = self::$_cache[$full_url];
-                } // From remote
-                else {
-                    $tmp_dir = $dompdf->getOptions()->getTempDir();
-                    if (($resolved_url = @tempnam($tmp_dir, "ca_dompdf_img_")) === false) {
-                        throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING);
+            $parsed_url = Helpers::explode_url($full_url);
+            $protocol = strtolower($parsed_url["protocol"]);
+            $is_data_uri = strpos($protocol, "data:") === 0;
+            
+            if (!$is_data_uri) {
+                $allowed_protocols = $options->getAllowedProtocols();
+                if (!array_key_exists($protocol, $allowed_protocols)) {
+                    throw new ImageException("Permission denied on $url. The communication protocol is not supported.", E_WARNING);
+                }
+                foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
+                    [$result, $message] = $rule($full_url);
+                    if (!$result) {
+                        throw new ImageException("Error loading $url: $message", E_WARNING);
                     }
-                    $tempfile = $resolved_url;
-                    $image = null;
+                }
+            }
 
-                    if ($data_uri) {
-                        if ($parsed_data_uri = Helpers::parse_data_uri($url)) {
-                            $image = $parsed_data_uri['data'];
-                        }
-                    } else {
-                        list($image, $http_response_header) = Helpers::getFileContent($full_url, $dompdf->getHttpContext());
-                    }
+            if ($protocol === "file://") {
+                $resolved_url = $full_url;
+            } elseif (isset(self::$_cache[$full_url])) {
+                $resolved_url = self::$_cache[$full_url];
+            } else {
+                $tmp_dir = $options->getTempDir();
+                if (($resolved_url = @tempnam($tmp_dir, "ca_dompdf_img_")) === false) {
+                    throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING);
+                }
+                $tempfile = $resolved_url;
 
-                    // Image not found or invalid
-                    if ($image === null) {
-                        $msg = ($data_uri ? "Data-URI could not be parsed" : "Image not found");
-                        throw new ImageException($msg, E_WARNING);
-                    } // Image found, put in cache and process
-                    else {
-                        //e.g. fetch.php?media=url.jpg&cache=1
-                        //- Image file name might be one of the dynamic parts of the url, don't strip off!
-                        //- a remote url does not need to have a file extension at all
-                        //- local cached file does not have a matching file extension
-                        //Therefore get image type from the content
-                        if (@file_put_contents($resolved_url, $image) === false) {
-                            throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING);
-                        }
+                $image = null;
+                if ($is_data_uri) {
+                    if (($parsed_data_uri = Helpers::parse_data_uri($url)) !== false) {
+                        $image = $parsed_data_uri["data"];
                     }
+                } else {
+                    list($image, $http_response_header) = Helpers::getFileContent($full_url, $options->getHttpContext());
                 }
-            } // Not remote, local image
-            else {
-                $resolved_url = Helpers::build_url($protocol, $host, $base_path, $url);
 
-                if ($protocol === "" || $protocol === "file://") {
-                    $realfile = realpath($resolved_url);
-        
-                    $rootDir = realpath($dompdf->getOptions()->getRootDir());
-                    if (strpos($realfile, $rootDir) !== 0) {
-                        $chroot = $dompdf->getOptions()->getChroot();
-                        $chrootValid = false;
-                        foreach ($chroot as $chrootPath) {
-                            $chrootPath = realpath($chrootPath);
-                            if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
-                                $chrootValid = true;
-                                break;
-                            }
-                        }
-                        if ($chrootValid !== true) {
-                            throw new ImageException("Permission denied on $resolved_url. The file could not be found under the paths specified by Options::chroot.", E_WARNING);
-                        }
-                    }
-        
-                    if (!$realfile) {
-                        throw new ImageException("File '$realfile' not found.", E_WARNING);
-                    }
-        
-                    $resolved_url = $realfile;
+                // Image not found or invalid
+                if ($image === null) {
+                    $msg = ($is_data_uri ? "Data-URI could not be parsed" : "Image not found");
+                    throw new ImageException($msg, E_WARNING);
+                }
+
+                if (@file_put_contents($resolved_url, $image) === false) {
+                    throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING);
                 }
+
+                self::$_cache[$full_url] = $resolved_url;
             }
 
             // Check if the local file is readable
             if (!is_readable($resolved_url) || !filesize($resolved_url)) {
                 throw new ImageException("Image not readable or empty", E_WARNING);
-            } // Check is the file is an image
-            else {
-                list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $dompdf->getHttpContext());
+            }
 
-                // Known image type
-                if ($width && $height && in_array($type, ["gif", "png", "jpeg", "bmp", "svg","webp"], true)) {
-                    //Don't put replacement image into cache - otherwise it will be deleted on cache cleanup.
-                    //Only execute on successful caching of remote image.
-                    if ($enable_remote && $remote || $data_uri) {
-                        self::$_cache[$full_url] = $resolved_url;
-                    }
-                } // Unknown image type
-                else {
-                    throw new ImageException("Image type unknown", E_WARNING);
-                }
+            list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext());
+
+            if (($width && $height && in_array($type, ["gif", "png", "jpeg", "bmp", "svg","webp"], true)) === false) {
+                throw new ImageException("Image type unknown", E_WARNING);
             }
         } catch (ImageException $e) {
             if ($tempfile) {
                 unlink($tempfile);
             }
             $resolved_url = self::$broken_image;
-            $type = "png";
+            $type = "svg";
             $message = self::$error_message;
             Helpers::record_warnings($e->getCode(), $e->getMessage() . " \n $url", $e->getFile(), $e->getLine());
             self::$_cache[$full_url] = $resolved_url;
@@ -229,7 +183,9 @@ static function clear(bool $debugPng = false)
             if ($debugPng) {
                 print "[clear unlink $file]";
             }
-            unlink($file);
+            if (file_exists($file)) {
+                unlink($file);
+            }
         }
 
         foreach (self::$tempImages as $versions) {
diff --git a/src/Options.php b/src/Options.php
index 69c43bd9d..d3a4da35a 100644
--- a/src/Options.php
+++ b/src/Options.php
@@ -62,6 +62,20 @@ class Options
      */
     private $chroot;
 
+    /**
+    * Protocol whitelist
+    *
+    * Protocols and PHP wrappers allowed in URIs. Full support is not
+    * guaranteed for the protocols/wrappers specified by this array.
+    *
+    * @var array
+    */
+    private $allowedProtocols = [
+        "file://" => ["rules" => []],
+        "http://" => ["rules" => []],
+        "https://" => ["rules" => []]
+    ];
+
     /**
      * @var string
      */
@@ -299,9 +313,12 @@ public function __construct(array $attributes = null)
         $this->setFontCache($this->getFontDir());
 
         $ver = "";
-        $versionFile = realpath(__DIR__ . "/../VERSION");
-        if (file_exists($versionFile) && ($version = trim(file_get_contents($versionFile))) !== false && $version !== '$Format:<%h>$') {
-            $ver = "/$version";
+        $versionFile = realpath(__DIR__ . '/../VERSION');
+        if (($version = file_get_contents($versionFile)) !== false) {
+            $version = trim($version);
+            if ($version !== '$Format:<%h>$') {
+                $ver = "/$version";
+            }
         }
         $this->setHttpContext([
             "http" => [
@@ -310,6 +327,8 @@ public function __construct(array $attributes = null)
             ]
         ]);
 
+        $this->setAllowedProtocols(["file://", "http://", "https://"]);
+
         if (null !== $attributes) {
             $this->set($attributes);
         }
@@ -334,6 +353,8 @@ public function set($attributes, $value = null)
                 $this->setFontCache($value);
             } elseif ($key === 'chroot') {
                 $this->setChroot($value);
+            } elseif ($key === 'allowedProtocols') {
+                $this->setAllowedProtocols($value);
             } elseif ($key === 'logOutputFile' || $key === 'log_output_file') {
                 $this->setLogOutputFile($value);
             } elseif ($key === 'defaultMediaType' || $key === 'default_media_type') {
@@ -399,6 +420,8 @@ public function get($key)
             return $this->getFontCache();
         } elseif ($key === 'chroot') {
             return $this->getChroot();
+        } elseif ($key === 'allowedProtocols') {
+            return $this->getAllowedProtocols();
         } elseif ($key === 'logOutputFile' || $key === 'log_output_file') {
             return $this->getLogOutputFile();
         } elseif ($key === 'defaultMediaType' || $key === 'default_media_type') {
@@ -499,6 +522,67 @@ public function setChroot($chroot, $delimiter = ',')
         return $this;
     }
 
+    /**
+     * @return array
+     */
+    public function getAllowedProtocols()
+    {
+        return $this->allowedProtocols;
+    }
+
+    /**
+     * @param array $allowedProtocols The protocols to allow as an array (["protocol://" => ["rules" => [callable]]], ...) or a string list of the protocols
+     * @return $this
+     */
+    public function setAllowedProtocols(array $allowedProtocols)
+    {
+        $protocols = [];
+        foreach ($allowedProtocols as $protocol => $config) {
+            if (is_string($protocol)) {
+                $protocols[$protocol] = [];
+                if (is_array($config)) {
+                    $protocols[$protocol] = $config;
+                }
+            } elseif (is_string($config)) {
+                $protocols[$config] = [];
+            }
+        }
+        $this->allowedProtocols = [];
+        foreach ($protocols as $protocol => $config) {
+            $this->addAllowedProtocol($protocol, ...($config["rules"] ?? []));
+        }
+        return $this;
+    }
+
+    /**
+     * Adds a new protocol to the allowed protocols collection
+     *
+     * @param string $protocol The scheme to add (e.g. "http://")
+     * @param callable $rule A callable that validates the protocol
+     * @return $this
+     */
+    public function addAllowedProtocol(string $protocol, callable ...$rules)
+    {
+        $protocol = strtolower($protocol);
+        if (empty($rules)) {
+            $rules = [];
+            switch ($protocol) {
+                case "file://":
+                    $rules[] = [$this, "validateLocalUri"];
+                    break;
+                case "http://":
+                case "https://":
+                    $rules[] = [$this, "validateRemoteUri"];
+                    break;
+                case "phar://":
+                    $rules[] = [$this, "validatePharUri"];
+                    break;
+            }
+        }
+        $this->allowedProtocols[$protocol] = ["rules" => $rules];
+        return $this;
+    }
+
     /**
      * @return array
      */
@@ -1011,4 +1095,56 @@ public function getHttpContext()
     {
         return $this->httpContext;
     }
+
+    public function validateLocalUri(string $uri)
+    {
+        if ($uri === null || strlen($uri) === 0) {
+            return [false, "The URI must not be empty."];
+        }
+
+        $realfile = realpath(str_replace("file://", "", $uri));
+
+        $dirs = $this->chroot;
+        $dirs[] = $this->rootDir;
+        $chrootValid = false;
+        foreach ($dirs as $chrootPath) {
+            $chrootPath = realpath($chrootPath);
+            if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
+                $chrootValid = true;
+                break;
+            }
+        }
+        if ($chrootValid !== true) {
+            return [false, "Permission denied. The file could not be found under the paths specified by Options::chroot."];
+        }
+
+        if (!$realfile) {
+            return [false, "File not found."];
+        }
+
+        return [true, null];
+    }
+
+    public function validatePharUri(string $uri)
+    {
+        if ($uri === null || strlen($uri) === 0) {
+            return [false, "The URI must not be empty."];
+        }
+
+        $file = substr(substr($uri, 0, strpos($uri, ".phar") + 5), 7);
+        return $this->validateLocalUri($file);
+    }
+
+    public function validateRemoteUri(string $uri)
+    {
+        if ($uri === null || strlen($uri) === 0) {
+            return [false, "The URI must not be empty."];
+        }
+
+        if (!$this->isRemoteEnabled) {
+            return [false, "Remote file requested, but remote file download is disabled."];
+        }
+
+        return [true, null];
+    }
 }
diff --git a/src/Renderer/AbstractRenderer.php b/src/Renderer/AbstractRenderer.php
index 4ba61c612..27dc365b2 100644
--- a/src/Renderer/AbstractRenderer.php
+++ b/src/Renderer/AbstractRenderer.php
@@ -97,7 +97,7 @@ protected function _background_image($url, $x, $y, $width, $height, $style)
             $sheet->get_protocol(),
             $sheet->get_host(),
             $sheet->get_base_path(),
-            $this->_dompdf
+            $this->_dompdf->getOptions()
         );
 
         // Bail if the image is no good
diff --git a/tests/Css/StyleTest.php b/tests/Css/StyleTest.php
index b09d1bd9e..45929fcb3 100644
--- a/tests/Css/StyleTest.php
+++ b/tests/Css/StyleTest.php
@@ -53,8 +53,8 @@ public function cssImageNoBaseHrefProvider(): array
     {
         $basePath = realpath(__DIR__ . "/..");
         return [
-            "local absolute" => ["url($basePath/_files/jamaica.jpg)", $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"],
-            "local relative" => ["url(../_files/jamaica.jpg)", $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"]
+            "local absolute" => ["url($basePath/_files/jamaica.jpg)", "file://" . $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"],
+            "local relative" => ["url(../_files/jamaica.jpg)", "file://" . $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"]
         ];
     }
 
@@ -62,8 +62,8 @@ public function cssImageWithBaseHrefProvider(): array
     {
         $basePath = realpath(__DIR__ . "/..");
         return [
-            "local absolute" => ["url($basePath/_files/jamaica.jpg)", $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"],
-            "local relative" => ["url(../_files/jamaica.jpg)", $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"]
+            "local absolute" => ["url($basePath/_files/jamaica.jpg)", "file://" . $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"],
+            "local relative" => ["url(../_files/jamaica.jpg)", "file://" . $basePath . DIRECTORY_SEPARATOR . "_files" . DIRECTORY_SEPARATOR . "jamaica.jpg"]
         ];
     }
 
diff --git a/tests/HelpersTest.php b/tests/HelpersTest.php
index aa2352d44..e69f2143b 100644
--- a/tests/HelpersTest.php
+++ b/tests/HelpersTest.php
@@ -101,4 +101,11 @@ public function testLengthEqual(float $a, float $b, bool $expected): void
         $this->assertSame($expected, Helpers::lengthEqual(-$a, -$b));
         $this->assertSame($expected, Helpers::lengthEqual(-$b, -$a));
     }
+
+    
+    public function testCustomProtocolParsing(): void
+    {
+        $uri = "mock://path/to/resource";
+        $this->assertSame($uri, Helpers::build_url("", "", "", $uri));
+    }
 }
diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php
index 2f051558b..5147ddee4 100644
--- a/tests/OptionsTest.php
+++ b/tests/OptionsTest.php
@@ -95,4 +95,44 @@ public function testSetters()
         $option->setChroot(['test11']);
         $this->assertEquals(['test11'], $option->getChroot());
     }
+
+    public function testAllowedProtocols()
+    {
+        $options = new Options(["isRemoteEnabled" => false]);
+        $options->setAllowedProtocols(["http://"]);
+        $allowedProtocols = $options->getAllowedProtocols();
+        $this->assertIsArray($allowedProtocols);
+        $this->assertEquals(1, count($allowedProtocols));
+        $this->assertArrayHasKey("http://", $allowedProtocols);
+        $this->assertIsArray($allowedProtocols["http://"]);
+        $this->assertArrayHasKey("rules", $allowedProtocols["http://"]);
+        $this->assertIsArray($allowedProtocols["http://"]["rules"]);
+        $this->assertEquals(1, count($allowedProtocols["http://"]["rules"]));
+        $this->assertEquals([$options, "validateRemoteUri"], $allowedProtocols["http://"]["rules"][0]);
+
+        [$validation_result] = $allowedProtocols["http://"]["rules"][0]("http://example.com/");
+        $this->assertFalse($validation_result);
+
+        
+        $mock_protocol = [
+            "mock://" => [
+                "rules" => [
+                    function ($uri) { return [true, null]; }
+                ]
+            ]
+        ];
+        $options->setAllowedProtocols($mock_protocol);
+        $allowedProtocols = $options->getAllowedProtocols();
+        $this->assertIsArray($allowedProtocols);
+        $this->assertEquals(1, count($allowedProtocols));
+        $this->assertArrayHasKey("mock://", $allowedProtocols);
+        $this->assertIsArray($allowedProtocols["mock://"]);
+        $this->assertArrayHasKey("rules", $allowedProtocols["mock://"]);
+        $this->assertIsArray($allowedProtocols["mock://"]["rules"]);
+        $this->assertEquals(1, count($allowedProtocols["mock://"]["rules"]));
+        $this->assertEquals($mock_protocol["mock://"]["rules"][0], $allowedProtocols["mock://"]["rules"][0]);
+
+        [$validation_result] = $allowedProtocols["mock://"]["rules"][0]("mock://example.com/");
+        $this->assertTrue($validation_result);
+    }
 }