diff options
457 files changed, 59328 insertions, 0 deletions
diff --git a/cache/.htaccess b/cache/.htaccess new file mode 100644 index 0000000..93169e4 --- /dev/null +++ b/cache/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all diff --git a/config/.htaccess b/config/.htaccess new file mode 100644 index 0000000..cb24fd7 --- /dev/null +++ b/config/.htaccess @@ -0,0 +1,2 @@ +Order allow,deny +Deny from all diff --git a/lib/client/file_ui_main.php b/lib/client/file_ui_main.php new file mode 100644 index 0000000..771dfc4 --- /dev/null +++ b/lib/client/file_ui_main.php @@ -0,0 +1,46 @@ +<?php +/* + +--------------------------------------------------------------------------+ + | This file is part of the Kolab File API | + | | + | Copyright (C) 2011-2012, Kolab Systems AG | + | | + | This program is free software: you can redistribute it and/or modify | + | it under the terms of the GNU Affero General Public License as published | + | by the Free Software Foundation, either version 3 of the License, or | + | (at your option) any later version. | + | | + | This program is distributed in the hope that it will be useful, | + | but WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | + | GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public License | + | along with this program. If not, see <http://www.gnu.org/licenses/> | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak <machniak@kolabsys.com> | + +--------------------------------------------------------------------------+ +*/ + +class file_ui_main extends file_ui +{ + public function action_default() + { + // assign token + $this->output->set_env('token', $_SESSION['user']['token']); + + // add watermark content + $this->output->set_env('watermark', $this->output->get_template('watermark')); +// $this->watermark('taskcontent'); + + // assign default set of translations + $this->output->add_translation('loading', 'saving', 'deleting', 'servererror', + 'search', 'search.loading', 'search.acchars'); + +// $this->output->assign('tasks', $this->menu); +// $this->output->assign('main_menu', $this->menu()); + $this->output->assign('user', $_SESSION['user']); + + $this->output->command('command', 'folder.list'); + } +} diff --git a/lib/ext/Auth/SASL.php b/lib/ext/Auth/SASL.php new file mode 100644 index 0000000..b2be93c --- /dev/null +++ b/lib/ext/Auth/SASL.php @@ -0,0 +1,104 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Client implementation of various SASL mechanisms +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('PEAR.php'); + +class Auth_SASL +{ + /** + * Factory class. Returns an object of the request + * type. + * + * @param string $type One of: Anonymous + * Plain + * CramMD5 + * DigestMD5 + * Types are not case sensitive + */ + function &factory($type) + { + switch (strtolower($type)) { + case 'anonymous': + $filename = 'Auth/SASL/Anonymous.php'; + $classname = 'Auth_SASL_Anonymous'; + break; + + case 'login': + $filename = 'Auth/SASL/Login.php'; + $classname = 'Auth_SASL_Login'; + break; + + case 'plain': + $filename = 'Auth/SASL/Plain.php'; + $classname = 'Auth_SASL_Plain'; + break; + + case 'external': + $filename = 'Auth/SASL/External.php'; + $classname = 'Auth_SASL_External'; + break; + + case 'crammd5': + $filename = 'Auth/SASL/CramMD5.php'; + $classname = 'Auth_SASL_CramMD5'; + break; + + case 'digestmd5': + $filename = 'Auth/SASL/DigestMD5.php'; + $classname = 'Auth_SASL_DigestMD5'; + break; + + default: + return PEAR::raiseError('Invalid SASL mechanism type'); + break; + } + + require_once($filename); + $obj = new $classname(); + return $obj; + } +} + +?> diff --git a/lib/ext/Auth/SASL/Anonymous.php b/lib/ext/Auth/SASL/Anonymous.php new file mode 100644 index 0000000..0811909 --- /dev/null +++ b/lib/ext/Auth/SASL/Anonymous.php @@ -0,0 +1,71 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of ANONYMOUS SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Anonymous extends Auth_SASL_Common +{ + /** + * Not much to do here except return the token supplied. + * No encoding, hashing or encryption takes place for this + * mechanism, simply one of: + * o An email address + * o An opaque string not containing "@" that can be interpreted + * by the sysadmin + * o Nothing + * + * We could have some logic here for the second option, but this + * would by no means create something interpretable. + * + * @param string $token Optional email address or string to provide + * as trace information. + * @return string The unaltered input token + */ + function getResponse($token = '') + { + return $token; + } +} +?>
\ No newline at end of file diff --git a/lib/ext/Auth/SASL/Common.php b/lib/ext/Auth/SASL/Common.php new file mode 100644 index 0000000..e7a18e2 --- /dev/null +++ b/lib/ext/Auth/SASL/Common.php @@ -0,0 +1,74 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Common functionality to SASL mechanisms +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +class Auth_SASL_Common +{ + /** + * Function which implements HMAC MD5 digest + * + * @param string $key The secret key + * @param string $data The data to protect + * @return string The HMAC MD5 digest + */ + function _HMAC_MD5($key, $data) + { + if (strlen($key) > 64) { + $key = pack('H32', md5($key)); + } + + if (strlen($key) < 64) { + $key = str_pad($key, 64, chr(0)); + } + + $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); + $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); + + $inner = pack('H32', md5($k_ipad . $data)); + $digest = md5($k_opad . $inner); + + return $digest; + } +} +?> diff --git a/lib/ext/Auth/SASL/CramMD5.php b/lib/ext/Auth/SASL/CramMD5.php new file mode 100644 index 0000000..d3fbf17 --- /dev/null +++ b/lib/ext/Auth/SASL/CramMD5.php @@ -0,0 +1,68 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of CRAM-MD5 SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_CramMD5 extends Auth_SASL_Common +{ + /** + * Implements the CRAM-MD5 SASL mechanism + * This DOES NOT base64 encode the return value, + * you will need to do that yourself. + * + * @param string $user Username + * @param string $pass Password + * @param string $challenge The challenge supplied by the server. + * this should be already base64_decoded. + * + * @return string The string to pass back to the server, of the form + * "<user> <digest>". This is NOT base64_encoded. + */ + function getResponse($user, $pass, $challenge) + { + return $user . ' ' . $this->_HMAC_MD5($pass, $challenge); + } +} +?>
\ No newline at end of file diff --git a/lib/ext/Auth/SASL/DigestMD5.php b/lib/ext/Auth/SASL/DigestMD5.php new file mode 100644 index 0000000..07007b7 --- /dev/null +++ b/lib/ext/Auth/SASL/DigestMD5.php @@ -0,0 +1,197 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of DIGEST-MD5 SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_DigestMD5 extends Auth_SASL_Common +{ + /** + * Provides the (main) client response for DIGEST-MD5 + * requires a few extra parameters than the other + * mechanisms, which are unavoidable. + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $challenge The digest challenge sent by the server + * @param string $hostname The hostname of the machine you're connecting to + * @param string $service The servicename (eg. imap, pop, acap etc) + * @param string $authzid Authorization id (username to proxy as) + * @return string The digest response (NOT base64 encoded) + * @access public + */ + function getResponse($authcid, $pass, $challenge, $hostname, $service, $authzid = '') + { + $challenge = $this->_parseChallenge($challenge); + $authzid_string = ''; + if ($authzid != '') { + $authzid_string = ',authzid="' . $authzid . '"'; + } + + if (!empty($challenge)) { + $cnonce = $this->_getCnonce(); + $digest_uri = sprintf('%s/%s', $service, $hostname); + $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid); + + if ($challenge['realm']) { + return sprintf('username="%s",realm="%s"' . $authzid_string . +',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + } else { + return sprintf('username="%s"' . $authzid_string . ',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + } + } else { + return PEAR::raiseError('Invalid digest challenge'); + } + } + + /** + * Parses and verifies the digest challenge* + * + * @param string $challenge The digest challenge + * @return array The parsed challenge as an assoc + * array in the form "directive => value". + * @access private + */ + function _parseChallenge($challenge) + { + $tokens = array(); + while (preg_match('/^([a-z-]+)=("[^"]+(?<!\\\)"|[^,]+)/i', $challenge, $matches)) { + + // Ignore these as per rfc2831 + if ($matches[1] == 'opaque' OR $matches[1] == 'domain') { + $challenge = substr($challenge, strlen($matches[0]) + 1); + continue; + } + + // Allowed multiple "realm" and "auth-param" + if (!empty($tokens[$matches[1]]) AND ($matches[1] == 'realm' OR $matches[1] == 'auth-param')) { + if (is_array($tokens[$matches[1]])) { + $tokens[$matches[1]][] = preg_replace('/^"(.*)"$/', '\\1', $matches[2]); + } else { + $tokens[$matches[1]] = array($tokens[$matches[1]], preg_replace('/^"(.*)"$/', '\\1', $matches[2])); + } + + // Any other multiple instance = failure + } elseif (!empty($tokens[$matches[1]])) { + $tokens = array(); + break; + + } else { + $tokens[$matches[1]] = preg_replace('/^"(.*)"$/', '\\1', $matches[2]); + } + + // Remove the just parsed directive from the challenge + $challenge = substr($challenge, strlen($matches[0]) + 1); + } + + /** + * Defaults and required directives + */ + // Realm + if (empty($tokens['realm'])) { + $tokens['realm'] = ""; + } + + // Maxbuf + if (empty($tokens['maxbuf'])) { + $tokens['maxbuf'] = 65536; + } + + // Required: nonce, algorithm + if (empty($tokens['nonce']) OR empty($tokens['algorithm'])) { + return array(); + } + + return $tokens; + } + + /** + * Creates the response= part of the digest response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $realm Realm as provided by the server + * @param string $nonce Nonce as provided by the server + * @param string $cnonce Client nonce + * @param string $digest_uri The digest-uri= value part of the response + * @param string $authzid Authorization id + * @return string The response= part of the digest response + * @access private + */ + function _getResponseValue($authcid, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = '') + { + if ($authzid == '') { + $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce); + } else { + $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce, $authzid); + } + $A2 = 'AUTHENTICATE:' . $digest_uri; + return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2))); + } + + /** + * Creates the client nonce for the response + * + * @return string The cnonce value + * @access private + */ + function _getCnonce() + { + if (@file_exists('/dev/urandom') && $fd = @fopen('/dev/urandom', 'r')) { + return base64_encode(fread($fd, 32)); + + } elseif (@file_exists('/dev/random') && $fd = @fopen('/dev/random', 'r')) { + return base64_encode(fread($fd, 32)); + + } else { + $str = ''; + for ($i=0; $i<32; $i++) { + $str .= chr(mt_rand(0, 255)); + } + + return base64_encode($str); + } + } +} +?> diff --git a/lib/ext/Auth/SASL/External.php b/lib/ext/Auth/SASL/External.php new file mode 100644 index 0000000..86a17cb --- /dev/null +++ b/lib/ext/Auth/SASL/External.php @@ -0,0 +1,63 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2008 Christoph Schulz | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Christoph Schulz <develop@kristov.de> | +// +-----------------------------------------------------------------------+ +// +// $Id: External.php 286825 2009-08-05 06:23:42Z cweiske $ + +/** +* Implmentation of EXTERNAL SASL mechanism +* +* @author Christoph Schulz <develop@kristov.de> +* @access public +* @version 1.0.3 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_External extends Auth_SASL_Common +{ + /** + * Returns EXTERNAL response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string EXTERNAL Response + */ + function getResponse($authcid, $pass, $authzid = '') + { + return $authzid; + } +} +?> diff --git a/lib/ext/Auth/SASL/Login.php b/lib/ext/Auth/SASL/Login.php new file mode 100644 index 0000000..918daee --- /dev/null +++ b/lib/ext/Auth/SASL/Login.php @@ -0,0 +1,65 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* This is technically not a SASL mechanism, however +* it's used by Net_Sieve, Net_Cyrus and potentially +* other protocols , so here is a good place to abstract +* it. +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Login extends Auth_SASL_Common +{ + /** + * Pseudo SASL LOGIN mechanism + * + * @param string $user Username + * @param string $pass Password + * @return string LOGIN string + */ + function getResponse($user, $pass) + { + return sprintf('LOGIN %s %s', $user, $pass); + } +} +?>
\ No newline at end of file diff --git a/lib/ext/Auth/SASL/Plain.php b/lib/ext/Auth/SASL/Plain.php new file mode 100644 index 0000000..57894d0 --- /dev/null +++ b/lib/ext/Auth/SASL/Plain.php @@ -0,0 +1,63 @@ +<?php +// +-----------------------------------------------------------------------+ +// | Copyright (c) 2002-2003 Richard Heyes | +// | All rights reserved. | +// | | +// | Redistribution and use in source and binary forms, with or without | +// | modification, are permitted provided that the following conditions | +// | are met: | +// | | +// | o Redistributions of source code must retain the above copyright | +// | notice, this list of conditions and the following disclaimer. | +// | o Redistributions in binary form must reproduce the above copyright | +// | notice, this list of conditions and the following disclaimer in the | +// | documentation and/or other materials provided with the distribution.| +// | o The names of the authors may not be used to endorse or promote | +// | products derived from this software without specific prior written | +// | permission. | +// | | +// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | +// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | +// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | +// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | +// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | +// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | +// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | +// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | +// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | +// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | +// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | +// | | +// +-----------------------------------------------------------------------+ +// | Author: Richard Heyes <richard@php.net> | +// +-----------------------------------------------------------------------+ +// +// $Id$ + +/** +* Implmentation of PLAIN SASL mechanism +* +* @author Richard Heyes <richard@php.net> +* @access public +* @version 1.0 +* @package Auth_SASL +*/ + +require_once('Auth/SASL/Common.php'); + +class Auth_SASL_Plain extends Auth_SASL_Common +{ + /** + * Returns PLAIN response + * + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string PLAIN Response + */ + function getResponse($authcid, $pass, $authzid = '') + { + return $authzid . chr(0) . $authcid . chr(0) . $pass; + } +} +?> diff --git a/lib/ext/Mail/mime.php b/lib/ext/Mail/mime.php new file mode 100644 index 0000000..c459b91 --- /dev/null +++ b/lib/ext/Mail/mime.php @@ -0,0 +1,1476 @@ +<?php +/** + * The Mail_Mime class is used to create MIME E-mail messages + * + * The Mail_Mime class provides an OO interface to create MIME + * enabled email messages. This way you can create emails that + * contain plain-text bodies, HTML bodies, attachments, inline + * images and specific headers. + * + * Compatible with PHP versions 4 and 5 + * + * LICENSE: This LICENSE is in the BSD license style. + * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> + * Copyright (c) 2003-2006, PEAR <pear-group@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @author Aleksander Machniak <alec@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 1.8.5 + * @link http://pear.php.net/package/Mail_mime + * + * This class is based on HTML Mime Mail class from + * Richard Heyes <richard@phpguru.org> which was based also + * in the mime_mail.class by Tobias Ratschiller <tobias@dnet.it> + * and Sascha Schumann <sascha@schumann.cx> + */ + + +/** + * require PEAR + * + * This package depends on PEAR to raise errors. + */ +require_once 'PEAR.php'; + +/** + * require Mail_mimePart + * + * Mail_mimePart contains the code required to + * create all the different parts a mail can + * consist of. + */ +require_once 'Mail/mimePart.php'; + + +/** + * The Mail_Mime class provides an OO interface to create MIME + * enabled email messages. This way you can create emails that + * contain plain-text bodies, HTML bodies, attachments, inline + * images and specific headers. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: 1.8.5 + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mime +{ + /** + * Contains the plain text part of the email + * + * @var string + * @access private + */ + var $_txtbody; + + /** + * Contains the html part of the email + * + * @var string + * @access private + */ + var $_htmlbody; + + /** + * list of the attached images + * + * @var array + * @access private + */ + var $_html_images = array(); + + /** + * list of the attachements + * + * @var array + * @access private + */ + var $_parts = array(); + + /** + * Headers for the mail + * + * @var array + * @access private + */ + var $_headers = array(); + + /** + * Build parameters + * + * @var array + * @access private + */ + var $_build_params = array( + // What encoding to use for the headers + // Options: quoted-printable or base64 + 'head_encoding' => 'quoted-printable', + // What encoding to use for plain text + // Options: 7bit, 8bit, base64, or quoted-printable + 'text_encoding' => 'quoted-printable', + // What encoding to use for html + // Options: 7bit, 8bit, base64, or quoted-printable + 'html_encoding' => 'quoted-printable', + // The character set to use for html + 'html_charset' => 'ISO-8859-1', + // The character set to use for text + 'text_charset' => 'ISO-8859-1', + // The character set to use for headers + 'head_charset' => 'ISO-8859-1', + // End-of-line sequence + 'eol' => "\r\n", + // Delay attachment files IO until building the message + 'delay_file_io' => false + ); + + /** + * Constructor function + * + * @param mixed $params Build parameters that change the way the email + * is built. Should be an associative array. + * See $_build_params. + * + * @return void + * @access public + */ + function Mail_mime($params = array()) + { + // Backward-compatible EOL setting + if (is_string($params)) { + $this->_build_params['eol'] = $params; + } else if (defined('MAIL_MIME_CRLF') && !isset($params['eol'])) { + $this->_build_params['eol'] = MAIL_MIME_CRLF; + } + + // Update build parameters + if (!empty($params) && is_array($params)) { + while (list($key, $value) = each($params)) { + $this->_build_params[$key] = $value; + } + } + } + + /** + * Set build parameter value + * + * @param string $name Parameter name + * @param string $value Parameter value + * + * @return void + * @access public + * @since 1.6.0 + */ + function setParam($name, $value) + { + $this->_build_params[$name] = $value; + } + + /** + * Get build parameter value + * + * @param string $name Parameter name + * + * @return mixed Parameter value + * @access public + * @since 1.6.0 + */ + function getParam($name) + { + return isset($this->_build_params[$name]) ? $this->_build_params[$name] : null; + } + + /** + * Accessor function to set the body text. Body text is used if + * it's not an html mail being sent or else is used to fill the + * text/plain part that emails clients who don't support + * html should show. + * + * @param string $data Either a string or + * the file name with the contents + * @param bool $isfile If true the first param should be treated + * as a file name, else as a string (default) + * @param bool $append If true the text or file is appended to + * the existing body, else the old body is + * overwritten + * + * @return mixed True on success or PEAR_Error object + * @access public + */ + function setTXTBody($data, $isfile = false, $append = false) + { + if (!$isfile) { + if (!$append) { + $this->_txtbody = $data; + } else { + $this->_txtbody .= $data; + } + } else { + $cont = $this->_file2str($data); + if (PEAR::isError($cont)) { + return $cont; + } + if (!$append) { + $this->_txtbody = $cont; + } else { + $this->_txtbody .= $cont; + } + } + return true; + } + + /** + * Get message text body + * + * @return string Text body + * @access public + * @since 1.6.0 + */ + function getTXTBody() + { + return $this->_txtbody; + } + + /** + * Adds a html part to the mail. + * + * @param string $data Either a string or the file name with the + * contents + * @param bool $isfile A flag that determines whether $data is a + * filename, or a string(false, default) + * + * @return bool True on success + * @access public + */ + function setHTMLBody($data, $isfile = false) + { + if (!$isfile) { + $this->_htmlbody = $data; + } else { + $cont = $this->_file2str($data); + if (PEAR::isError($cont)) { + return $cont; + } + $this->_htmlbody = $cont; + } + + return true; + } + + /** + * Get message HTML body + * + * @return string HTML body + * @access public + * @since 1.6.0 + */ + function getHTMLBody() + { + return $this->_htmlbody; + } + + /** + * Adds an image to the list of embedded images. + * + * @param string $file The image file name OR image data itself + * @param string $c_type The content type + * @param string $name The filename of the image. + * Only used if $file is the image data. + * @param bool $isfile Whether $file is a filename or not. + * Defaults to true + * @param string $content_id Desired Content-ID of MIME part + * Defaults to generated unique ID + * + * @return bool True on success + * @access public + */ + function addHTMLImage($file, + $c_type='application/octet-stream', + $name = '', + $isfile = true, + $content_id = null + ) { + $bodyfile = null; + + if ($isfile) { + // Don't load file into memory + if ($this->_build_params['delay_file_io']) { + $filedata = null; + $bodyfile = $file; + } else { + if (PEAR::isError($filedata = $this->_file2str($file))) { + return $filedata; + } + } + $filename = ($name ? $name : $file); + } else { + $filedata = $file; + $filename = $name; + } + + if (!$content_id) { + $content_id = md5(uniqid(time())); + } + + $this->_html_images[] = array( + 'body' => $filedata, + 'body_file' => $bodyfile, + 'name' => $filename, + 'c_type' => $c_type, + 'cid' => $content_id + ); + + return true; + } + + /** + * Adds a file to the list of attachments. + * + * @param string $file The file name of the file to attach + * or the file contents itself + * @param string $c_type The content type + * @param string $name The filename of the attachment + * Only use if $file is the contents + * @param bool $isfile Whether $file is a filename or not. Defaults to true + * @param string $encoding The type of encoding to use. Defaults to base64. + * Possible values: 7bit, 8bit, base64 or quoted-printable. + * @param string $disposition The content-disposition of this file + * Defaults to attachment. + * Possible values: attachment, inline. + * @param string $charset The character set of attachment's content. + * @param string $language The language of the attachment + * @param string $location The RFC 2557.4 location of the attachment + * @param string $n_encoding Encoding of the attachment's name in Content-Type + * By default filenames are encoded using RFC2231 method + * Here you can set RFC2047 encoding (quoted-printable + * or base64) instead + * @param string $f_encoding Encoding of the attachment's filename + * in Content-Disposition header. + * @param string $description Content-Description header + * @param string $h_charset The character set of the headers e.g. filename + * If not specified, $charset will be used + * @param array $add_headers Additional part headers. Array keys can be in form + * of <header_name>:<parameter_name> + * + * @return mixed True on success or PEAR_Error object + * @access public + */ + function addAttachment($file, + $c_type = 'application/octet-stream', + $name = '', + $isfile = true, + $encoding = 'base64', + $disposition = 'attachment', + $charset = '', + $language = '', + $location = '', + $n_encoding = null, + $f_encoding = null, + $description = '', + $h_charset = null, + $add_headers = array() + ) { + $bodyfile = null; + + if ($isfile) { + // Don't load file into memory + if ($this->_build_params['delay_file_io']) { + $filedata = null; + $bodyfile = $file; + } else { + if (PEAR::isError($filedata = $this->_file2str($file))) { + return $filedata; + } + } + // Force the name the user supplied, otherwise use $file + $filename = ($name ? $name : $file); + } else { + $filedata = $file; + $filename = $name; + } + + if (!strlen($filename)) { + $msg = "The supplied filename for the attachment can't be empty"; + $err = PEAR::raiseError($msg); + return $err; + } + $filename = $this->_basename($filename); + + $this->_parts[] = array( + 'body' => $filedata, + 'body_file' => $bodyfile, + 'name' => $filename, + 'c_type' => $c_type, + 'charset' => $charset, + 'encoding' => $encoding, + 'language' => $language, + 'location' => $location, + 'disposition' => $disposition, + 'description' => $description, + 'add_headers' => $add_headers, + 'name_encoding' => $n_encoding, + 'filename_encoding' => $f_encoding, + 'headers_charset' => $h_charset, + ); + + return true; + } + + /** + * Get the contents of the given file name as string + * + * @param string $file_name Path of file to process + * + * @return string Contents of $file_name + * @access private + */ + function &_file2str($file_name) + { + // Check state of file and raise an error properly + if (!file_exists($file_name)) { + $err = PEAR::raiseError('File not found: ' . $file_name); + return $err; + } + if (!is_file($file_name)) { + $err = PEAR::raiseError('Not a regular file: ' . $file_name); + return $err; + } + if (!is_readable($file_name)) { + $err = PEAR::raiseError('File is not readable: ' . $file_name); + return $err; + } + + // Temporarily reset magic_quotes_runtime and read file contents + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + $cont = file_get_contents($file_name); + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + return $cont; + } + + /** + * Adds a text subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * @param string $text The text to add. + * + * @return object The text mimePart object + * @access private + */ + function &_addTextPart(&$obj, $text) + { + $params['content_type'] = 'text/plain'; + $params['encoding'] = $this->_build_params['text_encoding']; + $params['charset'] = $this->_build_params['text_charset']; + $params['eol'] = $this->_build_params['eol']; + + if (is_object($obj)) { + $ret = $obj->addSubpart($text, $params); + return $ret; + } else { + $ret = new Mail_mimePart($text, $params); + return $ret; + } + } + + /** + * Adds a html subpart to the mimePart object and + * returns it during the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * + * @return object The html mimePart object + * @access private + */ + function &_addHtmlPart(&$obj) + { + $params['content_type'] = 'text/html'; + $params['encoding'] = $this->_build_params['html_encoding']; + $params['charset'] = $this->_build_params['html_charset']; + $params['eol'] = $this->_build_params['eol']; + + if (is_object($obj)) { + $ret = $obj->addSubpart($this->_htmlbody, $params); + return $ret; + } else { + $ret = new Mail_mimePart($this->_htmlbody, $params); + return $ret; + } + } + + /** + * Creates a new mimePart object, using multipart/mixed as + * the initial content-type and returns it during the + * build process. + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addMixedPart() + { + $params = array(); + $params['content_type'] = 'multipart/mixed'; + $params['eol'] = $this->_build_params['eol']; + + // Create empty multipart/mixed Mail_mimePart object to return + $ret = new Mail_mimePart('', $params); + return $ret; + } + + /** + * Adds a multipart/alternative part to a mimePart + * object (or creates one), and returns it during + * the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created. + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addAlternativePart(&$obj) + { + $params['content_type'] = 'multipart/alternative'; + $params['eol'] = $this->_build_params['eol']; + + if (is_object($obj)) { + return $obj->addSubpart('', $params); + } else { + $ret = new Mail_mimePart('', $params); + return $ret; + } + } + + /** + * Adds a multipart/related part to a mimePart + * object (or creates one), and returns it during + * the build process. + * + * @param mixed &$obj The object to add the part to, or + * null if a new object is to be created + * + * @return object The multipart/mixed mimePart object + * @access private + */ + function &_addRelatedPart(&$obj) + { + $params['content_type'] = 'multipart/related'; + $params['eol'] = $this->_build_params['eol']; + + if (is_object($obj)) { + return $obj->addSubpart('', $params); + } else { + $ret = new Mail_mimePart('', $params); + return $ret; + } + } + + /** + * Adds an html image subpart to a mimePart object + * and returns it during the build process. + * + * @param object &$obj The mimePart to add the image to + * @param array $value The image information + * + * @return object The image mimePart object + * @access private + */ + function &_addHtmlImagePart(&$obj, $value) + { + $params['content_type'] = $value['c_type']; + $params['encoding'] = 'base64'; + $params['disposition'] = 'inline'; + $params['filename'] = $value['name']; + $params['cid'] = $value['cid']; + $params['body_file'] = $value['body_file']; + $params['eol'] = $this->_build_params['eol']; + + if (!empty($value['name_encoding'])) { + $params['name_encoding'] = $value['name_encoding']; + } + if (!empty($value['filename_encoding'])) { + $params['filename_encoding'] = $value['filename_encoding']; + } + + $ret = $obj->addSubpart($value['body'], $params); + return $ret; + } + + /** + * Adds an attachment subpart to a mimePart object + * and returns it during the build process. + * + * @param object &$obj The mimePart to add the image to + * @param array $value The attachment information + * + * @return object The image mimePart object + * @access private + */ + function &_addAttachmentPart(&$obj, $value) + { + $params['eol'] = $this->_build_params['eol']; + $params['filename'] = $value['name']; + $params['encoding'] = $value['encoding']; + $params['content_type'] = $value['c_type']; + $params['body_file'] = $value['body_file']; + $params['disposition'] = isset($value['disposition']) ? + $value['disposition'] : 'attachment'; + + // content charset + if (!empty($value['charset'])) { + $params['charset'] = $value['charset']; + } + // headers charset (filename, description) + if (!empty($value['headers_charset'])) { + $params['headers_charset'] = $value['headers_charset']; + } + if (!empty($value['language'])) { + $params['language'] = $value['language']; + } + if (!empty($value['location'])) { + $params['location'] = $value['location']; + } + if (!empty($value['name_encoding'])) { + $params['name_encoding'] = $value['name_encoding']; + } + if (!empty($value['filename_encoding'])) { + $params['filename_encoding'] = $value['filename_encoding']; + } + if (!empty($value['description'])) { + $params['description'] = $value['description']; + } + if (is_array($value['add_headers'])) { + $params['headers'] = $value['add_headers']; + } + + $ret = $obj->addSubpart($value['body'], $params); + return $ret; + } + + /** + * Returns the complete e-mail, ready to send using an alternative + * mail delivery method. Note that only the mailpart that is made + * with Mail_Mime is created. This means that, + * YOU WILL HAVE NO TO: HEADERS UNLESS YOU SET IT YOURSELF + * using the $headers parameter! + * + * @param string $separation The separation between these two parts. + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * @param array $headers The extra headers that should be passed + * to the &headers() function. + * See that function for more info. + * @param bool $overwrite Overwrite the existing headers with new. + * + * @return mixed The complete e-mail or PEAR error object + * @access public + */ + function getMessage($separation = null, $params = null, $headers = null, + $overwrite = false + ) { + if ($separation === null) { + $separation = $this->_build_params['eol']; + } + + $body = $this->get($params); + + if (PEAR::isError($body)) { + return $body; + } + + $head = $this->txtHeaders($headers, $overwrite); + $mail = $head . $separation . $body; + return $mail; + } + + /** + * Returns the complete e-mail body, ready to send using an alternative + * mail delivery method. + * + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * + * @return mixed The e-mail body or PEAR error object + * @access public + * @since 1.6.0 + */ + function getMessageBody($params = null) + { + return $this->get($params, null, true); + } + + /** + * Writes (appends) the complete e-mail into file. + * + * @param string $filename Output file location + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * @param array $headers The extra headers that should be passed + * to the &headers() function. + * See that function for more info. + * @param bool $overwrite Overwrite the existing headers with new. + * + * @return mixed True or PEAR error object + * @access public + * @since 1.6.0 + */ + function saveMessage($filename, $params = null, $headers = null, $overwrite = false) + { + // Check state of file and raise an error properly + if (file_exists($filename) && !is_writable($filename)) { + $err = PEAR::raiseError('File is not writable: ' . $filename); + return $err; + } + + // Temporarily reset magic_quotes_runtime and read file contents + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + + if (!($fh = fopen($filename, 'ab'))) { + $err = PEAR::raiseError('Unable to open file: ' . $filename); + return $err; + } + + // Write message headers into file (skipping Content-* headers) + $head = $this->txtHeaders($headers, $overwrite, true); + if (fwrite($fh, $head) === false) { + $err = PEAR::raiseError('Error writing to file: ' . $filename); + return $err; + } + + fclose($fh); + + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + // Write the rest of the message into file + $res = $this->get($params, $filename); + + return $res ? $res : true; + } + + /** + * Writes (appends) the complete e-mail body into file. + * + * @param string $filename Output file location + * @param array $params The Build parameters passed to the + * &get() function. See &get for more info. + * + * @return mixed True or PEAR error object + * @access public + * @since 1.6.0 + */ + function saveMessageBody($filename, $params = null) + { + // Check state of file and raise an error properly + if (file_exists($filename) && !is_writable($filename)) { + $err = PEAR::raiseError('File is not writable: ' . $filename); + return $err; + } + + // Temporarily reset magic_quotes_runtime and read file contents + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + + if (!($fh = fopen($filename, 'ab'))) { + $err = PEAR::raiseError('Unable to open file: ' . $filename); + return $err; + } + + // Write the rest of the message into file + $res = $this->get($params, $filename, true); + + return $res ? $res : true; + } + + /** + * Builds the multipart message from the list ($this->_parts) and + * returns the mime content. + * + * @param array $params Build parameters that change the way the email + * is built. Should be associative. See $_build_params. + * @param resource $filename Output file where to save the message instead of + * returning it + * @param boolean $skip_head True if you want to return/save only the message + * without headers + * + * @return mixed The MIME message content string, null or PEAR error object + * @access public + */ + function &get($params = null, $filename = null, $skip_head = false) + { + if (isset($params)) { + while (list($key, $value) = each($params)) { + $this->_build_params[$key] = $value; + } + } + + if (isset($this->_headers['From'])) { + // Bug #11381: Illegal characters in domain ID + if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->_headers['From'], $matches)) { + $domainID = $matches[1]; + } else { + $domainID = '@localhost'; + } + foreach ($this->_html_images as $i => $img) { + $cid = $this->_html_images[$i]['cid']; + if (!preg_match('#'.preg_quote($domainID).'$#', $cid)) { + $this->_html_images[$i]['cid'] = $cid . $domainID; + } + } + } + + if (count($this->_html_images) && isset($this->_htmlbody)) { + foreach ($this->_html_images as $key => $value) { + $regex = array(); + $regex[] = '#(\s)((?i)src|background|href(?-i))\s*=\s*(["\']?)' . + preg_quote($value['name'], '#') . '\3#'; + $regex[] = '#(?i)url(?-i)\(\s*(["\']?)' . + preg_quote($value['name'], '#') . '\1\s*\)#'; + + $rep = array(); + $rep[] = '\1\2=\3cid:' . $value['cid'] .'\3'; + $rep[] = 'url(\1cid:' . $value['cid'] . '\1)'; + + $this->_htmlbody = preg_replace($regex, $rep, $this->_htmlbody); + $this->_html_images[$key]['name'] + = $this->_basename($this->_html_images[$key]['name']); + } + } + + $this->_checkParams(); + + $null = null; + $attachments = count($this->_parts) ? true : false; + $html_images = count($this->_html_images) ? true : false; + $html = strlen($this->_htmlbody) ? true : false; + $text = (!$html && strlen($this->_txtbody)) ? true : false; + + switch (true) { + case $text && !$attachments: + $message =& $this->_addTextPart($null, $this->_txtbody); + break; + + case !$text && !$html && $attachments: + $message =& $this->_addMixedPart(); + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $text && $attachments: + $message =& $this->_addMixedPart(); + $this->_addTextPart($message, $this->_txtbody); + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $html && !$attachments && !$html_images: + if (isset($this->_txtbody)) { + $message =& $this->_addAlternativePart($null); + $this->_addTextPart($message, $this->_txtbody); + $this->_addHtmlPart($message); + } else { + $message =& $this->_addHtmlPart($null); + } + break; + + case $html && !$attachments && $html_images: + // * Content-Type: multipart/alternative; + // * text + // * Content-Type: multipart/related; + // * html + // * image... + if (isset($this->_txtbody)) { + $message =& $this->_addAlternativePart($null); + $this->_addTextPart($message, $this->_txtbody); + + $ht =& $this->_addRelatedPart($message); + $this->_addHtmlPart($ht); + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($ht, $this->_html_images[$i]); + } + } else { + // * Content-Type: multipart/related; + // * html + // * image... + $message =& $this->_addRelatedPart($null); + $this->_addHtmlPart($message); + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($message, $this->_html_images[$i]); + } + } + /* + // #13444, #9725: the code below was a non-RFC compliant hack + // * Content-Type: multipart/related; + // * Content-Type: multipart/alternative; + // * text + // * html + // * image... + $message =& $this->_addRelatedPart($null); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $this->_addHtmlPart($alt); + } else { + $this->_addHtmlPart($message); + } + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($message, $this->_html_images[$i]); + } + */ + break; + + case $html && $attachments && !$html_images: + $message =& $this->_addMixedPart(); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $this->_addHtmlPart($alt); + } else { + $this->_addHtmlPart($message); + } + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + case $html && $attachments && $html_images: + $message =& $this->_addMixedPart(); + if (isset($this->_txtbody)) { + $alt =& $this->_addAlternativePart($message); + $this->_addTextPart($alt, $this->_txtbody); + $rel =& $this->_addRelatedPart($alt); + } else { + $rel =& $this->_addRelatedPart($message); + } + $this->_addHtmlPart($rel); + for ($i = 0; $i < count($this->_html_images); $i++) { + $this->_addHtmlImagePart($rel, $this->_html_images[$i]); + } + for ($i = 0; $i < count($this->_parts); $i++) { + $this->_addAttachmentPart($message, $this->_parts[$i]); + } + break; + + } + + if (!isset($message)) { + $ret = null; + return $ret; + } + + // Use saved boundary + if (!empty($this->_build_params['boundary'])) { + $boundary = $this->_build_params['boundary']; + } else { + $boundary = null; + } + + // Write output to file + if ($filename) { + // Append mimePart message headers and body into file + $headers = $message->encodeToFile($filename, $boundary, $skip_head); + if (PEAR::isError($headers)) { + return $headers; + } + $this->_headers = array_merge($this->_headers, $headers); + $ret = null; + return $ret; + } else { + $output = $message->encode($boundary, $skip_head); + if (PEAR::isError($output)) { + return $output; + } + $this->_headers = array_merge($this->_headers, $output['headers']); + $body = $output['body']; + return $body; + } + } + + /** + * Returns an array with the headers needed to prepend to the email + * (MIME-Version and Content-Type). Format of argument is: + * $array['header-name'] = 'header-value'; + * + * @param array $xtra_headers Assoc array with any extra headers (optional) + * (Don't set Content-Type for multipart messages here!) + * @param bool $overwrite Overwrite already existing headers. + * @param bool $skip_content Don't return content headers: Content-Type, + * Content-Disposition and Content-Transfer-Encoding + * + * @return array Assoc array with the mime headers + * @access public + */ + function &headers($xtra_headers = null, $overwrite = false, $skip_content = false) + { + // Add mime version header + $headers['MIME-Version'] = '1.0'; + + // Content-Type and Content-Transfer-Encoding headers should already + // be present if get() was called, but we'll re-set them to make sure + // we got them when called before get() or something in the message + // has been changed after get() [#14780] + if (!$skip_content) { + $headers += $this->_contentHeaders(); + } + + if (!empty($xtra_headers)) { + $headers = array_merge($headers, $xtra_headers); + } + + if ($overwrite) { + $this->_headers = array_merge($this->_headers, $headers); + } else { + $this->_headers = array_merge($headers, $this->_headers); + } + + $headers = $this->_headers; + + if ($skip_content) { + unset($headers['Content-Type']); + unset($headers['Content-Transfer-Encoding']); + unset($headers['Content-Disposition']); + } else if (!empty($this->_build_params['ctype'])) { + $headers['Content-Type'] = $this->_build_params['ctype']; + } + + $encodedHeaders = $this->_encodeHeaders($headers); + return $encodedHeaders; + } + + /** + * Get the text version of the headers + * (useful if you want to use the PHP mail() function) + * + * @param array $xtra_headers Assoc array with any extra headers (optional) + * (Don't set Content-Type for multipart messages here!) + * @param bool $overwrite Overwrite the existing headers with new. + * @param bool $skip_content Don't return content headers: Content-Type, + * Content-Disposition and Content-Transfer-Encoding + * + * @return string Plain text headers + * @access public + */ + function txtHeaders($xtra_headers = null, $overwrite = false, $skip_content = false) + { + $headers = $this->headers($xtra_headers, $overwrite, $skip_content); + + // Place Received: headers at the beginning of the message + // Spam detectors often flag messages with it after the Subject: as spam + if (isset($headers['Received'])) { + $received = $headers['Received']; + unset($headers['Received']); + $headers = array('Received' => $received) + $headers; + } + + $ret = ''; + $eol = $this->_build_params['eol']; + + foreach ($headers as $key => $val) { + if (is_array($val)) { + foreach ($val as $value) { + $ret .= "$key: $value" . $eol; + } + } else { + $ret .= "$key: $val" . $eol; + } + } + + return $ret; + } + + /** + * Sets message Content-Type header. + * Use it to build messages with various content-types e.g. miltipart/raport + * not supported by _contentHeaders() function. + * + * @param string $type Type name + * @param array $params Hash array of header parameters + * + * @return void + * @access public + * @since 1.7.0 + */ + function setContentType($type, $params = array()) + { + $header = $type; + + $eol = !empty($this->_build_params['eol']) + ? $this->_build_params['eol'] : "\r\n"; + + // add parameters + $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D' + . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; + if (is_array($params)) { + foreach ($params as $name => $value) { + if ($name == 'boundary') { + $this->_build_params['boundary'] = $value; + } + if (!preg_match($token_regexp, $value)) { + $header .= ";$eol $name=$value"; + } else { + $value = addcslashes($value, '\\"'); + $header .= ";$eol $name=\"$value\""; + } + } + } + + // add required boundary parameter if not defined + if (preg_match('/^multipart\//i', $type)) { + if (empty($this->_build_params['boundary'])) { + $this->_build_params['boundary'] = '=_' . md5(rand() . microtime()); + } + + $header .= ";$eol boundary=\"".$this->_build_params['boundary']."\""; + } + + $this->_build_params['ctype'] = $header; + } + + /** + * Sets the Subject header + * + * @param string $subject String to set the subject to. + * + * @return void + * @access public + */ + function setSubject($subject) + { + $this->_headers['Subject'] = $subject; + } + + /** + * Set an email to the From (the sender) header + * + * @param string $email The email address to use + * + * @return void + * @access public + */ + function setFrom($email) + { + $this->_headers['From'] = $email; + } + + /** + * Add an email to the To header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addTo($email) + { + if (isset($this->_headers['To'])) { + $this->_headers['To'] .= ", $email"; + } else { + $this->_headers['To'] = $email; + } + } + + /** + * Add an email to the Cc (carbon copy) header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addCc($email) + { + if (isset($this->_headers['Cc'])) { + $this->_headers['Cc'] .= ", $email"; + } else { + $this->_headers['Cc'] = $email; + } + } + + /** + * Add an email to the Bcc (blank carbon copy) header + * (multiple calls to this method are allowed) + * + * @param string $email The email direction to add + * + * @return void + * @access public + */ + function addBcc($email) + { + if (isset($this->_headers['Bcc'])) { + $this->_headers['Bcc'] .= ", $email"; + } else { + $this->_headers['Bcc'] = $email; + } + } + + /** + * Since the PHP send function requires you to specify + * recipients (To: header) separately from the other + * headers, the To: header is not properly encoded. + * To fix this, you can use this public method to + * encode your recipients before sending to the send + * function + * + * @param string $recipients A comma-delimited list of recipients + * + * @return string Encoded data + * @access public + */ + function encodeRecipients($recipients) + { + $input = array("To" => $recipients); + $retval = $this->_encodeHeaders($input); + return $retval["To"] ; + } + + /** + * Encodes headers as per RFC2047 + * + * @param array $input The header data to encode + * @param array $params Extra build parameters + * + * @return array Encoded data + * @access private + */ + function _encodeHeaders($input, $params = array()) + { + $build_params = $this->_build_params; + while (list($key, $value) = each($params)) { + $build_params[$key] = $value; + } + + foreach ($input as $hdr_name => $hdr_value) { + if (is_array($hdr_value)) { + foreach ($hdr_value as $idx => $value) { + $input[$hdr_name][$idx] = $this->encodeHeader( + $hdr_name, $value, + $build_params['head_charset'], $build_params['head_encoding'] + ); + } + } else { + $input[$hdr_name] = $this->encodeHeader( + $hdr_name, $hdr_value, + $build_params['head_charset'], $build_params['head_encoding'] + ); + } + } + + return $input; + } + + /** + * Encodes a header as per RFC2047 + * + * @param string $name The header name + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * + * @return string Encoded header data (without a name) + * @access public + * @since 1.5.3 + */ + function encodeHeader($name, $value, $charset, $encoding) + { + $mime_part = new Mail_mimePart; + return $mime_part->encodeHeader( + $name, $value, $charset, $encoding, $this->_build_params['eol'] + ); + } + + /** + * Get file's basename (locale independent) + * + * @param string $filename Filename + * + * @return string Basename + * @access private + */ + function _basename($filename) + { + // basename() is not unicode safe and locale dependent + if (stristr(PHP_OS, 'win') || stristr(PHP_OS, 'netware')) { + return preg_replace('/^.*[\\\\\\/]/', '', $filename); + } else { + return preg_replace('/^.*[\/]/', '', $filename); + } + } + + /** + * Get Content-Type and Content-Transfer-Encoding headers of the message + * + * @return array Headers array + * @access private + */ + function _contentHeaders() + { + $attachments = count($this->_parts) ? true : false; + $html_images = count($this->_html_images) ? true : false; + $html = strlen($this->_htmlbody) ? true : false; + $text = (!$html && strlen($this->_txtbody)) ? true : false; + $headers = array(); + + // See get() + switch (true) { + case $text && !$attachments: + $headers['Content-Type'] = 'text/plain'; + break; + + case !$text && !$html && $attachments: + case $text && $attachments: + case $html && $attachments && !$html_images: + case $html && $attachments && $html_images: + $headers['Content-Type'] = 'multipart/mixed'; + break; + + case $html && !$attachments && !$html_images && isset($this->_txtbody): + case $html && !$attachments && $html_images && isset($this->_txtbody): + $headers['Content-Type'] = 'multipart/alternative'; + break; + + case $html && !$attachments && !$html_images && !isset($this->_txtbody): + $headers['Content-Type'] = 'text/html'; + break; + + case $html && !$attachments && $html_images && !isset($this->_txtbody): + $headers['Content-Type'] = 'multipart/related'; + break; + + default: + return $headers; + } + + $this->_checkParams(); + + $eol = !empty($this->_build_params['eol']) + ? $this->_build_params['eol'] : "\r\n"; + + if ($headers['Content-Type'] == 'text/plain') { + // single-part message: add charset and encoding + $charset = 'charset=' . $this->_build_params['text_charset']; + // place charset parameter in the same line, if possible + // 26 = strlen("Content-Type: text/plain; ") + $headers['Content-Type'] + .= (strlen($charset) + 26 <= 76) ? "; $charset" : ";$eol $charset"; + $headers['Content-Transfer-Encoding'] + = $this->_build_params['text_encoding']; + } else if ($headers['Content-Type'] == 'text/html') { + // single-part message: add charset and encoding + $charset = 'charset=' . $this->_build_params['html_charset']; + // place charset parameter in the same line, if possible + $headers['Content-Type'] + .= (strlen($charset) + 25 <= 76) ? "; $charset" : ";$eol $charset"; + $headers['Content-Transfer-Encoding'] + = $this->_build_params['html_encoding']; + } else { + // multipart message: and boundary + if (!empty($this->_build_params['boundary'])) { + $boundary = $this->_build_params['boundary']; + } else if (!empty($this->_headers['Content-Type']) + && preg_match('/boundary="([^"]+)"/', $this->_headers['Content-Type'], $m) + ) { + $boundary = $m[1]; + } else { + $boundary = '=_' . md5(rand() . microtime()); + } + + $this->_build_params['boundary'] = $boundary; + $headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + } + + return $headers; + } + + /** + * Validate and set build parameters + * + * @return void + * @access private + */ + function _checkParams() + { + $encodings = array('7bit', '8bit', 'base64', 'quoted-printable'); + + $this->_build_params['text_encoding'] + = strtolower($this->_build_params['text_encoding']); + $this->_build_params['html_encoding'] + = strtolower($this->_build_params['html_encoding']); + + if (!in_array($this->_build_params['text_encoding'], $encodings)) { + $this->_build_params['text_encoding'] = '7bit'; + } + if (!in_array($this->_build_params['html_encoding'], $encodings)) { + $this->_build_params['html_encoding'] = '7bit'; + } + + // text body + if ($this->_build_params['text_encoding'] == '7bit' + && !preg_match('/ascii/i', $this->_build_params['text_charset']) + && preg_match('/[^\x00-\x7F]/', $this->_txtbody) + ) { + $this->_build_params['text_encoding'] = 'quoted-printable'; + } + // html body + if ($this->_build_params['html_encoding'] == '7bit' + && !preg_match('/ascii/i', $this->_build_params['html_charset']) + && preg_match('/[^\x00-\x7F]/', $this->_htmlbody) + ) { + $this->_build_params['html_encoding'] = 'quoted-printable'; + } + } + +} // End of class diff --git a/lib/ext/Mail/mimeDecode.php b/lib/ext/Mail/mimeDecode.php new file mode 100644 index 0000000..677d245 --- /dev/null +++ b/lib/ext/Mail/mimeDecode.php @@ -0,0 +1,1003 @@ +<?php +/** + * The Mail_mimeDecode class is used to decode mail/mime messages + * + * This class will parse a raw mime email and return + * the structure. Returned structure is similar to + * that returned by imap_fetchstructure(). + * + * +----------------------------- IMPORTANT ------------------------------+ + * | Usage of this class compared to native php extensions such as | + * | mailparse or imap, is slow and may be feature deficient. If available| + * | you are STRONGLY recommended to use the php extensions. | + * +----------------------------------------------------------------------+ + * + * Compatible with PHP versions 4 and 5 + * + * LICENSE: This LICENSE is in the BSD license style. + * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> + * Copyright (c) 2003-2006, PEAR <pear-group@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author George Schlossnagle <george@omniti.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version CVS: $Id$ + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * require PEAR + * + * This package depends on PEAR to raise errors. + */ +require_once 'PEAR.php'; + + +/** + * The Mail_mimeDecode class is used to decode mail/mime messages + * + * This class will parse a raw mime email and return the structure. + * Returned structure is similar to that returned by imap_fetchstructure(). + * + * +----------------------------- IMPORTANT ------------------------------+ + * | Usage of this class compared to native php extensions such as | + * | mailparse or imap, is slow and may be feature deficient. If available| + * | you are STRONGLY recommended to use the php extensions. | + * +----------------------------------------------------------------------+ + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author George Schlossnagle <george@omniti.com> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mimeDecode extends PEAR +{ + /** + * The raw email to decode + * + * @var string + * @access private + */ + var $_input; + + /** + * The header part of the input + * + * @var string + * @access private + */ + var $_header; + + /** + * The body part of the input + * + * @var string + * @access private + */ + var $_body; + + /** + * If an error occurs, this is used to store the message + * + * @var string + * @access private + */ + var $_error; + + /** + * Flag to determine whether to include bodies in the + * returned object. + * + * @var boolean + * @access private + */ + var $_include_bodies; + + /** + * Flag to determine whether to decode bodies + * + * @var boolean + * @access private + */ + var $_decode_bodies; + + /** + * Flag to determine whether to decode headers + * + * @var boolean + * @access private + */ + var $_decode_headers; + + /** + * Flag to determine whether to include attached messages + * as body in the returned object. Depends on $_include_bodies + * + * @var boolean + * @access private + */ + var $_rfc822_bodies; + + /** + * Constructor. + * + * Sets up the object, initialise the variables, and splits and + * stores the header and body of the input. + * + * @param string The input to decode + * @access public + */ + function Mail_mimeDecode($input) + { + list($header, $body) = $this->_splitBodyHeader($input); + + $this->_input = $input; + $this->_header = $header; + $this->_body = $body; + $this->_decode_bodies = false; + $this->_include_bodies = true; + $this->_rfc822_bodies = false; + } + + /** + * Begins the decoding process. If called statically + * it will create an object and call the decode() method + * of it. + * + * @param array An array of various parameters that determine + * various things: + * include_bodies - Whether to include the body in the returned + * object. + * decode_bodies - Whether to decode the bodies + * of the parts. (Transfer encoding) + * decode_headers - Whether to decode headers + * input - If called statically, this will be treated + * as the input + * @return object Decoded results + * @access public + */ + function decode($params = null) + { + // determine if this method has been called statically + $isStatic = empty($this) || !is_a($this, __CLASS__); + + // Have we been called statically? + // If so, create an object and pass details to that. + if ($isStatic AND isset($params['input'])) { + + $obj = new Mail_mimeDecode($params['input']); + $structure = $obj->decode($params); + + // Called statically but no input + } elseif ($isStatic) { + return PEAR::raiseError('Called statically and no input given'); + + // Called via an object + } else { + $this->_include_bodies = isset($params['include_bodies']) ? + $params['include_bodies'] : false; + $this->_decode_bodies = isset($params['decode_bodies']) ? + $params['decode_bodies'] : false; + $this->_decode_headers = isset($params['decode_headers']) ? + $params['decode_headers'] : false; + $this->_rfc822_bodies = isset($params['rfc_822bodies']) ? + $params['rfc_822bodies'] : false; + + $structure = $this->_decode($this->_header, $this->_body); + if ($structure === false) { + $structure = $this->raiseError($this->_error); + } + } + + return $structure; + } + + /** + * Performs the decoding. Decodes the body string passed to it + * If it finds certain content-types it will call itself in a + * recursive fashion + * + * @param string Header section + * @param string Body section + * @return object Results of decoding process + * @access private + */ + function _decode($headers, $body, $default_ctype = 'text/plain') + { + $return = new stdClass; + $return->headers = array(); + $headers = $this->_parseHeaders($headers); + + foreach ($headers as $value) { + $value['value'] = $this->_decode_headers ? $this->_decodeHeader($value['value']) : $value['value']; + if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]); + $return->headers[strtolower($value['name'])][] = $value['value']; + + } elseif (isset($return->headers[strtolower($value['name'])])) { + $return->headers[strtolower($value['name'])][] = $value['value']; + + } else { + $return->headers[strtolower($value['name'])] = $value['value']; + } + } + + + foreach ($headers as $key => $value) { + $headers[$key]['name'] = strtolower($headers[$key]['name']); + switch ($headers[$key]['name']) { + + case 'content-type': + $content_type = $this->_parseHeaderValue($headers[$key]['value']); + + if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) { + $return->ctype_primary = $regs[1]; + $return->ctype_secondary = $regs[2]; + } + + if (isset($content_type['other'])) { + foreach($content_type['other'] as $p_name => $p_value) { + $return->ctype_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-disposition': + $content_disposition = $this->_parseHeaderValue($headers[$key]['value']); + $return->disposition = $content_disposition['value']; + if (isset($content_disposition['other'])) { + foreach($content_disposition['other'] as $p_name => $p_value) { + $return->d_parameters[$p_name] = $p_value; + } + } + break; + + case 'content-transfer-encoding': + $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']); + break; + } + } + + if (isset($content_type)) { + switch (strtolower($content_type['value'])) { + case 'text/plain': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; + break; + + case 'text/html': + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; + break; + + case 'multipart/parallel': + case 'multipart/appledouble': // Appledouble mail + case 'multipart/report': // RFC1892 + case 'multipart/signed': // PGP + case 'multipart/digest': + case 'multipart/alternative': + case 'multipart/related': + case 'multipart/mixed': + case 'application/vnd.wap.multipart.related': + if(!isset($content_type['other']['boundary'])){ + $this->_error = 'No boundary found for ' . $content_type['value'] . ' part'; + return false; + } + + $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain'; + + $parts = $this->_boundarySplit($body, $content_type['other']['boundary']); + for ($i = 0; $i < count($parts); $i++) { + list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]); + $part = $this->_decode($part_header, $part_body, $default_ctype); + if($part === false) + $part = $this->raiseError($this->_error); + $return->parts[] = $part; + } + break; + + case 'message/rfc822': + if ($this->_rfc822_bodies) { + $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; + $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body); + } + $obj = new Mail_mimeDecode($body); + $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies, + 'decode_bodies' => $this->_decode_bodies, + 'decode_headers' => $this->_decode_headers)); + unset($obj); + break; + + default: + if(!isset($content_transfer_encoding['value'])) + $content_transfer_encoding['value'] = '7bit'; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null; + break; + } + + } else { + $ctype = explode('/', $default_ctype); + $return->ctype_primary = $ctype[0]; + $return->ctype_secondary = $ctype[1]; + $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null; + } + + return $return; + } + + /** + * Given the output of the above function, this will return an + * array of references to the parts, indexed by mime number. + * + * @param object $structure The structure to go through + * @param string $mime_number Internal use only. + * @return array Mime numbers + */ + function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '') + { + $return = array(); + if (!empty($structure->parts)) { + if ($mime_number != '') { + $structure->mime_id = $prepend . $mime_number; + $return[$prepend . $mime_number] = &$structure; + } + for ($i = 0; $i < count($structure->parts); $i++) { + + + if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { + $prepend = $prepend . $mime_number . '.'; + $_mime_number = ''; + } else { + $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1)); + } + + $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend); + foreach ($arr as $key => $val) { + $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key]; + } + } + } else { + if ($mime_number == '') { + $mime_number = '1'; + } + $structure->mime_id = $prepend . $mime_number; + $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; + } + + return $return; + } + + /** + * Given a string containing a header and body + * section, this function will split them (at the first + * blank line) and return them. + * + * @param string Input to split apart + * @return array Contains header and body section + * @access private + */ + function _splitBodyHeader($input) + { + if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { + return array($match[1], $match[2]); + } + // bug #17325 - empty bodies are allowed. - we just check that at least one line + // of headers exist.. + if (count(explode("\n",$input))) { + return array($input, ''); + } + $this->_error = 'Could not split header and body'; + return false; + } + + /** + * Parse headers given in $input and return + * as assoc array. + * + * @param string Headers to parse + * @return array Contains parsed headers + * @access private + */ + function _parseHeaders($input) + { + + if ($input !== '') { + // Unfold the input + $input = preg_replace("/\r?\n/", "\r\n", $input); + //#7065 - wrapping.. with encoded stuff.. - probably not needed, + // wrapping space should only get removed if the trailing item on previous line is a + // encoded character + $input = preg_replace("/=\r\n(\t| )+/", '=', $input); + $input = preg_replace("/\r\n(\t| )+/", ' ', $input); + + $headers = explode("\r\n", trim($input)); + + foreach ($headers as $value) { + $hdr_name = substr($value, 0, $pos = strpos($value, ':')); + $hdr_value = substr($value, $pos+1); + if($hdr_value[0] == ' ') + $hdr_value = substr($hdr_value, 1); + + $return[] = array( + 'name' => $hdr_name, + 'value' => $hdr_value + ); + } + } else { + $return = array(); + } + + return $return; + } + + /** + * Function to parse a header value, + * extract first part, and any secondary + * parts (after ;) This function is not as + * robust as it could be. Eg. header comments + * in the wrong place will probably break it. + * + * @param string Header value to parse + * @return array Contains parsed result + * @access private + */ + function _parseHeaderValue($input) + { + + if (($pos = strpos($input, ';')) === false) { + $input = $this->_decode_headers ? $this->_decodeHeader($input) : $input; + $return['value'] = trim($input); + return $return; + } + + + + $value = substr($input, 0, $pos); + $value = $this->_decode_headers ? $this->_decodeHeader($value) : $value; + $return['value'] = trim($value); + $input = trim(substr($input, $pos+1)); + + if (!strlen($input) > 0) { + return $return; + } + // at this point input contains xxxx=".....";zzzz="...." + // since we are dealing with quoted strings, we need to handle this properly.. + $i = 0; + $l = strlen($input); + $key = ''; + $val = false; // our string - including quotes.. + $q = false; // in quote.. + $lq = ''; // last quote.. + + while ($i < $l) { + + $c = $input[$i]; + //var_dump(array('i'=>$i,'c'=>$c,'q'=>$q, 'lq'=>$lq, 'key'=>$key, 'val' =>$val)); + + $escaped = false; + if ($c == '\\') { + $i++; + if ($i == $l-1) { // end of string. + break; + } + $escaped = true; + $c = $input[$i]; + } + + + // state - in key.. + if ($val === false) { + if (!$escaped && $c == '=') { + $val = ''; + $key = trim($key); + $i++; + continue; + } + if (!$escaped && $c == ';') { + if ($key) { // a key without a value.. + $key= trim($key); + $return['other'][$key] = ''; + $return['other'][strtolower($key)] = ''; + } + $key = ''; + } + $key .= $c; + $i++; + continue; + } + + // state - in value.. (as $val is set..) + + if ($q === false) { + // not in quote yet. + if ((!strlen($val) || $lq !== false) && $c == ' ' || $c == "\t") { + $i++; + continue; // skip leading spaces after '=' or after '"' + } + if (!$escaped && ($c == '"' || $c == "'")) { + // start quoted area.. + $q = $c; + // in theory should not happen raw text in value part.. + // but we will handle it as a merged part of the string.. + $val = !strlen(trim($val)) ? '' : trim($val); + $i++; + continue; + } + // got end.... + if (!$escaped && $c == ';') { + + $val = trim($val); + $added = false; + if (preg_match('/\*[0-9]+$/', $key)) { + // this is the extended aaa*0=...;aaa*1=.... code + // it assumes the pieces arrive in order, and are valid... + $key = preg_replace('/\*[0-9]+$/', '', $key); + if (isset($return['other'][$key])) { + $return['other'][$key] .= $val; + if (strtolower($key) != $key) { + $return['other'][strtolower($key)] .= $val; + } + $added = true; + } + // continue and use standard setters.. + } + if (!$added) { + $return['other'][$key] = $val; + $return['other'][strtolower($key)] = $val; + } + $val = false; + $key = ''; + $lq = false; + $i++; + continue; + } + + $val .= $c; + $i++; + continue; + } + + // state - in quote.. + if (!$escaped && $c == $q) { // potential exit state.. + + // end of quoted string.. + $lq = $q; + $q = false; + $i++; + continue; + } + + // normal char inside of quoted string.. + $val.= $c; + $i++; + } + + // do we have anything left.. + if (strlen(trim($key)) || $val !== false) { + + $val = trim($val); + $added = false; + if ($val !== false && preg_match('/\*[0-9]+$/', $key)) { + // no dupes due to our crazy regexp. + $key = preg_replace('/\*[0-9]+$/', '', $key); + if (isset($return['other'][$key])) { + $return['other'][$key] .= $val; + if (strtolower($key) != $key) { + $return['other'][strtolower($key)] .= $val; + } + $added = true; + } + // continue and use standard setters.. + } + if (!$added) { + $return['other'][$key] = $val; + $return['other'][strtolower($key)] = $val; + } + } + // decode values. + foreach($return['other'] as $key =>$val) { + $return['other'][$key] = $this->_decode_headers ? $this->_decodeHeader($val) : $val; + } + //print_r($return); + return $return; + } + + /** + * This function splits the input based + * on the given boundary + * + * @param string Input to parse + * @return array Contains array of resulting mime parts + * @access private + */ + function _boundarySplit($input, $boundary) + { + $parts = array(); + + $bs_possible = substr($boundary, 2, -2); + $bs_check = '\"' . $bs_possible . '\"'; + + if ($boundary == $bs_check) { + $boundary = $bs_possible; + } + $tmp = preg_split("/--".preg_quote($boundary, '/')."((?=\s)|--)/", $input); + + $len = count($tmp) -1; + for ($i = 1; $i < $len; $i++) { + if (strlen(trim($tmp[$i]))) { + $parts[] = $tmp[$i]; + } + } + + // add the last part on if it does not end with the 'closing indicator' + if (!empty($tmp[$len]) && strlen(trim($tmp[$len])) && $tmp[$len][0] != '-') { + $parts[] = $tmp[$len]; + } + return $parts; + } + + /** + * Given a header, this function will decode it + * according to RFC2047. Probably not *exactly* + * conformant, but it does pass all the given + * examples (in RFC2047). + * + * @param string Input header value to decode + * @return string Decoded header value + * @access private + */ + function _decodeHeader($input) + { + // Remove white space between encoded-words + $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); + + // For each encoded-word... + while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { + + $encoded = $matches[1]; + $charset = $matches[2]; + $encoding = $matches[3]; + $text = $matches[4]; + + switch (strtolower($encoding)) { + case 'b': + $text = base64_decode($text); + break; + + case 'q': + $text = str_replace('_', ' ', $text); + preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); + foreach($matches[1] as $value) + $text = str_replace('='.$value, chr(hexdec($value)), $text); + break; + } + + $input = str_replace($encoded, $text, $input); + } + + return $input; + } + + /** + * Given a body string and an encoding type, + * this function will decode and return it. + * + * @param string Input body to decode + * @param string Encoding type to use. + * @return string Decoded body + * @access private + */ + function _decodeBody($input, $encoding = '7bit') + { + switch (strtolower($encoding)) { + case '7bit': + return $input; + break; + + case 'quoted-printable': + return $this->_quotedPrintableDecode($input); + break; + + case 'base64': + return base64_decode($input); + break; + + default: + return $input; + } + } + + /** + * Given a quoted-printable string, this + * function will decode and return it. + * + * @param string Input body to decode + * @return string Decoded body + * @access private + */ + function _quotedPrintableDecode($input) + { + // Remove soft line breaks + $input = preg_replace("/=\r?\n/", '', $input); + + // Replace encoded characters + $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input); + + return $input; + } + + /** + * Checks the input for uuencoded files and returns + * an array of them. Can be called statically, eg: + * + * $files =& Mail_mimeDecode::uudecode($some_text); + * + * It will check for the begin 666 ... end syntax + * however and won't just blindly decode whatever you + * pass it. + * + * @param string Input body to look for attahcments in + * @return array Decoded bodies, filenames and permissions + * @access public + * @author Unknown + */ + function &uudecode($input) + { + // Find all uuencoded sections + preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches); + + for ($j = 0; $j < count($matches[3]); $j++) { + + $str = $matches[3][$j]; + $filename = $matches[2][$j]; + $fileperm = $matches[1][$j]; + + $file = ''; + $str = preg_split("/\r?\n/", trim($str)); + $strlen = count($str); + + for ($i = 0; $i < $strlen; $i++) { + $pos = 1; + $d = 0; + $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077); + + while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077)); + + $pos += 4; + $d += 3; + } + + if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); + + $pos += 3; + $d += 2; + } + + if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) { + $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); + $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); + $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); + + } + } + $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file); + } + + return $files; + } + + /** + * getSendArray() returns the arguments required for Mail::send() + * used to build the arguments for a mail::send() call + * + * Usage: + * $mailtext = Full email (for example generated by a template) + * $decoder = new Mail_mimeDecode($mailtext); + * $parts = $decoder->getSendArray(); + * if (!PEAR::isError($parts) { + * list($recipents,$headers,$body) = $parts; + * $mail = Mail::factory('smtp'); + * $mail->send($recipents,$headers,$body); + * } else { + * echo $parts->message; + * } + * @return mixed array of recipeint, headers,body or Pear_Error + * @access public + * @author Alan Knowles <alan@akbkhome.com> + */ + function getSendArray() + { + // prevent warning if this is not set + $this->_decode_headers = FALSE; + $headerlist =$this->_parseHeaders($this->_header); + $to = ""; + if (!$headerlist) { + return $this->raiseError("Message did not contain headers"); + } + foreach($headerlist as $item) { + $header[$item['name']] = $item['value']; + switch (strtolower($item['name'])) { + case "to": + case "cc": + case "bcc": + $to .= ",".$item['value']; + default: + break; + } + } + if ($to == "") { + return $this->raiseError("Message did not contain any recipents"); + } + $to = substr($to,1); + return array($to,$header,$this->_body); + } + + /** + * Returns a xml copy of the output of + * Mail_mimeDecode::decode. Pass the output in as the + * argument. This function can be called statically. Eg: + * + * $output = $obj->decode(); + * $xml = Mail_mimeDecode::getXML($output); + * + * The DTD used for this should have been in the package. Or + * alternatively you can get it from cvs, or here: + * http://www.phpguru.org/xmail/xmail.dtd. + * + * @param object Input to convert to xml. This should be the + * output of the Mail_mimeDecode::decode function + * @return string XML version of input + * @access public + */ + function getXML($input) + { + $crlf = "\r\n"; + $output = '<?xml version=\'1.0\'?>' . $crlf . + '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf . + '<email>' . $crlf . + Mail_mimeDecode::_getXML($input) . + '</email>'; + + return $output; + } + + /** + * Function that does the actual conversion to xml. Does a single + * mimepart at a time. + * + * @param object Input to convert to xml. This is a mimepart object. + * It may or may not contain subparts. + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML($input, $indent = 1) + { + $htab = "\t"; + $crlf = "\r\n"; + $output = ''; + $headers = @(array)$input->headers; + + foreach ($headers as $hdr_name => $hdr_value) { + + // Multiple headers with this name + if (is_array($headers[$hdr_name])) { + for ($i = 0; $i < count($hdr_value); $i++) { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent); + } + + // Only one header of this sort + } else { + $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent); + } + } + + if (!empty($input->parts)) { + for ($i = 0; $i < count($input->parts); $i++) { + $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf . + Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) . + str_repeat($htab, $indent) . '</mimepart>' . $crlf; + } + } elseif (isset($input->body)) { + $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' . + $input->body . ']]></body>' . $crlf; + } + + return $output; + } + + /** + * Helper function to _getXML(). Returns xml of a header. + * + * @param string Name of header + * @param string Value of header + * @param integer Number of tabs to indent + * @return string XML version of input + * @access private + */ + function _getXML_helper($hdr_name, $hdr_value, $indent) + { + $htab = "\t"; + $crlf = "\r\n"; + $return = ''; + + $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value); + $new_hdr_name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name))); + + // Sort out any parameters + if (!empty($new_hdr_value['other'])) { + foreach ($new_hdr_value['other'] as $paramname => $paramvalue) { + $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf . + str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf . + str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf; + } + + $params = implode('', $params); + } else { + $params = ''; + } + + $return = str_repeat($htab, $indent) . '<header>' . $crlf . + str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf . + str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf . + $params . + str_repeat($htab, $indent) . '</header>' . $crlf; + + return $return; + } + +} // End of class diff --git a/lib/ext/Mail/mimePart.php b/lib/ext/Mail/mimePart.php new file mode 100644 index 0000000..292227f --- /dev/null +++ b/lib/ext/Mail/mimePart.php @@ -0,0 +1,1228 @@ +<?php +/** + * The Mail_mimePart class is used to create MIME E-mail messages + * + * This class enables you to manipulate and build a mime email + * from the ground up. The Mail_Mime class is a userfriendly api + * to this class for people who aren't interested in the internals + * of mime mail. + * This class however allows full control over the email. + * + * Compatible with PHP versions 4 and 5 + * + * LICENSE: This LICENSE is in the BSD license style. + * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> + * Copyright (c) 2003-2006, PEAR <pear-group@php.net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * - Neither the name of the authors, nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @author Aleksander Machniak <alec@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 1.8.5 + * @link http://pear.php.net/package/Mail_mime + */ + + +/** + * The Mail_mimePart class is used to create MIME E-mail messages + * + * This class enables you to manipulate and build a mime email + * from the ground up. The Mail_Mime class is a userfriendly api + * to this class for people who aren't interested in the internals + * of mime mail. + * This class however allows full control over the email. + * + * @category Mail + * @package Mail_Mime + * @author Richard Heyes <richard@phpguru.org> + * @author Cipriano Groenendal <cipri@php.net> + * @author Sean Coates <sean@php.net> + * @author Aleksander Machniak <alec@php.net> + * @copyright 2003-2006 PEAR <pear-group@php.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version Release: 1.8.5 + * @link http://pear.php.net/package/Mail_mime + */ +class Mail_mimePart +{ + /** + * The encoding type of this part + * + * @var string + * @access private + */ + var $_encoding; + + /** + * An array of subparts + * + * @var array + * @access private + */ + var $_subparts; + + /** + * The output of this part after being built + * + * @var string + * @access private + */ + var $_encoded; + + /** + * Headers for this part + * + * @var array + * @access private + */ + var $_headers; + + /** + * The body of this part (not encoded) + * + * @var string + * @access private + */ + var $_body; + + /** + * The location of file with body of this part (not encoded) + * + * @var string + * @access private + */ + var $_body_file; + + /** + * The end-of-line sequence + * + * @var string + * @access private + */ + var $_eol = "\r\n"; + + + /** + * Constructor. + * + * Sets up the object. + * + * @param string $body The body of the mime part if any. + * @param array $params An associative array of optional parameters: + * content_type - The content type for this part eg multipart/mixed + * encoding - The encoding to use, 7bit, 8bit, + * base64, or quoted-printable + * charset - Content character set + * cid - Content ID to apply + * disposition - Content disposition, inline or attachment + * filename - Filename parameter for content disposition + * description - Content description + * name_encoding - Encoding of the attachment name (Content-Type) + * By default filenames are encoded using RFC2231 + * Here you can set RFC2047 encoding (quoted-printable + * or base64) instead + * filename_encoding - Encoding of the attachment filename (Content-Disposition) + * See 'name_encoding' + * headers_charset - Charset of the headers e.g. filename, description. + * If not set, 'charset' will be used + * eol - End of line sequence. Default: "\r\n" + * headers - Hash array with additional part headers. Array keys can be + * in form of <header_name>:<parameter_name> + * body_file - Location of file with part's body (instead of $body) + * + * @access public + */ + function Mail_mimePart($body = '', $params = array()) + { + if (!empty($params['eol'])) { + $this->_eol = $params['eol']; + } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat. + $this->_eol = MAIL_MIMEPART_CRLF; + } + + // Additional part headers + if (!empty($params['headers']) && is_array($params['headers'])) { + $headers = $params['headers']; + } + + foreach ($params as $key => $value) { + switch ($key) { + case 'encoding': + $this->_encoding = $value; + $headers['Content-Transfer-Encoding'] = $value; + break; + + case 'cid': + $headers['Content-ID'] = '<' . $value . '>'; + break; + + case 'location': + $headers['Content-Location'] = $value; + break; + + case 'body_file': + $this->_body_file = $value; + break; + + // for backward compatibility + case 'dfilename': + $params['filename'] = $value; + break; + } + } + + // Default content-type + if (empty($params['content_type'])) { + $params['content_type'] = 'text/plain'; + } + + // Content-Type + $headers['Content-Type'] = $params['content_type']; + if (!empty($params['charset'])) { + $charset = "charset={$params['charset']}"; + // place charset parameter in the same line, if possible + if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) { + $headers['Content-Type'] .= '; '; + } else { + $headers['Content-Type'] .= ';' . $this->_eol . ' '; + } + $headers['Content-Type'] .= $charset; + + // Default headers charset + if (!isset($params['headers_charset'])) { + $params['headers_charset'] = $params['charset']; + } + } + + // header values encoding parameters + $h_charset = !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII'; + $h_language = !empty($params['language']) ? $params['language'] : null; + $h_encoding = !empty($params['name_encoding']) ? $params['name_encoding'] : null; + + + if (!empty($params['filename'])) { + $headers['Content-Type'] .= ';' . $this->_eol; + $headers['Content-Type'] .= $this->_buildHeaderParam( + 'name', $params['filename'], $h_charset, $h_language, $h_encoding + ); + } + + // Content-Disposition + if (!empty($params['disposition'])) { + $headers['Content-Disposition'] = $params['disposition']; + if (!empty($params['filename'])) { + $headers['Content-Disposition'] .= ';' . $this->_eol; + $headers['Content-Disposition'] .= $this->_buildHeaderParam( + 'filename', $params['filename'], $h_charset, $h_language, + !empty($params['filename_encoding']) ? $params['filename_encoding'] : null + ); + } + + // add attachment size + $size = $this->_body_file ? filesize($this->_body_file) : strlen($body); + if ($size) { + $headers['Content-Disposition'] .= ';' . $this->_eol . ' size=' . $size; + } + } + + if (!empty($params['description'])) { + $headers['Content-Description'] = $this->encodeHeader( + 'Content-Description', $params['description'], $h_charset, $h_encoding, + $this->_eol + ); + } + + // Search and add existing headers' parameters + foreach ($headers as $key => $value) { + $items = explode(':', $key); + if (count($items) == 2) { + $header = $items[0]; + $param = $items[1]; + if (isset($headers[$header])) { + $headers[$header] .= ';' . $this->_eol; + } + $headers[$header] .= $this->_buildHeaderParam( + $param, $value, $h_charset, $h_language, $h_encoding + ); + unset($headers[$key]); + } + } + + // Default encoding + if (!isset($this->_encoding)) { + $this->_encoding = '7bit'; + } + + // Assign stuff to member variables + $this->_encoded = array(); + $this->_headers = $headers; + $this->_body = $body; + } + + /** + * Encodes and returns the email. Also stores + * it in the encoded member variable + * + * @param string $boundary Pre-defined boundary string + * + * @return An associative array containing two elements, + * body and headers. The headers element is itself + * an indexed array. On error returns PEAR error object. + * @access public + */ + function encode($boundary=null) + { + $encoded =& $this->_encoded; + + if (count($this->_subparts)) { + $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); + $eol = $this->_eol; + + $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + + $encoded['body'] = ''; + + for ($i = 0; $i < count($this->_subparts); $i++) { + $encoded['body'] .= '--' . $boundary . $eol; + $tmp = $this->_subparts[$i]->encode(); + if (PEAR::isError($tmp)) { + return $tmp; + } + foreach ($tmp['headers'] as $key => $value) { + $encoded['body'] .= $key . ': ' . $value . $eol; + } + $encoded['body'] .= $eol . $tmp['body'] . $eol; + } + + $encoded['body'] .= '--' . $boundary . '--' . $eol; + + } else if ($this->_body) { + $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding); + } else if ($this->_body_file) { + // Temporarily reset magic_quotes_runtime for file reads and writes + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding); + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + if (PEAR::isError($body)) { + return $body; + } + $encoded['body'] = $body; + } else { + $encoded['body'] = ''; + } + + // Add headers to $encoded + $encoded['headers'] =& $this->_headers; + + return $encoded; + } + + /** + * Encodes and saves the email into file. File must exist. + * Data will be appended to the file. + * + * @param string $filename Output file location + * @param string $boundary Pre-defined boundary string + * @param boolean $skip_head True if you don't want to save headers + * + * @return array An associative array containing message headers + * or PEAR error object + * @access public + * @since 1.6.0 + */ + function encodeToFile($filename, $boundary=null, $skip_head=false) + { + if (file_exists($filename) && !is_writable($filename)) { + $err = PEAR::raiseError('File is not writeable: ' . $filename); + return $err; + } + + if (!($fh = fopen($filename, 'ab'))) { + $err = PEAR::raiseError('Unable to open file: ' . $filename); + return $err; + } + + // Temporarily reset magic_quotes_runtime for file reads and writes + if ($magic_quote_setting = get_magic_quotes_runtime()) { + @ini_set('magic_quotes_runtime', 0); + } + + $res = $this->_encodePartToFile($fh, $boundary, $skip_head); + + fclose($fh); + + if ($magic_quote_setting) { + @ini_set('magic_quotes_runtime', $magic_quote_setting); + } + + return PEAR::isError($res) ? $res : $this->_headers; + } + + /** + * Encodes given email part into file + * + * @param string $fh Output file handle + * @param string $boundary Pre-defined boundary string + * @param boolean $skip_head True if you don't want to save headers + * + * @return array True on sucess or PEAR error object + * @access private + */ + function _encodePartToFile($fh, $boundary=null, $skip_head=false) + { + $eol = $this->_eol; + + if (count($this->_subparts)) { + $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); + $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; + } + + if (!$skip_head) { + foreach ($this->_headers as $key => $value) { + fwrite($fh, $key . ': ' . $value . $eol); + } + $f_eol = $eol; + } else { + $f_eol = ''; + } + + if (count($this->_subparts)) { + for ($i = 0; $i < count($this->_subparts); $i++) { + fwrite($fh, $f_eol . '--' . $boundary . $eol); + $res = $this->_subparts[$i]->_encodePartToFile($fh); + if (PEAR::isError($res)) { + return $res; + } + $f_eol = $eol; + } + + fwrite($fh, $eol . '--' . $boundary . '--' . $eol); + + } else if ($this->_body) { + fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding)); + } else if ($this->_body_file) { + fwrite($fh, $f_eol); + $res = $this->_getEncodedDataFromFile( + $this->_body_file, $this->_encoding, $fh + ); + if (PEAR::isError($res)) { + return $res; + } + } + + return true; + } + + /** + * Adds a subpart to current mime part and returns + * a reference to it + * + * @param string $body The body of the subpart, if any. + * @param array $params The parameters for the subpart, same + * as the $params argument for constructor. + * + * @return Mail_mimePart A reference to the part you just added. It is + * crucial if using multipart/* in your subparts that + * you use =& in your script when calling this function, + * otherwise you will not be able to add further subparts. + * @access public + */ + function &addSubpart($body, $params) + { + $this->_subparts[] = new Mail_mimePart($body, $params); + return $this->_subparts[count($this->_subparts) - 1]; + } + + /** + * Returns encoded data based upon encoding passed to it + * + * @param string $data The data to encode. + * @param string $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * + * @return string + * @access private + */ + function _getEncodedData($data, $encoding) + { + switch ($encoding) { + case 'quoted-printable': + return $this->_quotedPrintableEncode($data); + break; + + case 'base64': + return rtrim(chunk_split(base64_encode($data), 76, $this->_eol)); + break; + + case '8bit': + case '7bit': + default: + return $data; + } + } + + /** + * Returns encoded data based upon encoding passed to it + * + * @param string $filename Data file location + * @param string $encoding The encoding type to use, 7bit, base64, + * or quoted-printable. + * @param resource $fh Output file handle. If set, data will be + * stored into it instead of returning it + * + * @return string Encoded data or PEAR error object + * @access private + */ + function _getEncodedDataFromFile($filename, $encoding, $fh=null) + { + if (!is_readable($filename)) { + $err = PEAR::raiseError('Unable to read file: ' . $filename); + return $err; + } + + if (!($fd = fopen($filename, 'rb'))) { + $err = PEAR::raiseError('Could not open file: ' . $filename); + return $err; + } + + $data = ''; + + switch ($encoding) { + case 'quoted-printable': + while (!feof($fd)) { + $buffer = $this->_quotedPrintableEncode(fgets($fd)); + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + break; + + case 'base64': + while (!feof($fd)) { + // Should read in a multiple of 57 bytes so that + // the output is 76 bytes per line. Don't use big chunks + // because base64 encoding is memory expensive + $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB + $buffer = base64_encode($buffer); + $buffer = chunk_split($buffer, 76, $this->_eol); + if (feof($fd)) { + $buffer = rtrim($buffer); + } + + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + break; + + case '8bit': + case '7bit': + default: + while (!feof($fd)) { + $buffer = fread($fd, 1048576); // 1 MB + if ($fh) { + fwrite($fh, $buffer); + } else { + $data .= $buffer; + } + } + } + + fclose($fd); + + if (!$fh) { + return $data; + } + } + + /** + * Encodes data to quoted-printable standard. + * + * @param string $input The data to encode + * @param int $line_max Optional max line length. Should + * not be more than 76 chars + * + * @return string Encoded data + * + * @access private + */ + function _quotedPrintableEncode($input , $line_max = 76) + { + $eol = $this->_eol; + /* + // imap_8bit() is extremely fast, but doesn't handle properly some characters + if (function_exists('imap_8bit') && $line_max == 76) { + $input = preg_replace('/\r?\n/', "\r\n", $input); + $input = imap_8bit($input); + if ($eol != "\r\n") { + $input = str_replace("\r\n", $eol, $input); + } + return $input; + } + */ + $lines = preg_split("/\r?\n/", $input); + $escape = '='; + $output = ''; + + while (list($idx, $line) = each($lines)) { + $newline = ''; + $i = 0; + + while (isset($line[$i])) { + $char = $line[$i]; + $dec = ord($char); + $i++; + + if (($dec == 32) && (!isset($line[$i]))) { + // convert space at eol only + $char = '=20'; + } elseif ($dec == 9 && isset($line[$i])) { + ; // Do nothing if a TAB is not on eol + } elseif (($dec == 61) || ($dec < 32) || ($dec > 126)) { + $char = $escape . sprintf('%02X', $dec); + } elseif (($dec == 46) && (($newline == '') + || ((strlen($newline) + strlen("=2E")) >= $line_max)) + ) { + // Bug #9722: convert full-stop at bol, + // some Windows servers need this, won't break anything (cipri) + // Bug #11731: full-stop at bol also needs to be encoded + // if this line would push us over the line_max limit. + $char = '=2E'; + } + + // Note, when changing this line, also change the ($dec == 46) + // check line, as it mimics this line due to Bug #11731 + // EOL is not counted + if ((strlen($newline) + strlen($char)) >= $line_max) { + // soft line break; " =\r\n" is okay + $output .= $newline . $escape . $eol; + $newline = ''; + } + $newline .= $char; + } // end of for + $output .= $newline . $eol; + unset($lines[$idx]); + } + // Don't want last crlf + $output = substr($output, 0, -1 * strlen($eol)); + return $output; + } + + /** + * Encodes the parameter of a header. + * + * @param string $name The name of the header-parameter + * @param string $value The value of the paramter + * @param string $charset The characterset of $value + * @param string $language The language used in $value + * @param string $encoding Parameter encoding. If not set, parameter value + * is encoded according to RFC2231 + * @param int $maxLength The maximum length of a line. Defauls to 75 + * + * @return string + * + * @access private + */ + function _buildHeaderParam($name, $value, $charset=null, $language=null, + $encoding=null, $maxLength=75 + ) { + // RFC 2045: + // value needs encoding if contains non-ASCII chars or is longer than 78 chars + if (!preg_match('#[^\x20-\x7E]#', $value)) { + $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D' + . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; + if (!preg_match($token_regexp, $value)) { + // token + if (strlen($name) + strlen($value) + 3 <= $maxLength) { + return " {$name}={$value}"; + } + } else { + // quoted-string + $quoted = addcslashes($value, '\\"'); + if (strlen($name) + strlen($quoted) + 5 <= $maxLength) { + return " {$name}=\"{$quoted}\""; + } + } + } + + // RFC2047: use quoted-printable/base64 encoding + if ($encoding == 'quoted-printable' || $encoding == 'base64') { + return $this->_buildRFC2047Param($name, $value, $charset, $encoding); + } + + // RFC2231: + $encValue = preg_replace_callback( + '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/', + array($this, '_encodeReplaceCallback'), $value + ); + $value = "$charset'$language'$encValue"; + + $header = " {$name}*={$value}"; + if (strlen($header) <= $maxLength) { + return $header; + } + + $preLength = strlen(" {$name}*0*="); + $maxLength = max(16, $maxLength - $preLength - 3); + $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|"; + + $headers = array(); + $headCount = 0; + while ($value) { + $matches = array(); + $found = preg_match($maxLengthReg, $value, $matches); + if ($found) { + $headers[] = " {$name}*{$headCount}*={$matches[0]}"; + $value = substr($value, strlen($matches[0])); + } else { + $headers[] = " {$name}*{$headCount}*={$value}"; + $value = ''; + } + $headCount++; + } + + $headers = implode(';' . $this->_eol, $headers); + return $headers; + } + + /** + * Encodes header parameter as per RFC2047 if needed + * + * @param string $name The parameter name + * @param string $value The parameter value + * @param string $charset The parameter charset + * @param string $encoding Encoding type (quoted-printable or base64) + * @param int $maxLength Encoded parameter max length. Default: 76 + * + * @return string Parameter line + * @access private + */ + function _buildRFC2047Param($name, $value, $charset, + $encoding='quoted-printable', $maxLength=76 + ) { + // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in + // parameter of a MIME Content-Type or Content-Disposition field", + // but... it's supported by many clients/servers + $quoted = ''; + + if ($encoding == 'base64') { + $value = base64_encode($value); + $prefix = '=?' . $charset . '?B?'; + $suffix = '?='; + + // 2 x SPACE, 2 x '"', '=', ';' + $add_len = strlen($prefix . $suffix) + strlen($name) + 6; + $len = $add_len + strlen($value); + + while ($len > $maxLength) { + // We can cut base64-encoded string every 4 characters + $real_len = floor(($maxLength - $add_len) / 4) * 4; + $_quote = substr($value, 0, $real_len); + $value = substr($value, $real_len); + + $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' + $len = strlen($value) + $add_len; + } + $quoted .= $prefix . $value . $suffix; + + } else { + // quoted-printable + $value = $this->encodeQP($value); + $prefix = '=?' . $charset . '?Q?'; + $suffix = '?='; + + // 2 x SPACE, 2 x '"', '=', ';' + $add_len = strlen($prefix . $suffix) + strlen($name) + 6; + $len = $add_len + strlen($value); + + while ($len > $maxLength) { + $length = $maxLength - $add_len; + // don't break any encoded letters + if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) { + $_quote = $matches[1]; + } + + $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; + $value = substr($value, strlen($_quote)); + $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' + $len = strlen($value) + $add_len; + } + + $quoted .= $prefix . $value . $suffix; + } + + return " {$name}=\"{$quoted}\""; + } + + /** + * Encodes a header as per RFC2047 + * + * @param string $name The header name + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded header data (without a name) + * @access public + * @since 1.6.1 + */ + function encodeHeader($name, $value, $charset='ISO-8859-1', + $encoding='quoted-printable', $eol="\r\n" + ) { + // Structured headers + $comma_headers = array( + 'from', 'to', 'cc', 'bcc', 'sender', 'reply-to', + 'resent-from', 'resent-to', 'resent-cc', 'resent-bcc', + 'resent-sender', 'resent-reply-to', + 'return-receipt-to', 'disposition-notification-to', + ); + $other_headers = array( + 'references', 'in-reply-to', 'message-id', 'resent-message-id', + ); + + $name = strtolower($name); + + if (in_array($name, $comma_headers)) { + $separator = ','; + } else if (in_array($name, $other_headers)) { + $separator = ' '; + } + + if (!$charset) { + $charset = 'ISO-8859-1'; + } + + // Structured header (make sure addr-spec inside is not encoded) + if (!empty($separator)) { + // Simple e-mail address regexp + $email_regexp = '([^\s<]+|("[^\r\n"]+"))@\S+'; + + $parts = Mail_mimePart::_explodeQuotedString($separator, $value); + $value = ''; + + foreach ($parts as $part) { + $part = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $part); + $part = trim($part); + + if (!$part) { + continue; + } + if ($value) { + $value .= $separator==',' ? $separator.' ' : ' '; + } else { + $value = $name . ': '; + } + + // let's find phrase (name) and/or addr-spec + if (preg_match('/^<' . $email_regexp . '>$/', $part)) { + $value .= $part; + } else if (preg_match('/^' . $email_regexp . '$/', $part)) { + // address without brackets and without name + $value .= $part; + } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) { + // address with name (handle name) + $address = $matches[0]; + $word = str_replace($address, '', $part); + $word = trim($word); + // check if phrase requires quoting + if ($word) { + // non-ASCII: require encoding + if (preg_match('#([\x80-\xFF]){1}#', $word)) { + if ($word[0] == '"' && $word[strlen($word)-1] == '"') { + // de-quote quoted-string, encoding changes + // string to atom + $search = array("\\\"", "\\\\"); + $replace = array("\"", "\\"); + $word = str_replace($search, $replace, $word); + $word = substr($word, 1, -1); + } + // find length of last line + if (($pos = strrpos($value, $eol)) !== false) { + $last_len = strlen($value) - $pos; + } else { + $last_len = strlen($value); + } + $word = Mail_mimePart::encodeHeaderValue( + $word, $charset, $encoding, $last_len, $eol + ); + } else if (($word[0] != '"' || $word[strlen($word)-1] != '"') + && preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $word) + ) { + // ASCII: quote string if needed + $word = '"'.addcslashes($word, '\\"').'"'; + } + } + $value .= $word.' '.$address; + } else { + // addr-spec not found, don't encode (?) + $value .= $part; + } + + // RFC2822 recommends 78 characters limit, use 76 from RFC2047 + $value = wordwrap($value, 76, $eol . ' '); + } + + // remove header name prefix (there could be EOL too) + $value = preg_replace( + '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value + ); + + } else { + // Unstructured header + // non-ASCII: require encoding + if (preg_match('#([\x80-\xFF]){1}#', $value)) { + if ($value[0] == '"' && $value[strlen($value)-1] == '"') { + // de-quote quoted-string, encoding changes + // string to atom + $search = array("\\\"", "\\\\"); + $replace = array("\"", "\\"); + $value = str_replace($search, $replace, $value); + $value = substr($value, 1, -1); + } + $value = Mail_mimePart::encodeHeaderValue( + $value, $charset, $encoding, strlen($name) + 2, $eol + ); + } else if (strlen($name.': '.$value) > 78) { + // ASCII: check if header line isn't too long and use folding + $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value); + $tmp = wordwrap($name.': '.$value, 78, $eol . ' '); + $value = preg_replace('/^'.$name.':\s*/', '', $tmp); + // hard limit 998 (RFC2822) + $value = wordwrap($value, 998, $eol . ' ', true); + } + } + + return $value; + } + + /** + * Explode quoted string + * + * @param string $delimiter Delimiter expression string for preg_match() + * @param string $string Input string + * + * @return array String tokens array + * @access private + */ + function _explodeQuotedString($delimiter, $string) + { + $result = array(); + $strlen = strlen($string); + + for ($q=$p=$i=0; $i < $strlen; $i++) { + if ($string[$i] == "\"" + && (empty($string[$i-1]) || $string[$i-1] != "\\") + ) { + $q = $q ? false : true; + } else if (!$q && preg_match("/$delimiter/", $string[$i])) { + $result[] = substr($string, $p, $i - $p); + $p = $i + 1; + } + } + + $result[] = substr($string, $p); + return $result; + } + + /** + * Encodes a header value as per RFC2047 + * + * @param string $value The header data to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param int $prefix_len Prefix length. Default: 0 + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded header data + * @access public + * @since 1.6.1 + */ + function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n") + { + // #17311: Use multibyte aware method (requires mbstring extension) + if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) { + return $result; + } + + // Generate the header using the specified params and dynamicly + // determine the maximum length of such strings. + // 75 is the value specified in the RFC. + $encoding = $encoding == 'base64' ? 'B' : 'Q'; + $prefix = '=?' . $charset . '?' . $encoding .'?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix); + $maxLength1stLine = $maxLength - $prefix_len; + + if ($encoding == 'B') { + // Base64 encode the entire string + $value = base64_encode($value); + + // We can cut base64 every 4 characters, so the real max + // we can get must be rounded down. + $maxLength = $maxLength - ($maxLength % 4); + $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4); + + $cutpoint = $maxLength1stLine; + $output = ''; + + while ($value) { + // Split translated string at every $maxLength + $part = substr($value, 0, $cutpoint); + $value = substr($value, $cutpoint); + $cutpoint = $maxLength; + // RFC 2047 specifies that any split header should + // be seperated by a CRLF SPACE. + if ($output) { + $output .= $eol . ' '; + } + $output .= $prefix . $part . $suffix; + } + $value = $output; + } else { + // quoted-printable encoding has been selected + $value = Mail_mimePart::encodeQP($value); + + // This regexp will break QP-encoded text at every $maxLength + // but will not break any encoded letters. + $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|"; + $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|"; + + if (strlen($value) > $maxLength1stLine) { + // Begin with the regexp for the first line. + $reg = $reg1st; + $output = ''; + while ($value) { + // Split translated string at every $maxLength + // But make sure not to break any translated chars. + $found = preg_match($reg, $value, $matches); + + // After this first line, we need to use a different + // regexp for the first line. + $reg = $reg2nd; + + // Save the found part and encapsulate it in the + // prefix & suffix. Then remove the part from the + // $value_out variable. + if ($found) { + $part = $matches[0]; + $len = strlen($matches[0]); + $value = substr($value, $len); + } else { + $part = $value; + $value = ''; + } + + // RFC 2047 specifies that any split header should + // be seperated by a CRLF SPACE + if ($output) { + $output .= $eol . ' '; + } + $output .= $prefix . $part . $suffix; + } + $value = $output; + } else { + $value = $prefix . $value . $suffix; + } + } + + return $value; + } + + /** + * Encodes the given string using quoted-printable + * + * @param string $str String to encode + * + * @return string Encoded string + * @access public + * @since 1.6.0 + */ + function encodeQP($str) + { + // Bug #17226 RFC 2047 restricts some characters + // if the word is inside a phrase, permitted chars are only: + // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_" + + // "=", "_", "?" must be encoded + $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; + $str = preg_replace_callback( + $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str + ); + + return str_replace(' ', '_', $str); + } + + /** + * Encodes the given string using base64 or quoted-printable. + * This method makes sure that encoded-word represents an integral + * number of characters as per RFC2047. + * + * @param string $str String to encode + * @param string $charset Character set name + * @param string $encoding Encoding name (base64 or quoted-printable) + * @param int $prefix_len Prefix length. Default: 0 + * @param string $eol End-of-line sequence. Default: "\r\n" + * + * @return string Encoded string + * @access public + * @since 1.8.0 + */ + function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") + { + if (!function_exists('mb_substr') || !function_exists('mb_strlen')) { + return; + } + + $encoding = $encoding == 'base64' ? 'B' : 'Q'; + // 75 is the value specified in the RFC + $prefix = '=?' . $charset . '?'.$encoding.'?'; + $suffix = '?='; + $maxLength = 75 - strlen($prefix . $suffix); + + // A multi-octet character may not be split across adjacent encoded-words + // So, we'll loop over each character + // mb_stlen() with wrong charset will generate a warning here and return null + $length = mb_strlen($str, $charset); + $result = ''; + $line_length = $prefix_len; + + if ($encoding == 'B') { + // base64 + $start = 0; + $prev = ''; + + for ($i=1; $i<=$length; $i++) { + // See #17311 + $chunk = mb_substr($str, $start, $i-$start, $charset); + $chunk = base64_encode($chunk); + $chunk_len = strlen($chunk); + + if ($line_length + $chunk_len == $maxLength || $i == $length) { + if ($result) { + $result .= "\n"; + } + $result .= $chunk; + $line_length = 0; + $start = $i; + } else if ($line_length + $chunk_len > $maxLength) { + if ($result) { + $result .= "\n"; + } + if ($prev) { + $result .= $prev; + } + $line_length = 0; + $start = $i - 1; + } else { + $prev = $chunk; + } + } + } else { + // quoted-printable + // see encodeQP() + $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; + + for ($i=0; $i<=$length; $i++) { + $char = mb_substr($str, $i, 1, $charset); + // RFC recommends underline (instead of =20) in place of the space + // that's one of the reasons why we're not using iconv_mime_encode() + if ($char == ' ') { + $char = '_'; + $char_len = 1; + } else { + $char = preg_replace_callback( + $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char + ); + $char_len = strlen($char); + } + + if ($line_length + $char_len > $maxLength) { + if ($result) { + $result .= "\n"; + } + $line_length = 0; + } + + $result .= $char; + $line_length += $char_len; + } + } + + if ($result) { + $result = $prefix + .str_replace("\n", $suffix.$eol.' '.$prefix, $result).$suffix; + } + + return $result; + } + + /** + * Callback function to replace extended characters (\x80-xFF) with their + * ASCII values (RFC2047: quoted-printable) + * + * @param array $matches Preg_replace's matches array + * + * @return string Encoded character string + * @access private + */ + function _qpReplaceCallback($matches) + { + return sprintf('=%02X', ord($matches[1])); + } + + /** + * Callback function to replace extended characters (\x80-xFF) with their + * ASCII values (RFC2231) + * + * @param array $matches Preg_replace's matches array + * + * @return string Encoded character string + * @access private + */ + function _encodeReplaceCallback($matches) + { + return sprintf('%%%02X', ord($matches[1])); + } + +} // End of class diff --git a/lib/ext/Net/IDNA2.php b/lib/ext/Net/IDNA2.php new file mode 100644 index 0000000..8c366fb --- /dev/null +++ b/lib/ext/Net/IDNA2.php @@ -0,0 +1,3402 @@ +<?php + +// {{{ license + +/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */ +// +// +----------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or modify | +// | it under the terms of the GNU Lesser General Public License as | +// | published by the Free Software Foundation; either version 2.1 of the | +// | License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, but | +// | WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | +// | USA. | +// +----------------------------------------------------------------------+ +// + +// }}} +require_once 'Net/IDNA2/Exception.php'; +require_once 'Net/IDNA2/Exception/Nameprep.php'; + +/** + * Encode/decode Internationalized Domain Names. + * + * The class allows to convert internationalized domain names + * (see RFC 3490 for details) as they can be used with various registries worldwide + * to be translated between their original (localized) form and their encoded form + * as it will be used in the DNS (Domain Name System). + * + * The class provides two public methods, encode() and decode(), which do exactly + * what you would expect them to do. You are allowed to use complete domain names, + * simple strings and complete email addresses as well. That means, that you might + * use any of the following notations: + * + * - www.n�rgler.com + * - xn--nrgler-wxa + * - xn--brse-5qa.xn--knrz-1ra.info + * + * Unicode input might be given as either UTF-8 string, UCS-4 string or UCS-4 + * array. Unicode output is available in the same formats. + * You can select your preferred format via {@link set_paramter()}. + * + * ACE input and output is always expected to be ASCII. + * + * @package Net + * @author Markus Nix <mnix@docuverse.de> + * @author Matthias Sommerfeld <mso@phlylabs.de> + * @author Stefan Neufeind <pear.neufeind@speedpartner.de> + * @version $Id: IDNA2.php 305344 2010-11-14 23:52:42Z neufeind $ + */ +class Net_IDNA2 +{ + // {{{ npdata + /** + * These Unicode codepoints are + * mapped to nothing, See RFC3454 for details + * + * @static + * @var array + * @access private + */ + private static $_np_map_nothing = array( + 0xAD, + 0x34F, + 0x1806, + 0x180B, + 0x180C, + 0x180D, + 0x200B, + 0x200C, + 0x200D, + 0x2060, + 0xFE00, + 0xFE01, + 0xFE02, + 0xFE03, + 0xFE04, + 0xFE05, + 0xFE06, + 0xFE07, + 0xFE08, + 0xFE09, + 0xFE0A, + 0xFE0B, + 0xFE0C, + 0xFE0D, + 0xFE0E, + 0xFE0F, + 0xFEFF + ); + + /** + * Prohibited codepints + * + * @static + * @var array + * @access private + */ + private static $_general_prohibited = array( + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 0xA, + 0xB, + 0xC, + 0xD, + 0xE, + 0xF, + 0x10, + 0x11, + 0x12, + 0x13, + 0x14, + 0x15, + 0x16, + 0x17, + 0x18, + 0x19, + 0x1A, + 0x1B, + 0x1C, + 0x1D, + 0x1E, + 0x1F, + 0x20, + 0x21, + 0x22, + 0x23, + 0x24, + 0x25, + 0x26, + 0x27, + 0x28, + 0x29, + 0x2A, + 0x2B, + 0x2C, + 0x2F, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + 0x40, + 0x5B, + 0x5C, + 0x5D, + 0x5E, + 0x5F, + 0x60, + 0x7B, + 0x7C, + 0x7D, + 0x7E, + 0x7F, + 0x3002 + ); + + /** + * Codepints prohibited by Nameprep + * @static + * @var array + * @access private + */ + private static $_np_prohibit = array( + 0xA0, + 0x1680, + 0x2000, + 0x2001, + 0x2002, + 0x2003, + 0x2004, + 0x2005, + 0x2006, + 0x2007, + 0x2008, + 0x2009, + 0x200A, + 0x200B, + 0x202F, + 0x205F, + 0x3000, + 0x6DD, + 0x70F, + 0x180E, + 0x200C, + 0x200D, + 0x2028, + 0x2029, + 0xFEFF, + 0xFFF9, + 0xFFFA, + 0xFFFB, + 0xFFFC, + 0xFFFE, + 0xFFFF, + 0x1FFFE, + 0x1FFFF, + 0x2FFFE, + 0x2FFFF, + 0x3FFFE, + 0x3FFFF, + 0x4FFFE, + 0x4FFFF, + 0x5FFFE, + 0x5FFFF, + 0x6FFFE, + 0x6FFFF, + 0x7FFFE, + 0x7FFFF, + 0x8FFFE, + 0x8FFFF, + 0x9FFFE, + 0x9FFFF, + 0xAFFFE, + 0xAFFFF, + 0xBFFFE, + 0xBFFFF, + 0xCFFFE, + 0xCFFFF, + 0xDFFFE, + 0xDFFFF, + 0xEFFFE, + 0xEFFFF, + 0xFFFFE, + 0xFFFFF, + 0x10FFFE, + 0x10FFFF, + 0xFFF9, + 0xFFFA, + 0xFFFB, + 0xFFFC, + 0xFFFD, + 0x340, + 0x341, + 0x200E, + 0x200F, + 0x202A, + 0x202B, + 0x202C, + 0x202D, + 0x202E, + 0x206A, + 0x206B, + 0x206C, + 0x206D, + 0x206E, + 0x206F, + 0xE0001 + ); + + /** + * Codepoint ranges prohibited by nameprep + * + * @static + * @var array + * @access private + */ + private static $_np_prohibit_ranges = array( + array(0x80, 0x9F ), + array(0x2060, 0x206F ), + array(0x1D173, 0x1D17A ), + array(0xE000, 0xF8FF ), + array(0xF0000, 0xFFFFD ), + array(0x100000, 0x10FFFD), + array(0xFDD0, 0xFDEF ), + array(0xD800, 0xDFFF ), + array(0x2FF0, 0x2FFB ), + array(0xE0020, 0xE007F ) + ); + + /** + * Replacement mappings (casemapping, replacement sequences, ...) + * + * @static + * @var array + * @access private + */ + private static $_np_replacemaps = array( + 0x41 => array(0x61), + 0x42 => array(0x62), + 0x43 => array(0x63), + 0x44 => array(0x64), + 0x45 => array(0x65), + 0x46 => array(0x66), + 0x47 => array(0x67), + 0x48 => array(0x68), + 0x49 => array(0x69), + 0x4A => array(0x6A), + 0x4B => array(0x6B), + 0x4C => array(0x6C), + 0x4D => array(0x6D), + 0x4E => array(0x6E), + 0x4F => array(0x6F), + 0x50 => array(0x70), + 0x51 => array(0x71), + 0x52 => array(0x72), + 0x53 => array(0x73), + 0x54 => array(0x74), + 0x55 => array(0x75), + 0x56 => array(0x76), + 0x57 => array(0x77), + 0x58 => array(0x78), + 0x59 => array(0x79), + 0x5A => array(0x7A), + 0xB5 => array(0x3BC), + 0xC0 => array(0xE0), + 0xC1 => array(0xE1), + 0xC2 => array(0xE2), + 0xC3 => array(0xE3), + 0xC4 => array(0xE4), + 0xC5 => array(0xE5), + 0xC6 => array(0xE6), + 0xC7 => array(0xE7), + 0xC8 => array(0xE8), + 0xC9 => array(0xE9), + 0xCA => array(0xEA), + 0xCB => array(0xEB), + 0xCC => array(0xEC), + 0xCD => array(0xED), + 0xCE => array(0xEE), + 0xCF => array(0xEF), + 0xD0 => array(0xF0), + 0xD1 => array(0xF1), + 0xD2 => array(0xF2), + 0xD3 => array(0xF3), + 0xD4 => array(0xF4), + 0xD5 => array(0xF5), + 0xD6 => array(0xF6), + 0xD8 => array(0xF8), + 0xD9 => array(0xF9), + 0xDA => array(0xFA), + 0xDB => array(0xFB), + 0xDC => array(0xFC), + 0xDD => array(0xFD), + 0xDE => array(0xFE), + 0xDF => array(0x73, 0x73), + 0x100 => array(0x101), + 0x102 => array(0x103), + 0x104 => array(0x105), + 0x106 => array(0x107), + 0x108 => array(0x109), + 0x10A => array(0x10B), + 0x10C => array(0x10D), + 0x10E => array(0x10F), + 0x110 => array(0x111), + 0x112 => array(0x113), + 0x114 => array(0x115), + 0x116 => array(0x117), + 0x118 => array(0x119), + 0x11A => array(0x11B), + 0x11C => array(0x11D), + 0x11E => array(0x11F), + 0x120 => array(0x121), + 0x122 => array(0x123), + 0x124 => array(0x125), + 0x126 => array(0x127), + 0x128 => array(0x129), + 0x12A => array(0x12B), + 0x12C => array(0x12D), + 0x12E => array(0x12F), + 0x130 => array(0x69, 0x307), + 0x132 => array(0x133), + 0x134 => array(0x135), + 0x136 => array(0x137), + 0x139 => array(0x13A), + 0x13B => array(0x13C), + 0x13D => array(0x13E), + 0x13F => array(0x140), + 0x141 => array(0x142), + 0x143 => array(0x144), + 0x145 => array(0x146), + 0x147 => array(0x148), + 0x149 => array(0x2BC, 0x6E), + 0x14A => array(0x14B), + 0x14C => array(0x14D), + 0x14E => array(0x14F), + 0x150 => array(0x151), + 0x152 => array(0x153), + 0x154 => array(0x155), + 0x156 => array(0x157), + 0x158 => array(0x159), + 0x15A => array(0x15B), + 0x15C => array(0x15D), + 0x15E => array(0x15F), + 0x160 => array(0x161), + 0x162 => array(0x163), + 0x164 => array(0x165), + 0x166 => array(0x167), + 0x168 => array(0x169), + 0x16A => array(0x16B), + 0x16C => array(0x16D), + 0x16E => array(0x16F), + 0x170 => array(0x171), + 0x172 => array(0x173), + 0x174 => array(0x175), + 0x176 => array(0x177), + 0x178 => array(0xFF), + 0x179 => array(0x17A), + 0x17B => array(0x17C), + 0x17D => array(0x17E), + 0x17F => array(0x73), + 0x181 => array(0x253), + 0x182 => array(0x183), + 0x184 => array(0x185), + 0x186 => array(0x254), + 0x187 => array(0x188), + 0x189 => array(0x256), + 0x18A => array(0x257), + 0x18B => array(0x18C), + 0x18E => array(0x1DD), + 0x18F => array(0x259), + 0x190 => array(0x25B), + 0x191 => array(0x192), + 0x193 => array(0x260), + 0x194 => array(0x263), + 0x196 => array(0x269), + 0x197 => array(0x268), + 0x198 => array(0x199), + 0x19C => array(0x26F), + 0x19D => array(0x272), + 0x19F => array(0x275), + 0x1A0 => array(0x1A1), + 0x1A2 => array(0x1A3), + 0x1A4 => array(0x1A5), + 0x1A6 => array(0x280), + 0x1A7 => array(0x1A8), + 0x1A9 => array(0x283), + 0x1AC => array(0x1AD), + 0x1AE => array(0x288), + 0x1AF => array(0x1B0), + 0x1B1 => array(0x28A), + 0x1B2 => array(0x28B), + 0x1B3 => array(0x1B4), + 0x1B5 => array(0x1B6), + 0x1B7 => array(0x292), + 0x1B8 => array(0x1B9), + 0x1BC => array(0x1BD), + 0x1C4 => array(0x1C6), + 0x1C5 => array(0x1C6), + 0x1C7 => array(0x1C9), + 0x1C8 => array(0x1C9), + 0x1CA => array(0x1CC), + 0x1CB => array(0x1CC), + 0x1CD => array(0x1CE), + 0x1CF => array(0x1D0), + 0x1D1 => array(0x1D2), + 0x1D3 => array(0x1D4), + 0x1D5 => array(0x1D6), + 0x1D7 => array(0x1D8), + 0x1D9 => array(0x1DA), + 0x1DB => array(0x1DC), + 0x1DE => array(0x1DF), + 0x1E0 => array(0x1E1), + 0x1E2 => array(0x1E3), + 0x1E4 => array(0x1E5), + 0x1E6 => array(0x1E7), + 0x1E8 => array(0x1E9), + 0x1EA => array(0x1EB), + 0x1EC => array(0x1ED), + 0x1EE => array(0x1EF), + 0x1F0 => array(0x6A, 0x30C), + 0x1F1 => array(0x1F3), + 0x1F2 => array(0x1F3), + 0x1F4 => array(0x1F5), + 0x1F6 => array(0x195), + 0x1F7 => array(0x1BF), + 0x1F8 => array(0x1F9), + 0x1FA => array(0x1FB), + 0x1FC => array(0x1FD), + 0x1FE => array(0x1FF), + 0x200 => array(0x201), + 0x202 => array(0x203), + 0x204 => array(0x205), + 0x206 => array(0x207), + 0x208 => array(0x209), + 0x20A => array(0x20B), + 0x20C => array(0x20D), + 0x20E => array(0x20F), + 0x210 => array(0x211), + 0x212 => array(0x213), + 0x214 => array(0x215), + 0x216 => array(0x217), + 0x218 => array(0x219), + 0x21A => array(0x21B), + 0x21C => array(0x21D), + 0x21E => array(0x21F), + 0x220 => array(0x19E), + 0x222 => array(0x223), + 0x224 => array(0x225), + 0x226 => array(0x227), + 0x228 => array(0x229), + 0x22A => array(0x22B), + 0x22C => array(0x22D), + 0x22E => array(0x22F), + 0x230 => array(0x231), + 0x232 => array(0x233), + 0x345 => array(0x3B9), + 0x37A => array(0x20, 0x3B9), + 0x386 => array(0x3AC), + 0x388 => array(0x3AD), + 0x389 => array(0x3AE), + 0x38A => array(0x3AF), + 0x38C => array(0x3CC), + 0x38E => array(0x3CD), + 0x38F => array(0x3CE), + 0x390 => array(0x3B9, 0x308, 0x301), + 0x391 => array(0x3B1), + 0x392 => array(0x3B2), + 0x393 => array(0x3B3), + 0x394 => array(0x3B4), + 0x395 => array(0x3B5), + 0x396 => array(0x3B6), + 0x397 => array(0x3B7), + 0x398 => array(0x3B8), + 0x399 => array(0x3B9), + 0x39A => array(0x3BA), + 0x39B => array(0x3BB), + 0x39C => array(0x3BC), + 0x39D => array(0x3BD), + 0x39E => array(0x3BE), + 0x39F => array(0x3BF), + 0x3A0 => array(0x3C0), + 0x3A1 => array(0x3C1), + 0x3A3 => array(0x3C3), + 0x3A4 => array(0x3C4), + 0x3A5 => array(0x3C5), + 0x3A6 => array(0x3C6), + 0x3A7 => array(0x3C7), + 0x3A8 => array(0x3C8), + 0x3A9 => array(0x3C9), + 0x3AA => array(0x3CA), + 0x3AB => array(0x3CB), + 0x3B0 => array(0x3C5, 0x308, 0x301), + 0x3C2 => array(0x3C3), + 0x3D0 => array(0x3B2), + 0x3D1 => array(0x3B8), + 0x3D2 => array(0x3C5), + 0x3D3 => array(0x3CD), + 0x3D4 => array(0x3CB), + 0x3D5 => array(0x3C6), + 0x3D6 => array(0x3C0), + 0x3D8 => array(0x3D9), + 0x3DA => array(0x3DB), + 0x3DC => array(0x3DD), + 0x3DE => array(0x3DF), + 0x3E0 => array(0x3E1), + 0x3E2 => array(0x3E3), + 0x3E4 => array(0x3E5), + 0x3E6 => array(0x3E7), + 0x3E8 => array(0x3E9), + 0x3EA => array(0x3EB), + 0x3EC => array(0x3ED), + 0x3EE => array(0x3EF), + 0x3F0 => array(0x3BA), + 0x3F1 => array(0x3C1), + 0x3F2 => array(0x3C3), + 0x3F4 => array(0x3B8), + 0x3F5 => array(0x3B5), + 0x400 => array(0x450), + 0x401 => array(0x451), + 0x402 => array(0x452), + 0x403 => array(0x453), + 0x404 => array(0x454), + 0x405 => array(0x455), + 0x406 => array(0x456), + 0x407 => array(0x457), + 0x408 => array(0x458), + 0x409 => array(0x459), + 0x40A => array(0x45A), + 0x40B => array(0x45B), + 0x40C => array(0x45C), + 0x40D => array(0x45D), + 0x40E => array(0x45E), + 0x40F => array(0x45F), + 0x410 => array(0x430), + 0x411 => array(0x431), + 0x412 => array(0x432), + 0x413 => array(0x433), + 0x414 => array(0x434), + 0x415 => array(0x435), + 0x416 => array(0x436), + 0x417 => array(0x437), + 0x418 => array(0x438), + 0x419 => array(0x439), + 0x41A => array(0x43A), + 0x41B => array(0x43B), + 0x41C => array(0x43C), + 0x41D => array(0x43D), + 0x41E => array(0x43E), + 0x41F => array(0x43F), + 0x420 => array(0x440), + 0x421 => array(0x441), + 0x422 => array(0x442), + 0x423 => array(0x443), + 0x424 => array(0x444), + 0x425 => array(0x445), + 0x426 => array(0x446), + 0x427 => array(0x447), + 0x428 => array(0x448), + 0x429 => array(0x449), + 0x42A => array(0x44A), + 0x42B => array(0x44B), + 0x42C => array(0x44C), + 0x42D => array(0x44D), + 0x42E => array(0x44E), + 0x42F => array(0x44F), + 0x460 => array(0x461), + 0x462 => array(0x463), + 0x464 => array(0x465), + 0x466 => array(0x467), + 0x468 => array(0x469), + 0x46A => array(0x46B), + 0x46C => array(0x46D), + 0x46E => array(0x46F), + 0x470 => array(0x471), + 0x472 => array(0x473), + 0x474 => array(0x475), + 0x476 => array(0x477), + 0x478 => array(0x479), + 0x47A => array(0x47B), + 0x47C => array(0x47D), + 0x47E => array(0x47F), + 0x480 => array(0x481), + 0x48A => array(0x48B), + 0x48C => array(0x48D), + 0x48E => array(0x48F), + 0x490 => array(0x491), + 0x492 => array(0x493), + 0x494 => array(0x495), + 0x496 => array(0x497), + 0x498 => array(0x499), + 0x49A => array(0x49B), + 0x49C => array(0x49D), + 0x49E => array(0x49F), + 0x4A0 => array(0x4A1), + 0x4A2 => array(0x4A3), + 0x4A4 => array(0x4A5), + 0x4A6 => array(0x4A7), + 0x4A8 => array(0x4A9), + 0x4AA => array(0x4AB), + 0x4AC => array(0x4AD), + 0x4AE => array(0x4AF), + 0x4B0 => array(0x4B1), + 0x4B2 => array(0x4B3), + 0x4B4 => array(0x4B5), + 0x4B6 => array(0x4B7), + 0x4B8 => array(0x4B9), + 0x4BA => array(0x4BB), + 0x4BC => array(0x4BD), + 0x4BE => array(0x4BF), + 0x4C1 => array(0x4C2), + 0x4C3 => array(0x4C4), + 0x4C5 => array(0x4C6), + 0x4C7 => array(0x4C8), + 0x4C9 => array(0x4CA), + 0x4CB => array(0x4CC), + 0x4CD => array(0x4CE), + 0x4D0 => array(0x4D1), + 0x4D2 => array(0x4D3), + 0x4D4 => array(0x4D5), + 0x4D6 => array(0x4D7), + 0x4D8 => array(0x4D9), + 0x4DA => array(0x4DB), + 0x4DC => array(0x4DD), + 0x4DE => array(0x4DF), + 0x4E0 => array(0x4E1), + 0x4E2 => array(0x4E3), + 0x4E4 => array(0x4E5), + 0x4E6 => array(0x4E7), + 0x4E8 => array(0x4E9), + 0x4EA => array(0x4EB), + 0x4EC => array(0x4ED), + 0x4EE => array(0x4EF), + 0x4F0 => array(0x4F1), + 0x4F2 => array(0x4F3), + 0x4F4 => array(0x4F5), + 0x4F8 => array(0x4F9), + 0x500 => array(0x501), + 0x502 => array(0x503), + 0x504 => array(0x505), + 0x506 => array(0x507), + 0x508 => array(0x509), + 0x50A => array(0x50B), + 0x50C => array(0x50D), + 0x50E => array(0x50F), + 0x531 => array(0x561), + 0x532 => array(0x562), + 0x533 => array(0x563), + 0x534 => array(0x564), + 0x535 => array(0x565), + 0x536 => array(0x566), + 0x537 => array(0x567), + 0x538 => array(0x568), + 0x539 => array(0x569), + 0x53A => array(0x56A), + 0x53B => array(0x56B), + 0x53C => array(0x56C), + 0x53D => array(0x56D), + 0x53E => array(0x56E), + 0x53F => array(0x56F), + 0x540 => array(0x570), + 0x541 => array(0x571), + 0x542 => array(0x572), + 0x543 => array(0x573), + 0x544 => array(0x574), + 0x545 => array(0x575), + 0x546 => array(0x576), + 0x547 => array(0x577), + 0x548 => array(0x578), + 0x549 => array(0x579), + 0x54A => array(0x57A), + 0x54B => array(0x57B), + 0x54C => array(0x57C), + 0x54D => array(0x57D), + 0x54E => array(0x57E), + 0x54F => array(0x57F), + 0x550 => array(0x580), + 0x551 => array(0x581), + 0x552 => array(0x582), + 0x553 => array(0x583), + 0x554 => array(0x584), + 0x555 => array(0x585), + 0x556 => array(0x586), + 0x587 => array(0x565, 0x582), + 0x1E00 => array(0x1E01), + 0x1E02 => array(0x1E03), + 0x1E04 => array(0x1E05), + 0x1E06 => array(0x1E07), + 0x1E08 => array(0x1E09), + 0x1E0A => array(0x1E0B), + 0x1E0C => array(0x1E0D), + 0x1E0E => array(0x1E0F), + 0x1E10 => array(0x1E11), + 0x1E12 => array(0x1E13), + 0x1E14 => array(0x1E15), + 0x1E16 => array(0x1E17), + 0x1E18 => array(0x1E19), + 0x1E1A => array(0x1E1B), + 0x1E1C => array(0x1E1D), + 0x1E1E => array(0x1E1F), + 0x1E20 => array(0x1E21), + 0x1E22 => array(0x1E23), + 0x1E24 => array(0x1E25), + 0x1E26 => array(0x1E27), + 0x1E28 => array(0x1E29), + 0x1E2A => array(0x1E2B), + 0x1E2C => array(0x1E2D), + 0x1E2E => array(0x1E2F), + 0x1E30 => array(0x1E31), + 0x1E32 => array(0x1E33), + 0x1E34 => array(0x1E35), + 0x1E36 => array(0x1E37), + 0x1E38 => array(0x1E39), + 0x1E3A => array(0x1E3B), + 0x1E3C => array(0x1E3D), + 0x1E3E => array(0x1E3F), + 0x1E40 => array(0x1E41), + 0x1E42 => array(0x1E43), + 0x1E44 => array(0x1E45), + 0x1E46 => array(0x1E47), + 0x1E48 => array(0x1E49), + 0x1E4A => array(0x1E4B), + 0x1E4C => array(0x1E4D), + 0x1E4E => array(0x1E4F), + 0x1E50 => array(0x1E51), + 0x1E52 => array(0x1E53), + 0x1E54 => array(0x1E55), + 0x1E56 => array(0x1E57), + 0x1E58 => array(0x1E59), + 0x1E5A => array(0x1E5B), + 0x1E5C => array(0x1E5D), + 0x1E5E => array(0x1E5F), + 0x1E60 => array(0x1E61), + 0x1E62 => array(0x1E63), + 0x1E64 => array(0x1E65), + 0x1E66 => array(0x1E67), + 0x1E68 => array(0x1E69), + 0x1E6A => array(0x1E6B), + 0x1E6C => array(0x1E6D), + 0x1E6E => array(0x1E6F), + 0x1E70 => array(0x1E71), + 0x1E72 => array(0x1E73), + 0x1E74 => array(0x1E75), + 0x1E76 => array(0x1E77), + 0x1E78 => array(0x1E79), + 0x1E7A => array(0x1E7B), + 0x1E7C => array(0x1E7D), + 0x1E7E => array(0x1E7F), + 0x1E80 => array(0x1E81), + 0x1E82 => array(0x1E83), + 0x1E84 => array(0x1E85), + 0x1E86 => array(0x1E87), + 0x1E88 => array(0x1E89), + 0x1E8A => array(0x1E8B), + 0x1E8C => array(0x1E8D), + 0x1E8E => array(0x1E8F), + 0x1E90 => array(0x1E91), + 0x1E92 => array(0x1E93), + 0x1E94 => array(0x1E95), + 0x1E96 => array(0x68, 0x331), + 0x1E97 => array(0x74, 0x308), + 0x1E98 => array(0x77, 0x30A), + 0x1E99 => array(0x79, 0x30A), + 0x1E9A => array(0x61, 0x2BE), + 0x1E9B => array(0x1E61), + 0x1EA0 => array(0x1EA1), + 0x1EA2 => array(0x1EA3), + 0x1EA4 => array(0x1EA5), + 0x1EA6 => array(0x1EA7), + 0x1EA8 => array(0x1EA9), + 0x1EAA => array(0x1EAB), + 0x1EAC => array(0x1EAD), + 0x1EAE => array(0x1EAF), + 0x1EB0 => array(0x1EB1), + 0x1EB2 => array(0x1EB3), + 0x1EB4 => array(0x1EB5), + 0x1EB6 => array(0x1EB7), + 0x1EB8 => array(0x1EB9), + 0x1EBA => array(0x1EBB), + 0x1EBC => array(0x1EBD), + 0x1EBE => array(0x1EBF), + 0x1EC0 => array(0x1EC1), + 0x1EC2 => array(0x1EC3), + 0x1EC4 => array(0x1EC5), + 0x1EC6 => array(0x1EC7), + 0x1EC8 => array(0x1EC9), + 0x1ECA => array(0x1ECB), + 0x1ECC => array(0x1ECD), + 0x1ECE => array(0x1ECF), + 0x1ED0 => array(0x1ED1), + 0x1ED2 => array(0x1ED3), + 0x1ED4 => array(0x1ED5), + 0x1ED6 => array(0x1ED7), + 0x1ED8 => array(0x1ED9), + 0x1EDA => array(0x1EDB), + 0x1EDC => array(0x1EDD), + 0x1EDE => array(0x1EDF), + 0x1EE0 => array(0x1EE1), + 0x1EE2 => array(0x1EE3), + 0x1EE4 => array(0x1EE5), + 0x1EE6 => array(0x1EE7), + 0x1EE8 => array(0x1EE9), + 0x1EEA => array(0x1EEB), + 0x1EEC => array(0x1EED), + 0x1EEE => array(0x1EEF), + 0x1EF0 => array(0x1EF1), + 0x1EF2 => array(0x1EF3), + 0x1EF4 => array(0x1EF5), + 0x1EF6 => array(0x1EF7), + 0x1EF8 => array(0x1EF9), + 0x1F08 => array(0x1F00), + 0x1F09 => array(0x1F01), + 0x1F0A => array(0x1F02), + 0x1F0B => array(0x1F03), + 0x1F0C => array(0x1F04), + 0x1F0D => array(0x1F05), + 0x1F0E => array(0x1F06), + 0x1F0F => array(0x1F07), + 0x1F18 => array(0x1F10), + 0x1F19 => array(0x1F11), + 0x1F1A => array(0x1F12), + 0x1F1B => array(0x1F13), + 0x1F1C => array(0x1F14), + 0x1F1D => array(0x1F15), + 0x1F28 => array(0x1F20), + 0x1F29 => array(0x1F21), + 0x1F2A => array(0x1F22), + 0x1F2B => array(0x1F23), + 0x1F2C => array(0x1F24), + 0x1F2D => array(0x1F25), + 0x1F2E => array(0x1F26), + 0x1F2F => array(0x1F27), + 0x1F38 => array(0x1F30), + 0x1F39 => array(0x1F31), + 0x1F3A => array(0x1F32), + 0x1F3B => array(0x1F33), + 0x1F3C => array(0x1F34), + 0x1F3D => array(0x1F35), + 0x1F3E => array(0x1F36), + 0x1F3F => array(0x1F37), + 0x1F48 => array(0x1F40), + 0x1F49 => array(0x1F41), + 0x1F4A => array(0x1F42), + 0x1F4B => array(0x1F43), + 0x1F4C => array(0x1F44), + 0x1F4D => array(0x1F45), + 0x1F50 => array(0x3C5, 0x313), + 0x1F52 => array(0x3C5, 0x313, 0x300), + 0x1F54 => array(0x3C5, 0x313, 0x301), + 0x1F56 => array(0x3C5, 0x313, 0x342), + 0x1F59 => array(0x1F51), + 0x1F5B => array(0x1F53), + 0x1F5D => array(0x1F55), + 0x1F5F => array(0x1F57), + 0x1F68 => array(0x1F60), + 0x1F69 => array(0x1F61), + 0x1F6A => array(0x1F62), + 0x1F6B => array(0x1F63), + 0x1F6C => array(0x1F64), + 0x1F6D => array(0x1F65), + 0x1F6E => array(0x1F66), + 0x1F6F => array(0x1F67), + 0x1F80 => array(0x1F00, 0x3B9), + 0x1F81 => array(0x1F01, 0x3B9), + 0x1F82 => array(0x1F02, 0x3B9), + 0x1F83 => array(0x1F03, 0x3B9), + 0x1F84 => array(0x1F04, 0x3B9), + 0x1F85 => array(0x1F05, 0x3B9), + 0x1F86 => array(0x1F06, 0x3B9), + 0x1F87 => array(0x1F07, 0x3B9), + 0x1F88 => array(0x1F00, 0x3B9), + 0x1F89 => array(0x1F01, 0x3B9), + 0x1F8A => array(0x1F02, 0x3B9), + 0x1F8B => array(0x1F03, 0x3B9), + 0x1F8C => array(0x1F04, 0x3B9), + 0x1F8D => array(0x1F05, 0x3B9), + 0x1F8E => array(0x1F06, 0x3B9), + 0x1F8F => array(0x1F07, 0x3B9), + 0x1F90 => array(0x1F20, 0x3B9), + 0x1F91 => array(0x1F21, 0x3B9), + 0x1F92 => array(0x1F22, 0x3B9), + 0x1F93 => array(0x1F23, 0x3B9), + 0x1F94 => array(0x1F24, 0x3B9), + 0x1F95 => array(0x1F25, 0x3B9), + 0x1F96 => array(0x1F26, 0x3B9), + 0x1F97 => array(0x1F27, 0x3B9), + 0x1F98 => array(0x1F20, 0x3B9), + 0x1F99 => array(0x1F21, 0x3B9), + 0x1F9A => array(0x1F22, 0x3B9), + 0x1F9B => array(0x1F23, 0x3B9), + 0x1F9C => array(0x1F24, 0x3B9), + 0x1F9D => array(0x1F25, 0x3B9), + 0x1F9E => array(0x1F26, 0x3B9), + 0x1F9F => array(0x1F27, 0x3B9), + 0x1FA0 => array(0x1F60, 0x3B9), + 0x1FA1 => array(0x1F61, 0x3B9), + 0x1FA2 => array(0x1F62, 0x3B9), + 0x1FA3 => array(0x1F63, 0x3B9), + 0x1FA4 => array(0x1F64, 0x3B9), + 0x1FA5 => array(0x1F65, 0x3B9), + 0x1FA6 => array(0x1F66, 0x3B9), + 0x1FA7 => array(0x1F67, 0x3B9), + 0x1FA8 => array(0x1F60, 0x3B9), + 0x1FA9 => array(0x1F61, 0x3B9), + 0x1FAA => array(0x1F62, 0x3B9), + 0x1FAB => array(0x1F63, 0x3B9), + 0x1FAC => array(0x1F64, 0x3B9), + 0x1FAD => array(0x1F65, 0x3B9), + 0x1FAE => array(0x1F66, 0x3B9), + 0x1FAF => array(0x1F67, 0x3B9), + 0x1FB2 => array(0x1F70, 0x3B9), + 0x1FB3 => array(0x3B1, 0x3B9), + 0x1FB4 => array(0x3AC, 0x3B9), + 0x1FB6 => array(0x3B1, 0x342), + 0x1FB7 => array(0x3B1, 0x342, 0x3B9), + 0x1FB8 => array(0x1FB0), + 0x1FB9 => array(0x1FB1), + 0x1FBA => array(0x1F70), + 0x1FBB => array(0x1F71), + 0x1FBC => array(0x3B1, 0x3B9), + 0x1FBE => array(0x3B9), + 0x1FC2 => array(0x1F74, 0x3B9), + 0x1FC3 => array(0x3B7, 0x3B9), + 0x1FC4 => array(0x3AE, 0x3B9), + 0x1FC6 => array(0x3B7, 0x342), + 0x1FC7 => array(0x3B7, 0x342, 0x3B9), + 0x1FC8 => array(0x1F72), + 0x1FC9 => array(0x1F73), + 0x1FCA => array(0x1F74), + 0x1FCB => array(0x1F75), + 0x1FCC => array(0x3B7, 0x3B9), + 0x1FD2 => array(0x3B9, 0x308, 0x300), + 0x1FD3 => array(0x3B9, 0x308, 0x301), + 0x1FD6 => array(0x3B9, 0x342), + 0x1FD7 => array(0x3B9, 0x308, 0x342), + 0x1FD8 => array(0x1FD0), + 0x1FD9 => array(0x1FD1), + 0x1FDA => array(0x1F76), + 0x1FDB => array(0x1F77), + 0x1FE2 => array(0x3C5, 0x308, 0x300), + 0x1FE3 => array(0x3C5, 0x308, 0x301), + 0x1FE4 => array(0x3C1, 0x313), + 0x1FE6 => array(0x3C5, 0x342), + 0x1FE7 => array(0x3C5, 0x308, 0x342), + 0x1FE8 => array(0x1FE0), + 0x1FE9 => array(0x1FE1), + 0x1FEA => array(0x1F7A), + 0x1FEB => array(0x1F7B), + 0x1FEC => array(0x1FE5), + 0x1FF2 => array(0x1F7C, 0x3B9), + 0x1FF3 => array(0x3C9, 0x3B9), + 0x1FF4 => array(0x3CE, 0x3B9), + 0x1FF6 => array(0x3C9, 0x342), + 0x1FF7 => array(0x3C9, 0x342, 0x3B9), + 0x1FF8 => array(0x1F78), + 0x1FF9 => array(0x1F79), + 0x1FFA => array(0x1F7C), + 0x1FFB => array(0x1F7D), + 0x1FFC => array(0x3C9, 0x3B9), + 0x20A8 => array(0x72, 0x73), + 0x2102 => array(0x63), + 0x2103 => array(0xB0, 0x63), + 0x2107 => array(0x25B), + 0x2109 => array(0xB0, 0x66), + 0x210B => array(0x68), + 0x210C => array(0x68), + 0x210D => array(0x68), + 0x2110 => array(0x69), + 0x2111 => array(0x69), + 0x2112 => array(0x6C), + 0x2115 => array(0x6E), + 0x2116 => array(0x6E, 0x6F), + 0x2119 => array(0x70), + 0x211A => array(0x71), + 0x211B => array(0x72), + 0x211C => array(0x72), + 0x211D => array(0x72), + 0x2120 => array(0x73, 0x6D), + 0x2121 => array(0x74, 0x65, 0x6C), + 0x2122 => array(0x74, 0x6D), + 0x2124 => array(0x7A), + 0x2126 => array(0x3C9), + 0x2128 => array(0x7A), + 0x212A => array(0x6B), + 0x212B => array(0xE5), + 0x212C => array(0x62), + 0x212D => array(0x63), + 0x2130 => array(0x65), + 0x2131 => array(0x66), + 0x2133 => array(0x6D), + 0x213E => array(0x3B3), + 0x213F => array(0x3C0), + 0x2145 => array(0x64), + 0x2160 => array(0x2170), + 0x2161 => array(0x2171), + 0x2162 => array(0x2172), + 0x2163 => array(0x2173), + 0x2164 => array(0x2174), + 0x2165 => array(0x2175), + 0x2166 => array(0x2176), + 0x2167 => array(0x2177), + 0x2168 => array(0x2178), + 0x2169 => array(0x2179), + 0x216A => array(0x217A), + 0x216B => array(0x217B), + 0x216C => array(0x217C), + 0x216D => array(0x217D), + 0x216E => array(0x217E), + 0x216F => array(0x217F), + 0x24B6 => array(0x24D0), + 0x24B7 => array(0x24D1), + 0x24B8 => array(0x24D2), + 0x24B9 => array(0x24D3), + 0x24BA => array(0x24D4), + 0x24BB => array(0x24D5), + 0x24BC => array(0x24D6), + 0x24BD => array(0x24D7), + 0x24BE => array(0x24D8), + 0x24BF => array(0x24D9), + 0x24C0 => array(0x24DA), + 0x24C1 => array(0x24DB), + 0x24C2 => array(0x24DC), + 0x24C3 => array(0x24DD), + 0x24C4 => array(0x24DE), + 0x24C5 => array(0x24DF), + 0x24C6 => array(0x24E0), + 0x24C7 => array(0x24E1), + 0x24C8 => array(0x24E2), + 0x24C9 => array(0x24E3), + 0x24CA => array(0x24E4), + 0x24CB => array(0x24E5), + 0x24CC => array(0x24E6), + 0x24CD => array(0x24E7), + 0x24CE => array(0x24E8), + 0x24CF => array(0x24E9), + 0x3371 => array(0x68, 0x70, 0x61), + 0x3373 => array(0x61, 0x75), + 0x3375 => array(0x6F, 0x76), + 0x3380 => array(0x70, 0x61), + 0x3381 => array(0x6E, 0x61), + 0x3382 => array(0x3BC, 0x61), + 0x3383 => array(0x6D, 0x61), + 0x3384 => array(0x6B, 0x61), + 0x3385 => array(0x6B, 0x62), + 0x3386 => array(0x6D, 0x62), + 0x3387 => array(0x67, 0x62), + 0x338A => array(0x70, 0x66), + 0x338B => array(0x6E, 0x66), + 0x338C => array(0x3BC, 0x66), + 0x3390 => array(0x68, 0x7A), + 0x3391 => array(0x6B, 0x68, 0x7A), + 0x3392 => array(0x6D, 0x68, 0x7A), + 0x3393 => array(0x67, 0x68, 0x7A), + 0x3394 => array(0x74, 0x68, 0x7A), + 0x33A9 => array(0x70, 0x61), + 0x33AA => array(0x6B, 0x70, 0x61), + 0x33AB => array(0x6D, 0x70, 0x61), + 0x33AC => array(0x67, 0x70, 0x61), + 0x33B4 => array(0x70, 0x76), + 0x33B5 => array(0x6E, 0x76), + 0x33B6 => array(0x3BC, 0x76), + 0x33B7 => array(0x6D, 0x76), + 0x33B8 => array(0x6B, 0x76), + 0x33B9 => array(0x6D, 0x76), + 0x33BA => array(0x70, 0x77), + 0x33BB => array(0x6E, 0x77), + 0x33BC => array(0x3BC, 0x77), + 0x33BD => array(0x6D, 0x77), + 0x33BE => array(0x6B, 0x77), + 0x33BF => array(0x6D, 0x77), + 0x33C0 => array(0x6B, 0x3C9), + 0x33C1 => array(0x6D, 0x3C9), + /* 0x33C2 => array(0x61, 0x2E, 0x6D, 0x2E), */ + 0x33C3 => array(0x62, 0x71), + 0x33C6 => array(0x63, 0x2215, 0x6B, 0x67), + 0x33C7 => array(0x63, 0x6F, 0x2E), + 0x33C8 => array(0x64, 0x62), + 0x33C9 => array(0x67, 0x79), + 0x33CB => array(0x68, 0x70), + 0x33CD => array(0x6B, 0x6B), + 0x33CE => array(0x6B, 0x6D), + 0x33D7 => array(0x70, 0x68), + 0x33D9 => array(0x70, 0x70, 0x6D), + 0x33DA => array(0x70, 0x72), + 0x33DC => array(0x73, 0x76), + 0x33DD => array(0x77, 0x62), + 0xFB00 => array(0x66, 0x66), + 0xFB01 => array(0x66, 0x69), + 0xFB02 => array(0x66, 0x6C), + 0xFB03 => array(0x66, 0x66, 0x69), + 0xFB04 => array(0x66, 0x66, 0x6C), + 0xFB05 => array(0x73, 0x74), + 0xFB06 => array(0x73, 0x74), + 0xFB13 => array(0x574, 0x576), + 0xFB14 => array(0x574, 0x565), + 0xFB15 => array(0x574, 0x56B), + 0xFB16 => array(0x57E, 0x576), + 0xFB17 => array(0x574, 0x56D), + 0xFF21 => array(0xFF41), + 0xFF22 => array(0xFF42), + 0xFF23 => array(0xFF43), + 0xFF24 => array(0xFF44), + 0xFF25 => array(0xFF45), + 0xFF26 => array(0xFF46), + 0xFF27 => array(0xFF47), + 0xFF28 => array(0xFF48), + 0xFF29 => array(0xFF49), + 0xFF2A => array(0xFF4A), + 0xFF2B => array(0xFF4B), + 0xFF2C => array(0xFF4C), + 0xFF2D => array(0xFF4D), + 0xFF2E => array(0xFF4E), + 0xFF2F => array(0xFF4F), + 0xFF30 => array(0xFF50), + 0xFF31 => array(0xFF51), + 0xFF32 => array(0xFF52), + 0xFF33 => array(0xFF53), + 0xFF34 => array(0xFF54), + 0xFF35 => array(0xFF55), + 0xFF36 => array(0xFF56), + 0xFF37 => array(0xFF57), + 0xFF38 => array(0xFF58), + 0xFF39 => array(0xFF59), + 0xFF3A => array(0xFF5A), + 0x10400 => array(0x10428), + 0x10401 => array(0x10429), + 0x10402 => array(0x1042A), + 0x10403 => array(0x1042B), + 0x10404 => array(0x1042C), + 0x10405 => array(0x1042D), + 0x10406 => array(0x1042E), + 0x10407 => array(0x1042F), + 0x10408 => array(0x10430), + 0x10409 => array(0x10431), + 0x1040A => array(0x10432), + 0x1040B => array(0x10433), + 0x1040C => array(0x10434), + 0x1040D => array(0x10435), + 0x1040E => array(0x10436), + 0x1040F => array(0x10437), + 0x10410 => array(0x10438), + 0x10411 => array(0x10439), + 0x10412 => array(0x1043A), + 0x10413 => array(0x1043B), + 0x10414 => array(0x1043C), + 0x10415 => array(0x1043D), + 0x10416 => array(0x1043E), + 0x10417 => array(0x1043F), + 0x10418 => array(0x10440), + 0x10419 => array(0x10441), + 0x1041A => array(0x10442), + 0x1041B => array(0x10443), + 0x1041C => array(0x10444), + 0x1041D => array(0x10445), + 0x1041E => array(0x10446), + 0x1041F => array(0x10447), + 0x10420 => array(0x10448), + 0x10421 => array(0x10449), + 0x10422 => array(0x1044A), + 0x10423 => array(0x1044B), + 0x10424 => array(0x1044C), + 0x10425 => array(0x1044D), + 0x1D400 => array(0x61), + 0x1D401 => array(0x62), + 0x1D402 => array(0x63), + 0x1D403 => array(0x64), + 0x1D404 => array(0x65), + 0x1D405 => array(0x66), + 0x1D406 => array(0x67), + 0x1D407 => array(0x68), + 0x1D408 => array(0x69), + 0x1D409 => array(0x6A), + 0x1D40A => array(0x6B), + 0x1D40B => array(0x6C), + 0x1D40C => array(0x6D), + 0x1D40D => array(0x6E), + 0x1D40E => array(0x6F), + 0x1D40F => array(0x70), + 0x1D410 => array(0x71), + 0x1D411 => array(0x72), + 0x1D412 => array(0x73), + 0x1D413 => array(0x74), + 0x1D414 => array(0x75), + 0x1D415 => array(0x76), + 0x1D416 => array(0x77), + 0x1D417 => array(0x78), + 0x1D418 => array(0x79), + 0x1D419 => array(0x7A), + 0x1D434 => array(0x61), + 0x1D435 => array(0x62), + 0x1D436 => array(0x63), + 0x1D437 => array(0x64), + 0x1D438 => array(0x65), + 0x1D439 => array(0x66), + 0x1D43A => array(0x67), + 0x1D43B => array(0x68), + 0x1D43C => array(0x69), + 0x1D43D => array(0x6A), + 0x1D43E => array(0x6B), + 0x1D43F => array(0x6C), + 0x1D440 => array(0x6D), + 0x1D441 => array(0x6E), + 0x1D442 => array(0x6F), + 0x1D443 => array(0x70), + 0x1D444 => array(0x71), + 0x1D445 => array(0x72), + 0x1D446 => array(0x73), + 0x1D447 => array(0x74), + 0x1D448 => array(0x75), + 0x1D449 => array(0x76), + 0x1D44A => array(0x77), + 0x1D44B => array(0x78), + 0x1D44C => array(0x79), + 0x1D44D => array(0x7A), + 0x1D468 => array(0x61), + 0x1D469 => array(0x62), + 0x1D46A => array(0x63), + 0x1D46B => array(0x64), + 0x1D46C => array(0x65), + 0x1D46D => array(0x66), + 0x1D46E => array(0x67), + 0x1D46F => array(0x68), + 0x1D470 => array(0x69), + 0x1D471 => array(0x6A), + 0x1D472 => array(0x6B), + 0x1D473 => array(0x6C), + 0x1D474 => array(0x6D), + 0x1D475 => array(0x6E), + 0x1D476 => array(0x6F), + 0x1D477 => array(0x70), + 0x1D478 => array(0x71), + 0x1D479 => array(0x72), + 0x1D47A => array(0x73), + 0x1D47B => array(0x74), + 0x1D47C => array(0x75), + 0x1D47D => array(0x76), + 0x1D47E => array(0x77), + 0x1D47F => array(0x78), + 0x1D480 => array(0x79), + 0x1D481 => array(0x7A), + 0x1D49C => array(0x61), + 0x1D49E => array(0x63), + 0x1D49F => array(0x64), + 0x1D4A2 => array(0x67), + 0x1D4A5 => array(0x6A), + 0x1D4A6 => array(0x6B), + 0x1D4A9 => array(0x6E), + 0x1D4AA => array(0x6F), + 0x1D4AB => array(0x70), + 0x1D4AC => array(0x71), + 0x1D4AE => array(0x73), + 0x1D4AF => array(0x74), + 0x1D4B0 => array(0x75), + 0x1D4B1 => array(0x76), + 0x1D4B2 => array(0x77), + 0x1D4B3 => array(0x78), + 0x1D4B4 => array(0x79), + 0x1D4B5 => array(0x7A), + 0x1D4D0 => array(0x61), + 0x1D4D1 => array(0x62), + 0x1D4D2 => array(0x63), + 0x1D4D3 => array(0x64), + 0x1D4D4 => array(0x65), + 0x1D4D5 => array(0x66), + 0x1D4D6 => array(0x67), + 0x1D4D7 => array(0x68), + 0x1D4D8 => array(0x69), + 0x1D4D9 => array(0x6A), + 0x1D4DA => array(0x6B), + 0x1D4DB => array(0x6C), + 0x1D4DC => array(0x6D), + 0x1D4DD => array(0x6E), + 0x1D4DE => array(0x6F), + 0x1D4DF => array(0x70), + 0x1D4E0 => array(0x71), + 0x1D4E1 => array(0x72), + 0x1D4E2 => array(0x73), + 0x1D4E3 => array(0x74), + 0x1D4E4 => array(0x75), + 0x1D4E5 => array(0x76), + 0x1D4E6 => array(0x77), + 0x1D4E7 => array(0x78), + 0x1D4E8 => array(0x79), + 0x1D4E9 => array(0x7A), + 0x1D504 => array(0x61), + 0x1D505 => array(0x62), + 0x1D507 => array(0x64), + 0x1D508 => array(0x65), + 0x1D509 => array(0x66), + 0x1D50A => array(0x67), + 0x1D50D => array(0x6A), + 0x1D50E => array(0x6B), + 0x1D50F => array(0x6C), + 0x1D510 => array(0x6D), + 0x1D511 => array(0x6E), + 0x1D512 => array(0x6F), + 0x1D513 => array(0x70), + 0x1D514 => array(0x71), + 0x1D516 => array(0x73), + 0x1D517 => array(0x74), + 0x1D518 => array(0x75), + 0x1D519 => array(0x76), + 0x1D51A => array(0x77), + 0x1D51B => array(0x78), + 0x1D51C => array(0x79), + 0x1D538 => array(0x61), + 0x1D539 => array(0x62), + 0x1D53B => array(0x64), + 0x1D53C => array(0x65), + 0x1D53D => array(0x66), + 0x1D53E => array(0x67), + 0x1D540 => array(0x69), + 0x1D541 => array(0x6A), + 0x1D542 => array(0x6B), + 0x1D543 => array(0x6C), + 0x1D544 => array(0x6D), + 0x1D546 => array(0x6F), + 0x1D54A => array(0x73), + 0x1D54B => array(0x74), + 0x1D54C => array(0x75), + 0x1D54D => array(0x76), + 0x1D54E => array(0x77), + 0x1D54F => array(0x78), + 0x1D550 => array(0x79), + 0x1D56C => array(0x61), + 0x1D56D => array(0x62), + 0x1D56E => array(0x63), + 0x1D56F => array(0x64), + 0x1D570 => array(0x65), + 0x1D571 => array(0x66), + 0x1D572 => array(0x67), + 0x1D573 => array(0x68), + 0x1D574 => array(0x69), + 0x1D575 => array(0x6A), + 0x1D576 => array(0x6B), + 0x1D577 => array(0x6C), + 0x1D578 => array(0x6D), + 0x1D579 => array(0x6E), + 0x1D57A => array(0x6F), + 0x1D57B => array(0x70), + 0x1D57C => array(0x71), + 0x1D57D => array(0x72), + 0x1D57E => array(0x73), + 0x1D57F => array(0x74), + 0x1D580 => array(0x75), + 0x1D581 => array(0x76), + 0x1D582 => array(0x77), + 0x1D583 => array(0x78), + 0x1D584 => array(0x79), + 0x1D585 => array(0x7A), + 0x1D5A0 => array(0x61), + 0x1D5A1 => array(0x62), + 0x1D5A2 => array(0x63), + 0x1D5A3 => array(0x64), + 0x1D5A4 => array(0x65), + 0x1D5A5 => array(0x66), + 0x1D5A6 => array(0x67), + 0x1D5A7 => array(0x68), + 0x1D5A8 => array(0x69), + 0x1D5A9 => array(0x6A), + 0x1D5AA => array(0x6B), + 0x1D5AB => array(0x6C), + 0x1D5AC => array(0x6D), + 0x1D5AD => array(0x6E), + 0x1D5AE => array(0x6F), + 0x1D5AF => array(0x70), + 0x1D5B0 => array(0x71), + 0x1D5B1 => array(0x72), + 0x1D5B2 => array(0x73), + 0x1D5B3 => array(0x74), + 0x1D5B4 => array(0x75), + 0x1D5B5 => array(0x76), + 0x1D5B6 => array(0x77), + 0x1D5B7 => array(0x78), + 0x1D5B8 => array(0x79), + 0x1D5B9 => array(0x7A), + 0x1D5D4 => array(0x61), + 0x1D5D5 => array(0x62), + 0x1D5D6 => array(0x63), + 0x1D5D7 => array(0x64), + 0x1D5D8 => array(0x65), + 0x1D5D9 => array(0x66), + 0x1D5DA => array(0x67), + 0x1D5DB => array(0x68), + 0x1D5DC => array(0x69), + 0x1D5DD => array(0x6A), + 0x1D5DE => array(0x6B), + 0x1D5DF => array(0x6C), + 0x1D5E0 => array(0x6D), + 0x1D5E1 => array(0x6E), + 0x1D5E2 => array(0x6F), + 0x1D5E3 => array(0x70), + 0x1D5E4 => array(0x71), + 0x1D5E5 => array(0x72), + 0x1D5E6 => array(0x73), + 0x1D5E7 => array(0x74), + 0x1D5E8 => array(0x75), + 0x1D5E9 => array(0x76), + 0x1D5EA => array(0x77), + 0x1D5EB => array(0x78), + 0x1D5EC => array(0x79), + 0x1D5ED => array(0x7A), + 0x1D608 => array(0x61), + 0x1D609 => array(0x62), + 0x1D60A => array(0x63), + 0x1D60B => array(0x64), + 0x1D60C => array(0x65), + 0x1D60D => array(0x66), + 0x1D60E => array(0x67), + 0x1D60F => array(0x68), + 0x1D610 => array(0x69), + 0x1D611 => array(0x6A), + 0x1D612 => array(0x6B), + 0x1D613 => array(0x6C), + 0x1D614 => array(0x6D), + 0x1D615 => array(0x6E), + 0x1D616 => array(0x6F), + 0x1D617 => array(0x70), + 0x1D618 => array(0x71), + 0x1D619 => array(0x72), + 0x1D61A => array(0x73), + 0x1D61B => array(0x74), + 0x1D61C => array(0x75), + 0x1D61D => array(0x76), + 0x1D61E => array(0x77), + 0x1D61F => array(0x78), + 0x1D620 => array(0x79), + 0x1D621 => array(0x7A), + 0x1D63C => array(0x61), + 0x1D63D => array(0x62), + 0x1D63E => array(0x63), + 0x1D63F => array(0x64), + 0x1D640 => array(0x65), + 0x1D641 => array(0x66), + 0x1D642 => array(0x67), + 0x1D643 => array(0x68), + 0x1D644 => array(0x69), + 0x1D645 => array(0x6A), + 0x1D646 => array(0x6B), + 0x1D647 => array(0x6C), + 0x1D648 => array(0x6D), + 0x1D649 => array(0x6E), + 0x1D64A => array(0x6F), + 0x1D64B => array(0x70), + 0x1D64C => array(0x71), + 0x1D64D => array(0x72), + 0x1D64E => array(0x73), + 0x1D64F => array(0x74), + 0x1D650 => array(0x75), + 0x1D651 => array(0x76), + 0x1D652 => array(0x77), + 0x1D653 => array(0x78), + 0x1D654 => array(0x79), + 0x1D655 => array(0x7A), + 0x1D670 => array(0x61), + 0x1D671 => array(0x62), + 0x1D672 => array(0x63), + 0x1D673 => array(0x64), + 0x1D674 => array(0x65), + 0x1D675 => array(0x66), + 0x1D676 => array(0x67), + 0x1D677 => array(0x68), + 0x1D678 => array(0x69), + 0x1D679 => array(0x6A), + 0x1D67A => array(0x6B), + 0x1D67B => array(0x6C), + 0x1D67C => array(0x6D), + 0x1D67D => array(0x6E), + 0x1D67E => array(0x6F), + 0x1D67F => array(0x70), + 0x1D680 => array(0x71), + 0x1D681 => array(0x72), + 0x1D682 => array(0x73), + 0x1D683 => array(0x74), + 0x1D684 => array(0x75), + 0x1D685 => array(0x76), + 0x1D686 => array(0x77), + 0x1D687 => array(0x78), + 0x1D688 => array(0x79), + 0x1D689 => array(0x7A), + 0x1D6A8 => array(0x3B1), + 0x1D6A9 => array(0x3B2), + 0x1D6AA => array(0x3B3), + 0x1D6AB => array(0x3B4), + 0x1D6AC => array(0x3B5), + 0x1D6AD => array(0x3B6), + 0x1D6AE => array(0x3B7), + 0x1D6AF => array(0x3B8), + 0x1D6B0 => array(0x3B9), + 0x1D6B1 => array(0x3BA), + 0x1D6B2 => array(0x3BB), + 0x1D6B3 => array(0x3BC), + 0x1D6B4 => array(0x3BD), + 0x1D6B5 => array(0x3BE), + 0x1D6B6 => array(0x3BF), + 0x1D6B7 => array(0x3C0), + 0x1D6B8 => array(0x3C1), + 0x1D6B9 => array(0x3B8), + 0x1D6BA => array(0x3C3), + 0x1D6BB => array(0x3C4), + 0x1D6BC => array(0x3C5), + 0x1D6BD => array(0x3C6), + 0x1D6BE => array(0x3C7), + 0x1D6BF => array(0x3C8), + 0x1D6C0 => array(0x3C9), + 0x1D6D3 => array(0x3C3), + 0x1D6E2 => array(0x3B1), + 0x1D6E3 => array(0x3B2), + 0x1D6E4 => array(0x3B3), + 0x1D6E5 => array(0x3B4), + 0x1D6E6 => array(0x3B5), + 0x1D6E7 => array(0x3B6), + 0x1D6E8 => array(0x3B7), + 0x1D6E9 => array(0x3B8), + 0x1D6EA => array(0x3B9), + 0x1D6EB => array(0x3BA), + 0x1D6EC => array(0x3BB), + 0x1D6ED => array(0x3BC), + 0x1D6EE => array(0x3BD), + 0x1D6EF => array(0x3BE), + 0x1D6F0 => array(0x3BF), + 0x1D6F1 => array(0x3C0), + 0x1D6F2 => array(0x3C1), + 0x1D6F3 => array(0x3B8), + 0x1D6F4 => array(0x3C3), + 0x1D6F5 => array(0x3C4), + 0x1D6F6 => array(0x3C5), + 0x1D6F7 => array(0x3C6), + 0x1D6F8 => array(0x3C7), + 0x1D6F9 => array(0x3C8), + 0x1D6FA => array(0x3C9), + 0x1D70D => array(0x3C3), + 0x1D71C => array(0x3B1), + 0x1D71D => array(0x3B2), + 0x1D71E => array(0x3B3), + 0x1D71F => array(0x3B4), + 0x1D720 => array(0x3B5), + 0x1D721 => array(0x3B6), + 0x1D722 => array(0x3B7), + 0x1D723 => array(0x3B8), + 0x1D724 => array(0x3B9), + 0x1D725 => array(0x3BA), + 0x1D726 => array(0x3BB), + 0x1D727 => array(0x3BC), + 0x1D728 => array(0x3BD), + 0x1D729 => array(0x3BE), + 0x1D72A => array(0x3BF), + 0x1D72B => array(0x3C0), + 0x1D72C => array(0x3C1), + 0x1D72D => array(0x3B8), + 0x1D72E => array(0x3C3), + 0x1D72F => array(0x3C4), + 0x1D730 => array(0x3C5), + 0x1D731 => array(0x3C6), + 0x1D732 => array(0x3C7), + 0x1D733 => array(0x3C8), + 0x1D734 => array(0x3C9), + 0x1D747 => array(0x3C3), + 0x1D756 => array(0x3B1), + 0x1D757 => array(0x3B2), + 0x1D758 => array(0x3B3), + 0x1D759 => array(0x3B4), + 0x1D75A => array(0x3B5), + 0x1D75B => array(0x3B6), + 0x1D75C => array(0x3B7), + 0x1D75D => array(0x3B8), + 0x1D75E => array(0x3B9), + 0x1D75F => array(0x3BA), + 0x1D760 => array(0x3BB), + 0x1D761 => array(0x3BC), + 0x1D762 => array(0x3BD), + 0x1D763 => array(0x3BE), + 0x1D764 => array(0x3BF), + 0x1D765 => array(0x3C0), + 0x1D766 => array(0x3C1), + 0x1D767 => array(0x3B8), + 0x1D768 => array(0x3C3), + 0x1D769 => array(0x3C4), + 0x1D76A => array(0x3C5), + 0x1D76B => array(0x3C6), + 0x1D76C => array(0x3C7), + 0x1D76D => array(0x3C8), + 0x1D76E => array(0x3C9), + 0x1D781 => array(0x3C3), + 0x1D790 => array(0x3B1), + 0x1D791 => array(0x3B2), + 0x1D792 => array(0x3B3), + 0x1D793 => array(0x3B4), + 0x1D794 => array(0x3B5), + 0x1D795 => array(0x3B6), + 0x1D796 => array(0x3B7), + 0x1D797 => array(0x3B8), + 0x1D798 => array(0x3B9), + 0x1D799 => array(0x3BA), + 0x1D79A => array(0x3BB), + 0x1D79B => array(0x3BC), + 0x1D79C => array(0x3BD), + 0x1D79D => array(0x3BE), + 0x1D79E => array(0x3BF), + 0x1D79F => array(0x3C0), + 0x1D7A0 => array(0x3C1), + 0x1D7A1 => array(0x3B8), + 0x1D7A2 => array(0x3C3), + 0x1D7A3 => array(0x3C4), + 0x1D7A4 => array(0x3C5), + 0x1D7A5 => array(0x3C6), + 0x1D7A6 => array(0x3C7), + 0x1D7A7 => array(0x3C8), + 0x1D7A8 => array(0x3C9), + 0x1D7BB => array(0x3C3), + 0x3F9 => array(0x3C3), + 0x1D2C => array(0x61), + 0x1D2D => array(0xE6), + 0x1D2E => array(0x62), + 0x1D30 => array(0x64), + 0x1D31 => array(0x65), + 0x1D32 => array(0x1DD), + 0x1D33 => array(0x67), + 0x1D34 => array(0x68), + 0x1D35 => array(0x69), + 0x1D36 => array(0x6A), + 0x1D37 => array(0x6B), + 0x1D38 => array(0x6C), + 0x1D39 => array(0x6D), + 0x1D3A => array(0x6E), + 0x1D3C => array(0x6F), + 0x1D3D => array(0x223), + 0x1D3E => array(0x70), + 0x1D3F => array(0x72), + 0x1D40 => array(0x74), + 0x1D41 => array(0x75), + 0x1D42 => array(0x77), + 0x213B => array(0x66, 0x61, 0x78), + 0x3250 => array(0x70, 0x74, 0x65), + 0x32CC => array(0x68, 0x67), + 0x32CE => array(0x65, 0x76), + 0x32CF => array(0x6C, 0x74, 0x64), + 0x337A => array(0x69, 0x75), + 0x33DE => array(0x76, 0x2215, 0x6D), + 0x33DF => array(0x61, 0x2215, 0x6D) + ); + + /** + * Normalization Combining Classes; Code Points not listed + * got Combining Class 0. + * + * @static + * @var array + * @access private + */ + private static $_np_norm_combcls = array( + 0x334 => 1, + 0x335 => 1, + 0x336 => 1, + 0x337 => 1, + 0x338 => 1, + 0x93C => 7, + 0x9BC => 7, + 0xA3C => 7, + 0xABC => 7, + 0xB3C => 7, + 0xCBC => 7, + 0x1037 => 7, + 0x3099 => 8, + 0x309A => 8, + 0x94D => 9, + 0x9CD => 9, + 0xA4D => 9, + 0xACD => 9, + 0xB4D => 9, + 0xBCD => 9, + 0xC4D => 9, + 0xCCD => 9, + 0xD4D => 9, + 0xDCA => 9, + 0xE3A => 9, + 0xF84 => 9, + 0x1039 => 9, + 0x1714 => 9, + 0x1734 => 9, + 0x17D2 => 9, + 0x5B0 => 10, + 0x5B1 => 11, + 0x5B2 => 12, + 0x5B3 => 13, + 0x5B4 => 14, + 0x5B5 => 15, + 0x5B6 => 16, + 0x5B7 => 17, + 0x5B8 => 18, + 0x5B9 => 19, + 0x5BB => 20, + 0x5Bc => 21, + 0x5BD => 22, + 0x5BF => 23, + 0x5C1 => 24, + 0x5C2 => 25, + 0xFB1E => 26, + 0x64B => 27, + 0x64C => 28, + 0x64D => 29, + 0x64E => 30, + 0x64F => 31, + 0x650 => 32, + 0x651 => 33, + 0x652 => 34, + 0x670 => 35, + 0x711 => 36, + 0xC55 => 84, + 0xC56 => 91, + 0xE38 => 103, + 0xE39 => 103, + 0xE48 => 107, + 0xE49 => 107, + 0xE4A => 107, + 0xE4B => 107, + 0xEB8 => 118, + 0xEB9 => 118, + 0xEC8 => 122, + 0xEC9 => 122, + 0xECA => 122, + 0xECB => 122, + 0xF71 => 129, + 0xF72 => 130, + 0xF7A => 130, + 0xF7B => 130, + 0xF7C => 130, + 0xF7D => 130, + 0xF80 => 130, + 0xF74 => 132, + 0x321 => 202, + 0x322 => 202, + 0x327 => 202, + 0x328 => 202, + 0x31B => 216, + 0xF39 => 216, + 0x1D165 => 216, + 0x1D166 => 216, + 0x1D16E => 216, + 0x1D16F => 216, + 0x1D170 => 216, + 0x1D171 => 216, + 0x1D172 => 216, + 0x302A => 218, + 0x316 => 220, + 0x317 => 220, + 0x318 => 220, + 0x319 => 220, + 0x31C => 220, + 0x31D => 220, + 0x31E => 220, + 0x31F => 220, + 0x320 => 220, + 0x323 => 220, + 0x324 => 220, + 0x325 => 220, + 0x326 => 220, + 0x329 => 220, + 0x32A => 220, + 0x32B => 220, + 0x32C => 220, + 0x32D => 220, + 0x32E => 220, + 0x32F => 220, + 0x330 => 220, + 0x331 => 220, + 0x332 => 220, + 0x333 => 220, + 0x339 => 220, + 0x33A => 220, + 0x33B => 220, + 0x33C => 220, + 0x347 => 220, + 0x348 => 220, + 0x349 => 220, + 0x34D => 220, + 0x34E => 220, + 0x353 => 220, + 0x354 => 220, + 0x355 => 220, + 0x356 => 220, + 0x591 => 220, + 0x596 => 220, + 0x59B => 220, + 0x5A3 => 220, + 0x5A4 => 220, + 0x5A5 => 220, + 0x5A6 => 220, + 0x5A7 => 220, + 0x5AA => 220, + 0x655 => 220, + 0x656 => 220, + 0x6E3 => 220, + 0x6EA => 220, + 0x6ED => 220, + 0x731 => 220, + 0x734 => 220, + 0x737 => 220, + 0x738 => 220, + 0x739 => 220, + 0x73B => 220, + 0x73C => 220, + 0x73E => 220, + 0x742 => 220, + 0x744 => 220, + 0x746 => 220, + 0x748 => 220, + 0x952 => 220, + 0xF18 => 220, + 0xF19 => 220, + 0xF35 => 220, + 0xF37 => 220, + 0xFC6 => 220, + 0x193B => 220, + 0x20E8 => 220, + 0x1D17B => 220, + 0x1D17C => 220, + 0x1D17D => 220, + 0x1D17E => 220, + 0x1D17F => 220, + 0x1D180 => 220, + 0x1D181 => 220, + 0x1D182 => 220, + 0x1D18A => 220, + 0x1D18B => 220, + 0x59A => 222, + 0x5AD => 222, + 0x1929 => 222, + 0x302D => 222, + 0x302E => 224, + 0x302F => 224, + 0x1D16D => 226, + 0x5AE => 228, + 0x18A9 => 228, + 0x302B => 228, + 0x300 => 230, + 0x301 => 230, + 0x302 => 230, + 0x303 => 230, + 0x304 => 230, + 0x305 => 230, + 0x306 => 230, + 0x307 => 230, + 0x308 => 230, + 0x309 => 230, + 0x30A => 230, + 0x30B => 230, + 0x30C => 230, + 0x30D => 230, + 0x30E => 230, + 0x30F => 230, + 0x310 => 230, + 0x311 => 230, + 0x312 => 230, + 0x313 => 230, + 0x314 => 230, + 0x33D => 230, + 0x33E => 230, + 0x33F => 230, + 0x340 => 230, + 0x341 => 230, + 0x342 => 230, + 0x343 => 230, + 0x344 => 230, + 0x346 => 230, + 0x34A => 230, + 0x34B => 230, + 0x34C => 230, + 0x350 => 230, + 0x351 => 230, + 0x352 => 230, + 0x357 => 230, + 0x363 => 230, + 0x364 => 230, + 0x365 => 230, + 0x366 => 230, + 0x367 => 230, + 0x368 => 230, + 0x369 => 230, + 0x36A => 230, + 0x36B => 230, + 0x36C => 230, + 0x36D => 230, + 0x36E => 230, + 0x36F => 230, + 0x483 => 230, + 0x484 => 230, + 0x485 => 230, + 0x486 => 230, + 0x592 => 230, + 0x593 => 230, + 0x594 => 230, + 0x595 => 230, + 0x597 => 230, + 0x598 => 230, + 0x599 => 230, + 0x59C => 230, + 0x59D => 230, + 0x59E => 230, + 0x59F => 230, + 0x5A0 => 230, + 0x5A1 => 230, + 0x5A8 => 230, + 0x5A9 => 230, + 0x5AB => 230, + 0x5AC => 230, + 0x5AF => 230, + 0x5C4 => 230, + 0x610 => 230, + 0x611 => 230, + 0x612 => 230, + 0x613 => 230, + 0x614 => 230, + 0x615 => 230, + 0x653 => 230, + 0x654 => 230, + 0x657 => 230, + 0x658 => 230, + 0x6D6 => 230, + 0x6D7 => 230, + 0x6D8 => 230, + 0x6D9 => 230, + 0x6DA => 230, + 0x6DB => 230, + 0x6DC => 230, + 0x6DF => 230, + 0x6E0 => 230, + 0x6E1 => 230, + 0x6E2 => 230, + 0x6E4 => 230, + 0x6E7 => 230, + 0x6E8 => 230, + 0x6EB => 230, + 0x6EC => 230, + 0x730 => 230, + 0x732 => 230, + 0x733 => 230, + 0x735 => 230, + 0x736 => 230, + 0x73A => 230, + 0x73D => 230, + 0x73F => 230, + 0x740 => 230, + 0x741 => 230, + 0x743 => 230, + 0x745 => 230, + 0x747 => 230, + 0x749 => 230, + 0x74A => 230, + 0x951 => 230, + 0x953 => 230, + 0x954 => 230, + 0xF82 => 230, + 0xF83 => 230, + 0xF86 => 230, + 0xF87 => 230, + 0x170D => 230, + 0x193A => 230, + 0x20D0 => 230, + 0x20D1 => 230, + 0x20D4 => 230, + 0x20D5 => 230, + 0x20D6 => 230, + 0x20D7 => 230, + 0x20DB => 230, + 0x20DC => 230, + 0x20E1 => 230, + 0x20E7 => 230, + 0x20E9 => 230, + 0xFE20 => 230, + 0xFE21 => 230, + 0xFE22 => 230, + 0xFE23 => 230, + 0x1D185 => 230, + 0x1D186 => 230, + 0x1D187 => 230, + 0x1D189 => 230, + 0x1D188 => 230, + 0x1D1AA => 230, + 0x1D1AB => 230, + 0x1D1AC => 230, + 0x1D1AD => 230, + 0x315 => 232, + 0x31A => 232, + 0x302C => 232, + 0x35F => 233, + 0x362 => 233, + 0x35D => 234, + 0x35E => 234, + 0x360 => 234, + 0x361 => 234, + 0x345 => 240 + ); + // }}} + + // {{{ properties + /** + * @var string + * @access private + */ + private $_punycode_prefix = 'xn--'; + + /** + * @access private + */ + private $_invalid_ucs = 0x80000000; + + /** + * @access private + */ + private $_max_ucs = 0x10FFFF; + + /** + * @var int + * @access private + */ + private $_base = 36; + + /** + * @var int + * @access private + */ + private $_tmin = 1; + + /** + * @var int + * @access private + */ + private $_tmax = 26; + + /** + * @var int + * @access private + */ + private $_skew = 38; + + /** + * @var int + * @access private + */ + private $_damp = 700; + + /** + * @var int + * @access private + */ + private $_initial_bias = 72; + + /** + * @var int + * @access private + */ + private $_initial_n = 0x80; + + /** + * @var int + * @access private + */ + private $_slast; + + /** + * @access private + */ + private $_sbase = 0xAC00; + + /** + * @access private + */ + private $_lbase = 0x1100; + + /** + * @access private + */ + private $_vbase = 0x1161; + + /** + * @access private + */ + private $_tbase = 0x11a7; + + /** + * @var int + * @access private + */ + private $_lcount = 19; + + /** + * @var int + * @access private + */ + private $_vcount = 21; + + /** + * @var int + * @access private + */ + private $_tcount = 28; + + /** + * vcount * tcount + * + * @var int + * @access private + */ + private $_ncount = 588; + + /** + * lcount * tcount * vcount + * + * @var int + * @access private + */ + private $_scount = 11172; + + /** + * Default encoding for encode()'s input and decode()'s output is UTF-8; + * Other possible encodings are ucs4_string and ucs4_array + * See {@link setParams()} for how to select these + * + * @var bool + * @access private + */ + private $_api_encoding = 'utf8'; + + /** + * Overlong UTF-8 encodings are forbidden + * + * @var bool + * @access private + */ + private $_allow_overlong = false; + + /** + * Behave strict or not + * + * @var bool + * @access private + */ + private $_strict_mode = false; + + /** + * IDNA-version to use + * + * Values are "2003" and "2008". + * Defaults to "2003", since that was the original version and for + * compatibility with previous versions of this library. + * If you need to encode "new" characters like the German "Eszett", + * please switch to 2008 first before encoding. + * + * @var bool + * @access private + */ + private $_version = '2003'; + + /** + * Cached value indicating whether or not mbstring function overloading is + * on for strlen + * + * This is cached for optimal performance. + * + * @var boolean + * @see Net_IDNA2::_byteLength() + */ + private static $_mb_string_overload = null; + // }}} + + + // {{{ constructor + /** + * Constructor + * + * @param array $options Options to initialise the object with + * + * @access public + * @see setParams() + */ + public function __construct($options = null) + { + $this->_slast = $this->_sbase + $this->_lcount * $this->_vcount * $this->_tcount; + + if (is_array($options)) { + $this->setParams($options); + } + + // populate mbstring overloading cache if not set + if (self::$_mb_string_overload === null) { + self::$_mb_string_overload = (extension_loaded('mbstring') + && (ini_get('mbstring.func_overload') & 0x02) === 0x02); + } + } + // }}} + + + /** + * Sets a new option value. Available options and values: + * + * [utf8 - Use either UTF-8 or ISO-8859-1 as input (true for UTF-8, false + * otherwise); The output is always UTF-8] + * [overlong - Unicode does not allow unnecessarily long encodings of chars, + * to allow this, set this parameter to true, else to false; + * default is false.] + * [strict - true: strict mode, good for registration purposes - Causes errors + * on failures; false: loose mode, ideal for "wildlife" applications + * by silently ignoring errors and returning the original input instead] + * + * @param mixed $option Parameter to set (string: single parameter; array of Parameter => Value pairs) + * @param string $value Value to use (if parameter 1 is a string) + * + * @return boolean true on success, false otherwise + * @access public + */ + public function setParams($option, $value = false) + { + if (!is_array($option)) { + $option = array($option => $value); + } + + foreach ($option as $k => $v) { + switch ($k) { + case 'encoding': + switch ($v) { + case 'utf8': + case 'ucs4_string': + case 'ucs4_array': + $this->_api_encoding = $v; + break; + + default: + throw new InvalidArgumentException('Set Parameter: Unknown parameter '.$v.' for option '.$k); + } + + break; + + case 'overlong': + $this->_allow_overlong = ($v) ? true : false; + break; + + case 'strict': + $this->_strict_mode = ($v) ? true : false; + break; + + case 'version': + if (in_array($v, array('2003', '2008'))) { + $this->_version = $v; + } else { + throw new InvalidArgumentException('Set Parameter: Invalid parameter '.$v.' for option '.$k); + } + break; + + default: + return false; + } + } + + return true; + } + + /** + * Encode a given UTF-8 domain name. + * + * @param string $decoded Domain name (UTF-8 or UCS-4) + * @param string $one_time_encoding Desired input encoding, see {@link set_parameter} + * If not given will use default-encoding + * + * @return string Encoded Domain name (ACE string) + * @return mixed processed string + * @throws Exception + * @access public + */ + public function encode($decoded, $one_time_encoding = false) + { + // Forcing conversion of input to UCS4 array + // If one time encoding is given, use this, else the objects property + switch (($one_time_encoding) ? $one_time_encoding : $this->_api_encoding) { + case 'utf8': + $decoded = $this->_utf8_to_ucs4($decoded); + break; + case 'ucs4_string': + $decoded = $this->_ucs4_string_to_ucs4($decoded); + case 'ucs4_array': // No break; before this line. Catch case, but do nothing + break; + default: + throw new InvalidArgumentException('Unsupported input format'); + } + + // No input, no output, what else did you expect? + if (empty($decoded)) return ''; + + // Anchors for iteration + $last_begin = 0; + // Output string + $output = ''; + + foreach ($decoded as $k => $v) { + // Make sure to use just the plain dot + switch($v) { + case 0x3002: + case 0xFF0E: + case 0xFF61: + $decoded[$k] = 0x2E; + // It's right, no break here + // The codepoints above have to be converted to dots anyway + + // Stumbling across an anchoring character + case 0x2E: + case 0x2F: + case 0x3A: + case 0x3F: + case 0x40: + // Neither email addresses nor URLs allowed in strict mode + if ($this->_strict_mode) { + throw new InvalidArgumentException('Neither email addresses nor URLs are allowed in strict mode.'); + } + // Skip first char + if ($k) { + $encoded = ''; + $encoded = $this->_encode(array_slice($decoded, $last_begin, (($k)-$last_begin))); + if ($encoded) { + $output .= $encoded; + } else { + $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($k)-$last_begin))); + } + $output .= chr($decoded[$k]); + } + $last_begin = $k + 1; + } + } + // Catch the rest of the string + if ($last_begin) { + $inp_len = sizeof($decoded); + $encoded = ''; + $encoded = $this->_encode(array_slice($decoded, $last_begin, (($inp_len)-$last_begin))); + if ($encoded) { + $output .= $encoded; + } else { + $output .= $this->_ucs4_to_utf8(array_slice($decoded, $last_begin, (($inp_len)-$last_begin))); + } + return $output; + } + + if ($output = $this->_encode($decoded)) { + return $output; + } + + return $this->_ucs4_to_utf8($decoded); + } + + /** + * Decode a given ACE domain name. + * + * @param string $input Domain name (ACE string) + * @param string $one_time_encoding Desired output encoding, see {@link set_parameter} + * + * @return string Decoded Domain name (UTF-8 or UCS-4) + * @throws Exception + * @access public + */ + public function decode($input, $one_time_encoding = false) + { + // Optionally set + if ($one_time_encoding) { + switch ($one_time_encoding) { + case 'utf8': + case 'ucs4_string': + case 'ucs4_array': + break; + default: + throw new InvalidArgumentException('Unknown encoding '.$one_time_encoding); + } + } + // Make sure to drop any newline characters around + $input = trim($input); + + // Negotiate input and try to determine, wether it is a plain string, + // an email address or something like a complete URL + if (strpos($input, '@')) { // Maybe it is an email address + // No no in strict mode + if ($this->_strict_mode) { + throw new InvalidArgumentException('Only simple domain name parts can be handled in strict mode'); + } + list($email_pref, $input) = explode('@', $input, 2); + $arr = explode('.', $input); + foreach ($arr as $k => $v) { + $conv = $this->_decode($v); + if ($conv) $arr[$k] = $conv; + } + $return = $email_pref . '@' . join('.', $arr); + } elseif (preg_match('![:\./]!', $input)) { // Or a complete domain name (with or without paths / parameters) + // No no in strict mode + if ($this->_strict_mode) { + throw new InvalidArgumentException('Only simple domain name parts can be handled in strict mode'); + } + + $parsed = parse_url($input); + if (isset($parsed['host'])) { + $arr = explode('.', $parsed['host']); + foreach ($arr as $k => $v) { + $conv = $this->_decode($v); + if ($conv) $arr[$k] = $conv; + } + $parsed['host'] = join('.', $arr); + if (isset($parsed['scheme'])) { + $parsed['scheme'] .= (strtolower($parsed['scheme']) == 'mailto') ? ':' : '://'; + } + $return = $this->_unparse_url($parsed); + } else { // parse_url seems to have failed, try without it + $arr = explode('.', $input); + foreach ($arr as $k => $v) { + $conv = $this->_decode($v); + if ($conv) $arr[$k] = $conv; + } + $return = join('.', $arr); + } + } else { // Otherwise we consider it being a pure domain name string + $return = $this->_decode($input); + } + // The output is UTF-8 by default, other output formats need conversion here + // If one time encoding is given, use this, else the objects property + switch (($one_time_encoding) ? $one_time_encoding : $this->_api_encoding) { + case 'utf8': + return $return; + break; + case 'ucs4_string': + return $this->_ucs4_to_ucs4_string($this->_utf8_to_ucs4($return)); + break; + case 'ucs4_array': + return $this->_utf8_to_ucs4($return); + break; + default: + throw new InvalidArgumentException('Unsupported output format'); + } + } + + + // {{{ private + /** + * Opposite function to parse_url() + * + * Inspired by code from comments of php.net-documentation for parse_url() + * + * @param array $parts_arr parts (strings) as returned by parse_url() + * + * @return string + * @access private + */ + private function _unparse_url($parts_arr) + { + if (!empty($parts_arr['scheme'])) { + $ret_url = $parts_arr['scheme']; + } + if (!empty($parts_arr['user'])) { + $ret_url .= $parts_arr['user']; + if (!empty($parts_arr['pass'])) { + $ret_url .= ':' . $parts_arr['pass']; + } + $ret_url .= '@'; + } + $ret_url .= $parts_arr['host']; + if (!empty($parts_arr['port'])) { + $ret_url .= ':' . $parts_arr['port']; + } + $ret_url .= $parts_arr['path']; + if (!empty($parts_arr['query'])) { + $ret_url .= '?' . $parts_arr['query']; + } + if (!empty($parts_arr['fragment'])) { + $ret_url .= '#' . $parts_arr['fragment']; + } + return $ret_url; + } + + /** + * The actual encoding algorithm. + * + * @param string $decoded Decoded string which should be encoded + * + * @return string Encoded string + * @throws Exception + * @access private + */ + private function _encode($decoded) + { + // We cannot encode a domain name containing the Punycode prefix + $extract = self::_byteLength($this->_punycode_prefix); + $check_pref = $this->_utf8_to_ucs4($this->_punycode_prefix); + $check_deco = array_slice($decoded, 0, $extract); + + if ($check_pref == $check_deco) { + throw new InvalidArgumentException('This is already a punycode string'); + } + + // We will not try to encode strings consisting of basic code points only + $encodable = false; + foreach ($decoded as $k => $v) { + if ($v > 0x7a) { + $encodable = true; + break; + } + } + if (!$encodable) { + if ($this->_strict_mode) { + throw new InvalidArgumentException('The given string does not contain encodable chars'); + } + + return false; + } + + // Do NAMEPREP + $decoded = $this->_nameprep($decoded); + + $deco_len = count($decoded); + + // Empty array + if (!$deco_len) { + return false; + } + + // How many chars have been consumed + $codecount = 0; + + // Start with the prefix; copy it to output + $encoded = $this->_punycode_prefix; + + $encoded = ''; + // Copy all basic code points to output + for ($i = 0; $i < $deco_len; ++$i) { + $test = $decoded[$i]; + // Will match [0-9a-zA-Z-] + if ((0x2F < $test && $test < 0x40) + || (0x40 < $test && $test < 0x5B) + || (0x60 < $test && $test <= 0x7B) + || (0x2D == $test) + ) { + $encoded .= chr($decoded[$i]); + $codecount++; + } + } + + // All codepoints were basic ones + if ($codecount == $deco_len) { + return $encoded; + } + + // Start with the prefix; copy it to output + $encoded = $this->_punycode_prefix . $encoded; + + // If we have basic code points in output, add an hyphen to the end + if ($codecount) { + $encoded .= '-'; + } + + // Now find and encode all non-basic code points + $is_first = true; + $cur_code = $this->_initial_n; + $bias = $this->_initial_bias; + $delta = 0; + + while ($codecount < $deco_len) { + // Find the smallest code point >= the current code point and + // remember the last ouccrence of it in the input + for ($i = 0, $next_code = $this->_max_ucs; $i < $deco_len; $i++) { + if ($decoded[$i] >= $cur_code && $decoded[$i] <= $next_code) { + $next_code = $decoded[$i]; + } + } + + $delta += ($next_code - $cur_code) * ($codecount + 1); + $cur_code = $next_code; + + // Scan input again and encode all characters whose code point is $cur_code + for ($i = 0; $i < $deco_len; $i++) { + if ($decoded[$i] < $cur_code) { + $delta++; + } else if ($decoded[$i] == $cur_code) { + for ($q = $delta, $k = $this->_base; 1; $k += $this->_base) { + $t = ($k <= $bias)? + $this->_tmin : + (($k >= $bias + $this->_tmax)? $this->_tmax : $k - $bias); + + if ($q < $t) { + break; + } + + $encoded .= $this->_encodeDigit(ceil($t + (($q - $t) % ($this->_base - $t)))); + $q = ($q - $t) / ($this->_base - $t); + } + + $encoded .= $this->_encodeDigit($q); + $bias = $this->_adapt($delta, $codecount + 1, $is_first); + $codecount++; + $delta = 0; + $is_first = false; + } + } + + $delta++; + $cur_code++; + } + + return $encoded; + } + + /** + * The actual decoding algorithm. + * + * @param string $encoded Encoded string which should be decoded + * + * @return string Decoded string + * @throws Exception + * @access private + */ + private function _decode($encoded) + { + // We do need to find the Punycode prefix + if (!preg_match('!^' . preg_quote($this->_punycode_prefix, '!') . '!', $encoded)) { + return false; + } + + $encode_test = preg_replace('!^' . preg_quote($this->_punycode_prefix, '!') . '!', '', $encoded); + + // If nothing left after removing the prefix, it is hopeless + if (!$encode_test) { + return false; + } + + // Find last occurence of the delimiter + $delim_pos = strrpos($encoded, '-'); + + if ($delim_pos > self::_byteLength($this->_punycode_prefix)) { + for ($k = self::_byteLength($this->_punycode_prefix); $k < $delim_pos; ++$k) { + $decoded[] = ord($encoded{$k}); + } + } else { + $decoded = array(); + } + + $deco_len = count($decoded); + $enco_len = self::_byteLength($encoded); + + // Wandering through the strings; init + $is_first = true; + $bias = $this->_initial_bias; + $idx = 0; + $char = $this->_initial_n; + + for ($enco_idx = ($delim_pos)? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len) { + for ($old_idx = $idx, $w = 1, $k = $this->_base; 1 ; $k += $this->_base) { + $digit = $this->_decodeDigit($encoded{$enco_idx++}); + $idx += $digit * $w; + + $t = ($k <= $bias) ? + $this->_tmin : + (($k >= $bias + $this->_tmax)? $this->_tmax : ($k - $bias)); + + if ($digit < $t) { + break; + } + + $w = (int)($w * ($this->_base - $t)); + } + + $bias = $this->_adapt($idx - $old_idx, $deco_len + 1, $is_first); + $is_first = false; + $char += (int) ($idx / ($deco_len + 1)); + $idx %= ($deco_len + 1); + + if ($deco_len > 0) { + // Make room for the decoded char + for ($i = $deco_len; $i > $idx; $i--) { + $decoded[$i] = $decoded[($i - 1)]; + } + } + + $decoded[$idx++] = $char; + } + + return $this->_ucs4_to_utf8($decoded); + } + + /** + * Adapt the bias according to the current code point and position. + * + * @param int $delta ... + * @param int $npoints ... + * @param boolean $is_first ... + * + * @return int + * @access private + */ + private function _adapt($delta, $npoints, $is_first) + { + $delta = (int) ($is_first ? ($delta / $this->_damp) : ($delta / 2)); + $delta += (int) ($delta / $npoints); + + for ($k = 0; $delta > (($this->_base - $this->_tmin) * $this->_tmax) / 2; $k += $this->_base) { + $delta = (int) ($delta / ($this->_base - $this->_tmin)); + } + + return (int) ($k + ($this->_base - $this->_tmin + 1) * $delta / ($delta + $this->_skew)); + } + + /** + * Encoding a certain digit. + * + * @param int $d One digit to encode + * + * @return char Encoded digit + * @access private + */ + private function _encodeDigit($d) + { + return chr($d + 22 + 75 * ($d < 26)); + } + + /** + * Decode a certain digit. + * + * @param char $cp One digit (character) to decode + * + * @return int Decoded digit + * @access private + */ + private function _decodeDigit($cp) + { + $cp = ord($cp); + return ($cp - 48 < 10)? $cp - 22 : (($cp - 65 < 26)? $cp - 65 : (($cp - 97 < 26)? $cp - 97 : $this->_base)); + } + + /** + * Do Nameprep according to RFC3491 and RFC3454. + * + * @param array $input Unicode Characters + * + * @return string Unicode Characters, Nameprep'd + * @throws Exception + * @access private + */ + private function _nameprep($input) + { + $output = array(); + + // Walking through the input array, performing the required steps on each of + // the input chars and putting the result into the output array + // While mapping required chars we apply the cannonical ordering + + foreach ($input as $v) { + // Map to nothing == skip that code point + if (in_array($v, self::$_np_map_nothing)) { + continue; + } + + // Try to find prohibited input + if (in_array($v, self::$_np_prohibit) || in_array($v, self::$_general_prohibited)) { + throw new Net_IDNA2_Exception_Nameprep('Prohibited input U+' . sprintf('%08X', $v)); + } + + foreach (self::$_np_prohibit_ranges as $range) { + if ($range[0] <= $v && $v <= $range[1]) { + throw new Net_IDNA2_Exception_Nameprep('Prohibited input U+' . sprintf('%08X', $v)); + } + } + + // Hangul syllable decomposition + if (0xAC00 <= $v && $v <= 0xD7AF) { + foreach ($this->_hangulDecompose($v) as $out) { + $output[] = $out; + } + } else if (($this->_version == '2003') && isset(self::$_np_replacemaps[$v])) { + // There's a decomposition mapping for that code point + // Decompositions only in version 2003 (original) of IDNA + foreach ($this->_applyCannonicalOrdering(self::$_np_replacemaps[$v]) as $out) { + $output[] = $out; + } + } else { + $output[] = $v; + } + } + + // Combine code points + + $last_class = 0; + $last_starter = 0; + $out_len = count($output); + + for ($i = 0; $i < $out_len; ++$i) { + $class = $this->_getCombiningClass($output[$i]); + + if ((!$last_class || $last_class != $class) && $class) { + // Try to match + $seq_len = $i - $last_starter; + $out = $this->_combine(array_slice($output, $last_starter, $seq_len)); + + // On match: Replace the last starter with the composed character and remove + // the now redundant non-starter(s) + if ($out) { + $output[$last_starter] = $out; + + if (count($out) != $seq_len) { + for ($j = $i + 1; $j < $out_len; ++$j) { + $output[$j - 1] = $output[$j]; + } + + unset($output[$out_len]); + } + + // Rewind the for loop by one, since there can be more possible compositions + $i--; + $out_len--; + $last_class = ($i == $last_starter)? 0 : $this->_getCombiningClass($output[$i - 1]); + + continue; + } + } + + // The current class is 0 + if (!$class) { + $last_starter = $i; + } + + $last_class = $class; + } + + return $output; + } + + /** + * Decomposes a Hangul syllable + * (see http://www.unicode.org/unicode/reports/tr15/#Hangul). + * + * @param integer $char 32bit UCS4 code point + * + * @return array Either Hangul Syllable decomposed or original 32bit + * value as one value array + * @access private + */ + private function _hangulDecompose($char) + { + $sindex = $char - $this->_sbase; + + if ($sindex < 0 || $sindex >= $this->_scount) { + return array($char); + } + + $result = array(); + $T = $this->_tbase + $sindex % $this->_tcount; + $result[] = (int)($this->_lbase + $sindex / $this->_ncount); + $result[] = (int)($this->_vbase + ($sindex % $this->_ncount) / $this->_tcount); + + if ($T != $this->_tbase) { + $result[] = $T; + } + + return $result; + } + + /** + * Ccomposes a Hangul syllable + * (see http://www.unicode.org/unicode/reports/tr15/#Hangul). + * + * @param array $input Decomposed UCS4 sequence + * + * @return array UCS4 sequence with syllables composed + * @access private + */ + private function _hangulCompose($input) + { + $inp_len = count($input); + + if (!$inp_len) { + return array(); + } + + $result = array(); + $last = $input[0]; + $result[] = $last; // copy first char from input to output + + for ($i = 1; $i < $inp_len; ++$i) { + $char = $input[$i]; + + // Find out, wether two current characters from L and V + $lindex = $last - $this->_lbase; + + if (0 <= $lindex && $lindex < $this->_lcount) { + $vindex = $char - $this->_vbase; + + if (0 <= $vindex && $vindex < $this->_vcount) { + // create syllable of form LV + $last = ($this->_sbase + ($lindex * $this->_vcount + $vindex) * $this->_tcount); + $out_off = count($result) - 1; + $result[$out_off] = $last; // reset last + + // discard char + continue; + } + } + + // Find out, wether two current characters are LV and T + $sindex = $last - $this->_sbase; + + if (0 <= $sindex && $sindex < $this->_scount && ($sindex % $this->_tcount) == 0) { + $tindex = $char - $this->_tbase; + + if (0 <= $tindex && $tindex <= $this->_tcount) { + // create syllable of form LVT + $last += $tindex; + $out_off = count($result) - 1; + $result[$out_off] = $last; // reset last + + // discard char + continue; + } + } + + // if neither case was true, just add the character + $last = $char; + $result[] = $char; + } + + return $result; + } + + /** + * Returns the combining class of a certain wide char. + * + * @param integer $char Wide char to check (32bit integer) + * + * @return integer Combining class if found, else 0 + * @access private + */ + private function _getCombiningClass($char) + { + return isset(self::$_np_norm_combcls[$char])? self::$_np_norm_combcls[$char] : 0; + } + + /** + * Apllies the cannonical ordering of a decomposed UCS4 sequence. + * + * @param array $input Decomposed UCS4 sequence + * + * @return array Ordered USC4 sequence + * @access private + */ + private function _applyCannonicalOrdering($input) + { + $swap = true; + $size = count($input); + + while ($swap) { + $swap = false; + $last = $this->_getCombiningClass($input[0]); + + for ($i = 0; $i < $size - 1; ++$i) { + $next = $this->_getCombiningClass($input[$i + 1]); + + if ($next != 0 && $last > $next) { + // Move item leftward until it fits + for ($j = $i + 1; $j > 0; --$j) { + if ($this->_getCombiningClass($input[$j - 1]) <= $next) { + break; + } + + $t = $input[$j]; + $input[$j] = $input[$j - 1]; + $input[$j - 1] = $t; + $swap = 1; + } + + // Reentering the loop looking at the old character again + $next = $last; + } + + $last = $next; + } + } + + return $input; + } + + /** + * Do composition of a sequence of starter and non-starter. + * + * @param array $input UCS4 Decomposed sequence + * + * @return array Ordered USC4 sequence + * @access private + */ + private function _combine($input) + { + $inp_len = count($input); + + // Is it a Hangul syllable? + if (1 != $inp_len) { + $hangul = $this->_hangulCompose($input); + + // This place is probably wrong + if (count($hangul) != $inp_len) { + return $hangul; + } + } + + foreach (self::$_np_replacemaps as $np_src => $np_target) { + if ($np_target[0] != $input[0]) { + continue; + } + + if (count($np_target) != $inp_len) { + continue; + } + + $hit = false; + + foreach ($input as $k2 => $v2) { + if ($v2 == $np_target[$k2]) { + $hit = true; + } else { + $hit = false; + break; + } + } + + if ($hit) { + return $np_src; + } + } + + return false; + } + + /** + * This converts an UTF-8 encoded string to its UCS-4 (array) representation + * By talking about UCS-4 we mean arrays of 32bit integers representing + * each of the "chars". This is due to PHP not being able to handle strings with + * bit depth different from 8. This applies to the reverse method _ucs4_to_utf8(), too. + * The following UTF-8 encodings are supported: + * + * bytes bits representation + * 1 7 0xxxxxxx + * 2 11 110xxxxx 10xxxxxx + * 3 16 1110xxxx 10xxxxxx 10xxxxxx + * 4 21 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + * 5 26 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + * 6 31 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + * + * Each x represents a bit that can be used to store character data. + * + * @param string $input utf8-encoded string + * + * @return array ucs4-encoded array + * @throws Exception + * @access private + */ + private function _utf8_to_ucs4($input) + { + $output = array(); + $out_len = 0; + $inp_len = self::_byteLength($input, '8bit'); + $mode = 'next'; + $test = 'none'; + for ($k = 0; $k < $inp_len; ++$k) { + $v = ord($input{$k}); // Extract byte from input string + + if ($v < 128) { // We found an ASCII char - put into stirng as is + $output[$out_len] = $v; + ++$out_len; + if ('add' == $mode) { + throw new UnexpectedValueException('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k); + } + continue; + } + if ('next' == $mode) { // Try to find the next start byte; determine the width of the Unicode char + $start_byte = $v; + $mode = 'add'; + $test = 'range'; + if ($v >> 5 == 6) { // &110xxxxx 10xxxxx + $next_byte = 0; // Tells, how many times subsequent bitmasks must rotate 6bits to the left + $v = ($v - 192) << 6; + } elseif ($v >> 4 == 14) { // &1110xxxx 10xxxxxx 10xxxxxx + $next_byte = 1; + $v = ($v - 224) << 12; + } elseif ($v >> 3 == 30) { // &11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + $next_byte = 2; + $v = ($v - 240) << 18; + } elseif ($v >> 2 == 62) { // &111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + $next_byte = 3; + $v = ($v - 248) << 24; + } elseif ($v >> 1 == 126) { // &1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + $next_byte = 4; + $v = ($v - 252) << 30; + } else { + throw new UnexpectedValueException('This might be UTF-8, but I don\'t understand it at byte '.$k); + } + if ('add' == $mode) { + $output[$out_len] = (int) $v; + ++$out_len; + continue; + } + } + if ('add' == $mode) { + if (!$this->_allow_overlong && $test == 'range') { + $test = 'none'; + if (($v < 0xA0 && $start_byte == 0xE0) || ($v < 0x90 && $start_byte == 0xF0) || ($v > 0x8F && $start_byte == 0xF4)) { + throw new OutOfRangeException('Bogus UTF-8 character detected (out of legal range) at byte '.$k); + } + } + if ($v >> 6 == 2) { // Bit mask must be 10xxxxxx + $v = ($v - 128) << ($next_byte * 6); + $output[($out_len - 1)] += $v; + --$next_byte; + } else { + throw new UnexpectedValueException('Conversion from UTF-8 to UCS-4 failed: malformed input at byte '.$k); + } + if ($next_byte < 0) { + $mode = 'next'; + } + } + } // for + return $output; + } + + /** + * Convert UCS-4 array into UTF-8 string + * + * @param array $input ucs4-encoded array + * + * @return string utf8-encoded string + * @throws Exception + * @access private + */ + private function _ucs4_to_utf8($input) + { + $output = ''; + + foreach ($input as $v) { + // $v = ord($v); + + if ($v < 128) { + // 7bit are transferred literally + $output .= chr($v); + } else if ($v < 1 << 11) { + // 2 bytes + $output .= chr(192 + ($v >> 6)) + . chr(128 + ($v & 63)); + } else if ($v < 1 << 16) { + // 3 bytes + $output .= chr(224 + ($v >> 12)) + . chr(128 + (($v >> 6) & 63)) + . chr(128 + ($v & 63)); + } else if ($v < 1 << 21) { + // 4 bytes + $output .= chr(240 + ($v >> 18)) + . chr(128 + (($v >> 12) & 63)) + . chr(128 + (($v >> 6) & 63)) + . chr(128 + ($v & 63)); + } else if ($v < 1 << 26) { + // 5 bytes + $output .= chr(248 + ($v >> 24)) + . chr(128 + (($v >> 18) & 63)) + . chr(128 + (($v >> 12) & 63)) + . chr(128 + (($v >> 6) & 63)) + . chr(128 + ($v & 63)); + } else if ($v < 1 << 31) { + // 6 bytes + $output .= chr(252 + ($v >> 30)) + . chr(128 + (($v >> 24) & 63)) + . chr(128 + (($v >> 18) & 63)) + . chr(128 + (($v >> 12) & 63)) + . chr(128 + (($v >> 6) & 63)) + . chr(128 + ($v & 63)); + } else { + throw new UnexpectedValueException('Conversion from UCS-4 to UTF-8 failed: malformed input'); + } + } + + return $output; + } + + /** + * Convert UCS-4 array into UCS-4 string + * + * @param array $input ucs4-encoded array + * + * @return string ucs4-encoded string + * @throws Exception + * @access private + */ + private function _ucs4_to_ucs4_string($input) + { + $output = ''; + // Take array values and split output to 4 bytes per value + // The bit mask is 255, which reads &11111111 + foreach ($input as $v) { + $output .= ($v & (255 << 24) >> 24) . ($v & (255 << 16) >> 16) . ($v & (255 << 8) >> 8) . ($v & 255); + } + return $output; + } + + /** + * Convert UCS-4 string into UCS-4 array + * + * @param string $input ucs4-encoded string + * + * @return array ucs4-encoded array + * @throws InvalidArgumentException + * @access private + */ + private function _ucs4_string_to_ucs4($input) + { + $output = array(); + + $inp_len = self::_byteLength($input); + // Input length must be dividable by 4 + if ($inp_len % 4) { + throw new InvalidArgumentException('Input UCS4 string is broken'); + } + + // Empty input - return empty output + if (!$inp_len) { + return $output; + } + + for ($i = 0, $out_len = -1; $i < $inp_len; ++$i) { + // Increment output position every 4 input bytes + if (!$i % 4) { + $out_len++; + $output[$out_len] = 0; + } + $output[$out_len] += ord($input{$i}) << (8 * (3 - ($i % 4) ) ); + } + return $output; + } + + /** + * Echo hex representation of UCS4 sequence. + * + * @param array $input UCS4 sequence + * @param boolean $include_bit Include bitmask in output + * + * @return void + * @static + * @access private + */ + private static function _showHex($input, $include_bit = false) + { + foreach ($input as $k => $v) { + echo '[', $k, '] => ', sprintf('%X', $v); + + if ($include_bit) { + echo ' (', Net_IDNA2::_showBitmask($v), ')'; + } + + echo "\n"; + } + } + + /** + * Gives you a bit representation of given Byte (8 bits), Word (16 bits) or DWord (32 bits) + * Output width is automagically determined + * + * @param int $octet ... + * + * @return string Bitmask-representation + * @static + * @access private + */ + private static function _showBitmask($octet) + { + if ($octet >= (1 << 16)) { + $w = 31; + } else if ($octet >= (1 << 8)) { + $w = 15; + } else { + $w = 7; + } + + $return = ''; + + for ($i = $w; $i > -1; $i--) { + $return .= ($octet & (1 << $i))? '1' : '0'; + } + + return $return; + } + + /** + * Gets the length of a string in bytes even if mbstring function + * overloading is turned on + * + * @param string $string the string for which to get the length. + * + * @return integer the length of the string in bytes. + * + * @see Net_IDNA2::$_mb_string_overload + */ + private static function _byteLength($string) + { + if (self::$_mb_string_overload) { + return mb_strlen($string, '8bit'); + } + return strlen((binary)$string); + } + + // }}}} + + // {{{ factory + /** + * Attempts to return a concrete IDNA instance for either php4 or php5. + * + * @param array $params Set of paramaters + * + * @return Net_IDNA2 + * @access public + */ + function getInstance($params = array()) + { + return new Net_IDNA2($params); + } + // }}} + + // {{{ singleton + /** + * Attempts to return a concrete IDNA instance for either php4 or php5, + * only creating a new instance if no IDNA instance with the same + * parameters currently exists. + * + * @param array $params Set of paramaters + * + * @return object Net_IDNA2 + * @access public + */ + function singleton($params = array()) + { + static $instances; + if (!isset($instances)) { + $instances = array(); + } + + $signature = serialize($params); + if (!isset($instances[$signature])) { + $instances[$signature] = Net_IDNA2::getInstance($params); + } + + return $instances[$signature]; + } + // }}} +} + +?> diff --git a/lib/ext/Net/IDNA2/Exception.php b/lib/ext/Net/IDNA2/Exception.php new file mode 100644 index 0000000..72cb1ae --- /dev/null +++ b/lib/ext/Net/IDNA2/Exception.php @@ -0,0 +1,4 @@ +<?php +class Net_IDNA2_Exception extends Exception +{ +} diff --git a/lib/ext/Net/IDNA2/Exception/Nameprep.php b/lib/ext/Net/IDNA2/Exception/Nameprep.php new file mode 100644 index 0000000..44cbd6b --- /dev/null +++ b/lib/ext/Net/IDNA2/Exception/Nameprep.php @@ -0,0 +1,6 @@ +<?php +require_once 'Net/IDNA2/Exception.php'; + +class Net_IDNA2_Exception_Nameprep extends Net_IDNA2_Exception +{ +} diff --git a/lib/ext/Net/SMTP.php b/lib/ext/Net/SMTP.php new file mode 100644 index 0000000..4e04f91 --- /dev/null +++ b/lib/ext/Net/SMTP.php @@ -0,0 +1,1342 @@ +<?php +/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------+ +// | PHP Version 4 | +// +----------------------------------------------------------------------+ +// | Copyright (c) 1997-2003 The PHP Group | +// +----------------------------------------------------------------------+ +// | This source file is subject to version 2.02 of the PHP license, | +// | that is bundled with this package in the file LICENSE, and is | +// | available at through the world-wide-web at | +// | http://www.php.net/license/2_02.txt. | +// | If you did not receive a copy of the PHP license and are unable to | +// | obtain it through the world-wide-web, please send a note to | +// | license@php.net so we can mail you a copy immediately. | +// +----------------------------------------------------------------------+ +// | Authors: Chuck Hagenbuch <chuck@horde.org> | +// | Jon Parise <jon@php.net> | +// | Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> | +// +----------------------------------------------------------------------+ +// +// $Id$ + +require_once 'PEAR.php'; +require_once 'Net/Socket.php'; + +/** + * Provides an implementation of the SMTP protocol using PEAR's + * Net_Socket:: class. + * + * @package Net_SMTP + * @author Chuck Hagenbuch <chuck@horde.org> + * @author Jon Parise <jon@php.net> + * @author Damian Alejandro Fernandez Sosa <damlists@cnba.uba.ar> + * + * @example basic.php A basic implementation of the Net_SMTP package. + */ +class Net_SMTP +{ + /** + * The server to connect to. + * @var string + * @access public + */ + var $host = 'localhost'; + + /** + * The port to connect to. + * @var int + * @access public + */ + var $port = 25; + + /** + * The value to give when sending EHLO or HELO. + * @var string + * @access public + */ + var $localhost = 'localhost'; + + /** + * List of supported authentication methods, in preferential order. + * @var array + * @access public + */ + var $auth_methods = array(); + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP + * server supports it. + * + * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(), + * somlFrom() and samlFrom() do not wait for a response from the + * SMTP server but return immediately. + * + * @var bool + * @access public + */ + var $pipelining = false; + + /** + * Number of pipelined commands. + * @var int + * @access private + */ + var $_pipelined_commands = 0; + + /** + * Should debugging output be enabled? + * @var boolean + * @access private + */ + var $_debug = false; + + /** + * Debug output handler. + * @var callback + * @access private + */ + var $_debug_handler = null; + + /** + * The socket resource being used to connect to the SMTP server. + * @var resource + * @access private + */ + var $_socket = null; + + /** + * Array of socket options that will be passed to Net_Socket::connect(). + * @see stream_context_create() + * @var array + * @access private + */ + var $_socket_options = null; + + /** + * The socket I/O timeout value in seconds. + * @var int + * @access private + */ + var $_timeout = 0; + + /** + * The most recent server response code. + * @var int + * @access private + */ + var $_code = -1; + + /** + * The most recent server response arguments. + * @var array + * @access private + */ + var $_arguments = array(); + + /** + * Stores the SMTP server's greeting string. + * @var string + * @access private + */ + var $_greeting = null; + + /** + * Stores detected features of the SMTP server. + * @var array + * @access private + */ + var $_esmtp = array(); + + /** + * Instantiates a new Net_SMTP object, overriding any defaults + * with parameters that are passed in. + * + * If you have SSL support in PHP, you can connect to a server + * over SSL using an 'ssl://' prefix: + * + * // 465 is a common smtps port. + * $smtp = new Net_SMTP('ssl://mail.host.com', 465); + * $smtp->connect(); + * + * @param string $host The server to connect to. + * @param integer $port The port to connect to. + * @param string $localhost The value to give when sending EHLO or HELO. + * @param boolean $pipeling Use SMTP command pipelining + * @param integer $timeout Socket I/O timeout in seconds. + * @param array $socket_options Socket stream_context_create() options. + * + * @access public + * @since 1.0 + */ + function Net_SMTP($host = null, $port = null, $localhost = null, + $pipelining = false, $timeout = 0, $socket_options = null) + { + if (isset($host)) { + $this->host = $host; + } + if (isset($port)) { + $this->port = $port; + } + if (isset($localhost)) { + $this->localhost = $localhost; + } + $this->pipelining = $pipelining; + + $this->_socket = new Net_Socket(); + $this->_socket_options = $socket_options; + $this->_timeout = $timeout; + + /* Include the Auth_SASL package. If the package is available, we + * enable the authentication methods that depend upon it. */ + if ((@include_once 'Auth/SASL.php') === true) { + $this->setAuthMethod('CRAM-MD5', array($this, '_authCram_MD5')); + $this->setAuthMethod('DIGEST-MD5', array($this, '_authDigest_MD5')); + } + + /* These standard authentication methods are always available. */ + $this->setAuthMethod('LOGIN', array($this, '_authLogin'), false); + $this->setAuthMethod('PLAIN', array($this, '_authPlain'), false); + } + + /** + * Set the socket I/O timeout value in seconds plus microseconds. + * + * @param integer $seconds Timeout value in seconds. + * @param integer $microseconds Additional value in microseconds. + * + * @access public + * @since 1.5.0 + */ + function setTimeout($seconds, $microseconds = 0) { + return $this->_socket->setTimeout($seconds, $microseconds); + } + + /** + * Set the value of the debugging flag. + * + * @param boolean $debug New value for the debugging flag. + * + * @access public + * @since 1.1.0 + */ + function setDebug($debug, $handler = null) + { + $this->_debug = $debug; + $this->_debug_handler = $handler; + } + + /** + * Write the given debug text to the current debug output handler. + * + * @param string $message Debug mesage text. + * + * @access private + * @since 1.3.3 + */ + function _debug($message) + { + if ($this->_debug) { + if ($this->_debug_handler) { + call_user_func_array($this->_debug_handler, + array(&$this, $message)); + } else { + echo "DEBUG: $message\n"; + } + } + } + + /** + * Send the given string of data to the server. + * + * @param string $data The string of data to send. + * + * @return mixed The number of bytes that were actually written, + * or a PEAR_Error object on failure. + * + * @access private + * @since 1.1.0 + */ + function _send($data) + { + $this->_debug("Send: $data"); + + $result = $this->_socket->write($data); + if (!$result || PEAR::isError($result)) { + $msg = ($result) ? $result->getMessage() : "unknown error"; + return PEAR::raiseError("Failed to write to socket: $msg", + null, PEAR_ERROR_RETURN); + } + + return $result; + } + + /** + * Send a command to the server with an optional string of + * arguments. A carriage return / linefeed (CRLF) sequence will + * be appended to each command string before it is sent to the + * SMTP server - an error will be thrown if the command string + * already contains any newline characters. Use _send() for + * commands that must contain newlines. + * + * @param string $command The SMTP command to send to the server. + * @param string $args A string of optional arguments to append + * to the command. + * + * @return mixed The result of the _send() call. + * + * @access private + * @since 1.1.0 + */ + function _put($command, $args = '') + { + if (!empty($args)) { + $command .= ' ' . $args; + } + + if (strcspn($command, "\r\n") !== strlen($command)) { + return PEAR::raiseError('Commands cannot contain newlines', + null, PEAR_ERROR_RETURN); + } + + return $this->_send($command . "\r\n"); + } + + /** + * Read a reply from the SMTP server. The reply consists of a response + * code and a response message. + * + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * @param bool $later Do not parse the response now, but wait + * until the last command in the pipelined + * command group + * + * @return mixed True if the server returned a valid response code or + * a PEAR_Error object is an error condition is reached. + * + * @access private + * @since 1.1.0 + * + * @see getResponse + */ + function _parseResponse($valid, $later = false) + { + $this->_code = -1; + $this->_arguments = array(); + + if ($later) { + $this->_pipelined_commands++; + return true; + } + + for ($i = 0; $i <= $this->_pipelined_commands; $i++) { + while ($line = $this->_socket->readLine()) { + $this->_debug("Recv: $line"); + + /* If we receive an empty line, the connection was closed. */ + if (empty($line)) { + $this->disconnect(); + return PEAR::raiseError('Connection was closed', + null, PEAR_ERROR_RETURN); + } + + /* Read the code and store the rest in the arguments array. */ + $code = substr($line, 0, 3); + $this->_arguments[] = trim(substr($line, 4)); + + /* Check the syntax of the response code. */ + if (is_numeric($code)) { + $this->_code = (int)$code; + } else { + $this->_code = -1; + break; + } + + /* If this is not a multiline response, we're done. */ + if (substr($line, 3, 1) != '-') { + break; + } + } + } + + $this->_pipelined_commands = 0; + + /* Compare the server's response code with the valid code/codes. */ + if (is_int($valid) && ($this->_code === $valid)) { + return true; + } elseif (is_array($valid) && in_array($this->_code, $valid, true)) { + return true; + } + + return PEAR::raiseError('Invalid response code received from server', + $this->_code, PEAR_ERROR_RETURN); + } + + /** + * Issue an SMTP command and verify its response. + * + * @param string $command The SMTP command string or data. + * @param mixed $valid The set of valid response codes. These + * may be specified as an array of integer + * values or as a single integer value. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function command($command, $valid) + { + if (PEAR::isError($error = $this->_put($command))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse($valid))) { + return $error; + } + + return true; + } + + /** + * Return a 2-tuple containing the last response from the SMTP server. + * + * @return array A two-element array: the first element contains the + * response code as an integer and the second element + * contains the response's arguments as a string. + * + * @access public + * @since 1.1.0 + */ + function getResponse() + { + return array($this->_code, join("\n", $this->_arguments)); + } + + /** + * Return the SMTP server's greeting string. + * + * @return string A string containing the greeting string, or null if a + * greeting has not been received. + * + * @access public + * @since 1.3.3 + */ + function getGreeting() + { + return $this->_greeting; + } + + /** + * Attempt to connect to the SMTP server. + * + * @param int $timeout The timeout value (in seconds) for the + * socket connection attempt. + * @param bool $persistent Should a persistent socket connection + * be used? + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function connect($timeout = null, $persistent = false) + { + $this->_greeting = null; + $result = $this->_socket->connect($this->host, $this->port, + $persistent, $timeout, + $this->_socket_options); + if (PEAR::isError($result)) { + return PEAR::raiseError('Failed to connect socket: ' . + $result->getMessage()); + } + + /* + * Now that we're connected, reset the socket's timeout value for + * future I/O operations. This allows us to have different socket + * timeout values for the initial connection (our $timeout parameter) + * and all other socket operations. + */ + if ($this->_timeout > 0) { + if (PEAR::isError($error = $this->setTimeout($this->_timeout))) { + return $error; + } + } + + if (PEAR::isError($error = $this->_parseResponse(220))) { + return $error; + } + + /* Extract and store a copy of the server's greeting string. */ + list(, $this->_greeting) = $this->getResponse(); + + if (PEAR::isError($error = $this->_negotiate())) { + return $error; + } + + return true; + } + + /** + * Attempt to disconnect from the SMTP server. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function disconnect() + { + if (PEAR::isError($error = $this->_put('QUIT'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(221))) { + return $error; + } + if (PEAR::isError($error = $this->_socket->disconnect())) { + return PEAR::raiseError('Failed to disconnect socket: ' . + $error->getMessage()); + } + + return true; + } + + /** + * Attempt to send the EHLO command and obtain a list of ESMTP + * extensions available, and failing that just send HELO. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access private + * @since 1.1.0 + */ + function _negotiate() + { + if (PEAR::isError($error = $this->_put('EHLO', $this->localhost))) { + return $error; + } + + if (PEAR::isError($this->_parseResponse(250))) { + /* If we receive a 503 response, we're already authenticated. */ + if ($this->_code === 503) { + return true; + } + + /* If the EHLO failed, try the simpler HELO command. */ + if (PEAR::isError($error = $this->_put('HELO', $this->localhost))) { + return $error; + } + if (PEAR::isError($this->_parseResponse(250))) { + return PEAR::raiseError('HELO was not accepted: ', $this->_code, + PEAR_ERROR_RETURN); + } + + return true; + } + + foreach ($this->_arguments as $argument) { + $verb = strtok($argument, ' '); + $arguments = substr($argument, strlen($verb) + 1, + strlen($argument) - strlen($verb) - 1); + $this->_esmtp[$verb] = $arguments; + } + + if (!isset($this->_esmtp['PIPELINING'])) { + $this->pipelining = false; + } + + return true; + } + + /** + * Returns the name of the best authentication method that the server + * has advertised. + * + * @return mixed Returns a string containing the name of the best + * supported authentication method or a PEAR_Error object + * if a failure condition is encountered. + * @access private + * @since 1.1.0 + */ + function _getBestAuthMethod() + { + $available_methods = explode(' ', $this->_esmtp['AUTH']); + + foreach ($this->auth_methods as $method => $callback) { + if (in_array($method, $available_methods)) { + return $method; + } + } + + return PEAR::raiseError('No supported authentication methods', + null, PEAR_ERROR_RETURN); + } + + /** + * Attempt to do SMTP authentication. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The requested authentication method. If none is + * specified, the best supported method will be used. + * @param bool Flag indicating whether or not TLS should be attempted. + * @param string An optional authorization identifier. If specified, this + * identifier will be used as the authorization proxy. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function auth($uid, $pwd , $method = '', $tls = true, $authz = '') + { + /* We can only attempt a TLS connection if one has been requested, + * we're running PHP 5.1.0 or later, have access to the OpenSSL + * extension, are connected to an SMTP server which supports the + * STARTTLS extension, and aren't already connected over a secure + * (SSL) socket connection. */ + if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') && + extension_loaded('openssl') && isset($this->_esmtp['STARTTLS']) && + strncasecmp($this->host, 'ssl://', 6) !== 0) { + /* Start the TLS connection attempt. */ + if (PEAR::isError($result = $this->_put('STARTTLS'))) { + return $result; + } + if (PEAR::isError($result = $this->_parseResponse(220))) { + return $result; + } + if (PEAR::isError($result = $this->_socket->enableCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT))) { + return $result; + } elseif ($result !== true) { + return PEAR::raiseError('STARTTLS failed'); + } + + /* Send EHLO again to recieve the AUTH string from the + * SMTP server. */ + $this->_negotiate(); + } + + if (empty($this->_esmtp['AUTH'])) { + return PEAR::raiseError('SMTP server does not support authentication'); + } + + /* If no method has been specified, get the name of the best + * supported method advertised by the SMTP server. */ + if (empty($method)) { + if (PEAR::isError($method = $this->_getBestAuthMethod())) { + /* Return the PEAR_Error object from _getBestAuthMethod(). */ + return $method; + } + } else { + $method = strtoupper($method); + if (!array_key_exists($method, $this->auth_methods)) { + return PEAR::raiseError("$method is not a supported authentication method"); + } + } + + if (!isset($this->auth_methods[$method])) { + return PEAR::raiseError("$method is not a supported authentication method"); + } + + if (!is_callable($this->auth_methods[$method], false)) { + return PEAR::raiseError("$method authentication method cannot be called"); + } + + if (is_array($this->auth_methods[$method])) { + list($object, $method) = $this->auth_methods[$method]; + $result = $object->{$method}($uid, $pwd, $authz, $this); + } else { + $func = $this->auth_methods[$method]; + $result = $func($uid, $pwd, $authz, $this); + } + + /* If an error was encountered, return the PEAR_Error object. */ + if (PEAR::isError($result)) { + return $result; + } + + return true; + } + + /** + * Add a new authentication method. + * + * @param string The authentication method name (e.g. 'PLAIN') + * @param mixed The authentication callback (given as the name of a + * function or as an (object, method name) array). + * @param bool Should the new method be prepended to the list of + * available methods? This is the default behavior, + * giving the new method the highest priority. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function setAuthMethod($name, $callback, $prepend = true) + { + if (!is_string($name)) { + return PEAR::raiseError('Method name is not a string'); + } + + if (!is_string($callback) && !is_array($callback)) { + return PEAR::raiseError('Method callback must be string or array'); + } + + if (is_array($callback)) { + if (!is_object($callback[0]) || !is_string($callback[1])) + return PEAR::raiseError('Bad mMethod callback array'); + } + + if ($prepend) { + $this->auth_methods = array_merge(array($name => $callback), + $this->auth_methods); + } else { + $this->auth_methods[$name] = $callback; + } + + return true; + } + + /** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authDigest_MD5($uid, $pwd, $authz = '') + { + if (PEAR::isError($error = $this->_put('AUTH', 'DIGEST-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $digest = &Auth_SASL::factory('digestmd5'); + $auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, + $this->host, "smtp", + $authz)); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + /* We don't use the protocol's third step because SMTP doesn't + * allow subsequent authentication, so we just silently ignore + * it. */ + if (PEAR::isError($error = $this->_put(''))) { + return $error; + } + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the CRAM-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authCRAM_MD5($uid, $pwd, $authz = '') + { + if (PEAR::isError($error = $this->_put('AUTH', 'CRAM-MD5'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $challenge = base64_decode($this->_arguments[0]); + $cram = &Auth_SASL::factory('crammd5'); + $auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge)); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + } + + /** + * Authenticates the user using the LOGIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authLogin($uid, $pwd, $authz = '') + { + if (PEAR::isError($error = $this->_put('AUTH', 'LOGIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($uid)))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + return $error; + } + + if (PEAR::isError($error = $this->_put(base64_encode($pwd)))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Authenticates the user using the PLAIN method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access private + * @since 1.1.0 + */ + function _authPlain($uid, $pwd, $authz = '') + { + if (PEAR::isError($error = $this->_put('AUTH', 'PLAIN'))) { + return $error; + } + /* 334: Continue authentication request */ + if (PEAR::isError($error = $this->_parseResponse(334))) { + /* 503: Error: already authenticated */ + if ($this->_code === 503) { + return true; + } + return $error; + } + + $auth_str = base64_encode($authz . chr(0) . $uid . chr(0) . $pwd); + + if (PEAR::isError($error = $this->_put($auth_str))) { + return $error; + } + + /* 235: Authentication successful */ + if (PEAR::isError($error = $this->_parseResponse(235))) { + return $error; + } + + return true; + } + + /** + * Send the HELO command. + * + * @param string The domain name to say we are. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function helo($domain) + { + if (PEAR::isError($error = $this->_put('HELO', $domain))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Return the list of SMTP service extensions advertised by the server. + * + * @return array The list of SMTP service extensions. + * @access public + * @since 1.3 + */ + function getServiceExtensions() + { + return $this->_esmtp; + } + + /** + * Send the MAIL FROM: command. + * + * @param string $sender The sender (reverse path) to set. + * @param string $params String containing additional MAIL parameters, + * such as the NOTIFY flags defined by RFC 1891 + * or the VERP protocol. + * + * If $params is an array, only the 'verp' option + * is supported. If 'verp' is true, the XVERP + * parameter is appended to the MAIL command. If + * the 'verp' value is a string, the full + * XVERP=value parameter is appended. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function mailFrom($sender, $params = null) + { + $args = "FROM:<$sender>"; + + /* Support the deprecated array form of $params. */ + if (is_array($params) && isset($params['verp'])) { + /* XVERP */ + if ($params['verp'] === true) { + $args .= ' XVERP'; + + /* XVERP=something */ + } elseif (trim($params['verp'])) { + $args .= ' XVERP=' . $params['verp']; + } + } elseif (is_string($params) && !empty($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('MAIL', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the RCPT TO: command. + * + * @param string $recipient The recipient (forward path) to add. + * @param string $params String containing additional RCPT parameters, + * such as the NOTIFY flags defined by RFC 1891. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + */ + function rcptTo($recipient, $params = null) + { + $args = "TO:<$recipient>"; + if (is_string($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('RCPT', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 251), $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Quote the data so that it meets SMTP standards. + * + * This is provided as a separate public function to facilitate + * easier overloading for the cases where it is desirable to + * customize the quoting behavior. + * + * @param string $data The message text to quote. The string must be passed + * by reference, and the text will be modified in place. + * + * @access public + * @since 1.2 + */ + function quotedata(&$data) + { + /* Change Unix (\n) and Mac (\r) linefeeds into + * Internet-standard CRLF (\r\n) linefeeds. */ + $data = preg_replace(array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data); + + /* Because a single leading period (.) signifies an end to the + * data, legitimate leading periods need to be "doubled" + * (e.g. '..'). */ + $data = str_replace("\n.", "\n..", $data); + } + + /** + * Send the DATA command. + * + * @param mixed $data The message data, either as a string or an open + * file resource. + * @param string $headers The message headers. If $headers is provided, + * $data is assumed to contain only body data. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function data($data, $headers = null) + { + /* Verify that $data is a supported type. */ + if (!is_string($data) && !is_resource($data)) { + return PEAR::raiseError('Expected a string or file resource'); + } + + /* Start by considering the size of the optional headers string. We + * also account for the addition 4 character "\r\n\r\n" separator + * sequence. */ + $size = (is_null($headers)) ? 0 : strlen($headers) + 4; + + if (is_resource($data)) { + $stat = fstat($data); + if ($stat === false) { + return PEAR::raiseError('Failed to get file size'); + } + $size += $stat['size']; + } else { + $size += strlen($data); + } + + /* RFC 1870, section 3, subsection 3 states "a value of zero indicates + * that no fixed maximum message size is in force". Furthermore, it + * says that if "the parameter is omitted no information is conveyed + * about the server's fixed maximum message size". */ + $limit = (isset($this->_esmtp['SIZE'])) ? $this->_esmtp['SIZE'] : 0; + if ($limit > 0 && $size >= $limit) { + $this->disconnect(); + return PEAR::raiseError('Message size exceeds server limit'); + } + + /* Initiate the DATA command. */ + if (PEAR::isError($error = $this->_put('DATA'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(354))) { + return $error; + } + + /* If we have a separate headers string, send it first. */ + if (!is_null($headers)) { + $this->quotedata($headers); + if (PEAR::isError($result = $this->_send($headers . "\r\n\r\n"))) { + return $result; + } + } + + /* Now we can send the message body data. */ + if (is_resource($data)) { + /* Stream the contents of the file resource out over our socket + * connection, line by line. Each line must be run through the + * quoting routine. */ + while (strlen($line = fread($data, 8192)) > 0) { + /* If the last character is an newline, we need to grab the + * next character to check to see if it is a period. */ + while (!feof($data)) { + $char = fread($data, 1); + $line .= $char; + if ($char != "\n") { + break; + } + } + $this->quotedata($line); + if (PEAR::isError($result = $this->_send($line))) { + return $result; + } + } + } else { + /* + * Break up the data by sending one chunk (up to 512k) at a time. + * This approach reduces our peak memory usage. + */ + for ($offset = 0; $offset < $size;) { + $end = $offset + 512000; + + /* + * Ensure we don't read beyond our data size or span multiple + * lines. quotedata() can't properly handle character data + * that's split across two line break boundaries. + */ + if ($end >= $size) { + $end = $size; + } else { + for (; $end < $size; $end++) { + if ($data[$end] != "\n") { + break; + } + } + } + + /* Extract our chunk and run it through the quoting routine. */ + $chunk = substr($data, $offset, $end - $offset); + $this->quotedata($chunk); + + /* If we run into a problem along the way, abort. */ + if (PEAR::isError($result = $this->_send($chunk))) { + return $result; + } + + /* Advance the offset to the end of this chunk. */ + $offset = $end; + } + } + + /* Finally, send the DATA terminator sequence. */ + if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { + return $result; + } + + /* Verify that the data was successfully received by the server. */ + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the SEND FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function sendFrom($path) + { + if (PEAR::isError($error = $this->_put('SEND', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for sendFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function send_from($path) + { + return sendFrom($path); + } + + /** + * Send the SOML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function somlFrom($path) + { + if (PEAR::isError($error = $this->_put('SOML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for somlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function soml_from($path) + { + return somlFrom($path); + } + + /** + * Send the SAML FROM: command. + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.2.6 + */ + function samlFrom($path) + { + if (PEAR::isError($error = $this->_put('SAML', "FROM:<$path>"))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility wrapper for samlFrom(). + * + * @param string The reverse path to send. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * + * @access public + * @since 1.0 + * @deprecated 1.2.6 + */ + function saml_from($path) + { + return samlFrom($path); + } + + /** + * Send the RSET command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function rset() + { + if (PEAR::isError($error = $this->_put('RSET'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + return $error; + } + + return true; + } + + /** + * Send the VRFY command. + * + * @param string The string to verify + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function vrfy($string) + { + /* Note: 251 is also a valid response code */ + if (PEAR::isError($error = $this->_put('VRFY', $string))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(array(250, 252)))) { + return $error; + } + + return true; + } + + /** + * Send the NOOP command. + * + * @return mixed Returns a PEAR_Error with an error message on any + * kind of failure, or true on success. + * @access public + * @since 1.0 + */ + function noop() + { + if (PEAR::isError($error = $this->_put('NOOP'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250))) { + return $error; + } + + return true; + } + + /** + * Backwards-compatibility method. identifySender()'s functionality is + * now handled internally. + * + * @return boolean This method always return true. + * + * @access public + * @since 1.0 + */ + function identifySender() + { + return true; + } + +} diff --git a/lib/ext/Net/Socket.php b/lib/ext/Net/Socket.php new file mode 100644 index 0000000..dd1047c --- /dev/null +++ b/lib/ext/Net/Socket.php @@ -0,0 +1,653 @@ +<?php +/** + * Net_Socket + * + * PHP Version 4 + * + * Copyright (c) 1997-2003 The PHP Group + * + * This source file is subject to version 2.0 of the PHP license, + * that is bundled with this package in the file LICENSE, and is + * available at through the world-wide-web at + * http://www.php.net/license/2_02.txt. + * If you did not receive a copy of the PHP license and are unable to + * obtain it through the world-wide-web, please send a note to + * license@php.net so we can mail you a copy immediately. + * + * Authors: Stig Bakken <ssb@php.net> + * Chuck Hagenbuch <chuck@horde.org> + * + * @category Net + * @package Net_Socket + * @author Stig Bakken <ssb@php.net> + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @version CVS: $Id$ + * @link http://pear.php.net/packages/Net_Socket + */ + +require_once 'PEAR.php'; + +define('NET_SOCKET_READ', 1); +define('NET_SOCKET_WRITE', 2); +define('NET_SOCKET_ERROR', 4); + +/** + * Generalized Socket class. + * + * @category Net + * @package Net_Socket + * @author Stig Bakken <ssb@php.net> + * @author Chuck Hagenbuch <chuck@horde.org> + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @link http://pear.php.net/packages/Net_Socket + */ +class Net_Socket extends PEAR +{ + /** + * Socket file pointer. + * @var resource $fp + */ + var $fp = null; + + /** + * Whether the socket is blocking. Defaults to true. + * @var boolean $blocking + */ + var $blocking = true; + + /** + * Whether the socket is persistent. Defaults to false. + * @var boolean $persistent + */ + var $persistent = false; + + /** + * The IP address to connect to. + * @var string $addr + */ + var $addr = ''; + + /** + * The port number to connect to. + * @var integer $port + */ + var $port = 0; + + /** + * Number of seconds to wait on socket connections before assuming + * there's no more data. Defaults to no timeout. + * @var integer $timeout + */ + var $timeout = false; + + /** + * Number of bytes to read at a time in readLine() and + * readAll(). Defaults to 2048. + * @var integer $lineLength + */ + var $lineLength = 2048; + + /** + * The string to use as a newline terminator. Usually "\r\n" or "\n". + * @var string $newline + */ + var $newline = "\r\n"; + + /** + * Connect to the specified port. If called when the socket is + * already connected, it disconnects and connects again. + * + * @param string $addr IP address or host name. + * @param integer $port TCP port number. + * @param boolean $persistent (optional) Whether the connection is + * persistent (kept open between requests + * by the web server). + * @param integer $timeout (optional) How long to wait for data. + * @param array $options See options for stream_context_create. + * + * @access public + * + * @return boolean | PEAR_Error True on success or a PEAR_Error on failure. + */ + function connect($addr, $port = 0, $persistent = null, + $timeout = null, $options = null) + { + if (is_resource($this->fp)) { + @fclose($this->fp); + $this->fp = null; + } + + if (!$addr) { + return $this->raiseError('$addr cannot be empty'); + } elseif (strspn($addr, '.0123456789') == strlen($addr) || + strstr($addr, '/') !== false) { + $this->addr = $addr; + } else { + $this->addr = @gethostbyname($addr); + } + + $this->port = $port % 65536; + + if ($persistent !== null) { + $this->persistent = $persistent; + } + + if ($timeout !== null) { + $this->timeout = $timeout; + } + + $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen'; + $errno = 0; + $errstr = ''; + + $old_track_errors = @ini_set('track_errors', 1); + + if ($options && function_exists('stream_context_create')) { + if ($this->timeout) { + $timeout = $this->timeout; + } else { + $timeout = 0; + } + $context = stream_context_create($options); + + // Since PHP 5 fsockopen doesn't allow context specification + if (function_exists('stream_socket_client')) { + $flags = STREAM_CLIENT_CONNECT; + + if ($this->persistent) { + $flags = STREAM_CLIENT_PERSISTENT; + } + + $addr = $this->addr . ':' . $this->port; + $fp = stream_socket_client($addr, $errno, $errstr, + $timeout, $flags, $context); + } else { + $fp = @$openfunc($this->addr, $this->port, $errno, + $errstr, $timeout, $context); + } + } else { + if ($this->timeout) { + $fp = @$openfunc($this->addr, $this->port, $errno, + $errstr, $this->timeout); + } else { + $fp = @$openfunc($this->addr, $this->port, $errno, $errstr); + } + } + + if (!$fp) { + if ($errno == 0 && !strlen($errstr) && isset($php_errormsg)) { + $errstr = $php_errormsg; + } + @ini_set('track_errors', $old_track_errors); + return $this->raiseError($errstr, $errno); + } + + @ini_set('track_errors', $old_track_errors); + $this->fp = $fp; + + return $this->setBlocking($this->blocking); + } + + /** + * Disconnects from the peer, closes the socket. + * + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function disconnect() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + @fclose($this->fp); + $this->fp = null; + return true; + } + + /** + * Set the newline character/sequence to use. + * + * @param string $newline Newline character(s) + * @return boolean True + */ + function setNewline($newline) + { + $this->newline = $newline; + return true; + } + + /** + * Find out if the socket is in blocking mode. + * + * @access public + * @return boolean The current blocking mode. + */ + function isBlocking() + { + return $this->blocking; + } + + /** + * Sets whether the socket connection should be blocking or + * not. A read call to a non-blocking socket will return immediately + * if there is no data available, whereas it will block until there + * is data for blocking sockets. + * + * @param boolean $mode True for blocking sockets, false for nonblocking. + * + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function setBlocking($mode) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $this->blocking = $mode; + stream_set_blocking($this->fp, (int)$this->blocking); + return true; + } + + /** + * Sets the timeout value on socket descriptor, + * expressed in the sum of seconds and microseconds + * + * @param integer $seconds Seconds. + * @param integer $microseconds Microseconds. + * + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function setTimeout($seconds, $microseconds) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return socket_set_timeout($this->fp, $seconds, $microseconds); + } + + /** + * Sets the file buffering size on the stream. + * See php's stream_set_write_buffer for more information. + * + * @param integer $size Write buffer size. + * + * @access public + * @return mixed on success or an PEAR_Error object otherwise + */ + function setWriteBuffer($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $returned = stream_set_write_buffer($this->fp, $size); + if ($returned == 0) { + return true; + } + return $this->raiseError('Cannot set write buffer.'); + } + + /** + * Returns information about an existing socket resource. + * Currently returns four entries in the result array: + * + * <p> + * timed_out (bool) - The socket timed out waiting for data<br> + * blocked (bool) - The socket was blocked<br> + * eof (bool) - Indicates EOF event<br> + * unread_bytes (int) - Number of bytes left in the socket buffer<br> + * </p> + * + * @access public + * @return mixed Array containing information about existing socket + * resource or a PEAR_Error instance otherwise + */ + function getStatus() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return socket_get_status($this->fp); + } + + /** + * Get a specified line of data + * + * @param int $size ?? + * + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function gets($size = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if (is_null($size)) { + return @fgets($this->fp); + } else { + return @fgets($this->fp, $size); + } + } + + /** + * Read a specified amount of data. This is guaranteed to return, + * and has the added benefit of getting everything in one fread() + * chunk; if you know the size of the data you're getting + * beforehand, this is definitely the way to go. + * + * @param integer $size The number of bytes to read from the socket. + * + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function read($size) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return @fread($this->fp, $size); + } + + /** + * Write a specified amount of data. + * + * @param string $data Data to write. + * @param integer $blocksize Amount of data to write at once. + * NULL means all at once. + * + * @access public + * @return mixed If the socket is not connected, returns an instance of + * PEAR_Error + * If the write succeeds, returns the number of bytes written + * If the write fails, returns false. + */ + function write($data, $blocksize = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if (is_null($blocksize) && !OS_WINDOWS) { + return @fwrite($this->fp, $data); + } else { + if (is_null($blocksize)) { + $blocksize = 1024; + } + + $pos = 0; + $size = strlen($data); + while ($pos < $size) { + $written = @fwrite($this->fp, substr($data, $pos, $blocksize)); + if (!$written) { + return $written; + } + $pos += $written; + } + + return $pos; + } + } + + /** + * Write a line of data to the socket, followed by a trailing newline. + * + * @param string $data Data to write + * + * @access public + * @return mixed fputs result, or an error + */ + function writeLine($data) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return fwrite($this->fp, $data . $this->newline); + } + + /** + * Tests for end-of-file on a socket descriptor. + * + * Also returns true if the socket is disconnected. + * + * @access public + * @return bool + */ + function eof() + { + return (!is_resource($this->fp) || feof($this->fp)); + } + + /** + * Reads a byte of data + * + * @access public + * @return 1 byte of data from the socket, or a PEAR_Error if + * not connected. + */ + function readByte() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + return ord(@fread($this->fp, 1)); + } + + /** + * Reads a word of data + * + * @access public + * @return 1 word of data from the socket, or a PEAR_Error if + * not connected. + */ + function readWord() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 2); + return (ord($buf[0]) + (ord($buf[1]) << 8)); + } + + /** + * Reads an int of data + * + * @access public + * @return integer 1 int of data from the socket, or a PEAR_Error if + * not connected. + */ + function readInt() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return (ord($buf[0]) + (ord($buf[1]) << 8) + + (ord($buf[2]) << 16) + (ord($buf[3]) << 24)); + } + + /** + * Reads a zero-terminated string of data + * + * @access public + * @return string, or a PEAR_Error if + * not connected. + */ + function readString() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $string = ''; + while (($char = @fread($this->fp, 1)) != "\x00") { + $string .= $char; + } + return $string; + } + + /** + * Reads an IP Address and returns it in a dot formatted string + * + * @access public + * @return Dot formatted string, or a PEAR_Error if + * not connected. + */ + function readIPAddress() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $buf = @fread($this->fp, 4); + return sprintf('%d.%d.%d.%d', ord($buf[0]), ord($buf[1]), + ord($buf[2]), ord($buf[3])); + } + + /** + * Read until either the end of the socket or a newline, whichever + * comes first. Strips the trailing newline from the returned data. + * + * @access public + * @return All available data up to a newline, without that + * newline, or until the end of the socket, or a PEAR_Error if + * not connected. + */ + function readLine() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $line = ''; + + $timeout = time() + $this->timeout; + + while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) { + $line .= @fgets($this->fp, $this->lineLength); + if (substr($line, -1) == "\n") { + return rtrim($line, $this->newline); + } + } + return $line; + } + + /** + * Read until the socket closes, or until there is no more data in + * the inner PHP buffer. If the inner buffer is empty, in blocking + * mode we wait for at least 1 byte of data. Therefore, in + * blocking mode, if there is no data at all to be read, this + * function will never exit (unless the socket is closed on the + * remote end). + * + * @access public + * + * @return string All data until the socket closes, or a PEAR_Error if + * not connected. + */ + function readAll() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $data = ''; + while (!feof($this->fp)) { + $data .= @fread($this->fp, $this->lineLength); + } + return $data; + } + + /** + * Runs the equivalent of the select() system call on the socket + * with a timeout specified by tv_sec and tv_usec. + * + * @param integer $state Which of read/write/error to check for. + * @param integer $tv_sec Number of seconds for timeout. + * @param integer $tv_usec Number of microseconds for timeout. + * + * @access public + * @return False if select fails, integer describing which of read/write/error + * are ready, or PEAR_Error if not connected. + */ + function select($state, $tv_sec, $tv_usec = 0) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $read = null; + $write = null; + $except = null; + if ($state & NET_SOCKET_READ) { + $read[] = $this->fp; + } + if ($state & NET_SOCKET_WRITE) { + $write[] = $this->fp; + } + if ($state & NET_SOCKET_ERROR) { + $except[] = $this->fp; + } + if (false === ($sr = stream_select($read, $write, $except, + $tv_sec, $tv_usec))) { + return false; + } + + $result = 0; + if (count($read)) { + $result |= NET_SOCKET_READ; + } + if (count($write)) { + $result |= NET_SOCKET_WRITE; + } + if (count($except)) { + $result |= NET_SOCKET_ERROR; + } + return $result; + } + + /** + * Turns encryption on/off on a connected socket. + * + * @param bool $enabled Set this parameter to true to enable encryption + * and false to disable encryption. + * @param integer $type Type of encryption. See stream_socket_enable_crypto() + * for values. + * + * @see http://se.php.net/manual/en/function.stream-socket-enable-crypto.php + * @access public + * @return false on error, true on success and 0 if there isn't enough data + * and the user should try again (non-blocking sockets only). + * A PEAR_Error object is returned if the socket is not + * connected + */ + function enableCrypto($enabled, $type) + { + if (version_compare(phpversion(), "5.1.0", ">=")) { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + return @stream_socket_enable_crypto($this->fp, $enabled, $type); + } else { + $msg = 'Net_Socket::enableCrypto() requires php version >= 5.1.0'; + return $this->raiseError($msg); + } + } + +} diff --git a/lib/ext/PEAR.php b/lib/ext/PEAR.php new file mode 100644 index 0000000..f4dfd96 --- /dev/null +++ b/lib/ext/PEAR.php @@ -0,0 +1,1137 @@ +<?php +/** + * PEAR, the PHP Extension and Application Repository + * + * PEAR class and PEAR_Error class + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Sterling Hughes <sterling@php.net> + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V.Cox <cox@idecnet.com> + * @author Greg Beaver <cellog@php.net> + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id$ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/**#@+ + * ERROR constants + */ +define('PEAR_ERROR_RETURN', 1); +define('PEAR_ERROR_PRINT', 2); +define('PEAR_ERROR_TRIGGER', 4); +define('PEAR_ERROR_DIE', 8); +define('PEAR_ERROR_CALLBACK', 16); +/** + * WARNING: obsolete + * @deprecated + */ +define('PEAR_ERROR_EXCEPTION', 32); +/**#@-*/ +define('PEAR_ZE2', (function_exists('version_compare') && + version_compare(zend_version(), "2-dev", "ge"))); + +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); + define('OS_UNIX', false); + define('PEAR_OS', 'Windows'); +} else { + define('OS_WINDOWS', false); + define('OS_UNIX', true); + define('PEAR_OS', 'Unix'); // blatant assumption +} + +$GLOBALS['_PEAR_default_error_mode'] = PEAR_ERROR_RETURN; +$GLOBALS['_PEAR_default_error_options'] = E_USER_NOTICE; +$GLOBALS['_PEAR_destructor_object_list'] = array(); +$GLOBALS['_PEAR_shutdown_funcs'] = array(); +$GLOBALS['_PEAR_error_handler_stack'] = array(); + +@ini_set('track_errors', true); + +/** + * Base class for other PEAR classes. Provides rudimentary + * emulation of destructors. + * + * If you want a destructor in your class, inherit PEAR and make a + * destructor method called _yourclassname (same name as the + * constructor, but with a "_" prefix). Also, in your constructor you + * have to call the PEAR constructor: $this->PEAR();. + * The destructor method will be called without parameters. Note that + * at in some SAPI implementations (such as Apache), any output during + * the request shutdown (in which destructors are called) seems to be + * discarded. If you need to get any debug information from your + * destructor, use error_log(), syslog() or something similar. + * + * IMPORTANT! To use the emulated destructors you need to create the + * objects by reference: $obj =& new PEAR_child; + * + * @category pear + * @package PEAR + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Greg Beaver <cellog@php.net> + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.0 + * @link http://pear.php.net/package/PEAR + * @see PEAR_Error + * @since Class available since PHP 4.0.2 + * @link http://pear.php.net/manual/en/core.pear.php#core.pear.pear + */ +class PEAR +{ + // {{{ properties + + /** + * Whether to enable internal debug messages. + * + * @var bool + * @access private + */ + var $_debug = false; + + /** + * Default error mode for this object. + * + * @var int + * @access private + */ + var $_default_error_mode = null; + + /** + * Default error options used for this object when error mode + * is PEAR_ERROR_TRIGGER. + * + * @var int + * @access private + */ + var $_default_error_options = null; + + /** + * Default error handler (callback) for this object, if error mode is + * PEAR_ERROR_CALLBACK. + * + * @var string + * @access private + */ + var $_default_error_handler = ''; + + /** + * Which class to use for error objects. + * + * @var string + * @access private + */ + var $_error_class = 'PEAR_Error'; + + /** + * An array of expected errors. + * + * @var array + * @access private + */ + var $_expected_errors = array(); + + // }}} + + // {{{ constructor + + /** + * Constructor. Registers this object in + * $_PEAR_destructor_object_list for destructor emulation if a + * destructor object exists. + * + * @param string $error_class (optional) which class to use for + * error objects, defaults to PEAR_Error. + * @access public + * @return void + */ + function PEAR($error_class = null) + { + $classname = strtolower(get_class($this)); + if ($this->_debug) { + print "PEAR constructor called, class=$classname\n"; + } + if ($error_class !== null) { + $this->_error_class = $error_class; + } + while ($classname && strcasecmp($classname, "pear")) { + $destructor = "_$classname"; + if (method_exists($this, $destructor)) { + global $_PEAR_destructor_object_list; + $_PEAR_destructor_object_list[] = &$this; + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + break; + } else { + $classname = get_parent_class($classname); + } + } + } + + // }}} + // {{{ destructor + + /** + * Destructor (the emulated type of...). Does nothing right now, + * but is included for forward compatibility, so subclass + * destructors should always call it. + * + * See the note in the class desciption about output from + * destructors. + * + * @access public + * @return void + */ + function _PEAR() { + if ($this->_debug) { + printf("PEAR destructor called, class=%s\n", strtolower(get_class($this))); + } + } + + // }}} + // {{{ getStaticProperty() + + /** + * If you have a class that's mostly/entirely static, and you need static + * properties, you can use this method to simulate them. Eg. in your method(s) + * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar'); + * You MUST use a reference, or they will not persist! + * + * @access public + * @param string $class The calling classname, to prevent clashes + * @param string $var The variable to retrieve. + * @return mixed A reference to the variable. If not set it will be + * auto initialised to NULL. + */ + function &getStaticProperty($class, $var) + { + static $properties; + if (!isset($properties[$class])) { + $properties[$class] = array(); + } + + if (!array_key_exists($var, $properties[$class])) { + $properties[$class][$var] = null; + } + + return $properties[$class][$var]; + } + + // }}} + // {{{ registerShutdownFunc() + + /** + * Use this function to register a shutdown method for static + * classes. + * + * @access public + * @param mixed $func The function name (or array of class/method) to call + * @param mixed $args The arguments to pass to the function + * @return void + */ + function registerShutdownFunc($func, $args = array()) + { + // if we are called statically, there is a potential + // that no shutdown func is registered. Bug #6445 + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args); + } + + // }}} + // {{{ isError() + + /** + * Tell whether a value is a PEAR error. + * + * @param mixed $data the value to test + * @param int $code if $data is an error object, return true + * only if $code is a string and + * $obj->getMessage() == $code or + * $code is an integer and $obj->getCode() == $code + * @access public + * @return bool true if parameter is an error + */ + static function isError($data, $code = null) + { + if (!is_object($data) || !is_a($data, 'PEAR_Error')) { + return false; + } + + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() == $code; + } + + return $data->getCode() == $code; + } + + // }}} + // {{{ setErrorHandling() + + /** + * Sets how errors generated by this object should be handled. + * Can be invoked both in objects and statically. If called + * statically, setErrorHandling sets the default behaviour for all + * PEAR objects. If called in an object, setErrorHandling sets + * the default behaviour for that object. + * + * @param int $mode + * One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION. + * + * @param mixed $options + * When $mode is PEAR_ERROR_TRIGGER, this is the error level (one + * of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * + * When $mode is PEAR_ERROR_CALLBACK, this parameter is expected + * to be the callback function or method. A callback + * function is a string with the name of the function, a + * callback method is an array of two elements: the element + * at index 0 is the object, and the element at index 1 is + * the name of the method to call in the object. + * + * When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is + * a printf format string used when printing the error + * message. + * + * @access public + * @return void + * @see PEAR_ERROR_RETURN + * @see PEAR_ERROR_PRINT + * @see PEAR_ERROR_TRIGGER + * @see PEAR_ERROR_DIE + * @see PEAR_ERROR_CALLBACK + * @see PEAR_ERROR_EXCEPTION + * + * @since PHP 4.0.5 + */ + + function setErrorHandling($mode = null, $options = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $setmode = &$this->_default_error_mode; + $setoptions = &$this->_default_error_options; + } else { + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + } + + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + } + + // }}} + // {{{ expectError() + + /** + * This method is used to tell which errors you expect to get. + * Expected errors are always returned with error mode + * PEAR_ERROR_RETURN. Expected error codes are stored in a stack, + * and this method pushes a new element onto it. The list of + * expected errors are in effect until they are popped off the + * stack with the popExpect() method. + * + * Note that this method can not be called statically + * + * @param mixed $code a single error code or an array of error codes to expect + * + * @return int the new depth of the "expected errors" stack + * @access public + */ + function expectError($code = '*') + { + if (is_array($code)) { + array_push($this->_expected_errors, $code); + } else { + array_push($this->_expected_errors, array($code)); + } + return sizeof($this->_expected_errors); + } + + // }}} + // {{{ popExpect() + + /** + * This method pops one element off the expected error codes + * stack. + * + * @return array the list of error codes that were popped + */ + function popExpect() + { + return array_pop($this->_expected_errors); + } + + // }}} + // {{{ _checkDelExpect() + + /** + * This method checks unsets an error code if available + * + * @param mixed error code + * @return bool true if the error code was unset, false otherwise + * @access private + * @since PHP 4.3.0 + */ + function _checkDelExpect($error_code) + { + $deleted = false; + + foreach ($this->_expected_errors AS $key => $error_array) { + if (in_array($error_code, $error_array)) { + unset($this->_expected_errors[$key][array_search($error_code, $error_array)]); + $deleted = true; + } + + // clean up empty arrays + if (0 == count($this->_expected_errors[$key])) { + unset($this->_expected_errors[$key]); + } + } + return $deleted; + } + + // }}} + // {{{ delExpect() + + /** + * This method deletes all occurences of the specified element from + * the expected error codes stack. + * + * @param mixed $error_code error code that should be deleted + * @return mixed list of error codes that were deleted or error + * @access public + * @since PHP 4.3.0 + */ + function delExpect($error_code) + { + $deleted = false; + if ((is_array($error_code) && (0 != count($error_code)))) { + // $error_code is a non-empty array here; + // we walk through it trying to unset all + // values + foreach($error_code as $key => $error) { + if ($this->_checkDelExpect($error)) { + $deleted = true; + } else { + $deleted = false; + } + } + return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } elseif (!empty($error_code)) { + // $error_code comes alone, trying to unset it + if ($this->_checkDelExpect($error_code)) { + return true; + } else { + return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } + } + + // $error_code is empty + return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME + } + + // }}} + // {{{ raiseError() + + /** + * This method is a wrapper that returns an instance of the + * configured error class with this object's default error + * handling applied. If the $mode and $options parameters are not + * specified, the object's defaults are used. + * + * @param mixed $message a text error message or a PEAR error object + * + * @param int $code a numeric error code (it is up to your class + * to define these if you want to use codes) + * + * @param int $mode One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION. + * + * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter + * specifies the PHP-internal error level (one of + * E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * If $mode is PEAR_ERROR_CALLBACK, this + * parameter specifies the callback function or + * method. In other error modes this parameter + * is ignored. + * + * @param string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @param string $error_class The returned error object will be + * instantiated from this class, if specified. + * + * @param bool $skipmsg If true, raiseError will only pass error codes, + * the error message parameter will be dropped. + * + * @access public + * @return object a PEAR error object + * @see PEAR::setErrorHandling + * @since PHP 4.0.5 + */ + function &raiseError($message = null, + $code = null, + $mode = null, + $options = null, + $userinfo = null, + $error_class = null, + $skipmsg = false) + { + // The error is yet a PEAR error object + if (is_object($message)) { + $code = $message->getCode(); + $userinfo = $message->getUserInfo(); + $error_class = $message->getType(); + $message->error_message_prefix = ''; + $message = $message->getMessage(); + } + + if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) { + if ($exp[0] == "*" || + (is_int(reset($exp)) && in_array($code, $exp)) || + (is_string(reset($exp)) && in_array($message, $exp))) { + $mode = PEAR_ERROR_RETURN; + } + } + + // No mode given, try global ones + if ($mode === null) { + // Class error handler + if (isset($this) && isset($this->_default_error_mode)) { + $mode = $this->_default_error_mode; + $options = $this->_default_error_options; + // Global error handler + } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) { + $mode = $GLOBALS['_PEAR_default_error_mode']; + $options = $GLOBALS['_PEAR_default_error_options']; + } + } + + if ($error_class !== null) { + $ec = $error_class; + } elseif (isset($this) && isset($this->_error_class)) { + $ec = $this->_error_class; + } else { + $ec = 'PEAR_Error'; + } + + if (intval(PHP_VERSION) < 5) { + // little non-eval hack to fix bug #12147 + include 'PEAR/FixPHP5PEARWarnings.php'; + return $a; + } + + if ($skipmsg) { + $a = new $ec($code, $mode, $options, $userinfo); + } else { + $a = new $ec($message, $code, $mode, $options, $userinfo); + } + + return $a; + } + + // }}} + // {{{ throwError() + + /** + * Simpler form of raiseError with fewer options. In most cases + * message, code and userinfo are enough. + * + * @param string $message + * + */ + function &throwError($message = null, + $code = null, + $userinfo = null) + { + if (isset($this) && is_a($this, 'PEAR')) { + $a = &$this->raiseError($message, $code, null, null, $userinfo); + return $a; + } + + $a = &PEAR::raiseError($message, $code, null, null, $userinfo); + return $a; + } + + // }}} + function staticPushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + $stack[] = array($def_mode, $def_options); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $def_mode = $mode; + $def_options = $options; + break; + + case PEAR_ERROR_CALLBACK: + $def_mode = $mode; + // class/object method callback + if (is_callable($options)) { + $def_options = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + $stack[] = array($mode, $options); + return true; + } + + function staticPopErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + return true; + } + + // {{{ pushErrorHandling() + + /** + * Push a new error handler on top of the error handler options stack. With this + * you can easily override the actual error handler for some code and restore + * it later with popErrorHandling. + * + * @param mixed $mode (same as setErrorHandling) + * @param mixed $options (same as setErrorHandling) + * + * @return bool Always true + * + * @see PEAR::setErrorHandling + */ + function pushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + if (isset($this) && is_a($this, 'PEAR')) { + $def_mode = &$this->_default_error_mode; + $def_options = &$this->_default_error_options; + } else { + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + } + $stack[] = array($def_mode, $def_options); + + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + $stack[] = array($mode, $options); + return true; + } + + // }}} + // {{{ popErrorHandling() + + /** + * Pop the last error handler used + * + * @return bool Always true + * + * @see PEAR::pushErrorHandling + */ + function popErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + if (isset($this) && is_a($this, 'PEAR')) { + $this->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + return true; + } + + // }}} + // {{{ loadExtension() + + /** + * OS independant PHP extension load. Remember to take care + * on the correct extension name for case sensitive OSes. + * + * @param string $ext The extension name + * @return bool Success or not on the dl() call + */ + function loadExtension($ext) + { + if (!extension_loaded($ext)) { + // if either returns true dl() will produce a FATAL error, stop that + if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) { + return false; + } + + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; + } + + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + } + + return true; + } + + // }}} +} + +if (PEAR_ZE2) { + include_once 'PEAR5.php'; +} + +// {{{ _PEAR_call_destructors() + +function _PEAR_call_destructors() +{ + global $_PEAR_destructor_object_list; + if (is_array($_PEAR_destructor_object_list) && + sizeof($_PEAR_destructor_object_list)) + { + reset($_PEAR_destructor_object_list); + if (PEAR_ZE2) { + $destructLifoExists = PEAR5::getStaticProperty('PEAR', 'destructlifo'); + } else { + $destructLifoExists = PEAR::getStaticProperty('PEAR', 'destructlifo'); + } + + if ($destructLifoExists) { + $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list); + } + + while (list($k, $objref) = each($_PEAR_destructor_object_list)) { + $classname = get_class($objref); + while ($classname) { + $destructor = "_$classname"; + if (method_exists($objref, $destructor)) { + $objref->$destructor(); + break; + } else { + $classname = get_parent_class($classname); + } + } + } + // Empty the object list to ensure that destructors are + // not called more than once. + $_PEAR_destructor_object_list = array(); + } + + // Now call the shutdown functions + if (isset($GLOBALS['_PEAR_shutdown_funcs']) AND is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) { + foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) { + call_user_func_array($value[0], $value[1]); + } + } +} + +// }}} +/** + * Standard PEAR error class for PHP 4 + * + * This class is supserseded by {@link PEAR_Exception} in PHP 5 + * + * @category pear + * @package PEAR + * @author Stig Bakken <ssb@php.net> + * @author Tomas V.V. Cox <cox@idecnet.com> + * @author Gregory Beaver <cellog@php.net> + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.0 + * @link http://pear.php.net/manual/en/core.pear.pear-error.php + * @see PEAR::raiseError(), PEAR::throwError() + * @since Class available since PHP 4.0.2 + */ +class PEAR_Error +{ + // {{{ properties + + var $error_message_prefix = ''; + var $mode = PEAR_ERROR_RETURN; + var $level = E_USER_NOTICE; + var $code = -1; + var $message = ''; + var $userinfo = ''; + var $backtrace = null; + + // }}} + // {{{ constructor + + /** + * PEAR_Error constructor + * + * @param string $message message + * + * @param int $code (optional) error code + * + * @param int $mode (optional) error mode, one of: PEAR_ERROR_RETURN, + * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION + * + * @param mixed $options (optional) error level, _OR_ in the case of + * PEAR_ERROR_CALLBACK, the callback function or object/method + * tuple. + * + * @param string $userinfo (optional) additional user/debug info + * + * @access public + * + */ + function PEAR_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + if ($mode === null) { + $mode = PEAR_ERROR_RETURN; + } + $this->message = $message; + $this->code = $code; + $this->mode = $mode; + $this->userinfo = $userinfo; + + if (PEAR_ZE2) { + $skiptrace = PEAR5::getStaticProperty('PEAR_Error', 'skiptrace'); + } else { + $skiptrace = PEAR::getStaticProperty('PEAR_Error', 'skiptrace'); + } + + if (!$skiptrace) { + $this->backtrace = debug_backtrace(); + if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) { + unset($this->backtrace[0]['object']); + } + } + + if ($mode & PEAR_ERROR_CALLBACK) { + $this->level = E_USER_NOTICE; + $this->callback = $options; + } else { + if ($options === null) { + $options = E_USER_NOTICE; + } + + $this->level = $options; + $this->callback = null; + } + + if ($this->mode & PEAR_ERROR_PRINT) { + if (is_null($options) || is_int($options)) { + $format = "%s"; + } else { + $format = $options; + } + + printf($format, $this->getMessage()); + } + + if ($this->mode & PEAR_ERROR_TRIGGER) { + trigger_error($this->getMessage(), $this->level); + } + + if ($this->mode & PEAR_ERROR_DIE) { + $msg = $this->getMessage(); + if (is_null($options) || is_int($options)) { + $format = "%s"; + if (substr($msg, -1) != "\n") { + $msg .= "\n"; + } + } else { + $format = $options; + } + die(sprintf($format, $msg)); + } + + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_callable($this->callback)) { + call_user_func($this->callback, $this); + } + } + + if ($this->mode & PEAR_ERROR_EXCEPTION) { + trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING); + eval('$e = new Exception($this->message, $this->code);throw($e);'); + } + } + + // }}} + // {{{ getMode() + + /** + * Get the error mode from an error object. + * + * @return int error mode + * @access public + */ + function getMode() { + return $this->mode; + } + + // }}} + // {{{ getCallback() + + /** + * Get the callback function/method from an error object. + * + * @return mixed callback function or object/method array + * @access public + */ + function getCallback() { + return $this->callback; + } + + // }}} + // {{{ getMessage() + + + /** + * Get the error message from an error object. + * + * @return string full error message + * @access public + */ + function getMessage() + { + return ($this->error_message_prefix . $this->message); + } + + + // }}} + // {{{ getCode() + + /** + * Get error code from an error object + * + * @return int error code + * @access public + */ + function getCode() + { + return $this->code; + } + + // }}} + // {{{ getType() + + /** + * Get the name of this error/exception. + * + * @return string error/exception name (type) + * @access public + */ + function getType() + { + return get_class($this); + } + + // }}} + // {{{ getUserInfo() + + /** + * Get additional user-supplied information. + * + * @return string user-supplied information + * @access public + */ + function getUserInfo() + { + return $this->userinfo; + } + + // }}} + // {{{ getDebugInfo() + + /** + * Get additional debug information supplied by the application. + * + * @return string debug information + * @access public + */ + function getDebugInfo() + { + return $this->getUserInfo(); + } + + // }}} + // {{{ getBacktrace() + + /** + * Get the call backtrace from where the error was generated. + * Supported with PHP 4.3.0 or newer. + * + * @param int $frame (optional) what frame to fetch + * @return array Backtrace, or NULL if not available. + * @access public + */ + function getBacktrace($frame = null) + { + if (defined('PEAR_IGNORE_BACKTRACE')) { + return null; + } + if ($frame === null) { + return $this->backtrace; + } + return $this->backtrace[$frame]; + } + + // }}} + // {{{ addUserInfo() + + function addUserInfo($info) + { + if (empty($this->userinfo)) { + $this->userinfo = $info; + } else { + $this->userinfo .= " ** $info"; + } + } + + // }}} + // {{{ toString() + function __toString() + { + return $this->getMessage(); + } + // }}} + // {{{ toString() + + /** + * Make a string representation of this object. + * + * @return string a string with an object summary + * @access public + */ + function toString() { + $modes = array(); + $levels = array(E_USER_NOTICE => 'notice', + E_USER_WARNING => 'warning', + E_USER_ERROR => 'error'); + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_array($this->callback)) { + $callback = (is_object($this->callback[0]) ? + strtolower(get_class($this->callback[0])) : + $this->callback[0]) . '::' . + $this->callback[1]; + } else { + $callback = $this->callback; + } + return sprintf('[%s: message="%s" code=%d mode=callback '. + 'callback=%s prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + $callback, $this->error_message_prefix, + $this->userinfo); + } + if ($this->mode & PEAR_ERROR_PRINT) { + $modes[] = 'print'; + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + $modes[] = 'trigger'; + } + if ($this->mode & PEAR_ERROR_DIE) { + $modes[] = 'die'; + } + if ($this->mode & PEAR_ERROR_RETURN) { + $modes[] = 'return'; + } + return sprintf('[%s: message="%s" code=%d mode=%s level=%s '. + 'prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + implode("|", $modes), $levels[$this->level], + $this->error_message_prefix, + $this->userinfo); + } + + // }}} +} + +/* + * Local Variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ diff --git a/lib/ext/PEAR5.php b/lib/ext/PEAR5.php new file mode 100644 index 0000000..4286067 --- /dev/null +++ b/lib/ext/PEAR5.php @@ -0,0 +1,33 @@ +<?php +/** + * This is only meant for PHP 5 to get rid of certain strict warning + * that doesn't get hidden since it's in the shutdown function + */ +class PEAR5 +{ + /** + * If you have a class that's mostly/entirely static, and you need static + * properties, you can use this method to simulate them. Eg. in your method(s) + * do this: $myVar = &PEAR5::getStaticProperty('myclass', 'myVar'); + * You MUST use a reference, or they will not persist! + * + * @access public + * @param string $class The calling classname, to prevent clashes + * @param string $var The variable to retrieve. + * @return mixed A reference to the variable. If not set it will be + * auto initialised to NULL. + */ + static function &getStaticProperty($class, $var) + { + static $properties; + if (!isset($properties[$class])) { + $properties[$class] = array(); + } + + if (!array_key_exists($var, $properties[$class])) { + $properties[$class][$var] = null; + } + + return $properties[$class][$var]; + } +}
\ No newline at end of file diff --git a/lib/ext/Roundcube/html.php b/lib/ext/Roundcube/html.php new file mode 100644 index 0000000..880873d --- /dev/null +++ b/lib/ext/Roundcube/html.php @@ -0,0 +1,848 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/html.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2005-2011, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Helper class to create valid XHTML code | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Class for HTML code creation + * + * @package HTML + */ +class html +{ + protected $tagname; + protected $attrib = array(); + protected $allowed = array(); + protected $content; + + public static $doctype = 'xhtml'; + public static $lc_tags = true; + public static $common_attrib = array('id','class','style','title','align'); + public static $containers = array('iframe','div','span','p','h1','h2','h3','form','textarea','table','thead','tbody','tr','th','td','style','script'); + + /** + * Constructor + * + * @param array $attrib Hash array with tag attributes + */ + public function __construct($attrib = array()) + { + if (is_array($attrib)) { + $this->attrib = $attrib; + } + } + + /** + * Return the tag code + * + * @return string The finally composed HTML tag + */ + public function show() + { + return self::tag($this->tagname, $this->attrib, $this->content, array_merge(self::$common_attrib, $this->allowed)); + } + + /****** STATIC METHODS *******/ + + /** + * Generic method to create a HTML tag + * + * @param string $tagname Tag name + * @param array $attrib Tag attributes as key/value pairs + * @param string $content Optinal Tag content (creates a container tag) + * @param array $allowed_attrib List with allowed attributes, omit to allow all + * @return string The XHTML tag + */ + public static function tag($tagname, $attrib = array(), $content = null, $allowed_attrib = null) + { + if (is_string($attrib)) + $attrib = array('class' => $attrib); + + $inline_tags = array('a','span','img'); + $suffix = $attrib['nl'] || ($content && $attrib['nl'] !== false && !in_array($tagname, $inline_tags)) ? "\n" : ''; + + $tagname = self::$lc_tags ? strtolower($tagname) : $tagname; + if (isset($content) || in_array($tagname, self::$containers)) { + $suffix = $attrib['noclose'] ? $suffix : '</' . $tagname . '>' . $suffix; + unset($attrib['noclose'], $attrib['nl']); + return '<' . $tagname . self::attrib_string($attrib, $allowed_attrib) . '>' . $content . $suffix; + } + else { + return '<' . $tagname . self::attrib_string($attrib, $allowed_attrib) . '>' . $suffix; + } + } + + /** + * + */ + public static function doctype($type) + { + $doctypes = array( + 'html5' => '<!DOCTYPE html>', + 'xhtml' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', + 'xhtml-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', + 'xhtml-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">', + ); + + if ($doctypes[$type]) { + self::$doctype = preg_replace('/-\w+$/', '', $type); + return $doctypes[$type]; + } + + return ''; + } + + /** + * Derrived method for <div> containers + * + * @param mixed $attr Hash array with tag attributes or string with class name + * @param string $cont Div content + * @return string HTML code + * @see html::tag() + */ + public static function div($attr = null, $cont = null) + { + if (is_string($attr)) { + $attr = array('class' => $attr); + } + return self::tag('div', $attr, $cont, array_merge(self::$common_attrib, array('onclick'))); + } + + /** + * Derrived method for <p> blocks + * + * @param mixed $attr Hash array with tag attributes or string with class name + * @param string $cont Paragraph content + * @return string HTML code + * @see html::tag() + */ + public static function p($attr = null, $cont = null) + { + if (is_string($attr)) { + $attr = array('class' => $attr); + } + return self::tag('p', $attr, $cont, self::$common_attrib); + } + + /** + * Derrived method to create <img /> + * + * @param mixed $attr Hash array with tag attributes or string with image source (src) + * @return string HTML code + * @see html::tag() + */ + public static function img($attr = null) + { + if (is_string($attr)) { + $attr = array('src' => $attr); + } + return self::tag('img', $attr + array('alt' => ''), null, array_merge(self::$common_attrib, + array('src','alt','width','height','border','usemap','onclick'))); + } + + /** + * Derrived method for link tags + * + * @param mixed $attr Hash array with tag attributes or string with link location (href) + * @param string $cont Link content + * @return string HTML code + * @see html::tag() + */ + public static function a($attr, $cont) + { + if (is_string($attr)) { + $attr = array('href' => $attr); + } + return self::tag('a', $attr, $cont, array_merge(self::$common_attrib, + array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup'))); + } + + /** + * Derrived method for inline span tags + * + * @param mixed $attr Hash array with tag attributes or string with class name + * @param string $cont Tag content + * @return string HTML code + * @see html::tag() + */ + public static function span($attr, $cont) + { + if (is_string($attr)) { + $attr = array('class' => $attr); + } + return self::tag('span', $attr, $cont, self::$common_attrib); + } + + /** + * Derrived method for form element labels + * + * @param mixed $attr Hash array with tag attributes or string with 'for' attrib + * @param string $cont Tag content + * @return string HTML code + * @see html::tag() + */ + public static function label($attr, $cont) + { + if (is_string($attr)) { + $attr = array('for' => $attr); + } + return self::tag('label', $attr, $cont, array_merge(self::$common_attrib, array('for'))); + } + + /** + * Derrived method to create <iframe></iframe> + * + * @param mixed $attr Hash array with tag attributes or string with frame source (src) + * @return string HTML code + * @see html::tag() + */ + public static function iframe($attr = null, $cont = null) + { + if (is_string($attr)) { + $attr = array('src' => $attr); + } + return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib, + array('src','name','width','height','border','frameborder'))); + } + + /** + * Derrived method to create <script> tags + * + * @param mixed $attr Hash array with tag attributes or string with script source (src) + * @param string $cont Javascript code to be placed as tag content + * @return string HTML code + * @see html::tag() + */ + public static function script($attr, $cont = null) + { + if (is_string($attr)) { + $attr = array('src' => $attr); + } + if ($cont) { + if (self::$doctype == 'xhtml') + $cont = "\n/* <![CDATA[ */\n" . $cont . "\n/* ]]> */\n"; + else + $cont = "\n" . $cont . "\n"; + } + + return self::tag('script', $attr + array('type' => 'text/javascript', 'nl' => true), + $cont, array_merge(self::$common_attrib, array('src','type','charset'))); + } + + /** + * Derrived method for line breaks + * + * @return string HTML code + * @see html::tag() + */ + public static function br() + { + return self::tag('br'); + } + + /** + * Create string with attributes + * + * @param array $attrib Associative arry with tag attributes + * @param array $allowed List of allowed attributes + * @return string Valid attribute string + */ + public static function attrib_string($attrib = array(), $allowed = null) + { + if (empty($attrib)) { + return ''; + } + + $allowed_f = array_flip((array)$allowed); + $attrib_arr = array(); + foreach ($attrib as $key => $value) { + // skip size if not numeric + if ($key == 'size' && !is_numeric($value)) { + continue; + } + + // ignore "internal" or not allowed attributes + if ($key == 'nl' || ($allowed && !isset($allowed_f[$key])) || $value === null) { + continue; + } + + // skip empty eventhandlers + if (preg_match('/^on[a-z]+/', $key) && !$value) { + continue; + } + + // attributes with no value + if (in_array($key, array('checked', 'multiple', 'disabled', 'selected'))) { + if ($value) { + $attrib_arr[] = $key . '="' . $key . '"'; + } + } + else { + $attrib_arr[] = $key . '="' . self::quote($value) . '"'; + } + } + + return count($attrib_arr) ? ' '.implode(' ', $attrib_arr) : ''; + } + + /** + * Convert a HTML attribute string attributes to an associative array (name => value) + * + * @param string Input string + * @return array Key-value pairs of parsed attributes + */ + public static function parse_attrib_string($str) + { + $attrib = array(); + $regexp = '/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui'; + + preg_match_all($regexp, stripslashes($str), $regs, PREG_SET_ORDER); + + // convert attributes to an associative array (name => value) + if ($regs) { + foreach ($regs as $attr) { + $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]); + } + } + + return $attrib; + } + + /** + * Replacing specials characters in html attribute value + * + * @param string $str Input string + * + * @return string The quoted string + */ + public static function quote($str) + { + return @htmlspecialchars($str, ENT_COMPAT, RCMAIL_CHARSET); + } +} + + +/** + * Class to create an HTML input field + * + * @package HTML + */ +class html_inputfield extends html +{ + protected $tagname = 'input'; + protected $type = 'text'; + protected $allowed = array( + 'type','name','value','size','tabindex','autocapitalize', + 'autocomplete','checked','onchange','onclick','disabled','readonly', + 'spellcheck','results','maxlength','src','multiple','placeholder', + ); + + /** + * Object constructor + * + * @param array $attrib Associative array with tag attributes + */ + public function __construct($attrib = array()) + { + if (is_array($attrib)) { + $this->attrib = $attrib; + } + + if ($attrib['type']) { + $this->type = $attrib['type']; + } + } + + /** + * Compose input tag + * + * @param string $value Field value + * @param array $attrib Additional attributes to override + * @return string HTML output + */ + public function show($value = null, $attrib = null) + { + // overwrite object attributes + if (is_array($attrib)) { + $this->attrib = array_merge($this->attrib, $attrib); + } + + // set value attribute + if ($value !== null) { + $this->attrib['value'] = $value; + } + // set type + $this->attrib['type'] = $this->type; + return parent::show(); + } +} + +/** + * Class to create an HTML password field + * + * @package HTML + */ +class html_passwordfield extends html_inputfield +{ + protected $type = 'password'; +} + +/** + * Class to create an hidden HTML input field + * + * @package HTML + */ + +class html_hiddenfield extends html +{ + protected $tagname = 'input'; + protected $type = 'hidden'; + protected $fields_arr = array(); + protected $allowed = array('type','name','value','onchange','disabled','readonly'); + + /** + * Constructor + * + * @param array $attrib Named tag attributes + */ + public function __construct($attrib = null) + { + if (is_array($attrib)) { + $this->add($attrib); + } + } + + /** + * Add a hidden field to this instance + * + * @param array $attrib Named tag attributes + */ + public function add($attrib) + { + $this->fields_arr[] = $attrib; + } + + /** + * Create HTML code for the hidden fields + * + * @return string Final HTML code + */ + public function show() + { + $out = ''; + foreach ($this->fields_arr as $attrib) { + $out .= self::tag($this->tagname, array('type' => $this->type) + $attrib); + } + return $out; + } +} + +/** + * Class to create HTML radio buttons + * + * @package HTML + */ +class html_radiobutton extends html_inputfield +{ + protected $type = 'radio'; + + /** + * Get HTML code for this object + * + * @param string $value Value of the checked field + * @param array $attrib Additional attributes to override + * @return string HTML output + */ + public function show($value = '', $attrib = null) + { + // overwrite object attributes + if (is_array($attrib)) { + $this->attrib = array_merge($this->attrib, $attrib); + } + + // set value attribute + $this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']); + + return parent::show(); + } +} + +/** + * Class to create HTML checkboxes + * + * @package HTML + */ +class html_checkbox extends html_inputfield +{ + protected $type = 'checkbox'; + + /** + * Get HTML code for this object + * + * @param string $value Value of the checked field + * @param array $attrib Additional attributes to override + * @return string HTML output + */ + public function show($value = '', $attrib = null) + { + // overwrite object attributes + if (is_array($attrib)) { + $this->attrib = array_merge($this->attrib, $attrib); + } + + // set value attribute + $this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']); + + return parent::show(); + } +} + +/** + * Class to create an HTML textarea + * + * @package HTML + */ +class html_textarea extends html +{ + protected $tagname = 'textarea'; + protected $allowed = array('name','rows','cols','wrap','tabindex', + 'onchange','disabled','readonly','spellcheck'); + + /** + * Get HTML code for this object + * + * @param string $value Textbox value + * @param array $attrib Additional attributes to override + * @return string HTML output + */ + public function show($value = '', $attrib = null) + { + // overwrite object attributes + if (is_array($attrib)) { + $this->attrib = array_merge($this->attrib, $attrib); + } + + // take value attribute as content + if (empty($value) && !empty($this->attrib['value'])) { + $value = $this->attrib['value']; + } + + // make shure we don't print the value attribute + if (isset($this->attrib['value'])) { + unset($this->attrib['value']); + } + + if (!empty($value) && empty($this->attrib['is_escaped'])) { + $value = self::quote($value); + } + + return self::tag($this->tagname, $this->attrib, $value, + array_merge(self::$common_attrib, $this->allowed)); + } +} + +/** + * Builder for HTML drop-down menus + * Syntax:<pre> + * // create instance. arguments are used to set attributes of select-tag + * $select = new html_select(array('name' => 'fieldname')); + * + * // add one option + * $select->add('Switzerland', 'CH'); + * + * // add multiple options + * $select->add(array('Switzerland','Germany'), array('CH','DE')); + * + * // generate pulldown with selection 'Switzerland' and return html-code + * // as second argument the same attributes available to instanciate can be used + * print $select->show('CH'); + * </pre> + * + * @package HTML + */ +class html_select extends html +{ + protected $tagname = 'select'; + protected $options = array(); + protected $allowed = array('name','size','tabindex','autocomplete', + 'multiple','onchange','disabled','rel'); + + /** + * Add a new option to this drop-down + * + * @param mixed $names Option name or array with option names + * @param mixed $values Option value or array with option values + */ + public function add($names, $values = null) + { + if (is_array($names)) { + foreach ($names as $i => $text) { + $this->options[] = array('text' => $text, 'value' => $values[$i]); + } + } + else { + $this->options[] = array('text' => $names, 'value' => $values); + } + } + + /** + * Get HTML code for this object + * + * @param string $select Value of the selection option + * @param array $attrib Additional attributes to override + * @return string HTML output + */ + public function show($select = array(), $attrib = null) + { + // overwrite object attributes + if (is_array($attrib)) { + $this->attrib = array_merge($this->attrib, $attrib); + } + + $this->content = "\n"; + $select = (array)$select; + foreach ($this->options as $option) { + $attr = array( + 'value' => $option['value'], + 'selected' => (in_array($option['value'], $select, true) || + in_array($option['text'], $select, true)) ? 1 : null); + + $option_content = $option['text']; + if (empty($this->attrib['is_escaped'])) { + $option_content = self::quote($option_content); + } + + $this->content .= self::tag('option', $attr, $option_content); + } + + return parent::show(); + } +} + + +/** + * Class to build an HTML table + * + * @package HTML + */ +class html_table extends html +{ + protected $tagname = 'table'; + protected $allowed = array('id','class','style','width','summary', + 'cellpadding','cellspacing','border'); + + private $header = array(); + private $rows = array(); + private $rowindex = 0; + private $colindex = 0; + + /** + * Constructor + * + * @param array $attrib Named tag attributes + */ + public function __construct($attrib = array()) + { + $default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => 0) : array(); + $this->attrib = array_merge($attrib, $default_attrib); + } + + /** + * Add a table cell + * + * @param array $attr Cell attributes + * @param string $cont Cell content + */ + public function add($attr, $cont) + { + if (is_string($attr)) { + $attr = array('class' => $attr); + } + + $cell = new stdClass; + $cell->attrib = $attr; + $cell->content = $cont; + + $this->rows[$this->rowindex]->cells[$this->colindex] = $cell; + $this->colindex += max(1, intval($attr['colspan'])); + + if ($this->attrib['cols'] && $this->colindex >= $this->attrib['cols']) { + $this->add_row(); + } + } + + /** + * Add a table header cell + * + * @param array $attr Cell attributes + * @param string $cont Cell content + */ + public function add_header($attr, $cont) + { + if (is_string($attr)) { + $attr = array('class' => $attr); + } + + $cell = new stdClass; + $cell->attrib = $attr; + $cell->content = $cont; + $this->header[] = $cell; + } + + /** + * Remove a column from a table + * Useful for plugins making alterations + * + * @param string $class + */ + public function remove_column($class) + { + // Remove the header + foreach ($this->header as $index=>$header){ + if ($header->attrib['class'] == $class){ + unset($this->header[$index]); + break; + } + } + + // Remove cells from rows + foreach ($this->rows as $i=>$row){ + foreach ($row->cells as $j=>$cell){ + if ($cell->attrib['class'] == $class){ + unset($this->rows[$i]->cells[$j]); + break; + } + } + } + } + + /** + * Jump to next row + * + * @param array $attr Row attributes + */ + public function add_row($attr = array()) + { + $this->rowindex++; + $this->colindex = 0; + $this->rows[$this->rowindex] = new stdClass; + $this->rows[$this->rowindex]->attrib = $attr; + $this->rows[$this->rowindex]->cells = array(); + } + + /** + * Set row attributes + * + * @param array $attr Row attributes + * @param int $index Optional row index (default current row index) + */ + public function set_row_attribs($attr = array(), $index = null) + { + if (is_string($attr)) { + $attr = array('class' => $attr); + } + + if ($index === null) { + $index = $this->rowindex; + } + + $this->rows[$index]->attrib = $attr; + } + + /** + * Get row attributes + * + * @param int $index Row index + * + * @return array Row attributes + */ + public function get_row_attribs($index = null) + { + if ($index === null) { + $index = $this->rowindex; + } + + return $this->rows[$index] ? $this->rows[$index]->attrib : null; + } + + /** + * Build HTML output of the table data + * + * @param array $attrib Table attributes + * @return string The final table HTML code + */ + public function show($attrib = null) + { + if (is_array($attrib)) + $this->attrib = array_merge($this->attrib, $attrib); + + $thead = $tbody = ""; + + // include <thead> + if (!empty($this->header)) { + $rowcontent = ''; + foreach ($this->header as $c => $col) { + $rowcontent .= self::tag('td', $col->attrib, $col->content); + } + $thead = self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)); + } + + foreach ($this->rows as $r => $row) { + $rowcontent = ''; + foreach ($row->cells as $c => $col) { + $rowcontent .= self::tag('td', $col->attrib, $col->content); + } + + if ($r < $this->rowindex || count($row->cells)) { + $tbody .= self::tag('tr', $row->attrib, $rowcontent, parent::$common_attrib); + } + } + + if ($this->attrib['rowsonly']) { + return $tbody; + } + + // add <tbody> + $this->content = $thead . self::tag('tbody', null, $tbody); + + unset($this->attrib['cols'], $this->attrib['rowsonly']); + return parent::show(); + } + + /** + * Count number of rows + * + * @return The number of rows + */ + public function size() + { + return count($this->rows); + } + + /** + * Remove table body (all rows) + */ + public function remove_body() + { + $this->rows = array(); + $this->rowindex = 0; + } + +} diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php new file mode 100644 index 0000000..0e40b3c --- /dev/null +++ b/lib/ext/Roundcube/rcube.php @@ -0,0 +1,1251 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2008-2012, The Roundcube Dev Team | + | Copyright (C) 2011-2012, Kolab Systems AG | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Framework base class providing core functions and holding | + | instances of all 'global' objects like db- and storage-connections | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Base class of the Roundcube Framework + * implemented as singleton + * + * @package Framework + * @subpackage Core + */ +class rcube +{ + const INIT_WITH_DB = 1; + const INIT_WITH_PLUGINS = 2; + + /** + * Singleton instace of rcube + * + * @var rcmail + */ + static protected $instance; + + /** + * Stores instance of rcube_config. + * + * @var rcube_config + */ + public $config; + + /** + * Instace of database class. + * + * @var rcube_db + */ + public $db; + + /** + * Instace of Memcache class. + * + * @var Memcache + */ + public $memcache; + + /** + * Instace of rcube_session class. + * + * @var rcube_session + */ + public $session; + + /** + * Instance of rcube_smtp class. + * + * @var rcube_smtp + */ + public $smtp; + + /** + * Instance of rcube_storage class. + * + * @var rcube_storage + */ + public $storage; + + /** + * Instance of rcube_output class. + * + * @var rcube_output + */ + public $output; + + /** + * Instance of rcube_plugin_api. + * + * @var rcube_plugin_api + */ + public $plugins; + + + /* private/protected vars */ + protected $texts; + protected $caches = array(); + protected $shutdown_functions = array(); + protected $expunge_cache = false; + + + /** + * This implements the 'singleton' design pattern + * + * @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants + * + * @return rcube The one and only instance + */ + static function get_instance($mode = 0) + { + if (!self::$instance) { + self::$instance = new rcube(); + self::$instance->init($mode); + } + + return self::$instance; + } + + + /** + * Private constructor + */ + protected function __construct() + { + // load configuration + $this->config = new rcube_config; + $this->plugins = new rcube_dummy_plugin_api; + + register_shutdown_function(array($this, 'shutdown')); + } + + + /** + * Initial startup function + */ + protected function init($mode = 0) + { + // initialize syslog + if ($this->config->get('log_driver') == 'syslog') { + $syslog_id = $this->config->get('syslog_id', 'roundcube'); + $syslog_facility = $this->config->get('syslog_facility', LOG_USER); + openlog($syslog_id, LOG_ODELAY, $syslog_facility); + } + + // connect to database + if ($mode & self::INIT_WITH_DB) { + $this->get_dbh(); + } + + // create plugin API and load plugins + if ($mode & self::INIT_WITH_PLUGINS) { + $this->plugins = rcube_plugin_api::get_instance(); + } + } + + + /** + * Get the current database connection + * + * @return rcube_db Database object + */ + public function get_dbh() + { + if (!$this->db) { + $config_all = $this->config->all(); + $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']); + $this->db->set_debug((bool)$config_all['sql_debug']); + } + + return $this->db; + } + + + /** + * Get global handle for memcache access + * + * @return object Memcache + */ + public function get_memcache() + { + if (!isset($this->memcache)) { + // no memcache support in PHP + if (!class_exists('Memcache')) { + $this->memcache = false; + return false; + } + + $this->memcache = new Memcache; + $this->mc_available = 0; + + // add all configured hosts to pool + $pconnect = $this->config->get('memcache_pconnect', true); + foreach ($this->config->get('memcache_hosts', array()) as $host) { + if (substr($host, 0, 7) != 'unix://') { + list($host, $port) = explode(':', $host); + if (!$port) $port = 11211; + } + else { + $port = 0; + } + + $this->mc_available += intval($this->memcache->addServer( + $host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure'))); + } + + // test connection and failover (will result in $this->mc_available == 0 on complete failure) + $this->memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist + + if (!$this->mc_available) { + $this->memcache = false; + } + } + + return $this->memcache; + } + + + /** + * Callback for memcache failure + */ + public function memcache_failure($host, $port) + { + static $seen = array(); + + // only report once + if (!$seen["$host:$port"]++) { + $this->mc_available--; + self::raise_error(array( + 'code' => 604, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Memcache failure on host $host:$port"), + true, false); + } + } + + + /** + * Initialize and get cache object + * + * @param string $name Cache identifier + * @param string $type Cache type ('db', 'apc' or 'memcache') + * @param string $ttl Expiration time for cache items + * @param bool $packed Enables/disables data serialization + * + * @return rcube_cache Cache object + */ + public function get_cache($name, $type='db', $ttl=0, $packed=true) + { + if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) { + $this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed); + } + + return $this->caches[$name]; + } + + + /** + * Create SMTP object and connect to server + * + * @param boolean True if connection should be established + */ + public function smtp_init($connect = false) + { + $this->smtp = new rcube_smtp(); + + if ($connect) { + $this->smtp->connect(); + } + } + + + /** + * Initialize and get storage object + * + * @return rcube_storage Storage object + */ + public function get_storage() + { + // already initialized + if (!is_object($this->storage)) { + $this->storage_init(); + } + + return $this->storage; + } + + + /** + * Initialize storage object + */ + public function storage_init() + { + // already initialized + if (is_object($this->storage)) { + return; + } + + $driver = $this->config->get('storage_driver', 'imap'); + $driver_class = "rcube_{$driver}"; + + if (!class_exists($driver_class)) { + self::raise_error(array( + 'code' => 700, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Storage driver class ($driver) not found!"), + true, true); + } + + // Initialize storage object + $this->storage = new $driver_class; + + // for backward compat. (deprecated, will be removed) + $this->imap = $this->storage; + + // enable caching of mail data + $storage_cache = $this->config->get("{$driver}_cache"); + $messages_cache = $this->config->get('messages_cache'); + // for backward compatybility + if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) { + $storage_cache = 'db'; + $messages_cache = true; + } + + if ($storage_cache) { + $this->storage->set_caching($storage_cache); + } + if ($messages_cache) { + $this->storage->set_messages_caching(true); + } + + // set pagesize from config + $pagesize = $this->config->get('mail_pagesize'); + if (!$pagesize) { + $pagesize = $this->config->get('pagesize', 50); + } + $this->storage->set_pagesize($pagesize); + + // set class options + $options = array( + 'auth_type' => $this->config->get("{$driver}_auth_type", 'check'), + 'auth_cid' => $this->config->get("{$driver}_auth_cid"), + 'auth_pw' => $this->config->get("{$driver}_auth_pw"), + 'debug' => (bool) $this->config->get("{$driver}_debug"), + 'force_caps' => (bool) $this->config->get("{$driver}_force_caps"), + 'timeout' => (int) $this->config->get("{$driver}_timeout"), + 'skip_deleted' => (bool) $this->config->get('skip_deleted'), + 'driver' => $driver, + ); + + if (!empty($_SESSION['storage_host'])) { + $options['host'] = $_SESSION['storage_host']; + $options['user'] = $_SESSION['username']; + $options['port'] = $_SESSION['storage_port']; + $options['ssl'] = $_SESSION['storage_ssl']; + $options['password'] = $this->decrypt($_SESSION['password']); + $_SESSION[$driver.'_host'] = $_SESSION['storage_host']; + } + + $options = $this->plugins->exec_hook("storage_init", $options); + + // for backward compat. (deprecated, to be removed) + $options = $this->plugins->exec_hook("imap_init", $options); + + $this->storage->set_options($options); + $this->set_storage_prop(); + } + + + /** + * Set storage parameters. + * This must be done AFTER connecting to the server! + */ + protected function set_storage_prop() + { + $storage = $this->get_storage(); + + $storage->set_charset($this->config->get('default_charset', RCMAIL_CHARSET)); + + if ($default_folders = $this->config->get('default_folders')) { + $storage->set_default_folders($default_folders); + } + if (isset($_SESSION['mbox'])) { + $storage->set_folder($_SESSION['mbox']); + } + if (isset($_SESSION['page'])) { + $storage->set_page($_SESSION['page']); + } + } + + + /** + * Create session object and start the session. + */ + public function session_init() + { + // session started (Installer?) + if (session_id()) { + return; + } + + $sess_name = $this->config->get('session_name'); + $sess_domain = $this->config->get('session_domain'); + $sess_path = $this->config->get('session_path'); + $lifetime = $this->config->get('session_lifetime', 0) * 60; + + // set session domain + if ($sess_domain) { + ini_set('session.cookie_domain', $sess_domain); + } + // set session path + if ($sess_path) { + ini_set('session.cookie_path', $sess_path); + } + // set session garbage collecting time according to session_lifetime + if ($lifetime) { + ini_set('session.gc_maxlifetime', $lifetime * 2); + } + + ini_set('session.cookie_secure', rcube_utils::https_check()); + ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid'); + ini_set('session.use_cookies', 1); + ini_set('session.use_only_cookies', 1); + ini_set('session.serialize_handler', 'php'); + ini_set('session.cookie_httponly', 1); + + // use database for storing session data + $this->session = new rcube_session($this->get_dbh(), $this->config); + + $this->session->register_gc_handler(array($this, 'temp_gc')); + $this->session->register_gc_handler(array($this, 'cache_gc')); + + // start PHP session (if not in CLI mode) + if ($_SERVER['REMOTE_ADDR']) { + session_start(); + } + } + + + /** + * Configure session object internals + */ + public function session_configure() + { + if (!$this->session) { + return; + } + + $lifetime = $this->config->get('session_lifetime', 0) * 60; + $keep_alive = $this->config->get('keep_alive'); + + // set keep-alive/check-recent interval + if ($keep_alive) { + // be sure that it's less than session lifetime + if ($lifetime) { + $keep_alive = min($keep_alive, $lifetime - 30); + } + $keep_alive = max(60, $keep_alive); + $this->session->set_keep_alive($keep_alive); + } + + $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME'])); + $this->session->set_ip_check($this->config->get('ip_check')); + } + + + /** + * Garbage collector function for temp files. + * Remove temp files older than two days + */ + public function temp_gc() + { + $tmp = unslashify($this->config->get('temp_dir')); + $expire = time() - 172800; // expire in 48 hours + + if ($tmp && ($dir = opendir($tmp))) { + while (($fname = readdir($dir)) !== false) { + if ($fname{0} == '.') { + continue; + } + + if (filemtime($tmp.'/'.$fname) < $expire) { + @unlink($tmp.'/'.$fname); + } + } + + closedir($dir); + } + } + + + /** + * Garbage collector for cache entries. + * Set flag to expunge caches on shutdown + */ + public function cache_gc() + { + // because this gc function is called before storage is initialized, + // we just set a flag to expunge storage cache on shutdown. + $this->expunge_cache = true; + } + + + /** + * Get localized text in the desired language + * + * @param mixed $attrib Named parameters array or label name + * @param string $domain Label domain (plugin) name + * + * @return string Localized text + */ + public function gettext($attrib, $domain=null) + { + // load localization files if not done yet + if (empty($this->texts)) { + $this->load_language(); + } + + // extract attributes + if (is_string($attrib)) { + $attrib = array('name' => $attrib); + } + + $name = $attrib['name'] ? $attrib['name'] : ''; + + // attrib contain text values: use them from now + if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us'])) { + $this->texts[$name] = $setval; + } + + // check for text with domain + if ($domain && ($text = $this->texts[$domain.'.'.$name])) { + } + // text does not exist + else if (!($text = $this->texts[$name])) { + return "[$name]"; + } + + // replace vars in text + if (is_array($attrib['vars'])) { + foreach ($attrib['vars'] as $var_key => $var_value) { + $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text); + } + } + + // format output + if (($attrib['uppercase'] && strtolower($attrib['uppercase'] == 'first')) || $attrib['ucfirst']) { + return ucfirst($text); + } + else if ($attrib['uppercase']) { + return mb_strtoupper($text); + } + else if ($attrib['lowercase']) { + return mb_strtolower($text); + } + + return strtr($text, array('\n' => "\n")); + } + + + /** + * Check if the given text label exists + * + * @param string $name Label name + * @param string $domain Label domain (plugin) name or '*' for all domains + * @param string $ref_domain Sets domain name if label is found + * + * @return boolean True if text exists (either in the current language or in en_US) + */ + public function text_exists($name, $domain = null, &$ref_domain = null) + { + // load localization files if not done yet + if (empty($this->texts)) { + $this->load_language(); + } + + if (isset($this->texts[$name])) { + $ref_domain = ''; + return true; + } + + // any of loaded domains (plugins) + if ($domain == '*') { + foreach ($this->plugins->loaded_plugins() as $domain) { + if (isset($this->texts[$domain.'.'.$name])) { + $ref_domain = $domain; + return true; + } + } + } + // specified domain + else if ($domain) { + $ref_domain = $domain; + return isset($this->texts[$domain.'.'.$name]); + } + + return false; + } + + + /** + * Load a localization package + * + * @param string Language ID + * @param array Additional text labels/messages + */ + public function load_language($lang = null, $add = array()) + { + $lang = $this->language_prop(($lang ? $lang : $_SESSION['language'])); + + // load localized texts + if (empty($this->texts) || $lang != $_SESSION['language']) { + $this->texts = array(); + + // handle empty lines after closing PHP tag in localization files + ob_start(); + + // get english labels (these should be complete) + @include(INSTALL_PATH . 'program/localization/en_US/labels.inc'); + @include(INSTALL_PATH . 'program/localization/en_US/messages.inc'); + + if (is_array($labels)) + $this->texts = $labels; + if (is_array($messages)) + $this->texts = array_merge($this->texts, $messages); + + // include user language files + if ($lang != 'en' && $lang != 'en_US' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { + include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc'); + include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc'); + + if (is_array($labels)) + $this->texts = array_merge($this->texts, $labels); + if (is_array($messages)) + $this->texts = array_merge($this->texts, $messages); + } + + ob_end_clean(); + + $_SESSION['language'] = $lang; + } + + // append additional texts (from plugin) + if (is_array($add) && !empty($add)) { + $this->texts += $add; + } + } + + + /** + * Check the given string and return a valid language code + * + * @param string Language code + * + * @return string Valid language code + */ + protected function language_prop($lang) + { + static $rcube_languages, $rcube_language_aliases; + + // user HTTP_ACCEPT_LANGUAGE if no language is specified + if (empty($lang) || $lang == 'auto') { + $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); + $lang = str_replace('-', '_', $accept_langs[0]); + } + + if (empty($rcube_languages)) { + @include(INSTALL_PATH . 'program/localization/index.inc'); + } + + // check if we have an alias for that language + if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) { + $lang = $rcube_language_aliases[$lang]; + } + // try the first two chars + else if (!isset($rcube_languages[$lang])) { + $short = substr($lang, 0, 2); + + // check if we have an alias for the short language code + if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) { + $lang = $rcube_language_aliases[$short]; + } + // expand 'nn' to 'nn_NN' + else if (!isset($rcube_languages[$short])) { + $lang = $short.'_'.strtoupper($short); + } + } + + if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { + $lang = 'en_US'; + } + + return $lang; + } + + + /** + * Read directory program/localization and return a list of available languages + * + * @return array List of available localizations + */ + public function list_languages() + { + static $sa_languages = array(); + + if (!sizeof($sa_languages)) { + @include(INSTALL_PATH . 'program/localization/index.inc'); + + if ($dh = @opendir(INSTALL_PATH . 'program/localization')) { + while (($name = readdir($dh)) !== false) { + if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name)) { + continue; + } + + if ($label = $rcube_languages[$name]) { + $sa_languages[$name] = $label; + } + } + closedir($dh); + } + } + + return $sa_languages; + } + + + /** + * Encrypt using 3DES + * + * @param string $clear clear text input + * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' + * @param boolean $base64 whether or not to base64_encode() the result before returning + * + * @return string encrypted text + */ + public function encrypt($clear, $key = 'des_key', $base64 = true) + { + if (!$clear) { + return ''; + } + + /*- + * Add a single canary byte to the end of the clear text, which + * will help find out how much of padding will need to be removed + * upon decryption; see http://php.net/mcrypt_generic#68082 + */ + $clear = pack("a*H2", $clear, "80"); + + if (function_exists('mcrypt_module_open') && + ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) + ) { + $iv = $this->create_iv(mcrypt_enc_get_iv_size($td)); + mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); + $cipher = $iv . mcrypt_generic($td, $clear); + mcrypt_generic_deinit($td); + mcrypt_module_close($td); + } + else { + @include_once 'des.inc'; + + if (function_exists('des')) { + $des_iv_size = 8; + $iv = $this->create_iv($des_iv_size); + $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv); + } + else { + self::raise_error(array( + 'code' => 500, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available" + ), true, true); + } + } + + return $base64 ? base64_encode($cipher) : $cipher; + } + + + /** + * Decrypt 3DES-encrypted string + * + * @param string $cipher encrypted text + * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' + * @param boolean $base64 whether or not input is base64-encoded + * + * @return string decrypted text + */ + public function decrypt($cipher, $key = 'des_key', $base64 = true) + { + if (!$cipher) { + return ''; + } + + $cipher = $base64 ? base64_decode($cipher) : $cipher; + + if (function_exists('mcrypt_module_open') && + ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) + ) { + $iv_size = mcrypt_enc_get_iv_size($td); + $iv = substr($cipher, 0, $iv_size); + + // session corruption? (#1485970) + if (strlen($iv) < $iv_size) { + return ''; + } + + $cipher = substr($cipher, $iv_size); + mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); + $clear = mdecrypt_generic($td, $cipher); + mcrypt_generic_deinit($td); + mcrypt_module_close($td); + } + else { + @include_once 'des.inc'; + + if (function_exists('des')) { + $des_iv_size = 8; + $iv = substr($cipher, 0, $des_iv_size); + $cipher = substr($cipher, $des_iv_size); + $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv); + } + else { + self::raise_error(array( + 'code' => 500, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available" + ), true, true); + } + } + + /*- + * Trim PHP's padding and the canary byte; see note in + * rcube::encrypt() and http://php.net/mcrypt_generic#68082 + */ + $clear = substr(rtrim($clear, "\0"), 0, -1); + + return $clear; + } + + + /** + * Generates encryption initialization vector (IV) + * + * @param int Vector size + * + * @return string Vector string + */ + private function create_iv($size) + { + // mcrypt_create_iv() can be slow when system lacks entrophy + // we'll generate IV vector manually + $iv = ''; + for ($i = 0; $i < $size; $i++) { + $iv .= chr(mt_rand(0, 255)); + } + + return $iv; + } + + + /** + * Build a valid URL to this instance of Roundcube + * + * @param mixed Either a string with the action or url parameters as key-value pairs + * @return string Valid application URL + */ + public function url($p) + { + // STUB: should be overloaded by the application + return ''; + } + + + /** + * Function to be executed in script shutdown + * Registered with register_shutdown_function() + */ + public function shutdown() + { + foreach ($this->shutdown_functions as $function) { + call_user_func($function); + } + + if (is_object($this->smtp)) { + $this->smtp->disconnect(); + } + + foreach ($this->caches as $cache) { + if (is_object($cache)) { + $cache->close(); + } + } + + if (is_object($this->storage)) { + if ($this->expunge_cache) { + $this->storage->expunge_cache(); + } + $this->storage->close(); + } + } + + + /** + * Registers shutdown function to be executed on shutdown. + * The functions will be executed before destroying any + * objects like smtp, imap, session, etc. + * + * @param callback Function callback + */ + public function add_shutdown_function($function) + { + $this->shutdown_functions[] = $function; + } + + + /** + * Construct shell command, execute it and return output as string. + * Keywords {keyword} are replaced with arguments + * + * @param $cmd Format string with {keywords} to be replaced + * @param $values (zero, one or more arrays can be passed) + * + * @return output of command. shell errors not detectable + */ + public static function exec(/* $cmd, $values1 = array(), ... */) + { + $args = func_get_args(); + $cmd = array_shift($args); + $values = $replacements = array(); + + // merge values into one array + foreach ($args as $arg) { + $values += (array)$arg; + } + + preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER); + foreach ($matches as $tags) { + list(, $tag, $option, $key) = $tags; + $parts = array(); + + if ($option) { + foreach ((array)$values["-$key"] as $key => $value) { + if ($value === true || $value === false || $value === null) { + $parts[] = $value ? $key : ""; + } + else { + foreach ((array)$value as $val) { + $parts[] = "$key " . escapeshellarg($val); + } + } + } + } + else { + foreach ((array)$values[$key] as $value) { + $parts[] = escapeshellarg($value); + } + } + + $replacements[$tag] = join(" ", $parts); + } + + // use strtr behaviour of going through source string once + $cmd = strtr($cmd, $replacements); + + return (string)shell_exec($cmd); + } + + + /** + * Print or write debug messages + * + * @param mixed Debug message or data + */ + public static function console() + { + $args = func_get_args(); + + if (class_exists('rcube', false)) { + $rcube = self::get_instance(); + $plugin = $rcube->plugins->exec_hook('console', array('args' => $args)); + if ($plugin['abort']) { + return; + } + $args = $plugin['args']; + } + + $msg = array(); + foreach ($args as $arg) { + $msg[] = !is_string($arg) ? var_export($arg, true) : $arg; + } + + self::write_log('console', join(";\n", $msg)); + } + + + /** + * Append a line to a logfile in the logs directory. + * Date will be added automatically to the line. + * + * @param $name name of log file + * @param line Line to append + */ + public static function write_log($name, $line) + { + if (!is_string($line)) { + $line = var_export($line, true); + } + + $date_format = self::$instance ? self::$instance->config->get('log_date_format') : null; + $log_driver = self::$instance ? self::$instance->config->get('log_driver') : null; + + if (empty($date_format)) { + $date_format = 'd-M-Y H:i:s O'; + } + + $date = date($date_format); + + // trigger logging hook + if (is_object(self::$instance) && is_object(self::$instance->plugins)) { + $log = self::$instance->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line)); + $name = $log['name']; + $line = $log['line']; + $date = $log['date']; + if ($log['abort']) + return true; + } + + if ($log_driver == 'syslog') { + $prio = $name == 'errors' ? LOG_ERR : LOG_INFO; + syslog($prio, $line); + return true; + } + + // log_driver == 'file' is assumed here + + $line = sprintf("[%s]: %s\n", $date, $line); + $log_dir = self::$instance ? self::$instance->config->get('log_dir') : null; + + if (empty($log_dir)) { + $log_dir = INSTALL_PATH . 'logs'; + } + + // try to open specific log file for writing + $logfile = $log_dir.'/'.$name; + + if ($fp = @fopen($logfile, 'a')) { + fwrite($fp, $line); + fflush($fp); + fclose($fp); + return true; + } + + trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING); + return false; + } + + + /** + * Throw system error (and show error page). + * + * @param array Named parameters + * - code: Error code (required) + * - type: Error type [php|db|imap|javascript] (required) + * - message: Error message + * - file: File where error occured + * - line: Line where error occured + * @param boolean True to log the error + * @param boolean Terminate script execution + */ + public static function raise_error($arg = array(), $log = false, $terminate = false) + { + // handle PHP exceptions + if (is_object($arg) && is_a($arg, 'Exception')) { + $err = array( + 'type' => 'php', + 'code' => $arg->getCode(), + 'line' => $arg->getLine(), + 'file' => $arg->getFile(), + 'message' => $arg->getMessage(), + ); + $arg = $err; + } + + // installer + if (class_exists('rcube_install', false)) { + $rci = rcube_install::get_instance(); + $rci->raise_error($arg); + return; + } + + if (($log || $terminate) && $arg['type'] && $arg['message']) { + $arg['fatal'] = $terminate; + self::log_bug($arg); + } + + // display error page and terminate script + if ($terminate && is_object(self::$instance->output)) { + self::$instance->output->raise_error($arg['code'], $arg['message']); + } + } + + + /** + * Report error according to configured debug_level + * + * @param array Named parameters + * @see self::raise_error() + */ + public static function log_bug($arg_arr) + { + $program = strtoupper($arg_arr['type']); + $level = self::get_instance()->config->get('debug_level'); + + // disable errors for ajax requests, write to log instead (#1487831) + if (($level & 4) && !empty($_REQUEST['_remote'])) { + $level = ($level ^ 4) | 1; + } + + // write error to local log file + if (($level & 1) || !empty($arg_arr['fatal'])) { + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $post_query = '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']); + } + else { + $post_query = ''; + } + + $log_entry = sprintf("%s Error: %s%s (%s %s)", + $program, + $arg_arr['message'], + $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '', + $_SERVER['REQUEST_METHOD'], + $_SERVER['REQUEST_URI'] . $post_query); + + if (!self::write_log('errors', $log_entry)) { + // send error to PHPs error handler if write_log didn't succeed + trigger_error($arg_arr['message']); + } + } + + // report the bug to the global bug reporting system + if ($level & 2) { + // TODO: Send error via HTTP + } + + // show error if debug_mode is on + if ($level & 4) { + print "<b>$program Error"; + + if (!empty($arg_arr['file']) && !empty($arg_arr['line'])) { + print " in $arg_arr[file] ($arg_arr[line])"; + } + + print ':</b> '; + print nl2br($arg_arr['message']); + print '<br />'; + flush(); + } + } + + + /** + * Returns current time (with microseconds). + * + * @return float Current time in seconds since the Unix + */ + public static function timer() + { + return microtime(true); + } + + + /** + * Logs time difference according to provided timer + * + * @param float $timer Timer (self::timer() result) + * @param string $label Log line prefix + * @param string $dest Log file name + * + * @see self::timer() + */ + public static function print_timer($timer, $label = 'Timer', $dest = 'console') + { + static $print_count = 0; + + $print_count++; + $now = self::timer(); + $diff = $now - $timer; + + if (empty($label)) { + $label = 'Timer '.$print_count; + } + + self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff)); + } + + + /** + * Getter for logged user ID. + * + * @return mixed User identifier + */ + public function get_user_id() + { + if (is_object($this->user)) { + return $this->user->ID; + } + else if (isset($_SESSION['user_id'])) { + return $_SESSION['user_id']; + } + + return null; + } + + + /** + * Getter for logged user name. + * + * @return string User name + */ + public function get_user_name() + { + if (is_object($this->user)) { + return $this->user->get_username(); + } + + return null; + } +} + + +/** + * Lightweight plugin API class serving as a dummy if plugins are not enabled + * + * @package Core + */ +class rcube_dummy_plugin_api +{ + /** + * Triggers a plugin hook. + * @see rcube_plugin_api::exec_hook() + */ + public function exec_hook($hook, $args = array()) + { + return $args; + } +} diff --git a/lib/ext/Roundcube/rcube_addressbook.php b/lib/ext/Roundcube/rcube_addressbook.php new file mode 100644 index 0000000..892ae26 --- /dev/null +++ b/lib/ext/Roundcube/rcube_addressbook.php @@ -0,0 +1,532 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube_addressbook.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2006-2012, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Interface to the local address book database | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Abstract skeleton of an address book/repository + * + * @package Addressbook + */ +abstract class rcube_addressbook +{ + /** constants for error reporting **/ + const ERROR_READ_ONLY = 1; + const ERROR_NO_CONNECTION = 2; + const ERROR_VALIDATE = 3; + const ERROR_SAVING = 4; + const ERROR_SEARCH = 5; + + /** public properties (mandatory) */ + public $primary_key; + public $groups = false; + public $readonly = true; + public $searchonly = false; + public $undelete = false; + public $ready = false; + public $group_id = null; + public $list_page = 1; + public $page_size = 10; + public $sort_col = 'name'; + public $sort_order = 'ASC'; + public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1)); + + protected $error; + + /** + * Returns addressbook name (e.g. for addressbooks listing) + */ + abstract function get_name(); + + /** + * Save a search string for future listings + * + * @param mixed Search params to use in listing method, obtained by get_search_set() + */ + abstract function set_search_set($filter); + + /** + * Getter for saved search properties + * + * @return mixed Search properties used by this class + */ + abstract function get_search_set(); + + /** + * Reset saved results and search parameters + */ + abstract function reset(); + + /** + * Refresh saved search set after data has changed + * + * @return mixed New search set + */ + function refresh_search() + { + return $this->get_search_set(); + } + + /** + * List the current set of contact records + * + * @param array List of cols to show + * @param int Only return this number of records, use negative values for tail + * @return array Indexed list of contact records, each a hash array + */ + abstract function list_records($cols=null, $subset=0); + + /** + * Search records + * + * @param array List of fields to search in + * @param string Search value + * @param int Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * @param boolean True if results are requested, False if count only + * @param boolean True to skip the count query (select only) + * @param array List of fields that cannot be empty + * @return object rcube_result_set List of contact records and 'count' value + */ + abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()); + + /** + * Count number of available contacts in database + * + * @return rcube_result_set Result set with values for 'count' and 'first' + */ + abstract function count(); + + /** + * Return the last result set + * + * @return rcube_result_set Current result set or NULL if nothing selected yet + */ + abstract function get_result(); + + /** + * Get a specific contact record + * + * @param mixed record identifier(s) + * @param boolean True to return record as associative array, otherwise a result set is returned + * + * @return mixed Result object with all record fields or False if not found + */ + abstract function get_record($id, $assoc=false); + + /** + * Returns the last error occured (e.g. when updating/inserting failed) + * + * @return array Hash array with the following fields: type, message + */ + function get_error() + { + return $this->error; + } + + /** + * Setter for errors for internal use + * + * @param int Error type (one of this class' error constants) + * @param string Error message (name of a text label) + */ + protected function set_error($type, $message) + { + $this->error = array('type' => $type, 'message' => $message); + } + + /** + * Close connection to source + * Called on script shutdown + */ + function close() { } + + /** + * Set internal list page + * + * @param number Page number to list + * @access public + */ + function set_page($page) + { + $this->list_page = (int)$page; + } + + /** + * Set internal page size + * + * @param number Number of messages to display on one page + * @access public + */ + function set_pagesize($size) + { + $this->page_size = (int)$size; + } + + /** + * Set internal sort settings + * + * @param string $sort_col Sort column + * @param string $sort_order Sort order + */ + function set_sort_order($sort_col, $sort_order = null) + { + if ($sort_col != null && ($this->coltypes[$sort_col] || in_array($sort_col, $this->coltypes))) { + $this->sort_col = $sort_col; + } + if ($sort_order != null) { + $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC'; + } + } + + /** + * Check the given data before saving. + * If input isn't valid, the message to display can be fetched using get_error() + * + * @param array Assoziative array with data to save + * @param boolean Attempt to fix/complete record automatically + * @return boolean True if input is valid, False if not. + */ + public function validate(&$save_data, $autofix = false) + { + $rcmail = rcmail::get_instance(); + + // check validity of email addresses + foreach ($this->get_col_values('email', $save_data, true) as $email) { + if (strlen($email)) { + if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) { + $error = $rcmail->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email))); + $this->set_error(self::ERROR_VALIDATE, $error); + return false; + } + } + } + + return true; + } + + + /** + * Create a new contact record + * + * @param array Assoziative array with save data + * Keys: Field name with optional section in the form FIELD:SECTION + * Values: Field value. Can be either a string or an array of strings for multiple values + * @param boolean True to check for duplicates first + * @return mixed The created record ID on success, False on error + */ + function insert($save_data, $check=false) + { + /* empty for read-only address books */ + } + + /** + * Create new contact records for every item in the record set + * + * @param object rcube_result_set Recordset to insert + * @param boolean True to check for duplicates first + * @return array List of created record IDs + */ + function insertMultiple($recset, $check=false) + { + $ids = array(); + if (is_object($recset) && is_a($recset, rcube_result_set)) { + while ($row = $recset->next()) { + if ($insert = $this->insert($row, $check)) + $ids[] = $insert; + } + } + return $ids; + } + + /** + * Update a specific contact record + * + * @param mixed Record identifier + * @param array Assoziative array with save data + * Keys: Field name with optional section in the form FIELD:SECTION + * Values: Field value. Can be either a string or an array of strings for multiple values + * @return boolean True on success, False on error + */ + function update($id, $save_cols) + { + /* empty for read-only address books */ + } + + /** + * Mark one or more contact records as deleted + * + * @param array Record identifiers + * @param bool Remove records irreversible (see self::undelete) + */ + function delete($ids, $force=true) + { + /* empty for read-only address books */ + } + + /** + * Unmark delete flag on contact record(s) + * + * @param array Record identifiers + */ + function undelete($ids) + { + /* empty for read-only address books */ + } + + /** + * Mark all records in database as deleted + */ + function delete_all() + { + /* empty for read-only address books */ + } + + /** + * Setter for the current group + * (empty, has to be re-implemented by extending class) + */ + function set_group($gid) { } + + /** + * List all active contact groups of this source + * + * @param string Optional search string to match group name + * @return array Indexed list of contact groups, each a hash array + */ + function list_groups($search = null) + { + /* empty for address books don't supporting groups */ + return array(); + } + + /** + * Get group properties such as name and email address(es) + * + * @param string Group identifier + * @return array Group properties as hash array + */ + function get_group($group_id) + { + /* empty for address books don't supporting groups */ + return null; + } + + /** + * Create a contact group with the given name + * + * @param string The group name + * @return mixed False on error, array with record props in success + */ + function create_group($name) + { + /* empty for address books don't supporting groups */ + return false; + } + + /** + * Delete the given group and all linked group members + * + * @param string Group identifier + * @return boolean True on success, false if no data was changed + */ + function delete_group($gid) + { + /* empty for address books don't supporting groups */ + return false; + } + + /** + * Rename a specific contact group + * + * @param string Group identifier + * @param string New name to set for this group + * @param string New group identifier (if changed, otherwise don't set) + * @return boolean New name on success, false if no data was changed + */ + function rename_group($gid, $newname, &$newid) + { + /* empty for address books don't supporting groups */ + return false; + } + + /** + * Add the given contact records the a certain group + * + * @param string Group identifier + * @param array List of contact identifiers to be added + * @return int Number of contacts added + */ + function add_to_group($group_id, $ids) + { + /* empty for address books don't supporting groups */ + return 0; + } + + /** + * Remove the given contact records from a certain group + * + * @param string Group identifier + * @param array List of contact identifiers to be removed + * @return int Number of deleted group members + */ + function remove_from_group($group_id, $ids) + { + /* empty for address books don't supporting groups */ + return 0; + } + + /** + * Get group assignments of a specific contact record + * + * @param mixed Record identifier + * + * @return array List of assigned groups as ID=>Name pairs + * @since 0.5-beta + */ + function get_record_groups($id) + { + /* empty for address books don't supporting groups */ + return array(); + } + + + /** + * Utility function to return all values of a certain data column + * either as flat list or grouped by subtype + * + * @param string Col name + * @param array Record data array as used for saving + * @param boolean True to return one array with all values, False for hash array with values grouped by type + * @return array List of column values + */ + function get_col_values($col, $data, $flat = false) + { + $out = array(); + foreach ((array)$data as $c => $values) { + if ($c === $col || strpos($c, $col.':') === 0) { + if ($flat) { + $out = array_merge($out, (array)$values); + } + else { + list($f, $type) = explode(':', $c); + $out[$type] = array_merge((array)$out[$type], (array)$values); + } + } + } + + // remove duplicates + if ($flat && !empty($out)) { + $out = array_unique($out); + } + + return $out; + } + + + /** + * Normalize the given string for fulltext search. + * Currently only optimized for Latin-1 characters; to be extended + * + * @param string Input string (UTF-8) + * @return string Normalized string + * @deprecated since 0.9-beta + */ + protected static function normalize_string($str) + { + return rcube_utils::normalize_string($str); + } + + /** + * Compose a valid display name from the given structured contact data + * + * @param array Hash array with contact data as key-value pairs + * @param bool Don't attempt to extract components from the email address + * + * @return string Display name + */ + public static function compose_display_name($contact, $full_email = false) + { + $contact = rcmail::get_instance()->plugins->exec_hook('contact_displayname', $contact); + $fn = $contact['name']; + + if (!$fn) // default display name composition according to vcard standard + $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])))); + + // use email address part for name + $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; + + if ($email && (empty($fn) || $fn == $email)) { + // return full email + if ($full_email) + return $email; + + list($emailname) = explode('@', $email); + if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match)) + $fn = trim(ucfirst($match[1]).' '.ucfirst($match[2])); + else + $fn = ucfirst($emailname); + } + + return $fn; + } + + + /** + * Compose the name to display in the contacts list for the given contact record. + * This respects the settings parameter how to list conacts. + * + * @param array Hash array with contact data as key-value pairs + * @return string List name + */ + public static function compose_list_name($contact) + { + static $compose_mode; + + if (!isset($compose_mode)) // cache this + $compose_mode = rcmail::get_instance()->config->get('addressbook_name_listing', 0); + + if ($compose_mode == 3) + $fn = join(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename'])); + else if ($compose_mode == 2) + $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename'])); + else if ($compose_mode == 1) + $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname'])); + else + $fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])); + + $fn = trim($fn, ', '); + + // fallback to display name + if (empty($fn) && $contact['name']) + $fn = $contact['name']; + + // fallback to email address + $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; + if (empty($fn) && $email) + return $email; + + return $fn; + } + +} + diff --git a/lib/ext/Roundcube/rcube_base_replacer.php b/lib/ext/Roundcube/rcube_base_replacer.php new file mode 100644 index 0000000..4ec3675 --- /dev/null +++ b/lib/ext/Roundcube/rcube_base_replacer.php @@ -0,0 +1,107 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube_base_replacer.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2005-2012, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Provide basic functions for base URL replacement | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Helper class to turn relative urls into absolute ones + * using a predefined base + * + * @package Core + * @author Thomas Bruederli <roundcube@gmail.com> + */ +class rcube_base_replacer +{ + private $base_url; + + + public function __construct($base) + { + $this->base_url = $base; + } + + + public function callback($matches) + { + return $matches[1] . '="' . self::absolute_url($matches[3], $this->base_url) . '"'; + } + + + public function replace($body) + { + return preg_replace_callback(array( + '/(src|background|href)=(["\']?)([^"\'\s]+)(\2|\s|>)/Ui', + '/(url\s*\()(["\']?)([^"\'\)\s]+)(\2)\)/Ui', + ), + array($this, 'callback'), $body); + } + + + /** + * Convert paths like ../xxx to an absolute path using a base url + * + * @param string $path Relative path + * @param string $base_url Base URL + * + * @return string Absolute URL + */ + public static function absolute_url($path, $base_url) + { + $host_url = $base_url; + $abs_path = $path; + + // check if path is an absolute URL + if (preg_match('/^[fhtps]+:\/\//', $path)) { + return $path; + } + + // check if path is a content-id scheme + if (strpos($path, 'cid:') === 0) { + return $path; + } + + // cut base_url to the last directory + if (strrpos($base_url, '/') > 7) { + $host_url = substr($base_url, 0, strpos($base_url, '/', 7)); + $base_url = substr($base_url, 0, strrpos($base_url, '/')); + } + + // $path is absolute + if ($path[0] == '/') { + $abs_path = $host_url.$path; + } + else { + // strip './' because its the same as '' + $path = preg_replace('/^\.\//', '', $path); + + if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) { + foreach ($matches as $a_match) { + if (strrpos($base_url, '/')) { + $base_url = substr($base_url, 0, strrpos($base_url, '/')); + } + $path = substr($path, 3); + } + } + + $abs_path = $base_url.'/'.$path; + } + + return $abs_path; + } +} diff --git a/lib/ext/Roundcube/rcube_browser.php b/lib/ext/Roundcube/rcube_browser.php new file mode 100644 index 0000000..7cfae70 --- /dev/null +++ b/lib/ext/Roundcube/rcube_browser.php @@ -0,0 +1,72 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube_browser.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2007-2009, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Class representing the client browser's properties | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Provide details about the client's browser based on the User-Agent header + * + * @package Core + */ +class rcube_browser +{ + function __construct() + { + $HTTP_USER_AGENT = strtolower($_SERVER['HTTP_USER_AGENT']); + + $this->ver = 0; + $this->win = strpos($HTTP_USER_AGENT, 'win') != false; + $this->mac = strpos($HTTP_USER_AGENT, 'mac') != false; + $this->linux = strpos($HTTP_USER_AGENT, 'linux') != false; + $this->unix = strpos($HTTP_USER_AGENT, 'unix') != false; + + $this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false; + $this->ns4 = strpos($HTTP_USER_AGENT, 'mozilla/4') !== false && strpos($HTTP_USER_AGENT, 'msie') === false; + $this->ns = ($this->ns4 || strpos($HTTP_USER_AGENT, 'netscape') !== false); + $this->ie = !$this->opera && strpos($HTTP_USER_AGENT, 'compatible; msie') !== false; + $this->khtml = strpos($HTTP_USER_AGENT, 'khtml') !== false; + $this->mz = !$this->ie && !$this->khtml && strpos($HTTP_USER_AGENT, 'mozilla/5') !== false; + $this->chrome = strpos($HTTP_USER_AGENT, 'chrome') !== false; + $this->safari = !$this->chrome && ($this->khtml || strpos($HTTP_USER_AGENT, 'safari') !== false); + + if ($this->ns || $this->chrome) { + $test = preg_match('/(mozilla|chrome)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs); + $this->ver = $test ? (float)$regs[2] : 0; + } + else if ($this->mz) { + $test = preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs); + $this->ver = $test ? (float)$regs[1] : 0; + } + else if ($this->ie || $this->opera) { + $test = preg_match('/(msie|opera) ([0-9.]+)/', $HTTP_USER_AGENT, $regs); + $this->ver = $test ? (float)$regs[2] : 0; + } + + if (preg_match('/ ([a-z]{2})-([a-z]{2})/', $HTTP_USER_AGENT, $regs)) + $this->lang = $regs[1]; + else + $this->lang = 'en'; + + $this->dom = ($this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7)); + $this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) || + ($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7) ? true : false; + $this->imgdata = !$this->ie; + } +} + diff --git a/lib/ext/Roundcube/rcube_cache.php b/lib/ext/Roundcube/rcube_cache.php new file mode 100644 index 0000000..4e60dea --- /dev/null +++ b/lib/ext/Roundcube/rcube_cache.php @@ -0,0 +1,559 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube_cache.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2011, The Roundcube Dev Team | + | Copyright (C) 2011, Kolab Systems AG | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Caching engine | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + | Author: Aleksander Machniak <alec@alec.pl> | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Interface class for accessing Roundcube cache + * + * @package Cache + * @author Thomas Bruederli <roundcube@gmail.com> + * @author Aleksander Machniak <alec@alec.pl> + * @version 1.1 + */ +class rcube_cache +{ + /** + * Instance of database handler + * + * @var rcube_db|Memcache|bool + */ + private $db; + private $type; + private $userid; + private $prefix; + private $ttl; + private $packed; + private $index; + private $cache = array(); + private $cache_changes = array(); + private $cache_sums = array(); + + + /** + * Object constructor. + * + * @param string $type Engine type ('db' or 'memcache' or 'apc') + * @param int $userid User identifier + * @param string $prefix Key name prefix + * @param string $ttl Expiration time of memcache/apc items + * @param bool $packed Enables/disabled data serialization. + * It's possible to disable data serialization if you're sure + * stored data will be always a safe string + */ + function __construct($type, $userid, $prefix='', $ttl=0, $packed=true) + { + $rcube = rcube::get_instance(); + $type = strtolower($type); + + if ($type == 'memcache') { + $this->type = 'memcache'; + $this->db = $rcube->get_memcache(); + } + else if ($type == 'apc') { + $this->type = 'apc'; + $this->db = function_exists('apc_exists'); // APC 3.1.4 required + } + else { + $this->type = 'db'; + $this->db = $rcube->get_dbh(); + } + + // convert ttl string to seconds + $ttl = get_offset_sec($ttl); + if ($ttl > 2592000) $ttl = 2592000; + + $this->userid = (int) $userid; + $this->ttl = $ttl; + $this->packed = $packed; + $this->prefix = $prefix; + } + + + /** + * Returns cached value. + * + * @param string $key Cache key name + * + * @return mixed Cached value + */ + function get($key) + { + if (!array_key_exists($key, $this->cache)) { + return $this->read_record($key); + } + + return $this->cache[$key]; + } + + + /** + * Sets (add/update) value in cache. + * + * @param string $key Cache key name + * @param mixed $data Cache data + */ + function set($key, $data) + { + $this->cache[$key] = $data; + $this->cache_changed = true; + $this->cache_changes[$key] = true; + } + + + /** + * Returns cached value without storing it in internal memory. + * + * @param string $key Cache key name + * + * @return mixed Cached value + */ + function read($key) + { + if (array_key_exists($key, $this->cache)) { + return $this->cache[$key]; + } + + return $this->read_record($key, true); + } + + + /** + * Sets (add/update) value in cache and immediately saves + * it in the backend, no internal memory will be used. + * + * @param string $key Cache key name + * @param mixed $data Cache data + * + * @param boolean True on success, False on failure + */ + function write($key, $data) + { + return $this->write_record($key, $this->packed ? serialize($data) : $data); + } + + + /** + * Clears the cache. + * + * @param string $key Cache key name or pattern + * @param boolean $prefix_mode Enable it to clear all keys starting + * with prefix specified in $key + */ + function remove($key=null, $prefix_mode=false) + { + // Remove all keys + if ($key === null) { + $this->cache = array(); + $this->cache_changed = false; + $this->cache_changes = array(); + $this->cache_sums = array(); + } + // Remove keys by name prefix + else if ($prefix_mode) { + foreach (array_keys($this->cache) as $k) { + if (strpos($k, $key) === 0) { + $this->cache[$k] = null; + $this->cache_changes[$k] = false; + unset($this->cache_sums[$k]); + } + } + } + // Remove one key by name + else { + $this->cache[$key] = null; + $this->cache_changes[$key] = false; + unset($this->cache_sums[$key]); + } + + // Remove record(s) from the backend + $this->remove_record($key, $prefix_mode); + } + + + /** + * Remove cache records older than ttl + */ + function expunge() + { + if ($this->type == 'db' && $this->db) { + $this->db->query( + "DELETE FROM ".$this->db->table_name('cache'). + " WHERE user_id = ?". + " AND cache_key LIKE ?". + " AND " . $this->db->unixtimestamp('created')." < ?", + $this->userid, + $this->prefix.'.%', + time() - $this->ttl); + } + } + + + /** + * Writes the cache back to the DB. + */ + function close() + { + if (!$this->cache_changed) { + return; + } + + foreach ($this->cache as $key => $data) { + // The key has been used + if ($this->cache_changes[$key]) { + // Make sure we're not going to write unchanged data + // by comparing current md5 sum with the sum calculated on DB read + $data = $this->packed ? serialize($data) : $data; + + if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) { + $this->write_record($key, $data); + } + } + } + + $this->write_index(); + } + + + /** + * Reads cache entry. + * + * @param string $key Cache key name + * @param boolean $nostore Enable to skip in-memory store + * + * @return mixed Cached value + */ + private function read_record($key, $nostore=false) + { + if (!$this->db) { + return null; + } + + if ($this->type != 'db') { + if ($this->type == 'memcache') { + $data = $this->db->get($this->ckey($key)); + } + else if ($this->type == 'apc') { + $data = apc_fetch($this->ckey($key)); + } + + if ($data) { + $md5sum = md5($data); + $data = $this->packed ? unserialize($data) : $data; + + if ($nostore) { + return $data; + } + + $this->cache_sums[$key] = $md5sum; + $this->cache[$key] = $data; + } + else { + $this->cache[$key] = null; + } + } + else { + $sql_result = $this->db->limitquery( + "SELECT data, cache_key". + " FROM ".$this->db->table_name('cache'). + " WHERE user_id = ?". + " AND cache_key = ?". + // for better performance we allow more records for one key + // get the newer one + " ORDER BY created DESC", + 0, 1, $this->userid, $this->prefix.'.'.$key); + + if ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1); + $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null; + if ($sql_arr['data']) { + $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data']; + } + + if ($nostore) { + return $data; + } + + $this->cache[$key] = $data; + $this->cache_sums[$key] = $md5sum; + } + else { + $this->cache[$key] = null; + } + } + + return $this->cache[$key]; + } + + + /** + * Writes single cache record into DB. + * + * @param string $key Cache key name + * @param mxied $data Serialized cache data + * + * @param boolean True on success, False on failure + */ + private function write_record($key, $data) + { + if (!$this->db) { + return false; + } + + if ($this->type == 'memcache' || $this->type == 'apc') { + return $this->add_record($this->ckey($key), $data); + } + + $key_exists = array_key_exists($key, $this->cache_sums); + $key = $this->prefix . '.' . $key; + + // Remove NULL rows (here we don't need to check if the record exist) + if ($data == 'N;') { + $this->db->query( + "DELETE FROM ".$this->db->table_name('cache'). + " WHERE user_id = ?". + " AND cache_key = ?", + $this->userid, $key); + + return true; + } + + // update existing cache record + if ($key_exists) { + $result = $this->db->query( + "UPDATE ".$this->db->table_name('cache'). + " SET created = ". $this->db->now().", data = ?". + " WHERE user_id = ?". + " AND cache_key = ?", + $data, $this->userid, $key); + } + // add new cache record + else { + // for better performance we allow more records for one key + // so, no need to check if record exist (see rcube_cache::read_record()) + $result = $this->db->query( + "INSERT INTO ".$this->db->table_name('cache'). + " (created, user_id, cache_key, data)". + " VALUES (".$this->db->now().", ?, ?, ?)", + $this->userid, $key, $data); + } + + return $this->db->affected_rows($result); + } + + + /** + * Deletes the cache record(s). + * + * @param string $key Cache key name or pattern + * @param boolean $prefix_mode Enable it to clear all keys starting + * with prefix specified in $key + * + */ + private function remove_record($key=null, $prefix_mode=false) + { + if (!$this->db) { + return; + } + + if ($this->type != 'db') { + $this->load_index(); + + // Remove all keys + if ($key === null) { + foreach ($this->index as $key) { + $this->delete_record($key, false); + } + $this->index = array(); + } + // Remove keys by name prefix + else if ($prefix_mode) { + foreach ($this->index as $k) { + if (strpos($k, $key) === 0) { + $this->delete_record($k); + } + } + } + // Remove one key by name + else { + $this->delete_record($key); + } + + return; + } + + // Remove all keys (in specified cache) + if ($key === null) { + $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%'); + } + // Remove keys by name prefix + else if ($prefix_mode) { + $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%'); + } + // Remove one key by name + else { + $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key); + } + + $this->db->query( + "DELETE FROM ".$this->db->table_name('cache'). + " WHERE user_id = ?" . $where, + $this->userid); + } + + + /** + * Adds entry into memcache/apc DB. + * + * @param string $key Cache key name + * @param mxied $data Serialized cache data + * @param bollean $index Enables immediate index update + * + * @param boolean True on success, False on failure + */ + private function add_record($key, $data, $index=false) + { + if ($this->type == 'memcache') { + $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); + if (!$result) + $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); + } + else if ($this->type == 'apc') { + if (apc_exists($key)) + apc_delete($key); + $result = apc_store($key, $data, $this->ttl); + } + + // Update index + if ($index && $result) { + $this->load_index(); + + if (array_search($key, $this->index) === false) { + $this->index[] = $key; + $data = serialize($this->index); + $this->add_record($this->ikey(), $data); + } + } + + return $result; + } + + + /** + * Deletes entry from memcache/apc DB. + */ + private function delete_record($key, $index=true) + { + if ($this->type == 'memcache') { + // #1488592: use 2nd argument + $this->db->delete($this->ckey($key), 0); + } + else { + apc_delete($this->ckey($key)); + } + + if ($index) { + if (($idx = array_search($key, $this->index)) !== false) { + unset($this->index[$idx]); + } + } + } + + + /** + * Writes the index entry into memcache/apc DB. + */ + private function write_index() + { + if (!$this->db) { + return; + } + + if ($this->type == 'db') { + return; + } + + $this->load_index(); + + // Make sure index contains new keys + foreach ($this->cache as $key => $value) { + if ($value !== null) { + if (array_search($key, $this->index) === false) { + $this->index[] = $key; + } + } + } + + $data = serialize($this->index); + $this->add_record($this->ikey(), $data); + } + + + /** + * Gets the index entry from memcache/apc DB. + */ + private function load_index() + { + if (!$this->db) { + return; + } + + if ($this->index !== null) { + return; + } + + $index_key = $this->ikey(); + if ($this->type == 'memcache') { + $data = $this->db->get($index_key); + } + else if ($this->type == 'apc') { + $data = apc_fetch($index_key); + } + + $this->index = $data ? unserialize($data) : array(); + } + + + /** + * Creates per-user cache key name (for memcache and apc) + * + * @param string $key Cache key name + * + * @return string Cache key + */ + private function ckey($key) + { + return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key); + } + + + /** + * Creates per-user index cache key name (for memcache and apc) + * + * @return string Cache key + */ + private function ikey() + { + // This way each cache will have its own index + return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX'); + } +} diff --git a/lib/ext/Roundcube/rcube_charset.php b/lib/ext/Roundcube/rcube_charset.php new file mode 100644 index 0000000..ff4c2bb --- /dev/null +++ b/lib/ext/Roundcube/rcube_charset.php @@ -0,0 +1,763 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube_charset.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2005-2012, The Roundcube Dev Team | + | Copyright (C) 2011-2012, Kolab Systems AG | + | Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org> | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Provide charset conversion functionality | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + | Author: Aleksander Machniak <alec@alec.pl> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Character sets conversion functionality + * + * @package Core + * @author Thomas Bruederli <roundcube@gmail.com> + * @author Aleksander Machniak <alec@alec.pl> + * @author Edmund Grimley Evans <edmundo@rano.org> + */ +class rcube_charset +{ + // Aliases: some of them from HTML5 spec. + static public $aliases = array( + 'USASCII' => 'WINDOWS-1252', + 'ANSIX31101983' => 'WINDOWS-1252', + 'ANSIX341968' => 'WINDOWS-1252', + 'UNKNOWN8BIT' => 'ISO-8859-15', + 'UNKNOWN' => 'ISO-8859-15', + 'USERDEFINED' => 'ISO-8859-15', + 'KSC56011987' => 'EUC-KR', + 'GB2312' => 'GBK', + 'GB231280' => 'GBK', + 'UNICODE' => 'UTF-8', + 'UTF7IMAP' => 'UTF7-IMAP', + 'TIS620' => 'WINDOWS-874', + 'ISO88599' => 'WINDOWS-1254', + 'ISO885911' => 'WINDOWS-874', + 'MACROMAN' => 'MACINTOSH', + '77' => 'MAC', + '128' => 'SHIFT-JIS', + '129' => 'CP949', + '130' => 'CP1361', + '134' => 'GBK', + '136' => 'BIG5', + '161' => 'WINDOWS-1253', + '162' => 'WINDOWS-1254', + '163' => 'WINDOWS-1258', + '177' => 'WINDOWS-1255', + '178' => 'WINDOWS-1256', + '186' => 'WINDOWS-1257', + '204' => 'WINDOWS-1251', + '222' => 'WINDOWS-874', + '238' => 'WINDOWS-1250', + 'MS950' => 'CP950', + 'WINDOWS949' => 'UHC', + ); + + + /** + * Catch an error and throw an exception. + * + * @param int Level of the error + * @param string Error message + */ + public static function error_handler($errno, $errstr) + { + throw new ErrorException($errstr, 0, $errno); + } + + + /** + * Parse and validate charset name string (see #1485758). + * Sometimes charset string is malformed, there are also charset aliases + * but we need strict names for charset conversion (specially utf8 class) + * + * @param string $input Input charset name + * + * @return string The validated charset name + */ + public static function parse_charset($input) + { + static $charsets = array(); + $charset = strtoupper($input); + + if (isset($charsets[$input])) { + return $charsets[$input]; + } + + $charset = preg_replace(array( + '/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO + '/\$.*$/', // e.g. _ISO-8859-JP$SIO + '/UNICODE-1-1-*/', // RFC1641/1642 + '/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8) + ), '', $charset); + + if ($charset == 'BINARY') { + return $charsets[$input] = null; + } + + // allow A-Z and 0-9 only + $str = preg_replace('/[^A-Z0-9]/', '', $charset); + + if (isset(self::$aliases[$str])) { + $result = self::$aliases[$str]; + } + // UTF + else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m)) { + $result = 'UTF-' . $m[1] . $m[2]; + } + // ISO-8859 + else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) { + $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1); + // some clients sends windows-1252 text as latin1, + // it is safe to use windows-1252 for all latin1 + $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso; + } + // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE + else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) { + $result = 'WINDOWS-' . $m[2]; + } + // LATIN + else if (preg_match('/LATIN(.*)/', $str, $m)) { + $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10, + '7' => 13, '8' => 14, '9' => 15, '10' => 16, + 'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8 + ); + + // some clients sends windows-1252 text as latin1, + // it is safe to use windows-1252 for all latin1 + if ($m[1] == 1) { + $result = 'WINDOWS-1252'; + } + // if iconv is not supported we need ISO labels, it's also safe for iconv + else if (!empty($aliases[$m[1]])) { + $result = 'ISO-8859-'.$aliases[$m[1]]; + } + // iconv requires convertion of e.g. LATIN-1 to LATIN1 + else { + $result = $str; + } + } + else { + $result = $charset; + } + + $charsets[$input] = $result; + + return $result; + } + + + /** + * Convert a string from one charset to another. + * Uses mbstring and iconv functions if possible + * + * @param string Input string + * @param string Suspected charset of the input string + * @param string Target charset to convert to; defaults to RCMAIL_CHARSET + * + * @return string Converted string + */ + public static function convert($str, $from, $to = null) + { + static $iconv_options = null; + static $mbstring_list = null; + static $mbstring_sch = null; + static $conv = null; + + $to = empty($to) ? RCMAIL_CHARSET : $to; + $from = self::parse_charset($from); + + // It is a common case when UTF-16 charset is used with US-ASCII content (#1488654) + // In that case we can just skip the conversion (use UTF-8) + if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) { + $from = 'UTF-8'; + } + + if ($from == $to || empty($str) || empty($from)) { + return $str; + } + + if ($iconv_options === null) { + if (function_exists('iconv')) { + // ignore characters not available in output charset + $iconv_options = '//IGNORE'; + if (iconv('', $iconv_options, '') === false) { + // iconv implementation does not support options + $iconv_options = ''; + } + } + } + + // convert charset using iconv module + if ($iconv_options !== null && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') { + // throw an exception if iconv reports an illegal character in input + // it means that input string has been truncated + set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE); + try { + $_iconv = iconv($from, $to . $iconv_options, $str); + } catch (ErrorException $e) { + $_iconv = false; + } + restore_error_handler(); + + if ($_iconv !== false) { + return $_iconv; + } + } + + if ($mbstring_list === null) { + if (extension_loaded('mbstring')) { + $mbstring_sch = mb_substitute_character(); + $mbstring_list = mb_list_encodings(); + $mbstring_list = array_map('strtoupper', $mbstring_list); + } + } + + // convert charset using mbstring module + if ($mbstring_list !== null) { + $aliases['WINDOWS-1257'] = 'ISO-8859-13'; + // it happens that mbstring supports ASCII but not US-ASCII + if (($from == 'US-ASCII' || $to == 'US-ASCII') && !in_array('US-ASCII', $mbstring_list)) { + $aliases['US-ASCII'] = 'ASCII'; + } + + $mb_from = $aliases[$from] ? $aliases[$from] : $from; + $mb_to = $aliases[$to] ? $aliases[$to] : $to; + + // return if encoding found, string matches encoding and convert succeeded + if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) { + if (mb_check_encoding($str, $mb_from)) { + // Do the same as //IGNORE with iconv + mb_substitute_character('none'); + $out = mb_convert_encoding($str, $mb_to, $mb_from); + mb_substitute_character($mbstring_sch); + + if ($out !== false) { + return $out; + } + } + } + } + + // convert charset using bundled classes/functions + if ($to == 'UTF-8') { + if ($from == 'UTF7-IMAP') { + if ($_str = self::utf7imap_to_utf8($str)) { + return $_str; + } + } + else if ($from == 'UTF-7') { + if ($_str = self::utf7_to_utf8($str)) { + return $_str; + } + } + else if ($from == 'ISO-8859-1' && function_exists('utf8_encode')) { + return utf8_encode($str); + } + else if (class_exists('utf8')) { + if (!$conv) { + $conv = new utf8($from); + } + else { + $conv->loadCharset($from); + } + + if ($_str = $conv->strToUtf8($str)) { + return $_str; + } + } + } + + // encode string for output + if ($from == 'UTF-8') { + // @TODO: we need a function for UTF-7 (RFC2152) conversion + if ($to == 'UTF7-IMAP' || $to == 'UTF-7') { + if ($_str = self::utf8_to_utf7imap($str)) { + return $_str; + } + } + else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) { + return utf8_decode($str); + } + else if (class_exists('utf8')) { + if (!$conv) { + $conv = new utf8($to); + } + else { + $conv->loadCharset($from); + } + + if ($_str = $conv->strToUtf8($str)) { + return $_str; + } + } + } + + // return original string + return $str; + } + + + /** + * Converts string from standard UTF-7 (RFC 2152) to UTF-8. + * + * @param string Input string (UTF-7) + * + * @return string Converted string (UTF-8) + */ + public static function utf7_to_utf8($str) + { + $Index_64 = array( + 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0, + 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0, + 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, + 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, + 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, + 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, + ); + + $u7len = strlen($str); + $str = strval($str); + $res = ''; + + for ($i=0; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + if ($u7 == '+') { + $i++; + $u7len--; + $ch = ''; + + for (; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + + if (!$Index_64[ord($u7)]) { + break; + } + + $ch .= $u7; + } + + if ($ch == '') { + if ($u7 == '-') { + $res .= '+'; + } + + continue; + } + + $res .= self::utf16_to_utf8(base64_decode($ch)); + } + else { + $res .= $u7; + } + } + + return $res; + } + + + /** + * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion) + * + * @param string Input string + * + * @return string The converted string + */ + public static function utf16_to_utf8($str) + { + $len = strlen($str); + $dec = ''; + + for ($i = 0; $i < $len; $i += 2) { + $c = ord($str[$i]) << 8 | ord($str[$i + 1]); + if ($c >= 0x0001 && $c <= 0x007F) { + $dec .= chr($c); + } + else if ($c > 0x07FF) { + $dec .= chr(0xE0 | (($c >> 12) & 0x0F)); + $dec .= chr(0x80 | (($c >> 6) & 0x3F)); + $dec .= chr(0x80 | (($c >> 0) & 0x3F)); + } + else { + $dec .= chr(0xC0 | (($c >> 6) & 0x1F)); + $dec .= chr(0x80 | (($c >> 0) & 0x3F)); + } + } + + return $dec; + } + + + /** + * Convert the data ($str) from RFC 2060's UTF-7 to UTF-8. + * If input data is invalid, return the original input string. + * RFC 2060 obviously intends the encoding to be unique (see + * point 5 in section 5.1.3), so we reject any non-canonical + * form, such as &ACY- (instead of &-) or &AMA-&AMA- (instead + * of &AMAAwA-). + * + * Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com> + * + * @param string $str Input string (UTF7-IMAP) + * + * @return string Output string (UTF-8) + */ + public static function utf7imap_to_utf8($str) + { + $Index_64 = array( + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, 63,-1,-1,-1, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1,-1,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 + ); + + $u7len = strlen($str); + $str = strval($str); + $p = ''; + $err = ''; + + for ($i=0; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + if ($u7 == '&') { + $i++; + $u7len--; + $u7 = $str[$i]; + + if ($u7len && $u7 == '-') { + $p .= '&'; + continue; + } + + $ch = 0; + $k = 10; + for (; $u7len > 0; $i++, $u7len--) { + $u7 = $str[$i]; + + if ((ord($u7) & 0x80) || ($b = $Index_64[ord($u7)]) == -1) { + break; + } + + if ($k > 0) { + $ch |= $b << $k; + $k -= 6; + } + else { + $ch |= $b >> (-$k); + if ($ch < 0x80) { + // Printable US-ASCII + if (0x20 <= $ch && $ch < 0x7f) { + return $err; + } + $p .= chr($ch); + } + else if ($ch < 0x800) { + $p .= chr(0xc0 | ($ch >> 6)); + $p .= chr(0x80 | ($ch & 0x3f)); + } + else { + $p .= chr(0xe0 | ($ch >> 12)); + $p .= chr(0x80 | (($ch >> 6) & 0x3f)); + $p .= chr(0x80 | ($ch & 0x3f)); + } + + $ch = ($b << (16 + $k)) & 0xffff; + $k += 10; + } + } + + // Non-zero or too many extra bits + if ($ch || $k < 6) { + return $err; + } + + // BASE64 not properly terminated + if (!$u7len || $u7 != '-') { + return $err; + } + + // Adjacent BASE64 sections + if ($u7len > 2 && $str[$i+1] == '&' && $str[$i+2] != '-') { + return $err; + } + } + // Not printable US-ASCII + else if (ord($u7) < 0x20 || ord($u7) >= 0x7f) { + return $err; + } + else { + $p .= $u7; + } + } + + return $p; + } + + + /** + * Convert the data ($str) from UTF-8 to RFC 2060's UTF-7. + * Unicode characters above U+FFFF are replaced by U+FFFE. + * If input data is invalid, return an empty string. + * + * Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com> + * + * @param string $str Input string (UTF-8) + * + * @return string Output string (UTF7-IMAP) + */ + public static function utf8_to_utf7imap($str) + { + $B64Chars = array( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', '+', ',' + ); + + $u8len = strlen($str); + $base64 = 0; + $i = 0; + $p = ''; + $err = ''; + + while ($u8len) { + $u8 = $str[$i]; + $c = ord($u8); + + if ($c < 0x80) { + $ch = $c; + $n = 0; + } + else if ($c < 0xc2) { + return $err; + } + else if ($c < 0xe0) { + $ch = $c & 0x1f; + $n = 1; + } + else if ($c < 0xf0) { + $ch = $c & 0x0f; + $n = 2; + } + else if ($c < 0xf8) { + $ch = $c & 0x07; + $n = 3; + } + else if ($c < 0xfc) { + $ch = $c & 0x03; + $n = 4; + } + else if ($c < 0xfe) { + $ch = $c & 0x01; + $n = 5; + } + else { + return $err; + } + + $i++; + $u8len--; + + if ($n > $u8len) { + return $err; + } + + for ($j=0; $j < $n; $j++) { + $o = ord($str[$i+$j]); + if (($o & 0xc0) != 0x80) { + return $err; + } + $ch = ($ch << 6) | ($o & 0x3f); + } + + if ($n > 1 && !($ch >> ($n * 5 + 1))) { + return $err; + } + + $i += $n; + $u8len -= $n; + + if ($ch < 0x20 || $ch >= 0x7f) { + if (!$base64) { + $p .= '&'; + $base64 = 1; + $b = 0; + $k = 10; + } + if ($ch & ~0xffff) { + $ch = 0xfffe; + } + + $p .= $B64Chars[($b | $ch >> $k)]; + $k -= 6; + for (; $k >= 0; $k -= 6) { + $p .= $B64Chars[(($ch >> $k) & 0x3f)]; + } + + $b = ($ch << (-$k)) & 0x3f; + $k += 16; + } + else { + if ($base64) { + if ($k > 10) { + $p .= $B64Chars[$b]; + } + $p .= '-'; + $base64 = 0; + } + + $p .= chr($ch); + if (chr($ch) == '&') { + $p .= '-'; + } + } + } + + if ($base64) { + if ($k > 10) { + $p .= $B64Chars[$b]; + } + $p .= '-'; + } + + return $p; + } + + + /** + * A method to guess character set of a string. + * + * @param string $string String. + * @param string $failover Default result for failover. + * + * @return string Charset name + */ + public static function detect($string, $failover='') + { + if (!function_exists('mb_detect_encoding')) { + return $failover; + } + + // FIXME: the order is important, because sometimes + // iso string is detected as euc-jp and etc. + $enc = array( + 'UTF-8', 'SJIS', 'BIG5', 'GB2312', + 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', + 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', + 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', + 'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', + 'ISO-2022-KR', 'ISO-2022-JP' + ); + + $result = mb_detect_encoding($string, join(',', $enc)); + + return $result ? $result : $failover; + } + + + /** + * Removes non-unicode characters from input. + * + * @param mixed $input String or array. + * + * @return mixed String or array + */ + public static function clean($input) + { + // handle input of type array + if (is_array($input)) { + foreach ($input as $idx => $val) { + $input[$idx] = self::clean($val); + } + return $input; + } + + if (!is_string($input) || $input == '') { + return $input; + } + + // iconv/mbstring are much faster (especially with long strings) + if (function_exists('mb_convert_encoding')) { + if (($res = mb_convert_encoding($input, 'UTF-8', 'UTF-8')) !== false) { + return $res; + } + } + + if (function_exists('iconv')) { + if (($res = @iconv('UTF-8', 'UTF-8//IGNORE', $input)) !== false) { + return $res; + } + } + + $seq = ''; + $out = ''; + $regexp = '/^('. +// '[\x00-\x7F]'. // UTF8-1 + '|[\xC2-\xDF][\x80-\xBF]'. // UTF8-2 + '|\xE0[\xA0-\xBF][\x80-\xBF]'. // UTF8-3 + '|[\xE1-\xEC][\x80-\xBF][\x80-\xBF]'. // UTF8-3 + '|\xED[\x80-\x9F][\x80-\xBF]'. // UTF8-3 + '|[\xEE-\xEF][\x80-\xBF][\x80-\xBF]'. // UTF8-3 + '|\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF]'. // UTF8-4 + '|[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]'.// UTF8-4 + '|\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF]'. // UTF8-4 + ')$/'; + + for ($i = 0, $len = strlen($input); $i < $len; $i++) { + $chr = $input[$i]; + $ord = ord($chr); + + // 1-byte character + if ($ord <= 0x7F) { + if ($seq) { + $out .= preg_match($regexp, $seq) ? $seq : ''; + } + $seq = ''; + $out .= $chr; + // first (or second) byte of multibyte sequence + } + else if ($ord >= 0xC0) { + if (strlen($seq) > 1) { + $out .= preg_match($regexp, $seq) ? $seq : ''; + $seq = ''; + } + else if ($seq && ord($seq) < 0xC0) { + $seq = ''; + } + $seq .= $chr; + // next byte of multibyte sequence + } + else if ($seq) { + $seq .= $chr; + } + } + + if ($seq) { + $out .= preg_match($regexp, $seq) ? $seq : ''; + } + + return $out; + } + +} diff --git a/lib/ext/Roundcube/rcube_config.php b/lib/ext/Roundcube/rcube_config.php new file mode 100644 index 0000000..b9fd955 --- /dev/null +++ b/lib/ext/Roundcube/rcube_config.php @@ -0,0 +1,418 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcube_config.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2008-2012, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Class to read configuration settings | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Configuration class for Roundcube + * + * @package Core + */ +class rcube_config +{ + const DEFAULT_SKIN = 'larry'; + + private $prop = array(); + private $errors = array(); + private $userprefs = array(); + + /** + * Renamed options + * + * @var array + */ + private $legacy_props = array( + // new name => old name + 'default_folders' => 'default_imap_folders', + 'mail_pagesize' => 'pagesize', + 'addressbook_pagesize' => 'pagesize', + 'reply_mode' => 'top_posting', + ); + + + /** + * Object constructor + */ + public function __construct() + { + $this->load(); + + // Defaults, that we do not require you to configure, + // but contain information that is used in various + // locations in the code: + $this->set('contactlist_fields', array('name', 'firstname', 'surname', 'email')); + } + + + /** + * Load config from local config file + * + * @todo Remove global $CONFIG + */ + private function load() + { + // load main config file + if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/main.inc.php')) + $this->errors[] = 'main.inc.php was not found.'; + + // load database config + if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/db.inc.php')) + $this->errors[] = 'db.inc.php was not found.'; + + // load host-specific configuration + $this->load_host_config(); + + // set skin (with fallback to old 'skin_path' property) + if (empty($this->prop['skin'])) { + if (!empty($this->prop['skin_path'])) { + $this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path'])); + } + else { + $this->prop['skin'] = self::DEFAULT_SKIN; + } + } + + // larry is the new default skin :-) + if ($this->prop['skin'] == 'default') + $this->prop['skin'] = self::DEFAULT_SKIN; + + // fix paths + $this->prop['log_dir'] = $this->prop['log_dir'] ? realpath(unslashify($this->prop['log_dir'])) : INSTALL_PATH . 'logs'; + $this->prop['temp_dir'] = $this->prop['temp_dir'] ? realpath(unslashify($this->prop['temp_dir'])) : INSTALL_PATH . 'temp'; + + // fix default imap folders encoding + foreach (array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox') as $folder) + $this->prop[$folder] = rcube_charset::convert($this->prop[$folder], RCMAIL_CHARSET, 'UTF7-IMAP'); + + if (!empty($this->prop['default_folders'])) + foreach ($this->prop['default_folders'] as $n => $folder) + $this->prop['default_folders'][$n] = rcube_charset::convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP'); |