1<?php // $Id: Server.php 17189 2007-11-17 08:53:36Z bharat $
2/*
3   +----------------------------------------------------------------------+
4   | Copyright (c) 2002-2007 Christian Stocker, Hartmut Holzgraefe        |
5   | All rights reserved                                                  |
6   |                                                                      |
7   | Redistribution and use in source and binary forms, with or without   |
8   | modification, are permitted provided that the following conditions   |
9   | are met:                                                             |
10   |                                                                      |
11   | 1. Redistributions of source code must retain the above copyright    |
12   |    notice, this list of conditions and the following disclaimer.     |
13   | 2. Redistributions in binary form must reproduce the above copyright |
14   |    notice, this list of conditions and the following disclaimer in   |
15   |    the documentation and/or other materials provided with the        |
16   |    distribution.                                                     |
17   | 3. The names of the authors may not be used to endorse or promote    |
18   |    products derived from this software without specific prior        |
19   |    written permission.                                               |
20   |                                                                      |
21   | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS  |
22   | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT    |
23   | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS    |
24   | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE       |
25   | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,  |
26   | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
27   | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;     |
28   | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER     |
29   | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT   |
30   | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN    |
31   | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE      |
32   | POSSIBILITY OF SUCH DAMAGE.                                          |
33   +----------------------------------------------------------------------+
34*/
35
36require_once(dirname(__FILE__) . '/Tools/_parse_propfind.php');
37require_once(dirname(__FILE__) . '/Tools/_parse_proppatch.php');
38require_once(dirname(__FILE__) . '/Tools/_parse_lockinfo.php');
39
40define('HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE',
41    'urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882');
42
43/**
44 * Virtual base class for implementing WebDAV servers
45 *
46 * WebDAV server base class, needs to be extended to do useful work
47 *
48 * @package HTTP_WebDAV_Server
49 * @author Hartmut Holzgraefe <hholzgra@php.net>
50 * @version 0.99.1dev
51 */
52class HTTP_WebDAV_Server
53{
54    // {{{ Member Variables
55
56    /**
57     * URL path for this request
58     *
59     * @var string
60     */
61    var $path;
62
63    /**
64     * Base URL for this request
65     *
66     * See PHP parse_url structure
67     *
68     * @var array
69     */
70    var $baseUrl;
71
72    /**
73     * Realm string to be used in authentification popups
74     *
75     * @var string
76     */
77    var $http_auth_realm = 'PHP WebDAV';
78
79    /**
80     * String to be used in "X-Dav-Powered-By" header
81     *
82     * @var string
83     */
84    var $dav_powered_by = '';
85
86    /**
87     * Remember parsed If: (RFC2518 9.4) header conditions
88     *
89     * @var array
90     */
91    var $_if_header_uris = array();
92
93    /**
94     * HTTP response headers
95     *
96     * @var array
97     */
98    var $headers = array();
99
100    /**
101     * Encoding of property values passed in
102     *
103     * @var string
104     */
105    var $_prop_encoding = 'utf-8';
106
107    // }}}
108
109    // {{{ handleRequest
110
111    /**
112     * Handle WebDAV request
113     *
114     * Dispatch WebDAV request to the apropriate method wrapper
115     *
116     * @param void
117     * @return void
118     */
119    function handleRequest()
120    {
121        // identify ourselves
122        if (empty($this->dav_powered_by)) {
123            $this->dav_powered_by = 'PHP class: ' . get_class($this);
124        }
125        $this->setResponseHeader('X-Dav-Powered-By: ' . $this->dav_powered_by);
126
127        // set path
128        if (empty($this->path)) {
129            $this->path = $this->_urldecode($_SERVER['PATH_INFO']);
130            $this->path = trim($this->path, '/');
131        }
132
133        if (ini_get('magic_quotes_gpc')) {
134            $this->path = stripslashes($this->path);
135        }
136
137        // set base URL
138        if (empty($this->baseUrl)) {
139            $this->baseUrl = parse_url(
140                "http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
141            $this->baseUrl['path'] = substr($this->baseUrl['path'], 0,
142                strlen($this->baseUrl['path']) - strlen($_SERVER['PATH_INFO']));
143        }
144
145        // check authentication
146        if (!$this->check_auth_wrapper()) {
147
148            // RFC2518 says we must use Digest instead of Basic but Microsoft
149            // clients do not support Digest and we don't support NTLM or
150            // Kerberos so we are stuck with Basic here
151            $this->setResponseHeader('WWW-Authenticate: Basic realm="'
152                . $this->http_auth_realm . '"');
153
154            // Windows seems to require this being the last header sent
155            // (changed according to PECL bug #3138)
156            $this->setResponseStatus('401 Authentication Required');
157            return;
158        }
159
160        // check
161        if (!$this->_check_if_header_conditions()) {
162            $this->setResponseStatus('412 Precondition Failed');
163            return;
164        }
165
166        // detect requested method names
167        $method = strtolower($_SERVER['REQUEST_METHOD']);
168        $wrapper = $method . '_wrapper';
169
170        // emulate HEAD using GET if no HEAD method found
171        if ($wrapper == 'head_wrapper' &&
172                !method_exists($this, 'head')) {
173            $method = 'get';
174        }
175
176        if (method_exists($this, $method) &&
177                method_exists($this, $wrapper) ||
178                $method == 'options') {
179            $this->$wrapper();
180            return;
181        }
182
183        // method not found/implemented
184        if ($method == 'lock') {
185            $this->setResponseStatus('412 Precondition Failed');
186            return;
187        }
188
189        // tell client what's allowed
190        $this->setResponseStatus('405 Method Not Allowed');
191        $this->setResponseHeader('Allow: ' . implode(', ', $this->_allow()));
192    }
193
194    // }}}
195
196    // {{{ abstract WebDAV methods
197
198    // {{{ PROPFIND
199
200    /**
201     * PROPFIND implementation
202     *
203     * @abstract
204     * @param array &$params
205     * @returns int HTTP-Statuscode
206     */
207
208    /* abstract
209       function propfind()
210       {
211           // dummy entry for PHPDoc
212       }
213    */
214
215    // }}}
216
217    // {{{ PROPPATCH
218
219    /**
220     * PROPPATCH implementation
221     *
222     * @abstract
223     * @param array &$params
224     * @returns int HTTP-Statuscode
225     */
226
227    /* abstract
228       function proppatch()
229       {
230           // dummy entry for PHPDoc
231       }
232    */
233
234    // }}}
235
236    // {{{ MKCOL
237
238    /**
239     * MKCOL implementation
240     *
241     * @abstract
242     * @param array &$params
243     * @returns int HTTP-Statuscode
244     */
245
246    /* abstract
247       function mkcol()
248       {
249           // dummy entry for PHPDoc
250       }
251    */
252
253    // }}}
254
255    // {{{ GET
256
257    /**
258     * GET implementation
259     *
260     * Overload this method to retrieve resources from your server
261     *
262     * @abstract
263     * @param array &$params array of input and output parameters
264     * <br><b>input</b><ul>
265     * <li> path -
266     * </ul>
267     * <br><b>output</b><ul>
268     * <li> size -
269     * </ul>
270     * @returns int HTTP-Statuscode
271     */
272
273    /* abstract
274       function get()
275       {
276           // dummy entry for PHPDoc
277       }
278    */
279
280    // }}}
281
282    // {{{ DELETE
283
284    /**
285     * DELETE implementation
286     *
287     * @abstract
288     * @param array &$params
289     * @returns int HTTP-Statuscode
290     */
291
292    /* abstract
293       function delete()
294       {
295           // dummy entry for PHPDoc
296       }
297    */
298
299    // }}}
300
301    // {{{ PUT
302
303    /**
304     * PUT implementation
305     *
306     * @abstract
307     * @param array &$params
308     * @returns int HTTP-Statuscode
309     */
310
311    /* abstract
312       function put()
313       {
314           // dummy entry for PHPDoc
315       }
316    */
317
318    // }}}
319
320    // {{{ COPY
321
322    /**
323     * COPY implementation
324     *
325     * @abstract
326     * @param array &$params
327     * @returns int HTTP-Statuscode
328     */
329
330    /* abstract
331       function copy()
332       {
333           // dummy entry for PHPDoc
334       }
335    */
336
337    // }}}
338
339    // {{{ MOVE
340
341    /**
342     * MOVE implementation
343     *
344     * @abstract
345     * @param array &$params
346     * @returns int HTTP-Statuscode
347     */
348
349    /* abstract
350       function move()
351       {
352           // dummy entry for PHPDoc
353       }
354    */
355
356    // }}}
357
358    // {{{ LOCK
359
360    /**
361     * LOCK implementation
362     *
363     * @abstract
364     * @param array &$params
365     * @returns int HTTP-Statuscode
366     */
367
368    /* abstract
369       function lock()
370       {
371           // dummy entry for PHPDoc
372       }
373    */
374
375    // }}}
376
377    // {{{ UNLOCK
378
379    /**
380     * UNLOCK implementation
381     *
382     * @abstract
383     * @param array &$params
384     * @returns int HTTP-Statuscode
385     */
386
387    /* abstract
388       function unlock()
389       {
390           // dummy entry for PHPDoc
391       }
392    */
393
394    // }}}
395
396    // }}}
397
398    // {{{ other abstract methods
399
400    // {{{ checkAuth
401
402    /**
403     * Check authentication
404     *
405     * Overload this method to retrieve and confirm authentication information
406     *
407     * @abstract
408     * @param string type Authentication type, e.g. "basic" or "digest"
409     * @param string username Transmitted username
410     * @param string passwort Transmitted password
411     * @returns bool Authentication status
412     */
413
414    /* abstract
415       function checkAuth($type, $username, $password)
416       {
417           // dummy entry for PHPDoc
418       }
419    */
420
421    // }}}
422
423    // {{{ getLocks
424
425    /**
426     * Get lock entries for a resource
427     *
428     * Overload this method to return shared and exclusive locks active for
429     * this resource
430     *
431     * @abstract
432     * @param string resource path to check
433     * @returns array of lock entries each consisting
434     *                of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
435     */
436
437    /* abstract
438       function getLocks($path)
439       {
440           // dummy entry for PHPDoc
441       }
442    */
443
444    // }}}
445
446    // }}}
447
448    // {{{ WebDAV HTTP method wrappers
449
450    // {{{ options
451
452    /**
453     * OPTIONS method handler
454     *
455     * The OPTIONS method handler creates a valid OPTIONS reply including Dav:
456     * and Allowed: heaers based on the implemented methods found in the actual
457     * instance
458     *
459     * @param void
460     * @return void
461     */
462    function options()
463    {
464        // get allowed methods
465        $allow = $this->_allow();
466
467        // dav header
468        $dav = array(1); // assume we are always dav class 1 compliant
469        if (in_array('LOCK', $allow) && in_array('UNLOCK', $allow)) {
470            $dav[] = 2; // dav class 2 requires that locking is supported
471        }
472
473        // tell clients what we found
474        $this->setResponseHeader('Allow: ' . implode(', ', $allow));
475        $this->setResponseHeader('DAV: ' . implode(',', $dav));
476        $this->setResponseHeader('Content-Length: 0');
477
478        // Microsoft clients default to the Frontpage protocol unless we tell
479        // them to use WebDAV
480        $this->setResponseHeader('MS-Author-Via: DAV');
481
482        $this->setResponseStatus('200 OK');
483    }
484
485    // }}}
486
487    // {{{ propfind_request_helper
488
489    /**
490     * PROPFIND request helper - prepares data-structures from PROPFIND requests
491     *
492     * @param options
493     * @return void
494     */
495    function propfind_request_helper(&$options)
496    {
497        $options = array();
498        $options['path'] = $this->path;
499
500        // get depth from header (default is 'infinity')
501        $options['depth'] = 'infinity';
502        if ($this->_hasNonEmptyDepthRequestHeader()) {
503            $options['depth'] = $_SERVER['HTTP_DEPTH'];
504        }
505
506        // analyze request payload
507        $parser = new _parse_propfind($this->openRequestBody());
508        if (!$parser->success) {
509            $this->setResponseStatus('400 Bad Request');
510            return;
511        }
512
513        $options['props'] = $parser->props;
514
515        return true;
516    }
517
518    // }}}
519
520    // {{{ propfind_response_helper
521
522    /**
523     * PROPFIND response helper - format PROPFIND response
524     *
525     * @param options
526     * @param files
527     * @return void
528     */
529    function propfind_response_helper($options, $files)
530    {
531        $responses = array();
532
533        // now loop over all returned files
534        foreach ($files as $file) {
535            $response = array();
536
537            if (empty($file['href'])) {
538                $response['href'] = $this->getHref($file['path']);
539            } else {
540                $response['href'] = $file['href'];
541            }
542
543            $response['propstat'] = array();
544
545            // collect namespaces here
546            $response['namespaces'] = array();
547            if (!empty($options['namespaces'])) {
548                $response['namespaces'] = $options['namespaces'];
549            }
550
551            // Microsoft needs this special namespace for date and time values
552            $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] =
553                'ns' . count($response['namespaces']);
554
555            if (is_array($options['props'])) {
556
557                // loop over all requested properties
558                foreach ($options['props'] as $reqprop) {
559                    $status = '200 OK';
560                    $prop = $this->getProp($reqprop, $file, $options);
561
562                    if (!empty($prop['status'])) {
563                        $status = $prop['status'];
564                    }
565
566                    if (empty($response['propstat'][$status])) {
567                        $response['propstat'][$status] = array();
568                    }
569
570                    $response['propstat'][$status][] = $prop;
571
572                    // namespace handling
573                    if (empty($prop['ns']) || // empty namespace
574                            $prop['ns'] == 'DAV:' || // default namespace
575                            !empty($response['namespaces'][$prop['ns']])) { // already known
576                        continue;
577                    }
578
579                    // register namespace
580                    $response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']);
581                }
582            } else if (is_array($file['props'])) {
583
584                // loop over all returned properties
585                foreach ($file['props'] as $prop) {
586                    $status = '200 OK';
587
588                    if (!empty($prop['status'])) {
589                        $status = $prop['status'];
590                    }
591
592                    if (empty($response['propstat'][$status])) {
593                        $response['propstat'][$status] = array();
594                    }
595
596                    if ($options['props'] == 'propname') {
597
598                        // only names of all existing properties were requested
599                        // so remove values
600                        unset($prop['value']);
601                    }
602
603                    $response['propstat'][$status][] = $prop;
604                        unset($prop['value']);
605
606                    // namespace handling
607                    if (empty($prop['ns']) || // empty namespace
608                            $prop['ns'] == 'DAV:' || // default namespace
609                            !empty($response['namespaces'][$prop['ns']])) { // already known
610                        continue;
611                    }
612
613                    // register namespace
614                    $response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']);
615                }
616            }
617
618            $responses[] = $response;
619        }
620
621        $this->_multistatusResponseHelper($responses);
622    }
623
624    // }}}
625
626    // {{{ propfind_wrapper
627
628    /**
629     * PROPFIND method wrapper
630     *
631     * @param void
632     * @return void
633     */
634    function propfind_wrapper()
635    {
636        // prepare data-structure from PROPFIND request
637        if (!$this->propfind_request_helper($options)) {
638            return;
639        }
640
641        // call user handler
642        if (!$this->propfind($options, $files)) {
643            return;
644        }
645
646        // format PROPFIND response
647        $this->propfind_response_helper($options, $files);
648    }
649
650    // }}}
651
652    // {{{ proppatch_request_helper
653
654    /**
655     * PROPPATCH request helper - prepares data-structures from PROPPATCH requests
656     *
657     * @param options
658     * @return void
659     */
660    function proppatch_request_helper(&$options)
661    {
662        $options = array();
663        $options['path'] = $this->path;
664
665        $propinfo = new _parse_proppatch($this->openRequestBody());
666
667        if (!$propinfo->success) {
668            $this->setResponseStatus('400 Bad Request');
669            return;
670        }
671
672        $options['props'] = $propinfo->props;
673
674        return true;
675    }
676
677    // }}}
678
679    // {{{ proppatch_response_helper
680
681    /**
682     * PROPPATCH response helper - format PROPPATCH response
683     *
684     * @param options
685     * @param responsedescr
686     * @return void
687     */
688    function proppatch_response_helper($options, $responsedescription=null)
689    {
690        $response = array();
691
692        if (empty($options['href'])) {
693            $response['href'] = $this->getHref($options['path']);
694        } else {
695            $response['href'] = $options['href'];
696        }
697
698        $response['propstat'] = array();
699
700        // collect namespaces here
701        $response['namespaces'] = array();
702        if (!empty($options['namespaces'])) {
703            $response['namespaces'] = $options['namespaces'];
704        }
705
706        if (!empty($options['props']) && is_array($options['props'])) {
707            foreach ($options['props'] as $prop) {
708                $status = '200 OK';
709                if (!empty($prop['status'])) {
710                    $status = $prop['status'];
711                }
712
713                if (empty($response['propstat'][$status])) {
714                    $response['propstat'][$status] = array();
715                }
716
717                $response['propstat'][$status][] = $prop;
718
719                // namespace handling
720                if (empty($prop['ns']) || // empty namespace
721                        $prop['ns'] == 'DAV:' || // default namespace
722                        !empty($response['namespaces'][$prop['ns']])) { // already known
723                    continue;
724                }
725
726                // register namespace
727                $response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']);
728            }
729        }
730
731        $response['responsedescription'] = $responsedescription;
732
733        $this->_multistatusResponseHelper(array($response));
734    }
735
736    // }}}
737
738    // {{{ proppatch_wrapper
739
740    /**
741     * PROPPATCH method wrapper
742     *
743     * @param void
744     * @return void
745     */
746    function proppatch_wrapper()
747    {
748        // check resource is not locked
749        if (!$this->check_locks_wrapper($this->path)) {
750            $this->setResponseStatus('423 Locked');
751            return;
752        }
753
754        // perpare data-structure from PROPATCH request
755        if (!$this->proppatch_request_helper($options)) {
756            return;
757        }
758
759        // call user handler
760        $responsedescription = $this->proppatch($options);
761
762        // format PROPPATCH response
763        $this->proppatch_response_helper($options, $responsedescription);
764    }
765
766    // }}}
767
768    // {{{ mkcol_wrapper
769
770    /**
771     * MKCOL method wrapper
772     *
773     * @param void
774     * @return void
775     */
776    function mkcol_wrapper()
777    {
778        $options = array();
779        $options['path'] = $this->path;
780
781        $status = $this->mkcol($options);
782
783        $this->setResponseStatus($status);
784    }
785
786    // }}}
787
788    // {{{ get_request_helper
789
790    /**
791     * GET request helper - prepares data-structures from GET requests
792     *
793     * @param options
794     * @return void
795     */
796    function get_request_helper(&$options)
797    {
798        // TODO check for invalid stream
799
800        $options = array();
801        $options['path'] = $this->path;
802
803        $this->_get_ranges($options);
804
805        return true;
806    }
807
808    /**
809     * Parse HTTP Range: header
810     *
811     * @param  array options array to store result in
812     * @return void
813     */
814    function _get_ranges(&$options)
815    {
816        // process Range: header if present
817        if (!empty($_SERVER['HTTP_RANGE'])) {
818
819            // we only support standard 'bytes' range specifications for now
820            if (ereg('bytes[[:space:]]*=[[:space:]]*(.+)', $_SERVER['HTTP_RANGE'], $matches)) {
821                $options['ranges'] = array();
822
823                // ranges are comma separated
824                foreach (explode(',', $matches[1]) as $range) {
825                    // ranges are either from-to pairs or just end positions
826                    list($start, $end) = explode('-', $range);
827                    $options['ranges'][] = ($start === '') ? array('last' => $end) : array('start' => $start, 'end' => $end);
828                }
829            }
830        }
831    }
832
833    // }}}
834
835    // {{{ get_response_helper
836
837    /**
838     * GET response helper - format GET response
839     *
840     * @param options
841     * @param status
842     * @return void
843     */
844    function get_response_helper($options, $status)
845    {
846        if (empty($status)) {
847            $status = '404 Not Found';
848        }
849
850        // set headers before we start printing
851        $this->setResponseStatus($status);
852
853        if ($status !== true) {
854            return;
855        }
856
857        if (empty($options['mimetype'])) {
858            $options['mimetype'] = 'application/octet-stream';
859        }
860        $this->setResponseHeader("Content-Type: $options[mimetype]");
861
862        if (!empty($options['mtime'])) {
863            $this->setResponseHeader('Last-Modified:'
864                . gmdate('D, d M Y H:i:s', $options['mtime']) . 'GMT');
865        }
866
867        if ($options['stream']) {
868            // GET handler returned a stream
869
870            if (!empty($options['ranges']) &&
871                    (fseek($options['stream'], 0, SEEK_SET) === 0)) {
872                // partial request and stream is seekable
873
874                if (count($options['ranges']) === 1) {
875                    $range = $options['ranges'][0];
876
877                    if (!empty($range['start'])) {
878                        fseek($options['stream'], $range['start'], SEEK_SET);
879                        if (feof($options['stream'])) {
880                            $this->setResponseStatus(
881                                '416 Requested Range Not Satisfiable');
882                            return;
883                        }
884
885                        if (!empty($range['end'])) {
886                            $size = $range['end'] - $range['start'] + 1;
887                            $this->setResponseStatus('206 Partial');
888                            $this->setResponseHeader("Content-Length: $size");
889                            $this->setResponseHeader(
890                                "Content-Range: $range[start]-$range[end]/"
891                                . (!empty($options['size']) ? $options['size'] : '*'));
892                            while ($size && !feof($options['stream'])) {
893                                $buffer = fread($options['stream'], 4096);
894                                $size -= strlen($buffer);
895                                echo $buffer;
896                            }
897                        } else {
898                            $this->setResponseStatus('206 Partial');
899                            if (!empty($options['size'])) {
900                                $this->setResponseHeader("Content-Length: "
901                                    . ($options['size'] - $range['start']));
902                                $this->setResponseHeader(
903                                    "Content-Range: $range[start]-$range[end]/"
904                                    . (!empty($options['size']) ? $options['size'] : '*'));
905                            }
906                            fpassthru($options['stream']);
907                        }
908                    } else {
909                        $this->setResponseHeader("Content-Length: $range[last]");
910                        fseek($options['stream'], -$range['last'], SEEK_END);
911                        fpassthru($options['stream']);
912                    }
913                } else {
914                    $this->_multipart_byterange_header(); // init multipart
915                    foreach ($options['ranges'] as $range) {
916
917                        // TODO what if size unknown? 500?
918                        if (!empty($range['start'])) {
919                            $from = $range['start'];
920                            $to = !empty($range['end']) ? $range['end'] : $options['size'] - 1;
921                        } else {
922                            $from = $options['size'] - $range['last'] - 1;
923                            $to = $options['size'] - 1;
924                        }
925                        $total = !empty($options['size']) ? $options['size'] : '*';
926                        $size = $to - $from + 1;
927                        $this->_multipart_byterange_header($options['mimetype'],
928                            $from, $to, $total);
929
930                        fseek($options['stream'], $start, SEEK_SET);
931                        while ($size && !feof($options['stream'])) {
932                            $buffer = fread($options['stream'], 4096);
933                            $size -= strlen($buffer);
934                            echo $buffer;
935                        }
936                    }
937
938                    // end multipart
939                    $this->_multipart_byterange_header();
940                }
941            } else {
942                // normal request or stream isn't seekable, return full content
943                if (!empty($options['size'])) {
944                    $this->setResponseHeader("Content-Length: $options[size]");
945                }
946
947                fpassthru($options['stream']);
948            }
949        } else if (!empty($options['data']))  {
950            if (is_array($options['data'])) {
951                // reply to partial request
952            } else {
953                $this->setResponseHeader("Content-Length: "
954                    . strlen($options['data']));
955                echo $options['data'];
956            }
957        }
958    }
959
960    /**
961     * Generate separator headers for multipart response
962     *
963     * First and last call happen without parameters to generate the initial
964     * header and closing sequence, all calls inbetween require content
965     * mimetype, start and end byte position and optionaly the total byte
966     * length of the requested resource
967     *
968     * @param string mimetype
969     * @param int start byte position
970     * @param int end byte position
971     * @param int total resource byte size
972     */
973    function _multipart_byterange_header($mimetype = false, $from = false,
974        $to = false, $total = false)
975    {
976        if ($mimetype === false) {
977            if (empty($this->multipart_separator)) {
978                // init
979                // a little naive, this sequence *might* be part of the content
980                // but it's really not likely and rather expensive to check
981                $this->multipart_separator = 'SEPARATOR_' . md5(microtime());
982
983                // generate HTTP header
984                $this->setResponseHeader(
985                    'Content-Type: multipart/byteranges; boundary='
986                    . $this->multipart_separator);
987                return;
988            }
989
990            // end
991            // generate closing multipart sequence
992            echo "\n--{$this->multipart_separator}--";
993            return;
994        }
995
996        // generate separator and header for next part
997        echo "\n--{$this->multipart_separator}\n";
998        echo "Content-Type: $mimetype\n";
999        echo "Content-Range: $from-$to/"
1000            . ($total === false ? "*" : $total) . "\n\n";
1001    }
1002
1003    // }}}
1004
1005    // {{{ get_wrapper
1006
1007    /**
1008     * GET method wrapper
1009     *
1010     * @param void
1011     * @return void
1012     */
1013    function get_wrapper()
1014    {
1015        // perpare data-structure from GET request
1016        if (!$this->get_request_helper($options)) {
1017            return;
1018        }
1019
1020        // call user handler
1021        $status = $this->get($options);
1022
1023        // format GET response
1024        $this->get_response_helper($options, $status);
1025    }
1026
1027    // }}}
1028
1029    // {{{ head_response_helper
1030
1031    /**
1032     * HEAD response helper - format HEAD response
1033     *
1034     * @param options
1035     * @param status
1036     * @return void
1037     */
1038    function head_response_helper($options, $status)
1039    {
1040        if (empty($status)) {
1041            $status = '404 Not Found';
1042        }
1043
1044        // set headers before we start printing
1045        $this->setResponseStatus($status);
1046
1047        if ($status !== true) {
1048            return;
1049        }
1050
1051        if (empty($options['mimetype'])) {
1052            $options['mimetype'] = 'application/octet-stream';
1053        }
1054        $this->setResponseHeader("Content-Type: $options[mimetype]");
1055
1056        if (!empty($options['mtime'])) {
1057            $this->setResponseHeader('Last-Modified:'
1058                . gmdate('D, d M Y H:i:s', $options['mtime']) . 'GMT');
1059        }
1060
1061        if (!empty($options['stream'])) {
1062            // GET handler returned a stream
1063
1064            if (!empty($options['ranges'])
1065                    && (fseek($options['stream'], 0, SEEK_SET) === 0)) {
1066                // partial request and stream is seekable
1067
1068                if (count($options['ranges']) === 1) {
1069                    $range = $options['ranges'][0];
1070
1071                    if (!empty($range['start'])) {
1072                        fseek($options['stream'], $range['start'], SEEK_SET);
1073                        if (feof($options['stream'])) {
1074                            $this->setResponseStatus(
1075                                '416 Requested Range Not Satisfiable');
1076                            return;
1077                        }
1078
1079                        if (!empty($range['end'])) {
1080                            $size = $range['end'] - $range['start'] + 1;
1081                            $this->setResponseStatus('206 Partial');
1082                            $this->setResponseHeader("Content-Length: $size");
1083                            $this->setResponseHeader(
1084                                "Content-Range: $range[start]-$range[end]/"
1085                                . (!empty($options['size']) ? $options['size'] : '*'));
1086                        } else {
1087                            $this->setResponseStatus('206 Partial');
1088                            if (!empty($options['size'])) {
1089                                $this->setResponseHeader("Content-Length: "
1090                                    . ($options['size'] - $range['start']));
1091                                $this->setResponseHeader(
1092                                    "Content-Range: $start-$end/"
1093                                    . (!empty($options['size']) ? $options['size'] : '*'));
1094                            }
1095                        }
1096                    } else {
1097                        $this->setResponseHeader(
1098                            "Content-Length: $range[last]");
1099                        fseek($options['stream'], -$range['last'], SEEK_END);
1100                    }
1101                } else {
1102                    $this->_multipart_byterange_header(); // init multipart
1103                    foreach ($options['ranges'] as $range) {
1104
1105                        // TODO what if size unknown? 500?
1106                        if (!empty($range['start'])) {
1107                            $from = $range['start'];
1108                            $to = !empty($range['end']) ? $range['end'] :
1109                                $options['size'] - 1;
1110                        } else {
1111                            $from = $options['size'] - $range['last'] - 1;
1112                            $to = $options['size'] - 1;
1113                        }
1114                        $total = !empty($options['size']) ? $options['size'] :
1115                            '*';
1116                        $size = $to - $from + 1;
1117                        $this->_multipart_byterange_header($options['mimetype'],
1118                            $from, $to, $total);
1119
1120                        fseek($options['stream'], $start, SEEK_SET);
1121                    }
1122                    $this->_multipart_byterange_header(); // end multipart
1123                }
1124            } else {
1125                // normal request or stream isn't seekable, return full content
1126                if (!empty($options['size'])) {
1127                    $this->setResponseHeader("Content-Length: $options[size]");
1128                }
1129            }
1130        } else if (!empty($options['data']))  {
1131            if (is_array($options['data'])) {
1132                // reply to partial request
1133            } else {
1134                $this->setResponseHeader("Content-Length: "
1135                    . strlen($options['data']));
1136            }
1137        }
1138    }
1139
1140    // }}}
1141
1142    // {{{ head_wrapper
1143
1144    /**
1145     * HEAD method wrapper
1146     *
1147     * @param void
1148     * @return void
1149     */
1150    function head_wrapper()
1151    {
1152        $options = array();
1153        $options['path'] = $this->path;
1154
1155        // call user handler
1156        if (method_exists($this, 'head')) {
1157            $status = $this->head($options);
1158        } else {
1159
1160            // can emulate HEAD using GET
1161            ob_start();
1162            $status = $this->get($options);
1163            ob_end_clean();
1164        }
1165
1166        // format HEAD response
1167        $this->head_response_helper($options, $status);
1168    }
1169
1170    // }}}
1171
1172    // {{{ put_request_helper
1173
1174    /**
1175     * PUT request helper - prepares data-structures from PUT requests
1176     *
1177     * @param options
1178     * @return void
1179     */
1180    function put_request_helper(&$options)
1181    {
1182        $options = array();
1183        $options['path'] = $this->path;
1184
1185        /* Content-Length may be zero */
1186        if (!isset($_SERVER['CONTENT_LENGTH'])) {
1187            return;
1188        }
1189        $options['content_length'] = $_SERVER['CONTENT_LENGTH'];
1190
1191        // default content type if none given
1192        $options['content_type'] = 'application/unknown';
1193
1194        // get the content-type
1195        if (!empty($_SERVER['CONTENT_TYPE'])) {
1196
1197            // for now we do not support any sort of multipart requests
1198            if (!strncmp($_SERVER['CONTENT_TYPE'], 'multipart/', 10)) {
1199                $this->setResponseStatus('501 Not Implemented');
1200                echo 'The service does not support mulipart PUT requests';
1201                return;
1202            }
1203
1204            $options['content_type'] = $_SERVER['CONTENT_TYPE'];
1205        }
1206
1207        // RFC2616 2.6: The recipient of the entity MUST NOT ignore any
1208        // Content-* (e.g. Content-Range) headers that it does not understand
1209        // or implement and MUST return a 501 (Not Implemented) response in
1210        // such cases.
1211        foreach ($_SERVER as $key => $value) {
1212            if (strncmp($key, 'HTTP_CONTENT', 11)) {
1213                continue;
1214            }
1215
1216            switch ($key) {
1217            case 'HTTP_CONTENT_ENCODING': // RFC2616 14.11
1218
1219                // TODO support this if ext/zlib filters are available
1220                $this->setResponseStatus('501 Not Implemented');
1221                echo "The service does not support '$value' content encoding";
1222                return;
1223
1224            case 'HTTP_CONTENT_LANGUAGE': // RFC2616 14.12
1225
1226                // we assume it is not critical if this one is ignored in the
1227                // actual PUT implementation
1228                $options['content_language'] = $value;
1229                break;
1230
1231            case 'HTTP_CONTENT_LENGTH':
1232
1233                // defined on IIS and has the same value as CONTENT_LENGTH
1234                break;
1235
1236            case 'HTTP_CONTENT_LOCATION': // RFC2616 14.14
1237
1238                // meaning of the Content-Location header in PUT or POST
1239                // requests is undefined; servers are free to ignore it in
1240                // those cases
1241                break;
1242
1243            case 'HTTP_CONTENT_RANGE': // RFC2616 14.16
1244
1245                // single byte range requests are supported
1246                // the header format is also specified in RFC2616 14.16
1247                // TODO we have to ensure that implementations support this or send 501 instead
1248                if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $value, $matches)) {
1249                    $this->setResponseStatus('400 Bad Request');
1250                    echo 'The service does only support single byte ranges';
1251                    return;
1252                }
1253
1254                $range = array('start' => $matches[1], 'end' => $matches[2]);
1255                if (is_numeric($matches[3])) {
1256                    $range['total_length'] = $matches[3];
1257                }
1258                $option['ranges'][] = $range;
1259
1260                // TODO make sure the implementation supports partial PUT
1261                // this has to be done in advance to avoid data being overwritten
1262                // on implementations that do not support this...
1263                break;
1264
1265            case 'HTTP_CONTENT_MD5': // RFC2616 14.15
1266
1267                // TODO maybe we can just pretend here?
1268                $this->setResponseStatus('501 Not Implemented');
1269                echo 'The service does not support content MD5 checksum verification';
1270                return;
1271
1272            case 'HTTP_CONTENT_TYPE':
1273
1274                // defined on IIS and has the same value as CONTENT_TYPE
1275                break;
1276
1277            default:
1278
1279                // any other unknown Content-* headers
1280                $this->setResponseStatus('501 Not Implemented');
1281                echo "The service does not support '$key'";
1282                return;
1283            }
1284        }
1285
1286        $options['stream'] = $this->openRequestBody();
1287
1288        return true;
1289    }
1290
1291    // }}}
1292
1293    // {{{ put_response_helper
1294
1295    /**
1296     * PUT response helper - format PUT response
1297     *
1298     * @param options
1299     * @param status
1300     * @return void
1301     */
1302    function put_response_helper($options, $status)
1303    {
1304        if (empty($status)) {
1305            $status = '403 Forbidden';
1306        } else if (is_resource($status)
1307                && get_resource_type($status) == 'stream') {
1308            $stream = $status;
1309            $status = isset($options['new']) && $options['new'] === false ? '204 No Content' : '201 Created';
1310
1311            if (!empty($options['ranges'])) {
1312
1313                // TODO multipart support is missing (see also above)
1314                if (0 == fseek($stream, $range[0]['start'], SEEK_SET)) {
1315                    $length = $range[0]['end'] - $range[0]['start'] + 1;
1316                    if (!fwrite($stream, fread($options['stream'], $length))) {
1317                        $status = '403 Forbidden';
1318                    }
1319                } else {
1320                    $status = '403 Forbidden';
1321                }
1322            } else {
1323                while (!feof($options['stream'])) {
1324                    $buf = fread($options['stream'], 4096);
1325                    if (fwrite($stream, $buf) != 4096) {
1326                        break;
1327                    }
1328                }
1329            }
1330
1331            fclose($stream);
1332        }
1333
1334        $this->setResponseStatus($status);
1335    }
1336
1337    // }}}
1338
1339    // {{{ put_wrapper
1340
1341    /**
1342     * PUT method wrapper
1343     *
1344     * @param void
1345     * @return void
1346     */
1347    function put_wrapper()
1348    {
1349        // check resource is not locked
1350        if (!$this->check_locks_wrapper($this->path)) {
1351            $this->setResponseStatus('423 Locked');
1352            return;
1353        }
1354
1355        // perpare data-structure from PUT request
1356        if (!$this->put_request_helper($options)) {
1357            return;
1358        }
1359
1360        // call user handler
1361        $status = $this->put($options);
1362
1363        // format PUT response
1364        $this->put_response_helper($options, $status);
1365    }
1366
1367    // }}}
1368
1369    // {{{ delete_wrapper
1370
1371    /**
1372     * DELETE method wrapper
1373     *
1374     * @param void
1375     * @return void
1376     */
1377    function delete_wrapper()
1378    {
1379        // RFC2518 9.2 last paragraph
1380        if (isset($_SERVER['HTTP_DEPTH'])
1381                && $_SERVER['HTTP_DEPTH'] !== 'infinity') {
1382            $this->setResponseStatus('400 Bad Request');
1383            return;
1384        }
1385
1386        // check resource is not locked
1387        if (!$this->check_locks_wrapper($this->path)) {
1388            $this->setResponseStatus('423 Locked');
1389            return;
1390        }
1391
1392        $options = array();
1393        $options['path'] = $this->path;
1394
1395        // call user handler
1396        $status = $this->delete($options);
1397        if ($status === true) {
1398            $status = '204 No Content';
1399        }
1400
1401        $this->setResponseStatus($status);
1402    }
1403
1404    // }}}
1405
1406    // {{{ copymove_request_helper
1407
1408    /**
1409     * COPY/MOVE request helper - prepares data-structures from COPY/MOVE
1410     * requests
1411     *
1412     * @param options
1413     * @return void
1414     */
1415    function copymove_request_helper(&$options)
1416    {
1417        $options = array();
1418        $options['path'] = $this->path;
1419
1420        $options['depth'] = 'infinity';
1421        if ($this->_hasNonEmptyDepthRequestHeader()) {
1422            $options['depth'] = $_SERVER['HTTP_DEPTH'];
1423        }
1424
1425        // RFC2518 9.6, 8.8.4 and 8.9.3
1426        $options['overwrite'] = true;
1427        if (!empty($_SERVER['HTTP_OVERWRITE'])) {
1428            $options['overwrite'] = $_SERVER['HTTP_OVERWRITE'] == 'T';
1429        }
1430
1431        $url = parse_url($_SERVER['HTTP_DESTINATION']);
1432
1433        // does the destination resource belong on this server?
1434        if ($url['host'] == $this->baseUrl['host']
1435                && (empty($url['port']) ? 80 : $url['port']) == (empty($this->baseUrl['port']) ? 80 : $this->baseUrl['port'])
1436                && !strncmp($url['path'], $this->baseUrl['path'], strlen($this->baseUrl['path']))) {
1437            if (!empty($this->baseurl['query'])) {
1438                foreach (explode('&', $this->baseUrl['query']) as $queryComponent) {
1439                    if (!in_array($queryComponent, explode('&', $url['query']))) {
1440                        $options['dest_url'] = $_SERVER['HTTP_DESTINATION'];
1441
1442                        return true;
1443                    }
1444                }
1445            }
1446
1447            $options['dest'] =
1448                substr($url['path'], strlen($this->baseUrl['path']));
1449
1450            $options['dest'] = $this->_urldecode($options['dest']);
1451            $options['dest'] = trim($options['dest'], '/');
1452
1453            // check source and destination are not the same - data could be lost
1454            // if overwrite is true - RFC2518 8.8.5
1455            if ($options['dest'] == $this->path) {
1456                $this->setResponseStatus('403 Forbidden');
1457                return;
1458            }
1459
1460            return true;
1461        }
1462
1463        $options['dest_url'] = $_SERVER['HTTP_DESTINATION'];
1464
1465        return true;
1466    }
1467
1468    // }}}
1469
1470    // {{{ copy_wrapper
1471
1472    /**
1473     * COPY method wrapper
1474     *
1475     * @param void
1476     * @return void
1477     */
1478    function copy_wrapper()
1479    {
1480        // no need to check source is not locked
1481
1482        // perpare data-structure from COPY request
1483        if (!$this->copymove_request_helper($options)) {
1484            return;
1485        }
1486
1487        // check destination is not locked
1488        if (!empty($options['dest']) &&
1489                !$this->check_locks_wrapper($options['dest'])) {
1490            $this->setResponseStatus('423 Locked');
1491            return;
1492        }
1493
1494        // call user handler
1495        $status = $this->copy($options);
1496        if ($status === true) {
1497            $status = $options['new'] === false ? '204 No Content' :
1498                '201 Created';
1499        }
1500
1501        $this->setResponseStatus($status);
1502    }
1503
1504    // }}}
1505
1506    // {{{ move_wrapper
1507
1508    /**
1509     * MOVE method wrapper
1510     *
1511     * @param void
1512     * @return void
1513     */
1514    function move_wrapper()
1515    {
1516        // check resource is not locked
1517        if (!$this->check_locks_wrapper($this->path)) {
1518            $this->setResponseStatus('423 Locked');
1519            return;
1520        }
1521
1522        // perpare data-structure from MOVE request
1523        if (!$this->copymove_request_helper($options)) {
1524            return;
1525        }
1526
1527        // check destination is not locked
1528        if (!empty($options['dest']) &&
1529                !$this->check_locks_wrapper($options['dest'])) {
1530            $this->setResponseStatus('423 Locked');
1531            return;
1532        }
1533
1534        // call user handler
1535        $status = $this->move($options);
1536        if ($status === true) {
1537            $status = $options['new'] === false ? '204 No Content' :
1538                '201 Created';
1539        }
1540
1541        $this->setResponseStatus($status);
1542    }
1543
1544    // }}}
1545
1546    // {{{ lock_request_helper
1547
1548    /**
1549     * LOCK request helper - prepares data-structures from LOCK requests
1550     *
1551     * @param options
1552     * @return void
1553     */
1554    function lock_request_helper(&$options)
1555    {
1556        $options = array();
1557        $options['path'] = $this->path;
1558
1559        // a LOCK request with an If header but without a body is used to
1560        // refresh a lock.  Content-Lenght may be unset or zero.
1561        if (empty($_SERVER['CONTENT_LENGTH']) && !empty($_SERVER['HTTP_IF'])) {
1562
1563            // FIXME: Refresh multiple locks?
1564            $options['update'] = substr($_SERVER['HTTP_IF'], 2, -2);
1565
1566            return true;
1567        }
1568
1569        $options['depth'] = 'infinity';
1570        if ($this->_hasNonEmptyDepthRequestHeader()) {
1571            $options['depth'] = $_SERVER['HTTP_DEPTH'];
1572        }
1573
1574        if (!empty($_SERVER['HTTP_TIMEOUT'])) {
1575            $options['timeout'] = explode(',', $_SERVER['HTTP_TIMEOUT']);
1576        }
1577
1578        // extract lock request information from request XML payload
1579        $lockinfo = new _parse_lockinfo($this->openRequestBody());
1580        if (!$lockinfo->success) {
1581            $this->setResponseStatus('400 Bad Request');
1582            return;
1583        }
1584
1585        // new lock
1586        $options['scope'] = $lockinfo->lockscope;
1587        $options['type']  = $lockinfo->locktype;
1588        $options['owner'] = $lockinfo->owner;
1589
1590        $options['token'] = $this->_new_locktoken();
1591
1592        return true;
1593    }
1594
1595    // }}}
1596
1597    // {{{ lock_response_helper
1598
1599    /**
1600     * LOCK response helper - format LOCK response
1601     *
1602     * @param options
1603     * @param status
1604     * @return void
1605     */
1606    function lock_response_helper($options, $status)
1607    {
1608        if (!empty($options['locks']) && is_array($options['locks'])) {
1609            $this->setResponseStatus('409 Conflict');
1610
1611            $responses = array();
1612            foreach ($options['locks'] as $lock) {
1613                $response = array();
1614
1615                if (empty($lock['href'])) {
1616                    $response['href'] = $this->getHref($lock['path']);
1617                } else {
1618                    $response['href'] = $lock['href'];
1619                }
1620
1621                $response['status'] = '423 Locked';
1622
1623                $responses[] = $response;
1624            }
1625
1626            $this->_multistatusResponseHelper($responses);
1627
1628            return;
1629        }
1630
1631        if ($status === true) {
1632            $status = '200 OK';
1633        } else if ($status === false) {
1634            $status = '423 Locked';
1635        }
1636
1637        // set headers before we start printing
1638        $this->setResponseStatus($status);
1639
1640        if ($status{0} == 2) { // 2xx states are ok
1641            $this->setResponseHeader('Content-Type: text/xml; charset="utf-8"');
1642
1643            // RFC2518 8.10.1: In order to indicate the lock token associated
1644            // with a newly created lock, a Lock-Token response header MUST be
1645            // included in the response for every successful LOCK request for a
1646            // new lock.  Note that the Lock-Token header would not be returned
1647            // in the response for a successful refresh LOCK request because a
1648            // new lock was not created.
1649            if (empty($options['update']) || !empty($options['token'])) {
1650                $this->setResponseHeader("Lock-Token: <$options[token]>");
1651            }
1652
1653            $lock = array();
1654            foreach (array('scope', 'type', 'depth', 'owner') as $key) {
1655                $lock[$key] = $options[$key];
1656            }
1657
1658            if (!empty($options['expires'])) {
1659                $lock['expires'] = $options['expires'];
1660            } else {
1661                $lock['timeout'] = $options['timeout'];
1662            }
1663
1664            if (!empty($options['update'])) {
1665                $lock['token'] = $options['update'];
1666            } else {
1667                $lock['token'] = $options['token'];
1668            }
1669
1670            echo "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
1671            echo "<D:prop xmlns:D=\"DAV:\">\n";
1672            echo "  <D:lockdiscovery>\n";
1673            echo '    ' . $this->_activelocksResponseHelper(array($lock))
1674                . "\n";
1675            echo "  </D:lockdiscovery>\n";
1676            echo "</D:prop>\n";
1677        }
1678    }
1679
1680    // }}}
1681
1682    // {{{ lock_wrapper
1683
1684    /**
1685     * LOCK method wrapper
1686     *
1687     * @param void
1688     * @return void
1689     */
1690    function lock_wrapper()
1691    {
1692        // perpare data-structure from LOCK request
1693        if (!$this->lock_request_helper($options)) {
1694            return;
1695        }
1696
1697        // check resource is not locked
1698        if (!empty($options['update'])
1699                && !$this->check_locks_wrapper(
1700                    $this->path, $options['scope'] == 'shared')) {
1701            $this->setResponseStatus('423 Locked');
1702            return;
1703        }
1704
1705        $options['locks'] = $this->getDescendentsLocks($this->path);
1706        if (empty($options['locks'])) {
1707
1708            // call user handler
1709            $status = $this->lock($options);
1710        }
1711
1712        // format LOCK response
1713        $this->lock_response_helper($options, $status);
1714    }
1715
1716    // }}}
1717
1718    // {{{ unlock_request_helper
1719
1720    /**
1721     * UNLOCK request helper - prepares data-structures from UNLOCK requests
1722     *
1723     * @param options
1724     * @return void
1725     */
1726    function unlock_request_helper(&$options)
1727    {
1728        $options = array();
1729        $options['path'] = $this->path;
1730
1731        if (empty($_SERVER['HTTP_LOCK_TOKEN'])) {
1732            return;
1733        }
1734
1735        // strip surrounding <>
1736        $options['token'] = substr(trim($_SERVER['HTTP_LOCK_TOKEN']), 1, -1);
1737
1738        return true;
1739    }
1740
1741    // }}}
1742
1743    // {{{ unlock_wrapper
1744
1745    /**
1746     * UNLOCK method wrapper
1747     *
1748     * @param void
1749     * @return void
1750     */
1751    function unlock_wrapper()
1752    {
1753        // perpare data-structure from DELETE request
1754        if (!$this->unlock_request_helper($options)) {
1755            return;
1756        }
1757
1758        // call user handler
1759        $status = $this->unlock($options);
1760
1761        // RFC2518 8.11.1
1762        if ($status === true) {
1763            $status = '204 No Content';
1764        }
1765
1766        $this->setResponseStatus($status);
1767    }
1768
1769    // }}}
1770
1771    function _multistatusResponseHelper($responses)
1772    {
1773        // now we generate the response header...
1774        $this->setResponseStatus('207 Multi-Status', false);
1775        $this->setResponseHeader('Content-Type: text/xml; charset="utf-8"');
1776
1777        // ...& payload
1778        echo "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
1779        echo "<D:multistatus xmlns:D=\"DAV:\">\n";
1780
1781        foreach ($responses as $response) {
1782
1783            // ignore empty or incomplete entries
1784            if (!is_array($response) || empty($response)) {
1785                continue;
1786            }
1787
1788            $namespaces = '';
1789            if (!empty($response['namespaces'])) {
1790                foreach ($response['namespaces'] as $name => $prefix) {
1791                    $namespaces .= " xmlns:$prefix=\"$name\"";
1792                }
1793            }
1794            echo "  <D:response$namespaces>\n";
1795            echo "    <D:href>$response[href]</D:href>\n";
1796
1797            // report all found properties and their values (if any)
1798            // nothing to do if no properties were returend for a file
1799            if (!empty($response['propstat']) &&
1800                    is_array($response['propstat'])) {
1801
1802                foreach ($response['propstat'] as $status => $props) {
1803                    echo "    <D:propstat>\n";
1804                    echo "      <D:prop>\n";
1805
1806                    foreach ($props as $prop) {
1807                        if (!is_array($prop) || empty($prop['name'])) {
1808                            continue;
1809                        }
1810
1811                        // empty properties (cannot use empty for check as '0'
1812                        // is a legal value here)
1813                        if (empty($prop['value']) && (!isset($prop['value'])
1814                                || $prop['value'] !== 0)) {
1815                            if ($prop['ns'] == 'DAV:') {
1816                                echo "        <D:$prop[name]/>\n";
1817                                continue;
1818                            }
1819
1820                            if (!empty($prop['ns'])) {
1821                                echo '        <' . $response['namespaces'][$prop['ns']] . ":$prop[name]/>\n";
1822                                continue;
1823                            }
1824
1825                            echo "        <$prop[name] xmlns=\"\"/>";
1826                            continue;
1827                        }
1828
1829                        // some WebDAV properties need special treatment
1830                        if ($prop['ns'] == 'DAV:') {
1831
1832                            switch ($prop['name']) {
1833                            case 'creationdate':
1834                                echo '        <D:creationdate ' . $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] . ':dt="dateTime.tz">' . gmdate('Y-m-d\TH:i:s\Z', $prop['value']) . "</D:creationdate>\n";
1835                                break;
1836
1837                            case 'getlastmodified':
1838                                echo '        <D:getlastmodified ' . $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] . ':dt="dateTime.rfc1123">' . gmdate('D, d M Y H:i:s', $prop['value']) . " UTC</D:getlastmodified>\n";
1839                                break;
1840
1841                            case 'resourcetype':
1842                                echo "        <D:resourcetype><D:$prop[value]/></D:resourcetype>\n";
1843                                break;
1844
1845                            case 'supportedlock':
1846
1847                                if (is_array($prop['value'])) {
1848                                    $prop['value'] =
1849                                        $this->_lockentriesResponseHelper(
1850                                            $prop['value']);
1851                                }
1852                                echo "        <D:supportedlock>\n";
1853                                echo "          $prop[value]\n";
1854                                echo "        </D:supportedlock>\n";
1855                                break;
1856
1857                            case 'lockdiscovery':
1858
1859                                if (is_array($prop['value'])) {
1860                                    $prop['value'] =
1861                                        $this->_activelocksResponseHelper(
1862                                            $prop['value']);
1863                                }
1864                                echo "        <D:lockdiscovery>\n";
1865                                echo "          $prop[value]\n";
1866                                echo "        </D:lockdiscovery>\n";
1867                                break;
1868
1869                            default:
1870                                echo "        <D:$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['value'])) . "</D:$prop[name]>\n";
1871                            }
1872
1873                            continue;
1874                        }
1875
1876                        if (!empty($prop['ns'])) {
1877                            echo '        <' . $response['namespaces'][$prop['ns']] . ":$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['value'])) . '</' . $response['namespaces'][$prop['ns']] . ":$prop[name]>\n";
1878
1879                            continue;
1880                        }
1881
1882                        echo "        <$prop[name] xmlns=\"\">" . $this->_prop_encode(htmlspecialchars($prop['value'])) . "</$prop[name]>\n";
1883                    }
1884
1885                    echo "      </D:prop>\n";
1886                    echo "      <D:status>HTTP/1.1 $status</D:status>\n";
1887                    echo "    </D:propstat>\n";
1888                }
1889            }
1890
1891            if (!empty($response['status'])) {
1892                echo "    <D:status>HTTP/1.1 $response[status]</D:status>\n";
1893            }
1894
1895            if (!empty($response['responsedescription'])) {
1896                echo '    <D:responsedescription>' . $this->_prop_encode(htmlspecialchars($response['responsedescription'])) . "</D:responsedescription>\n";
1897            }
1898
1899            echo "  </D:response>\n";
1900        }
1901
1902        echo "</D:multistatus>\n";
1903    }
1904
1905    function _activelocksResponseHelper($locks)
1906    {
1907        if (!is_array($locks) || empty($locks)) {
1908            return '';
1909        }
1910
1911        foreach ($locks as $key => $lock) {
1912            if (!is_array($lock) || empty($lock)) {
1913                continue;
1914            }
1915
1916            // check for 'timeout' or 'expires'
1917            $timeout = 'Infinite';
1918            if (!empty($lock['expires'])) {
1919                $timeout = 'Second-' . ($lock['expires'] - time());
1920            } else if (!empty($lock['timeout'])) {
1921
1922                // more than a million is considered an absolute timestamp
1923                // less is more likely a relative value
1924                $timeout = "Second-$lock[timeout]";
1925                if ($lock['timeout'] > 1000000) {
1926                    $timeout = 'Second-' . ($lock['timeout'] - time());
1927                }
1928            }
1929
1930            // genreate response block
1931            $locks[$key] = "<D:activelock>
1932            <D:lockscope><D:$lock[scope]/></D:lockscope>
1933            <D:locktype><D:$lock[type]/></D:locktype>
1934            <D:depth>" . ($lock['depth'] == 'infinity' ? 'Infinity' : $lock['depth']) . "</D:depth>
1935            <D:owner>$lock[owner]</D:owner>
1936            <D:timeout>$timeout</D:timeout>
1937            <D:locktoken><D:href>$lock[token]</D:href></D:locktoken>
1938          </D:activelock>";
1939        }
1940
1941        return implode('', $locks);
1942    }
1943
1944    function _lockentriesResponseHelper($locks)
1945    {
1946        if (!is_array($locks) || empty($locks)) {
1947            return '';
1948        }
1949
1950        foreach ($locks as $key => $lock) {
1951            if (!is_array($lock) || empty($lock)) {
1952                continue;
1953            }
1954
1955            $locks[$key] = "<D:lockentry>
1956            <D:lockscope><D:$lock[scope]/></D:lockscope>
1957            <D:locktype><D:$lock[type]/></D:locktype>
1958          </D:lockentry>";
1959        }
1960
1961        return implode('', $locks);
1962    }
1963
1964    function getHref($path)
1965    {
1966        return $this->baseUrl['path'] . '/' . $path;
1967    }
1968
1969    function getProp($reqprop, $file, $options)
1970    {
1971        // check if property exists in response
1972        foreach ($file['props'] as $prop) {
1973            if ($reqprop['name'] == $prop['name']
1974                    && $reqprop['ns'] == $prop['ns']) {
1975                return $prop;
1976            }
1977        }
1978
1979        if ($reqprop['name'] == 'lockdiscovery'
1980                && $reqprop['ns'] == 'DAV:'
1981                && method_exists($this, 'getLocks')) {
1982            return $this->mkprop('DAV:', 'lockdiscovery',
1983                $this->getLocks($file['path']));
1984        }
1985
1986        // incase the requested property had a value, like calendar-data
1987        unset($reqprop['value']);
1988        $reqprop['status'] = '404 Not Found';
1989
1990        return $reqprop;
1991    }
1992
1993    function getDescendentsLocks($path)
1994    {
1995        $options = array();
1996        $options['path'] = $path;
1997        $options['depth'] = 'infinity';
1998        $options['props'] = array();
1999        $options['props'][] = $this->mkprop('DAV:', 'lockdiscovery', null);
2000
2001        // call user handler
2002        if (!$this->propfind($options, $files)) {
2003            return;
2004        }
2005
2006        return $files;
2007    }
2008
2009    // {{{ _allow()
2010
2011    /**
2012     * List implemented methods
2013     *
2014     * @param void
2015     * @return array something
2016     */
2017    function _allow()
2018    {
2019        // OPTIONS is always there
2020        $allow = array('OPTIONS');
2021
2022        // all other methods need both a method_wrapper() and a method()
2023        // implementation
2024        // the base class defines only wrappers
2025        foreach(get_class_methods($this) as $method) {
2026
2027            // strncmp breaks with negative len -
2028            // http://bugs.php.net/bug.php?id=36944
2029            //if (!strncmp('_wrapper', $method, -8)) {
2030            if (!strcmp(substr($method, -8), '_wrapper')) {
2031                $method = strtolower(substr($method, 0, -8));
2032                if (method_exists($this, $method) &&
2033                        ($method != 'lock' && $method != 'unlock' ||
2034                        method_exists($this, 'getLocks'))) {
2035                    $allow[] = $method;
2036                }
2037            }
2038        }
2039
2040        // we can emulate a missing HEAD implemetation using GET
2041        if (in_array('GET', $allow)) {
2042            $allow[] = 'HEAD';
2043        }
2044
2045        return $allow;
2046    }
2047
2048    // }}}
2049
2050    // {{{ mkprop
2051
2052    /**
2053     * Helper for property element creation
2054     *
2055     * @param string XML namespace (optional)
2056     * @param string property name
2057     * @param string property value
2058     * @return array property array
2059     */
2060    function mkprop()
2061    {
2062        $args = func_get_args();
2063
2064        $prop = array();
2065        $prop['ns'] = 'DAV:';
2066        if (count($args) > 2) {
2067            $prop['ns'] = array_shift($args);
2068        }
2069
2070        $prop['name'] = array_shift($args);
2071        $prop['value'] = array_shift($args);
2072        $prop['status'] = array_shift($args);
2073
2074        return $prop;
2075    }
2076
2077    // }}}
2078
2079    // {{{ check_auth_wrapper
2080
2081    /**
2082     * Check authentication if implemented
2083     *
2084     * @param void
2085     * @return boolean true if authentication succeded or not necessary
2086     */
2087    function check_auth_wrapper()
2088    {
2089        if (method_exists($this, 'checkAuth')) {
2090
2091            // PEAR style method name
2092            return $this->checkAuth(@$_SERVER['AUTH_TYPE'],
2093                @$_SERVER['PHP_AUTH_USER'],
2094                @$_SERVER['PHP_AUTH_PW']);
2095        }
2096
2097        if (method_exists($this, 'check_auth')) {
2098
2099            // old (pre 1.0) method name
2100            return $this->check_auth(@$_SERVER['AUTH_TYPE'],
2101                @$_SERVER['PHP_AUTH_USER'],
2102                @$_SERVER['PHP_AUTH_PW']);
2103        }
2104
2105        // no method found -> no authentication required
2106        return true;
2107    }
2108
2109    // }}}
2110
2111    // {{{ UUID stuff
2112
2113    /**
2114     * Generate Unique Universal IDentifier for lock token
2115     *
2116     * @param void
2117     * @return string a new UUID
2118     */
2119    function _new_uuid()
2120    {
2121        // use uuid extension from PECL if available
2122        if (function_exists('uuid_create')) {
2123            return uuid_create();
2124        }
2125
2126        // fallback
2127        $uuid = md5(microtime() . getmypid()); // this should be random enough for now
2128
2129        // set variant and version fields for 'true' random uuid
2130        $uuid{12} = '4';
2131        $n = 8 + (ord($uuid{16}) & 3);
2132        $hex = '0123456789abcdef';
2133        $uuid{16} = $hex{$n};
2134
2135        // return formated uuid
2136        return substr($uuid,  0, 8)
2137            . '-' . substr($uuid,  8, 4)
2138            . '-' . substr($uuid, 12, 4)
2139            . '-' . substr($uuid, 16, 4)
2140            . '-' . substr($uuid, 20);
2141    }
2142
2143    /**
2144     * Create a new opaque lock token as defined in RFC2518
2145     *
2146     * @param void
2147     * @return string new RFC2518 opaque lock token
2148     */
2149    function _new_locktoken()
2150    {
2151        return 'opaquelocktoken:' . $this->_new_uuid();
2152    }
2153
2154    // }}}
2155
2156    // {{{ WebDAV If: header parsing
2157
2158    /**
2159     *
2160     *
2161     * @param string header string to parse
2162     * @param int current parsing position
2163     * @return array next token (type and value)
2164     */
2165    function _if_header_lexer($string, &$pos)
2166    {
2167        // skip whitespace
2168        while (ctype_space($string{$pos})) {
2169            ++$pos;
2170        }
2171
2172        // already at end of string?
2173        if (strlen($string) <= $pos) {
2174            return;
2175        }
2176
2177        // get next character
2178        $c = $string{$pos++};
2179
2180        // now it depends on what we found
2181        switch ($c) {
2182
2183            // URLs are enclosed in <...>
2184            case '<':
2185                $pos2 = strpos($string, '>', $pos);
2186                $uri = substr($string, $pos, $pos2 - $pos);
2187                $pos = $pos2 + 1;
2188                return array('URI', $uri);
2189
2190            // ETags are enclosed in [...]
2191            case '[':
2192                $type = 'ETAG_STRONG';
2193                if ($string{$pos} == 'W') {
2194                    $type = 'ETAG_WEAK';
2195                    $pos += 2;
2196                }
2197
2198                $pos2 = strpos($string, ']', $pos);
2199                $etag = substr($string, $pos + 1, $pos2 - $pos - 2);
2200                $pos = $pos2 + 1;
2201                return array($type, $etag);
2202
2203            // 'N' indicates negation
2204            case 'N':
2205                $pos += 2;
2206                return array('NOT', 'Not');
2207
2208            // anything else is passed verbatim char by char
2209            default:
2210                return array('CHAR', $c);
2211        }
2212    }
2213
2214    /**
2215     * Parse If: header
2216     *
2217     * @param string header string
2218     * @return array URLs and their conditions
2219     */
2220    function _if_header_parser($str)
2221    {
2222        $pos = 0;
2223        $len = strlen($str);
2224
2225        $uris = array();
2226
2227        // parser loop
2228        while ($pos < $len) {
2229
2230            // get next token
2231            $token = $this->_if_header_lexer($str, $pos);
2232
2233            // check for URL
2234            $uri = '';
2235            if ($token[0] == 'URI') {
2236                $uri = $token[1]; // remember URL
2237                $token = $this->_if_header_lexer($str, $pos); // get next token
2238            }
2239
2240            // sanity check
2241            if ($token[0] != 'CHAR' || $token[1] != '(') {
2242                return;
2243            }
2244
2245            $list = array();
2246            $level = 1;
2247            while ($level) {
2248                $token = $this->_if_header_lexer($str, $pos);
2249
2250                $not = '';
2251                if ($token[0] == 'NOT') {
2252                    $not = '!';
2253                    $token = $this->_if_header_lexer($str, $pos);
2254                }
2255
2256                switch ($token[0]) {
2257                    case 'CHAR':
2258                        switch ($token[1]) {
2259                            case '(':
2260                                $level++;
2261                                break;
2262
2263                            case ')':
2264                                $level--;
2265                                break;
2266
2267                            default:
2268                                return;
2269                        }
2270                        break;
2271
2272                    case 'URI':
2273                        $list[] = $not . "<$token[1]>";
2274                        break;
2275
2276                    case 'ETAG_WEAK':
2277                        $list[] = $not . "[W/'$token[1]']";
2278                        break;
2279
2280                    case 'ETAG_STRONG':
2281                        $list[] = $not . "['$token[1]']";
2282                        break;
2283
2284                    default:
2285                        return;
2286                }
2287            }
2288
2289            // RFC2518 9.4.1: The No-tag-list production describes a series of
2290            // state tokens and ETags.  If multiple No-tag-list productions are
2291            // used then one only needs to match the state of the resource for
2292            // the method to be allowed to continue.
2293            //
2294            // FIXME: Since only one list of conditions must be satisfied, it
2295            // is a mistake to merge all lists of conditions.  Instead, a URL
2296            // should reference an array of arrays of conditions, the inner
2297            // array representing a conjunction and the outer array
2298            // representing a disjunction, or $uris shouldn't be associative,
2299            // but be an array of productions array('href' => $href,
2300            // 'conditions' => array($condition, ...))
2301            if (!empty($uris[$uri])) {
2302                $uris[$uri] = array_merge($uris[$uri], $list);
2303                continue;
2304            }
2305            $uris[$uri] = $list;
2306        }
2307
2308        return $uris;
2309    }
2310
2311    /**
2312     * Check if conditions from If: headers are met
2313     *
2314     * The If: header is an extension to HTTP/1.1 defined in RFC2518 9.4
2315     *
2316     * @param void
2317     * @return boolean
2318     */
2319    function _check_if_header_conditions()
2320    {
2321        if (empty($_SERVER['HTTP_IF'])) {
2322            return true;
2323        }
2324
2325        $this->_if_header_uris =
2326            $this->_if_header_parser($_SERVER['HTTP_IF']);
2327
2328        // any match is ok
2329        foreach ($this->_if_header_uris as $uri => $conditions) {
2330
2331            // RFC2518 9.4.1: If a method, due to the presence of a Depth or
2332            // Destination header, is applied to multiple resources then the
2333            // No-tag-list production MUST be applied to each resource the
2334            // method is applied to.
2335            if (empty($uri)) {
2336                $uri = $this->getHref($this->path);
2337            }
2338
2339            // all must match
2340            foreach ($conditions as $condition) {
2341
2342                // lock tokens may be free form (RFC2518 6.3)
2343                // but if opaquelocktokens are used (RFC2518 6.4)
2344                // we have to check the format (litmus tests this)
2345                if (!strncmp($condition, '<opaquelocktoken:', strlen('<opaquelocktoken'))) {
2346                    if (!ereg('^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$', $condition)) {
2347                        return;
2348                    }
2349                }
2350
2351                if (!$this->_check_uri_condition($uri, $condition)) {
2352                    continue 2;
2353                }
2354            }
2355
2356            return true;
2357        }
2358    }
2359
2360    /**
2361     * Check a single URL condition parsed from an if-header
2362     *
2363     * @abstract
2364     * @param string URL to check
2365     * @param string condition to check for this URL
2366     * @return boolean condition check result
2367     */
2368    function _check_uri_condition($uri, $condition)
2369    {
2370        // not really implemented here,
2371        // implementations must override
2372        return true;
2373    }
2374
2375    /**
2376     * For each lock on the requested resource, check that the lock token is in
2377     * the If header.  If requesting a shared lock, check only exclusive locks
2378     * on the requested resource.
2379     *
2380     * @param array of locks
2381     * @param string path of resource to check
2382     * @param boolean exclusive lock?
2383     * @return boolean true if the request is allowed
2384     */
2385    function check_locks_helper($locks, $path, $exclusive_only=false)
2386    {
2387        $conditions = array();
2388        if (!empty($_SERVER['HTTP_IF'])) {
2389            $conditions = $this->_if_header_parser($_SERVER['HTTP_IF']);
2390        }
2391
2392        foreach ($locks as $lock) {
2393            if ($exclusive_only && ($lock['scope'] == 'shared')) {
2394                continue;
2395            }
2396
2397            // for both Tagged-list and No-tag-list productions
2398            foreach (array($this->getHref($path), '') as $href) {
2399                if (!empty($conditions[$href])
2400                        && in_array("<$lock[token]>", $conditions[$href])) {
2401                    continue 2;
2402                }
2403            }
2404
2405            return false;
2406        }
2407
2408        return true;
2409    }
2410
2411    /**
2412     * @param string path of resource to check
2413     * @param boolean exclusive lock?
2414     */
2415    function check_locks_wrapper($path, $exclusive_only=false)
2416    {
2417        if (!method_exists($this, 'getLocks')) {
2418            return true;
2419        }
2420
2421        return $this->check_locks_helper(
2422            $this->getLocks($path), $path, $exclusive_only);
2423    }
2424
2425    // }}}
2426
2427    /**
2428     * Open request body input stream
2429     *
2430     * Because it's not possible to write to php://input (unlike the potential
2431     * to set request variables) and not possible until PHP 5.1 to register
2432     * alternative stream wrappers with php:// URLs, this function enables
2433     * sub-classes to override the request body.  Gallery uses this for unit
2434     * testing.  This function also collects all instances of opening the
2435     * request body in one place.
2436     *
2437     * @return resource a file descriptor
2438     */
2439    function openRequestBody()
2440    {
2441        return fopen('php://input', 'rb');
2442    }
2443
2444    /**
2445     * Set HTTP response header
2446     *
2447     * This function enables sub-classes to override header-setting.  Gallery
2448     * uses this to avoid replacing headers elsewhere in the application, and
2449     * for testability.
2450     *
2451     * @param string status code and message
2452     * @return void
2453     */
2454    function setResponseHeader($header, $replace=true)
2455    {
2456        $key = 'status';
2457        if (strncasecmp($header, 'HTTP/', 5) !== 0) {
2458            $key = strtolower(substr($header, 0, strpos($header, ':')));
2459        }
2460
2461        if ($replace || empty($this->headers[$key])) {
2462            header($header);
2463            $this->headers[$key] = $header;
2464        }
2465    }
2466
2467    /**
2468     * Set HTTP response status and mirror it in a private header
2469     *
2470     * @param string status code and message
2471     * @return void
2472     */
2473    function setResponseStatus($status, $replace=true)
2474    {
2475        // simplified success case
2476        if ($status === true) {
2477            $status = '200 OK';
2478        }
2479
2480        // didn't set a more specific status code
2481        if (empty($status)) {
2482            $status = '500 Internal Server Error';
2483        }
2484
2485        // generate HTTP status response
2486        $this->setResponseHeader("HTTP/1.1 $status", $replace);
2487        $this->setResponseHeader("X-WebDAV-Status: $status", $replace);
2488    }
2489
2490    /**
2491     * Private minimalistic version of PHP urlencode
2492     *
2493     * Only blanks and XML special chars must be encoded here.  Full urlencode
2494     * encoding confuses some clients.
2495     *
2496     * @param string URL to encode
2497     * @return string encoded URL
2498     */
2499    function _urlencode($url)
2500    {
2501        return strtr($url, array(' ' => '%20',
2502                '&' => '%26',
2503                '<' => '%3C',
2504                '>' => '%3E'));
2505    }
2506
2507    /**
2508     * Private version of PHP urldecode
2509     *
2510     * Not really needed but added for completenes.
2511     *
2512     * @param string URL to decode
2513     * @return string decoded URL
2514     */
2515    function _urldecode($path)
2516    {
2517        return urldecode($path);
2518    }
2519
2520    /**
2521     * UTF-8 encode property values if not already done so
2522     *
2523     * @param string text to encode
2524     * @return string UTF-8 encoded text
2525     */
2526    function _prop_encode($text)
2527    {
2528        switch (strtolower($this->_prop_encoding)) {
2529        case 'utf-8':
2530            return $text;
2531        case 'iso-8859-1':
2532        case 'iso-8859-15':
2533        case 'latin-1':
2534        default:
2535            return utf8_encode($text);
2536        }
2537    }
2538
2539    /**
2540     * Make sure path ends in a slash
2541     *
2542     * @param string directory path
2543     * @return string directory path with trailing slash
2544     */
2545    function _slashify($path)
2546    {
2547        if (substr($path, -1) != '/') {
2548            $path .= '/';
2549        }
2550
2551        return $path;
2552    }
2553
2554    /**
2555     * Verify that the Depth request header is set and has a non-empty value.
2556     * Doesn't verify for allowed values though.
2557     * @return boolean true if set and not empty
2558     */
2559    function _hasNonEmptyDepthRequestHeader() {
2560        return isset($_SERVER['HTTP_DEPTH']) && strlen(trim((string)$_SERVER['HTTP_DEPTH']));
2561    }
2562}
2563
2564// Local variables:
2565// tab-width: 4
2566// c-basic-offset: 4
2567// End:
2568?>
2569