| 1 |
<?php |
| 2 |
/** |
| 3 |
* @package dompdf |
| 4 |
* @link https://github.com/dompdf/dompdf |
| 5 |
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License |
| 6 |
*/ |
| 7 |
namespace Dompdf\Image; |
| 8 |
|
| 9 |
use Dompdf\Options; |
| 10 |
use Dompdf\Helpers; |
| 11 |
use Dompdf\Exception\ImageException; |
| 12 |
|
| 13 |
/** |
| 14 |
* Static class that resolves image urls and downloads and caches |
| 15 |
* remote images if required. |
| 16 |
* |
| 17 |
* @package dompdf |
| 18 |
*/ |
| 19 |
class Cache |
| 20 |
{ |
| 21 |
/** |
| 22 |
* Array of downloaded images. Cached so that identical images are |
| 23 |
* not needlessly downloaded. |
| 24 |
* |
| 25 |
* @var array |
| 26 |
*/ |
| 27 |
protected static $_cache = []; |
| 28 |
|
| 29 |
/** |
| 30 |
* @var array |
| 31 |
*/ |
| 32 |
protected static $tempImages = []; |
| 33 |
|
| 34 |
/** |
| 35 |
* The url to the "broken image" used when images can't be loaded |
| 36 |
* |
| 37 |
* @var string |
| 38 |
*/ |
| 39 |
public static $broken_image = "data:image/svg+xml;charset=utf8,%3C?xml version='1.0'?%3E%3Csvg width='64' height='64' xmlns='http://www.w3.org/2000/svg'%3E%3Cg%3E%3Crect stroke='%23666666' id='svg_1' height='60.499994' width='60.166667' y='1.666669' x='1.999998' stroke-width='1.5' fill='none'/%3E%3Cline stroke-linecap='null' stroke-linejoin='null' id='svg_3' y2='59.333253' x2='59.749916' y1='4.333415' x1='4.250079' stroke-width='1.5' stroke='%23999999' fill='none'/%3E%3Cline stroke-linecap='null' stroke-linejoin='null' id='svg_4' y2='59.999665' x2='4.062838' y1='3.750342' x1='60.062164' stroke-width='1.5' stroke='%23999999' fill='none'/%3E%3C/g%3E%3C/svg%3E"; |
| 40 |
|
| 41 |
public static $error_message = "Image not found or type unknown"; |
| 42 |
|
| 43 |
/** |
| 44 |
* Resolve and fetch an image for use. |
| 45 |
* |
| 46 |
* @param string $url The url of the image |
| 47 |
* @param string $protocol Default protocol if none specified in $url |
| 48 |
* @param string $host Default host if none specified in $url |
| 49 |
* @param string $base_path Default path if none specified in $url |
| 50 |
* @param Options $options An instance of Dompdf\Options |
| 51 |
* |
| 52 |
* @return array An array with three elements: The local path to the image, the image |
| 53 |
* extension, and an error message if the image could not be cached |
| 54 |
*/ |
| 55 |
static function resolve_url($url, $protocol, $host, $base_path, Options $options) |
| 56 |
{ |
| 57 |
$tempfile = null; |
| 58 |
$resolved_url = null; |
| 59 |
$type = null; |
| 60 |
$message = null; |
| 61 |
|
| 62 |
try { |
| 63 |
$full_url = Helpers::build_url($protocol, $host, $base_path, $url); |
| 64 |
|
| 65 |
if ($full_url === null) { |
| 66 |
throw new ImageException("Unable to parse image URL $url.", E_WARNING); |
| 67 |
} |
| 68 |
|
| 69 |
$parsed_url = Helpers::explode_url($full_url); |
| 70 |
$protocol = strtolower($parsed_url["protocol"]); |
| 71 |
$is_data_uri = strpos($protocol, "data:") === 0; |
| 72 |
|
| 73 |
if (!$is_data_uri) { |
| 74 |
$allowed_protocols = $options->getAllowedProtocols(); |
| 75 |
if (!array_key_exists($protocol, $allowed_protocols)) { |
| 76 |
throw new ImageException("Permission denied on $url. The communication protocol is not supported.", E_WARNING); |
| 77 |
} |
| 78 |
foreach ($allowed_protocols[$protocol]["rules"] as $rule) { |
| 79 |
[$result, $message] = $rule($full_url); |
| 80 |
if (!$result) { |
| 81 |
throw new ImageException("Error loading $url: $message", E_WARNING); |
| 82 |
} |
| 83 |
} |
| 84 |
} |
| 85 |
|
| 86 |
if ($protocol === "file://") { |
| 87 |
$resolved_url = $full_url; |
| 88 |
} elseif (isset(self::$_cache[$full_url])) { |
| 89 |
$resolved_url = self::$_cache[$full_url]; |
| 90 |
} else { |
| 91 |
$tmp_dir = $options->getTempDir(); |
| 92 |
if (($resolved_url = @tempnam($tmp_dir, "ca_dompdf_img_")) === false) { |
| 93 |
throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING); |
| 94 |
} |
| 95 |
$tempfile = $resolved_url; |
| 96 |
|
| 97 |
$image = null; |
| 98 |
if ($is_data_uri) { |
| 99 |
if (($parsed_data_uri = Helpers::parse_data_uri($url)) !== false) { |
| 100 |
$image = $parsed_data_uri["data"]; |
| 101 |
} |
| 102 |
} else { |
| 103 |
list($image, $http_response_header) = Helpers::getFileContent($full_url, $options->getHttpContext()); |
| 104 |
} |
| 105 |
|
| 106 |
// Image not found or invalid |
| 107 |
if ($image === null) { |
| 108 |
$msg = ($is_data_uri ? "Data-URI could not be parsed" : "Image not found"); |
| 109 |
throw new ImageException($msg, E_WARNING); |
| 110 |
} |
| 111 |
|
| 112 |
if (@file_put_contents($resolved_url, $image) === false) { |
| 113 |
throw new ImageException("Unable to create temporary image in " . $tmp_dir, E_WARNING); |
| 114 |
} |
| 115 |
|
| 116 |
self::$_cache[$full_url] = $resolved_url; |
| 117 |
} |
| 118 |
|
| 119 |
// Check if the local file is readable |
| 120 |
if (!is_readable($resolved_url) || !filesize($resolved_url)) { |
| 121 |
throw new ImageException("Image not readable or empty", E_WARNING); |
| 122 |
} |
| 123 |
|
| 124 |
list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext()); |
| 125 |
|
| 126 |
if (($width && $height && in_array($type, ["gif", "png", "jpeg", "bmp", "svg","webp"], true)) === false) { |
| 127 |
throw new ImageException("Image type unknown", E_WARNING); |
| 128 |
} |
| 129 |
|
| 130 |
if ($type === "svg") { |
| 131 |
$parser = xml_parser_create("utf-8"); |
| 132 |
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false); |
| 133 |
xml_set_element_handler( |
| 134 |
$parser, |
| 135 |
function ($parser, $name, $attributes) use ($options, $parsed_url, $full_url) { |
| 136 |
if ($name === "image") { |
| 137 |
$attributes = array_change_key_case($attributes, CASE_LOWER); |
| 138 |
$url = $attributes["xlink:href"] ?? $attributes["href"]; |
| 139 |
if (!empty($url)) { |
| 140 |
$inner_full_url = Helpers::build_url($parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $url); |
| 141 |
if ($inner_full_url === $full_url) { |
| 142 |
throw new ImageException("SVG self-reference is not allowed", E_WARNING); |
| 143 |
} |
| 144 |
[$resolved_url, $type, $message] = self::resolve_url($url, $parsed_url["protocol"], $parsed_url["host"], $parsed_url["path"], $options); |
| 145 |
if (!empty($message)) { |
| 146 |
throw new ImageException("This SVG document references a restricted resource. $message", E_WARNING); |
| 147 |
} |
| 148 |
} |
| 149 |
} |
| 150 |
}, |
| 151 |
false |
| 152 |
); |
| 153 |
|
| 154 |
if (($fp = fopen($resolved_url, "r")) !== false) { |
| 155 |
while ($line = fread($fp, 8192)) { |
| 156 |
xml_parse($parser, $line, false); |
| 157 |
} |
| 158 |
fclose($fp); |
| 159 |
} |
| 160 |
xml_parser_free($parser); |
| 161 |
} |
| 162 |
} catch (ImageException $e) { |
| 163 |
if ($tempfile) { |
| 164 |
unlink($tempfile); |
| 165 |
} |
| 166 |
$resolved_url = self::$broken_image; |
| 167 |
list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext()); |
| 168 |
$message = self::$error_message; |
| 169 |
Helpers::record_warnings($e->getCode(), $e->getMessage() . " \n $url", $e->getFile(), $e->getLine()); |
| 170 |
self::$_cache[$full_url] = $resolved_url; |
| 171 |
} |
| 172 |
|
| 173 |
return [$resolved_url, $type, $message]; |
| 174 |
} |
| 175 |
|
| 176 |
/** |
| 177 |
* Register a temp file for the given original image file. |
| 178 |
* |
| 179 |
* @param string $filePath The path of the original image. |
| 180 |
* @param string $tempPath The path of the temp file to register. |
| 181 |
* @param string $key An optional key to register the temp file at. |
| 182 |
*/ |
| 183 |
static function addTempImage(string $filePath, string $tempPath, string $key = "default"): void |
| 184 |
{ |
| 185 |
if (!isset(self::$tempImages[$filePath])) { |
| 186 |
self::$tempImages[$filePath] = []; |
| 187 |
} |
| 188 |
|
| 189 |
self::$tempImages[$filePath][$key] = $tempPath; |
| 190 |
} |
| 191 |
|
| 192 |
/** |
| 193 |
* Get the path of a temp file registered for the given original image file. |
| 194 |
* |
| 195 |
* @param string $filePath The path of the original image. |
| 196 |
* @param string $key The key the temp file is registered at. |
| 197 |
*/ |
| 198 |
static function getTempImage(string $filePath, string $key = "default"): ?string |
| 199 |
{ |
| 200 |
return self::$tempImages[$filePath][$key] ?? null; |
| 201 |
} |
| 202 |
|
| 203 |
/** |
| 204 |
* Unlink all cached images (i.e. temporary images either downloaded |
| 205 |
* or converted) except for the bundled "broken image" |
| 206 |
*/ |
| 207 |
static function clear(bool $debugPng = false) |
| 208 |
{ |
| 209 |
foreach (self::$_cache as $file) { |
| 210 |
if ($file === self::$broken_image) { |
| 211 |
continue; |
| 212 |
} |
| 213 |
if ($debugPng) { |
| 214 |
print "[clear unlink $file]"; |
| 215 |
} |
| 216 |
if (file_exists($file)) { |
| 217 |
unlink($file); |
| 218 |
} |
| 219 |
} |
| 220 |
|
| 221 |
foreach (self::$tempImages as $versions) { |
| 222 |
foreach ($versions as $file) { |
| 223 |
if ($file === self::$broken_image) { |
| 224 |
continue; |
| 225 |
} |
| 226 |
if ($debugPng) { |
| 227 |
print "[unlink temp image $file]"; |
| 228 |
} |
| 229 |
if (file_exists($file)) { |
| 230 |
unlink($file); |
| 231 |
} |
| 232 |
} |
| 233 |
} |
| 234 |
|
| 235 |
self::$_cache = []; |
| 236 |
self::$tempImages = []; |
| 237 |
} |
| 238 |
|
| 239 |
static function detect_type($file, $context = null) |
| 240 |
{ |
| 241 |
list(, , $type) = Helpers::dompdf_getimagesize($file, $context); |
| 242 |
|
| 243 |
return $type; |
| 244 |
} |
| 245 |
|
| 246 |
static function is_broken($url) |
| 247 |
{ |
| 248 |
return $url === self::$broken_image; |
| 249 |
} |
| 250 |
} |
| 251 |
|
| 252 |
if (file_exists(realpath(__DIR__ . "/../../lib/res/broken_image.svg"))) { |
| 253 |
Cache::$broken_image = realpath(__DIR__ . "/../../lib/res/broken_image.svg"); |
| 254 |
} |
| 255 |
|