Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/* vim: set expandtab tabstop=4 shiftwidth=4: */
3
/**
4
* File containing the Net_LDAP2_LDIF interface class.
5
*
6
* PHP version 5
7
*
8
* @category  Net
9
* @package   Net_LDAP2
10
* @author    Benedikt Hallinger <beni@php.net>
11
* @copyright 2009 Benedikt Hallinger
12
* @license   http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
13
* @version   SVN: $Id: LDIF.php 302696 2010-08-23 12:48:07Z beni $
14
* @link      http://pear.php.net/package/Net_LDAP2/
15
*/
16
 
17
/**
18
* Includes
19
*/
20
require_once 'PEAR.php';
21
require_once 'Net/LDAP2.php';
22
require_once 'Net/LDAP2/Entry.php';
23
require_once 'Net/LDAP2/Util.php';
24
 
25
/**
26
* LDIF capabilitys for Net_LDAP2, closely taken from PERLs Net::LDAP
27
*
28
* It provides a means to convert between Net_LDAP2_Entry objects and LDAP entries
29
* represented in LDIF format files. Reading and writing are supported and may
30
* manipulate single entries or lists of entries.
31
*
32
* Usage example:
33
* <code>
34
* // Read and parse an ldif-file into Net_LDAP2_Entry objects
35
* // and print out the DNs. Store the entries for later use.
36
* require 'Net/LDAP2/LDIF.php';
37
* $options = array(
38
*       'onerror' => 'die'
39
* );
40
* $entries = array();
41
* $ldif = new Net_LDAP2_LDIF('test.ldif', 'r', $options);
42
* do {
43
*       $entry = $ldif->read_entry();
44
*       $dn    = $entry->dn();
45
*       echo " done building entry: $dn\n";
46
*       array_push($entries, $entry);
47
* } while (!$ldif->eof());
48
* $ldif->done();
49
*
50
*
51
* // write those entries to another file
52
* $ldif = new Net_LDAP2_LDIF('test.out.ldif', 'w', $options);
53
* $ldif->write_entry($entries);
54
* $ldif->done();
55
* </code>
56
*
57
* @category Net
58
* @package  Net_LDAP2
59
* @author   Benedikt Hallinger <beni@php.net>
60
* @license  http://www.gnu.org/copyleft/lesser.html LGPL
61
* @link     http://pear.php.net/package/Net_LDAP22/
62
* @see      http://www.ietf.org/rfc/rfc2849.txt
63
* @todo     Error handling should be PEARified
64
* @todo     LDAPv3 controls are not implemented yet
65
*/
66
class Net_LDAP2_LDIF extends PEAR
67
{
68
    /**
69
    * Options
70
    *
71
    * @access protected
72
    * @var array
73
    */
74
    protected $_options = array('encode'    => 'base64',
75
                                'onerror'   => null,
76
                                'change'    => 0,
77
                                'lowercase' => 0,
78
                                'sort'      => 0,
79
                                'version'   => null,
80
                                'wrap'      => 78,
81
                                'raw'       => ''
82
                               );
83
 
84
    /**
85
    * Errorcache
86
    *
87
    * @access protected
88
    * @var array
89
    */
90
    protected $_error = array('error' => null,
91
                              'line'  => 0
92
                             );
93
 
94
    /**
95
    * Filehandle for read/write
96
    *
97
    * @access protected
98
    * @var array
99
    */
100
    protected $_FH = null;
101
 
102
    /**
103
    * Says, if we opened the filehandle ourselves
104
    *
105
    * @access protected
106
    * @var array
107
    */
108
    protected $_FH_opened = false;
109
 
110
    /**
111
    * Linecounter for input file handle
112
    *
113
    * @access protected
114
    * @var array
115
    */
116
    protected $_input_line = 0;
117
 
118
    /**
119
    * counter for processed entries
120
    *
121
    * @access protected
122
    * @var int
123
    */
124
    protected $_entrynum = 0;
125
 
126
    /**
127
    * Mode we are working in
128
    *
129
    * Either 'r', 'a' or 'w'
130
    *
131
    * @access protected
132
    * @var string
133
    */
134
    protected $_mode = false;
135
 
136
    /**
137
    * Tells, if the LDIF version string was already written
138
    *
139
    * @access protected
140
    * @var boolean
141
    */
142
    protected $_version_written = false;
143
 
144
    /**
145
    * Cache for lines that have build the current entry
146
    *
147
    * @access protected
148
    * @var boolean
149
    */
150
    protected $_lines_cur = array();
151
 
152
    /**
153
    * Cache for lines that will build the next entry
154
    *
155
    * @access protected
156
    * @var boolean
157
    */
158
    protected $_lines_next = array();
159
 
160
    /**
161
    * Open LDIF file for reading or for writing
162
    *
163
    * new (FILE):
164
    * Open the file read-only. FILE may be the name of a file
165
    * or an already open filehandle.
166
    * If the file doesn't exist, it will be created if in write mode.
167
    *
168
    * new (FILE, MODE, OPTIONS):
169
    *     Open the file with the given MODE (see PHPs fopen()), eg "w" or "a".
170
    *     FILE may be the name of a file or an already open filehandle.
171
    *     PERLs Net_LDAP2 "FILE|" mode does not work curently.
172
    *
173
    *     OPTIONS is an associative array and may contain:
174
    *       encode => 'none' | 'canonical' | 'base64'
175
    *         Some DN values in LDIF cannot be written verbatim and have to be encoded in some way:
176
    *         'none'       No encoding.
177
    *         'canonical'  See "canonical_dn()" in Net::LDAP::Util.
178
    *         'base64'     Use base64. (default, this differs from the Perl interface.
179
    *                                   The perl default is "none"!)
180
    *
181
    *       onerror => 'die' | 'warn' | NULL
182
    *         Specify what happens when an error is detected.
183
    *         'die'  Net_LDAP2_LDIF will croak with an appropriate message.
184
    *         'warn' Net_LDAP2_LDIF will warn (echo) with an appropriate message.
185
    *         NULL   Net_LDAP2_LDIF will not warn (default), use error().
186
    *
187
    *       change => 1
188
    *         Write entry changes to the LDIF file instead of the entries itself. I.e. write LDAP
189
    *         operations acting on the entries to the file instead of the entries contents.
190
    *         This writes the changes usually carried out by an update() to the LDIF file.
191
    *
192
    *       lowercase => 1
193
    *         Convert attribute names to lowercase when writing.
194
    *
195
    *       sort => 1
196
    *         Sort attribute names when writing entries according to the rule:
197
    *         objectclass first then all other attributes alphabetically sorted by attribute name
198
    *
199
    *       version => '1'
200
    *         Set the LDIF version to write to the resulting LDIF file.
201
    *         According to RFC 2849 currently the only legal value for this option is 1.
202
    *         When this option is set Net_LDAP2_LDIF tries to adhere more strictly to
203
    *         the LDIF specification in RFC2489 in a few places.
204
    *         The default is NULL meaning no version information is written to the LDIF file.
205
    *
206
    *       wrap => 78
207
    *         Number of columns where output line wrapping shall occur.
208
    *         Default is 78. Setting it to 40 or lower inhibits wrapping.
209
    *
210
    *       raw => REGEX
211
    *         Use REGEX to denote the names of attributes that are to be
212
    *         considered binary in search results if writing entries.
213
    *         Example: raw => "/(?i:^jpegPhoto|;binary)/i"
214
    *
215
    * @param string|ressource $file    Filename or filehandle
216
    * @param string           $mode    Mode to open filename
217
    * @param array            $options Options like described above
218
    */
219
    public function __construct($file, $mode = 'r', $options = array())
220
    {
221
        $this->PEAR('Net_LDAP2_Error'); // default error class
222
 
223
        // First, parse options
224
        // todo: maybe implement further checks on possible values
225
        foreach ($options as $option => $value) {
226
            if (!array_key_exists($option, $this->_options)) {
227
                $this->dropError('Net_LDAP2_LDIF error: option '.$option.' not known!');
228
                return;
229
            } else {
230
                $this->_options[$option] = strtolower($value);
231
            }
232
        }
233
 
234
        // setup LDIF class
235
        $this->version($this->_options['version']);
236
 
237
        // setup file mode
238
        if (!preg_match('/^[rwa]\+?$/', $mode)) {
239
            $this->dropError('Net_LDAP2_LDIF error: file mode '.$mode.' not supported!');
240
        } else {
241
            $this->_mode = $mode;
242
 
243
            // setup filehandle
244
            if (is_resource($file)) {
245
                // TODO: checks on mode possible?
246
                $this->_FH =& $file;
247
            } else {
248
                $imode = substr($this->_mode, 0, 1);
249
                if ($imode == 'r') {
250
                    if (!file_exists($file)) {
251
                        $this->dropError('Unable to open '.$file.' for read: file not found');
252
                        $this->_mode = false;
253
                    }
254
                    if (!is_readable($file)) {
255
                        $this->dropError('Unable to open '.$file.' for read: permission denied');
256
                        $this->_mode = false;
257
                    }
258
                }
259
 
260
                if (($imode == 'w' || $imode == 'a')) {
261
                    if (file_exists($file)) {
262
                        if (!is_writable($file)) {
263
                            $this->dropError('Unable to open '.$file.' for write: permission denied');
264
                            $this->_mode = false;
265
                        }
266
                    } else {
267
                        if (!@touch($file)) {
268
                            $this->dropError('Unable to create '.$file.' for write: permission denied');
269
                            $this->_mode = false;
270
                        }
271
                    }
272
                }
273
 
274
                if ($this->_mode) {
275
                    $this->_FH = @fopen($file, $this->_mode);
276
                    if (false === $this->_FH) {
277
                        // Fallback; should never be reached if tests above are good enough!
278
                        $this->dropError('Net_LDAP2_LDIF error: Could not open file '.$file);
279
                    } else {
280
                        $this->_FH_opened = true;
281
                    }
282
                }
283
            }
284
        }
285
    }
286
 
287
    /**
288
    * Read one entry from the file and return it as a Net::LDAP::Entry object.
289
    *
290
    * @return Net_LDAP2_Entry
291
    */
292
    public function read_entry()
293
    {
294
        // read fresh lines, set them as current lines and create the entry
295
        $attrs = $this->next_lines(true);
296
        if (count($attrs) > 0) {
297
            $this->_lines_cur = $attrs;
298
        }
299
        return $this->current_entry();
300
    }
301
 
302
    /**
303
    * Returns true when the end of the file is reached.
304
    *
305
    * @return boolean
306
    */
307
    public function eof()
308
    {
309
        return feof($this->_FH);
310
    }
311
 
312
    /**
313
    * Write the entry or entries to the LDIF file.
314
    *
315
    * If you want to build an LDIF file containing several entries AND
316
    * you want to call write_entry() several times, you must open the filehandle
317
    * in append mode ("a"), otherwise you will always get the last entry only.
318
    *
319
    * @param Net_LDAP2_Entry|array $entries Entry or array of entries
320
    *
321
    * @return void
322
    * @todo implement operations on whole entries (adding a whole entry)
323
    */
324
    public function write_entry($entries)
325
    {
326
        if (!is_array($entries)) {
327
            $entries = array($entries);
328
        }
329
 
330
        foreach ($entries as $entry) {
331
            $this->_entrynum++;
332
            if (!$entry instanceof Net_LDAP2_Entry) {
333
                $this->dropError('Net_LDAP2_LDIF error: entry '.$this->_entrynum.' is not an Net_LDAP2_Entry object');
334
            } else {
335
                if ($this->_options['change']) {
336
                    // LDIF change mode
337
                    // fetch change information from entry
338
                    $entry_attrs_changes = $entry->getChanges();
339
                    $num_of_changes      = count($entry_attrs_changes['add'])
340
                                           + count($entry_attrs_changes['replace'])
341
                                           + count($entry_attrs_changes['delete']);
342
 
343
                    $is_changed = ($num_of_changes > 0 || $entry->willBeDeleted() || $entry->willBeMoved());
344
 
345
                    // write version if not done yet
346
                    // also write DN of entry
347
                    if ($is_changed) {
348
                        if (!$this->_version_written) {
349
                            $this->write_version();
350
                        }
351
                        $this->writeDN($entry->currentDN());
352
                    }
353
 
354
                    // process changes
355
                    // TODO: consider DN add!
356
                    if ($entry->willBeDeleted()) {
357
                        $this->writeLine("changetype: delete".PHP_EOL);
358
                    } elseif ($entry->willBeMoved()) {
359
                        $this->writeLine("changetype: modrdn".PHP_EOL);
360
                        $olddn     = Net_LDAP2_Util::ldap_explode_dn($entry->currentDN(), array('casefold' => 'none')); // maybe gives a bug if using multivalued RDNs
361
                        $oldrdn    = array_shift($olddn);
362
                        $oldparent = implode(',', $olddn);
363
                        $newdn     = Net_LDAP2_Util::ldap_explode_dn($entry->dn(), array('casefold' => 'none')); // maybe gives a bug if using multivalued RDNs
364
                        $rdn       = array_shift($newdn);
365
                        $parent    = implode(',', $newdn);
366
                        $this->writeLine("newrdn: ".$rdn.PHP_EOL);
367
                        $this->writeLine("deleteoldrdn: 1".PHP_EOL);
368
                        if ($parent !== $oldparent) {
369
                            $this->writeLine("newsuperior: ".$parent.PHP_EOL);
370
                        }
371
                        // TODO: What if the entry has attribute changes as well?
372
                        //       I think we should check for that and make a dummy
373
                        //       entry with the changes that is written to the LDIF file
374
                    } elseif ($num_of_changes > 0) {
375
                        // write attribute change data
376
                        $this->writeLine("changetype: modify".PHP_EOL);
377
                        foreach ($entry_attrs_changes as $changetype => $entry_attrs) {
378
                            foreach ($entry_attrs as $attr_name => $attr_values) {
379
                                $this->writeLine("$changetype: $attr_name".PHP_EOL);
380
                                if ($attr_values !== null) $this->writeAttribute($attr_name, $attr_values, $changetype);
381
                                $this->writeLine("-".PHP_EOL);
382
                            }
383
                        }
384
                    }
385
 
386
                    // finish this entrys data if we had changes
387
                    if ($is_changed) {
388
                        $this->finishEntry();
389
                    }
390
                } else {
391
                    // LDIF-content mode
392
                    // fetch attributes for further processing
393
                    $entry_attrs = $entry->getValues();
394
 
395
                    // sort and put objectclass-attrs to first position
396
                    if ($this->_options['sort']) {
397
                        ksort($entry_attrs);
398
                        if (array_key_exists('objectclass', $entry_attrs)) {
399
                            $oc = $entry_attrs['objectclass'];
400
                            unset($entry_attrs['objectclass']);
401
                            $entry_attrs = array_merge(array('objectclass' => $oc), $entry_attrs);
402
                        }
403
                    }
404
 
405
                    // write data
406
                    if (!$this->_version_written) {
407
                        $this->write_version();
408
                    }
409
                    $this->writeDN($entry->dn());
410
                    foreach ($entry_attrs as $attr_name => $attr_values) {
411
                        $this->writeAttribute($attr_name, $attr_values);
412
                    }
413
                    $this->finishEntry();
414
                }
415
            }
416
        }
417
    }
418
 
419
    /**
420
    * Write version to LDIF
421
    *
422
    * If the object's version is defined, this method allows to explicitely write the version before an entry is written.
423
    * If not called explicitely, it gets called automatically when writing the first entry.
424
    *
425
    * @return void
426
    */
427
    public function write_version()
428
    {
429
        $this->_version_written = true;
430
        if (!is_null($this->version())) {
431
            return $this->writeLine('version: '.$this->version().PHP_EOL, 'Net_LDAP2_LDIF error: unable to write version');
432
        }
433
    }
434
 
435
    /**
436
    * Get or set LDIF version
437
    *
438
    * If called without arguments it returns the version of the LDIF file or NULL if no version has been set.
439
    * If called with an argument it sets the LDIF version to VERSION.
440
    * According to RFC 2849 currently the only legal value for VERSION is 1.
441
    *
442
    * @param int $version (optional) LDIF version to set
443
    *
444
    * @return int
445
    */
446
    public function version($version = null)
447
    {
448
        if ($version !== null) {
449
            if ($version != 1) {
450
                $this->dropError('Net_LDAP2_LDIF error: illegal LDIF version set');
451
            } else {
452
                $this->_options['version'] = $version;
453
            }
454
        }
455
        return $this->_options['version'];
456
    }
457
 
458
    /**
459
    * Returns the file handle the Net_LDAP2_LDIF object reads from or writes to.
460
    *
461
    * You can, for example, use this to fetch the content of the LDIF file yourself
462
    *
463
    * @return null|resource
464
    */
465
    public function &handle()
466
    {
467
        if (!is_resource($this->_FH)) {
468
            $this->dropError('Net_LDAP2_LDIF error: invalid file resource');
469
            $null = null;
470
            return $null;
471
        } else {
472
            return $this->_FH;
473
        }
474
    }
475
 
476
    /**
477
    * Clean up
478
    *
479
    * This method signals that the LDIF object is no longer needed.
480
    * You can use this to free up some memory and close the file handle.
481
    * The file handle is only closed, if it was opened from Net_LDAP2_LDIF.
482
    *
483
    * @return void
484
    */
485
    public function done()
486
    {
487
        // close FH if we opened it
488
        if ($this->_FH_opened) {
489
            fclose($this->handle());
490
        }
491
 
492
        // free variables
493
        foreach (get_object_vars($this) as $name => $value) {
494
            unset($this->$name);
495
        }
496
    }
497
 
498
    /**
499
    * Returns last error message if error was found.
500
    *
501
    * Example:
502
    * <code>
503
    *  $ldif->someAction();
504
    *  if ($ldif->error()) {
505
    *     echo "Error: ".$ldif->error()." at input line: ".$ldif->error_lines();
506
    *  }
507
    * </code>
508
    *
509
    * @param boolean $as_string If set to true, only the message is returned
510
    *
511
    * @return false|Net_LDAP2_Error
512
    */
513
    public function error($as_string = false)
514
    {
515
        if (Net_LDAP2::isError($this->_error['error'])) {
516
            return ($as_string)? $this->_error['error']->getMessage() : $this->_error['error'];
517
        } else {
518
            return false;
519
        }
520
    }
521
 
522
    /**
523
    * Returns lines that resulted in error.
524
    *
525
    * Perl returns an array of faulty lines in list context,
526
    * but we always just return an int because of PHPs language.
527
    *
528
    * @return int
529
    */
530
    public function error_lines()
531
    {
532
        return $this->_error['line'];
533
    }
534
 
535
    /**
536
    * Returns the current Net::LDAP::Entry object.
537
    *
538
    * @return Net_LDAP2_Entry|false
539
    */
540
    public function current_entry()
541
    {
542
        return $this->parseLines($this->current_lines());
543
    }
544
 
545
    /**
546
    * Parse LDIF lines of one entry into an Net_LDAP2_Entry object
547
    *
548
    * @param array $lines LDIF lines for one entry
549
    *
550
    * @return Net_LDAP2_Entry|false Net_LDAP2_Entry object for those lines
551
    * @todo what about file inclusions and urls? "jpegphoto:< file:///usr/local/directory/photos/fiona.jpg"
552
    */
553
    public function parseLines($lines)
554
    {
555
        // parse lines into an array of attributes and build the entry
556
        $attributes = array();
557
        $dn = false;
558
        foreach ($lines as $line) {
559
            if (preg_match('/^(\w+(;binary)?)(:|::|:<)\s(.+)$/', $line, $matches)) {
560
                $attr  =& $matches[1] . $matches[2];
561
                $delim =& $matches[3];
562
                $data  =& $matches[4];
563
 
564
                if ($delim == ':') {
565
                    // normal data
566
                    $attributes[$attr][] = $data;
567
                } elseif ($delim == '::') {
568
                    // base64 data
569
                    $attributes[$attr][] = base64_decode($data);
570
                } elseif ($delim == ':<') {
571
                    // file inclusion
572
                    // TODO: Is this the job of the LDAP-client or the server?
573
                    $this->dropError('File inclusions are currently not supported');
574
                    //$attributes[$attr][] = ...;
575
                } else {
576
                    // since the pattern above, the delimeter cannot be something else.
577
                    $this->dropError('Net_LDAP2_LDIF parsing error: invalid syntax at parsing entry line: '.$line);
578
                    continue;
579
                }
580
 
581
                if (strtolower($attr) == 'dn') {
582
                    // DN line detected
583
                    $dn = $attributes[$attr][0];  // save possibly decoded DN
584
                    unset($attributes[$attr]);    // remove wrongly added "dn: " attribute
585
                }
586
            } else {
587
                // line not in "attr: value" format -> ignore
588
                // maybe we should rise an error here, but this should be covered by
589
                // next_lines() already. A problem arises, if users try to feed data of
590
                // several entries to this method - the resulting entry will
591
                // get wrong attributes. However, this is already mentioned in the
592
                // methods documentation above.
593
            }
594
        }
595
 
596
        if (false === $dn) {
597
            $this->dropError('Net_LDAP2_LDIF parsing error: unable to detect DN for entry');
598
            return false;
599
        } else {
600
            $newentry = Net_LDAP2_Entry::createFresh($dn, $attributes);
601
            return $newentry;
602
        }
603
    }
604
 
605
    /**
606
    * Returns the lines that generated the current Net::LDAP::Entry object.
607
    *
608
    * Note that this returns an empty array if no lines have been read so far.
609
    *
610
    * @return array Array of lines
611
    */
612
    public function current_lines()
613
    {
614
        return $this->_lines_cur;
615
    }
616
 
617
    /**
618
    * Returns the lines that will generate the next Net::LDAP::Entry object.
619
    *
620
    * If you set $force to TRUE then you can iterate over the lines that build
621
    * up entries manually. Otherwise, iterating is done using {@link read_entry()}.
622
    * Force will move the file pointer forward, thus returning the next entries lines.
623
    *
624
    * Wrapped lines will be unwrapped. Comments are stripped.
625
    *
626
    * @param boolean $force Set this to true if you want to iterate over the lines manually
627
    *
628
    * @return array
629
    */
630
    public function next_lines($force = false)
631
    {
632
        // if we already have those lines, just return them, otherwise read
633
        if (count($this->_lines_next) == 0 || $force) {
634
            $this->_lines_next = array(); // empty in case something was left (if used $force)
635
            $entry_done        = false;
636
            $fh                = &$this->handle();
637
            $commentmode       = false; // if we are in an comment, for wrapping purposes
638
            $datalines_read    = 0;     // how many lines with data we have read
639
 
640
            while (!$entry_done && !$this->eof()) {
641
                $this->_input_line++;
642
                // Read line. Remove line endings, we want only data;
643
                // this is okay since ending spaces should be encoded
644
                $data = rtrim(fgets($fh));
645
                if ($data === false) {
646
                    // error only, if EOF not reached after fgets() call
647
                    if (!$this->eof()) {
648
                        $this->dropError('Net_LDAP2_LDIF error: error reading from file at input line '.$this->_input_line, $this->_input_line);
649
                    }
650
                    break;
651
                } else {
652
                    if (count($this->_lines_next) > 0 && preg_match('/^$/', $data)) {
653
                        // Entry is finished if we have an empty line after we had data
654
                        $entry_done = true;
655
 
656
                        // Look ahead if the next EOF is nearby. Comments and empty
657
                        // lines at the file end may cause problems otherwise
658
                        $current_pos = ftell($fh);
659
                        $data        = fgets($fh);
660
                        while (!feof($fh)) {
661
                            if (preg_match('/^\s*$/', $data) || preg_match('/^#/', $data)) {
662
                                // only empty lines or comments, continue to seek
663
                                // TODO: Known bug: Wrappings for comments are okay but are treaten as
664
                                //       error, since we do not honor comment mode here.
665
                                //       This should be a very theoretically case, however
666
                                //       i am willing to fix this if really necessary.
667
                                $this->_input_line++;
668
                                $current_pos = ftell($fh);
669
                                $data        = fgets($fh);
670
                            } else {
671
                                // Data found if non emtpy line and not a comment!!
672
                                // Rewind to position prior last read and stop lookahead
673
                                fseek($fh, $current_pos);
674
                                break;
675
                            }
676
                        }
677
                        // now we have either the file pointer at the beginning of
678
                        // a new data position or at the end of file causing feof() to return true
679
 
680
                    } else {
681
                        // build lines
682
                        if (preg_match('/^version:\s(.+)$/', $data, $match)) {
683
                            // version statement, set version
684
                            $this->version($match[1]);
685
                        } elseif (preg_match('/^\w+(;binary)?::?\s.+$/', $data)) {
686
                            // normal attribute: add line
687
                            $commentmode         = false;
688
                            $this->_lines_next[] = trim($data);
689
                            $datalines_read++;
690
                        } elseif (preg_match('/^\s(.+)$/', $data, $matches)) {
691
                            // wrapped data: unwrap if not in comment mode
692
                            // note that the \s above is some more liberal than
693
                            // the RFC requests as it also matches tabs etc.
694
                            if (!$commentmode) {
695
                                if ($datalines_read == 0) {
696
                                    // first line of entry: wrapped data is illegal
697
                                    $this->dropError('Net_LDAP2_LDIF error: illegal wrapping at input line '.$this->_input_line, $this->_input_line);
698
                                } else {
699
                                    $last                = array_pop($this->_lines_next);
700
                                    $last                = $last.$matches[1];
701
                                    $this->_lines_next[] = $last;
702
                                    $datalines_read++;
703
                                }
704
                            }
705
                        } elseif (preg_match('/^#/', $data)) {
706
                            // LDIF comments
707
                            $commentmode = true;
708
                        } elseif (preg_match('/^\s*$/', $data)) {
709
                            // empty line but we had no data for this
710
                            // entry, so just ignore this line
711
                            $commentmode = false;
712
                        } else {
713
                            $this->dropError('Net_LDAP2_LDIF error: invalid syntax at input line '.$this->_input_line, $this->_input_line);
714
                            continue;
715
                        }
716
 
717
                    }
718
                }
719
            }
720
        }
721
        return $this->_lines_next;
722
    }
723
 
724
    /**
725
    * Convert an attribute and value to LDIF string representation
726
    *
727
    * It honors correct encoding of values according to RFC 2849.
728
    * Line wrapping will occur at the configured maximum but only if
729
    * the value is greater than 40 chars.
730
    *
731
    * @param string $attr_name  Name of the attribute
732
    * @param string $attr_value Value of the attribute
733
    *
734
    * @access protected
735
    * @return string LDIF string for that attribute and value
736
    */
737
    protected function convertAttribute($attr_name, $attr_value)
738
    {
739
        // Handle empty attribute or process
740
        if (strlen($attr_value) == 0) {
741
            $attr_value = " ";
742
        } else {
743
            $base64 = false;
744
            // ASCII-chars that are NOT safe for the
745
            // start and for being inside the value.
746
            // These are the int values of those chars.
747
            $unsafe_init = array(0, 10, 13, 32, 58, 60);
748
            $unsafe      = array(0, 10, 13);
749
 
750
            // Test for illegal init char
751
            $init_ord = ord(substr($attr_value, 0, 1));
752
            if ($init_ord > 127 || in_array($init_ord, $unsafe_init)) {
753
                $base64 = true;
754
            }
755
 
756
            // Test for illegal content char
757
            for ($i = 0; $i < strlen($attr_value); $i++) {
758
                $char_ord = ord(substr($attr_value, $i, 1));
759
                if ($char_ord > 127 || in_array($char_ord, $unsafe)) {
760
                    $base64 = true;
761
                }
762
            }
763
 
764
            // Test for ending space
765
            if (substr($attr_value, -1) == ' ') {
766
                $base64 = true;
767
            }
768
 
769
            // If converting is needed, do it
770
            // Either we have some special chars or a matching "raw" regex
771
            if ($base64 || ($this->_options['raw'] && preg_match($this->_options['raw'], $attr_name))) {
772
                $attr_name .= ':';
773
                $attr_value = base64_encode($attr_value);
774
            }
775
 
776
            // Lowercase attr names if requested
777
            if ($this->_options['lowercase']) $attr_name = strtolower($attr_name);
778
 
779
            // Handle line wrapping
780
            if ($this->_options['wrap'] > 40 && strlen($attr_value) > $this->_options['wrap']) {
781
                $attr_value = wordwrap($attr_value, $this->_options['wrap'], PHP_EOL." ", true);
782
            }
783
        }
784
 
785
        return $attr_name.': '.$attr_value;
786
    }
787
 
788
    /**
789
    * Convert an entries DN to LDIF string representation
790
    *
791
    * It honors correct encoding of values according to RFC 2849.
792
    *
793
    * @param string $dn UTF8-Encoded DN
794
    *
795
    * @access protected
796
    * @return string LDIF string for that DN
797
    * @todo I am not sure, if the UTF8 stuff is correctly handled right now
798
    */
799
    protected function convertDN($dn)
800
    {
801
        $base64 = false;
802
        // ASCII-chars that are NOT safe for the
803
        // start and for being inside the dn.
804
        // These are the int values of those chars.
805
        $unsafe_init = array(0, 10, 13, 32, 58, 60);
806
        $unsafe      = array(0, 10, 13);
807
 
808
        // Test for illegal init char
809
        $init_ord = ord(substr($dn, 0, 1));
810
        if ($init_ord >= 127 || in_array($init_ord, $unsafe_init)) {
811
            $base64 = true;
812
        }
813
 
814
        // Test for illegal content char
815
        for ($i = 0; $i < strlen($dn); $i++) {
816
            $char = substr($dn, $i, 1);
817
            if (ord($char) >= 127 || in_array($init_ord, $unsafe)) {
818
                $base64 = true;
819
            }
820
        }
821
 
822
        // Test for ending space
823
        if (substr($dn, -1) == ' ') {
824
            $base64 = true;
825
        }
826
 
827
        // if converting is needed, do it
828
        return ($base64)? 'dn:: '.base64_encode($dn) : 'dn: '.$dn;
829
    }
830
 
831
    /**
832
    * Writes an attribute to the filehandle
833
    *
834
    * @param string       $attr_name   Name of the attribute
835
    * @param string|array $attr_values Single attribute value or array with attribute values
836
    *
837
    * @access protected
838
    * @return void
839
    */
840
    protected function writeAttribute($attr_name, $attr_values)
841
    {
842
        // write out attribute content
843
        if (!is_array($attr_values)) {
844
            $attr_values = array($attr_values);
845
        }
846
        foreach ($attr_values as $attr_val) {
847
            $line = $this->convertAttribute($attr_name, $attr_val).PHP_EOL;
848
            $this->writeLine($line, 'Net_LDAP2_LDIF error: unable to write attribute '.$attr_name.' of entry '.$this->_entrynum);
849
        }
850
    }
851
 
852
    /**
853
    * Writes a DN to the filehandle
854
    *
855
    * @param string $dn DN to write
856
    *
857
    * @access protected
858
    * @return void
859
    */
860
    protected function writeDN($dn)
861
    {
862
        // prepare DN
863
        if ($this->_options['encode'] == 'base64') {
864
            $dn = $this->convertDN($dn).PHP_EOL;
865
        } elseif ($this->_options['encode'] == 'canonical') {
866
            $dn = Net_LDAP2_Util::canonical_dn($dn, array('casefold' => 'none')).PHP_EOL;
867
        } else {
868
            $dn = $dn.PHP_EOL;
869
        }
870
        $this->writeLine($dn, 'Net_LDAP2_LDIF error: unable to write DN of entry '.$this->_entrynum);
871
    }
872
 
873
    /**
874
    * Finishes an LDIF entry
875
    *
876
    * @access protected
877
    * @return void
878
    */
879
    protected function finishEntry()
880
    {
881
        $this->writeLine(PHP_EOL, 'Net_LDAP2_LDIF error: unable to close entry '.$this->_entrynum);
882
    }
883
 
884
    /**
885
    * Just write an arbitary line to the filehandle
886
    *
887
    * @param string $line  Content to write
888
    * @param string $error If error occurs, drop this message
889
    *
890
    * @access protected
891
    * @return true|false
892
    */
893
    protected function writeLine($line, $error = 'Net_LDAP2_LDIF error: unable to write to filehandle')
894
    {
895
        if (is_resource($this->handle()) && fwrite($this->handle(), $line, strlen($line)) === false) {
896
            $this->dropError($error);
897
            return false;
898
        } else {
899
            return true;
900
        }
901
    }
902
 
903
    /**
904
    * Optionally raises an error and pushes the error on the error cache
905
    *
906
    * @param string $msg  Errortext
907
    * @param int    $line Line in the LDIF that caused the error
908
    *
909
    * @access protected
910
    * @return void
911
    */
912
    protected function dropError($msg, $line = null)
913
    {
914
        $this->_error['error'] = new Net_LDAP2_Error($msg);
915
        if ($line !== null) $this->_error['line'] = $line;
916
 
917
        if ($this->_options['onerror'] == 'die') {
918
            die($msg.PHP_EOL);
919
        } elseif ($this->_options['onerror'] == 'warn') {
920
            echo $msg.PHP_EOL;
921
        }
922
    }
923
}
924
?>