1 : <?php
2 : /**
3 : * Magentron EmailImages Extension
4 : *
5 : * @category Magentron
6 : * @package Magentron_EmailImages
7 : * @author Jeroen Derks
8 : * @copyright Copyright (c) 2011 Jeroen Derks http://www.magentron.com
9 : * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
10 : */
11 : class Magentron_EmailImages_Helper_Data extends Mage_Core_Helper_Abstract
12 1 : {
13 : /** Configuration path for extension enablement.
14 : * @var string
15 : * @see isEnabled()
16 : */
17 : const XML_EMAIL_IMAGES_ENABLE = 'system/emailimages/enable';
18 :
19 : /** Configuration path for maximum cache lifetime.
20 : * @var string
21 : * @see getCacheTime()
22 : */
23 : const XML_EMAIL_IMAGES_CACHE_TIME = 'system/emailimages/cache_time';
24 :
25 : /** Configuration path for regular expression used to find image URLs in HTML.
26 : * @var string
27 : * @see getRegularExpression()
28 : */
29 : const XML_EMAIL_IMAGES_REGEXP = 'system/emailimages/regexp';
30 :
31 : /** Configuration path for index into matches of regular expression used to find image URLs in HTML.
32 : * @var string
33 : * @see XML_EMAIL_IMAGES_REGEXP,
34 : * getRegularExpressionIndex()
35 : */
36 : const XML_EMAIL_IMAGES_REGEXP_INDEX = 'system/emailimages/regexp_index';
37 :
38 : /** Default maximum lifetime used for cache.
39 : * @var integer
40 : * @see getCacheTime()
41 : */
42 : const DEFAULT_CACHE_TIME = 86400;
43 :
44 : /** Default regular expression to extract image URLs from the email HTML body.
45 : * @var string
46 : * @see getRegularExpression()
47 : */
48 : const DEFAULT_REGEXP = '/(<[iI][mM][gG] [sS][rR][cC]="|image:url\(\'?)([^\'"\)]*)(["\'\)])/';
49 :
50 : /** Default index in the regular expression matches to extract the image URLs from the email HTML body.
51 : * @var integer
52 : * @see getRegularExpressionIndex()
53 : */
54 : const DEFAULT_REGEXP_INDEX = 2;
55 :
56 : /** Cache tag to use.
57 : * @var string
58 : */
59 : const CACHE_TAG = 'MAGENTRON_EMAILIMAGES';
60 :
61 : /** Cache type to use.
62 : * @var string
63 : */
64 : const CACHE_TYPE = 'emailimages';
65 :
66 :
67 : /**
68 : * Is the EmailImages extension enabled to actually attach images?
69 : *
70 : * @return boolean
71 : *
72 : * @see XML_EMAIL_IMAGES_ENABLE
73 : */
74 : public function isEnabled()
75 : {
76 8 : return (boolean) Mage::getStoreConfig(self::XML_EMAIL_IMAGES_ENABLE);
77 : }
78 :
79 :
80 : /**
81 : * Retrieve the maximum lifetime for caching in seconds.
82 : *
83 : * @return integer
84 : *
85 : * @see XML_EMAIL_IMAGES_CACHE_TIME, DEFAULT_CACHE_TIME
86 : */
87 : public function getCacheTime()
88 : {
89 3 : $config = Mage::getStoreConfig(self::XML_EMAIL_IMAGES_CACHE_TIME);
90 :
91 3 : if ( !is_numeric($config) )
92 3 : $config = self::DEFAULT_CACHE_TIME;
93 :
94 3 : return (integer) $config;
95 : }
96 :
97 :
98 : /**
99 : * Retrieve the regular expression to extract the image URLs from the email HTML body.
100 : *
101 : * @return string
102 : *
103 : * @see XML_EMAIL_IMAGES_REGEXP, DEFAULT_REGEXP
104 : */
105 : public function getRegularExpression()
106 : {
107 5 : $config = Mage::getStoreConfig(self::XML_EMAIL_IMAGES_REGEXP);
108 :
109 5 : if ( '' == $config )
110 5 : $config = self::DEFAULT_REGEXP;
111 :
112 5 : return (string) $config;
113 : }
114 :
115 :
116 : /**
117 : * Retrieve the index in the regular expression matches to extract the image URLs from the email HTML body.
118 : *
119 : * @return integer
120 : *
121 : * @see XML_EMAIL_IMAGES_REGEXP_INDEX, DEFAULT_REGEXP_INDEX
122 : */
123 : public function getRegularExpressionIndex()
124 : {
125 7 : $config = Mage::getStoreConfig(self::XML_EMAIL_IMAGES_REGEXP_INDEX);
126 :
127 7 : if ( !is_numeric($config) )
128 7 : $config = self::DEFAULT_REGEXP_INDEX;
129 :
130 7 : return (integer) $config;
131 : }
132 :
133 : /**
134 : * Attach images to mail object.
135 : *
136 : * @param Zend_Mail $mail Zend_Mail instance to attach images to.
137 : * @param string $context [optional] Set to unique identifier for template, so that body needs to be parsed only once per template (NB: case-insensitive).
138 : * @return void Fails silently if unable to attach image, warning message sent to log.
139 : *
140 : * @see isEnabled(), _getImageUrlsFromMail(), _attachImageUrls()
141 : */
142 : public function addImages( Zend_Mail $mail, $context = null )
143 : {
144 : // check whether the administrator has enabled the module
145 6 : if ( !$this->isEnabled() )
146 6 : {
147 1 : Mage::log('EmailImages - extension disabled');
148 1 : return;
149 : }
150 :
151 : try
152 : {
153 5 : $urls = $this->_getImageUrlsFromMail($mail, $context);
154 : if ( $urls )
155 4 : $this->_attachImageUrls($mail, $urls);
156 : }
157 5 : catch ( Exception $e )
158 : {
159 : // ignore exception, but do log it
160 1 : Mage::log('EmailImages - ERROR: exception caught: ' . $e, Zend_Log::ERR);
161 : }
162 5 : }
163 :
164 :
165 : /**
166 : * Remove cached image and context data from the cache.
167 : *
168 : * @return Magentron_EmailImages_Helper_Data Provides fluent interface
169 : *
170 : * @see CACHE_TAG,
171 : * Mage_Core_Model_Cache::flush()
172 : */
173 : public function cleanCache()
174 : {
175 : /** @var $cache Mage_Core_Model_Cache */
176 1 : $cache = Mage::getSingleton('core/cache');
177 1 : $cache->flush(self::CACHE_TAG);
178 :
179 1 : return $this;
180 : }
181 :
182 : /**
183 : * Retrieve image URLs from email content
184 : *
185 : * @param Zend_Mail $mail Zend_Mail instance to attach images to.
186 : * @param string $context [optional] Set to unique identifier for template, so that body needs to be parsed only once per template (NB: case-insensitive).
187 : * @return array Array of image URLs.
188 : *
189 : * @see CACHE_TYPE,
190 : * _getContextDataFromCache(), _getBodyHtml(), _getImageUrlsFromBodyHtml(), _saveContextDataToCache(),
191 : * Mage_Core_Model_Cache::canUse()
192 : */
193 : protected function _getImageUrlsFromMail( Zend_Mail $mail, $context = null )
194 : {
195 : // check cache for context
196 : /** @var $cache Mage_Core_Model_Cache */
197 5 : $cache = Mage::getSingleton('core/cache');
198 5 : $context_data = null;
199 5 : $use_cache = null !== $context && $cache->canUse(self::CACHE_TYPE);
200 :
201 4 : Mage::log(__CLASS__ . '::' . __FUNCTION__ . '(): use_cache = ' . var_export($use_cache, 1));
202 :
203 : if ( $use_cache )
204 4 : {
205 1 : $context_cache_id = self::CACHE_TYPE . '-urls-' . (is_string($context) ? $context : md5(serialize($context)));
206 1 : $context_data = $this->_getContextDataFromCache($context_cache_id);
207 1 : }
208 :
209 4 : if ( !$context_data )
210 4 : {
211 4 : $bodyHtml = $this->_getBodyHtml($mail);
212 4 : $urls = $this->_getImageUrlsFromBodyHtml($bodyHtml);
213 :
214 : // save URLs to cache, if context defined
215 : if ( $use_cache )
216 4 : {
217 1 : $isHtml = (boolean) $bodyHtml;
218 :
219 1 : $this->_saveContextDataToCache($context_cache_id, $isHtml, $urls);
220 1 : }
221 4 : }
222 : else
223 : {
224 1 : $urls = $context_data['is_html'] ? $context_data['urls'] : array();
225 :
226 1 : Mage::log('EmailImages - loaded URLs from cache (cache ID: ' . $context_cache_id . ')');
227 : }
228 :
229 4 : return $urls;
230 : }
231 :
232 : /**
233 : * Retrieve image URL from email body HTML
234 : *
235 : * @param string $bodyHtml Email body HTML to use.
236 : * @return array Array of image URLs.
237 : *
238 : * @see getRegularExpression(), getRegularExpressionIndex()
239 : */
240 : protected function _getImageUrlsFromBodyHtml( $bodyHtml )
241 : {
242 4 : $urls = array();
243 :
244 4 : Mage::log('EmailImages - parsing HTML body');
245 :
246 : if ( $bodyHtml )
247 4 : {
248 : // find image URLs in email HTML body
249 4 : $regexp = $this->getRegularExpression();
250 4 : if ( preg_match_all($regexp, $bodyHtml, $matches) )
251 4 : {
252 4 : $index = $this->getRegularExpressionIndex();
253 4 : $urls = $matches[$index];
254 4 : $urls = array_unique($urls);
255 4 : }
256 :
257 4 : if ( 0 == count($urls) ) // no URLs in HTML body?
258 4 : Mage::log('EmailImages - no images found in email HTML body', Zend_Log::WARN);
259 4 : }
260 : else // otherwise no HTML body?
261 : {
262 3 : Mage::log('EmailImages - no HTML body for email', Zend_Log::WARN);
263 : }
264 :
265 4 : return $urls;
266 : }
267 :
268 : /**
269 : * Retrieve cached context data.
270 : *
271 : * @param string $context_cache_id Cached context data cache ID.
272 : * @return array Array containing keys 'isHtml' to indicate a HTML body, 'urls' for image URLs from that HTML body.
273 : *
274 : * @see Mage_Core_Model_Cache::load()
275 : */
276 : protected function _getContextDataFromCache( $context_cache_id )
277 : {
278 : /** @var $cache Mage_Core_Model_Cache */
279 1 : $cache = Mage::getSingleton('core/cache');
280 1 : $context_data = $cache->load($context_cache_id);
281 :
282 : if ( $context_data )
283 1 : $context_data = unserialize($context_data);
284 :
285 1 : Mage::log(__CLASS__ . '::' . __FUNCTION__ . '(): context_data=' . print_r($context_data, 1));
286 :
287 1 : return $context_data;
288 : }
289 :
290 : /**
291 : * Retrieve HTML body from mail object.
292 : *
293 : * @param Zend_Mail $mail Mail object instance to use.
294 : * @return string|false HTML body from the mail object instance, if any;
295 : * false, otherwise.
296 : *
297 : * @see Zend_Mail::getBodyHtml(), Zend_Mime_Part::getContent()
298 : */
299 : protected function _getBodyHtml( Zend_Mail $mail )
300 : {
301 4 : $bodyHtmlObject = $mail->getBodyHtml();
302 4 : if ( $bodyHtmlObject instanceof Zend_Mime_Part )
303 4 : {
304 4 : $bodyHtml = $bodyHtmlObject->getContent();
305 :
306 4 : if ( $bodyHtmlObject->encoding == Zend_Mime::ENCODING_QUOTEDPRINTABLE )
307 4 : $bodyHtml = quoted_printable_decode($bodyHtml);
308 4 : }
309 : else
310 : {
311 3 : $bodyHtml = $bodyHtmlObject;
312 : }
313 :
314 4 : if ( !is_string($bodyHtml) )
315 4 : {
316 3 : Mage::log(__CLASS__ . '::' . __FUNCTION__ . '(): unsupported bodyHtml = ' . substr(var_export($bodyHtml, 1), 0, 128) . '...');
317 :
318 3 : $bodyHtml = false;
319 3 : }
320 :
321 4 : Mage::log(__CLASS__ . '::' . __FUNCTION__ . '(): bodyHtml = ' . ($bodyHtml ? preg_replace('/[\s\t\r\n\k]+/', ' ', substr($bodyHtml, 0, 128)) : '<empty>'));
322 :
323 4 : return $bodyHtml;
324 : }
325 :
326 : /**
327 : * Save context data to cache.
328 : *
329 : * @param string $context_cache_id Cache ID to use.
330 : * @param boolean $isHtml Set to true if email body is HTML.
331 : * @param array $urls Array of image URLs from HTML body.
332 : * @return void
333 : *
334 : * @see CACHE_TAG,
335 : * getCacheTime(),
336 : * Mage_Core_Model_Cache::save()
337 : */
338 : protected function _saveContextDataToCache( $context_cache_id, $isHtml, $urls )
339 : {
340 : /** @var $cache Mage_Core_Model_Cache */
341 1 : $cache = Mage::getSingleton('core/cache');
342 1 : $cache_time = $this->getCacheTime();
343 :
344 : $context_data = array(
345 1 : 'urls' => $urls,
346 1 : 'is_html' => $isHtml,
347 1 : );
348 1 : $context_data = serialize($context_data);
349 :
350 1 : $cache->save($context_data, $context_cache_id, array( self::CACHE_TAG ), $cache_time);
351 :
352 1 : Mage::log('EmailImages - saved context URLs to cache');
353 1 : }
354 :
355 : /**
356 : * Attach image URLs to the email.
357 : *
358 : * @param Zend_Mail $mail Zend_Mail instance to attach images to.
359 : * @param array $urls Array of image URLs to attach.
360 : * @return void
361 : *
362 : * @see _retrieveImageData(),
363 : * Zend_Mime::MULTIPART_RELATED,
364 : * Zend_Mail::createAttachment(), Zend_Mail::setType(),
365 : * Zend_Mime_Part
366 : */
367 : protected function _attachImageUrls( Zend_Mail $mail, array $urls )
368 : {
369 4 : foreach ( $urls as $index => $url )
370 : {
371 : if ( $url )
372 4 : {
373 : // retrieved image data
374 4 : $data = $this->_retrieveImageData($url);
375 :
376 : if ( $data )
377 4 : {
378 : // attach image data
379 4 : $mp = $mail->createAttachment($data['image']
380 4 : , $data['size']['mime']
381 4 : , Zend_Mime::DISPOSITION_INLINE
382 4 : , Zend_Mime::ENCODING_BASE64, basename($url)
383 4 : );
384 4 : $mp->id = md5($url);
385 4 : $mp->location = $url;
386 4 : }
387 : else
388 : {
389 2 : Mage::log('EmailImages - unable to retrieve image from URL ' . $url, Zend_Log::WARN);
390 :
391 : // remove images that failed to load
392 2 : UnSet($urls[$index]);
393 : }
394 4 : }
395 4 : }
396 :
397 : // set Content-Type to multipart/related to properly display the images inline, if any
398 4 : if ( 0 < count($urls) )
399 4 : $mail->setType(Zend_Mime::MULTIPART_RELATED);
400 4 : }
401 :
402 : /**
403 : * Retrieve image data from URL.
404 : * NB: uses file_get_contents().
405 : *
406 : * @TODO should we try to use curl if available? should that be configurable by admin?
407 : *
408 : * @param string $url URL to retrieve image data from.
409 : * @return array|false Array with keys 'image' for image binary, 'size' for image size, if successfully retrieved image from URL;
410 : * false, otherwise.
411 : *
412 : * @see CACHE_TAG, CACHE_TYPE,
413 : * getCacheTime(),
414 : * Mage_Core_Model_Cache::canUse(), Mage_Core_Model_Cache::load(), Mage_Core_Model_Cache::save(),
415 : * file_get_contents()
416 : */
417 : protected function _retrieveImageData( $url )
418 : {
419 : // retrieve image from cache or URL
420 : /** @var $cache Mage_Core_Model_Cache */
421 4 : $cache = Mage::getSingleton('core/cache');
422 4 : $use_cache = $cache->canUse(self::CACHE_TYPE);
423 4 : $data = false;
424 :
425 : if ( $use_cache )
426 4 : {
427 1 : $cache_id = self::CACHE_TYPE . $url;
428 1 : $data = $cache->load(self::CACHE_TYPE . $url);
429 :
430 : if ( $data )
431 1 : $data = unserialize($data);
432 1 : }
433 :
434 4 : if ( !$data )
435 4 : {
436 : // retrieve image data from URL
437 4 : Mage::log('EmailImages - loading image from URL ' . $url);
438 :
439 4 : $data = file_get_contents($url);
440 : if ( $data )
441 4 : {
442 : $data = array(
443 4 : 'image' => $data,
444 4 : 'size' => getimagesize($url),
445 4 : );
446 4 : }
447 :
448 : // save retrieved image data to cache even if retrieving the image failed, if allowed
449 : if ( $use_cache )
450 4 : {
451 1 : $serialized_data = serialize($data);
452 1 : $cache_time = $this->getCacheTime();
453 :
454 1 : $cache->save($serialized_data, $cache_id, array( self::CACHE_TAG ), $cache_time);
455 :
456 1 : Mage::log('EmailImages - saved image to cache');
457 1 : }
458 4 : }
459 : else
460 : {
461 1 : Mage::log('EmailImages - loaded image from cache');
462 : }
463 :
464 4 : return $data;
465 : }
|