<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Ldap\Converter;

use 
DateTime;
use 
DateTimeZone;
use 
Zend\Stdlib\ErrorHandler;

/**
 * Zend\Ldap\Converter is a collection of useful LDAP related conversion functions.
 */
class Converter
{
    const 
STANDARD         0;
    const 
BOOLEAN          1;
    const 
GENERALIZED_TIME 2;

    
/**
     * Converts all ASCII chars < 32 to "\HEX"
     *
     * @see    Net_LDAP2_Util::asc2hex32() from Benedikt Hallinger <beni@php.net>
     * @link   http://pear.php.net/package/Net_LDAP2
     * @author Benedikt Hallinger <beni@php.net>
     *
     * @param string $string String to convert
     * @return string
     */
    
public static function ascToHex32($string)
    {
        for (
$i 0$len strlen($string); $i $len$i++) {
            
$char substr($string$i1);
            if (
ord($char) < 32) {
                
$hex dechex(ord($char));
                if (
strlen($hex) == 1) {
                    
$hex '0' $hex;
                }
                
$string str_replace($char'\\' $hex$string);
            }
        }
        return 
$string;
    }

    
/**
     * Converts all Hex expressions ("\HEX") to their original ASCII characters
     *
     * @see    Net_LDAP2_Util::hex2asc() from Benedikt Hallinger <beni@php.net>,
     *         heavily based on work from DavidSmith@byu.net
     * @link   http://pear.php.net/package/Net_LDAP2
     * @author Benedikt Hallinger <beni@php.net>, heavily based on work from DavidSmith@byu.net
     *
     * @param string $string String to convert
     * @return string
     */
    
public static function hex32ToAsc($string)
    {
        
$string preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', function ($matches) {
            return 
chr(hexdec($matches[1]));
        }, 
$string);
        return 
$string;
    }


    
/**
     * Convert any value to an LDAP-compatible value.
     *
     * By setting the <var>$type</var>-parameter the conversion of a certain
     * type can be forced
     *
     * @todo write more tests
     *
     * @param mixed $value The value to convert
     * @param int   $type  The conversion type to use
     * @return string|null
     * @throws Exception\ConverterException
     */
    
public static function toLdap($value$type self::STANDARD)
    {
        try {
            switch (
$type) {
                case 
self::BOOLEAN:
                    return static::
toldapBoolean($value);
                    break;
                case 
self::GENERALIZED_TIME:
                    return static::
toLdapDatetime($value);
                    break;
                default:
                    if (
is_string($value)) {
                        return 
$value;
                    } elseif (
is_int($value) || is_float($value)) {
                        return (string) 
$value;
                    } elseif (
is_bool($value)) {
                        return static::
toldapBoolean($value);
                    } elseif (
is_object($value)) {
                        if (
$value instanceof DateTime) {
                            return static::
toLdapDatetime($value);
                        } else {
                            return static::
toLdapSerialize($value);
                        }
                    } elseif (
is_array($value)) {
                        return static::
toLdapSerialize($value);
                    } elseif (
is_resource($value) && get_resource_type($value) === 'stream') {
                        return 
stream_get_contents($value);
                    } else {
                        return 
null;
                    }
                    break;
            }
        } catch (\
Exception $e) {
            throw new 
Exception\ConverterException($e->getMessage(), $e->getCode(), $e);
        }
    }

    
/**
     * Converts a date-entity to an LDAP-compatible date-string
     *
     * The date-entity <var>$date</var> can be either a timestamp, a
     * DateTime Object, a string that is parseable by strtotime().
     *
     * @param integer|string|DateTime $date  The date-entity
     * @param  bool                 $asUtc Whether to return the LDAP-compatible date-string as UTC or as local value
     * @return string
     * @throws Exception\InvalidArgumentException
     */
    
public static function toLdapDateTime($date$asUtc true)
    {
        if (!(
$date instanceof DateTime)) {
            if (
is_int($date)) {
                
$date = new DateTime('@' $date);
                
$date->setTimezone(new DateTimeZone(date_default_timezone_get()));
            } elseif (
is_string($date)) {
                
$date = new DateTime($date);
            } else {
                throw new 
Exception\InvalidArgumentException('Parameter $date is not of the expected type');
            }
        }
        
$timezone $date->format('O');
        if (
true === $asUtc) {
            
$date->setTimezone(new DateTimeZone('UTC'));
            
$timezone 'Z';
        }
        if (
'+0000' === $timezone) {
            
$timezone 'Z';
        }
        return 
$date->format('YmdHis') . $timezone;
    }

    
