1<?php 2 3/** 4 * A Swift Mailer plugin to download remote images and stylesheets then embed them. 5 * This also embeds local files from disk. 6 * Please read the LICENSE file 7 * @package Swift_Plugin 8 * @author Chris Corbyn <chris@w3style.co.uk> 9 * @license GNU Lesser General Public License 10 */ 11 12require_once dirname(__FILE__) . "/../ClassLoader.php"; 13Swift_ClassLoader::load("Swift_Events_BeforeSendListener"); 14 15/** 16 * Swift FileEmbedder Plugin to embed remote files. 17 * Scans a Swift_Message instance for remote files and then embeds them before sending. 18 * This also embeds local files from disk. 19 * @package Swift_Plugin 20 * @author Chris Corbyn <chris@w3style.co.uk> 21 */ 22class Swift_Plugin_FileEmbedder implements Swift_Events_BeforeSendListener 23{ 24 /** 25 * True if remote files will be embedded. 26 * @var boolean 27 */ 28 protected $embedRemoteFiles = true; 29 /** 30 * True if local files will be embedded. 31 * @var boolean 32 */ 33 protected $embedLocalFiles = true; 34 /** 35 * (X)HTML tag defintions listing allowed attributes and extensions. 36 * @var array 37 */ 38 protected $definitions = array( 39 "img" => array( 40 "attributes" => array("src"), 41 "extensions" => array("gif", "png", "jpg", "jpeg", "pjpeg") 42 ), 43 "link" => array( 44 "attributes" => array("href"), 45 "extensions" => array("css") 46 ), 47 "script" => array( 48 "attributes" => array("src"), 49 "extensions" => array("js") 50 )); 51 /** 52 * Protocols which may be used to download a remote file. 53 * @var array 54 */ 55 protected $protocols = array( 56 "http" => "http", 57 "https" => "https", 58 "ftp" => "ftp" 59 ); 60 /** 61 * A PCRE regexp which will be passed via sprintf() to produce a complete pattern. 62 * @var string 63 */ 64 protected $remoteFilePatternFormat = "~ 65 (<(?:%s)\\s+[^>]*? #Opening tag followed by (possible) attributes 66 (?:%s)=((?:\"|')?)) #Permitted attributes followed by (possible) quotation marks 67 ((?:%s)://[\\x01-\\x7F]*?(?:%s)?) #Remote URL (matching a permitted protocol) 68 (\\2[^>]*>) #Remaining attributes followed by end of tag 69 ~isx"; 70 /** 71 * A PCRE regexp which will be passed via sprintf() to produce a complete pattern. 72 * @var string 73 */ 74 protected $localFilePatternFormat = "~ 75 (<(?:%s)\\s+[^>]*? #Opening tag followed by (possible) attributes 76 (?:%s)=((?:\"|')?)) #Permitted attributes followed by (possible) quotation marks 77 ((?:/|[a-z]:\\\\|[a-z]:/)[\\x01-\\x7F]*?(?:%s)?) #Local, absolute path 78 (\\2[^>]*>) #Remaining attributes followed by end of tag 79 ~isx"; 80 /** 81 * A list of extensions mapping to their usual MIME types. 82 * @var array 83 */ 84 protected $mimeTypes = array( 85 "gif" => "image/gif", 86 "png" => "image/png", 87 "jpeg" => "image/jpeg", 88 "jpg" => "image/jpeg", 89 "pjpeg" => "image/pjpeg", 90 "js" => "text/javascript", 91 "css" => "text/css"); 92 /** 93 * Child IDs of files already embedded. 94 * @var array 95 */ 96 protected $registeredFiles = array(); 97 98 /** 99 * Get the MIME type based upon the extension. 100 * @param string The extension (sans the dot). 101 * @return string 102 */ 103 public function getType($ext) 104 { 105 $ext = strtolower($ext); 106 if (isset($this->mimeTypes[$ext])) 107 { 108 return $this->mimeTypes[$ext]; 109 } 110 else return null; 111 } 112 /** 113 * Add a new MIME type defintion (or overwrite an existing one). 114 * @param string The extension (sans the dot) 115 * @param string The MIME type (e.g. image/jpeg) 116 */ 117 public function addType($ext, $type) 118 { 119 $this->mimeTypes[strtolower($ext)] = strtolower($type); 120 } 121 /** 122 * Set the PCRE pattern which finds -full- HTML tags and copies the path for a local file into a backreference. 123 * The pattern contains three %s replacements for sprintf(). 124 * First replacement is the tag name (e.g. img) 125 * Second replacement is the attribute name (e.g. src) 126 * Third replacement is the file extension (e.g. jpg) 127 * This pattern should contain the full URL in backreference index 3. 128 * @param string sprintf() format string containing a PCRE regexp. 129 */ 130 public function setLocalFilePatternFormat($format) 131 { 132 $this->localFilePatternFormat = $format; 133 } 134 /** 135 * Gets the sprintf() format string for the PCRE pattern to scan for remote files. 136 * @return string 137 */ 138 public function getLocalFilePatternFormat() 139 { 140 return $this->localFilePatternFormat; 141 } 142 /** 143 * Set the PCRE pattern which finds -full- HTML tags and copies the URL for the remote file into a backreference. 144 * The pattern contains four %s replacements for sprintf(). 145 * First replacement is the tag name (e.g. img) 146 * Second replacement is the attribute name (e.g. src) 147 * Third replacement is the protocol (e.g. http) 148 * Fourth replacement is the file extension (e.g. jpg) 149 * This pattern should contain the full URL in backreference index 3. 150 * @param string sprintf() format string containing a PCRE regexp. 151 */ 152 public function setRemoteFilePatternFormat($format) 153 { 154 $this->remoteFilePatternFormat = $format; 155 } 156 /** 157 * Gets the sprintf() format string for the PCRE pattern to scan for remote files. 158 * @return string 159 */ 160 public function getRemoteFilePatternFormat() 161 { 162 return $this->remoteFilePatternFormat; 163 } 164 /** 165 * Add a new protocol which can be used to download files. 166 * Protocols should not include the "://" portion. This method expects alphanumeric characters only. 167 * @param string The protocol name (e.g. http or ftp) 168 */ 169 public function addProtocol($prot) 170 { 171 $prot = strtolower($prot); 172 $this->protocols[$prot] = $prot; 173 } 174 /** 175 * Remove a protocol from the list of allowed protocols once added. 176 * @param string The name of the protocol (e.g. http) 177 */ 178 public function removeProtocol($prot) 179 { 180 unset($this->protocols[strtolower($prot)]); 181 } 182 /** 183 * Get a list of all registered protocols. 184 * @return array 185 */ 186 public function getProtocols() 187 { 188 return array_values($this->protocols); 189 } 190 /** 191 * Add, or modify a tag definition. 192 * This affects how the plugins scans for files to download. 193 * @param string The name of a tag to search for (e.g. img) 194 * @param string The name of attributes to look for (e.g. src). You can pass an array if there are multiple possibilities. 195 * @param array A list of extensions to allow (sans dot). If there's only one you can just pass a string. 196 */ 197 public function setTagDefinition($tag, $attributes, $extensions) 198 { 199 $tag = strtolower($tag); 200 $attributes = (array)$attributes; 201 $extensions = (array)$extensions; 202 203 if (empty($tag) || empty($attributes) || empty($extensions)) 204 { 205 return null; 206 } 207 208 $this->definitions[$tag] = array("attributes" => $attributes, "extensions" => $extensions); 209 return true; 210 } 211 /** 212 * Remove a tag definition for remote files. 213 * @param string The name of the tag 214 */ 215 public function removeTagDefinition($tag) 216 { 217 unset($this->definitions[strtolower($tag)]); 218 } 219 /** 220 * Get a tag definition. 221 * Returns an array with indexes "attributes" and "extensions". 222 * Each element is an array listing the values within it. 223 * @param string The name of the tag 224 * @return array 225 */ 226 public function getTagDefinition($tag) 227 { 228 $tag = strtolower($tag); 229 if (isset($this->definitions[$tag])) return $this->definitions[$tag]; 230 else return null; 231 } 232 /** 233 * Get the PCRE pattern for a remote file based on the tag name. 234 * @param string The name of the tag 235 * @return string 236 */ 237 public function getRemoteFilePattern($tag_name) 238 { 239 $tag_name = strtolower($tag_name); 240 $pattern_format = $this->getRemoteFilePatternFormat(); 241 if ($def = $this->getTagDefinition($tag_name)) 242 { 243 $pattern = sprintf($pattern_format, $tag_name, implode("|", $def["attributes"]), 244 implode("|", $this->getProtocols()), implode("|", $def["extensions"])); 245 return $pattern; 246 } 247 else return null; 248 } 249 /** 250 * Get the PCRE pattern for a local file based on the tag name. 251 * @param string The name of the tag 252 * @return string 253 */ 254 public function getLocalFilePattern($tag_name) 255 { 256 $tag_name = strtolower($tag_name); 257 $pattern_format = $this->getLocalFilePatternFormat(); 258 if ($def = $this->getTagDefinition($tag_name)) 259 { 260 $pattern = sprintf($pattern_format, $tag_name, implode("|", $def["attributes"]), 261 implode("|", $def["extensions"])); 262 return $pattern; 263 } 264 else return null; 265 } 266 /** 267 * Register a file which has been downloaded so it doesn't need to be downloaded twice. 268 * @param string The remote URL 269 * @param string The ID as attached in the message 270 * @param Swift_Message_EmbeddedFile The file object itself 271 */ 272 public function registerFile($url, $cid, $file) 273 { 274 $url = strtolower($url); 275 if (!isset($this->registeredFiles[$url])) $this->registeredFiles[$url] = array("cids" => array(), "obj" => null); 276 $this->registeredFiles[$url]["cids"][] = $cid; 277 if (empty($this->registeredFiles[$url]["obj"])) $this->registeredFiles[$url]["obj"] = $file; 278 } 279 /** 280 * Turn on or off remote file embedding. 281 * @param boolean 282 */ 283 public function setEmbedRemoteFiles($set) 284 { 285 $this->embedRemoteFiles = (bool)$set; 286 } 287 /** 288 * Returns true if remote files can be embedded, or false if not. 289 * @return boolean 290 */ 291 public function getEmbedRemoteFiles() 292 { 293 return $this->embedRemoteFiles; 294 } 295 /** 296 * Turn on or off local file embedding. 297 * @param boolean 298 */ 299 public function setEmbedLocalFiles($set) 300 { 301 $this->embedLocalFiles = (bool)$set; 302 } 303 /** 304 * Returns true if local files can be embedded, or false if not. 305 * @return boolean 306 */ 307 public function getEmbedLocalFiles() 308 { 309 return $this->embedLocalFiles; 310 } 311 /** 312 * Callback method for preg_replace(). 313 * Embeds files which have been found during scanning. 314 * @param array Backreferences from preg_replace() 315 * @return string The tag with it's URL replaced with a CID 316 */ 317 protected function embedRemoteFile($matches) 318 { 319 $url = preg_replace("~^([^#]+)#.*\$~s", "\$1", $matches[3]); 320 $bits = parse_url($url); 321 $ext = preg_replace("~^.*?\\.([^\\.]+)\$~s", "\$1", $bits["path"]); 322 323 $lower_url = strtolower($url); 324 if (array_key_exists($lower_url, $this->registeredFiles)) 325 { 326 $registered = $this->registeredFiles[$lower_url]; 327 foreach ($registered["cids"] as $cid) 328 { 329 if ($this->message->hasChild($cid)) 330 { 331 return $matches[1] . $cid . $matches[4]; 332 } 333 } 334 //If we get here the file is downloaded, but not embedded 335 $cid = $this->message->attach($registered["obj"]); 336 $this->registerFile($url, $cid, $registered["obj"]); 337 return $matches[1] . $cid . $matches[4]; 338 } 339 $magic_quotes = get_magic_quotes_runtime(); 340 set_magic_quotes_runtime(0); 341 $filedata = @file_get_contents($url); 342 set_magic_quotes_runtime($magic_quotes); 343 if (!$filedata) 344 { 345 return $matches[1] . $matches[3] . $matches[4]; 346 } 347 $filename = preg_replace("~^.*/([^/]+)\$~s", "\$1", $url); 348 $att = new Swift_Message_EmbeddedFile($filedata, $filename, $this->getType($ext)); 349 $id = $this->message->attach($att); 350 $this->registerFile($url, $id, $att); 351 return $matches[1] . $id . $matches[4]; 352 } 353 /** 354 * Callback method for preg_replace(). 355 * Embeds files which have been found during scanning. 356 * @param array Backreferences from preg_replace() 357 * @return string The tag with it's path replaced with a CID 358 */ 359 protected function embedLocalFile($matches) 360 { 361 $path = realpath($matches[3]); 362 if (!$path) 363 { 364 return $matches[1] . $matches[3] . $matches[4]; 365 } 366 $ext = preg_replace("~^.*?\\.([^\\.]+)\$~s", "\$1", $path); 367 368 $lower_path = strtolower($path); 369 if (array_key_exists($lower_path, $this->registeredFiles)) 370 { 371 $registered = $this->registeredFiles[$lower_path]; 372 foreach ($registered["cids"] as $cid) 373 { 374 if ($this->message->hasChild($cid)) 375 { 376 return $matches[1] . $cid . $matches[4]; 377 } 378 } 379 //If we get here the file is downloaded, but not embedded 380 $cid = $this->message->attach($registered["obj"]); 381 $this->registerFile($path, $cid, $registered["obj"]); 382 return $matches[1] . $cid . $matches[4]; 383 } 384 $filename = basename($path); 385 $att = new Swift_Message_EmbeddedFile(new Swift_File($path), $filename, $this->getType($ext)); 386 $id = $this->message->attach($att); 387 $this->registerFile($path, $id, $att); 388 return $matches[1] . $id . $matches[4]; 389 } 390 /** 391 * Empty out the cache of registered files. 392 */ 393 public function clearCache() 394 { 395 $this->registeredFiles = null; 396 $this->registeredFiles = array(); 397 } 398 /** 399 * Swift's BeforeSendListener required method. 400 * Runs just before Swift sends a message. Here is where we do all the replacements. 401 * @param Swift_Events_SendEvent 402 */ 403 public function beforeSendPerformed(Swift_Events_SendEvent $e) 404 { 405 $this->message = $e->getMessage(); 406 407 foreach ($this->message->listChildren() as $id) 408 { 409 $part = $this->message->getChild($id); 410 $body = $part->getData(); 411 if (!is_string($body) || substr(strtolower($part->getContentType()), 0, 5) != "text/") continue; 412 413 foreach ($this->definitions as $tag_name => $def) 414 { 415 if ($this->getEmbedRemoteFiles()) 416 { 417 $re = $this->getRemoteFilePattern($tag_name); 418 $body = preg_replace_callback($re, array($this, "embedRemoteFile"), $body); 419 } 420 421 if ($this->getEmbedLocalFiles()) 422 { 423 $re = $this->getLocalFilePattern($tag_name); 424 $body = preg_replace_callback($re, array($this, "embedLocalFile"), $body); 425 } 426 } 427 428 $part->setData($body); 429 } 430 } 431} 432