1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4: */
3/**
4* File containing the Net_LDAP2_Schema interface class.
5*
6* PHP version 5
7*
8* @category  Net
9* @package   Net_LDAP2
10* @author    Jan Wagner <wagner@netsols.de>
11* @author    Benedikt Hallinger <beni@php.net>
12* @copyright 2009 Jan Wagner, Benedikt Hallinger
13* @license   http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
14* @version   SVN: $Id$
15* @link      http://pear.php.net/package/Net_LDAP2/
16* @todo see the comment at the end of the file
17*/
18
19/**
20* Includes
21*/
22require_once 'PEAR.php';
23
24/**
25* Syntax definitions
26*
27* Please don't forget to add binary attributes to isBinary() below
28* to support proper value fetching from Net_LDAP2_Entry
29*/
30define('NET_LDAP2_SYNTAX_BOOLEAN',            '1.3.6.1.4.1.1466.115.121.1.7');
31define('NET_LDAP2_SYNTAX_DIRECTORY_STRING',   '1.3.6.1.4.1.1466.115.121.1.15');
32define('NET_LDAP2_SYNTAX_DISTINGUISHED_NAME', '1.3.6.1.4.1.1466.115.121.1.12');
33define('NET_LDAP2_SYNTAX_INTEGER',            '1.3.6.1.4.1.1466.115.121.1.27');
34define('NET_LDAP2_SYNTAX_JPEG',               '1.3.6.1.4.1.1466.115.121.1.28');
35define('NET_LDAP2_SYNTAX_NUMERIC_STRING',     '1.3.6.1.4.1.1466.115.121.1.36');
36define('NET_LDAP2_SYNTAX_OID',                '1.3.6.1.4.1.1466.115.121.1.38');
37define('NET_LDAP2_SYNTAX_OCTET_STRING',       '1.3.6.1.4.1.1466.115.121.1.40');
38
39/**
40* Load an LDAP Schema and provide information
41*
42* This class takes a Subschema entry, parses this information
43* and makes it available in an array. Most of the code has been
44* inspired by perl-ldap( http://perl-ldap.sourceforge.net).
45* You will find portions of their implementation in here.
46*
47* @category Net
48* @package  Net_LDAP2
49* @author   Jan Wagner <wagner@netsols.de>
50* @author   Benedikt Hallinger <beni@php.net>
51* @license  http://www.gnu.org/copyleft/lesser.html LGPL
52* @link     http://pear.php.net/package/Net_LDAP22/
53*/
54class Net_LDAP2_Schema extends PEAR
55{
56    /**
57    * Map of entry types to ldap attributes of subschema entry
58    *
59    * @access public
60    * @var array
61    */
62    public $types = array(
63            'attribute'        => 'attributeTypes',
64            'ditcontentrule'   => 'dITContentRules',
65            'ditstructurerule' => 'dITStructureRules',
66            'matchingrule'     => 'matchingRules',
67            'matchingruleuse'  => 'matchingRuleUse',
68            'nameform'         => 'nameForms',
69            'objectclass'      => 'objectClasses',
70            'syntax'           => 'ldapSyntaxes'
71        );
72
73    /**
74    * Array of entries belonging to this type
75    *
76    * @access protected
77    * @var array
78    */
79    protected $_attributeTypes    = array();
80    protected $_matchingRules     = array();
81    protected $_matchingRuleUse   = array();
82    protected $_ldapSyntaxes      = array();
83    protected $_objectClasses     = array();
84    protected $_dITContentRules   = array();
85    protected $_dITStructureRules = array();
86    protected $_nameForms         = array();
87
88
89    /**
90    * hash of all fetched oids
91    *
92    * @access protected
93    * @var array
94    */
95    protected $_oids = array();
96
97    /**
98    * Tells if the schema is initialized
99    *
100    * @access protected
101    * @var boolean
102    * @see parse(), get()
103    */
104    protected $_initialized = false;
105
106
107    /**
108    * Constructor of the class
109    *
110    * @access protected
111    */
112    public function __construct()
113    {
114        parent::__construct('Net_LDAP2_Error'); // default error class
115    }
116
117    /**
118    * Fetch the Schema from an LDAP connection
119    *
120    * @param Net_LDAP2 $ldap LDAP connection
121    * @param string    $dn   (optional) Subschema entry dn
122    *
123    * @access public
124    * @return Net_LDAP2_Schema|NET_LDAP2_Error
125    */
126    public static function fetch($ldap, $dn = null)
127    {
128        if (!$ldap instanceof Net_LDAP2) {
129            return PEAR::raiseError("Unable to fetch Schema: Parameter \$ldap must be a Net_LDAP2 object!");
130        }
131
132        $schema_o = new Net_LDAP2_Schema();
133
134        if (is_null($dn)) {
135            // get the subschema entry via root dse
136            $dse = $ldap->rootDSE(array('subschemaSubentry'));
137            if (false == Net_LDAP2::isError($dse)) {
138                $base = $dse->getValue('subschemaSubentry', 'single');
139                if (!Net_LDAP2::isError($base)) {
140                    $dn = $base;
141                }
142            }
143        }
144
145        // Support for buggy LDAP servers (e.g. Siemens DirX 6.x) that incorrectly
146        // call this entry subSchemaSubentry instead of subschemaSubentry.
147        // Note the correct case/spelling as per RFC 2251.
148        if (is_null($dn)) {
149            // get the subschema entry via root dse
150            $dse = $ldap->rootDSE(array('subSchemaSubentry'));
151            if (false == Net_LDAP2::isError($dse)) {
152                $base = $dse->getValue('subSchemaSubentry', 'single');
153                if (!Net_LDAP2::isError($base)) {
154                    $dn = $base;
155                }
156            }
157        }
158
159        // Final fallback case where there is no subschemaSubentry attribute
160        // in the root DSE (this is a bug for an LDAP v3 server so report this
161        // to your LDAP vendor if you get this far).
162        if (is_null($dn)) {
163            $dn = 'cn=Subschema';
164        }
165
166        // fetch the subschema entry
167        $result = $ldap->search($dn, '(objectClass=*)',
168                                array('attributes' => array_values($schema_o->types),
169                                        'scope' => 'base'));
170        if (Net_LDAP2::isError($result)) {
171            return PEAR::raiseError('Could not fetch Subschema entry: '.$result->getMessage());
172        }
173
174        $entry = $result->shiftEntry();
175        if (!$entry instanceof Net_LDAP2_Entry) {
176            if ($entry instanceof Net_LDAP2_Error) {
177                return PEAR::raiseError('Could not fetch Subschema entry: '.$entry->getMessage());
178            } else {
179                return PEAR::raiseError('Could not fetch Subschema entry (search returned '.$result->count().' entries. Check parameter \'basedn\')');
180            }
181        }
182
183        $schema_o->parse($entry);
184        return $schema_o;
185    }
186
187    /**
188    * Return a hash of entries for the given type
189    *
190    * Returns a hash of entry for the givene type. Types may be:
191    * objectclasses, attributes, ditcontentrules, ditstructurerules, matchingrules,
192    * matchingruleuses, nameforms, syntaxes
193    *
194    * @param string $type Type to fetch
195    *
196    * @access public
197    * @return array|Net_LDAP2_Error Array or Net_LDAP2_Error
198    */
199    public function &getAll($type)
200    {
201        $map = array('objectclasses'     => &$this->_objectClasses,
202                     'attributes'        => &$this->_attributeTypes,
203                     'ditcontentrules'   => &$this->_dITContentRules,
204                     'ditstructurerules' => &$this->_dITStructureRules,
205                     'matchingrules'     => &$this->_matchingRules,
206                     'matchingruleuses'  => &$this->_matchingRuleUse,
207                     'nameforms'         => &$this->_nameForms,
208                     'syntaxes'          => &$this->_ldapSyntaxes );
209
210        $key = strtolower($type);
211        $ret = ((key_exists($key, $map)) ? $map[$key] : PEAR::raiseError("Unknown type $type"));
212        return $ret;
213    }
214
215    /**
216    * Return a specific entry
217    *
218    * @param string $type Type of name
219    * @param string $name Name or OID to fetch
220    *
221    * @access public
222    * @return mixed Entry or Net_LDAP2_Error
223    */
224    public function &get($type, $name)
225    {
226        if ($this->_initialized) {
227            $type = strtolower($type);
228            if (false == key_exists($type, $this->types)) {
229                return PEAR::raiseError("No such type $type");
230            }
231
232            $name     = strtolower($name);
233            $type_var = &$this->{'_' . $this->types[$type]};
234
235            if (key_exists($name, $type_var)) {
236                return $type_var[$name];
237            } elseif (key_exists($name, $this->_oids) && $this->_oids[$name]['type'] == $type) {
238                return $this->_oids[$name];
239            } else {
240                return PEAR::raiseError("Could not find $type $name");
241            }
242        } else {
243            $return = null;
244            return $return;
245        }
246    }
247
248
249    /**
250    * Fetches attributes that MAY be present in the given objectclass
251    *
252    * @param string $oc Name or OID of objectclass
253    *
254    * @access public
255    * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
256    */
257    public function may($oc)
258    {
259        return $this->_getAttr($oc, 'may');
260    }
261
262    /**
263    * Fetches attributes that MUST be present in the given objectclass
264    *
265    * @param string $oc Name or OID of objectclass
266    *
267    * @access public
268    * @return array|Net_LDAP2_Error Array with attributes or Net_LDAP2_Error
269    */
270    public function must($oc)
271    {
272        return $this->_getAttr($oc, 'must');
273    }
274
275    /**
276    * Fetches the given attribute from the given objectclass
277    *
278    * @param string $oc   Name or OID of objectclass
279    * @param string $attr Name of attribute to fetch
280    *
281    * @access protected
282    * @return array|Net_LDAP2_Error The attribute or Net_LDAP2_Error
283    */
284    protected function _getAttr($oc, $attr)
285    {
286        $oc = strtolower($oc);
287        if (key_exists($oc, $this->_objectClasses) && key_exists($attr, $this->_objectClasses[$oc])) {
288            return $this->_objectClasses[$oc][$attr];
289        } elseif (key_exists($oc, $this->_oids) &&
290                $this->_oids[$oc]['type'] == 'objectclass' &&
291                key_exists($attr, $this->_oids[$oc])) {
292            return $this->_oids[$oc][$attr];
293        } else {
294            return PEAR::raiseError("Could not find $attr attributes for $oc ");
295        }
296    }
297
298    /**
299    * Returns the name(s) of the immediate superclass(es)
300    *
301    * @param string $oc Name or OID of objectclass
302    *
303    * @access public
304    * @return array|Net_LDAP2_Error  Array of names or Net_LDAP2_Error
305    */
306    public function superclass($oc)
307    {
308        $o = $this->get('objectclass', $oc);
309        if (Net_LDAP2::isError($o)) {
310            return $o;
311        }
312        return (key_exists('sup', $o) ? $o['sup'] : array());
313    }
314
315    /**
316    * Parses the schema of the given Subschema entry
317    *
318    * @param Net_LDAP2_Entry &$entry Subschema entry
319    *
320    * @access public
321    * @return void
322    */
323    public function parse(&$entry)
324    {
325        foreach ($this->types as $type => $attr) {
326            // initialize map type to entry
327            $type_var          = '_' . $attr;
328            $this->{$type_var} = array();
329
330            // get values for this type
331            if ($entry->exists($attr)) {
332                $values = $entry->getValue($attr);
333                if (is_array($values)) {
334                    foreach ($values as $value) {
335
336                        unset($schema_entry); // this was a real mess without it
337
338                        // get the schema entry
339                        $schema_entry = $this->_parse_entry($value);
340
341                        // set the type
342                        $schema_entry['type'] = $type;
343
344                        // save a ref in $_oids
345                        $this->_oids[$schema_entry['oid']] = &$schema_entry;
346
347                        // save refs for all names in type map
348                        $names = $schema_entry['aliases'];
349                        array_push($names, $schema_entry['name']);
350                        foreach ($names as $name) {
351                            $this->{$type_var}[strtolower($name)] = &$schema_entry;
352                        }
353                    }
354                }
355            }
356        }
357        $this->_initialized = true;
358    }
359
360    /**
361    * Parses an attribute value into a schema entry
362    *
363    * @param string $value Attribute value
364    *
365    * @access protected
366    * @return array|false Schema entry array or false
367    */
368    protected function &_parse_entry($value)
369    {
370        // tokens that have no value associated
371        $noValue = array('single-value',
372                         'obsolete',
373                         'collective',
374                         'no-user-modification',
375                         'abstract',
376                         'structural',
377                         'auxiliary');
378
379        // tokens that can have multiple values
380        $multiValue = array('must', 'may', 'sup');
381
382        $schema_entry = array('aliases' => array()); // initilization
383
384        $tokens = $this->_tokenize($value); // get an array of tokens
385
386        // remove surrounding brackets
387        if ($tokens[0] == '(') array_shift($tokens);
388        if ($tokens[count($tokens) - 1] == ')') array_pop($tokens); // -1 doesnt work on arrays :-(
389
390        $schema_entry['oid'] = array_shift($tokens); // first token is the oid
391
392        // cycle over the tokens until none are left
393        while (count($tokens) > 0) {
394            $token = strtolower(array_shift($tokens));
395            if (in_array($token, $noValue)) {
396                $schema_entry[$token] = 1; // single value token
397            } else {
398                // this one follows a string or a list if it is multivalued
399                if (($schema_entry[$token] = array_shift($tokens)) == '(') {
400                    // this creates the list of values and cycles through the tokens
401                    // until the end of the list is reached ')'
402                    $schema_entry[$token] = array();
403                    while ($tmp = array_shift($tokens)) {
404                        if ($tmp == ')') break;
405                        if ($tmp != '$') array_push($schema_entry[$token], $tmp);
406                    }
407                }
408                // create a array if the value should be multivalued but was not
409                if (in_array($token, $multiValue) && !is_array($schema_entry[$token])) {
410                    $schema_entry[$token] = array($schema_entry[$token]);
411                }
412            }
413        }
414        // get max length from syntax
415        if (key_exists('syntax', $schema_entry)) {
416            if (preg_match('/{(\d+)}/', $schema_entry['syntax'], $matches)) {
417                $schema_entry['max_length'] = $matches[1];
418            }
419        }
420        // force a name
421        if (empty($schema_entry['name'])) {
422            $schema_entry['name'] = $schema_entry['oid'];
423        }
424        // make one name the default and put the other ones into aliases
425        if (is_array($schema_entry['name'])) {
426            $aliases                 = $schema_entry['name'];
427            $schema_entry['name']    = array_shift($aliases);
428            $schema_entry['aliases'] = $aliases;
429        }
430        return $schema_entry;
431    }
432
433    /**
434    * Tokenizes the given value into an array of tokens
435    *
436    * @param string $value String to parse
437    *
438    * @access protected
439    * @return array Array of tokens
440    */
441    protected function _tokenize($value)
442    {
443        $tokens  = array();       // array of tokens
444        $matches = array();       // matches[0] full pattern match, [1,2,3] subpatterns
445
446        // this one is taken from perl-ldap, modified for php
447        $pattern = "/\s* (?:([()]) | ([^'\s()]+) | '((?:[^']+|'[^\s)])*)') \s*/x";
448
449        /**
450         * This one matches one big pattern wherin only one of the three subpatterns matched
451         * We are interested in the subpatterns that matched. If it matched its value will be
452         * non-empty and so it is a token. Tokens may be round brackets, a string, or a string
453         * enclosed by '
454         */
455        preg_match_all($pattern, $value, $matches);
456
457        for ($i = 0; $i < count($matches[0]); $i++) {     // number of tokens (full pattern match)
458            for ($j = 1; $j < 4; $j++) {                  // each subpattern
459                if (null != trim($matches[$j][$i])) {     // pattern match in this subpattern
460                    $tokens[$i] = trim($matches[$j][$i]); // this is the token
461                }
462            }
463        }
464        return $tokens;
465    }
466
467    /**
468    * Returns wether a attribute syntax is binary or not
469    *
470    * This method gets used by Net_LDAP2_Entry to decide which
471    * PHP function needs to be used to fetch the value in the
472    * proper format (e.g. binary or string)
473    *
474    * @param string $attribute The name of the attribute (eg.: 'sn')
475    *
476    * @access public
477    * @return boolean
478    */
479    public function isBinary($attribute)
480    {
481        $return = false; // default to false
482
483        // This list contains all syntax that should be treaten as
484        // containing binary values
485        // The Syntax Definitons go into constants at the top of this page
486        $syntax_binary = array(
487                           NET_LDAP2_SYNTAX_OCTET_STRING,
488                           NET_LDAP2_SYNTAX_JPEG
489                         );
490
491        // Check Syntax
492        $attr_s = $this->get('attribute', $attribute);
493        if (Net_LDAP2::isError($attr_s)) {
494            // Attribute not found in schema
495            $return = false; // consider attr not binary
496        } elseif (isset($attr_s['syntax']) && in_array($attr_s['syntax'], $syntax_binary)) {
497            // Syntax is defined as binary in schema
498            $return = true;
499        } else {
500            // Syntax not defined as binary, or not found
501            // if attribute is a subtype, check superior attribute syntaxes
502            if (isset($attr_s['sup'])) {
503                foreach ($attr_s['sup'] as $superattr) {
504                    $return = $this->isBinary($superattr);
505                    if ($return) {
506                        break; // stop checking parents since we are binary
507                    }
508                }
509            }
510        }
511
512        return $return;
513    }
514
515    /**
516    * See if an schema element exists
517    *
518    * @param string $type Type of name, see get()
519    * @param string $name Name or OID
520    *
521    * @return boolean
522    */
523    public function exists($type, $name)
524    {
525        $entry = $this->get($type, $name);
526        if ($entry instanceof Net_LDAP2_ERROR) {
527                return false;
528        } else {
529            return true;
530        }
531    }
532
533    /**
534    * See if an attribute is defined in the schema
535    *
536    * @param string $attribute Name or OID of the attribute
537    * @return boolean
538    */
539    public function attributeExists($attribute)
540    {
541        return $this->exists('attribute', $attribute);
542    }
543
544    /**
545    * See if an objectClass is defined in the schema
546    *
547    * @param string $ocl Name or OID of the objectClass
548    * @return boolean
549    */
550    public function objectClassExists($ocl)
551    {
552        return $this->exists('objectclass', $ocl);
553    }
554
555
556    /**
557    * See to which ObjectClasses an attribute is assigned
558    *
559    * The objectclasses are sorted into the keys 'may' and 'must'.
560    *
561    * @param string $attribute Name or OID of the attribute
562    *
563    * @return array|Net_LDAP2_Error Associative array with OCL names or Error
564    */
565    public function getAssignedOCLs($attribute)
566    {
567        $may  = array();
568        $must = array();
569
570        // Test if the attribute type is defined in the schema,
571        // if so, retrieve real name for lookups
572        $attr_entry = $this->get('attribute', $attribute);
573        if ($attr_entry instanceof Net_LDAP2_ERROR) {
574            return PEAR::raiseError("Attribute $attribute not defined in schema: ".$attr_entry->getMessage());
575        } else {
576            $attribute = $attr_entry['name'];
577        }
578
579
580        // We need to get all defined OCLs for this.
581        $ocls = $this->getAll('objectclasses');
582        foreach ($ocls as $ocl => $ocl_data) {
583            // Fetch the may and must attrs and see if our searched attr is contained.
584            // If so, record it in the corresponding array.
585            $ocl_may_attrs  = $this->may($ocl);
586            $ocl_must_attrs = $this->must($ocl);
587            if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
588                array_push($may, $ocl_data['name']);
589            }
590            if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
591                array_push($must, $ocl_data['name']);
592            }
593        }
594
595        return array('may' => $may, 'must' => $must);
596    }
597
598    /**
599    * See if an attribute is available in a set of objectClasses
600    *
601    * @param string $attribute Attribute name or OID
602    * @param array $ocls       Names of OCLs to check for
603    *
604    * @return boolean TRUE, if the attribute is defined for at least one of the OCLs
605    */
606    public function checkAttribute($attribute, $ocls)
607    {
608        foreach ($ocls as $ocl) {
609            $ocl_entry = $this->get('objectclass', $ocl);
610            $ocl_may_attrs  = $this->may($ocl);
611            $ocl_must_attrs = $this->must($ocl);
612            if (is_array($ocl_may_attrs) && in_array($attribute, $ocl_may_attrs)) {
613                return true;
614            }
615            if (is_array($ocl_must_attrs) && in_array($attribute, $ocl_must_attrs)) {
616                return true;
617            }
618        }
619        return false; // no ocl for the ocls found.
620    }
621}
622?>