/**
     * Convert a boolean value to an LDAP-compatible string
     *
     * This converts a boolean value of TRUE, an integer-value of 1 and a
     * case-insensitive string 'true' to an LDAP-compatible 'TRUE'. All other
     * other values are converted to an LDAP-compatible 'FALSE'.
     *
     * @param  bool|integer|string $value The boolean value to encode
     * @return string
     */
    
public static function toLdapBoolean($value)
    {
        
$return 'FALSE';
        if (!
is_scalar($value)) {
            return 
$return;
        }
        if (
true === $value || 'true' === strtolower($value) || === $value) {
            
$return 'TRUE';
        }
        return 
$return;
    }

    
/**
     * Serialize any value for storage in LDAP
     *
     * @param mixed $value The value to serialize
     * @return string
     */
    
public static function toLdapSerialize($value)
    {
        return 
serialize($value);
    }

    
/**
     * Convert an LDAP-compatible value to a corresponding PHP-value.
     *
     * By setting the <var>$type</var>-parameter the conversion of a certain
     * type can be forced.
     *
     * @see Converter::STANDARD
     * @see Converter::BOOLEAN
     * @see Converter::GENERALIZED_TIME
     * @param string  $value         The value to convert
     * @param int     $type          The conversion type to use
     * @param  bool $dateTimeAsUtc Return DateTime values in UTC timezone
     * @return mixed
     */
    
public static function fromLdap($value$type self::STANDARD$dateTimeAsUtc true)
    {
        switch (
$type) {
            case 
self::BOOLEAN:
                return static::
fromldapBoolean($value);
                break;
            case 
self::GENERALIZED_TIME:
                return static::
fromLdapDateTime($value);
                break;
            default:
                if (
is_numeric($value)) {
                    
// prevent numeric values to be treated as date/time
                    
return $value;
                } elseif (
'TRUE' === $value || 'FALSE' === $value) {
                    return static::
fromLdapBoolean($value);
                }
                if (
preg_match('/^\d{4}[\d\+\-Z\.]*$/'$value)) {
                    return static::
fromLdapDateTime($value$dateTimeAsUtc);
                }
                try {
                    return static::
fromLdapUnserialize($value);
                } catch (
Exception\UnexpectedValueException $e) {
                    
// Do nothing
                
}
                break;
        }

        return 
$value;
    }

    
/**
     * Convert an LDAP-Generalized-Time-entry into a DateTime-Object
     *
     * CAVEAT: The DateTime-Object returned will always be set to UTC-Timezone.
     *
     * @param string  $date  The generalized-Time
     * @param  bool $asUtc Return the DateTime with UTC timezone
     * @return DateTime
     * @throws Exception\InvalidArgumentException if a non-parseable-format is given
     */
    
