1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
13:
14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
25: class IMP_Mime_Viewer_Html extends Horde_Mime_Viewer_Html
26: {
27:
28: const CSS_BG_PREG = '/(background(?:-image)?:[^;\}]*(?:url\(["\']?))(.*?)((?:["\']?\)))/i';
29:
30:
31: const CSSBLOCK = 'htmlcssblocked';
32: const IMGBLOCK = 'htmlimgblocked';
33: const SRCSETBLOCK = 'htmlimgblocked_srcset';
34:
35: 36: 37: 38: 39:
40: protected $_imptmp = array();
41:
42: 43: 44: 45: 46:
47: protected $_capability = array(
48: 'full' => true,
49: 'info' => true,
50: 'inline' => true,
51: 'raw' => false
52: );
53:
54: 55: 56: 57: 58:
59: protected function _render()
60: {
61: return array(
62: $this->_mimepart->getMimeId() => $this->_IMPrender(false)
63: );
64: }
65:
66: 67: 68: 69: 70:
71: protected function _renderInline()
72: {
73: global $page_output, $registry;
74:
75: $data = $this->_IMPrender(true);
76:
77: switch ($view = $registry->getView()) {
78: case $registry::VIEW_MINIMAL:
79: $data['status'] = new IMP_Mime_Status(array(
80: _("This message part contains HTML data, but this data can not be displayed inline."),
81: $this->getConfigParam('imp_contents')->linkView($this->_mimepart, 'view_attach', _("View HTML data in new window."))
82: ));
83: break;
84:
85: default:
86: $uid = strval(new Horde_Support_Randomid());
87:
88: $page_output->addScriptPackage('IMP_Script_Package_Imp');
89:
90: $data['metadata'] = array(array('html', $uid, $data['data']));
91: $data['data'] = '<div>' . _("Loading...") . '</div><iframe class="htmlMsgData" id="' . $uid . '" src="javascript:false" frameborder="0" style="display:none;height:auto;"></iframe>';
92: $data['type'] = 'text/html; charset=UTF-8';
93: break;
94: }
95:
96: return array(
97: $this->_mimepart->getMimeId() => $data
98: );
99: }
100:
101: 102: 103: 104: 105:
106: protected function _renderInfo()
107: {
108: if ($this->canRender('inline') ||
109: ($this->_mimepart->getDisposition() == 'attachment')) {
110: return array();
111: }
112:
113: $status = new IMP_Mime_Status(array(
114: _("This message part contains HTML data, but inline HTML display is disabled."),
115: $this->getConfigParam('imp_contents')->linkViewJS($this->_mimepart, 'view_attach', _("View HTML data in new window.")),
116: $this->getConfigParam('imp_contents')->linkViewJS($this->_mimepart, 'view_attach', _("Convert HTML data to plain text and view in new window."), array('params' => array('convert_text' => 1)))
117: ));
118: $status->icon('mime/html.png');
119:
120: return array(
121: $this->_mimepart->getMimeId() => array(
122: 'data' => '',
123: 'status' => $status,
124: 'type' => 'text/html; charset=' . $this->getConfigParam('charset')
125: )
126: );
127: }
128:
129: 130: 131: 132: 133: 134: 135:
136: protected function _IMPrender($inline)
137: {
138: global $injector, $registry;
139:
140: $data = $this->_mimepart->getContents();
141: $view = $registry->getView();
142:
143: $contents = $this->getConfigParam('imp_contents');
144: $convert_text = ($view == $registry::VIEW_MINIMAL) ||
145: $injector->getInstance('Horde_Variables')->convert_text;
146:
147: 148:
149: $this->_imptmp = array();
150: if ($inline && !$convert_text) {
151: $this->_imptmp += array(
152: 'cid' => null,
153: 'cid_used' => array(),
154: 'cssblock' => false,
155: 'cssbroken' => false,
156: 'imgblock' => false,
157: 'imgbroken' => false,
158: 'inline' => $inline,
159: 'style' => array()
160: );
161: }
162:
163: 164:
165: if ($related_part = $contents->findMimeType($this->_mimepart->getMimeId(), 'multipart/related')) {
166: $this->_imptmp['cid'] = $related_part->getMetadata('related_ob');
167: }
168:
169:
170: $data = $this->_cleanHTML($data, array(
171: 'noprefetch' => ($inline && ($view != Horde_Registry::VIEW_MINIMAL)),
172: 'phishing' => $inline
173: ));
174:
175: if (!empty($this->_imptmp['style'])) {
176: $this->_processDomDocument($data->dom);
177: }
178:
179: if ($inline) {
180: $charset = 'UTF-8';
181: $data = $data->returnHtml(array(
182: 'charset' => $charset,
183: 'metacharset' => true
184: ));
185: } else {
186: $charset = $this->_mimepart->getCharset();
187: $data = $data->returnHtml();
188: }
189:
190: $status = array();
191: if ($this->_phishWarn) {
192: $status[] = new IMP_Mime_Status(array(
193: sprintf(_("%s: This message may not be from whom it claims to be."), _("WARNING")),
194: _("Beware of following any links in it or of providing the sender with any personal information."),
195: _("The links that caused this warning have this background color:") . ' <span style="' . $this->_phishCss . '">' . _("EXAMPLE LINK") . '</span>'
196: ));
197: }
198:
199: 200:
201: if ($convert_text) {
202: $data = $this->_textFilter($data, 'Html2text', array(
203: 'wrap' => false
204: ));
205:
206:
207: return array(
208: 'data' => IMP::filterText($data),
209: 'type' => 'text/plain; charset=' . $charset
210: );
211: }
212:
213: if ($inline) {
214: switch ($view) {
215: case $registry::VIEW_SMARTMOBILE:
216: if ($this->_imptmp['imgblock']) {
217: $tmp_txt = _("Show images...");
218: } elseif ($this->_imptmp['cssblock']) {
219: $tmp_txt = _("Load message styling...");
220: } else {
221: $tmp_txt = null;
222: }
223:
224: if (!is_null($tmp_txt)) {
225: $tmp = new IMP_Mime_Status(array(
226: '<a href="#unblock-image" data-role="button" data-theme="e">' . $tmp_txt . '</a>'
227: ));
228: $tmp->views = array($view);
229: $status[] = $tmp;
230: }
231: break;
232:
233: default:
234: $class = 'unblockImageLink';
235: if (!$injector->getInstance('IMP_Prefs_Special_ImageReplacement')->canAddToSafeAddrList() ||
236: $injector->getInstance('IMP_Identity')->hasAddress($contents->getHeader()->getOb('from'))) {
237: $class .= ' noUnblockImageAdd';
238: }
239:
240: $link = $text = null;
241: if ($this->_imptmp['imgblock']) {
242: $text = _("Images have been blocked in this message part.");
243: $link = _("Show Images?");
244: } elseif ($this->_imptmp['cssblock']) {
245: $text = _("Message styling has been suppressed in this message part since the style data lives on a remote server.");
246: $link = _("Load Styling?");
247: }
248:
249: if (!is_null($link)) {
250: $tmp = new IMP_Mime_Status(array(
251: $text,
252: Horde::link('#', '', $class, '', '', '', '', array(
253: 'muid' => strval($contents->getIndicesOb())
254: )) . $link . '</a>'
255: ));
256: $tmp->icon('mime/image.png');
257: $status[] = $tmp;
258: }
259:
260: if ($this->_imptmp['cssbroken']) {
261: $tmp = new IMP_Mime_Status_RenderIssue(array(
262: _("This message contains corrupt styling data so the message contents may not appear correctly below."),
263: $contents->linkViewJS($this->_mimepart, 'view_attach', _("Click to view HTML data in new window; it is possible this will allow you to view the message correctly."))
264: ));
265: $tmp->icon('mime/image.png');
266: $status[] = $tmp;
267: }
268:
269: if ($this->_imptmp['imgbroken']) {
270: $tmp = new IMP_Mime_Status_RenderIssue(array(
271: _("This message contains images that cannot be loaded.")
272: ));
273: $tmp->icon('mime/image.png');
274: $status[] = $tmp;
275: }
276: break;
277: }
278: }
279:
280:
281: if ($inline && !empty($this->_imptmp['cid'])) {
282: $related_part->setMetadata('related_cids_used', $this->_imptmp['cid_used']);
283: }
284:
285: return array(
286: 'data' => $data,
287: 'status' => $status,
288: 'type' => 'text/html; charset=' . $charset
289: );
290: }
291:
292: 293:
294: protected function _node($doc, $node)
295: {
296: parent::_node($doc, $node);
297:
298: if (empty($this->_imptmp) || !($node instanceof DOMElement)) {
299: if (($node instanceof DOMText) && ($node->length > 1)) {
300:
301: $text = IMP::filterText($node->data);
302: if ($node->data != $text) {
303: $node->replaceData(0, $node->length, $text);
304: }
305: }
306: return;
307: }
308:
309: $tag = Horde_String::lower($node->tagName);
310:
311: 312:
313: foreach ($node->attributes as $key => $val) {
314: if ($key == 'style') {
315:
316: $parts = array_filter(explode(';', $val->value));
317: foreach ($parts as $k2 => $v2) {
318: if (preg_match("/^\s*height:\s*/i", $v2)) {
319: unset($parts[$k2]);
320: }
321: }
322: $val->value = implode(';', $parts);
323: }
324: }
325:
326: switch ($tag) {
327: case 'a':
328: case 'area':
329: 330:
331: if ($node->hasAttribute('href')) {
332: $url = parse_url($node->getAttribute('href'));
333: if (isset($url['scheme']) && ($url['scheme'] == 'mailto')) {
334: 335:
336: $clink = new IMP_Compose_Link($node->getAttribute('href'));
337: $node->setAttribute('href', $clink->link(true));
338: $node->removeAttribute('target');
339: } elseif (!empty($this->_imptmp['inline']) &&
340: isset($url['fragment']) &&
341: empty($url['path']) &&
342: $GLOBALS['browser']->isBrowser('mozilla')) {
343: 344:
345: $node->removeAttribute('href');
346: } elseif (empty($url)) {
347: 348:
349: $node->removeAttribute('href');
350: $node->removeAttribute('target');
351: } else {
352: $node->setAttribute('target', strval(new Horde_Support_Randomid()));
353: }
354: }
355: break;
356:
357: case 'body':
358: $style = $node->hasAttribute('style')
359: ? (rtrim($node->getAttribute('style'), ';') . ';')
360: : '';
361: $node->setAttribute('style', $style . 'width:auto !important');
362: break;
363:
364: case 'img':
365: case 'input':
366: if ($node->hasAttribute('src')) {
367: $val = $node->getAttribute('src');
368:
369:
370: if (($tag == 'img') && ($id = $this->_cidSearch($val))) {
371: $val = $this->getConfigParam('imp_contents')->urlView(null, 'view_attach', array('params' => array(
372: 'ctype' => 'image/*',
373: 'id' => $id,
374: 'imp_img_view' => 'data'
375: )));
376: }
377:
378:
379: if ($this->_imgBlock()) {
380: if (Horde_Url_Data::isData($val)) {
381: $url = new Horde_Url_Data($val);
382: } else {
383: 384: 385:
386: $parsed_url = parse_url($val);
387: if (isset($parsed_url['host'])) {
388: $url = new Horde_Url($val);
389: $url->setScheme();
390: } else {
391: $url = null;
392: }
393: }
394:
395: if ($url) {
396: $node->setAttribute(self::IMGBLOCK, $url);
397: $node->setAttribute('src', $this->_imgBlockImg());
398: $this->_imptmp['imgblock'] = true;
399: } else {
400: $node->parentNode->removeChild($node);
401: $this->_imptmp['imgbroken'] = true;
402: }
403: } else {
404: $node->removeAttribute('src');
405: $node->setAttribute('data-src', $val);
406: }
407: }
408:
409:
410: if (($tag == 'img') &&
411: $this->_imgBlock() &&
412: $node->hasAttribute('srcset')) {
413: $node->setAttribute(self::SRCSETBLOCK, $node->getAttribute('srcset'));
414: $node->setAttribute('srcset', '');
415: $this->_imptmp['imgblock'] = true;
416: }
417: break;
418:
419: case 'link':
420: 421: 422: 423:
424: $delete_link = true;
425:
426: switch (Horde_String::lower($node->getAttribute('type'))) {
427: case 'text/css':
428: if ($node->hasAttribute('href')) {
429: $tmp = $node->getAttribute('href');
430:
431: if ($id = $this->_cidSearch($tmp, false)) {
432: $this->_imptmp['style'][] = $this->getConfigParam('imp_contents')->getMIMEPart($id)->getContents();
433: } elseif ($this->_imgBlock()) {
434: $node->setAttribute(self::CSSBLOCK, $node->getAttribute('href'));
435: $node->removeAttribute('href');
436: $this->_imptmp['cssblock'] = true;
437: $delete_link = false;
438: }
439: }
440: break;
441: }
442:
443: if ($delete_link &&
444: $node->hasAttribute('href') &&
445: $node->parentNode) {
446: $node->parentNode->removeChild($node);
447: }
448: break;
449:
450: case 'style':
451: switch (Horde_String::lower($node->getAttribute('type'))) {
452: case 'text/css':
453: $this->_imptmp['style'][] = str_replace(
454: array('<!--', '-->'),
455: '',
456: $node->nodeValue
457: );
458: $node->parentNode->removeChild($node);
459: break;
460: }
461: break;
462:
463: case 'table':
464: 465:
466: if (!empty($this->_imptmp['inline']) &&
467: $node->hasAttribute('height') &&
468: ($node->getAttribute('height') == '100%')) {
469: $node->removeAttribute('height');
470: }
471:
472:
473:
474: case 'body':
475: case 'td':
476: if ($node->hasAttribute('background')) {
477: $val = $node->getAttribute('background');
478:
479:
480: if ($id = $this->_cidSearch($val)) {
481: $val = $this->getConfigParam('imp_contents')->urlView(null, 'view_attach', array('params' => array(
482: 'id' => $id,
483: 'imp_img_view' => 'data'
484: )));
485: $node->setAttribute('background', $val);
486: }
487:
488:
489: if ($this->_imgBlock()) {
490: $node->setAttribute(self::IMGBLOCK, $val);
491: $node->setAttribute('background', $this->_imgBlockImg());
492: $this->_imptmp['imgblock'] = true;
493: }
494: }
495: break;
496: }
497:
498: $remove = array();
499: foreach ($node->attributes as $val) {
500: 501:
502: if (stripos($val->value, 'mailto:') === 0) {
503: $remove[] = $val->name;
504: }
505: }
506:
507: foreach ($remove as $val) {
508: $node->removeAttribute($val);
509: }
510:
511: if ($node->hasAttribute('style')) {
512: if (strpos($node->getAttribute('style'), 'content:') !== false) {
513:
514: $node->removeAttribute('style');
515: } elseif (!empty($this->_imptmp['cid']) || $this->_imgBlock()) {
516: $this->_imptmp['node'] = $node;
517: $style = preg_replace_callback(self::CSS_BG_PREG, array($this, '_styleCallback'), $node->getAttribute('style'), -1, $matches);
518: if ($matches) {
519: $node->setAttribute('style', $style);
520: }
521: }
522: }
523: }
524:
525: 526:
527: protected function _processDomDocument($doc)
528: {
529: try {
530: $css = new Horde_Css_Parser(implode("\n", $this->_imptmp['style']));
531: } catch (Exception $e) {
532: 533:
534: $this->_imptmp['cssbroken'] = true;
535: return;
536: }
537: $blocked = clone $css;
538:
539:
540: $css_text = $this->_parseCss($css, false);
541:
542: 543:
544: $blocked_text = $this->_parseCss($blocked, true);
545:
546: if (strlen($css_text) || strlen($blocked_text)) {
547:
548: $head = $doc->getElementsByTagName('head');
549: if ($head->length) {
550: $headelt = $head->item(0);
551: } else {
552: $headelt = $doc->createElement('head');
553: $doc->appendChild($headelt);
554: }
555: } else {
556: $headelt = $doc->createElement('head');
557: $doc->appendChild($headelt);
558: }
559:
560: if (strlen($css_text)) {
561: $style_elt = $doc->createElement('style');
562: $style_elt->appendChild(new DOMText($css_text));
563: $style_elt->setAttribute('type', 'text/css');
564: $headelt->appendChild($style_elt);
565: }
566:
567: 568: 569:
570: if (strlen($blocked_text)) {
571: $block_elt = $doc->createElement('style');
572: $block_elt->appendChild(new DOMText($blocked_text));
573: $block_elt->setAttribute('type', 'text/x-imp-cssblocked');
574: $headelt->appendChild($block_elt);
575: }
576: }
577:
578: 579:
580: protected function _parseCss($css, $blocked)
581: {
582: foreach ($css->doc->getContents() as $val) {
583: if ($val instanceof Sabberworm\CSS\RuleSet\RuleSet) {
584: foreach ($val->getRules() as $val2) {
585: $item = $val2->getValue();
586:
587: if ($item instanceof Sabberworm\CSS\Value\URL) {
588: if (!$blocked) {
589: $val->removeRule($val2);
590: }
591: } elseif ($item instanceof Sabberworm\CSS\Value\RuleValueList) {
592: $components = $item->getListComponents();
593: foreach ($components as $key3 => $val3) {
594: if ($val3 instanceof Sabberworm\CSS\Value\URL) {
595: if (!$blocked) {
596: unset($components[$key3]);
597: }
598: } elseif ($blocked) {
599: unset($components[$key3]);
600: }
601: }
602: $item->setListComponents($components);
603: } elseif ($blocked) {
604: $val->removeRule($val2);
605: }
606: }
607: } elseif ($val instanceof Sabberworm\CSS\Property\Import) {
608: if (!$blocked) {
609: $css->doc->remove($val);
610: }
611: } elseif ($blocked) {
612: $css->doc->remove($val);
613: }
614: }
615:
616: return $css->compress();
617: }
618:
619: 620: 621: 622: 623: 624: 625: 626:
627: protected function _styleCallback($matches)
628: {
629: if ($id = $this->_cidSearch($matches[2])) {
630: $replace = $this->getConfigParam('imp_contents')->urlView(null, 'view_attach', array('params' => array(
631: 'id' => $id,
632: 'imp_img_view' => 'data'
633: )));
634: } else {
635: $this->_imptmp['node']->setAttribute(self::IMGBLOCK, $matches[2]);
636: $this->_imptmp['imgblock'] = true;
637: $replace = $this->_imgBlockImg();
638: }
639: return $matches[1] . $replace . $matches[3];
640: }
641:
642: 643: 644: 645: 646: 647: 648: 649:
650: protected function _cidSearch($cid, $save = true)
651: {
652: if (empty($this->_imptmp['cid']) ||
653: (strpos($cid, 'cid:') !== 0) ||
654: !($id = $this->_imptmp['cid']->cidSearch(substr($cid, 4)))) {
655: return null;
656: }
657:
658: if ($save) {
659: $this->_imptmp['cid_used'][] = $id;
660: }
661:
662: return $id;
663: }
664:
665: 666: 667: 668: 669:
670: protected function _imgBlock()
671: {
672: global $injector;
673:
674: 675:
676: if (!isset($this->_imptmp['img'])) {
677: $this->_imptmp['img'] =
678: ($this->_imptmp['inline'] &&
679: !$injector->getInstance('IMP_Images')->showInlineImage($this->getConfigParam('imp_contents')));
680: }
681:
682: return $this->_imptmp['img'];
683: }
684:
685: 686: 687: 688: 689:
690: protected function _imgBlockImg()
691: {
692: if (!isset($this->_imptmp['blockimg'])) {
693: $this->_imptmp['blockimg'] = strval(Horde_Themes::img('spacer_red.png'));
694: }
695:
696: return $this->_imptmp['blockimg'];
697: }
698:
699: }
700: