Aleksander Machniak
2012-12-07 996af3bfd9bfcac84396790a9a215d177b17c79e
commit | author | age
1c4f23 1 <?php
A 2
8b92d2 3 /*
1c4f23 4  +-----------------------------------------------------------------------+
A 5  | program/include/rcube_mime.php                                        |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
9  | Copyright (C) 2011-2012, Kolab Systems AG                             |
7fe381 10  |                                                                       |
T 11  | Licensed under the GNU General Public License version 3 or            |
12  | any later version with exceptions for skins & plugins.                |
13  | See the README file for a full license statement.                     |
1c4f23 14  |                                                                       |
A 15  | PURPOSE:                                                              |
16  |   MIME message parsing utilities                                      |
17  |                                                                       |
18  +-----------------------------------------------------------------------+
19  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
20  | Author: Aleksander Machniak <alec@alec.pl>                            |
21  +-----------------------------------------------------------------------+
22 */
23
24
25 /**
26  * Class for parsing MIME messages
27  *
9ab346 28  * @package    Framework
AM 29  * @subpackage Storage
30  * @author     Thomas Bruederli <roundcube@gmail.com>
31  * @author     Aleksander Machniak <alec@alec.pl>
1c4f23 32  */
A 33 class rcube_mime
34 {
eede51 35     private static $default_charset;
1c4f23 36
A 37
38     /**
39      * Object constructor.
40      */
41     function __construct($default_charset = null)
42     {
eede51 43         self::$default_charset = $default_charset;
0f5dee 44     }
AM 45
46
47     /**
48      * Returns message/object character set name
49      *
50      * @return string Characted set name
51      */
52     public static function get_charset()
53     {
54         if (self::$default_charset) {
55             return self::$default_charset;
1c4f23 56         }
0f5dee 57
AM 58         if ($charset = rcube::get_instance()->config->get('default_charset')) {
59             return $charset;
60         }
61
a92beb 62         return RCUBE_CHARSET;
1c4f23 63     }
A 64
65
66     /**
8b92d2 67      * Parse the given raw message source and return a structure
T 68      * of rcube_message_part objects.
69      *
70      * It makes use of the PEAR:Mail_mimeDecode library
71      *
72      * @param string  The message source
73      * @return object rcube_message_part The message structure
74      */
75     public static function parse_message($raw_body)
76     {
77         $mime = new Mail_mimeDecode($raw_body);
78         $struct = $mime->decode(array('include_bodies' => true, 'decode_bodies' => true));
79         return self::structure_part($struct);
80     }
81
82
83     /**
84      * Recursive method to convert a Mail_mimeDecode part into a rcube_message_part object
85      *
86      * @param object  A message part struct
87      * @param int     Part count
88      * @param string  Parent MIME ID
89      *
90      * @return object rcube_message_part
91      */
92     private static function structure_part($part, $count=0, $parent='')
93     {
94         $struct = new rcube_message_part;
95         $struct->mime_id = $part->mime_id ? $part->mime_id : (empty($parent) ? (string)$count : "$parent.$count");
96         $struct->headers = $part->headers;
97         $struct->ctype_primary = $part->ctype_primary;
98         $struct->ctype_secondary = $part->ctype_secondary;
99         $struct->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
100         $struct->ctype_parameters = $part->ctype_parameters;
101
102         if ($part->headers['content-transfer-encoding'])
103             $struct->encoding = $part->headers['content-transfer-encoding'];
104         if ($part->ctype_parameters['charset'])
105             $struct->charset = $part->ctype_parameters['charset'];
106
0f5dee 107         $part_charset = $struct->charset ? $struct->charset : self::get_charset();
8b92d2 108
77c779 109         // determine filename
8b92d2 110         if (($filename = $part->d_parameters['filename']) || ($filename = $part->ctype_parameters['name'])) {
T 111             $struct->filename = rcube_mime::decode_mime_string($filename, $part_charset);
112         }
113
114         // copy part body and convert it to UTF-8 if necessary
77c779 115         $struct->body = $part->ctype_primary == 'text' || !$part->ctype_parameters['charset'] ? rcube_charset::convert($part->body, $part_charset) : $part->body;
8b92d2 116         $struct->size = strlen($part->body);
T 117         $struct->disposition = $part->disposition;
118
119         foreach ((array)$part->parts as $child_part) {
120             $struct->parts[] = self::structure_part($child_part, ++$count, $struct->mime_id);
121         }
122
123         return $struct;
124     }
125
126
127     /**
1c4f23 128      * Split an address list into a structured array list
A 129      *
130      * @param string  $input    Input string
131      * @param int     $max      List only this number of addresses
132      * @param boolean $decode   Decode address strings
133      * @param string  $fallback Fallback charset if none specified
134      *
135      * @return array  Indexed list of addresses
136      */
137     static function decode_address_list($input, $max = null, $decode = true, $fallback = null)
138     {
139         $a   = self::parse_address_list($input, $decode, $fallback);
140         $out = array();
141         $j   = 0;
142
143         // Special chars as defined by RFC 822 need to in quoted string (or escaped).
144         $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
145
146         if (!is_array($a))
147             return $out;
148
149         foreach ($a as $val) {
150             $j++;
151             $address = trim($val['address']);
152             $name    = trim($val['name']);
153
154             if ($name && $address && $name != $address)
155                 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
156             else if ($address)
157                 $string = $address;
158             else if ($name)
159                 $string = $name;
160
161             $out[$j] = array(
162                 'name'   => $name,
163                 'mailto' => $address,
164                 'string' => $string
165             );
166
167             if ($max && $j==$max)
168                 break;
169         }
170
171         return $out;
172     }
173
174
175     /**
176      * Decode a message header value
177      *
178      * @param string  $input         Header value
179      * @param string  $fallback      Fallback charset if none specified
180      *
181      * @return string Decoded string
182      */
183     public static function decode_header($input, $fallback = null)
184     {
185         $str = self::decode_mime_string((string)$input, $fallback);
186
187         return $str;
188     }
189
190
191     /**
192      * Decode a mime-encoded string to internal charset
193      *
194      * @param string $input    Header value
195      * @param string $fallback Fallback charset if none specified
196      *
197      * @return string Decoded string
198      */
199     public static function decode_mime_string($input, $fallback = null)
200     {
0f5dee 201         $default_charset = !empty($fallback) ? $fallback : self::get_charset();
1c4f23 202
A 203         // rfc: all line breaks or other characters not found
204         // in the Base64 Alphabet must be ignored by decoding software
205         // delete all blanks between MIME-lines, differently we can
206         // receive unnecessary blanks and broken utf-8 symbols
207         $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
208
209         // encoded-word regexp
210         $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
211
212         // Find all RFC2047's encoded words
213         if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
214             // Initialize variables
215             $tmp   = array();
216             $out   = '';
217             $start = 0;
218
219             foreach ($matches as $idx => $m) {
220                 $pos      = $m[0][1];
221                 $charset  = $m[1][0];
222                 $encoding = $m[2][0];
223                 $text     = $m[3][0];
224                 $length   = strlen($m[0][0]);
225
226                 // Append everything that is before the text to be decoded
227                 if ($start != $pos) {
228                     $substr = substr($input, $start, $pos-$start);
eebd44 229                     $out   .= rcube_charset::convert($substr, $default_charset);
1c4f23 230                     $start  = $pos;
A 231                 }
232                 $start += $length;
233
234                 // Per RFC2047, each string part "MUST represent an integral number
235                 // of characters . A multi-octet character may not be split across
236                 // adjacent encoded-words." However, some mailers break this, so we
237                 // try to handle characters spanned across parts anyway by iterating
238                 // through and aggregating sequential encoded parts with the same
239                 // character set and encoding, then perform the decoding on the
240                 // aggregation as a whole.
241
242                 $tmp[] = $text;
243                 if ($next_match = $matches[$idx+1]) {
244                     if ($next_match[0][1] == $start
245                         && $next_match[1][0] == $charset
246                         && $next_match[2][0] == $encoding
247                     ) {
248                         continue;
249                     }
250                 }
251
252                 $count = count($tmp);
253                 $text  = '';
254
255                 // Decode and join encoded-word's chunks
256                 if ($encoding == 'B' || $encoding == 'b') {
257                     // base64 must be decoded a segment at a time
258                     for ($i=0; $i<$count; $i++)
259                         $text .= base64_decode($tmp[$i]);
260                 }
261                 else { //if ($encoding == 'Q' || $encoding == 'q') {
262                     // quoted printable can be combined and processed at once
263                     for ($i=0; $i<$count; $i++)
264                         $text .= $tmp[$i];
265
266                     $text = str_replace('_', ' ', $text);
267                     $text = quoted_printable_decode($text);
268                 }
269
eebd44 270                 $out .= rcube_charset::convert($text, $charset);
1c4f23 271                 $tmp = array();
A 272             }
273
274             // add the last part of the input string
275             if ($start != strlen($input)) {
eebd44 276                 $out .= rcube_charset::convert(substr($input, $start), $default_charset);
1c4f23 277             }
A 278
279             // return the results
280             return $out;
281         }
282
283         // no encoding information, use fallback
eebd44 284         return rcube_charset::convert($input, $default_charset);
1c4f23 285     }
A 286
287
288     /**
289      * Decode a mime part
290      *
291      * @param string $input    Input string
292      * @param string $encoding Part encoding
293      * @return string Decoded string
294      */
295     public static function decode($input, $encoding = '7bit')
296     {
297         switch (strtolower($encoding)) {
298         case 'quoted-printable':
299             return quoted_printable_decode($input);
300         case 'base64':
301             return base64_decode($input);
302         case 'x-uuencode':
303         case 'x-uue':
304         case 'uue':
305         case 'uuencode':
306             return convert_uudecode($input);
307         case '7bit':
308         default:
309             return $input;
310         }
311     }
312
313
314     /**
315      * Split RFC822 header string into an associative array
316      * @access private
317      */
318     public static function parse_headers($headers)
319     {
320         $a_headers = array();
321         $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
322         $lines = explode("\n", $headers);
323         $c = count($lines);
324
325         for ($i=0; $i<$c; $i++) {
326             if ($p = strpos($lines[$i], ': ')) {
327                 $field = strtolower(substr($lines[$i], 0, $p));
328                 $value = trim(substr($lines[$i], $p+1));
329                 if (!empty($value))
330                     $a_headers[$field] = $value;
331             }
332         }
333
334         return $a_headers;
335     }
336
337
338     /**
339      * @access private
340      */
341     private static function parse_address_list($str, $decode = true, $fallback = null)
342     {
343         // remove any newlines and carriage returns before
344         $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
345
346         // extract list items, remove comments
347         $str = self::explode_header_string(',;', $str, true);
348         $result = array();
349
350         // simplified regexp, supporting quoted local part
351         $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
352
353         foreach ($str as $key => $val) {
354             $name    = '';
355             $address = '';
356             $val     = trim($val);
357
358             if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
359                 $address = $m[2];
360                 $name    = trim($m[1]);
361             }
362             else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
363                 $address = $m[1];
364                 $name    = '';
365             }
366             else {
367                 $name = $val;
368             }
369
370             // dequote and/or decode name
371             if ($name) {
372                 if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
373                     $name = substr($name, 1, -1);
374                     $name = stripslashes($name);
375                 }
376                 if ($decode) {
377                     $name = self::decode_header($name, $fallback);
378                 }
379             }
380
381             if (!$address && $name) {
382                 $address = $name;
383             }
384
385             if ($address) {
386                 $result[$key] = array('name' => $name, 'address' => $address);
387             }
388         }
389
390         return $result;
391     }
392
393
394     /**
395      * Explodes header (e.g. address-list) string into array of strings
396      * using specified separator characters with proper handling
397      * of quoted-strings and comments (RFC2822)
398      *
399      * @param string $separator       String containing separator characters
400      * @param string $str             Header string
401      * @param bool   $remove_comments Enable to remove comments
402      *
403      * @return array Header items
404      */
405     public static function explode_header_string($separator, $str, $remove_comments = false)
406     {
407         $length  = strlen($str);
408         $result  = array();
409         $quoted  = false;
410         $comment = 0;
411         $out     = '';
412
413         for ($i=0; $i<$length; $i++) {
414             // we're inside a quoted string
415             if ($quoted) {
416                 if ($str[$i] == '"') {
417                     $quoted = false;
418                 }
419                 else if ($str[$i] == "\\") {
420                     if ($comment <= 0) {
421                         $out .= "\\";
422                     }
423                     $i++;
424                 }
425             }
426             // we are inside a comment string
427             else if ($comment > 0) {
428                 if ($str[$i] == ')') {
429                     $comment--;
430                 }
431                 else if ($str[$i] == '(') {
432                     $comment++;
433                 }
434                 else if ($str[$i] == "\\") {
435                     $i++;
436                 }
437                 continue;
438             }
439             // separator, add to result array
440             else if (strpos($separator, $str[$i]) !== false) {
441                 if ($out) {
442                     $result[] = $out;
443                 }
444                 $out = '';
445                 continue;
446             }
447             // start of quoted string
448             else if ($str[$i] == '"') {
449                 $quoted = true;
450             }
451             // start of comment
452             else if ($remove_comments && $str[$i] == '(') {
453                 $comment++;
454             }
455
456             if ($comment <= 0) {
457                 $out .= $str[$i];
458             }
459         }
460
461         if ($out && $comment <= 0) {
462             $result[] = $out;
463         }
464
465         return $result;
466     }
467
468
469     /**
470      * Interpret a format=flowed message body according to RFC 2646
471      *
472      * @param string  $text Raw body formatted as flowed text
473      *
474      * @return string Interpreted text with unwrapped lines and stuffed space removed
475      */
476     public static function unfold_flowed($text)
477     {
478         $text = preg_split('/\r?\n/', $text);
479         $last = -1;
480         $q_level = 0;
481
482         foreach ($text as $idx => $line) {
483             if ($line[0] == '>' && preg_match('/^(>+\s*)/', $line, $regs)) {
484                 $q = strlen(str_replace(' ', '', $regs[0]));
485                 $line = substr($line, strlen($regs[0]));
486
487                 if ($q == $q_level && $line
488                     && isset($text[$last])
489                     && $text[$last][strlen($text[$last])-1] == ' '
490                 ) {
491                     $text[$last] .= $line;
492                     unset($text[$idx]);
493                 }
494                 else {
495                     $last = $idx;
496                 }
497             }
498             else {
499                 $q = 0;
500                 if ($line == '-- ') {
501                     $last = $idx;
502                 }
503                 else {
504                     // remove space-stuffing
505                     $line = preg_replace('/^\s/', '', $line);
506
507                     if (isset($text[$last]) && $line
508                         && $text[$last] != '-- '
509                         && $text[$last][strlen($text[$last])-1] == ' '
510                     ) {
511                         $text[$last] .= $line;
512                         unset($text[$idx]);
513                     }
514                     else {
515                         $text[$idx] = $line;
516                         $last = $idx;
517                     }
518                 }
519             }
520             $q_level = $q;
521         }
522
523         return implode("\r\n", $text);
524     }
525
526
527     /**
528      * Wrap the given text to comply with RFC 2646
529      *
530      * @param string $text Text to wrap
531      * @param int $length Length
c72a96 532      * @param string $charset Character encoding of $text
1c4f23 533      *
A 534      * @return string Wrapped text
535      */
c72a96 536     public static function format_flowed($text, $length = 72, $charset=null)
1c4f23 537     {
A 538         $text = preg_split('/\r?\n/', $text);
539
540         foreach ($text as $idx => $line) {
541             if ($line != '-- ') {
42b8a6 542                 if ($line[0] == '>' && preg_match('/^(>+ {0,1})+/', $line, $regs)) {
AM 543                     $level  = substr_count($regs[0], '>');
544                     $prefix = str_repeat('>', $level) . ' ';
545                     $line   = rtrim(substr($line, strlen($regs[0])));
546                     $line   = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset);
1c4f23 547                 }
A 548                 else if ($line) {
c72a96 549                     $line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset);
1c4f23 550                     // space-stuffing
A 551                     $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
552                 }
553
554                 $text[$idx] = $line;
555             }
556         }
557
558         return implode("\r\n", $text);
559     }
560
b6a182 561
A 562     /**
563      * Improved wordwrap function.
564      *
565      * @param string $string  Text to wrap
566      * @param int    $width   Line width
567      * @param string $break   Line separator
568      * @param bool   $cut     Enable to cut word
c72a96 569      * @param string $charset Charset of $string
b6a182 570      *
A 571      * @return string Text
572      */
c72a96 573     public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null)
b6a182 574     {
a92beb 575         if ($charset && function_exists('mb_internal_encoding')) {
c72a96 576             mb_internal_encoding($charset);
a92beb 577         }
c72a96 578
TB 579         $para   = preg_split('/\r?\n/', $string);
b6a182 580         $string = '';
A 581
582         while (count($para)) {
583             $line = array_shift($para);
584             if ($line[0] == '>') {
585                 $string .= $line.$break;
586                 continue;
587             }
588
589             $list = explode(' ', $line);
590             $len = 0;
591             while (count($list)) {
592                 $line   = array_shift($list);
593                 $l      = mb_strlen($line);
594                 $newlen = $len + $l + ($len ? 1 : 0);
595
596                 if ($newlen <= $width) {
597                     $string .= ($len ? ' ' : '').$line;
598                     $len += (1 + $l);
599                 }
600                 else {
601                     if ($l > $width) {
602                         if ($cut) {
603                             $start = 0;
604                             while ($l) {
605                                 $str = mb_substr($line, $start, $width);
606                                 $strlen = mb_strlen($str);
607                                 $string .= ($len ? $break : '').$str;
608                                 $start += $strlen;
609                                 $l -= $strlen;
610                                 $len = $strlen;
611                             }
612                         }
613                         else {
614                             $string .= ($len ? $break : '').$line;
615                             if (count($list)) {
616                                 $string .= $break;
617                             }
618                             $len = 0;
619                         }
620                     }
621                     else {
622                         $string .= $break.$line;
623                         $len = $l;
624                     }
625                 }
626             }
627
628             if (count($para)) {
629                 $string .= $break;
630             }
631         }
632
a92beb 633         if ($charset && function_exists('mb_internal_encoding')) {
AM 634             mb_internal_encoding(RCUBE_CHARSET);
635         }
c72a96 636
b6a182 637         return $string;
A 638     }
639
640
641     /**
642      * A method to guess the mime_type of an attachment.
643      *
0a8397 644      * @param string $path      Path to the file or file contents
b6a182 645      * @param string $name      File name (with suffix)
0a8397 646      * @param string $failover  Mime type supplied for failover
TB 647      * @param boolean $is_stream   Set to True if $path contains file contents
648      * @param boolean $skip_suffix Set to True if the config/mimetypes.php mappig should be ignored
b6a182 649      *
A 650      * @return string
651      * @author Till Klampaeckel <till@php.net>
652      * @see    http://de2.php.net/manual/en/ref.fileinfo.php
653      * @see    http://de2.php.net/mime_content_type
654      */
0a8397 655     public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
b6a182 656     {
A 657         $mime_type = null;
658         $mime_magic = rcube::get_instance()->config->get('mime_magic');
0a8397 659         $mime_ext = $skip_suffix ? null : @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
b6a182 660
A 661         // use file name suffix with hard-coded mime-type map
662         if (is_array($mime_ext) && $name) {
663             if ($suffix = substr($name, strrpos($name, '.')+1)) {
664                 $mime_type = $mime_ext[strtolower($suffix)];
665             }
666         }
667
668         // try fileinfo extension if available
669         if (!$mime_type && function_exists('finfo_open')) {
670             if ($finfo = finfo_open(FILEINFO_MIME, $mime_magic)) {
671                 if ($is_stream)
672                     $mime_type = finfo_buffer($finfo, $path);
673                 else
674                     $mime_type = finfo_file($finfo, $path);
675                 finfo_close($finfo);
676             }
677         }
678
679         // try PHP's mime_content_type
680         if (!$mime_type && !$is_stream && function_exists('mime_content_type')) {
681             $mime_type = @mime_content_type($path);
682         }
683
684         // fall back to user-submitted string
685         if (!$mime_type) {
686             $mime_type = $failover;
687         }
688         else {
689             // Sometimes (PHP-5.3?) content-type contains charset definition,
690             // Remove it (#1487122) also "charset=binary" is useless
691             $mime_type = array_shift(preg_split('/[; ]/', $mime_type));
692         }
693
694         return $mime_type;
695     }
696
697
698     /**
0a8397 699      * Get mimetype => file extension mapping
TB 700      *
701      * @param string  Mime-Type to get extensions for
702      * @return array  List of extensions matching the given mimetype or a hash array with ext -> mimetype mappings if $mimetype is not given
703      */
704     public static function get_mime_extensions($mimetype = null)
705     {
706         static $mime_types, $mime_extensions;
707
708         // return cached data
709         if (is_array($mime_types)) {
710             return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
711         }
712
713         // load mapping file
714         $file_paths = array();
715
716         if ($mime_types = rcube::get_instance()->config->get('mime_types'))
717             $file_paths[] = $mime_types;
718
719         // try common locations
720         $file_paths[] = '/etc/httpd/mime.types';
721         $file_paths[] = '/etc/httpd2/mime.types';
722         $file_paths[] = '/etc/apache/mime.types';
723         $file_paths[] = '/etc/apache2/mime.types';
724         $file_paths[] = '/usr/local/etc/httpd/conf/mime.types';
725         $file_paths[] = '/usr/local/etc/apache/conf/mime.types';
726
727         foreach ($file_paths as $fp) {
728             if (is_readable($fp)) {
729                 $lines = file($fp, FILE_IGNORE_NEW_LINES);
730                 break;
731             }
732         }
733
734         $mime_types = $mime_extensions = array();
735         $regex = "/([\w\+\-\.\/]+)\t+([\w\s]+)/i"; 
736         foreach((array)$lines as $line) {
737              // skip comments or mime types w/o any extensions
738             if ($line[0] == '#' || !preg_match($regex, $line, $matches))
739                 continue;
740
741             $mime = $matches[1];
742             foreach (explode(' ', $matches[2]) as $ext) {
743                 $ext = trim($ext);
744                 $mime_types[$mime][] = $ext;
745                 $mime_extensions[$ext] = $mime;
746             }
747         }
748
749         // fallback to some well-known types most important for daily emails
750         if (empty($mime_types)) {
751             $mime_extensions = @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
752             $mime_extensions += array('gif' => 'image/gif', 'png' => 'image/png', 'jpg' => 'image/jpg', 'jpeg' => 'image/jpeg', 'tif' => 'image/tiff');
753
754             foreach ($mime_extensions as $ext => $mime)
755                 $mime_types[$mime][] = $ext;
756         }
757
758         return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
759     }
760
761
762     /**
b6a182 763      * Detect image type of the given binary data by checking magic numbers.
A 764      *
765      * @param string $data  Binary file content
766      *
767      * @return string Detected mime-type or jpeg as fallback
768      */
769     public static function image_content_type($data)
770     {
771         $type = 'jpeg';
772         if      (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
773         else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
774         else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
775     //  else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
776
777         return 'image/' . $type;
778     }
779
1c4f23 780 }