PageRenderTime 24ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/swiftmailer/lib/Swift/Plugin/FileEmbedder.php

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