Net_LDAP2
[ class tree: Net_LDAP2 ] [ index: Net_LDAP2 ] [ all elements ]

Source for file Util.php

Documentation is available at Util.php

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4: */
  3.  
  4. require_once 'PEAR.php';
  5.  
  6. /**
  7. * Utility Class for Net_LDAP2
  8. *
  9. * This class servers some functionality to the other classes of Net_LDAP2 but most of
  10. * the methods can be used separately as well.
  11. *
  12. @category Net
  13. @package  Net_LDAP2
  14. @author   Benedikt Hallinger <beni@php.net>
  15. @license  http://www.gnu.org/copyleft/lesser.html LGPL
  16. @version  CVS: $Id: Util.php,v 1.2 2008/03/20 09:32:39 beni Exp $
  17. @link     http://pear.php.net/package/Net_LDAP22/
  18. */
  19. class Net_LDAP2_Util extends PEAR
  20. {
  21.     /**
  22.      * Constructor
  23.      *
  24.      * @access public
  25.      */
  26.     public function __construct()
  27.     {
  28.          // We do nothing here, since all methods can be called statically.
  29.          // In Net_LDAP <= 0.7, we needed a instance of Util, because
  30.          // it was possible to do utf8 encoding and decoding, but this
  31.          // has been moved to the LDAP class. The constructor remains only
  32.          // here to document the downward compatibility of creating an instance.
  33.     }
  34.  
  35.     /**
  36.     * Explodes the given DN into its elements
  37.     *
  38.     * {@link http://www.ietf.org/rfc/rfc2253.txt RFC 2253} says, a Distinguished Name is a sequence
  39.     * of Relative Distinguished Names (RDNs), which themselves
  40.     * are sets of Attributes. For each RDN a array is constructed where the RDN part is stored.
  41.     *
  42.     * For example, the DN 'OU=Sales+CN=J. Smith,DC=example,DC=net' is exploded to:
  43.     * <kbd>array( [0] => array([0] => 'OU=Sales', [1] => 'CN=J. Smith'), [2] => 'DC=example', [3] => 'DC=net' )</kbd>
  44.     *
  45.     * [NOT IMPLEMENTED] DNs might also contain values, which are the bytes of the BER encoding of
  46.     * the X.500 AttributeValue rather than some LDAP string syntax. These values are hex-encoded
  47.     * and prefixed with a #. To distinguish such BER values, ldap_explode_dn uses references to
  48.     * the actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is exploded to:
  49.     * [ { '1.3.6.1.4.1.1466.0' => "\004\002Hi" }, { 'DC' => 'example' }, { 'DC' => 'com' } ];
  50.     * See {@link http://www.vijaymukhi.com/vmis/berldap.htm} for more information on BER.
  51.     *
  52.     *  It also performs the following operations on the given DN:
  53.     *   - Unescape "\" followed by ",", "+", """, "\", "<", ">", ";", "#", "=", " ", or a hexpair
  54.     *     and strings beginning with "#".
  55.     *   - Removes the leading 'OID.' characters if the type is an OID instead of a name.
  56.     *   - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order.
  57.     *
  58.     * OPTIONS is a list of name/value pairs, valid options are:
  59.     *   casefold    Controls case folding of attribute types names.
  60.     *               Attribute values are not affected by this option.
  61.     *               The default is to uppercase. Valid values are:
  62.     *               lower        Lowercase attribute types names.
  63.     *               upper        Uppercase attribute type names. This is the default.
  64.     *               none         Do not change attribute type names.
  65.     *   reverse     If TRUE, the RDN sequence is reversed.
  66.     *   onlyvalues  If TRUE, then only attributes values are returned ('foo' instead of 'cn=foo')
  67.     *
  68.  
  69.     * @param string $dn      The DN that should be exploded
  70.     * @param array  $options Options to use
  71.     *
  72.     * @static
  73.     * @return array   Parts of the exploded DN
  74.     * @todo implement BER
  75.     */
  76.     public static function ldap_explode_dn($dn$options = array('casefold' => 'upper'))
  77.     {
  78.         if (!isset($options['onlyvalues'])) $options['onlyvalues']  = false;
  79.         if (!isset($options['reverse']))    $options['reverse']     = false;
  80.         if (!isset($options['casefold']))   $options['casefold']    'upper';
  81.  
  82.         // Escaping of DN and stripping of "OID."
  83.         $dn = self::canonical_dn($dnarray('casefold' => $options['casefold']));
  84.  
  85.         // splitting the DN
  86.         $dn_array preg_split('/(?<=[^\\\\]),/'$dn);
  87.  
  88.         // clear wrong splitting (possibly we have split too much)
  89.         // /!\ Not clear, if this is neccessary here
  90.         //$dn_array = self::_correct_dn_splitting($dn_array, ',');
  91.  
  92.         // construct subarrays for multivalued RDNs and unescape DN value
  93.         // also convert to output format and apply casefolding
  94.         foreach ($dn_array as $key => $value{
  95.             $value_u = self::unescape_dn_value($value);
  96.             $rdns    = self::split_rdn_multival($value_u[0]);
  97.             if (count($rdns> 1{
  98.                 // MV RDN!
  99.                 foreach ($rdns as $subrdn_k => $subrdn_v{
  100.                     // Casefolding
  101.                     if ($options['casefold'== 'upper'$subrdn_v preg_replace("/^(\w+=)/e""''.strtoupper('\\1').''"$subrdn_v);
  102.                     if ($options['casefold'== 'lower'$subrdn_v preg_replace("/^(\w+=)/e""''.strtolower('\\1').''"$subrdn_v);
  103.  
  104.                     if ($options['onlyvalues']{
  105.                         preg_match('/(.+?)(?<!\\\\)=(.+)/'$subrdn_v$matches);
  106.                         $rdn_ocl         $matches[1];
  107.                         $rdn_val         $matches[2];
  108.                         $unescaped       = self::unescape_dn_value($rdn_val);
  109.                         $rdns[$subrdn_k$unescaped[0];
  110.                     else {
  111.                         $unescaped = self::unescape_dn_value($subrdn_v);
  112.                         $rdns[$subrdn_k$unescaped[0];
  113.                     }
  114.                 }
  115.  
  116.                 $dn_array[$key$rdns;
  117.             else {
  118.                 // normal RDN
  119.  
  120.                 // Casefolding
  121.                 if ($options['casefold'== 'upper'$value preg_replace("/^(\w+=)/e""''.strtoupper('\\1').''"$value);
  122.                 if ($options['casefold'== 'lower'$value preg_replace("/^(\w+=)/e""''.strtolower('\\1').''"$value);
  123.  
  124.                 if ($options['onlyvalues']{
  125.                     preg_match('/(.+?)(?<!\\\\)=(.+)/'$value$matches);
  126.                     $dn_ocl         $matches[1];
  127.                     $dn_val         $matches[2];
  128.                     $unescaped      = self::unescape_dn_value($dn_val);
  129.                     $dn_array[$key$unescaped[0];
  130.                 else {
  131.                     $unescaped = self::unescape_dn_value($value);
  132.                     $dn_array[$key$unescaped[0];
  133.                 }
  134.             }
  135.         }
  136.  
  137.         if ($options['reverse']{
  138.             return array_reverse($dn_array);
  139.         else {
  140.             return $dn_array;
  141.         }
  142.     }
  143.  
  144.     /**
  145.     * Escapes a DN value according to RFC 2253
  146.     *
  147.     * Escapes the given VALUES according to RFC 2253 so that they can be safely used in LDAP DNs.
  148.     * The characters ",", "+", """, "\", "<", ">", ";", "#", "=" with a special meaning in RFC 2252
  149.     * are preceeded by ba backslash. Control characters with an ASCII code < 32 are represented as \hexpair.
  150.     * Finally all leading and trailing spaces are converted to sequences of \20.
  151.     *
  152.     * @param array $values An array containing the DN values that should be escaped
  153.     *
  154.     * @static
  155.     * @return array The array $values, but escaped
  156.     */
  157.     public static function escape_dn_value($values = array())
  158.     {
  159.         // Parameter validation
  160.         if (!is_array($values)) {
  161.             $values = array($values);
  162.         }
  163.  
  164.         foreach ($values as $key => $val{
  165.             // Escaping of filter meta characters
  166.             $val str_replace('\\''\\\\'$val);
  167.             $val str_replace(',',    '\,'$val);
  168.             $val str_replace('+',    '\+'$val);
  169.             $val str_replace('"',    '\"'$val);
  170.             $val str_replace('<',    '\<'$val);
  171.             $val str_replace('>',    '\>'$val);
  172.             $val str_replace(';',    '\;'$val);
  173.             $val str_replace('#',    '\#'$val);
  174.             $val str_replace('=',    '\='$val);
  175.  
  176.             // ASCII < 32 escaping
  177.             $val = self::asc2hex32($val);
  178.  
  179.             // Convert all leading and trailing spaces to sequences of \20.
  180.             if (preg_match('/^(\s*)(.+?)(\s*)$/'$val$matches)) {
  181.                 $val $matches[2];
  182.                 for ($i = 0; $i strlen($matches[1])$i++{
  183.                     $val '\20'.$val;
  184.                 }
  185.                 for ($i = 0; $i strlen($matches[3])$i++{
  186.                     $val $val.'\20';
  187.                 }
  188.             }
  189.  
  190.             if (null === $val$val '\0';  // apply escaped "null" if string is empty
  191.  
  192.             $values[$key$val;
  193.         }
  194.  
  195.         return $values;
  196.     }
  197.  
  198.     /**
  199.     * Undoes the conversion done by escape_dn_value().
  200.     *
  201.     * Any escape sequence starting with a baskslash - hexpair or special character -
  202.     * will be transformed back to the corresponding character.
  203.     *
  204.     * @param array $values Array of DN Values
  205.     *
  206.     * @return array Same as $values, but unescaped
  207.     * @static
  208.     */
  209.     public static function unescape_dn_value($values = array())
  210.     {
  211.         // Parameter validation
  212.         if (!is_array($values)) {
  213.             $values = array($values);
  214.         }
  215.  
  216.         foreach ($values as $key => $val{
  217.             // strip slashes from special chars
  218.             $val str_replace('\\\\''\\'$val);
  219.             $val str_replace('\,',    ','$val);
  220.             $val str_replace('\+',    '+'$val);
  221.             $val str_replace('\"',    '"'$val);
  222.             $val str_replace('\<',    '<'$val);
  223.             $val str_replace('\>',    '>'$val);
  224.             $val str_replace('\;',    ';'$val);
  225.             $val str_replace('\#',    '#'$val);
  226.             $val str_replace('\=',    '='$val);
  227.  
  228.             // Translate hex code into ascii
  229.             $values[$key= self::hex2asc($val);
  230.         }
  231.  
  232.         return $values;
  233.     }
  234.  
  235.     /**
  236.     * Returns the given DN in a canonical form
  237.     *
  238.     * Returns false if DN is not a valid Distinguished Name.
  239.     * DN can either be a string or an array
  240.     * as returned by ldap_explode_dn, which is useful when constructing a DN.
  241.     * The DN array may have be indexed (each array value is a OCL=VALUE pair)
  242.     * or associative (array key is OCL and value is VALUE).
  243.     *
  244.     * It performs the following operations on the given DN:
  245.     *     - Removes the leading 'OID.' characters if the type is an OID instead of a name.
  246.     *     - Escapes all RFC 2253 special characters (",", "+", """, "\", "<", ">", ";", "#", "="), slashes ("/"), and any other character where the ASCII code is < 32 as \hexpair.
  247.     *     - Converts all leading and trailing spaces in values to be \20.
  248.     *     - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order.
  249.     *
  250.     * OPTIONS is a list of name/value pairs, valid options are:
  251.     *     casefold    Controls case folding of attribute type names.
  252.     *                 Attribute values are not affected by this option. The default is to uppercase.
  253.     *                 Valid values are:
  254.     *                 lower        Lowercase attribute type names.
  255.     *                 upper        Uppercase attribute type names. This is the default.
  256.     *                 none         Do not change attribute type names.
  257.     *     [NOT IMPLEMENTED] mbcescape   If TRUE, characters that are encoded as a multi-octet UTF-8 sequence will be escaped as \(hexpair){2,*}.
  258.     *     reverse     If TRUE, the RDN sequence is reversed.
  259.     *     separator   Separator to use between RDNs. Defaults to comma (',').
  260.     *
  261.     * Note: The empty string "" is a valid DN, so be sure not to do a "$can_dn == false" test,
  262.     *       because an empty string evaluates to false. Use the "===" operator instead.
  263.     *
  264.     * @param array|string$dn      The DN
  265.     * @param array        $options Options to use
  266.     *
  267.     * @static
  268.     * @return false|stringThe canonical DN or FALSE
  269.     * @todo implement option mbcescape
  270.     */
  271.     public static function canonical_dn($dn$options = array('casefold' => 'upper''separator' => ','))
  272.     {
  273.         if ($dn === ''return $dn;  // empty DN is valid!
  274.  
  275.         // options check
  276.         if (!isset($options['reverse'])) {
  277.             $options['reverse'= false;
  278.         else {
  279.             $options['reverse'= true;
  280.         }
  281.         if (!isset($options['casefold']))  $options['casefold''upper';
  282.         if (!isset($options['separator'])) $options['separator'',';
  283.  
  284.  
  285.         if (!is_array($dn)) {
  286.             // It is not clear to me if the perl implementation splits by the user defined
  287.             // separator or if it just uses this separator to construct the new DN
  288.             $dn preg_split('/(?<=[^\\\\])'.$options['separator'].'/'$dn);
  289.  
  290.             // clear wrong splitting (possibly we have split too much)
  291.             $dn = self::_correct_dn_splitting($dn$options['separator']);
  292.         else {
  293.             // Is array, check, if the array is indexed or associative
  294.             $assoc = false;
  295.             foreach ($dn as $dn_key => $dn_part{
  296.                 if (!is_int($dn_key)) {
  297.                     $assoc = true;
  298.                 }
  299.             }
  300.             // convert to indexed, if associative array detected
  301.             if ($assoc{
  302.                 $newdn = array();
  303.                 foreach ($dn as $dn_key => $dn_part{
  304.                     if (is_array($dn_part)) {
  305.                         ksort($dn_partSORT_STRING)// we assume here, that the rdn parts are also associative
  306.                         $newdn[$dn_part;  // copy array as-is, so we can resolve it later
  307.                     else {
  308.                         $newdn[$dn_key.'='.$dn_part;
  309.                     }
  310.                 }
  311.                 $dn =$newdn;
  312.             }
  313.         }
  314.  
  315.         // Escaping and casefolding
  316.         foreach ($dn as $pos => $dnval{
  317.             if (is_array($dnval)) {
  318.                 // subarray detected, this means very surely, that we had
  319.                 // a multivalued dn part, which must be resolved
  320.                 $dnval_new '';
  321.                 foreach ($dnval as $subkey => $subval{
  322.                     // build RDN part
  323.                     if (!is_int($subkey)) {
  324.                         $subval $subkey.'='.$subval;
  325.                     }
  326.                     $subval_processed = self::canonical_dn($subval);
  327.                     if (false === $subval_processedreturn false;
  328.                     $dnval_new .= $subval_processed.'+';
  329.                 }
  330.                 $dn[$possubstr($dnval_new0-1)// store RDN part, strip last plus
  331.             else {
  332.                 // try to split multivalued RDNS into array
  333.                 $rdns = self::split_rdn_multival($dnval);
  334.                 if (count($rdns> 1{
  335.                     // Multivalued RDN was detected!
  336.                     // The RDN value is expected to be correctly split by split_rdn_multival().
  337.                     // It's time to sort the RDN and build the DN!
  338.                     $rdn_string '';
  339.                     sort($rdnsSORT_STRING)// Sort RDN keys alphabetically
  340.                     foreach ($rdns as $rdn{
  341.                         $subval_processed = self::canonical_dn($rdn);
  342.                         if (false === $subval_processedreturn false;
  343.                         $rdn_string .= $subval_processed.'+';
  344.                     }
  345.  
  346.                     $dn[$possubstr($rdn_string0-1)// store RDN part, strip last plus
  347.  
  348.                 else {
  349.                     // no multivalued RDN!
  350.                     // split at first unescaped "="
  351.                     $dn_comp preg_split('/(?<=[^\\\\])=/'$rdns[0]2);
  352.                     $ocl     ltrim($dn_comp[0]);  // trim left whitespaces 'cause of "cn=foo, l=bar" syntax (whitespace after comma)
  353.                     $val     $dn_comp[1];
  354.  
  355.                     // strip 'OID.', otherwise apply casefolding and escaping
  356.                     if (substr(strtolower($ocl)04== 'oid.'{
  357.                         $ocl substr($ocl4);
  358.                     else {
  359.                         if ($options['casefold'== 'upper'$ocl strtoupper($ocl);
  360.                         if ($options['casefold'== 'lower'$ocl strtolower($ocl);
  361.                         $ocl = self::escape_dn_value(array($ocl));
  362.                         $ocl $ocl[0];
  363.                     }
  364.  
  365.                     // escaping of dn-value
  366.                     $val = self::escape_dn_value(array($val));
  367.                     $val str_replace('/''\/'$val[0]);
  368.  
  369.                     $dn[$pos$ocl.'='.$val;
  370.                 }
  371.             }
  372.         }
  373.  
  374.         if ($options['reverse']$dn array_reverse($dn);
  375.         return implode($options['separator']$dn);
  376.     }
  377.  
  378.     /**
  379.     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
  380.     *
  381.     * Any control characters with an ACII code < 32 as well as the characters with special meaning in
  382.     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
  383.     * backslash followed by two hex digits representing the hexadecimal value of the character.
  384.     *
  385.     * @param array $values Array of values to escape
  386.     *
  387.     * @static
  388.     * @return array Array $values, but escaped
  389.     */
  390.     public static function escape_filter_value($values = array())
  391.     {
  392.         // Parameter validation
  393.         if (!is_array($values)) {
  394.             $values = array($values);
  395.         }
  396.  
  397.         foreach ($values as $key => $val{
  398.             // Escaping of filter meta characters
  399.             $val str_replace('\\''\5c'$val);
  400.             $val str_replace('*',  '\2a'$val);
  401.             $val str_replace('(',  '\28'$val);
  402.             $val str_replace(')',  '\29'$val);
  403.  
  404.             // ASCII < 32 escaping
  405.             $val = self::asc2hex32($val);
  406.  
  407.             if (null === $val$val '\0';  // apply escaped "null" if string is empty
  408.  
  409.             $values[$key$val;
  410.         }
  411.  
  412.         return $values;
  413.     }
  414.  
  415.     /**
  416.     * Undoes the conversion done by {@link escape_filter_value()}.
  417.     *
  418.     * Converts any sequences of a backslash followed by two hex digits into the corresponding character.
  419.     *
  420.     * @param array $values Array of values to escape
  421.     *
  422.     * @static
  423.     * @return array Array $values, but unescaped
  424.     */
  425.     public static function unescape_filter_value($values = array())
  426.     {
  427.         // Parameter validation
  428.         if (!is_array($values)) {
  429.             $values = array($values);
  430.         }
  431.  
  432.         foreach ($values as $key => $value{
  433.             // Translate hex code into ascii
  434.             $values[$key= self::hex2asc($value);
  435.         }
  436.  
  437.         return $values;
  438.     }
  439.  
  440.     /**
  441.     * Converts all ASCII chars < 32 to "\HEX"
  442.     *
  443.     * @param string $string String to convert
  444.     *
  445.     * @static
  446.     * @return string 
  447.     */
  448.     public static function asc2hex32($string)
  449.     {
  450.         for ($i = 0; $i strlen($string)$i++{
  451.             $char substr($string$i1);
  452.             if (ord($char< 32{
  453.                 $hex dechex(ord($char));
  454.                 if (strlen($hex== 1$hex '0'.$hex;
  455.                 $string str_replace($char'\\'.$hex$string);
  456.             }
  457.         }
  458.         return $string;
  459.     }
  460.  
  461.     /**
  462.     * Converts all Hex expressions ("\HEX") to their original ASCII characters
  463.     *
  464.     * @param string $string String to convert
  465.     *
  466.     * @static
  467.     * @author beni@php.net, heavily based on work from DavidSmith@byu.net
  468.     * @return string 
  469.     */
  470.     public static function hex2asc($string)
  471.     {
  472.         $string preg_replace("/\\\([0-9A-Fa-f]{2})/e""''.chr(hexdec('\\1')).''"$string);
  473.         return $string;
  474.     }
  475.  
  476.     /**
  477.     * Split an multivalued RDN value into an Array
  478.     *
  479.     * A RDN can contain multiple values, spearated by a plus sign.
  480.     * This function returns each separate ocl=value pair of the RDN part.
  481.     *
  482.     * If no multivalued RDN is detected, an array containing only
  483.     * the original rdn part is returned.
  484.     *
  485.     * For example, the multivalued RDN 'OU=Sales+CN=J. Smith' is exploded to:
  486.     * <kbd>array([0] => 'OU=Sales', [1] => 'CN=J. Smith')</kbd>
  487.     *
  488.     * The method trys to be smart if it encounters unescaped "+" characters, but may fail,
  489.     * so ensure escaped "+"es in attr names and attr values.
  490.     *
  491.     * [BUG] If you have a multivalued RDN with unescaped plus characters
  492.     *       and there is a unescaped plus sign at the end of an value followed by an
  493.     *       attribute name containing an unescaped plus, then you will get wrong splitting:
  494.     *         $rdn = 'OU=Sales+C+N=J. Smith';
  495.     *       returns:
  496.     *         array('OU=Sales+C', 'N=J. Smith');
  497.     *       The "C+" is treaten as value of the first pair instead as attr name of the second pair.
  498.     *       To prevent this, escape correctly.
  499.     *
  500.     * @param string $rdn Part of an (multivalued) escaped RDN (eg. ou=foo OR ou=foo+cn=bar)
  501.     *
  502.     * @static
  503.     * @return array Array with the components of the multivalued RDN or Error
  504.     */
  505.     public static function split_rdn_multival($rdn)
  506.     {
  507.         $rdns preg_split('/(?<!\\\\)\+/'$rdn);
  508.         $rdns = self::_correct_dn_splitting($rdns'+');
  509.         return array_values($rdns);
  510.     }
  511.  
  512.     /**
  513.     * Splits a attribute=value syntax into an array
  514.     *
  515.     * The split will occur at the first unescaped '=' character.
  516.     *
  517.     * @param string $attr Attribute and Value Syntax
  518.     *
  519.     * @return array Indexed array: 0=attribute name, 1=attribute value
  520.     */
  521.     public static function split_attribute_string($attr)
  522.     {
  523.         return preg_split('/(?<!\\\\)=/'$attr2);
  524.     }
  525.  
  526.     /**
  527.     * Corrects splitting of dn parts
  528.     *
  529.     * @param array $dn        Raw DN array
  530.     * @param array $separator Separator that was used when splitting
  531.     *
  532.     * @return array Corrected array
  533.     * @access protected
  534.     */
  535.     protected static function _correct_dn_splitting($dn = array()$separator ',')
  536.     {
  537.         foreach ($dn as $key => $dn_value{
  538.             $dn_value $dn[$key]// refresh value (foreach caches!)
  539.             // if the dn_value is not in attr=value format, then we had an
  540.             // unescaped separator character inside the attr name or the value.
  541.             // We assume, that it was the attribute value.
  542.             // [TODO] To solve this, we might ask the schema. Keep in mind, that UTIL class
  543.             //        must remain independent from the other classes or connections.
  544.             if (!preg_match('/.+(?<!\\\\)=.+/'$dn_value)) {
  545.                 unset($dn[$key]);
  546.                 if (array_key_exists($key-1$dn)) {
  547.                     $dn[$key-1$dn[$key-1].$separator.$dn_value// append to previous attr value
  548.                 else {
  549.                     $dn[$key+1$dn_value.$separator.$dn[$key+1]// first element: prepend to next attr name
  550.                 }
  551.             }
  552.         }
  553.         return array_values($dn);
  554.     }
  555. }
  556.  
  557. ?>

Documentation generated on Mon, 11 Mar 2019 15:17:51 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.