1: <?php
  2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17: 
 18: class IMP_Mime_Viewer_Html extends Horde_Mime_Viewer_Html
 19: {
 20:     const CSS_BG_PREG = '/(background(?:-image)?:[^;\}]*(?:url\(["\']?))(.*?)((?:["\']?\)))/i';
 21: 
 22:      23:  24:  25:  26: 
 27:     protected $_imptmp = array();
 28: 
 29:      30:  31:  32:  33: 
 34:     protected $_capability = array(
 35:         'full' => true,
 36:         'info' => true,
 37:         'inline' => true,
 38:         'raw' => false
 39:     );
 40: 
 41:      42:  43:  44:  45: 
 46:     protected function _render()
 47:     {
 48:         return array(
 49:             $this->_mimepart->getMimeId() => $this->_IMPrender(false)
 50:         );
 51:     }
 52: 
 53:      54:  55:  56:  57: 
 58:     protected function _renderInline()
 59:     {
 60:         $data = $this->_IMPrender(true);
 61: 
 62:         if (!is_null($this->_imptmp)) {
 63:             $uid = strval(new Horde_Support_Randomid());
 64: 
 65:             Horde::addScriptFile('imp.js', 'imp');
 66: 
 67:             $data['js'] = array('IMP_JS.iframeInject("' . $uid . '", ' . Horde_Serialize::serialize($data['data'], Horde_Serialize::JSON, $this->_mimepart->getCharset()) . ')');
 68:             $data['data'] = '<div>' . _("Loading...") . '</div><iframe class="htmlMsgData" id="' . $uid . '" src="javascript:false" frameborder="0" style="display:none"></iframe>';
 69:             $data['type'] = 'text/html; charset=UTF-8';
 70:         }
 71: 
 72:         return array(
 73:             $this->_mimepart->getMimeId() => $data
 74:         );
 75:     }
 76: 
 77:      78:  79:  80:  81: 
 82:     protected function _renderInfo()
 83:     {
 84:         if ($this->canRender('inline') ||
 85:             ($this->_mimepart->getDisposition() == 'attachment')) {
 86:             return array();
 87:         }
 88: 
 89:         $status = new IMP_Mime_Status(array(
 90:             _("This message part contains HTML data, but inline HTML display is disabled."),
 91:             $this->getConfigParam('imp_contents')->linkViewJS($this->_mimepart, 'view_attach', _("View HTML data in new window.")),
 92:             $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)))
 93:         ));
 94:         $status->icon('mime/html.png');
 95: 
 96:         return array(
 97:             $this->_mimepart->getMimeId() => array(
 98:                 'data' => '',
 99:                 'status' => $status,
100:                 'type' => 'text/html; charset=' . $this->getConfigParam('charset')
101:             )
102:         );
103:     }
104: 
105:     106: 107: 108: 109: 110: 111: 
112:     protected function _IMPrender($inline)
113:     {
114:         $data = $this->_mimepart->getContents();
115: 
116:         117: 
118:         if ((IMP::getViewMode() == 'mimp') ||
119:             (!$inline && Horde_Util::getFormData('convert_text'))) {
120:             $this->_imptmp = null;
121:         } else {
122:             if ($inline) {
123:                 $imgview = new IMP_Ui_Imageview();
124:                 $blockimg = !$imgview->showInlineImage($this->getConfigParam('imp_contents'));
125:             } else {
126:                 $blockimg = false;
127:             }
128: 
129:             $this->_imptmp = array(
130:                 'blockimg' => null,
131:                 'cid' => null,
132:                 'cid_used' => array(),
133:                 'cssblock' => false,
134:                 'img' => $blockimg,
135:                 'imgblock' => false,
136:                 'inline' => $inline,
137:                 'style' => array(),
138:                 'target' => strval(new Horde_Support_Randomid())
139:             );
140: 
141:             
142:             if ($this->_imptmp['img']) {
143:                 $this->_imptmp['blockimg'] = Horde::url(Horde_Themes::img('spacer_red.png'), true, array('append_session' => -1));
144:             }
145: 
146:             147: 
148:             $this->_imptmp['cid'] = array();
149:             if (($related_part = $this->getConfigParam('imp_contents')->findMimeType($this->_mimepart->getMimeId(), 'multipart/related')) &&
150:                 ($related_cids = $related_part->getMetadata('related_cids'))) {
151:                 $cid_replace = array();
152: 
153:                 foreach ($related_cids as $mime_id => $cid) {
154:                     if ($cid = trim($cid, '<>')) {
155:                         $cid_replace['cid:' . $cid] = $mime_id;
156:                     }
157:                 }
158: 
159:                 if (!empty($cid_replace)) {
160:                     $this->_imptmp['cid'] = $cid_replace;
161:                 }
162:             }
163:         }
164: 
165:         
166:         $data = $this->_cleanHTML($data, array(
167:             'noprefetch' => ($inline && (IMP::getViewMode() != 'mimp')),
168:             'phishing' => $inline
169:         ));
170: 
171:         $status = array();
172:         if ($this->_phishWarn) {
173:             $status[] = new IMP_Mime_Status(array(
174:                 sprintf(_("%s: This message may not be from whom it claims to be. Beware of following any links in it or of providing the sender with any personal information."), _("Warning")),
175:                 _("The links that caused this warning have this background color:") . ' <span style="' . $this->_phishCss . '">' . _("EXAMPLE") . '</span>'
176:             ));
177:         }
178: 
179:         180: 
181:         if (is_null($this->_imptmp)) {
182:             $data = $this->_textFilter($data, 'Html2text', array(
183:                 'wrap' => false
184:             ));
185: 
186:             
187:             return array(
188:                 'data' => IMP::filterText($data),
189:                 'type' => 'text/plain; charset=' . $this->getConfigParam('charset')
190:             );
191:         }
192: 
193:         if ($this->_imptmp['imgblock']) {
194:             $tmp = new IMP_Mime_Status(array(
195:                 _("Images have been blocked in this message part."),
196:                 Horde::link('#', '', 'unblockImageLink') . _("Show Images?") . '</a>'
197:             ));
198:             $tmp->icon('mime/image.png');
199:             $status[] = $tmp;
200:         } elseif ($this->_imptmp['cssblock']) {
201:             202: 
203:             $tmp = new IMP_Mime_Status(array(
204:                 _("Message styling has been suppressed in this message part since the style data lives on a remote server."),
205:                 Horde::link('#', '', 'unblockImageLink') . _("Load Styling?") . '</a>'
206:             ));
207:             $tmp->icon('mime/image.png');
208:             $status[] = $tmp;
209:         }
210: 
211:         $filters = array();
212:         if ($GLOBALS['prefs']->getValue('emoticons')) {
213:             $filters['emoticons'] = array(
214:                 'entities' => true
215:             );
216:         }
217: 
218:         if ($inline) {
219:             $filters['emails'] = array(
220:                 'callback' => array($this, 'emailsCallback')
221:             );
222:         }
223: 
224:         if (!empty($filters)) {
225:             $data = $this->_textFilter($data, array_keys($filters), array_values($filters));
226:         }
227: 
228:         
229:         $data = IMP::filterText($data);
230: 
231:         
232:         if (!empty($this->_imptmp['cid'])) {
233:             $related_part->setMetadata('related_cids_used', $this->_imptmp['cid_used']);
234:         }
235: 
236:         return array(
237:             'data' => $data,
238:             'status' => $status,
239:             'type' => $this->_mimepart->getType(true)
240:         );
241:     }
242: 
243:     244: 245: 246: 247: 
248:     protected function _inAddressBook()
249:     {
250:         if (!$this->getConfigParam('imp_contents')) {
251:             return false;
252:         }
253: 
254:         $from = Horde_Mime_Address::bareAddress($this->getConfigParam('imp_contents')->getHeader()->getValue('from'));
255: 
256:         if ($GLOBALS['prefs']->getValue('html_image_addrbook') &&
257:             $GLOBALS['registry']->hasMethod('contacts/getField')) {
258:             $params = IMP::getAddressbookSearchParams();
259:             try {
260:                 if ($GLOBALS['registry']->call('contacts/getField', array($from, '__key', $params['sources'], true, true))) {
261:                     return true;
262:                 }
263:             } catch (Horde_Exception $e) {}
264:         }
265: 
266:         
267:         $safe_addrs = $this->getConfigParam('safe_addrs');
268:         return (!empty($safe_addrs) && in_array($from, $safe_addrs));
269:     }
270: 
271:     272: 273: 274: 275: 276: 277: 278: 279: 
280:     public function emailsCallback($args, $extra)
281:     {
282:         return IMP::composeLink($args, $extra, true);
283:     }
284: 
285:     286: 
287:     protected function _node($doc, $node)
288:     {
289:         parent::_node($doc, $node);
290: 
291:         if ($doc == $node) {
292:             
293:             if ($this->_imptmp && !empty($this->_imptmp['style'])) {
294:                 
295:                 try {
296:                     $style = $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter(implode("\n", $this->_imptmp['style']), 'csstidy', array(
297:                         'ob' => true,
298:                         'preserve_css' => false
299:                     ));
300: 
301:                     $blocked = array();
302:                     foreach ($style->import as $val) {
303:                         $blocked[] = '@import "' . $val . '";';
304:                     }
305:                     $style->import = array();
306: 
307:                     foreach ($style->css as $key => $val) {
308:                         foreach ($val as $key2 => $val2) {
309:                             foreach ($val2 as $key3 => $val3) {
310:                                 foreach ($val3['p'] as $key4 => $val4) {
311:                                     if (preg_match('/^\s*url\(["\']?.*?["\']?\)/i', $val4)) {
312:                                         $blocked[] = $key2 . '{' . $key3 . ':' . $val4 . ';}';
313:                                         unset($style->css[$key][$key2][$key3]['p'][$key4]);
314:                                     }
315:                                 }
316:                             }
317:                         }
318:                     }
319: 
320:                     $css_text = $style->print->plain();
321: 
322:                     if ($css_text || !empty($blocked)) {
323:                         
324:                         
325:                         $head = $doc->getElementsByTagName('head');
326:                         if ($head->length) {
327:                             $headelt = $head->item(0);
328:                         } else {
329:                             $headelt = $doc->createElement('head');
330:                             $doc->appendChild($headelt);
331:                         }
332:                     }
333: 
334:                     if ($css_text) {
335:                         $style_elt = $doc->createElement('style', $css_text);
336:                         $style_elt->setAttribute('type', 'text/css');
337:                         $headelt->appendChild($style_elt);
338:                     }
339: 
340:                     341: 342: 343: 
344:                     if (!empty($blocked)) {
345:                         $block_elt = $doc->createElement('style', implode('', $blocked));
346:                         $block_elt->setAttribute('type', 'text/x-imp-cssblocked');
347:                         $headelt->appendChild($block_elt);
348:                     }
349:                 } catch (Horde_Exception $e) {}
350:             }
351:         }
352:     }
353: 
354:     355: 356: 357: 358: 359: 
360:     protected function _nodeCallback($doc, $node)
361:     {
362:         if (is_null($this->_imptmp)) {
363:             return;
364:         }
365: 
366:         if ($node instanceof DOMElement) {
367:             $tag = Horde_String::lower($node->tagName);
368: 
369:             switch ($tag) {
370:             case 'a':
371:             case 'area':
372:                 373: 
374:                 if ($node->hasAttribute('href')) {
375:                     $url = parse_url($node->getAttribute('href'));
376:                     if (isset($url['scheme']) && ($url['scheme'] == 'mailto')) {
377:                         378: 
379:                         $node->setAttribute('href', IMP::composeLink($node->getAttribute('href'), array(), true));
380:                         $node->removeAttribute('target');
381:                     } elseif ($this->_imptmp['inline'] &&
382:                               isset($url['fragment']) &&
383:                               empty($url['path']) &&
384:                               $GLOBALS['browser']->isBrowser('mozilla')) {
385:                         386: 
387:                         $node->removeAttribute('href');
388:                     } elseif (!$node->hasAttribute('target') ||
389:                               Horde_String::lower($node->getAttribute('target')) == '_self') {
390:                         $node->setAttribute('target', $this->_imptmp['target']);
391:                     }
392:                 }
393:                 break;
394: 
395:             case 'img':
396:             case 'input':
397:                 if ($this->_imptmp && $node->hasAttribute('src')) {
398:                     $val = $node->getAttribute('src');
399: 
400:                     
401:                     if (($tag == 'img') &&
402:                         isset($this->_imptmp['cid'][$val])) {
403:                         $this->_imptmp['cid_used'][] = $this->_imptmp['cid'][$val];
404:                         $val = $this->getConfigParam('imp_contents')->urlView(null, 'view_attach', array('params' => array(
405:                             'ctype' => 'image/*',
406:                             'id' => $this->_imptmp['cid'][$val],
407:                             'imp_img_view' => 'data'
408:                         )));
409:                         $node->setAttribute('src', $val);
410:                     }
411: 
412:                     
413:                     if ($this->_imptmp['img']) {
414:                         $node->setAttribute('htmlimgblocked', $val);
415:                         $node->setAttribute('src', $this->_imptmp['blockimg']);
416:                         $this->_imptmp['imgblock'] = true;
417:                     }
418:                 }
419:                 break;
420: 
421:             case 'link':
422:                 423: 424: 425: 
426:                 $delete_link = true;
427: 
428:                 switch (Horde_String::lower($node->getAttribute('type'))) {
429:                 case 'text/css':
430:                     if ($this->_imptmp && $node->hasAttribute('href')) {
431:                         $tmp = $node->getAttribute('href');
432: 
433:                         if (isset($this->_imptmp['cid'][$tmp])) {
434:                             $this->_imptmp['style'][] = $this->getConfigParam('imp_contents')->getMIMEPart($this->_imptmp['cid'][$tmp])->getContents();
435:                         } else {
436:                             $node->setAttribute('htmlcssblocked', $node->getAttribute('href'));
437:                             $node->removeAttribute('href');
438:                             $this->_imptmp['cssblock'] = true;
439:                             $delete_link = false;
440:                         }
441:                     }
442:                     break;
443:                 }
444: 
445:                 if ($delete_link &&
446:                     $node->hasAttribute('href') &&
447:                     $node->parentNode) {
448:                     $node->parentNode->removeChild($node);
449:                 }
450:                 break;
451: 
452:             case 'style':
453:                 switch (Horde_String::lower($node->getAttribute('type'))) {
454:                 case 'text/css':
455:                     if ($this->_imptmp) {
456:                         $this->_imptmp['style'][] = $node->nodeValue;
457:                     }
458:                     $node->parentNode->removeChild($node);
459:                     break;
460:                 }
461:                 break;
462: 
463:             case 'table':
464:                 465: 466: 
467:                 if ($this->_imptmp['inline'] &&
468:                     $node->hasAttribute('height') &&
469:                     ($node->getAttribute('height') == '100%')) {
470:                     $node->removeAttribute('height');
471:                 }
472: 
473:                 
474: 
475:             case 'body':
476:             case 'td':
477:                 if ($this->_imptmp &&
478:                     $node->hasAttribute('background')) {
479:                     $val = $node->getAttribute('background');
480: 
481:                     
482:                     if (isset($this->_imptmp['cid'][$val])) {
483:                         $this->_imptmp['cid_used'][] = $this->_imptmp['cid'][$val];
484:                         $val = $this->getConfigParam('imp_contents')->urlView(null, 'view_attach', array('params' => array(
485:                             'id' => $this->_imptmp['cid'][$val],
486:                             'imp_img_view' => 'data'
487:                         )));
488:                         $node->setAttribute('background', $val);
489:                     }
490: 
491:                     
492:                     if ($this->_imptmp['img']) {
493:                         $node->setAttribute('htmlimgblocked', $val);
494:                         $node->setAttribute('background', $this->_imptmp['blockimg']);
495:                         $this->_imptmp['imgblock'] = true;
496:                     }
497:                 }
498:                 break;
499:             }
500: 
501: 
502:             if (!is_null($this->_imptmp)) {
503:                 $remove = array();
504:                 foreach ($node->attributes as $val) {
505:                     506: 
507:                     if (stripos($val->value, 'mailto:') === 0) {
508:                         $remove[] = $val->name;
509:                     }
510:                 }
511: 
512:                 foreach ($remove as $val) {
513:                     $node->removeAttribute($val);
514:                 }
515: 
516:                 if ($node->hasAttribute('style')) {
517:                     if (strpos($node->getAttribute('style'), 'content:') !== false) {
518:                         
519:                         $node->removeAttribute('style');
520:                     } elseif ($this->_imptmp['img'] || $this->_imptmp['cid']) {
521:                         $this->_imptmp['node'] = $node;
522:                         $style = preg_replace_callback(self::CSS_BG_PREG, array($this, '_styleCallback'), $node->getAttribute('style'), -1, $matches);
523:                         if ($matches) {
524:                             $node->setAttribute('style', $style);
525:                         }
526:                     }
527:                 }
528:             }
529:         }
530:     }
531: 
532:     533: 534: 535: 536: 537: 538: 539: 
540:     protected function _styleCallback($matches)
541:     {
542:         if (isset($this->_imptmp['cid'][$matches[2]])) {
543:             $replace = $this->getConfigParam('imp_contents')->urlView(null, 'view_attach', array('params' => array(
544:                 'id' => $this->_imptmp['cid'][$matches[2]],
545:                 'imp_img_view' => 'data'
546:             )));
547:             $this->_imptmp['cid_used'][] = $this->_imptmp['cid'][$matches[2]];
548:         } else {
549:             $this->_imptmp['node']->setAttribute('htmlimgblocked', $matches[2]);
550:             $this->_imptmp['imgblock'] = true;
551:             $replace = $this->_imptmp['blockimg'];
552:         }
553:         return $matches[1] . $replace . $matches[3];
554:     }
555: 
556: }
557: