Plugins like preview_pane should not close their own pages without displaying errors...
[squirrelmail.git] / functions / mime.php
index c86621a42155a6ea8b05c7bc6a6a1d2e5da4780a..5ff5e79bcb1f92e0eadc462eae851aa6ba5dded6 100644 (file)
@@ -6,7 +6,7 @@
  * This contains the functions necessary to detect and decode MIME
  * messages.
  *
- * @copyright © 1999-2006 The SquirrelMail Project Team
+ * @copyright © 1999-2007 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
@@ -57,6 +57,7 @@ function mime_structure ($bodystructure, $flags=array()) {
     $read = trim(substr ($read, 0, -1));
     $i = 0;
     $msg = Message::parseStructure($read,$i);
+
     if (!is_object($msg)) {
         global $color, $mailbox;
         /* removed urldecode because $_GET is auto urldecoded ??? */
@@ -350,7 +351,10 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
     global $startMessage, $languages, $squirrelmail_language,
            $show_html_default, $sort, $has_unsafe_images, $passed_ent_id,
            $use_iframe, $iframe_height, $download_and_unsafe_link,
-           $download_href, $unsafe_image_toggle_href, $unsafe_image_toggle_text;
+           $download_href, $unsafe_image_toggle_href, $unsafe_image_toggle_text,
+           $oTemplate;
+
+    $nbsp = $oTemplate->fetch('non_breaking_space.tpl');
 
     // workaround for not updated config.php
     if (! isset($use_iframe)) $use_iframe = false;
@@ -373,8 +377,9 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
                 $body = call_user_func($languages[$squirrelmail_language]['XTRA_CODE'] . '_decode',$body);
             }
         }
-        $hookResults = do_hook("message_body", $body);
-        $body = $hookResults[1];
+
+        /* As of 1.5.2, $body is passed (and modified) by reference */
+        do_hook('message_body', $body);
 
         /* If there are other types that shouldn't be formatted, add
          * them here.
@@ -417,7 +422,7 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
                 global $oTemplate;
                 $oTemplate->assign('iframe_url', $iframeurl);
                 $oTemplate->assign('html_body', $html_body);
-                
+
                 $body = $oTemplate->fetch('read_html_iframe.tpl');
             } else {
                 // old way of html rendering
@@ -448,7 +453,8 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
             $link .= '&passed_ent_id='.$passed_ent_id;
         }
         $download_href = SM_PATH . 'src/download.php?absolute_dl=true&' . $link;
-        $download_and_unsafe_link .= '&nbsp;|&nbsp;<a href="'. $download_href .'">' . _("Download this as a file") .  '</a>';
+        $download_and_unsafe_link .= "$nbsp|$nbsp" 
+            . create_hyperlink($download_href, _("Download this as a file"));
         if ($view_unsafe_images) {
             $text = _("Hide Unsafe Images");
         } else {
@@ -462,7 +468,8 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
         if($text != '') {
             $unsafe_image_toggle_href = SM_PATH . 'src/read_body.php?'.$link;
             $unsafe_image_toggle_text = $text;
-            $download_and_unsafe_link .= '&nbsp;|&nbsp;<a href="'. $unsafe_image_toggle_href .'">' . $text . '</a>';
+            $download_and_unsafe_link .= "$nbsp|$nbsp"
+                . create_hyperlink($unsafe_image_toggle_href, $text);
         }
     }
     return $body;
@@ -472,7 +479,7 @@ function formatBody($imap_stream, $message, $color, $wrap_at, $ent_num, $id, $ma
  * Generate attachments array for passing to templates.  Separated from
  * formatAttachments() below so that the same array can be given to the
  * print-friendly version.
- * 
+ *
  * @since 1.5.2
  * @param object $message SquirrelMail message object
  * @param array $exclude_id message parts that are not attachments.
@@ -496,7 +503,7 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
         $links['download link']['text'] = _("Download");
         $links['download link']['href'] = $base_uri .
             "src/download.php?absolute_dl=true&amp;passed_id=$id&amp;mailbox=$urlMailbox&amp;ent_id=$ent";
-            
+
         if ($type0 =='message' && $type1 == 'rfc822') {
             $default_page = $base_uri  . 'src/read_body.php';
             $rfc822_header = $att->rfc822_header;
@@ -540,22 +547,43 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
         if ($where && $what) {
             $defaultlink .= '&amp;where='. urlencode($where).'&amp;what='.urlencode($what);
         }
+        // IE does make use of mime content sniffing. Forcing a download
+        // prohibit execution of XSS inside an application/octet-stream attachment
+        if ($type0 == 'application' && $type1 == 'octet-stream') {
+            $defaultlink .= '&amp;absolute_dl=true';
+        }
 
         /* This executes the attachment hook with a specific MIME-type.
          * If that doesn't have results, it tries if there's a rule
-         * for a more generic type.
+         * for a more generic type. Finally, a hook for ALL attachment
+         * types is run as well.
          */
-        $hookresults = do_hook("attachment $type0/$type1", $links,
-                $startMessage, $id, $urlMailbox, $ent, $defaultlink,
-                $display_filename, $where, $what);
-        if(count($hookresults[1]) <= 1) {
-            $hookresults = do_hook("attachment $type0/*", $links,
-                    $startMessage, $id, $urlMailbox, $ent, $defaultlink,
-                    $display_filename, $where, $what);
+        /* The API for this hook has changed as of 1.5.2 so that all plugin
+           arguments are passed in an array instead of each their own plugin
+           argument, and arguments are passed by reference, so instead of
+           returning any changes, changes should simply be made to the original
+           arguments themselves. */
+        do_hook("attachment $type0/$type1", $temp=array(&$links,
+                &$startMessage, &$id, &$urlMailbox, &$ent, &$defaultlink,
+                &$display_filename, &$where, &$what));
+        if(count($links) <= 1) {
+            /* The API for this hook has changed as of 1.5.2 so that all plugin
+               arguments are passed in an array instead of each their own plugin
+               argument, and arguments are passed by reference, so instead of
+               returning any changes, changes should simply be made to the original
+               arguments themselves. */
+            do_hook("attachment $type0/*", $temp=array(&$links,
+                    &$startMessage, &$id, &$urlMailbox, &$ent, &$defaultlink,
+                    &$display_filename, &$where, &$what));
         }
-
-        $links = $hookresults[1];
-        $defaultlink = $hookresults[6];
+        /* The API for this hook has changed as of 1.5.2 so that all plugin
+           arguments are passed in an array instead of each their own plugin
+           argument, and arguments are passed by reference, so instead of
+           returning any changes, changes should simply be made to the original
+           arguments themselves. */
+        do_hook("attachment */*", $temp=array(&$links,
+                &$startMessage, &$id, &$urlMailbox, &$ent, &$defaultlink, 
+                &$display_filename, &$where, &$what));
 
         $this_attachment = array();
         $this_attachment['Name'] = decodeHeader($display_filename);
@@ -571,17 +599,17 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
                 continue;
             if (empty($val['text']) && empty($val['extra']))
                 continue;
-                
+
             $temp = array();
             $temp['HREF'] = $val['href'];
             $temp['Text'] = (empty($val['text']) ? '' : $val['text']) . (empty($val['extra']) ? '' : $val['extra']);
             $this_attachment['OtherLinks'][] = $temp;
         }
         $attachments[] = $this_attachment;
-        
+
         unset($links);
     }
-    
+
     return $attachments;
 }
 
