Overview

Packages

  • None
  • Rpc

Classes

  • Horde_Rpc
  • Horde_Rpc_ActiveSync
  • Horde_Rpc_Exception
  • Horde_Rpc_Phpgw
  • Horde_Rpc_Soap
  • Horde_Rpc_Syncml
  • Horde_Rpc_Syncml_Wbxml
  • Horde_Rpc_Translation
  • Horde_Rpc_Webdav
  • Horde_Rpc_Webdav2
  • Horde_Rpc_Xmlrpc
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * The Horde_Rpc_Webdav class provides a WebDAV implementation of the
   4:  * Horde RPC system.
   5:  *
   6:  * Copyright 2008-2012 Horde LLC (http://www.horde.org/)
   7:  *
   8:  * Derived from the HTTP_WebDAV_Server PEAR package:
   9:  * +------------------------------------------------------------------------+
  10:  * | Portions Copyright 2002-2007 Christian Stocker, Hartmut Holzgraefe |
  11:  * | All rights reserved                                                    |
  12:  * |                                                                        |
  13:  * | Redistribution and use in source and binary forms, with or without     |
  14:  * | modification, are permitted provided that the following conditions     |
  15:  * | are met:                                                               |
  16:  * |                                                                        |
  17:  * | 1. Redistributions of source code must retain the above copyright      |
  18:  * |    notice, this list of conditions and the following disclaimer.       |
  19:  * | 2. Redistributions in binary form must reproduce the above copyright   |
  20:  * |    notice, this list of conditions and the following disclaimer in     |
  21:  * |    the documentation and/or other materials provided with the          |
  22:  * |    distribution.                                                       |
  23:  * | 3. The names of the authors may not be used to endorse or promote      |
  24:  * |    products derived from this software without specific prior          |
  25:  * |    written permission.                                                 |
  26:  * |                                                                        |
  27:  * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS    |
  28:  * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT      |
  29:  * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS      |
  30:  * | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE         |
  31:  * | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,    |
  32:  * | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,   |
  33:  * | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;       |
  34:  * | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER       |
  35:  * | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT     |
  36:  * | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN      |
  37:  * | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE        |
  38:  * | POSSIBILITY OF SUCH DAMAGE.                                            |
  39:  * +------------------------------------------------------------------------+
  40:  *
  41:  * Portions Copyright 2004-2012 Horde LLC (http://www.horde.org/)
  42:  *
  43:  * See the enclosed file COPYING for license information (LGPL). If you
  44:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  45:  *
  46:  * @author  Chuck Hagenbuch <chuck@horde.org>
  47:  * @author  Ben Klang <ben@alkaloid.net>
  48:  * @author  Hartmut Holzgraefe
  49:  * @author  Christian Stocker
  50:  * @package Rpc
  51:  */
  52: class Horde_Rpc_Webdav extends Horde_Rpc
  53: {
  54:     /**
  55:      * CalDAV XML namespace
  56:      *
  57:      * @var string
  58:      */
  59:     const CALDAVNS = 'urn:ietf:params:xml:ns:caldav';
  60: 
  61:     /**
  62:      * Realm string to be used in authentification popups
  63:      *
  64:      * @var string
  65:      */
  66:     var $http_auth_realm = 'Horde WebDAV';
  67: 
  68:     /**
  69:      * String to be used in "X-Dav-Powered-By" header
  70:      *
  71:      * @var string
  72:      */
  73:     var $dav_powered_by = 'Horde WebDAV Server';
  74: 
  75:     /**
  76:      * success state flag
  77:      *
  78:      * @var bool
  79:      * @access public
  80:      */
  81:     var $parseSuccess = false;
  82: 
  83:     /**
  84:      * found properties are collected here
  85:      *
  86:      * @var array
  87:      * @access public
  88:      */
  89:     var $parseProps = false;
  90: 
  91:     /**
  92:      * internal tag nesting depth counter
  93:      *
  94:      * @var int
  95:      * @access private
  96:      */
  97:     var $parseDepth = 0;
  98: 
  99:     /**
 100:      * lock type, currently only "write"
 101:      *
 102:      * @var string
 103:      * @access public
 104:      */
 105:     var $locktype = "";
 106: 
 107:     /**
 108:      * lock scope, "shared" or "exclusive"
 109:      *
 110:      * @var string
 111:      * @access public
 112:      */
 113:     var $lockscope = "";
 114: 
 115:     /**
 116:      * lock owner information
 117:      *
 118:      * @var string
 119:      * @access public
 120:      */
 121:     var $owner = "";
 122: 
 123:     /**
 124:      * flag that is set during lock owner read
 125:      *
 126:      * @var bool
 127:      * @access private
 128:      */
 129:     var $collect_owner = false;
 130: 
 131:     /**
 132:      *
 133:      *
 134:      * @var
 135:      * @access
 136:      */
 137:     var $mode;
 138: 
 139:     /**
 140:      *
 141:      *
 142:      * @var
 143:      * @access
 144:      */
 145:     var $current;
 146: 
 147:     /**
 148:      * complete URI for this request
 149:      *
 150:      * @var string
 151:      */
 152:     var $uri;
 153: 
 154: 
 155:     /**
 156:      * base URI for this request
 157:      *
 158:      * @var string
 159:      */
 160:     var $base_uri;
 161: 
 162: 
 163:     /**
 164:      * URI path for this request
 165:      *
 166:      * @var string
 167:      */
 168:     var $path;
 169: 
 170:     /**
 171:      * Remember parsed If: (RFC2518/9.4) header conditions
 172:      *
 173:      * @var array
 174:      */
 175:     var $_if_header_uris = array();
 176: 
 177:     /**
 178:      * HTTP response status/message
 179:      *
 180:      * @var string
 181:      */
 182:     var $_http_status = "200 OK";
 183: 
 184:     /**
 185:      * Copy of $_SERVER superglobal array
 186:      *
 187:      * Derived classes may extend the constructor to
 188:      * modify its contents
 189:      *
 190:      * @var array
 191:      */
 192:     var $_SERVER;
 193: 
 194:     /**
 195:      * Mapping of XML namespaces to their XML nickname
 196:      *
 197:      * @var array
 198:      */
 199:     var $ns_hash = array('DAV:' => 'D');
 200: 
 201:     /**
 202:      * Xml_Element object
 203:      * @var object
 204:      */
 205:     var $_xml;
 206: 
 207:     /**
 208:      * WebDav server constructor.
 209:      *
 210:      * @access private
 211:      */
 212:     public function __construct($request, $params = array())
 213:     {
 214:         // PHP messages destroy XML output -> switch them off
 215:         ini_set('display_errors', 0);
 216: 
 217:         // copy $_SERVER variables to local _SERVER array
 218:         // so that derived classes can simply modify these
 219:         $this->_SERVER = $_SERVER;
 220: 
 221:         parent::__construct($request, $params);
 222:     }
 223: 
 224:     /**
 225:      * WebDAV handles authentication internally, so bypass the
 226:      * system-level auth check by just returning true here.
 227:      */
 228:     function authorize()
 229:     {
 230:         return true;
 231:     }
 232: 
 233:     /**
 234:      * If the webdav backend is used, the input should not be read, it is
 235:      * being read by HTTP_WebDAV_Server.
 236:      */
 237:     function getInput()
 238:     {
 239:     }
 240: 
 241:     /**
 242:      * Sends an RPC request to the server and returns the result.
 243:      *
 244:      * @param string  The raw request string.
 245:      *
 246:      * @return string  The XML encoded response from the server.
 247:      */
 248:     function getResponse($request)
 249:     {
 250:         $this->ServeRequest();
 251:         exit;
 252:     }
 253: 
 254:     /**
 255:      * GET implementation.
 256:      *
 257:      * @param array $options  Array of input and output parameters.
 258:      * <br><strong>input</strong><ul>
 259:      * <li> path -
 260:      * </ul>
 261:      * <br><strong>output</strong><ul>
 262:      * <li> size -
 263:      * </ul>
 264:      *
 265:      * @return string|boolean  HTTP-Statuscode.
 266:      */
 267:     function GET(&$options)
 268:     {
 269:         if ($options['path'] == '/') {
 270:             $options['mimetype'] = 'httpd/unix-directory';
 271:         } else {
 272:             // Ensure we only retrieve the exact item
 273:             $options['depth'] = 0;
 274:             try {
 275:                 $result = $this->_list($options, false);
 276:                 if ($result === false) {
 277:                     return '404 File Not Found';
 278:                 }
 279:             } catch (Horde_Rpc_Exception $e) {
 280:                 if ($e->getCode()) {
 281:                     // Allow called applications to set the result code
 282:                     return $this->_checkHTTPCode($e->getCode())
 283:                         . ' ' . $e->getMessage();
 284:                 }
 285:                 return '500 Internal Server Error';
 286:             }
 287:             $options = $result;
 288:         }
 289: 
 290:         return true;
 291:     }
 292: 
 293:     /**
 294:      * PUT implementation.
 295:      *
 296:      * @param array &$options  Parameter passing array.
 297:      *
 298:      * @return string|boolean  HTTP-Statuscode.
 299:      */
 300:     function PUT(&$options)
 301:     {
 302:         $path = trim($options['path'], '/');
 303: 
 304:         if (empty($path)) {
 305:             return '403 PUT requires a path.';
 306:         }
 307: 
 308:         $pieces = explode('/', $path);
 309: 
 310:         if (count($pieces) < 2 || empty($pieces[0])) {
 311:             return '403 PUT denied outside of application directories.';
 312:         }
 313: 
 314:         $content = '';
 315:         while (!feof($options['stream'])) {
 316:             $content .= fgets($options['stream']);
 317:         }
 318: 
 319:         try {
 320:             $GLOBALS['registry']->callByPackage($pieces[0], 'put', array('path' => $path, 'content' => $content, 'type' => $options['content_type']));
 321:         } catch (Horde_Exception $e) {
 322:             Horde::logMessage($e, 'ERR');
 323:             if ($e->getCode()) {
 324:                 return $this->_checkHTTPCode($e->getCode()) . ' ' . $result->getMessage();
 325:             }
 326: 
 327:             return '500 Internal Server Error. Check server logs';
 328:         }
 329: 
 330:         return true;
 331:     }
 332: 
 333:     /**
 334:      * Performs a WebDAV DELETE.
 335:      *
 336:      * Deletes a single object from a database. The path passed in must
 337:      * be in [app]/[path] format.
 338:      *
 339:      * @see HTTP_WebDAV_Server::http_DELETE()
 340:      *
 341:      * @param array $options An array of parameters from the setup
 342:      * method in HTTP_WebDAV_Server.
 343:      *
 344:      * @return string|boolean  HTTP-Statuscode.
 345:      */
 346:     function DELETE($options)
 347:     {
 348:         $path = $options['path'];
 349:         $pieces = explode('/', trim($this->path, '/'), 2);
 350: 
 351:         if (count($pieces) != 2) {
 352:             Horde::logMessage(sprintf(Horde_Rpc_Translation::t("Error deleting from path %s; must be [app]/[path]", $options['path'])), 'INFO');
 353:             return '403 Must supply a resource within the application to delete.';
 354:         }
 355: 
 356:         $app = $pieces[0];
 357:         $path = $pieces[1];
 358: 
 359:         // TODO: Support HTTP/1.1 If-Match on ETag here
 360: 
 361:         // Delete access is checked in each app.
 362:         try {
 363:             $GLOBALS['registry']->callByPackage($app, 'path_delete', array($path));
 364:         } catch (Horde_Exception $e) {
 365:             Horde::logMessage($e, 'INFO');
 366:             if ($e->getCode()) {
 367:                 return $this->_checkHTTPCode($e->getCode()) . ' ' . $e->getMessage();
 368:             }
 369: 
 370:             return '500 Internal Server Error. Check server logs';
 371:         }
 372: 
 373:         return '204 No Content';
 374:     }
 375: 
 376:     /**
 377:      * PROPFIND method handler
 378:      *
 379:      * @param array $options  General parameter passing array.
 380:      * @param array &$files   Return array for file properties.
 381:      *
 382:      * @return boolean  True on success.
 383:      */
 384:     function PROPFIND($options, &$files)
 385:     {
 386:         // Always return '404 File Not Found';
 387:         // Work around HTTP_WebDAV_Server behavior.
 388:         // See: http://pear.php.net/bugs/bug.php?id=11390
 389:         try {
 390:             $list = $this->_list($options, true);
 391:             if ($list === false) {
 392:                 return false;
 393:             }
 394:         } catch (Horde_Rpc_Exception $e) {
 395:             return false;
 396:         }
 397:         $files['files'] = $list;
 398:         return true;
 399:     }
 400: 
 401:     /**
 402:      * MKCOL method handler
 403:      *
 404:      * @param array $options
 405:      * @return string HTTP response string
 406:      */
 407:     function MKCOL($options)
 408:     {
 409:         $path = $options['path'];
 410:         if (substr($path, 0, 1) == '/') {
 411:             $path = substr($path, 1);
 412:         }
 413: 
 414:         // Take the module name from the path
 415:         $pieces = explode('/', $path, 2);
 416:         if (count($pieces) != 2) {
 417:             Horde::logMessage(sprintf(Horde_Rpc_Translation::t("Unable to create directory %s; must be [app]/[path]"), $path), 'INFO');
 418:             return '403 Must specify a resource within an application.  MKCOL disallowed at top level.';
 419:         }
 420: 
 421:         // Send the request to the application
 422:         try {
 423:             $GLOBALS['registry']->callByPackage($pieces[0], 'mkcol', array('path' => $path));
 424:         } catch (Horde_Exception $e) {
 425:             Horde::logMessage($result, 'ERR');
 426:             if ($e->getCode()) {
 427:                 return $this->_checkHTTPCode($e->getCode()) . ' ' . $e->getMessage();
 428:             }
 429: 
 430:             return '500 Internal Server Error. Check server logs';
 431:         }
 432: 
 433:         return '200 OK';
 434:     }
 435: 
 436:     /**
 437:      * MOVE method handler
 438:      *
 439:      * @param array $options
 440:      * @return string HTTP response string
 441:      */
 442:     function MOVE($options)
 443:     {
 444:         $path = $options['path'];
 445:         if (substr($path, 0, 1) == '/') {
 446:             $path = substr($path, 1);
 447:         }
 448: 
 449:         // Take the module name from the path
 450:         $sourcePieces = explode('/', $path, 2);
 451:         if (count($sourcePieces) != 2) {
 452:             Horde::logMessage(sprintf(Horde_Rpc_Translation::t("Unable to rename %s; must be [app]/[path] and within the same application."), $path), 'INFO');
 453:             return '403 Must specify a resource within an application.  MOVE disallowed at top level.';
 454:         }
 455: 
 456:         $destPieces = explode('/', $options['dest'], 2);
 457:         if (!(count($destPieces) == 2) || $sourcesPieces[0] != $destPieces[0]) {
 458:             return '400 Can not move across applications.';
 459:         }
 460: 
 461:         // Send the request to the module
 462:         try {
 463:             $GLOBALS['registry']->callByPackage($sourcePieces[0], 'move', array('path' => $path, 'dest' => $options['dest']));
 464:         } catch (Horde_Exception $e) {
 465:             Horde::logMessage($e, 'ERR');
 466:             if ($e->getCode()) {
 467:                 return $this->_checkHTTPCode($e->getCode()) . ' ' . $e->getMessage();
 468:             }
 469: 
 470:             return '500 Internal Server Error. Check server logs';
 471:         }
 472: 
 473:         return '200 OK';
 474:     }
 475: 
 476:     /**
 477:      * Generates a response to a GET or PROPFIND request.
 478:      *
 479:      * @param array $options        Array of WebDAV options
 480:      * @param boolean $propperties  Whether to only return properties or actual
 481:      *                              data.
 482:      *
 483:      * @return mixed  Array of objects with properties if the request is a dir,
 484:      *                array of file metadata + data if request is a file,
 485:      *                false if the object is not found.
 486:      * @throws Horde_Exception
 487:      */
 488:     function _list($options, $properties)
 489:     {
 490:         global $registry;
 491: 
 492:         // $path (or $options['path']) is the node on which we will list
 493:         // collections and resources.  $this->path is the path of the original
 494:         // request from the client.
 495:         $path = $options['path'];
 496:         $depth = $options['depth'];
 497: 
 498:         // $list will contain the data to return to the client
 499:         $list = array();
 500: 
 501:         if ($path == '/') {
 502:             // $root is a virtual collection describing the root of the Horde
 503:             // WebDAV space
 504:             $now = time();
 505:             $root = array('name' => '/',
 506:                           'created' => $now,
 507:                           'modified' => $now,
 508:                           'contenttype' => 'httpd/unix-directory',
 509:                           'contentlength' => 0,
 510:                           'browseable' => true);
 511:             $list[] = array('path' => $path,
 512:                             'props' => $this->_getProps($options['props'], $root));
 513: 
 514:             try {
 515:                 $apps = $registry->listApps(null, false, Horde_Perms::READ);
 516:             } catch (Horde_Exception $e) {
 517:                 Horde::logMessage($e);
 518:                 throw new Horde_Rpc_Exception($e);
 519:             }
 520:             foreach ($apps as $app) {
 521:                 // Only include apps that have browse() methods.
 522:                 if ($registry->hasMethod('browse', $app)) {
 523:                     $list[] = array('path' => '/' . $app,
 524:                                     'props' => $this->_getProps($options['props'], array_merge($root, array('name' => $registry->get('name', $app)))));
 525:                 }
 526:             }
 527:             return $list;
 528:         } else {
 529:             $path = trim($path, '/');
 530:             $pieces = explode('/', $path);
 531: 
 532:             try {
 533:                 $items = $registry->callByPackage($pieces[0], 'browse', array('path' => $path, 'properties' => array('name', 'browseable', 'contenttype', 'contentlength', 'created', 'modified')));
 534:             } catch (Horde_Exception $e) {
 535:                 Horde::logMessage($e);
 536:                 throw new Horde_Rpc_Exception($e);
 537:             }
 538: 
 539:             if ($items === false) {
 540:                 // File not found
 541:                 return $items;
 542:             }
 543:             if (empty($items)) {
 544:                 // No content exists at this level.
 545:                 return array();
 546:             }
 547:             if (!is_array(reset($items))) {
 548:                 /* A one-dimensional array means we have an actual object with
 549:                  * data to return to the client. */
 550:                 if ($properties) {
 551:                     $props = $this->_getProps($options['props'], $items);
 552:                     $items = array(array('path' => $this->path,
 553:                                          'props' => $props));
 554:                 }
 555:                 return $items;
 556:             }
 557: 
 558:             /* A directory full of objects has been returned. */
 559:             foreach ($items as $sub_path => $i) {
 560:                 $props = $this->_getProps($options['props'], $i);
 561: 
 562:                 $item = array('path' => '/' . $sub_path,
 563:                               'props' => $props);
 564:                 $list[] = $item;
 565:             }
 566:         }
 567: 
 568:         return $list;
 569:     }
 570: 
 571:     /**
 572:      * Given a set of requested properties ($reqprops) and an items holding
 573:      * properties, return a list of properties and values from the item that
 574:      * were requested.
 575:      *
 576:      * @param array  $reqprops List of requested properties
 577:      * @param array  $item     Item with properties to be filtered
 578:      *
 579:      * @return array           List of filtered properties and values
 580:      */
 581:     function _getProps($reqprops, $item)
 582:     {
 583:         $props = array();
 584:         $properties = array();
 585:         foreach ($reqprops as $prop) {
 586:             if (!isset($properties[$prop['xmlns']])) {
 587:                 $properties[$prop['xmlns']] = array();
 588:             }
 589:             $properties[$prop['xmlns']][$prop['name']] = $prop['name'];
 590:         }
 591: 
 592:         // Handle certain standard properties specially
 593:         if (in_array('displayname', $properties['DAV:'])) {
 594:             $props[] = $this->mkprop('displayname', $item['name']);
 595:             unset($properties['DAV:']['displayname']);
 596:         }
 597:         if (in_array('resourcetype', $properties['DAV:'])) {
 598:             $props[] = $this->mkprop('resourcetype', $item['browseable'] ? 'collection' : '');
 599:             unset($properties['DAV:']['resourcetype']);
 600:         }
 601:         if (in_array('getcontenttype', $properties['DAV:'])) {
 602:             $props[] = $this->mkprop('getcontenttype', empty($item['contenttype']) ? 'application/octet-stream' : $item['contenttype']);
 603:             unset($properties['DAV:']['getcontenttype']);
 604:         }
 605:         if (in_array('getcontentlength', $properties['DAV:'])) {
 606:             if (empty($item['contentlength']) && empty($item['data'])) {
 607:                 $size = 0;
 608:             } else {
 609:                 $size = empty($item['contentlength']) ? strlen($item['data']) : $item['contentlength'];
 610:             }
 611:             $props[] = $this->mkprop('getcontentlength', $size);
 612:             unset($properties['DAV:']['getcontentlength']);
 613:         }
 614:         if (in_array('creationdate', $properties['DAV:'])) {
 615:             $props[] = $this->mkprop('creationdate', empty($item['created']) ? time() : $item['created']);
 616:             unset($properties['DAV:']['creationdate']);
 617:         }
 618:         if (in_array('getlastmodified', $properties['DAV:'])) {
 619:             $props[] = $this->mkprop('getlastmodified', empty($item['modified']) ? time() : $item['modified']);
 620:             unset($properties['DAV:']['getlastmodified']);
 621:         }
 622: 
 623:         if (isset($properties[self::CALDAVNS])) {
 624:             if (in_array('calendar-home-set', $properties[self::CALDAVNS]) &&
 625:                 isset($item[self::CALDAVNS . ':calendar-home-set'])) {
 626:                 $calendar_home_set = array();
 627:                 foreach ($item[self::CALDAVNS . ':calendar-home-set'] as $calUrl) {
 628:                     $calendar_home_set[] = $this->mkprop('href', $calUrl);
 629:                 }
 630:                 $props[] = $this->mkprop('caldav', 'calendar-home-set', $calendar_home_set);
 631:                 unset($properties[self::CALDAVNS]['calendar-home-set']);
 632:             }
 633: 
 634:             if (in_array('calendar-user-address-set', $properties[self::CALDAVNS]) &&
 635:                 isset($item[self::CALDAVNS . ':calendar-user-address-set'])) {
 636:                 $calendar_user_address_set = array();
 637:                 foreach ($item[self::CALDAVNS . ':calendar-user-address-set'] as $userAddress) {
 638:                     $calendar_user_address_set[] = $this->mkprop('href', $userAddress);
 639:                 }
 640:                 $props[] = $this->mkprop('caldav', 'calendar-user-address-set', $calendar_user_address_set);
 641:                 unset($properties[self::CALDAVNS]['calendar-user-address-set']);
 642:             }
 643:         }
 644: 
 645:         // Handle any other requested properties genericly
 646:         $itemprops = array_keys($item);
 647:         foreach (array_keys($properties) as $xmlns) {
 648:             foreach ($properties[$xmlns] as $propname) {
 649:                 if ($xmlns != 'DAV:') {
 650:                     $propname = $xmlns . ':' . $propname;
 651:                 }
 652:                 if (in_array($propname, $itemprops)) {
 653:                     $props[] = $this->mkprop($xmlns, $propname, $item[$propname]);
 654:                 }
 655:             }
 656:         }
 657: 
 658:         return $props;
 659:     }
 660: 
 661:     /**
 662:      * Attempts to set a lock on a specified resource.
 663:      *
 664:      * @param array &$params  Reference to array of parameters.  These
 665:      *                        parameters should be overwritten with the lock
 666:      *                        information.
 667:      *
 668:      * @return int            HTTP status code
 669:      */
 670:     function LOCK(&$params)
 671:     {
 672:         if (!isset($GLOBALS['conf']['lock']['driver']) ||
 673:             $GLOBALS['conf']['lock']['driver'] == 'none') {
 674:             return 500;
 675:         }
 676: 
 677:         if (empty($params['path'])) {
 678:             Horde::logMessage('Empty path supplied to LOCK()', 'ERR');
 679:             return 403;
 680:         }
 681:         if ($params['path'] == '/') {
 682:             // Locks are always denied to the root directory
 683:             return 403;
 684:         }
 685:         if (isset($params['depth']) && $params['depth'] == 'infinity') {
 686:             // For now we categorically disallow recursive locks
 687:             return 403;
 688:         }
 689: 
 690:         if (!is_array($params['timeout']) || count($params['timeout']) != 1) {
 691:             // Unexpected timeout parameter.  Assume 600 seconds.
 692:             $timeout = 600;
 693:         }
 694:         $tmp = explode('-', $params['timeout'][0]);
 695:         if (count($tmp) != 2) {
 696:             // Unexpected timeout parameter.  Assume 600 seconds.
 697:             $timeout = 600;
 698:         }
 699:         if (strtolower($tmp[0]) == 'second') {
 700:             $timeout = $tmp[1];
 701:         } else {
 702:             // Unexpected timeout parameter.  Assume 600 seconds.
 703:             $timeout = 600;
 704:         }
 705: 
 706:         try {
 707:             $locks = $GLOBALS['injector']->getInstance('Horde_Lock');
 708:         } catch (Horde_Lock_Exception $e) {
 709:             Horde::logMessage($e, 'ERR');
 710:             return 500;
 711:         }
 712: 
 713:         $locktype = Horde_Lock::TYPE_SHARED;
 714:         if ($params['scope'] == 'exclusive') {
 715:             $locktype = Horde_Lock::TYPE_EXCLUSIVE;
 716:         }
 717: 
 718:         try {
 719:             $lockid = $locks->setLock($GLOBALS['registry']->getAuth(), 'webdav', $params['path'],
 720:                                       $timeout, $locktype);
 721:         } catch (Horde_Lock_Exception $e) {
 722:             Horde::logMessage($e, 'ERR');
 723:             return 500;
 724:         }
 725:         if ($lockid === false) {
 726:             // Resource is already locked.
 727:             return 423;
 728:         }
 729: 
 730:         $params['locktoken'] = $lockid;
 731:         $params['owner'] = $GLOBALS['registry']->getAuth();
 732:         $params['timeout'] = $timeout;
 733: 
 734:         return "200";
 735:     }
 736: 
 737:     /**
 738:      * Attempts to remove a specified lock.
 739:      *
 740:      * @param array &$params  Reference to array of parameters.  These
 741:      *                        parameters should be overwritten with the lock
 742:      *                        information.
 743:      *
 744:      * @return int            HTTP status code
 745:      */
 746:     function UNLOCK(&$params)
 747:     {
 748:         if (!isset($GLOBALS['conf']['lock']['driver']) ||
 749:             $GLOBALS['conf']['lock']['driver'] == 'none') {
 750:             return 500;
 751:         }
 752: 
 753:         try {
 754:             $locks = $GLOBALS['injector']->getInstance('Horde_Lock');
 755:         } catch (Horde_Lock_Exception $e) {
 756:             Horde::logMessage($e, 'ERR');
 757:             return 500;
 758:         }
 759: 
 760:         try {
 761:             $res = $locks->clearLock($params['token']);
 762:         } catch (Horde_Lock_Exception $e) {
 763:             Horde::logMessage($e, 'ERR');
 764:             return 500;
 765:         }
 766:         if ($res === false) {
 767:             Horde::logMessage('clearLock() returned false', 'ERR');
 768:             // Something else has failed:  424 (Method Failure)
 769:             return 424;
 770:         }
 771: 
 772:         // Lock cleared.  Use 204 (No Content) instead of 200 because there is
 773:         // no lock information to return to the client.
 774:         return 204;
 775:     }
 776: 
 777:     function checkLock($resource)
 778:     {
 779:         if (!isset($GLOBALS['conf']['lock']['driver']) ||
 780:             $GLOBALS['conf']['lock']['driver'] == 'none') {
 781:             Horde::logMessage('WebDAV locking failed because no lock driver has been configured.', 'WARN');
 782:             return false;
 783:         }
 784: 
 785:         try {
 786:             $locks = $GLOBALS['injector']->getInstance('Horde_Lock');
 787:         } catch (Horde_Lock_Exception $e) {
 788:             Horde::logMessage($e, 'ERR');
 789:             return false;
 790:         }
 791: 
 792:         try {
 793:             $res =  $locks->getLocks('webdav', $resource);
 794:         } catch (Horde_Lock_Exception $e) {
 795:             Horde::logMessage($e, 'ERR');
 796:             return false;
 797:         }
 798: 
 799:         if (empty($res)) {
 800:             // No locks found.
 801:             return $res;
 802:         }
 803: 
 804:         // WebDAV only supports one lock.  Return the first lock.
 805:         $lock = reset($res);
 806: 
 807:         // Format the array keys for HTTP_WebDAV_Server
 808:         $ret = array();
 809:         if ($lock['lock_type'] == Horde_Lock::TYPE_EXCLUSIVE) {
 810:             $ret['scope'] = 'exclusive';
 811:         } else {
 812:             $ret['scope'] = 'shared';
 813:         }
 814:         $ret['type'] = 'write';
 815:         $ret['expires'] = $lock['lock_expiry_timestamp'];
 816:         $ret['token'] = $lock['lock_id'];
 817:         $ret['depth'] = 1;
 818: 
 819:         return $ret;
 820:     }
 821: 
 822:     /**
 823:      * Check authentication. We always return true here since we
 824:      * handle permissions based on the resource that's requested, but
 825:      * we do record the authenticated user for later use.
 826:      *
 827:      * @param string $type      Authentication type, e.g. "basic" or "digest"
 828:      * @param string $username  Transmitted username.
 829:      * @param string $password  Transmitted password.
 830:      *
 831:      * @return boolean  Authentication status. Always true.
 832:      */
 833:     function check_auth($type, $username, $password)
 834:     {
 835:         $auth = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create();
 836:         return $auth->authenticate($username, array('password' => $password));
 837:     }
 838: 
 839:     /**
 840:      * Make sure the error code returned in the PEAR_Error object is a valid
 841:      * HTTP response code.
 842:      *
 843:      * This is necessary because in pre-Horde 3.2 apps the response codes are
 844:      * not sanitized.  This backward compatibility check can be removed when
 845:      * we drop support for pre-3.2 apps.  Intentionally, not every valid HTTP
 846:      * code is listed here.  Only common ones are here to reduce the
 847:      * possibility of an invalid code being confused with a valid HTTP code.
 848:      *
 849:      * @todo Remove for Horde 4.0
 850:      *
 851:      * @param integer $code  Status code to check for validity.
 852:      *
 853:      * @return integer  Either the original code if valid or 500 for internal
 854:      *                  server error.
 855:      */
 856:     function _checkHTTPcode($code)
 857:     {
 858:         $valid = array(200, // OK
 859:                        201, // Created
 860:                        202, // Accepted
 861:                        204, // No Content
 862:                        301, // Moved Permanently
 863:                        302, // Found
 864:                        304, // Not Modified
 865:                        307, // Temporary Redirect
 866:                        400, // Bad Request
 867:                        401, // Unauthorized
 868:                        403, // Forbidden
 869:                        404, // Not Found
 870:                        405, // Method Not Allowed
 871:                        406, // Not Acceptable
 872:                        408, // Request Timeout
 873:                        413, // Request Entity Too Large
 874:                        415, // Unsupported Media Type
 875:                        500, // Internal Server Error
 876:                        501, // Not Implemented
 877:                        503, // Service Unavailable
 878:         );
 879:         if (in_array($code, $valid)) {
 880:             return $code;
 881:         } else {
 882:             return 500;
 883:         }
 884:     }
 885: 
 886:     /**
 887:      * Serve WebDAV HTTP request
 888:      *
 889:      * dispatch WebDAV HTTP request to the apropriate method handler
 890:      *
 891:      * @param  void
 892:      * @return void
 893:      */
 894:     function ServeRequest()
 895:     {
 896:         // prevent warning in litmus check 'delete_fragment'
 897:         if (strstr($this->_SERVER["REQUEST_URI"], '#')) {
 898:             $this->http_status("400 Bad Request");
 899:             return;
 900:         }
 901: 
 902:         // default uri is the complete request uri
 903:         $script_name = preg_replace('/index.php$/', '', $this->_SERVER["SCRIPT_NAME"]);
 904:         $uri = "http";
 905:         if (isset($this->_SERVER["HTTPS"]) && $this->_SERVER["HTTPS"] === "on") {
 906:           $uri = "https";
 907:         }
 908:         $uri.= "://".$this->_SERVER["HTTP_HOST"].$script_name;
 909: 
 910:         // WebDAV has no concept of a query string and clients (including cadaver)
 911:         // seem to pass '?' unencoded, so we need to extract the path info out
 912:         // of the request URI ourselves
 913:         $path_info = substr($this->_SERVER["REQUEST_URI"], strlen($script_name));
 914: 
 915:         // just in case the path came in empty ...
 916:         if (empty($path_info)) {
 917:             $path_info = "/";
 918:         }
 919: 
 920:         $this->uri = $this->base_uri = $uri;
 921:         if (substr($uri, -1) == '/') {
 922:             $this->uri = substr($this->uri, 0, -1);
 923:         }
 924:         $this->uri .= $path_info;
 925: 
 926:         // set path
 927:         $this->path = $this->_urldecode($path_info);
 928:         if (!strlen($this->path)) {
 929:             if ($this->_SERVER["REQUEST_METHOD"] == "GET") {
 930:                 // redirect clients that try to GET a collection
 931:                 // WebDAV clients should never try this while
 932:                 // regular HTTP clients might ...
 933:                 header("Location: ".$this->base_uri."/");
 934:                 return;
 935:             } else {
 936:                 // if a WebDAV client didn't give a path we just assume '/'
 937:                 $this->path = "/";
 938:             }
 939:         }
 940: 
 941:         if (ini_get("magic_quotes_gpc")) {
 942:             $this->path = stripslashes($this->path);
 943:         }
 944: 
 945: 
 946:         // identify ourselves
 947:         if (empty($this->dav_powered_by)) {
 948:             header("X-Dav-Powered-By: PHP class: ".get_class($this));
 949:         } else {
 950:             header("X-Dav-Powered-By: ".$this->dav_powered_by);
 951:         }
 952: 
 953:         // check authentication
 954:         // for the motivation for not checking OPTIONS requests on / see
 955:         // http://pear.php.net/bugs/bug.php?id=5363
 956:         if ( (   !(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/")))
 957:              && (!$this->_check_auth())) {
 958:             // RFC2518 says we must use Digest instead of Basic
 959:             // but Microsoft Clients do not support Digest
 960:             // and we don't support NTLM and Kerberos
 961:             // so we are stuck with Basic here
 962:             header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"');
 963: 
 964:             // Windows seems to require this being the last header sent
 965:             // (changed according to PECL bug #3138)
 966:             $this->http_status('401 Unauthorized');
 967: 
 968:             return;
 969:         }
 970: 
 971:         // check
 972:         if (! $this->_check_if_header_conditions()) {
 973:             return;
 974:         }
 975: 
 976:         // detect requested method names
 977:         $method  = strtolower($this->_SERVER["REQUEST_METHOD"]);
 978:         $wrapper = "http_".$method;
 979: 
 980:         // activate HEAD emulation by GET if no HEAD method found
 981:         if ($method == "head" && !method_exists($this, "head")) {
 982:             $method = "get";
 983:         }
 984: 
 985:         if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) {
 986:             $this->$wrapper();  // call method by name
 987:         } else { // method not found/implemented
 988:             if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") {
 989:                 $this->http_status("412 Precondition failed");
 990:             } else {
 991:                 $this->http_status("405 Method not allowed");
 992:                 header("Allow: ".join(", ", $this->_allow()));  // tell client what's allowed
 993:             }
 994:         }
 995:     }
 996: 
 997:     // }}}
 998: 
 999:     // {{{ abstract WebDAV methods
1000: 
1001:     // {{{ GET()
1002:     /**
1003:      * GET implementation
1004:      *
1005:      * overload this method to retrieve resources from your server
1006:      * <br>
1007:      *
1008:      *
1009:      * @abstract
1010:      * @param array &$params Array of input and output parameters
1011:      * <br><b>input</b><ul>
1012:      * <li> path -
1013:      * </ul>
1014:      * <br><b>output</b><ul>
1015:      * <li> size -
1016:      * </ul>
1017:      * @returns int HTTP-Statuscode
1018:      */
1019: 
1020:     /* abstract
1021:      function GET(&$params)
1022:      {
1023:      // dummy entry for PHPDoc
1024:      }
1025:     */
1026: 
1027:     // }}}
1028: 
1029:     // {{{ PUT()
1030:     /**
1031:      * PUT implementation
1032:      *
1033:      * PUT implementation
1034:      *
1035:      * @abstract
1036:      * @param array &$params
1037:      * @returns int HTTP-Statuscode
1038:      */
1039: 
1040:     /* abstract
1041:      function PUT()
1042:      {
1043:      // dummy entry for PHPDoc
1044:      }
1045:     */
1046: 
1047:     // }}}
1048: 
1049:     // {{{ COPY()
1050: 
1051:     /**
1052:      * COPY implementation
1053:      *
1054:      * COPY implementation
1055:      *
1056:      * @abstract
1057:      * @param array &$params
1058:      * @returns int HTTP-Statuscode
1059:      */
1060: 
1061:     /* abstract
1062:      function COPY()
1063:      {
1064:      // dummy entry for PHPDoc
1065:      }
1066:     */
1067: 
1068:     // }}}
1069: 
1070:     // {{{ MOVE()
1071: 
1072:     /**
1073:      * MOVE implementation
1074:      *
1075:      * MOVE implementation
1076:      *
1077:      * @abstract
1078:      * @param array &$params
1079:      * @returns int HTTP-Statuscode
1080:      */
1081: 
1082:     /* abstract
1083:      function MOVE()
1084:      {
1085:      // dummy entry for PHPDoc
1086:      }
1087:     */
1088: 
1089:     // }}}
1090: 
1091:     // {{{ DELETE()
1092: 
1093:     /**
1094:      * DELETE implementation
1095:      *
1096:      * DELETE implementation
1097:      *
1098:      * @abstract
1099:      * @param array &$params
1100:      * @returns int HTTP-Statuscode
1101:      */
1102: 
1103:     /* abstract
1104:      function DELETE()
1105:      {
1106:      // dummy entry for PHPDoc
1107:      }
1108:     */
1109:     // }}}
1110: 
1111:     // {{{ PROPFIND()
1112: 
1113:     /**
1114:      * PROPFIND implementation
1115:      *
1116:      * PROPFIND implementation
1117:      *
1118:      * @abstract
1119:      * @param array &$params
1120:      * @returns int HTTP-Statuscode
1121:      */
1122: 
1123:     /* abstract
1124:      function PROPFIND()
1125:      {
1126:      // dummy entry for PHPDoc
1127:      }
1128:     */
1129: 
1130:     // }}}
1131: 
1132:     // {{{ PROPPATCH()
1133: 
1134:     /**
1135:      * PROPPATCH implementation
1136:      *
1137:      * PROPPATCH implementation
1138:      *
1139:      * @abstract
1140:      * @param array &$params
1141:      * @returns int HTTP-Statuscode
1142:      */
1143: 
1144:     /* abstract
1145:      function PROPPATCH()
1146:      {
1147:      // dummy entry for PHPDoc
1148:      }
1149:     */
1150:     // }}}
1151: 
1152:     // {{{ LOCK()
1153: 
1154:     /**
1155:      * LOCK implementation
1156:      *
1157:      * LOCK implementation
1158:      *
1159:      * @abstract
1160:      * @param array &$params
1161:      * @returns int HTTP-Statuscode
1162:      */
1163: 
1164:     /* abstract
1165:      function LOCK()
1166:      {
1167:      // dummy entry for PHPDoc
1168:      }
1169:     */
1170:     // }}}
1171: 
1172:     // {{{ UNLOCK()
1173: 
1174:     /**
1175:      * UNLOCK implementation
1176:      *
1177:      * UNLOCK implementation
1178:      *
1179:      * @abstract
1180:      * @param array &$params
1181:      * @returns int HTTP-Statuscode
1182:      */
1183: 
1184:     /* abstract
1185:      function UNLOCK()
1186:      {
1187:      // dummy entry for PHPDoc
1188:      }
1189:     */
1190:     // }}}
1191: 
1192:     // }}}
1193: 
1194:     // {{{ other abstract methods
1195: 
1196:     // {{{ check_auth()
1197: 
1198:     /**
1199:      * check authentication
1200:      *
1201:      * overload this method to retrieve and confirm authentication information
1202:      *
1203:      * @abstract
1204:      * @param string type Authentication type, e.g. "basic" or "digest"
1205:      * @param string username Transmitted username
1206:      * @param string passwort Transmitted password
1207:      * @returns bool Authentication status
1208:      */
1209: 
1210:     /* abstract
1211:      function checkAuth($type, $username, $password)
1212:      {
1213:      // dummy entry for PHPDoc
1214:      }
1215:     */
1216: 
1217:     // }}}
1218: 
1219:     // {{{ checklock()
1220: 
1221:     /**
1222:      * check lock status for a resource
1223:      *
1224:      * overload this method to return shared and exclusive locks
1225:      * active for this resource
1226:      *
1227:      * @abstract
1228:      * @param string resource Resource path to check
1229:      * @returns array An array of lock entries each consisting
1230:      *                of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
1231:      */
1232: 
1233:     /* abstract
1234:      function checklock($resource)
1235:      {
1236:      // dummy entry for PHPDoc
1237:      }
1238:     */
1239: 
1240:     // }}}
1241: 
1242:     // }}}
1243: 
1244:     // {{{ WebDAV HTTP method wrappers
1245: 
1246:     // {{{ http_OPTIONS()
1247: 
1248:     /**
1249:      * OPTIONS method handler
1250:      *
1251:      * The OPTIONS method handler creates a valid OPTIONS reply
1252:      * including Dav: and Allowed: headers
1253:      * based on the implemented methods found in the actual instance
1254:      *
1255:      * @param  void
1256:      * @return void
1257:      */
1258:     function http_OPTIONS()
1259:     {
1260:         // Microsoft clients default to the Frontpage protocol
1261:         // unless we tell them to use WebDAV
1262:         header("MS-Author-Via: DAV");
1263: 
1264:         // get allowed methods
1265:         $allow = $this->_allow();
1266: 
1267:         // dav header
1268:         $dav = array(1);        // assume we are always dav class 1 compliant
1269:         if (isset($allow['LOCK'])) {
1270:             $dav[] = 2;         // dav class 2 requires that locking is supported
1271:         }
1272: 
1273:         // tell clients what we found
1274:         $this->http_status("200 OK");
1275:         header("DAV: "  .join(", ", $dav));
1276:         header("Allow: ".join(", ", $allow));
1277: 
1278:         header("Content-length: 0");
1279:     }
1280: 
1281:     // }}}
1282: 
1283: 
1284:     // {{{ http_PROPFIND()
1285: 
1286:     /**
1287:      * PROPFIND method handler
1288:      *
1289:      * @param  void
1290:      * @return void
1291:      */
1292:     function http_PROPFIND()
1293:     {
1294:         $options = Array();
1295:         $files   = Array();
1296: 
1297:         $options["path"] = $this->path;
1298: 
1299:         // search depth from header (default is "infinity)
1300:         if (isset($this->_SERVER['HTTP_DEPTH'])) {
1301:             $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
1302:         } else {
1303:             $options["depth"] = "infinity";
1304:         }
1305: 
1306:         // analyze request payload
1307:         $propinfo = $this->_parse_propfind("php://input");
1308:         if (!$this->parseSuccess) {
1309:             $this->http_status("400 Error");
1310:             return;
1311:         }
1312:         $options['props'] = $this->parseProps;
1313: 
1314:         // call user handler
1315:         if (!$this->PROPFIND($options, $files)) {
1316:             $files = array("files" => array());
1317:             if (method_exists($this, "checkLock")) {
1318:                 // is locked?
1319:                 $lock = $this->checkLock($this->path);
1320: 
1321:                 if (is_array($lock) && count($lock)) {
1322:                     $created          = isset($lock['created'])  ? $lock['created']  : time();
1323:                     $modified         = isset($lock['modified']) ? $lock['modified'] : time();
1324:                     $files['files'][] = array("path"  => $this->_slashify($this->path),
1325:                                               "props" => array($this->mkprop("displayname",      $this->path),
1326:                                                                $this->mkprop("creationdate",     $created),
1327:                                                                $this->mkprop("getlastmodified",  $modified),
1328:                                                                $this->mkprop("resourcetype",     ""),
1329:                                                                $this->mkprop("getcontenttype",   ""),
1330:                                                                $this->mkprop("getcontentlength", 0))
1331:                                               );
1332:                 }
1333:             }
1334: 
1335:             if (empty($files['files'])) {
1336:                 $this->http_status("404 Not Found");
1337:                 return;
1338:             }
1339:         }
1340: 
1341:         $this->_xml = new Horde_Xml_Element('<D:multistatus xmlns:D="DAV:"/>');
1342:         $this->_xml->registerNamespace('D', "DAV:");
1343:         $this->_xml->registerNamespace('caldav', self::CALDAVNS);
1344:         // Microsoft Clients need this special namespace for date and
1345:         // time values
1346:         //$this->_xml->registerNamespace('xmldata', "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/");
1347: 
1348:         // now we loop over all returned file entries
1349:         foreach ($files["files"] as $filekey => $file) {
1350: 
1351:             // nothing to do if no properties were returend for a file
1352:             if (!isset($file["props"]) || !is_array($file["props"])) {
1353:                 continue;
1354:             }
1355: 
1356:             // now loop over all returned properties
1357:             foreach ($file["props"] as $key => $prop) {
1358:                 // as a convenience feature we do not require that user handlers
1359:                 // restrict returned properties to the requested ones
1360:                 // here we strip all unrequested entries out of the response
1361: 
1362:                 switch($options['props']) {
1363:                 case "all":
1364:                     // nothing to remove
1365:                     break;
1366: 
1367:                 case "names":
1368:                     // only the names of all existing properties were requested
1369:                     // so we remove all values
1370:                     unset($files["files"][$filekey]["props"][$key]["val"]);
1371:                     break;
1372: 
1373:                 default:
1374:                     $found = false;
1375: 
1376:                     // search property name in requested properties
1377:                     foreach ((array)$options["props"] as $reqprop) {
1378:                         if (!isset($reqprop["xmlns"])) {
1379:                             $reqprop["xmlns"] = "";
1380:                         }
1381:                         if (   $reqprop["name"]  == $prop["name"]
1382:                                && $reqprop["xmlns"] == $prop["ns"]) {
1383:                             $found = true;
1384:                             break;
1385:                         }
1386:                     }
1387: 
1388:                     // unset property and continue with next one if not found/requested
1389:                     if (!$found) {
1390:                         $files["files"][$filekey]["props"][$key]="";
1391:                         continue(2);
1392:                     }
1393:                     break;
1394:                 }
1395: 
1396:                 // namespace handling
1397:                 if (empty($prop["ns"])) continue; // no namespace
1398:                 $ns = $prop["ns"];
1399:                 if ($ns == "DAV:") continue; // default namespace
1400:                 if (isset($this->ns_hash[$ns])) continue; // already known
1401: 
1402:                 // register namespace
1403:                 $ns_name = "ns".(count($this->ns_hash));
1404:                 $this->ns_hash[$ns] = $ns_name;
1405:                 $this->_xml->registerNamespace($ns_name, $ns);
1406:             }
1407: 
1408:             // we also need to add empty entries for properties that were requested
1409:             // but for which no values where returned by the user handler
1410:             if (is_array($options['props'])) {
1411:                 foreach ($options["props"] as $reqprop) {
1412:                     if ($reqprop['name']=="") continue; // skip empty entries
1413: 
1414:                     $found = false;
1415: 
1416:                     if (!isset($reqprop["xmlns"])) {
1417:                         $reqprop["xmlns"] = "";
1418:                     }
1419: 
1420:                     // check if property exists in result
1421:                     foreach ($file["props"] as $prop) {
1422:                         if (   $reqprop["name"]  == $prop["name"]
1423:                                && $reqprop["xmlns"] == $prop["ns"]) {
1424:                             $found = true;
1425:                             break;
1426:                         }
1427:                     }
1428: 
1429:                     if (!$found) {
1430:                         if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") {
1431:                             // lockdiscovery is handled by the base class
1432:                             $files["files"][$filekey]["props"][]
1433:                                 = $this->mkprop("DAV:",
1434:                                                 "lockdiscovery",
1435:                                                 $this->lockdiscovery($files["files"][$filekey]['path']));
1436:                         } else {
1437:                             // add empty value for this property
1438:                             $files["files"][$filekey]["noprops"][] =
1439:                                 $this->mkprop($reqprop["xmlns"], $reqprop["name"], "");
1440: 
1441:                             // register property namespace if not known yet
1442:                             if ($reqprop["xmlns"] != "DAV:" && !isset($this->ns_hash[$reqprop["xmlns"]])) {
1443:                                 $ns_name = "ns".(count($this->ns_hash));
1444:                                 $this->ns_hash[$reqprop["xmlns"]] = $ns_name;
1445:                                 $this->_xml->registerNamespace($ns_name, $reqprop['xmlns']);
1446:                             }
1447:                         }
1448:                     }
1449:                 }
1450:             }
1451:         }
1452: 
1453:         // now we generate the reply header ...
1454:         $this->http_status("207 Multi-Status");
1455:         header('Content-Type: text/xml; charset="utf-8"');
1456: 
1457:         // ... and payload
1458:         foreach ($files["files"] as $file) {
1459:             // ignore empty or incomplete entries
1460:             if (!is_array($file) || empty($file) || !isset($file["path"])) continue;
1461:             $path = $file['path'];
1462:             if (!is_string($path) || $path==="") continue;
1463: 
1464:             $xmldata = array('D:response' => array());
1465:             #echo " <D:response $ns_defs>\n";
1466: 
1467:             /* TODO right now the user implementation has to make sure
1468:              collections end in a slash, this should be done in here
1469:              by checking the resource attribute */
1470:             $href = $this->_mergePaths($this->base_uri, $path);
1471: 
1472:             /* minimal urlencoding is needed for the resource path */
1473:             $xmldata['D:response']['D:href'] = $this->_urlencode($href);
1474:             #echo "  <D:href>$href</D:href>\n";
1475: 
1476:             // report all found properties and their values (if any)
1477:             if (isset($file["props"]) && is_array($file["props"])) {
1478:                 #echo "  <D:propstat>\n";
1479:                 $i = 0;
1480:                 $propstats = array($i => array('D:prop' => array()));
1481:                 #echo "   <D:prop>\n";
1482: 
1483:                 foreach ($file["props"] as $key => $prop) {
1484: 
1485:                     if (!is_array($prop)) continue;
1486:                     if (!isset($prop["name"])) continue;
1487: 
1488:                     if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) {
1489:                         // empty properties (cannot use empty() for check as "0" is a legal value here)
1490:                         if ($prop["ns"]=="DAV:") {
1491:                             $propstats[$i]['D:prop']['D:' . $prop['name']] = '';
1492:                             #echo "     <D:$prop[name]/>\n";
1493:                         } else if (!empty($prop["ns"])) {
1494:                             $propstats[$i]['D:prop'][$this->ns_hash[$prop["ns"]].':'.$prop['name']] = '';
1495:                             #echo "     <".$this->ns_hash[$prop["ns"]].":$prop[name]/>\n";
1496:                         } else {
1497:                             $propstats[$i]['D:prop'][$prop['name']] = '';
1498:                             $propstats[$i]['D:prop'][$prop['name'] . '#xmlns'] = '';
1499:                             #echo "     <$prop[name] xmlns=\"\"/>";
1500:                         }
1501:                     } else if ($prop["ns"] == "DAV:") {
1502:                         // some WebDAV properties need special treatment
1503:                         switch ($prop["name"]) {
1504:                         case "creationdate":
1505:                             $propstats[$i]['D:prop']['D:creationdate'] = gmdate("Y-m-d\\TH:i:s\\Z", $prop['val']);
1506:                             $propstats[$i]['D:prop']['D:creationdate#xmldata:dt'] = 'dateTime.tz';
1507:                             #echo "     <D:creationdate ns0:dt=\"dateTime.tz\">"
1508:                             #    . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val'])
1509:                             #    . "</D:creationdate>\n";
1510:                             break;
1511:                         case "getlastmodified":
1512:                             $propstats[$i]['D:prop']['D:getlastmodified'] = gmdate("D, d M Y H:i:s ", $prop['val']);
1513:                             $propstats[$i]['D:prop']['D:getlastmodified#xmldata:dt'] = 'dateTime.rfc1123';
1514:                             #echo "     <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"
1515:                             #    . gmdate("D, d M Y H:i:s ", $prop['val'])
1516:                             #    . "GMT</D:getlastmodified>\n";
1517:                             break;
1518:                         case "resourcetype":
1519:                             $propstats[$i]['D:prop']['D:resourcetype']['D:'.$prop['val']] = '';
1520:                             #echo "     <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
1521:                             break;
1522:                         case "supportedlock":
1523:                             $propstats[$i]['D:prop']['D:supportedlock'] = $prop['val'];
1524:                             #echo "     <D:supportedlock>$prop[val]</D:supportedlock>\n";
1525:                             break;
1526:                         case "lockdiscovery":
1527:                             $propstats[$i]['D:prop']['D:lockdiscovery'] = $prop['val'];
1528:                             #echo "     <D:lockdiscovery>\n";
1529:                             #echo $prop["val"];
1530:                             #echo "     </D:lockdiscovery>\n";
1531:                             break;
1532:                         // the following are non-standard Microsoft extensions to the DAV namespace
1533:                         case "lastaccessed":
1534:                             $propstats[$i]['D:prop']['D:lastaccessed'] = gmdate("D, d M Y H:i:s ", $prop['val']);
1535:                             $propstats[$i]['D:prop']['D:lastaccessed#xmldata:dt'] = 'dateTime.rfc1123';
1536:                             #echo "     <D:lastaccessed ns0:dt=\"dateTime.rfc1123\">"
1537:                             #    . gmdate("D, d M Y H:i:s ", $prop['val'])
1538:                             #    . "GMT</D:lastaccessed>\n";
1539:                             break;
1540:                         case "ishidden":
1541:                             $propstats[$i]['D:prop']['D:ishidden'] = is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false');
1542:                             #echo "     <D:ishidden>"
1543:                             #    . is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false')
1544:                             #    . "</D:ishidden>\n";
1545:                             break;
1546:                         default:
1547:                             $propstats[$i]['D:prop']['D:'. $prop['name']] = $prop['val'];
1548:                             #echo "     <D:$prop[name]>"
1549:                             #    . htmlspecialchars($prop['val'])
1550:                             #    .     "</D:$prop[name]>\n";
1551:                             break;
1552:                         }
1553:                     } else {
1554:                         list($key, $val) = $this->_prop2xml($prop);
1555:                         $propstats[$i]['D:prop'][$key] = $val;
1556:                         #echo $this->_prop2xml($prop);
1557:                     }
1558:                 }
1559: 
1560:                 #echo "   </D:prop>\n";
1561:                 $propstats[$i]['D:status'] = 'HTTP/1.1 200 OK';
1562:                 #echo "   <D:status>HTTP/1.1 200 OK</D:status>\n";
1563:                 #echo "  </D:propstat>\n";
1564:             }
1565:             // Increment to the next propstat stanza.
1566:             $i++;
1567: 
1568:             // now report all properties requested but not found
1569:             if (isset($file["noprops"])) {
1570:                 #echo "  <D:propstat>\n";
1571:                 $propstats[$i]['D:prop'] = array();
1572:                 #echo "   <D:prop>\n";
1573: 
1574:                 foreach ($file["noprops"] as $key => $prop) {
1575:                     if ($prop["ns"] == "DAV:") {
1576:                         $propstats[$i]['D:prop']['D:' . $prop['name']] = '';
1577:                         #echo "     <D:$prop[name]/>\n";
1578:                     } else if ($prop["ns"] == "") {
1579:                         $propstats[$i]['D:prop'][$prop['name']] = '';
1580:                         $propstats[$i]['D:prop'][$prop['name'] . '#xmlns'] = '';
1581:                         #echo "     <$prop[name] xmlns=\"\"/>\n";
1582:                     } else {
1583:                         $propstats[$i]['D:prop'][$this->ns_hash[$prop['ns']] . ':' . $prop['name']] = '';
1584:                         #echo "     <" . $this->ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
1585:                     }
1586:                 }
1587: 
1588:                 #echo "   </D:prop>\n";
1589:                 $propstats[$i]['D:status'] = 'HTTP/1.1 404 Not Found';
1590:                 #echo "   <D:status>HTTP/1.1 404 Not Found</D:status>\n";
1591:                 #echo "  </D:propstat>\n";
1592:             }
1593: 
1594:             $xmldata['D:response']['D:propstat'] = $propstats;
1595:             #echo " </D:response>\n";
1596:             $this->_xml->fromArray($xmldata);
1597:         }
1598: 
1599:         #echo "</D:multistatus>\n";
1600:         echo $this->_xml->saveXml();
1601:     }
1602: 
1603: 
1604:     // }}}
1605: 
1606:     // {{{ http_PROPPATCH()
1607: 
1608:     /**
1609:      * PROPPATCH method handler
1610:      *
1611:      * @param  void
1612:      * @return void
1613:      */
1614:     function http_PROPPATCH()
1615:     {
1616:         if ($this->_check_lock_status($this->path)) {
1617:             $options = Array();
1618: 
1619:             $options["path"] = $this->path;
1620: 
1621:             $propinfo = $this->_parse_proppatch("php://input");
1622: 
1623:             if (!$this->parseSuccess) {
1624:                 $this->http_status("400 Error");
1625:                 return;
1626:             }
1627: 
1628:             $options['props'] = $this->parseProps;
1629: 
1630:             $responsedescr = $this->PROPPATCH($options);
1631: 
1632:             $this->http_status("207 Multi-Status");
1633:             header('Content-Type: text/xml; charset="utf-8"');
1634: 
1635:             echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1636: 
1637:             echo "<D:multistatus xmlns:D=\"DAV:\">\n";
1638:             echo " <D:response>\n";
1639:             echo "  <D:href>".$this->_urlencode($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path))."</D:href>\n";
1640: 
1641:             foreach ($options["props"] as $prop) {
1642:                 echo "   <D:propstat>\n";
1643:                 echo "    <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n";
1644:                 echo "    <D:status>HTTP/1.1 $prop[status]</D:status>\n";
1645:                 echo "   </D:propstat>\n";
1646:             }
1647: 
1648:             if ($responsedescr) {
1649:                 echo "  <D:responsedescription>".
1650:                     htmlspecialchars($responsedescr).
1651:                     "</D:responsedescription>\n";
1652:             }
1653: 
1654:             echo " </D:response>\n";
1655:             echo "</D:multistatus>\n";
1656:         } else {
1657:             $this->http_status("423 Locked");
1658:         }
1659:     }
1660: 
1661:     // }}}
1662: 
1663: 
1664:     // {{{ http_MKCOL()
1665: 
1666:     /**
1667:      * MKCOL method handler
1668:      *
1669:      * @param  void
1670:      * @return void
1671:      */
1672:     function http_MKCOL()
1673:     {
1674:         $options = Array();
1675: 
1676:         $options["path"] = $this->path;
1677: 
1678:         $stat = $this->MKCOL($options);
1679: 
1680:         $this->http_status($stat);
1681:     }
1682: 
1683:     // }}}
1684: 
1685: 
1686:     // {{{ http_GET()
1687: 
1688:     /**
1689:      * GET method handler
1690:      *
1691:      * @param void
1692:      * @returns void
1693:      */
1694:     function http_GET()
1695:     {
1696:         // TODO check for invalid stream
1697:         $options         = Array();
1698:         $options["path"] = $this->path;
1699: 
1700:         $this->_get_ranges($options);
1701: 
1702:         if (true === ($status = $this->GET($options))) {
1703:             if (!headers_sent()) {
1704:                 $status = "200 OK";
1705: 
1706:                 if (!isset($options['mimetype'])) {
1707:                     $options['mimetype'] = "application/octet-stream";
1708:                 }
1709:                 header("Content-type: $options[mimetype]");
1710: 
1711:                 if (isset($options['mtime'])) {
1712:                     header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
1713:                 }
1714: 
1715:                 if (isset($options['stream'])) {
1716:                     // GET handler returned a stream
1717:                     if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) {
1718:                         // partial request and stream is seekable
1719: 
1720:                         if (count($options['ranges']) === 1) {
1721:                             $range = $options['ranges'][0];
1722: 
1723:                             if (isset($range['start'])) {
1724:                                 fseek($options['stream'], $range['start'], SEEK_SET);
1725:                                 if (feof($options['stream'])) {
1726:                                     $this->http_status("416 Requested range not satisfiable");
1727:                                     return;
1728:                                 }
1729: 
1730:                                 if (isset($range['end'])) {
1731:                                     $size = $range['end']-$range['start']+1;
1732:                                     $this->http_status("206 partial");
1733:                                     header("Content-length: $size");
1734:                                     header("Content-range: $range[start]-$range[end]/"
1735:                                            . (isset($options['size']) ? $options['size'] : "*"));
1736:                                     while ($size && !feof($options['stream'])) {
1737:                                         $buffer = fread($options['stream'], 4096);
1738:                                         $size  -= $this->bytes($buffer);
1739:                                         echo $buffer;
1740:                                     }
1741:                                 } else {
1742:                                     $this->http_status("206 partial");
1743:                                     if (isset($options['size'])) {
1744:                                         header("Content-length: ".($options['size'] - $range['start']));
1745:                                         header("Content-range: ".$range['start']."-".$range['end']."/"
1746:                                                . (isset($options['size']) ? $options['size'] : "*"));
1747:                                     }
1748:                                     fpassthru($options['stream']);
1749:                                 }
1750:                             } else {
1751:                                 header("Content-length: ".$range['last']);
1752:                                 fseek($options['stream'], -$range['last'], SEEK_END);
1753:                                 fpassthru($options['stream']);
1754:                             }
1755:                         } else {
1756:                             $this->_multipart_byterange_header(); // init multipart
1757:                             foreach ($options['ranges'] as $range) {
1758:                                 // TODO what if size unknown? 500?
1759:                                 if (isset($range['start'])) {
1760:                                     $from = $range['start'];
1761:                                     $to   = !empty($range['end']) ? $range['end'] : $options['size']-1;
1762:                                 } else {
1763:                                     $from = $options['size'] - $range['last']-1;
1764:                                     $to   = $options['size'] -1;
1765:                                 }
1766:                                 $total = isset($options['size']) ? $options['size'] : "*";
1767:                                 $size  = $to - $from + 1;
1768:                                 $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total);
1769: 
1770: 
1771:                                 fseek($options['stream'], $from, SEEK_SET);
1772:                                 while ($size && !feof($options['stream'])) {
1773:                                     $buffer = fread($options['stream'], 4096);
1774:                                     $size  -= $this->bytes($buffer);
1775:                                     echo $buffer;
1776:                                 }
1777:                             }
1778:                             $this->_multipart_byterange_header(); // end multipart
1779:                         }
1780:                     } else {
1781:                         // normal request or stream isn't seekable, return full content
1782:                         if (isset($options['size'])) {
1783:                             header("Content-length: ".$options['size']);
1784:                         }
1785:                         fpassthru($options['stream']);
1786:                         return; // no more headers
1787:                     }
1788:                 } elseif (isset($options['data'])) {
1789:                     if (is_array($options['data'])) {
1790:                         // reply to partial request
1791:                     } else {
1792:                         header("Content-length: ".$this->bytes($options['data']));
1793:                         echo $options['data'];
1794:                     }
1795:                 }
1796:             }
1797:         }
1798: 
1799:         if (!headers_sent()) {
1800:             if (false === $status) {
1801:                 $this->http_status("404 not found");
1802:             } else {
1803:                 // TODO: check setting of headers in various code paths above
1804:                 $this->http_status("$status");
1805:             }
1806:         }
1807:     }
1808: 
1809: 
1810:     /**
1811:      * parse HTTP Range: header
1812:      *
1813:      * @param  array options array to store result in
1814:      * @return void
1815:      */
1816:     function _get_ranges(&$options)
1817:     {
1818:         // process Range: header if present
1819:         if (isset($this->_SERVER['HTTP_RANGE'])) {
1820: 
1821:             // we only support standard "bytes" range specifications for now
1822:             if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) {
1823:                 $options["ranges"] = array();
1824: 
1825:                 // ranges are comma separated
1826:                 foreach (explode(",", $matches[1]) as $range) {
1827:                     // ranges are either from-to pairs or just end positions
1828:                     list($start, $end) = explode("-", $range);
1829:                     $options["ranges"][] = ($start==="")
1830:                         ? array("last"=>$end)
1831:                         : array("start"=>$start, "end"=>$end);
1832:                 }
1833:             }
1834:         }
1835:     }
1836: 
1837:     /**
1838:      * generate separator headers for multipart response
1839:      *
1840:      * first and last call happen without parameters to generate
1841:      * the initial header and closing sequence, all calls inbetween
1842:      * require content mimetype, start and end byte position and
1843:      * optionaly the total byte length of the requested resource
1844:      *
1845:      * @param  string  mimetype
1846:      * @param  int     start byte position
1847:      * @param  int     end   byte position
1848:      * @param  int     total resource byte size
1849:      */
1850:     function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false)
1851:     {
1852:         if ($mimetype === false) {
1853:             if (!isset($this->multipart_separator)) {
1854:                 // initial
1855: 
1856:                 // a little naive, this sequence *might* be part of the content
1857:                 // but it's really not likely and rather expensive to check
1858:                 $this->multipart_separator = "SEPARATOR_" . uniqid(mt_rand());
1859: 
1860:                 // generate HTTP header
1861:                 header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator);
1862:             } else {
1863:                 // final
1864: 
1865:                 // generate closing multipart sequence
1866:                 echo "\n--{$this->multipart_separator}--";
1867:             }
1868:         } else {
1869:             // generate separator and header for next part
1870:             echo "\n--{$this->multipart_separator}\n";
1871:             echo "Content-type: $mimetype\n";
1872:             echo "Content-range: $from-$to/". ($total === false ? "*" : $total);
1873:             echo "\n\n";
1874:         }
1875:     }
1876: 
1877: 
1878: 
1879:     // }}}
1880: 
1881:     // {{{ http_HEAD()
1882: 
1883:     /**
1884:      * HEAD method handler
1885:      *
1886:      * @param  void
1887:      * @return void
1888:      */
1889:     function http_HEAD()
1890:     {
1891:         $status          = false;
1892:         $options         = Array();
1893:         $options["path"] = $this->path;
1894: 
1895:         if (method_exists($this, "HEAD")) {
1896:             $status = $this->head($options);
1897:         } else if (method_exists($this, "GET")) {
1898:             ob_start();
1899:             $status = $this->GET($options);
1900:             if (!isset($options['size'])) {
1901:                 $options['size'] = ob_get_length();
1902:             }
1903:             ob_end_clean();
1904:         }
1905: 
1906:         if (!isset($options['mimetype'])) {
1907:             $options['mimetype'] = "application/octet-stream";
1908:         }
1909:         header("Content-type: $options[mimetype]");
1910: 
1911:         if (isset($options['mtime'])) {
1912:             header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
1913:         }
1914: 
1915:         if (isset($options['size'])) {
1916:             header("Content-length: ".$options['size']);
1917:         }
1918: 
1919:         if ($status === true)  $status = "200 OK";
1920:         if ($status === false) $status = "404 Not found";
1921: 
1922:         $this->http_status($status);
1923:     }
1924: 
1925:     // }}}
1926: 
1927:     // {{{ http_PUT()
1928: 
1929:     /**
1930:      * PUT method handler
1931:      *
1932:      * @param  void
1933:      * @return void
1934:      */
1935:     function http_PUT()
1936:     {
1937:         if ($this->_check_lock_status($this->path)) {
1938:             $options                   = Array();
1939:             $options["path"]           = $this->path;
1940:             $options["content_length"] = $this->_SERVER["CONTENT_LENGTH"];
1941: 
1942:             // get the Content-type
1943:             if (isset($this->_SERVER["CONTENT_TYPE"])) {
1944:                 // for now we do not support any sort of multipart requests
1945:                 if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) {
1946:                     $this->http_status("501 not implemented");
1947:                     echo "The service does not support mulipart PUT requests";
1948:                     return;
1949:                 }
1950:                 $options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
1951:             } else {
1952:                 // default content type if none given
1953:                 $options["content_type"] = "application/octet-stream";
1954:             }
1955: 
1956:             /* RFC 2616 2.6 says: "The recipient of the entity MUST NOT
1957:              ignore any Content-* (e.g. Content-Range) headers that it
1958:              does not understand or implement and MUST return a 501
1959:              (Not Implemented) response in such cases."
1960:             */
1961:             foreach ($this->_SERVER as $key => $val) {
1962:                 if (strncmp($key, "HTTP_CONTENT", 11)) continue;
1963:                 switch ($key) {
1964:                 case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
1965:                     // TODO support this if ext/zlib filters are available
1966:                     $this->http_status("501 not implemented");
1967:                     echo "The service does not support '$val' content encoding";
1968:                     return;
1969: 
1970:                 case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12
1971:                     // we assume it is not critical if this one is ignored
1972:                     // in the actual PUT implementation ...
1973:                     $options["content_language"] = $val;
1974:                     break;
1975: 
1976:                 case 'HTTP_CONTENT_LENGTH':
1977:                     // defined on IIS and has the same value as CONTENT_LENGTH
1978:                     break;
1979: 
1980:                 case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14
1981:                     /* The meaning of the Content-Location header in PUT
1982:                      or POST requests is undefined; servers are free
1983:                      to ignore it in those cases. */
1984:                     break;
1985: 
1986:                 case 'HTTP_CONTENT_RANGE':    // RFC 2616 14.16
1987:                     // single byte range requests are supported
1988:                     // the header format is also specified in RFC 2616 14.16
1989:                     // TODO we have to ensure that implementations support this or send 501 instead
1990:                     if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) {
1991:                         $this->http_status("400 bad request");
1992:                         echo "The service does only support single byte ranges";
1993:                         return;
1994:                     }
1995: 
1996:                     $range = array("start"=>$matches[1], "end"=>$matches[2]);
1997:                     if (is_numeric($matches[3])) {
1998:                         $range["total_length"] = $matches[3];
1999:                     }
2000:                     $option["ranges"][] = $range;
2001: 
2002:                     // TODO make sure the implementation supports partial PUT
2003:                     // this has to be done in advance to avoid data being overwritten
2004:                     // on implementations that do not support this ...
2005:                     break;
2006: 
2007:                 case 'HTTP_CONTENT_TYPE':
2008:                     // defined on IIS and has the same value as CONTENT_TYPE
2009:                     break;
2010: 
2011:                 case 'HTTP_CONTENT_MD5':      // RFC 2616 14.15
2012:                     // TODO: maybe we can just pretend here?
2013:                     $this->http_status("501 not implemented");
2014:                     echo "The service does not support content MD5 checksum verification";
2015:                     return;
2016: 
2017:                 default:
2018:                     // any other unknown Content-* headers
2019:                     $this->http_status("501 not implemented");
2020:                     echo "The service does not support '$key'";
2021:                     return;
2022:                 }
2023:             }
2024: 
2025:             $options["stream"] = fopen("php://input", "r");
2026: 
2027:             $stat = $this->PUT($options);
2028: 
2029:             if ($stat === false) {
2030:                 $stat = "403 Forbidden";
2031:             } else if (is_resource($stat) && get_resource_type($stat) == "stream") {
2032:                 $stream = $stat;
2033: 
2034:                 $stat = $options["new"] ? "201 Created" : "204 No Content";
2035: 
2036:                 if (!empty($options["ranges"])) {
2037:                     // TODO multipart support is missing (see also above)
2038:                     if (0 == fseek($stream, $range[0]["start"], SEEK_SET)) {
2039:                         $length = $range[0]["end"]-$range[0]["start"]+1;
2040:                         if (!fwrite($stream, fread($options["stream"], $length))) {
2041:                             $stat = "403 Forbidden";
2042:                         }
2043:                     } else {
2044:                         $stat = "403 Forbidden";
2045:                     }
2046:                 } else {
2047:                     while (!feof($options["stream"])) {
2048:                         if (false === fwrite($stream, fread($options["stream"], 4096))) {
2049:                             $stat = "403 Forbidden";
2050:                             break;
2051:                         }
2052:                     }
2053:                 }
2054: 
2055:                 fclose($stream);
2056:             }
2057: 
2058:             $this->http_status($stat);
2059:         } else {
2060:             $this->http_status("423 Locked");
2061:         }
2062:     }
2063: 
2064:     // }}}
2065: 
2066: 
2067:     // {{{ http_DELETE()
2068: 
2069:     /**
2070:      * DELETE method handler
2071:      *
2072:      * @param  void
2073:      * @return void
2074:      */
2075:     function http_DELETE()
2076:     {
2077:         // check RFC 2518 Section 9.2, last paragraph
2078:         if (isset($this->_SERVER["HTTP_DEPTH"])) {
2079:             if ($this->_SERVER["HTTP_DEPTH"] != "infinity") {
2080:                 $this->http_status("400 Bad Request");
2081:                 return;
2082:             }
2083:         }
2084: 
2085:         // check lock status
2086:         if ($this->_check_lock_status($this->path)) {
2087:             // ok, proceed
2088:             $options         = Array();
2089:             $options["path"] = $this->path;
2090: 
2091:             $stat = $this->DELETE($options);
2092: 
2093:             $this->http_status($stat);
2094:         } else {
2095:             // sorry, its locked
2096:             $this->http_status("423 Locked");
2097:         }
2098:     }
2099: 
2100:     // }}}
2101: 
2102:     // {{{ http_COPY()
2103: 
2104:     /**
2105:      * COPY method handler
2106:      *
2107:      * @param  void
2108:      * @return void
2109:      */
2110:     function http_COPY()
2111:     {
2112:         // no need to check source lock status here
2113:         // destination lock status is always checked by the helper method
2114:         $this->_copymove("copy");
2115:     }
2116: 
2117:     // }}}
2118: 
2119:     // {{{ http_MOVE()
2120: 
2121:     /**
2122:      * MOVE method handler
2123:      *
2124:      * @param  void
2125:      * @return void
2126:      */
2127:     function http_MOVE()
2128:     {
2129:         if ($this->_check_lock_status($this->path)) {
2130:             // destination lock status is always checked by the helper method
2131:             $this->_copymove("move");
2132:         } else {
2133:             $this->http_status("423 Locked");
2134:         }
2135:     }
2136: 
2137:     // }}}
2138: 
2139: 
2140:     // {{{ http_LOCK()
2141: 
2142:     /**
2143:      * LOCK method handler
2144:      *
2145:      * @param  void
2146:      * @return void
2147:      */
2148:     function http_LOCK()
2149:     {
2150:         $options         = Array();
2151:         $options["path"] = $this->path;
2152: 
2153:         if (isset($this->_SERVER['HTTP_DEPTH'])) {
2154:             $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
2155:         } else {
2156:             $options["depth"] = "infinity";
2157:         }
2158: 
2159:         if (isset($this->_SERVER["HTTP_TIMEOUT"])) {
2160:             $options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]);
2161:         }
2162: 
2163:         if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) {
2164:             // check if locking is possible
2165:             if (!$this->_check_lock_status($this->path)) {
2166:                 $this->http_status("423 Locked");
2167:                 return;
2168:             }
2169: 
2170:             // refresh lock
2171:             $options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2);
2172:             $options["update"]    = $options["locktoken"];
2173: 
2174:             // setting defaults for required fields, LOCK() SHOULD overwrite these
2175:             $options['owner']     = "unknown";
2176:             $options['scope']     = "exclusive";
2177:             $options['type']      = "write";
2178: 
2179: 
2180:             $stat = $this->LOCK($options);
2181:         } else {
2182:             // extract lock request information from request XML payload
2183:             $lockinfo = $this->_parse_lockinfo("php://input");
2184:             if (!$lockinfo->parseSuccess) {
2185:                 $this->http_status("400 bad request");
2186:             }
2187: 
2188:             // check if locking is possible
2189:             if (!$this->_check_lock_status($this->path, $this->lockscope === "shared")) {
2190:                 $this->http_status("423 Locked");
2191:                 return;
2192:             }
2193: 
2194:             // new lock
2195:             $options["scope"]     = $this->lockscope;
2196:             $options["type"]      = $this->locktype;
2197:             $options["owner"]     = $this->owner;
2198:             $options["locktoken"] = $this->_new_locktoken();
2199: 
2200:             $stat = $this->LOCK($options);
2201:         }
2202: 
2203:         if (is_bool($stat)) {
2204:             $http_stat = $stat ? "200 OK" : "423 Locked";
2205:         } else {
2206:             $http_stat = (string)$stat;
2207:         }
2208:         $this->http_status($http_stat);
2209: 
2210:         if ($http_stat{0} == 2) { // 2xx states are ok
2211:             if ($options["timeout"]) {
2212:                 // if multiple timeout values were given we take the first only
2213:                 if (is_array($options["timeout"])) {
2214:                     reset($options["timeout"]);
2215:                     $options["timeout"] = current($options["timeout"]);
2216:                 }
2217:                 // if the timeout is numeric only we need to reformat it
2218:                 if (is_numeric($options["timeout"])) {
2219:                     // more than a million is considered an absolute timestamp
2220:                     // less is more likely a relative value
2221:                     if ($options["timeout"]>1000000) {
2222:                         $timeout = "Second-".($options['timeout']-time());
2223:                     } else {
2224:                         $timeout = "Second-$options[timeout]";
2225:                     }
2226:                 } else {
2227:                     // non-numeric values are passed on verbatim,
2228:                     // no error checking is performed here in this case
2229:                     // TODO: send "Infinite" on invalid timeout strings?
2230:                     $timeout = $options["timeout"];
2231:                 }
2232:             } else {
2233:                 $timeout = "Infinite";
2234:             }
2235: 
2236:             header('Content-Type: text/xml; charset="utf-8"');
2237:             header("Lock-Token: <$options[locktoken]>");
2238:             echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
2239:             echo "<D:prop xmlns:D=\"DAV:\">\n";
2240:             echo " <D:lockdiscovery>\n";
2241:             echo "  <D:activelock>\n";
2242:             echo "   <D:lockscope><D:$options[scope]/></D:lockscope>\n";
2243:             echo "   <D:locktype><D:$options[type]/></D:locktype>\n";
2244:             echo "   <D:depth>$options[depth]</D:depth>\n";
2245:             echo "   <D:owner>$options[owner]</D:owner>\n";
2246:             echo "   <D:timeout>$timeout</D:timeout>\n";
2247:             echo "   <D:locktoken><D:href>$options[locktoken]</D:href></D:locktoken>\n";
2248:             echo "  </D:activelock>\n";
2249:             echo " </D:lockdiscovery>\n";
2250:             echo "</D:prop>\n\n";
2251:         }
2252:     }
2253: 
2254: 
2255:     // }}}
2256: 
2257:     // {{{ http_UNLOCK()
2258: 
2259:     /**
2260:      * UNLOCK method handler
2261:      *
2262:      * @param  void
2263:      * @return void
2264:      */
2265:     function http_UNLOCK()
2266:     {
2267:         $options         = Array();
2268:         $options["path"] = $this->path;
2269: 
2270:         if (isset($this->_SERVER['HTTP_DEPTH'])) {
2271:             $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
2272:         } else {
2273:             $options["depth"] = "infinity";
2274:         }
2275: 
2276:         // strip surrounding <>
2277:         $options["token"] = substr(trim($this->_SERVER["HTTP_LOCK_TOKEN"]), 1, -1);
2278: 
2279:         // call user method
2280:         $stat = $this->UNLOCK($options);
2281: 
2282:         $this->http_status($stat);
2283:     }
2284: 
2285:     // }}}
2286: 
2287:     // }}}
2288: 
2289:     // {{{ _copymove()
2290: 
2291:     function _copymove($what)
2292:     {
2293:         $options         = Array();
2294:         $options["path"] = $this->path;
2295: 
2296:         if (isset($this->_SERVER["HTTP_DEPTH"])) {
2297:             $options["depth"] = $this->_SERVER["HTTP_DEPTH"];
2298:         } else {
2299:             $options["depth"] = "infinity";
2300:         }
2301: 
2302:         $http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_HOST"]);
2303: 
2304:         $url  = parse_url($this->_SERVER["HTTP_DESTINATION"]);
2305:         $path = urldecode($url["path"]);
2306: 
2307:         if (isset($url["host"])) {
2308:             // TODO check url scheme, too
2309:             $http_host = $url["host"];
2310:             if (isset($url["port"]) && $url["port"] != 80)
2311:                 $http_host.= ":".$url["port"];
2312:         } else {
2313:             // only path given, set host to self
2314:             $http_host == $http_header_host;
2315:         }
2316: 
2317:         if ($http_host == $http_header_host &&
2318:             !strncmp($this->_SERVER["SCRIPT_NAME"], $path,
2319:                      strlen($this->_SERVER["SCRIPT_NAME"]))) {
2320:             $options["dest"] = substr($path, strlen($this->_SERVER["SCRIPT_NAME"]));
2321:             if (!$this->_check_lock_status($options["dest"])) {
2322:                 $this->http_status("423 Locked");
2323:                 return;
2324:             }
2325: 
2326:         } else {
2327:             $options["dest_url"] = $this->_SERVER["HTTP_DESTINATION"];
2328:         }
2329: 
2330:         // see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3
2331:         if (isset($this->_SERVER["HTTP_OVERWRITE"])) {
2332:             $options["overwrite"] = $this->_SERVER["HTTP_OVERWRITE"] == "T";
2333:         } else {
2334:             $options["overwrite"] = true;
2335:         }
2336: 
2337:         $stat = $this->$what($options);
2338:         $this->http_status($stat);
2339:     }
2340: 
2341:     // }}}
2342: 
2343:     // {{{ _allow()
2344: 
2345:     /**
2346:      * check for implemented HTTP methods
2347:      *
2348:      * @param  void
2349:      * @return array something
2350:      */
2351:     function _allow()
2352:     {
2353:         // OPTIONS is always there
2354:         $allow = array("OPTIONS" =>"OPTIONS");
2355: 
2356:         // all other METHODS need both a http_method() wrapper
2357:         // and a method() implementation
2358:         // the base class supplies wrappers only
2359:         foreach (get_class_methods($this) as $method) {
2360:             if (!strncmp("http_", $method, 5)) {
2361:                 $method = strtoupper(substr($method, 5));
2362:                 if (method_exists($this, $method)) {
2363:                     $allow[$method] = $method;
2364:                 }
2365:             }
2366:         }
2367: 
2368:         // we can emulate a missing HEAD implemetation using GET
2369:         if (isset($allow["GET"]))
2370:             $allow["HEAD"] = "HEAD";
2371: 
2372:         // no LOCK without checklok()
2373:         if (!method_exists($this, "checklock")) {
2374:             unset($allow["LOCK"]);
2375:             unset($allow["UNLOCK"]);
2376:         }
2377: 
2378:         return $allow;
2379:     }
2380: 
2381:     // }}}
2382: 
2383:     /**
2384:      * helper for property element creation
2385:      *
2386:      * @param  string  XML namespace (optional)
2387:      * @param  string  property name
2388:      * @param  string  property value
2389:      * @return array   property array
2390:      */
2391:     function mkprop()
2392:     {
2393:         $args = func_get_args();
2394:         if (count($args) == 3) {
2395:             return array("ns"   => $args[0],
2396:                          "name" => $args[1],
2397:                          "val"  => $args[2]);
2398:         } else {
2399:             return array("ns"   => "DAV:",
2400:                          "name" => $args[0],
2401:                          "val"  => $args[1]);
2402:         }
2403:     }
2404: 
2405:     // {{{ _check_auth
2406: 
2407:     /**
2408:      * check authentication if check is implemented
2409:      *
2410:      * @param  void
2411:      * @return bool  true if authentication succeded or not necessary
2412:      */
2413:     function _check_auth()
2414:     {
2415:         $auth_type = isset($this->_SERVER["AUTH_TYPE"])
2416:             ? $this->_SERVER["AUTH_TYPE"]
2417:             : null;
2418: 
2419:         $auth_user = isset($this->_SERVER["PHP_AUTH_USER"])
2420:             ? $this->_SERVER["PHP_AUTH_USER"]
2421:             : null;
2422: 
2423:         $auth_pw   = isset($this->_SERVER["PHP_AUTH_PW"])
2424:             ? $this->_SERVER["PHP_AUTH_PW"]
2425:             : null;
2426: 
2427:         if (method_exists($this, "checkAuth")) {
2428:             // PEAR style method name
2429:             return $this->checkAuth($auth_type, $auth_user, $auth_pw);
2430:         } else if (method_exists($this, "check_auth")) {
2431:             // old (pre 1.0) method name
2432:             return $this->check_auth($auth_type, $auth_user, $auth_pw);
2433:         } else {
2434:             // no method found -> no authentication required
2435:             return true;
2436:         }
2437:     }
2438: 
2439:     // }}}
2440: 
2441:     // {{{ UUID stuff
2442: 
2443:     /**
2444:      * create a new opaque lock token as defined in RFC2518
2445:      *
2446:      * @param  void
2447:      * @return string  new RFC2518 opaque lock token
2448:      */
2449:     function _new_locktoken()
2450:     {
2451:         return "opaquelocktoken:" . ((string)new Horde_Support_Uuid());
2452:     }
2453: 
2454:     // }}}
2455: 
2456:     // {{{ WebDAV If: header parsing
2457: 
2458:     /**
2459:      *
2460:      *
2461:      * @param  string  header string to parse
2462:      * @param  int     current parsing position
2463:      * @return array   next token (type and value)
2464:      */
2465:     function _if_header_lexer($string, &$pos)
2466:     {
2467:         // skip whitespace
2468:         while (ctype_space($string{$pos})) {
2469:             ++$pos;
2470:         }
2471: 
2472:         // already at end of string?
2473:         if (strlen($string) <= $pos) {
2474:             return false;
2475:         }
2476: 
2477:         // get next character
2478:         $c = $string{$pos++};
2479: 
2480:         // now it depends on what we found
2481:         switch ($c) {
2482:         case "<":
2483:             // URIs are enclosed in <...>
2484:             $pos2 = strpos($string, ">", $pos);
2485:             $uri  = substr($string, $pos, $pos2 - $pos);
2486:             $pos  = $pos2 + 1;
2487:             return array("URI", $uri);
2488: 
2489:         case "[":
2490:             //Etags are enclosed in [...]
2491:             if ($string{$pos} == "W") {
2492:                 $type = "ETAG_WEAK";
2493:                 $pos += 2;
2494:             } else {
2495:                 $type = "ETAG_STRONG";
2496:             }
2497:             $pos2 = strpos($string, "]", $pos);
2498:             $etag = substr($string, $pos + 1, $pos2 - $pos - 2);
2499:             $pos  = $pos2 + 1;
2500:             return array($type, $etag);
2501: 
2502:         case "N":
2503:             // "N" indicates negation
2504:             $pos += 2;
2505:             return array("NOT", "Not");
2506: 
2507:         default:
2508:             // anything else is passed verbatim char by char
2509:             return array("CHAR", $c);
2510:         }
2511:     }
2512: 
2513:     /**
2514:      * parse If: header
2515:      *
2516:      * @param  string  header string
2517:      * @return array   URIs and their conditions
2518:      */
2519:     function _if_header_parser($str)
2520:     {
2521:         $pos  = 0;
2522:         $len  = strlen($str);
2523:         $uris = array();
2524: 
2525:         // parser loop
2526:         while ($pos < $len) {
2527:             // get next token
2528:             $token = $this->_if_header_lexer($str, $pos);
2529: 
2530:             // check for URI
2531:             if ($token[0] == "URI") {
2532:                 $uri   = $token[1]; // remember URI
2533:                 $token = $this->_if_header_lexer($str, $pos); // get next token
2534:             } else {
2535:                 $uri = "";
2536:             }
2537: 
2538:             // sanity check
2539:             if ($token[0] != "CHAR" || $token[1] != "(") {
2540:                 return false;
2541:             }
2542: 
2543:             $list  = array();
2544:             $level = 1;
2545:             $not   = "";
2546:             while ($level) {
2547:                 $token = $this->_if_header_lexer($str, $pos);
2548:                 if ($token[0] == "NOT") {
2549:                     $not = "!";
2550:                     continue;
2551:                 }
2552:                 switch ($token[0]) {
2553:                 case "CHAR":
2554:                     switch ($token[1]) {
2555:                     case "(":
2556:                         $level++;
2557:                         break;
2558:                     case ")":
2559:                         $level--;
2560:                         break;
2561:                     default:
2562:                         return false;
2563:                     }
2564:                     break;
2565: 
2566:                 case "URI":
2567:                     $list[] = $not."<$token[1]>";
2568:                     break;
2569: 
2570:                 case "ETAG_WEAK":
2571:                     $list[] = $not."[W/'$token[1]']>";
2572:                     break;
2573: 
2574:                 case "ETAG_STRONG":
2575:                     $list[] = $not."['$token[1]']>";
2576:                     break;
2577: 
2578:                 default:
2579:                     return false;
2580:                 }
2581:                 $not = "";
2582:             }
2583: 
2584:             if (isset($uris[$uri]) && is_array($uris[$uri])) {
2585:                 $uris[$uri] = array_merge($uris[$uri], $list);
2586:             } else {
2587:                 $uris[$uri] = $list;
2588:             }
2589:         }
2590: 
2591:         return $uris;
2592:     }
2593: 
2594:     /**
2595:      * check if conditions from "If:" headers are meat
2596:      *
2597:      * the "If:" header is an extension to HTTP/1.1
2598:      * defined in RFC 2518 section 9.4
2599:      *
2600:      * @param  void
2601:      * @return void
2602:      */
2603:     function _check_if_header_conditions()
2604:     {
2605:         if (isset($this->_SERVER["HTTP_IF"])) {
2606:             $this->_if_header_uris =
2607:                 $this->_if_header_parser($this->_SERVER["HTTP_IF"]);
2608: 
2609:             foreach ($this->_if_header_uris as $uri => $conditions) {
2610:                 if ($uri == "") {
2611:                     $uri = $this->uri;
2612:                 }
2613:                 // all must match
2614:                 $state = true;
2615:                 foreach ($conditions as $condition) {
2616:                     // lock tokens may be free form (RFC2518 6.3)
2617:                     // but if opaquelocktokens are used (RFC2518 6.4)
2618:                     // we have to check the format (litmus tests this)
2619:                     if (!strncmp($condition, "<opaquelocktoken:", strlen("<opaquelocktoken"))) {
2620:                         if (!preg_match('/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/', $condition)) {
2621:                             $this->http_status("423 Locked");
2622:                             return false;
2623:                         }
2624:                     }
2625:                     if (!$this->_check_uri_condition($uri, $condition)) {
2626:                         $this->http_status("412 Precondition failed");
2627:                         $state = false;
2628:                         break;
2629:                     }
2630:                 }
2631: 
2632:                 // any match is ok
2633:                 if ($state == true) {
2634:                     return true;
2635:                 }
2636:             }
2637:             return false;
2638:         }
2639:         return true;
2640:     }
2641: 
2642:     /**
2643:      * Check a single URI condition parsed from an if-header
2644:      *
2645:      * Check a single URI condition parsed from an if-header
2646:      *
2647:      * @abstract
2648:      * @param string $uri URI to check
2649:      * @param string $condition Condition to check for this URI
2650:      * @returns bool Condition check result
2651:      */
2652:     function _check_uri_condition($uri, $condition)
2653:     {
2654:         // not really implemented here,
2655:         // implementations must override
2656: 
2657:         // a lock token can never be from the DAV: scheme
2658:         // litmus uses DAV:no-lock in some tests
2659:         if (!strncmp("<DAV:", $condition, 5)) {
2660:             return false;
2661:         }
2662: 
2663:         return true;
2664:     }
2665: 
2666: 
2667:     /**
2668:      *
2669:      *
2670:      * @param  string  path of resource to check
2671:      * @param  bool    exclusive lock?
2672:      */
2673:     function _check_lock_status($path, $exclusive_only = false)
2674:     {
2675:         // FIXME depth -> ignored for now
2676:         if (method_exists($this, "checkLock")) {
2677:             // is locked?
2678:             $lock = $this->checkLock($path);
2679: 
2680:             // ... and lock is not owned?
2681:             if (is_array($lock) && count($lock)) {
2682:                 // FIXME doesn't check uri restrictions yet
2683:                 if (!isset($this->_SERVER["HTTP_IF"]) || !strstr($this->_SERVER["HTTP_IF"], $lock["token"])) {
2684:                     if (!$exclusive_only || ($lock["scope"] !== "shared"))
2685:                         return false;
2686:                 }
2687:             }
2688:         }
2689:         return true;
2690:     }
2691: 
2692: 
2693:     // }}}
2694: 
2695: 
2696:     /**
2697:      * Generate lockdiscovery reply from checklock() result
2698:      *
2699:      * @param   string  resource path to check
2700:      * @return  string  lockdiscovery response
2701:      */
2702:     function lockdiscovery($path)
2703:     {
2704:         // no lock support without checklock() method
2705:         if (!method_exists($this, "checklock")) {
2706:             return "";
2707:         }
2708: 
2709:         // collect response here
2710:         $activelocks = "";
2711: 
2712:         // get checklock() reply
2713:         $lock = $this->checklock($path);
2714: 
2715:         // generate <activelock> block for returned data
2716:         if (is_array($lock) && count($lock)) {
2717:             // check for 'timeout' or 'expires'
2718:             if (!empty($lock["expires"])) {
2719:                 $timeout = "Second-".($lock["expires"] - time());
2720:             } else if (!empty($lock["timeout"])) {
2721:                 $timeout = "Second-$lock[timeout]";
2722:             } else {
2723:                 $timeout = "Infinite";
2724:             }
2725: 
2726:             // genreate response block
2727:             $activelocks.= "
2728:               <D:activelock>
2729:                <D:lockscope><D:$lock[scope]/></D:lockscope>
2730:                <D:locktype><D:$lock[type]/></D:locktype>
2731:                <D:depth>$lock[depth]</D:depth>
2732:                <D:owner>$lock[owner]</D:owner>
2733:                <D:timeout>$timeout</D:timeout>
2734:                <D:locktoken><D:href>$lock[token]</D:href></D:locktoken>
2735:               </D:activelock>
2736:              ";
2737:         }
2738: 
2739:         // return generated response
2740:         return $activelocks;
2741:     }
2742: 
2743:     /**
2744:      * set HTTP return status and mirror it in a private header
2745:      *
2746:      * @param  string  status code and message
2747:      * @return void
2748:      */
2749:     function http_status($status)
2750:     {
2751:         // simplified success case
2752:         if ($status === true) {
2753:             $status = "200 OK";
2754:         }
2755: 
2756:         // remember status
2757:         $this->_http_status = $status;
2758: 
2759:         // generate HTTP status response
2760:         header("HTTP/1.1 $status");
2761:         header("X-WebDAV-Status: $status", true);
2762:     }
2763: 
2764:     /**
2765:      * private minimalistic version of PHP urlencode()
2766:      *
2767:      * only blanks, percent and XML special chars must be encoded here
2768:      * full urlencode() encoding confuses some clients ...
2769:      *
2770:      * @param  string  URL to encode
2771:      * @return string  encoded URL
2772:      */
2773:     function _urlencode($url)
2774:     {
2775:         return strtr($url, array(" "=>"%20",
2776:                                  "%"=>"%25",
2777:                                  "&"=>"%26",
2778:                                  "<"=>"%3C",
2779:                                  ">"=>"%3E",
2780:                                  ));
2781:     }
2782: 
2783:     /**
2784:      * private version of PHP urldecode
2785:      *
2786:      * not really needed but added for completenes
2787:      *
2788:      * @param  string  URL to decode
2789:      * @return string  decoded URL
2790:      */
2791:     function _urldecode($path)
2792:     {
2793:         return rawurldecode($path);
2794:     }
2795: 
2796:     /**
2797:      * Slashify - make sure path ends in a slash
2798:      *
2799:      * @param   string directory path
2800:      * @returns string directory path wiht trailing slash
2801:      */
2802:     function _slashify($path)
2803:     {
2804:         if ($path[strlen($path)-1] != '/') {
2805:             $path = $path."/";
2806:         }
2807:         return $path;
2808:     }
2809: 
2810:     /**
2811:      * Unslashify - make sure path doesn't in a slash
2812:      *
2813:      * @param   string directory path
2814:      * @returns string directory path wihtout trailing slash
2815:      */
2816:     function _unslashify($path)
2817:     {
2818:         if ($path[strlen($path)-1] == '/') {
2819:             $path = substr($path, 0, strlen($path) -1);
2820:         }
2821:         return $path;
2822:     }
2823: 
2824:     /**
2825:      * Merge two paths, make sure there is exactly one slash between them
2826:      *
2827:      * @param  string  parent path
2828:      * @param  string  child path
2829:      * @return string  merged path
2830:      */
2831:     function _mergePaths($parent, $child)
2832:     {
2833:         if ($child{0} == '/') {
2834:             return $this->_unslashify($parent).$child;
2835:         } else {
2836:             return $this->_slashify($parent).$child;
2837:         }
2838:     }
2839: 
2840:     function _prop2xml($prop)
2841:     {
2842:         $res = array();
2843: 
2844:         // properties from namespaces != "DAV:" or without any namespace
2845:         if ($prop["ns"]) {
2846:             $key = $this->ns_hash[$prop['ns']] . ':' . $prop['name'];
2847:             #$res .= "<" . $this->ns_hash[$prop["ns"]] . ":$prop[name]>";
2848:         } else {
2849:             $key = $prop['name'] . '#xmlns=""';
2850:             #$res .= "<$prop[name] xmlns=\"\">";
2851:         }
2852: 
2853:         // Check for and handle nested properties
2854:         if (is_array($prop['val'] && isset($prop['val']['name']))) {
2855:             // This is a single nested property
2856:             $res[$key] = $this->_prop2xml($prop['val']);
2857:         } elseif (is_array($prop['val'])) {
2858:             // This nested property has multiple values
2859:             foreach ($prop['val'] as $entry) {
2860:                 $res[$key] = $this->_prop2xml($entry);
2861:             }
2862:         } else {
2863:             // This is a simple property value
2864:             $res[$key] = $prop['val'];
2865:         }
2866: 
2867:         return $res;
2868:     }
2869: 
2870:     /**
2871:      * mbstring.func_overload save strlen version: counting the bytes not the chars
2872:      *
2873:      * @param string $str
2874:      * @return int
2875:      */
2876:     function bytes($str)
2877:     {
2878:         static $func_overload;
2879: 
2880:         if (is_null($func_overload))
2881:         {
2882:             $func_overload = @extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0;
2883:         }
2884:         return $func_overload & 2 ? mb_strlen($str,'ascii') : strlen($str);
2885:     }
2886: 
2887: 
2888:     function _parse_propfind($path)
2889:     {
2890:         // success state flag
2891:         $this->parseSuccess = true;
2892: 
2893:         // property storage array
2894:         $this->parseProps = array();
2895: 
2896:         // internal tag depth counter
2897:         $this->parseDepth = 0;
2898: 
2899:         // remember if any input was parsed
2900:         $had_input = false;
2901: 
2902:         // open input stream
2903:         $f_in = fopen($path, "r");
2904:         if (!$f_in) {
2905:             $this->parseSuccess = false;
2906:             return;
2907:         }
2908: 
2909:         // create XML parser
2910:         $xml_parser = xml_parser_create_ns("UTF-8", " ");
2911: 
2912:         // set tag and data handlers
2913:         xml_set_element_handler($xml_parser,
2914:                                 array(&$this, "_startPropinfoElement"),
2915:                                 array(&$this, "_endPropinfoElement"));
2916: 
2917:         // we want a case sensitive parser
2918:         xml_parser_set_option($xml_parser,
2919:                               XML_OPTION_CASE_FOLDING, false);
2920: 
2921: 
2922:         // parse input
2923:         while ($this->parseSuccess && !feof($f_in)) {
2924:             $line = fgets($f_in);
2925:             if (is_string($line)) {
2926:                 $had_input = true;
2927:                 $this->parseSuccess &= xml_parse($xml_parser, $line, false);
2928:             }
2929:         }
2930: 
2931:         // finish parsing
2932:         if ($had_input) {
2933:             $this->parseSuccess &= xml_parse($xml_parser, "", true);
2934:         }
2935: 
2936:         // free parser
2937:         xml_parser_free($xml_parser);
2938: 
2939:         // close input stream
2940:         fclose($f_in);
2941: 
2942:         // if no input was parsed it was a request
2943:         if(!count($this->parseProps)) $this->parseProps = "all"; // default
2944:     }
2945: 
2946: 
2947:     /**
2948:      * start tag handler
2949:      *
2950:      * @access private
2951:      * @param  resource  parser
2952:      * @param  string    tag name
2953:      * @param  array     tag attributes
2954:      */
2955:     function _startPropinfoElement($parser, $name, $attrs)
2956:     {
2957:         // name space handling
2958:         if (strstr($name, " ")) {
2959:             list($ns, $tag) = explode(" ", $name);
2960:             if ($ns == "")
2961:                 $this->parseSuccess = false;
2962:         } else {
2963:             $ns  = "";
2964:             $tag = $name;
2965:         }
2966: 
2967:         // special tags at level 1: <allprop> and <propname>
2968:         if ($this->parseDepth == 1) {
2969:             if ($tag == "allprop")
2970:                 $this->parseProps = "all";
2971: 
2972:             if ($tag == "propname")
2973:                 $this->parseProps = "names";
2974:         }
2975: 
2976:         // requested properties are found at level 2
2977:         if ($this->parseDepth == 2) {
2978:             $prop = array("name" => $tag);
2979:             if ($ns)
2980:                 $prop["xmlns"] = $ns;
2981:             $this->parseProps[] = $prop;
2982:         }
2983: 
2984:         // increment depth count
2985:         $this->parseDepth++;
2986:     }
2987: 
2988: 
2989:     /**
2990:      * end tag handler
2991:      *
2992:      * @access private
2993:      * @param  resource  parser
2994:      * @param  string    tag name
2995:      */
2996:     function _endPropinfoElement($parser, $name)
2997:     {
2998:         // here we only need to decrement the depth count
2999:         $this->parseDepth--;
3000:     }
3001: 
3002:     function _parse_lockinfo($path)
3003:     {
3004:         // we assume success unless problems occur
3005:         $this->parseSuccess = true;
3006: 
3007:         // remember if any input was parsed
3008:         $had_input = false;
3009: 
3010:         // open stream
3011:         $f_in = fopen($path, "r");
3012:         if (!$f_in) {
3013:             $this->parseSuccess = false;
3014:             return;
3015:         }
3016: 
3017:         // create namespace aware parser
3018:         $xml_parser = xml_parser_create_ns("UTF-8", " ");
3019: 
3020:         // set tag and data handlers
3021:         xml_set_element_handler($xml_parser,
3022:                                 array(&$this, "_startLockElement"),
3023:                                 array(&$this, "_endLockElement"));
3024:         xml_set_character_data_handler($xml_parser,
3025:                                        array(&$this, "_lockData"));
3026: 
3027:         // we want a case sensitive parser
3028:         xml_parser_set_option($xml_parser,
3029:                               XML_OPTION_CASE_FOLDING, false);
3030: 
3031:         // parse input
3032:         while ($this->parseSuccess && !feof($f_in)) {
3033:             $line = fgets($f_in);
3034:             if (is_string($line)) {
3035:                 $had_input = true;
3036:                 $this->parseSuccess &= xml_parse($xml_parser, $line, false);
3037:             }
3038:         }
3039: 
3040:         // finish parsing
3041:         if ($had_input) {
3042:             $this->parseSuccess &= xml_parse($xml_parser, "", true);
3043:         }
3044: 
3045:         // check if required tags where found
3046:         $this->parseSuccess &= !empty($this->locktype);
3047:         $this->parseSuccess &= !empty($this->lockscope);
3048: 
3049:         // free parser resource
3050:         xml_parser_free($xml_parser);
3051: 
3052:         // close input stream
3053:         fclose($f_in);
3054:     }
3055: 
3056: 
3057:     /**
3058:      * tag start handler
3059:      *
3060:      * @param  resource  parser
3061:      * @param  string    tag name
3062:      * @param  array     tag attributes
3063:      * @return void
3064:      * @access private
3065:      */
3066:     function _startLockElement($parser, $name, $attrs)
3067:     {
3068:         // namespace handling
3069:         if (strstr($name, " ")) {
3070:             list($ns, $tag) = explode(" ", $name);
3071:         } else {
3072:             $ns  = "";
3073:             $tag = $name;
3074:         }
3075: 
3076: 
3077:         if ($this->collect_owner) {
3078:             // everything within the <owner> tag needs to be collected
3079:             $ns_short = "";
3080:             $ns_attr  = "";
3081:             if ($ns) {
3082:                 if ($ns == "DAV:") {
3083:                     $ns_short = "D:";
3084:                 } else {
3085:                     $ns_attr = " xmlns='$ns'";
3086:                 }
3087:             }
3088:             $this->owner .= "<$ns_short$tag$ns_attr>";
3089:         } else if ($ns == "DAV:") {
3090:             // parse only the essential tags
3091:             switch ($tag) {
3092:             case "write":
3093:                 $this->locktype = $tag;
3094:                 break;
3095:             case "exclusive":
3096:             case "shared":
3097:                 $this->lockscope = $tag;
3098:                 break;
3099:             case "owner":
3100:                 $this->collect_owner = true;
3101:                 break;
3102:             }
3103:         }
3104:     }
3105: 
3106:     /**
3107:      * data handler
3108:      *
3109:      * @param  resource  parser
3110:      * @param  string    data
3111:      * @return void
3112:      * @access private
3113:      */
3114:     function _lockData($parser, $data)
3115:     {
3116:         // only the <owner> tag has data content
3117:         if ($this->collect_owner) {
3118:             $this->owner .= $data;
3119:         }
3120:     }
3121: 
3122:     /**
3123:      * tag end handler
3124:      *
3125:      * @param  resource  parser
3126:      * @param  string    tag name
3127:      * @return void
3128:      * @access private
3129:      */
3130:     function _endLockElement($parser, $name)
3131:     {
3132:         // namespace handling
3133:         if (strstr($name, " ")) {
3134:             list($ns, $tag) = explode(" ", $name);
3135:         } else {
3136:             $ns  = "";
3137:             $tag = $name;
3138:         }
3139: 
3140:         // <owner> finished?
3141:         if (($ns == "DAV:") && ($tag == "owner")) {
3142:             $this->collect_owner = false;
3143:         }
3144: 
3145:         // within <owner> we have to collect everything
3146:         if ($this->collect_owner) {
3147:             $ns_short = "";
3148:             $ns_attr  = "";
3149:             if ($ns) {
3150:                 if ($ns == "DAV:") {
3151:                     $ns_short = "D:";
3152:                 } else {
3153:                     $ns_attr = " xmlns='$ns'";
3154:                 }
3155:             }
3156:             $this->owner .= "</$ns_short$tag$ns_attr>";
3157:         }
3158:     }
3159: 
3160:     function _parse_proppatch($path)
3161:     {
3162:         $this->parseSuccess = true;
3163: 
3164:         $this->parseDepth = 0;
3165:         $this->parseProps = array();
3166:         $had_input = false;
3167: 
3168:         $f_in = fopen($path, "r");
3169:         if (!$f_in) {
3170:             $this->parseSuccess = false;
3171:             return;
3172:         }
3173: 
3174:         $xml_parser = xml_parser_create_ns("UTF-8", " ");
3175: 
3176:         xml_set_element_handler($xml_parser,
3177:                                 array(&$this, "_startProppatchElement"),
3178:                                 array(&$this, "_endProppatchElement"));
3179: 
3180:         xml_set_character_data_handler($xml_parser,
3181:                                        array(&$this, "_proppatchData"));
3182: 
3183:         xml_parser_set_option($xml_parser,
3184:                               XML_OPTION_CASE_FOLDING, false);
3185: 
3186:         while($this->parseSuccess && !feof($f_in)) {
3187:             $line = fgets($f_in);
3188:             if (is_string($line)) {
3189:                 $had_input = true;
3190:                 $this->parseSuccess &= xml_parse($xml_parser, $line, false);
3191:             }
3192:         }
3193: 
3194:         if($had_input) {
3195:             $this->parseSuccess &= xml_parse($xml_parser, "", true);
3196:         }
3197: 
3198:         xml_parser_free($xml_parser);
3199: 
3200:         fclose($f_in);
3201:     }
3202: 
3203:     /**
3204:      * tag start handler
3205:      *
3206:      * @param  resource  parser
3207:      * @param  string    tag name
3208:      * @param  array     tag attributes
3209:      * @return void
3210:      * @access private
3211:      */
3212:     function _startProppatchElement($parser, $name, $attrs)
3213:     {
3214:         if (strstr($name, " ")) {
3215:             list($ns, $tag) = explode(" ", $name);
3216:             if ($ns == "")
3217:                 $this->parseSuccess = false;
3218:         } else {
3219:             $ns = "";
3220:             $tag = $name;
3221:         }
3222: 
3223:         if ($this->parseDepth == 1) {
3224:             $this->mode = $tag;
3225:         }
3226: 
3227:         if ($this->parseDepth == 3) {
3228:             $prop = array("name" => $tag);
3229:             $this->current = array("name" => $tag, "ns" => $ns, "status"=> 200);
3230:             if ($this->mode == "set") {
3231:                 $this->current["val"] = "";     // default set val
3232:             }
3233:         }
3234: 
3235:         if ($this->parseDepth >= 4) {
3236:             $this->current["val"] .= "<$tag";
3237:             if (isset($attr)) {
3238:                 foreach ($attr as $key => $val) {
3239:                     $this->current["val"] .= ' '.$key.'="'.str_replace('"','&quot;', $val).'"';
3240:                 }
3241:             }
3242:             $this->current["val"] .= ">";
3243:         }
3244: 
3245: 
3246: 
3247:         $this->parseDepth++;
3248:     }
3249: 
3250:     /**
3251:      * tag end handler
3252:      *
3253:      * @param  resource  parser
3254:      * @param  string    tag name
3255:      * @return void
3256:      * @access private
3257:      */
3258:     function _endProppatchElement($parser, $name)
3259:     {
3260:         if (strstr($name, " ")) {
3261:             list($ns, $tag) = explode(" ", $name);
3262:             if ($ns == "")
3263:                 $this->parseSuccess = false;
3264:         } else {
3265:             $ns = "";
3266:             $tag = $name;
3267:         }
3268: 
3269:         $this->parseDepth--;
3270: 
3271:         if ($this->parseDepth >= 4) {
3272:             $this->current["val"] .= "</$tag>";
3273:         }
3274: 
3275:         if ($this->parseDepth == 3) {
3276:             if (isset($this->current)) {
3277:                 $this->parseProps[] = $this->current;
3278:                 unset($this->current);
3279:             }
3280:         }
3281:     }
3282: 
3283:     /**
3284:      * input data handler
3285:      *
3286:      * @param  resource  parser
3287:      * @param  string    data
3288:      * @return void
3289:      * @access private
3290:      */
3291:     function _proppatchData($parser, $data)
3292:     {
3293:         if (isset($this->current)) {
3294:             $this->current["val"] .= $data;
3295:         }
3296:     }
3297: 
3298: }
3299: 
API documentation generated by ApiGen