1: <?php
2: /**
3: * The Horde_Mime:: class provides methods for dealing with various MIME (see,
4: * e.g., RFC 2045-2049; 2183; 2231) standards.
5: *
6: * -----
7: *
8: * This file contains code adapted from PEAR's Mail_mimeDecode library (v1.5).
9: *
10: * http://pear.php.net/package/Mail_mime
11: *
12: * This code appears in Horde_Mime::decodeParam().
13: *
14: * This code was originally released under this license:
15: *
16: * LICENSE: This LICENSE is in the BSD license style.
17: * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
18: * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
19: * All rights reserved.
20: *
21: * Redistribution and use in source and binary forms, with or
22: * without modification, are permitted provided that the following
23: * conditions are met:
24: *
25: * - Redistributions of source code must retain the above copyright
26: * notice, this list of conditions and the following disclaimer.
27: * - Redistributions in binary form must reproduce the above copyright
28: * notice, this list of conditions and the following disclaimer in the
29: * documentation and/or other materials provided with the distribution.
30: * - Neither the name of the authors, nor the names of its contributors
31: * may be used to endorse or promote products derived from this
32: * software without specific prior written permission.
33: *
34: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
35: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
36: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
37: * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
38: * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
39: * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
40: * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
41: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
42: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
43: * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
44: * THE POSSIBILITY OF SUCH DAMAGE.
45: *
46: * -----
47: *
48: * This file contains code adapted from PEAR's PHP_Compat library (v1.6.0a3).
49: *
50: * http://pear.php.net/package/PHP_Compat
51: *
52: * This code appears in Horde_Mime::_uudecode().
53: *
54: * This code was originally released under the LGPL 2.1
55: *
56: * -----
57: *
58: * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
59: *
60: * See the enclosed file COPYING for license information (LGPL). If you
61: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
62: *
63: * @author Chuck Hagenbuch <chuck@horde.org>
64: * @author Michael Slusarz <slusarz@horde.org>
65: * @category Horde
66: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
67: * @package Mime
68: */
69: class Horde_Mime
70: {
71: /**
72: * The RFC defined EOL string.
73: *
74: * @var string
75: */
76: const EOL = "\r\n";
77:
78: /**
79: * Attempt to work around non RFC 2231-compliant MUAs by generating both
80: * a RFC 2047-like parameter name and also the correct RFC 2231
81: * parameter. See:
82: * http://lists.horde.org/archives/dev/Week-of-Mon-20040426/014240.html
83: *
84: * @var boolean
85: */
86: static public $brokenRFC2231 = false;
87:
88: /**
89: * Use windows-1252 charset when decoding ISO-8859-1 data?
90: *
91: * @var boolean
92: */
93: static public $decodeWindows1252 = false;
94:
95: /**
96: * Determines if a string contains 8-bit (non US-ASCII) characters.
97: *
98: * @param string $string The string to check.
99: * @param string $charset The charset of the string. Defaults to
100: * US-ASCII.
101: *
102: * @return boolean True if string contains non US-ASCII characters.
103: */
104: static public function is8bit($string, $charset = null)
105: {
106: if (empty($charset)) {
107: $charset = 'us-ascii';
108: }
109:
110: /* ISO-2022-JP is a 7bit charset, but it is an 8bit representation so
111: * it needs to be entirely encoded. */
112: return is_string($string) &&
113: ((stristr('iso-2022-jp', $charset) &&
114: (strstr($string, "\x1b\$B"))) ||
115: preg_match('/[\x80-\xff]/', $string));
116: }
117:
118: /**
119: * Encodes a string containing non-ASCII characters according to RFC 2047.
120: *
121: * @param string $text The text to encode.
122: * @param string $charset The character set of the text.
123: *
124: * @return string The text, encoded only if it contains non-ASCII
125: * characters.
126: */
127: static public function encode($text, $charset)
128: {
129: $charset = Horde_String::lower($charset);
130:
131: if (($charset == 'us-ascii') || !self::is8bit($text, $charset)) {
132: return $text;
133: }
134:
135: /* Get the list of elements in the string. */
136: $size = preg_match_all('/([^\s]+)([\s]*)/', $text, $matches, PREG_SET_ORDER);
137:
138: $line = '';
139:
140: /* Return if nothing needs to be encoded. */
141: foreach ($matches as $key => $val) {
142: if (self::is8bit($val[1], $charset)) {
143: if ((($key + 1) < $size) &&
144: self::is8bit($matches[$key + 1][1], $charset)) {
145: $line .= self::_encode($val[1] . $val[2], $charset) . ' ';
146: } else {
147: $line .= self::_encode($val[1], $charset) . $val[2];
148: }
149: } else {
150: $line .= $val[1] . $val[2];
151: }
152: }
153:
154: return rtrim($line);
155: }
156:
157: /**
158: * Internal recursive function to RFC 2047 encode a string.
159: *
160: * @param string $text The text to encode.
161: * @param string $charset The character set of the text.
162: *
163: * @return string The text, encoded only if it contains non-ASCII
164: * characters.
165: */
166: static protected function _encode($text, $charset)
167: {
168: $encoded = trim(base64_encode($text));
169: $c_size = strlen($charset) + 7;
170:
171: if ((strlen($encoded) + $c_size) > 75) {
172: $parts = explode(self::EOL, rtrim(chunk_split($encoded, intval((75 - $c_size) / 4) * 4)));
173: } else {
174: $parts[] = $encoded;
175: }
176:
177: $p_size = count($parts);
178: $out = '';
179:
180: foreach ($parts as $key => $val) {
181: $out .= '=?' . $charset . '?b?' . $val . '?=';
182: if ($p_size > $key + 1) {
183: /* RFC 2047 [2]: no encoded word can be more than 75
184: * characters long. If longer, you must split the word with
185: * CRLF SPACE. */
186: $out .= self::EOL . ' ';
187: }
188: }
189:
190: return $out;
191: }
192:
193: /**
194: * Encodes a line via quoted-printable encoding.
195: *
196: * @param string $text The text to encode.
197: * @param string $eol The EOL sequence to use.
198: * @param integer $wrap Wrap a line at this many characters.
199: *
200: * @return string The quoted-printable encoded string.
201: */
202: static public function quotedPrintableEncode($text, $eol = self::EOL,
203: $wrap = 76)
204: {
205: $output = '';
206: $curr_length = 0;
207:
208: /* We need to go character by character through the data. */
209: for ($i = 0, $length = strlen($text); $i < $length; ++$i) {
210: $char = $text[$i];
211:
212: /* If we have reached the end of the line, reset counters. */
213: if ($char == "\n") {
214: $output .= $eol;
215: $curr_length = 0;
216: continue;
217: } elseif ($char == "\r") {
218: continue;
219: }
220:
221: /* Spaces or tabs at the end of the line are NOT allowed. Also,
222: * ASCII characters below 32 or above 126 AND 61 must be
223: * encoded. */
224: $ascii = ord($char);
225: if ((($ascii === 32) &&
226: ($i + 1 != $length) &&
227: (($text[$i + 1] == "\n") || ($text[$i + 1] == "\r"))) ||
228: (($ascii < 32) || ($ascii > 126) || ($ascii === 61))) {
229: $char_len = 3;
230: $char = '=' . Horde_String::upper(sprintf('%02s', dechex($ascii)));
231: } else {
232: $char_len = 1;
233: }
234:
235: /* Lines must be $wrap characters or less. */
236: $curr_length += $char_len;
237: if ($curr_length > $wrap) {
238: $output .= '=' . $eol;
239: $curr_length = $char_len;
240: }
241: $output .= $char;
242: }
243:
244: return $output;
245: }
246:
247: /**
248: * Encodes a string containing email addresses according to RFC 2047.
249: *
250: * This differs from encode() because it keeps email addresses legal, only
251: * encoding the personal information.
252: *
253: * @param mixed $addresses The email addresses to encode (either a
254: * string or an array of addresses).
255: * @param string $charset The character set of the text.
256: * @param string $defserver The default domain to append to mailboxes.
257: *
258: * @return string The text, encoded only if it contains non-ASCII
259: * characters.
260: * @throws Horde_Mime_Exception
261: */
262: static public function encodeAddress($addresses, $charset,
263: $defserver = null)
264: {
265: if (!is_array($addresses)) {
266: $addresses = trim($addresses);
267: $addresses = Horde_Mime_Address::parseAddressList($addresses, array(
268: 'defserver' => $defserver,
269: 'nestgroups' => true
270: ));
271: }
272:
273: $text = array();
274: foreach ($addresses as $addr) {
275: $addrobs = empty($addr['groupname'])
276: ? array($addr)
277: : $addr['addresses'];
278: $addrlist = array();
279:
280: foreach ($addrobs as $val) {
281: if (empty($val['personal'])) {
282: $personal = '';
283: } else {
284: if (($val['personal'][0] == '"') &&
285: (substr($val['personal'], -1) == '"')) {
286: $val['personal'] = stripslashes(substr($val['personal'], 1, -1));
287: }
288: $personal = self::encode($val['personal'], $charset);
289: }
290: $addrlist[] = Horde_Mime_Address::writeAddress($val['mailbox'], $val['host'], $personal);
291: }
292:
293: $text[] = empty($addr['groupname'])
294: ? reset($addrlist)
295: : Horde_Mime_Address::writeGroupAddress($addr['groupname'], $addrlist);
296: }
297:
298: return implode(', ', $text);
299: }
300:
301: /**
302: * Decodes an RFC 2047-encoded string.
303: *
304: * @param string $string The text to decode.
305: * @param string $to_charset The charset that the text should be decoded
306: * to.
307: *
308: * @return string The decoded text.
309: */
310: static public function decode($string, $to_charset)
311: {
312: /* Take out any spaces between multiple encoded words. */
313: $string = preg_replace('|\?=\s+=\?|', '?==?', $string);
314:
315: $out = '';
316: $old_pos = 0;
317:
318: while (($pos = strpos($string, '=?', $old_pos)) !== false) {
319: /* Save any preceding text. */
320: $out .= substr($string, $old_pos, $pos - $old_pos);
321:
322: /* Search for first delimiting question mark (charset). */
323: if (($d1 = strpos($string, '?', $pos + 2)) === false) {
324: break;
325: }
326:
327: $orig_charset = substr($string, $pos + 2, $d1 - $pos - 2);
328: if (self::$decodeWindows1252 &&
329: (Horde_String::lower($orig_charset) == 'iso-8859-1')) {
330: $orig_charset = 'windows-1252';
331: }
332:
333: /* Search for second delimiting question mark (encoding). */
334: if (($d2 = strpos($string, '?', $d1 + 1)) === false) {
335: break;
336: }
337:
338: $encoding = substr($string, $d1 + 1, $d2 - $d1 - 1);
339:
340: /* Search for end of encoded data. */
341: if (($end = strpos($string, '?=', $d2 + 1)) === false) {
342: break;
343: }
344:
345: $encoded_text = substr($string, $d2 + 1, $end - $d2 - 1);
346:
347: switch ($encoding) {
348: case 'Q':
349: case 'q':
350: $out .= Horde_String::convertCharset(
351: preg_replace('/=([0-9a-f]{2})/ie', 'chr(0x\1)', str_replace('_', ' ', $encoded_text)),
352: $orig_charset,
353: $to_charset
354: );
355: break;
356:
357: case 'B':
358: case 'b':
359: $out .= Horde_String::convertCharset(
360: base64_decode($encoded_text),
361: $orig_charset,
362: $to_charset
363: );
364: break;
365:
366: default:
367: // Ignore unknown encoding.
368: break;
369: }
370:
371: $old_pos = $end + 2;
372: }
373:
374: return $out . substr($string, $old_pos);
375: }
376:
377: /**
378: * Decodes an RFC 2047-encoded address string.
379: *
380: * @param string $string The text to decode.
381: * @param string $to_charset The charset that the text should be decoded
382: * to.
383: *
384: * @return string The decoded text.
385: * @throws Horde_Mime_Exception
386: */
387: static public function decodeAddrString($string, $to_charset)
388: {
389: $addr_list = array();
390: foreach (Horde_Mime_Address::parseAddressList($string) as $ob) {
391: $ob['personal'] = isset($ob['personal'])
392: ? self::decode($ob['personal'], $to_charset)
393: : '';
394: $addr_list[] = $ob;
395: }
396:
397: return Horde_Mime_Address::addrArray2String($addr_list);
398: }
399:
400: /**
401: * Encodes a MIME parameter string pursuant to RFC 2183 & 2231
402: * (Content-Type and Content-Disposition headers).
403: *
404: * @param string $name The parameter name.
405: * @param string $val The parameter value.
406: * @param string $charset The charset the text should be encoded with.
407: * @param array $opts Additional options:
408: * <pre>
409: * 'escape' - (boolean) If true, escape param values as described in
410: * RFC 2045 [Appendix A].
411: * DEFAULT: false
412: * 'lang' - (string) The language to use when encoding.
413: * DEFAULT: None specified
414: * </pre>
415: *
416: * @return array The encoded parameter string.
417: */
418: static public function encodeParam($name, $val, $charset, $opts = array())
419: {
420: $encode = $wrap = false;
421: $output = array();
422: $curr = 0;
423:
424: // 2 = '=', ';'
425: $pre_len = strlen($name) + 2;
426:
427: if (self::is8bit($val, $charset)) {
428: $string = Horde_String::lower($charset) . '\'' . (empty($opts['lang']) ? '' : Horde_String::lower($opts['lang'])) . '\'' . rawurlencode($val);
429: $encode = true;
430: /* Account for trailing '*'. */
431: ++$pre_len;
432: } else {
433: $string = $val;
434: }
435:
436: if (($pre_len + strlen($string)) > 75) {
437: /* Account for continuation '*'. */
438: ++$pre_len;
439: $wrap = true;
440:
441: while ($string) {
442: $chunk = 75 - $pre_len - strlen($curr);
443: $pos = min($chunk, strlen($string) - 1);
444:
445: /* Don't split in the middle of an encoded char. */
446: if (($chunk == $pos) && ($pos > 2)) {
447: for ($i = 0; $i <= 2; ++$i) {
448: if ($string[$pos - $i] == '%') {
449: $pos -= $i + 1;
450: break;
451: }
452: }
453: }
454:
455: $lines[] = substr($string, 0, $pos + 1);
456: $string = substr($string, $pos + 1);
457: ++$curr;
458: }
459: } else {
460: $lines = array($string);
461: }
462:
463: foreach ($lines as $i => $line) {
464: $output[$name . (($wrap) ? ('*' . $i) : '') . (($encode) ? '*' : '')] = $line;
465: }
466:
467: if (self::$brokenRFC2231 && !isset($output[$name])) {
468: $output = array_merge(array($name => self::encode($val, $charset)), $output);
469: }
470:
471: /* Escape certain characters in params (See RFC 2045 [Appendix A]). */
472: if (!empty($opts['escape'])) {
473: foreach (array_keys($output) as $key) {
474: if (strcspn($output[$key], "\11\40\"(),/:;<=>?@[\\]") != strlen($output[$key])) {
475: $output[$key] = '"' . addcslashes($output[$key], '\\"') . '"';
476: }
477: }
478: }
479:
480: return $output;
481: }
482:
483: /**
484: * Decodes a MIME parameter string pursuant to RFC 2183 & 2231
485: * (Content-Type and Content-Disposition headers).
486: *
487: * @param string $type Either 'Content-Type' or 'Content-Disposition'
488: * (case-insensitive).
489: * @param mixed $data The text of the header or an array of
490: * param name => param values.
491: * @param string $charset The charset the text should be decoded to.
492: *
493: * @return array An array with the following entries:
494: * <pre>
495: * 'params' - (array) The header's parameter values.
496: * 'val' - (string) The header's "base" value.
497: * </pre>
498: */
499: static public function decodeParam($type, $data, $charset)
500: {
501: $convert = array();
502: $ret = array('params' => array(), 'val' => '');
503: $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/';
504: $type = Horde_String::lower($type);
505:
506: if (is_array($data)) {
507: // Use dummy base values
508: $ret['val'] = ($type == 'content-type')
509: ? 'text/plain'
510: : 'attachment';
511: $params = $data;
512: } else {
513: /* This code was adapted from PEAR's Mail_mimeDecode::. */
514: if (($pos = strpos($data, ';')) === false) {
515: $ret['val'] = trim($data);
516: return $ret;
517: }
518:
519: $ret['val'] = trim(substr($data, 0, $pos));
520: $data = trim(substr($data, ++$pos));
521: $params = $tmp = array();
522:
523: if (strlen($data) > 0) {
524: /* This splits on a semi-colon, if there's no preceeding
525: * backslash. */
526: preg_match_all($splitRegex, $data, $matches);
527:
528: for ($i = 0, $cnt = count($matches[0]); $i < $cnt; ++$i) {
529: $param = $matches[0][$i];
530: while (substr($param, -2) == '\;') {
531: $param .= $matches[0][++$i];
532: }
533: $tmp[] = $param;
534: }
535:
536: for ($i = 0, $cnt = count($tmp); $i < $cnt; ++$i) {
537: $pos = strpos($tmp[$i], '=');
538: $p_name = trim(substr($tmp[$i], 0, $pos), "'\";\t\\ ");
539: $p_val = trim(str_replace('\;', ';', substr($tmp[$i], $pos + 1)), "'\";\t\\ ");
540: if (strlen($p_val) && ($p_val[0] == '"')) {
541: $p_val = substr($p_val, 1, -1);
542: }
543:
544: $params[$p_name] = $p_val;
545: }
546: }
547: /* End of code adapted from PEAR's Mail_mimeDecode::. */
548: }
549:
550: /* Sort the params list. Prevents us from having to manually keep
551: * track of continuation values below. */
552: uksort($params, 'strnatcasecmp');
553:
554: foreach ($params as $name => $val) {
555: /* Asterisk at end indicates encoded value. */
556: if (substr($name, -1) == '*') {
557: $name = substr($name, 0, -1);
558: $encode = true;
559: } else {
560: $encode = false;
561: }
562:
563: /* This asterisk indicates continuation parameter. */
564: if (($pos = strrpos($name, '*')) !== false) {
565: $name = substr($name, 0, $pos);
566: }
567:
568: if (!isset($ret['params'][$name]) ||
569: ($encode && !isset($convert[$name]))) {
570: $ret['params'][$name] = '';
571: }
572:
573: $ret['params'][$name] .= $val;
574:
575: if ($encode) {
576: $convert[$name] = true;
577: }
578: }
579:
580: foreach (array_keys($convert) as $name) {
581: $val = $ret['params'][$name];
582: $quote = strpos($val, "'");
583: $orig_charset = substr($val, 0, $quote);
584: if (self::$decodeWindows1252 &&
585: (Horde_String::lower($orig_charset) == 'iso-8859-1')) {
586: $orig_charset = 'windows-1252';
587: }
588: /* Ignore language. */
589: $quote = strpos($val, "'", $quote + 1);
590: substr($val, $quote + 1);
591: $ret['params'][$name] = Horde_String::convertCharset(urldecode(substr($val, $quote + 1)), $orig_charset, $charset);
592: }
593:
594: /* MIME parameters are supposed to be encoded via RFC 2231, but many
595: * mailers do RFC 2045 encoding instead. However, if we see at least
596: * one RFC 2231 encoding, then assume the sending mailer knew what
597: * it was doing. */
598: if (empty($convert)) {
599: foreach (array_diff(array_keys($ret['params']), array_keys($convert)) as $name) {
600: $ret['params'][$name] = self::decode($ret['params'][$name], $charset);
601: }
602: }
603:
604: return $ret;
605: }
606:
607: /**
608: * Generates a Message-ID string conforming to RFC 2822 [3.6.4] and the
609: * standards outlined in 'draft-ietf-usefor-message-id-01.txt'.
610: *
611: * @param string A message ID string.
612: */
613: static public function generateMessageId()
614: {
615: return '<' . strval(new Horde_Support_Guid(array('prefix' => 'Horde'))) . '>';
616: }
617:
618: /**
619: * Performs MIME ID "arithmetic" on a given ID.
620: *
621: * @param string $id The MIME ID string.
622: * @param string $action One of the following:
623: * <pre>
624: * 'down' - ID of child. Note: down will first traverse to "$id.0" if
625: * given an ID *NOT* of the form "$id.0". If given an ID of the
626: * form "$id.0", down will traverse to "$id.1". This behavior
627: * can be avoided if 'norfc822' option is set.
628: * 'next' - ID of next sibling.
629: * 'prev' - ID of previous sibling.
630: * 'up' - ID of parent. Note: up will first traverse to "$id.0" if
631: * given an ID *NOT* of the form "$id.0". If given an ID of the
632: * form "$id.0", down will traverse to "$id". This behavior can be
633: * avoided if 'norfc822' option is set.
634: * </pre>
635: * @param array $options Additional options:
636: * <pre>
637: * 'count' - (integer) How many levels to traverse.
638: * DEFAULT: 1
639: * 'norfc822' - (boolean) Don't traverse rfc822 sub-levels
640: * DEFAULT: false
641: * </pre>
642: *
643: * @return mixed The resulting ID string, or null if that ID can not
644: * exist.
645: */
646: static public function mimeIdArithmetic($id, $action, $options = array())
647: {
648: $pos = strrpos($id, '.');
649: $end = ($pos === false) ? $id : substr($id, $pos + 1);
650:
651: switch ($action) {
652: case 'down':
653: if ($end == '0') {
654: $id = ($pos === false) ? 1 : substr_replace($id, '1', $pos + 1);
655: } else {
656: $id .= empty($options['norfc822']) ? '.0' : '.1';
657: }
658: break;
659:
660: case 'next':
661: ++$end;
662: $id = ($pos === false) ? $end : substr_replace($id, $end, $pos + 1);
663: break;
664:
665: case 'prev':
666: if (($end == '0') ||
667: (empty($options['norfc822']) && ($end == '1'))) {
668: $id = null;
669: } elseif ($pos === false) {
670: $id = --$end;
671: } else {
672: $id = substr_replace($id, --$end, $pos + 1);
673: }
674: break;
675:
676: case 'up':
677: if ($pos === false) {
678: $id = ($end == '0') ? null : '0';
679: } elseif (!empty($options['norfc822']) || ($end == '0')) {
680: $id = substr($id, 0, $pos);
681: } else {
682: $id = substr_replace($id, '0', $pos + 1);
683: }
684: break;
685: }
686:
687: return (!is_null($id) && !empty($options['count']) && --$options['count'])
688: ? self::mimeIdArithmetic($id, $action, $options)
689: : $id;
690: }
691:
692: /**
693: * Determines if a given MIME ID lives underneath a base ID.
694: *
695: * @param string $base The base MIME ID.
696: * @param string $id The MIME ID to query.
697: *
698: * @return boolean Whether $id lives underneath $base.
699: */
700: static public function isChild($base, $id)
701: {
702: $base = (substr($base, -2) == '.0')
703: ? substr($base, 0, -1)
704: : rtrim($base, '.') . '.';
705:
706: return ((($base == 0) && ($id != 0)) ||
707: (strpos(strval($id), strval($base)) === 0));
708: }
709:
710: /**
711: * Scans $input for uuencoded data and converts it to unencoded data.
712: *
713: * @param string $input The input data
714: *
715: * @return array A list of arrays, with each array corresponding to
716: * a file in the input and containing the following keys:
717: * <pre>
718: * 'data' - (string) Unencoded data.
719: * 'name' - (string) Filename.
720: * 'perms' - (string) Octal permissions.
721: * </pre>
722: */
723: static public function uudecode($input)
724: {
725: $data = array();
726:
727: /* Find all uuencoded sections. */
728: if (preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches, PREG_SET_ORDER)) {
729: reset($matches);
730: while (list(,$v) = each($matches)) {
731: $data[] = array(
732: 'data' => self::_uudecode($v[3]),
733: 'name' => $v[2],
734: 'perm' => $v[1]
735: );
736: }
737: }
738:
739: return $data;
740: }
741:
742: /**
743: * PHP 5's built-in convert_uudecode() is broken. Need this wrapper.
744: *
745: * @param string $input UUencoded input.
746: *
747: * @return string Decoded string.
748: */
749: static protected function _uudecode($input)
750: {
751: $decoded = '';
752:
753: foreach (explode("\n", $input) as $line) {
754: $c = count($bytes = unpack('c*', substr(trim($line,"\r\n\t"), 1)));
755:
756: while ($c % 4) {
757: $bytes[++$c] = 0;
758: }
759:
760: foreach (array_chunk($bytes, 4) as $b) {
761: $b0 = ($b[0] == 0x60) ? 0 : $b[0] - 0x20;
762: $b1 = ($b[1] == 0x60) ? 0 : $b[1] - 0x20;
763: $b2 = ($b[2] == 0x60) ? 0 : $b[2] - 0x20;
764: $b3 = ($b[3] == 0x60) ? 0 : $b[3] - 0x20;
765:
766: $b0 <<= 2;
767: $b0 |= ($b1 >> 4) & 0x03;
768: $b1 <<= 4;
769: $b1 |= ($b2 >> 2) & 0x0F;
770: $b2 <<= 6;
771: $b2 |= $b3 & 0x3F;
772:
773: $decoded .= pack('c*', $b0, $b1, $b2);
774: }
775: }
776:
777: return rtrim($decoded, "\0");
778: }
779:
780: }
781: