<?php
/**
 * The Serialize:: class provides various methods of encapsulating data.
 *
 * Copyright 2001-2016 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author   Stephane Huther <shuther1@free.fr>
 * @author   Michael Slusarz <slusarz@horde.org>
 * @package  Serialize
 * @category Horde
 */
class Horde_Serialize
{
    /* Type constants */
    const UNKNOWN = -1;
    const NONE = 0;
    const WDDX = 1;
    const BZIP = 2;
    const IMAP8 = 3;
    const IMAPUTF7 = 4;
    const IMAPUTF8 = 5;
    const BASIC = 6;
    const GZ_DEFLATE = 7;
    const GZ_COMPRESS = 8;
    const GZ_ENCODE = 9;
    const BASE64 = 10;
    const RAW = 12;
    const URL = 13;
    const UTF7 = 14;
    const UTF7_BASIC = 15;
    const JSON = 16;
    const LZF = 17;

    /**
     * Serialize a value.
     *
     * See the list of constants at the top of the file for the serializing
     * techniques that can be used.
     *
     * @param mixed $data    The data to be serialized.
     * @param mixed $mode    The mode of serialization. Can be either a single
     *                       mode or array of modes.  If array, will be
     *                       serialized in the order provided.
     * @param mixed $params  Any additional parameters the serialization method
     *                       requires.
     *
     * @return string  The serialized data.
     * @throws Horde_Serialize_Exception
     */
    public static function serialize($data, $mode = array(self::BASIC),
                                     $params = null)
    {
        if (!is_array($mode)) {
            $mode = array($mode);
        }

        /* Parse through the list of serializing modes. */
        foreach ($mode as $val) {
            /* Check to make sure the mode is supported. */
            if (!self::hasCapability($val)) {
                throw new Horde_Serialize_Exception('Unsupported serialization type');
            }
            $data = self::_serialize($data, $val, $params);
        }

        return $data;
    }

    /**
     * Unserialize a value.
     *
     * See the list of constants at the top of the file for the serializing
     * techniques that can be used.
     *
     * @param mixed $data    The data to be unserialized.
     * @param mixed $mode    The mode of unserialization.  Can be either a
     *                       single mode or array of modes.  If array, will be
     *                       unserialized in the order provided.
     * @param mixed $params  Any additional parameters the unserialization
     *                       method requires.
     *
     * @return string  The unserialized data.
     * @throws Horde_Serialize_Exception
     */
    public static function unserialize($data, $mode = self::BASIC,
                                       $params = null)
    {
        if (!is_array($mode)) {
            $mode = array($mode);
        }

        /* Parse through the list of unserializing modes. */
        foreach ($mode as $val) {
            /* Check to make sure the mode is supported. */
            if (!self::hasCapability($val)) {
                throw new Horde_Serialize_Exception('Unsupported unserialization type');
            }
            $data = self::_unserialize($data, $val, $params);
        }

        return $data;
    }

    /**
     * Check whether or not a serialization method is supported.
     *
     * @param integer $mode  The serialization method.
     *
     * @return boolean  True if supported, false if not.
     */
    public static function hasCapability($mode)
    {
        switch ($mode) {
        case self::BZIP:
            return Horde_Util::extensionExists('bz2');

        case self::WDDX:
            return Horde_Util::extensionExists('wddx');

        case self::IMAPUTF7:
            return class_exists('Horde_Imap_Client');

        case self::IMAPUTF8:
            return class_exists('Horde_Mime');

        case self::GZ_DEFLATE:
        case self::GZ_COMPRESS:
        case self::GZ_ENCODE:
            return Horde_Util::extensionExists('zlib');

        case self::LZF:
            return Horde_Util::extensionExists('lzf');

        case self::NONE:
        case self::BASIC:
        case self::BASE64:
        case self::IMAP8:
        case self::RAW:
        case self::URL:
        case self::UTF7:
        case self::UTF7_BASIC:
        case self::JSON:
            return true;

        default:
            return false;
        }
    }

    /**
     * Serialize data.
     *
     * @param mixed $data    The data to be serialized.
     * @param mixed $mode    The mode of serialization. Can be
     *                       either a single mode or array of modes.
     *                       If array, will be serialized in the
     *                       order provided.
     * @param mixed $params  Any additional parameters the serialization method
     *                       requires.
     *
     * @return string  A serialized string.
     * @throws Horde_Serialize_Exception
     */
    protected static function _serialize($data, $mode, $params = null)
    {
        switch ($mode) {
        case self::NONE:
            break;

        // $params['level'] = Level of compression (default: 3)
        // $params['workfactor'] = How does compression phase behave when given
        //                         worst case, highly repetitive, input data
        //                         (default: 30)
        case self::BZIP:
            $data = bzcompress($data, isset($params['level']) ? $params['level'] : 3, isset($params['workfactor']) ? $params['workfactor'] : 30);
            if (is_integer($data)) {
                $data = false;
            }
            break;

        case self::WDDX:
            $data = wddx_serialize_value($data);
            break;

        case self::IMAP8:
            $data = quoted_printable_encode($data);
            break;

        case self::IMAPUTF7:
            $data = Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap(Horde_String::convertCharset($data, 'ISO-8859-1', 'UTF-8'));
            break;

        case self::IMAPUTF8:
            $data = Horde_Mime::decode($data);
            break;

        // $params['level'] = Level of compression (default: 3)
        case self::GZ_DEFLATE:
            $data = gzdeflate($data, isset($params['level']) ? $params['level'] : 3);
            break;

        case self::BASIC:
            $data = serialize($data);
            break;

        // $params['level'] = Level of compression (default: 3)
        case self::GZ_COMPRESS:
            $data = gzcompress($data, isset($params['level']) ? $params['level'] : 3);
            break;

        case self::BASE64:
            $data = base64_encode($data);
            break;

        // $params['level'] = Level of compression (default: 3)
        case self::GZ_ENCODE:
            $data = gzencode($data, isset($params['level']) ? $params['level'] : 3);
            break;

        case self::RAW:
            $data = rawurlencode($data);
            break;

        case self::URL:
            $data = urlencode($data);
            break;

        // $params = Source character set
        case self::UTF7:
            $data = Horde_String::convertCharset($data, $params, 'UTF-7');
            break;

        // $params = Source character set
        case self::UTF7_BASIC:
            $data = self::serialize($data, array(self::UTF7, self::BASIC), $params);
            break;

        case self::JSON:
            $tmp = json_encode($data);

            /* Basic error handling attempts.
             * TODO: JSON_ERROR_UTF8 = 5; available as of PHP 5.3.3 */
            if (json_last_error() === 5) {
                $data = json_encode(Horde_String::convertCharset($data, $params, 'UTF-8', true));
            } else {
                $data = $tmp;
            }
            break;

        case self::LZF:
            $data = lzf_compress($data);
            break;
        }

        if ($data === false) {
            throw new Horde_Serialize_Exception('Serialization failed.');
        }
        return $data;
    }

    /**
     * Unserialize data.
     *
     * @param mixed $data    The data to be unserialized.
     * @param mixed $mode    The mode of unserialization. Can be either a
     *                       single mode or array of modes.  If array, will be
     *                       unserialized in the order provided.
     * @param mixed $params  Any additional parameters the unserialization
     *                       method requires.
     *
     * @return mixed  Unserialized data.
     * @throws Horde_Serialize_Exception
     */
    protected static function _unserialize(&$data, $mode, $params = null)
    {
        switch ($mode) {
        case self::NONE:
            break;

        case self::RAW:
            $data = rawurldecode($data);
            break;

        case self::URL:
            $data = urldecode($data);
            break;

        case self::WDDX:
            $data = wddx_deserialize($data);
            break;

        case self::BZIP:
            // $params['small'] = Use bzip2 'small memory' mode?
            $data = bzdecompress($data, isset($params['small']) ? $params['small'] : false);
            break;

        case self::IMAP8:
            $data = quoted_printable_decode($data);
            break;

        case self::IMAPUTF7:
            $data = Horde_String::convertCharset(Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($data), 'UTF-8', 'ISO-8859-1');
            break;

        case self::IMAPUTF8:
            $data = Horde_Mime::encode($data);
            break;

        case self::BASIC:
            $data2 = @unserialize($data);
            // Unserialize can return false both on error and if $data is the
            // false value.
            if (($data2 === false) && ($data == serialize(false))) {
                return $data2;
            }
            $data = $data2;
            break;

        case self::GZ_DEFLATE:
            $data = gzinflate($data);
            break;

        case self::BASE64:
            $data = base64_decode($data);
            break;

        case self::GZ_COMPRESS:
            $data = gzuncompress($data);
            break;

        // $params = Output character set
        case self::UTF7:
            $data = Horde_String::convertCharset($data, 'utf-7', $params);
            break;

        // $params = Output character set
        case self::UTF7_BASIC:
            $data = self::unserialize($data, array(self::BASIC, self::UTF7), $params);
            break;

        case self::JSON:
            $out = json_decode($data);
            if (!is_null($out) || (strcasecmp($data, 'null') === 0)) {
                return $out;
            }
            break;

        case self::LZF:
            $data = @lzf_decompress($data);
            break;
        }

        if ($data === false) {
            throw new Horde_Serialize_Exception('Unserialization failed.');
        }

        return $data;
    }

}
