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

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