public static function fromLdapDateTime($date$asUtc true)
    {
        
$datepart = array();
        if (!
preg_match('/^(\d{4})/'$date$datepart)) {
            throw new 
Exception\InvalidArgumentException('Invalid date format found');
        }

        if (
$datepart[1] < 4) {
            throw new 
Exception\InvalidArgumentException('Invalid date format found (too short)');
        }

        
$time = array(
            
// The year is mandatory!
            
'year'          => $datepart[1],
            
'month'         => 1,
            
'day'           => 1,
            
'hour'          => 0,
            
'minute'        => 0,
            
'second'        => 0,
            
'offdir'        => '+',
            
'offsethours'   => 0,
            
'offsetminutes' => 0
        
);

        
$length strlen($date);

        
// Check for month.
        
if ($length >= 6) {
            
$month substr($date42);
            if (
$month || $month 12) {
                throw new 
Exception\InvalidArgumentException('Invalid date format found (invalid month)');
            }
            
$time['month'] = $month;
        }

        
// Check for day
        
if ($length >= 8) {
            
$day substr($date62);
            if (
$day || $day 31) {
                throw new 
Exception\InvalidArgumentException('Invalid date format found (invalid day)');
            }
            
$time['day'] = $day;
        }

        
// Check for Hour
        
if ($length >= 10) {
            
$hour substr($date82);
            if (
$hour || $hour 23) {
                throw new 
Exception\InvalidArgumentException('Invalid date format found (invalid hour)');
            }
            
$time['hour'] = $hour;
        }

        
// Check for minute
        
if ($length >= 12) {
            
$minute substr($date102);
            if (
$minute || $minute 59) {
                throw new 
Exception\InvalidArgumentException('Invalid date format found (invalid minute)');
            }
            
$time['minute'] = $minute;
        }

        
// Check for seconds
        
if ($length >= 14) {
            
$second substr($date122);
            if (
$second || $second 59) {
                throw new 
Exception\InvalidArgumentException('Invalid date format found (invalid second)');
            }
            
$time['second'] = $second;
        }

        
// Set Offset
        
$offsetRegEx '/([Z\-\+])(\d{2}\'?){0,1}(\d{2}\'?){0,1}$/';
        
$off         = array();
        if (
preg_match($offsetRegEx$date$off)) {
            
$offset $off[1];
            if (
$offset == '+' || $offset == '-') {
                
$time['offdir'] = $offset;
                
// we have an offset, so lets calculate it.
                
if (isset($off[2])) {
                    
$offsetHours substr($off[2], 02);
                    if (
$offsetHours || $offsetHours 12) {
                        throw new 
Exception\InvalidArgumentException('Invalid date format found (invalid offset hour)');
                    }
                    
$time['offsethours'] = $offsetHours;
                }
                if (isset(
$off[3])) {
                    
$offsetMinutes substr($off[3], 02);
                    if (
$offsetMinutes || $offsetMinutes 59) {
                        throw new 
Exception\InvalidArgumentException('Invalid date format found (invalid offset minute)');
                    }
                    
$time['offsetminutes'] = $offsetMinutes;
                }
            }
        }

        
// Raw-Data is present, so lets create a DateTime-Object from it.
        
$offset     $time['offdir']
                      . 
str_pad($time['offsethours'], 2'0'STR_PAD_LEFT)
                      . 
str_pad($time['offsetminutes'], 2'0'STR_PAD_LEFT);
        
$timestring $time['year'] . '-'
                      
str_pad($time['month'], 2'0'STR_PAD_LEFT) . '-'
                      
str_pad($time['day'], 2'0'STR_PAD_LEFT) . ' '
                      
str_pad($time['hour'], 2'0'STR_PAD_LEFT) . ':'
                      
str_pad($time['minute'], 2'0'STR_PAD_LEFT) . ':'
                      
str_pad($time['second'], 2'0'STR_PAD_LEFT)
                      . 
$time['offdir']
                      . 
str_pad($time['offsethours'], 2'0'STR_PAD_LEFT)
                      . 
str_pad($time['offsetminutes'], 2'0'STR_PAD_LEFT);
        
$date       = new DateTime($timestring);
        if (
$asUtc) {
            
$date->setTimezone(new DateTimeZone('UTC'));
        }
        return 
$date;
    }

    
/**
     * Convert an LDAP-compatible boolean value into a PHP-compatible one
     *
     * @param string $value The value to convert
     * @return bool
     * @throws Exception\InvalidArgumentException
     */
    
public static function fromLdapBoolean($value)
    {
        if (
'TRUE' === $value) {
            return 
true;
        } elseif (
'FALSE' === $value) {
            return 
false;
        } else {
            throw new 
Exception\InvalidArgumentException('The given value is not a boolean value');
        }
    }

    
/**
     * Unserialize a serialized value to return the corresponding object
     *
     * @param string $value The value to convert
     * @return mixed
     * @throws Exception\UnexpectedValueException
     */
    
public static function fromLdapUnserialize($value)
    {
        
ErrorHandler::start(E_NOTICE);
        
$v unserialize($value);
        
ErrorHandler::stop();

        if (
false === $v && $value != 'b:0;') {
            throw new 
Exception\UnexpectedValueException('The given value could not be unserialized');
        }
        return 
$v;
    }
}