@@ -602,7 +630,7 @@ function buildAttachmentArray($message, $exclude_id, $mailbox, $id) {
  */
 function formatAttachments($message, $exclude_id, $mailbox, $id) {
     global $oTemplate;
-    
+
     $attach = buildAttachmentArray($message, $exclude_id, $mailbox, $id);
 
     $oTemplate->assign('attachments', $attach);
@@ -649,7 +677,7 @@ function decodeBody($body, $encoding) {
     $body = str_replace("\r\n", "\n", $body);
     $encoding = strtolower($encoding);
 
-    $encoding_handler = do_hook_function('decode_body', $encoding);
+    $encoding_handler = do_hook('decode_body', $encoding);
 
 
     // plugins get first shot at decoding the body
@@ -1117,6 +1145,11 @@ function sq_defang(&$attvalue){
         return;
     }
     $m = false;
+    // before deent, translate the dangerous unicode characters and ... to safe values
+    // otherwise the regular expressions do not match.
+
+
+
     do {
         $m = false;
         $m = $m || sq_deent($attvalue, '/\&#0*(\d+);*/s');
@@ -1142,6 +1175,75 @@ function sq_unspace(&$attvalue){
     }
 }
 
+/**
+ * Translate all dangerous Unicode or Shift_JIS characters which are accepted by
+ * IE as regular characters.
+ *
+ * @param  attvalue  The attribute value before dangerous characters are translated.
+ * @return attvalue  Nothing, modifies a reference value.
+ * @author Marc Groot Koerkamp.
+ */
+function sq_fixIE_idiocy(&$attvalue) {
+    // remove NUL
+    $attvalue = str_replace("\0", "", $attvalue);
+    // remove comments
+    $attvalue = preg_replace("/(\/\*.*?\*\/)/","",$attvalue);
+
+    // IE has the evil habit of accepting every possible value for the attribute expression.
+    // The table below contains characters which are parsed by IE if they are used in the "expression"
+    // attribute value.
+    $aDangerousCharsReplacementTable = array(
+                        array('&#x029F;', '&#0671;' ,/* L UNICODE IPA Extension */
+                              '&#x0280;', '&#0640;' ,/* R UNICODE IPA Extension */
+                              '&#x0274;', '&#0628;' ,/* N UNICODE IPA Extension */
+                              '&#xFF25;', '&#65317' ,/* Unicode FULLWIDTH LATIN CAPITAL LETTER E */
+                              '&#xFF45;', '&#65349' ,/* Unicode FULLWIDTH LATIN SMALL LETTER E */
+                              '&#xFF38;', '&#65336;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER X */
+                              '&#xFF58;', '&#65368;',/* Unicode FULLWIDTH LATIN SMALL LETTER X */
+                              '&#xFF30;', '&#65328;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER P */
+                              '&#xFF50;', '&#65360;',/* Unicode FULLWIDTH LATIN SMALL LETTER P */
+                              '&#xFF32;', '&#65330;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER R */
+                              '&#xFF52;', '&#65362;',/* Unicode FULLWIDTH LATIN SMALL LETTER R */
+                              '&#xFF33;', '&#65331;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER S */
+                              '&#xFF53;', '&#65363;',/* Unicode FULLWIDTH LATIN SMALL LETTER S */
+                              '&#xFF29;', '&#65321;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER I */
+                              '&#xFF49;', '&#65353;',/* Unicode FULLWIDTH LATIN SMALL LETTER I */
+                              '&#xFF2F;', '&#65327;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER O */
+                              '&#xFF4F;', '&#65359;',/* Unicode FULLWIDTH LATIN SMALL LETTER O */
+                              '&#xFF2E;', '&#65326;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER N */
+                              '&#xFF4E;', '&#65358;',/* Unicode FULLWIDTH LATIN SMALL LETTER N */
+                              '&#xFF2C;', '&#65324;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER L */
+                              '&#xFF4C;', '&#65356;',/* Unicode FULLWIDTH LATIN SMALL LETTER L */
+                              '&#xFF35;', '&#65333;',/* Unicode FULLWIDTH LATIN CAPITAL LETTER U */
+                              '&#xFF55;', '&#65365;',/* Unicode FULLWIDTH LATIN SMALL LETTER U */
+                              '&#x207F;', '&#8319;' ,/* Unicode SUPERSCRIPT LATIN SMALL LETTER N */
+                              '&#x8264;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER E */   // in unicode this is some Chinese char range
+                              '&#x8285;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER E */
+                              '&#x8277;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER X */
+                              '&#x8298;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER X */
+                              '&#x826F;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER P */
+                              '&#x8290;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER P */
+                              '&#x8271;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER R */
+                              '&#x8292;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER R */
+                              '&#x8272;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER S */
+                              '&#x8293;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER S */
+                              '&#x8268;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER I */
+                              '&#x8289;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER I */
+                              '&#x826E;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER O */
+                              '&#x828F;', /* Shift JIS FULLWIDTH LATIN SMALL LETTER O */
+                              '&#x826D;', /* Shift JIS FULLWIDTH LATIN CAPITAL LETTER N */
+                              '&#x828E;'), /* Shift JIS FULLWIDTH LATIN SMALL LETTER N */
+                       array('l', 'l', 'r','r','n','n',
+                             'E','E','e','e','X','X','x','x','P','P','p','p','S','S','s','s','I','I',
+                             'i','i','O','O','o','o','N','N','n','n','L','L','l','l','U','U','u','u','n',
+                             'E','e','X','x','P','p','S','s','I','i','O','o','N','n'));
+    $attvalue = str_replace($aDangerousCharsReplacementTable[0],$aDangerousCharsReplacementTable[1],$attvalue);
+
+    // Escapes are useful for special characters like "{}[]()'&. In other cases they are
+    // used for XSS.
+    $attvalue = preg_replace("/(\\\\)([a-zA-Z]{1})/",'$2',$attvalue);
+}
+
 /**
  * This function returns the final tag out of the tag name, an array
  * of attributes, and the type of the tag. This function is called by
@@ -1555,6 +1657,8 @@ function sq_getnxtag($body, $offset){
 function sq_deent(&$attvalue, $regex, $hex=false){
     $me = 'sq_deent';
     $ret_match = false;
+    // remove comments
+    //$attvalue = preg_replace("/(\/\*.*\*\/)/","",$attvalue);
     preg_match_all($regex, $attvalue, $matches);
     if (is_array($matches) && sizeof($matches[0]) > 0){
         $repl = Array();
@@ -1608,10 +1712,23 @@ function sq_fixatts($tagname,
                 }
             }
         }
+        /**
+         * Workaround for IE quirks
+         */
+        sq_fixIE_idiocy($attvalue);
+
         /**
          * Remove any backslashes, entities, and extraneous whitespace.
          */
+
+        $oldattvalue = $attvalue;
         sq_defang($attvalue);
+        if ($attname == 'style' && $attvalue !== $oldattvalue) {
+            // entities are used in the attribute value. In 99% of the cases it's there as XSS
+            // i.e.<div style="{ left:exp&#x0280;essio&#x0274;( alert('XSS') ) }">
+            $attvalue = "idiocy";
+            $attary{$attname} = $attvalue;
+        }
         sq_unspace($attvalue);
 
         /**
@@ -1691,12 +1808,70 @@ function sq_fixatts($tagname,
 function sq_fixstyle($body, $pos, $message, $id, $mailbox){
     global $view_unsafe_images;
     $me = 'sq_fixstyle';
-    $ret = sq_findnxreg($body, $pos, '</\s*style\s*>');
-    if ($ret == FALSE){
+    // workaround for </style> in between comments
+    $iCurrentPos = $pos;
+    $content = '';
+    $sToken = '';
+    $bSucces = false;
+    $bEndTag = false;
+    for ($i=$pos,$iCount=strlen($body);$i<$iCount;++$i) {
+        $char = $body{$i};
+        switch ($char) {
+            case '<':
+                $sToken .= $char;
+                break;
+            case '/':
+                 if ($sToken == '<') {
+                    $sToken .= $char;
+                    $bEndTag = true;
+                 } else {
+                    $content .= $char;
+                 }
+                 break;
+            case '>':
+                 if ($bEndTag) {
+                    $sToken .= $char;
+                    if (preg_match('/\<\/\s*style\s*\>/i',$sToken,$aMatch)) {
+                        $newpos = $i + 1;
+                        $bSucces = true;
+                        break 2;
+                    } else {
+                        $content .= $sToken;
+                    }
+                    $bEndTag = false;
+                 } else {
+                    $content .= $char;
+                 }
+                 break;
+            case '!':
+                if ($sToken == '<') {
+                    // possible comment
+                    if (isset($body{$i+2}) && substr($body,$i,3) == '!--') {
+                        $i = strpos($body,'-->',$i+3);
+                        if ($i === false) { // no end comment
+                            $i = strlen($body);
+                        }
+                        $sToken = '';
+                    }
+                } else {
+                    $content .= $char;
+                }
+                break;
+            default:
+                if ($bEndTag) {
+                    $sToken .= $char;
+                } else {
+                    $content .= $char;
+                }
+                break;
+        }
+    }
+    if ($bSucces == FALSE){
         return array(FALSE, strlen($body));
     }
-    $newpos = $ret[0] + strlen($ret[2]);
-    $content = $ret[1];
+
+
+
     /**
     * First look for general BODY style declaration, which would be
     * like so:
@@ -1710,9 +1885,15 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
     */
     //   $content = preg_replace("|url\s*\(\s*([\'\"])\s*\S+script\s*:.*?([\'\"])\s*\)|si",
     //                           "url(\\1$secremoveimg\\2)", $content);
-    // remove NUL
-    $content = str_replace("\0", "", $content);
+
+    // IE Sucks hard. We have a special function for it.
+    sq_fixIE_idiocy($content);
+
+    // remove @import line
+    $content = preg_replace("/^\s*(@import.*)$/mi","\n<!-- @import rules forbidden -->\n",$content);
+
     // translate ur\l and variations (IE parses that)
+    // TODO check if the sq_fixIE_idiocy function already handles this.
     $content = preg_replace("/(\\\\)?u(\\\\)?r(\\\\)?l(\\\\)?/i", 'url', $content);
     // NB I insert NUL characters to keep to avoid an infinite loop. They are removed after the loop.
     while (preg_match("/url\s*\(\s*[\'\"]?([^:]+):(.*)?[\'\"]?\s*\)/si", $content, $matches)) {
@@ -1771,8 +1952,10 @@ function sq_fixstyle($body, $pos, $message, $id, $mailbox){
                     '/expression/i',
                     '/behaviou*r/i',
                     '/binding/i',
-                    '/include-source/i');
-    $replace = Array('','idiocy', 'idiocy', 'idiocy', 'idiocy');
+                    '/include-source/i',
+                    '/javascript/i',
+                    '/script/i');
+    $replace = Array('','idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy', 'idiocy');
     $contentNew = preg_replace($match, $replace, $contentTemp);
     if ($contentNew !== $contentTemp) {
         // insecure css declarations are used. From now on we don't care
@@ -2090,7 +2273,7 @@ function magicHTML($body, $id, $message, $mailbox = 'INBOX', $take_mailto_links
            $has_unsafe_images;
     /**
      * Don't display attached images in HTML mode.
-     * 
+     *
      * SB: why?
      */
     $attachment_common_show_images = false;
@@ -2385,11 +2568,14 @@ function SendDownloadHeaders($type0, $type1, $filename, $force, $filesize=0) {
 
             // This works for most types, but doesn't work with Word files
             header ("Content-Type: application/download; name=\"$filename\"");
-
+            header ("Content-Type: application/force-download; name=\"$filename\"");
             // These are spares, just in case.  :-)
             //header("Content-Type: $type0/$type1; name=\"$filename\"");
             //header("Content-Type: application/x-msdownload; name=\"$filename\"");
             //header("Content-Type: application/octet-stream; name=\"$filename\"");
+        } else if ($isIE) {
+             // This is to prevent IE for MIME sniffing and auto open a file in IE
+             header ("Content-Type: application/force-download; name=\"$filename\"");
         } else {
             // another application/octet-stream forces download for Netscape
             header ("Content-Type: application/octet-stream; name=\"$filename\"");