success = true; $this->depth = 0; $this->props = array(); $had_input = false; $f_in = fopen($path, "r"); if (!$f_in) { $this->success = false; return; } $xml_parser = xml_parser_create_ns("UTF-8", " "); xml_set_element_handler($xml_parser, array(&$this, "_startElement"), array(&$this, "_endElement")); xml_set_character_data_handler($xml_parser, array(&$this, "_data")); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, false); while($this->success && !feof($f_in)) { $line = fgets($f_in); if (is_string($line)) { $had_input = true; $this->success &= xml_parse($xml_parser, $line, false); } } if($had_input) { $this->success &= xml_parse($xml_parser, "", true); } xml_parser_free($xml_parser); fclose($f_in); } /** * tag start handler * * @param resource parser * @param string tag name * @param array tag attributes * @return void * @access private */ function _startElement($parser, $name, $attrs) { if (strstr($name, " ")) { list($ns, $tag) = explode(" ", $name); if ($ns == "") $this->success = false; } else { $ns = ""; $tag = $name; } if ($this->depth == 1) { if ($tag == 'sync-token') { // Parse sync token data $prop = array("name" => $tag); $this->current = array("name" => $tag, "ns" => $ns); $this->current["val"] = ""; // default set val } else if ($tag == "prop") { // Parse requested properties $this->current = array(); } else if ($tag == "allprop") { $this->props = "all"; } else if ($tag == "propname") { $this->props = "names"; } } else if ($this->depth == 2) { if (isset($this->current)) { error_log("in if..."); $prop = array("name" => $tag); if ($ns) $prop["xmlns"] = $ns; $this->props[] = $prop; } } $this->depth++; } /** * tag end handler * * @param resource parser * @param string tag name * @return void * @access private */ function _endElement($parser, $name) { if (strstr($name, " ")) { list($ns, $tag) = explode(" ", $name); if ($ns == "") $this->success = false; } else { $ns = ""; $tag = $name; } $this->depth--; if ($this->depth == 1) { if (isset($this->current)) { if ($tag == 'sync-token') { $this->sync_token = $this->current['val']; } unset($this->current); } } } /** * input data handler * * @param resource parser * @param string data * @return void * @access private */ function _data($parser, $data) { if (isset($this->current)) { // Concatenate data when one of the parents requested it by creating the 'current' object $this->current["val"] .= $data; } } } /** * Access Zarafa contacts via WebDAV ('CardDAV') */ class HTTP_WebDAV_Server_Zarafa extends HTTP_WebDAV_Server { var $extension = ".vcf"; var $mime = "text/x-vcard"; var $server = "http://localhost:236/zarafa"; var $zarafa = false; var $specialprops = array( "fileas" => "PT_STRING8:{00062004-0000-0000-C000-000000000046}:0x8005", "email1" => "PT_STRING8:{00062004-0000-0000-C000-000000000046}:0x8083", "business_street" => "PT_STRING8:{00062004-0000-0000-C000-000000000046}:0x8045", "business_postcode" => "PT_STRING8:{00062004-0000-0000-C000-000000000046}:0x8048", "business_city" => "PT_STRING8:{00062004-0000-0000-C000-000000000046}:0x8046", "business_state" => "PT_STRING8:{00062004-0000-0000-C000-000000000046}:0x8047", "business_country" => "PT_STRING8:{00062004-0000-0000-C000-000000000046}:0x8049" ); var $etag_salt = "pepper ;-)"; /** * Serve a webdav request */ function ServeRequest() { $this->http_auth_realm = "Zarafa CardDAV"; // let the base class do all the work parent::ServeRequest(); } /** * Check authentication * @param string HTTP Authentication type (Basic, Digest, ...) * @param string Username * @param string Password * @return bool true on successful authentication */ function check_auth($type, $user, $pass) { $user = str_replace('%', '@', $user); // This fixes a logon problem with Evolution, login with user%domain instead of user@domain which seems to confuse evolution... $user = str_replace('$', '@', $user); // Mac OS X Address Book App fix $session = mapi_logon_zarafa($user, $pass, $this->server); if (false != $session) { $this->zarafa = $this->open_zarafa($session); $this->zarafa['user'] = $user; $this->specialprops = getPropIdsFromStrings($this->zarafa["store"], $this->specialprops); return true; } return false; } /** * PROPFIND method handler * * @param array general parameter passing array * @param array return array for file properties * @return bool true on success */ function PROPFIND(&$options, &$files) { $store = $this->zarafa["store"]; $contacts = $this->zarafa["contacts"]; $files["files"] = array(); if ($options["path"] == "/") { $global_etag = ""; $rootOnly = !(($options["depth"] == "infinity") || ($options["depth"] > 0)); foreach ($contacts as $contact) { $message = mapi_msgstore_openentry($store, $contact[PR_ENTRYID]); $props = mapi_getprops($message); if (!$rootOnly) // If the requested depth was 0, we only compute the global etag for the root $files["files"][] = $this->contactinfo($props); $etag = $this->get_etag($props); $global_etag = $global_etag.$etag; } $global_etag = sha1($global_etag); $info["path"] = $this->zarafa["root"]; $info["props"] = array(); $info["props"][] = $this->mkprop("resourcetype", ""); $info["props"][] = $this->mkprop("displayname", "ROOT"); $info["props"][] = $this->mkprop("getetag", '"'.$global_etag.'"'); $info["props"][] = $this->mkprop("http://calendarserver.org/ns/", "getctag", $global_etag); $info["props"][] = $this->mkprop("supported-report-set", "". ""); // We don't actually support this, do we...? $files["files"][] = $info; } else { $contactprops = $this->get_contact($options["path"]); if ($contactprops) { $info = $this->contactinfo($contactprops); $files["files"][] = $info; $global_etag = $this->get_etag($contactprops); } } header("ETAG: \"".$global_etag."\""); return true; } /** * REPORT request handler * * @param options Request options as parsed by wrapper * @param files Files (i.e. sync-responses). This should be a changeset since the version the client reported as last known state, but we currently only support full resyncs. * @return True on success. */ function REPORT(&$options, &$files) { $store = $this->zarafa["store"]; $contacts = $this->zarafa["contacts"]; if ($options['sync-token'] != '') { /** * We don't keep the sync states for each client, thus we enforce a full resync * even if an older state (known by the client) is specified in the request. */ $options['sync-token-refresh'] = true; return false; } $files["files"] = array(); if ($options["path"] == "/") { $global_etag = ""; foreach ($contacts as $contact) { $message = mapi_msgstore_openentry($store, $contact[PR_ENTRYID]); $props = mapi_getprops($message); $files["files"][] = $this->contactinfo($props); $etag = $this->get_etag($props); $global_etag = $global_etag.$etag; } $global_etag = sha1($global_etag); } else { $options['path-not-supported'] = true; return false; } $options['sync-token'] = $global_etag; header("ETAG: \"".$global_etag."\""); return true; } /** * get_contact * * @param string Path to the contact's vcf file. * @return array Array of contact properties or false if not found. */ function get_contact($path) { foreach ($this->zarafa["contacts"] as $contact) { $message = mapi_msgstore_openentry($this->zarafa["store"], $contact[PR_ENTRYID]); $props = mapi_getprops($message); if ($this->zarafa["root"].sha1($props[PR_DISPLAY_NAME]).$this->extension == $path) { return $props; } } return false; } /** * open_zarafa * * @param descriptor An open Zarafa session id. * @return array Array with session and store information. */ function open_zarafa($session) { $ret = array("root" => "/"); $storesTable = mapi_getmsgstorestable($session); $stores = mapi_table_queryallrows($storesTable, array(PR_ENTRYID, PR_MDB_PROVIDER)); for($i=0;$isetVersion('2.1'); $charset = 'UTF-8'; //// GENERAL INFORMATION $vcard->setName( $this->toUTF8(isset($contactprops[PR_SURNAME]) ? $contactprops[PR_SURNAME] : ''), $this->toUTF8(isset($contactprops[PR_GIVEN_NAME]) ? $contactprops[PR_GIVEN_NAME] : ''), $this->toUTF8(isset($contactprops[PR_MIDDLE_NAME]) ? $contactprops[PR_MIDDLE_NAME] : ''), $this->toUTF8(isset($contactprops[PR_DISPLAY_NAME_PREFIX]) ? $contactprops[PR_DISPLAY_NAME_PREFIX] : ''), '' // Suffix ); $vcard->addParam('CHARSET',$charset); $vcard->setFormattedName($this->toUTF8(isset($contactprops[PR_DISPLAY_NAME]) ? $contactprops[PR_DISPLAY_NAME] : '')); $vcard->addParam('CHARSET',$charset); if (isset($contactprops[PR_BIRTHDAY])) $vcard->setBirthday ($this->toUTF8(date('Y-m-d', $contactprops[PR_BIRTHDAY]))); if (isset($contactprops[PR_PROFESSION])) $vcard->setRole ($this->toUTF8($contactprops[PR_PROFESSION])); if (isset($contactprops[PR_NICKNAME])) $vcard->addNickname ($this->toUTF8($contactprops[PR_NICKNAME])); if (isset($contactprops[PR_COMPANY_NAME])) $vcard->addOrganization ($this->toUTF8($contactprops[PR_COMPANY_NAME])); //$this->toUTF8($contactprops[PR_DEPARTMENT_NAME] // how to append this as outlook does and prevent escaping of ';' ?? if (isset($contactprops[PR_TITLE])) $vcard->setTitle ($this->toUTF8($contactprops[PR_TITLE])); if (isset($this->specialprops["email1"]) && isset($contactprops[$this->specialprops["email1"]])) $vcard->addEmail ($this->toUTF8($contactprops[$this->specialprops["email1"]])); if (isset($contactprops[PR_BUSINESS_HOME_PAGE])) { $vcard->setUrl ($this->toUTF8($contactprops[PR_BUSINESS_HOME_PAGE])); $vcard->addParam('TYPE','WORK'); } if (isset($contactprops[PR_COMMENT])) $vcard->setNote ($this->toUTF8($contactprops[PR_COMMENT])); //// HOME ADDRESS $v1 = $v2 = $v3 = $v4 = $v5 = $v6 = ''; if (isset($contactprops[PR_HOME_ADDRESS_POST_OFFICE_BOX])) $v1 = $contactprops[PR_HOME_ADDRESS_POST_OFFICE_BOX]; if (isset($contactprops[PR_HOME_ADDRESS_STREET])) $v2 = $contactprops[PR_HOME_ADDRESS_STREET]; if (isset($contactprops[PR_HOME_ADDRESS_CITY])) $v3 = $contactprops[PR_HOME_ADDRESS_CITY]; if (isset($contactprops[PR_HOME_ADDRESS_STATE_OR_PROVINCE])) $v4 = $contactprops[PR_HOME_ADDRESS_STATE_OR_PROVINCE]; if (isset($contactprops[PR_HOME_ADDRESS_POSTAL_CODE])) $v5 = $contactprops[PR_HOME_ADDRESS_POSTAL_CODE]; if (isset($contactprops[PR_HOME_ADDRESS_COUNTRY])) $v6 = $contactprops[PR_HOME_ADDRESS_COUNTRY]; if ($v1!='' || $v2!='' || $v3!='' || $v4!='' || $v5!='' || $v6!='') { $vcard->addAddress( $this->toUTF8($v1), '', // extended address $this->toUTF8($v2), $this->toUTF8($v3), $this->toUTF8($v4), $this->toUTF8($v5), $this->toUTF8($v6)); $vcard->addParam('TYPE', 'HOME'); } //// WORK ADDRESS $v1 = $v2 = $v3 = $v4 = $v5 = $v6 = ''; // business post office pox (where to get this from?) if (isset($this->specialprops["business_street"]) && isset($contactprops[$this->specialprops["business_street"]])) $v2 = $contactprops[$this->specialprops["business_street"]]; if (isset($this->specialprops["business_city"]) && isset($contactprops[$this->specialprops["business_city"]])) $v3 = $contactprops[$this->specialprops["business_city"]]; if (isset($this->specialprops["business_state"]) && isset($contactprops[$this->specialprops["business_state"]])) $v4 = $contactprops[$this->specialprops["business_state"]]; if (isset($this->specialprops["business_postcode"]) && isset($contactprops[$this->specialprops["business_postcode"]])) $v5 = $contactprops[$this->specialprops["business_postcode"]]; if (isset($this->specialprops["business_country"]) && isset($contactprops[$this->specialprops["business_country"]])) $v6 = $contactprops[$this->specialprops["business_country"]]; if ($v1!='' || $v2!='' || $v3!='' || $v4!='' || $v5!='' || $v6!='') { $vcard->addAddress( $this->toUTF8($v1), '', // extended address $this->toUTF8($v2), $this->toUTF8($v3), $this->toUTF8($v4), $this->toUTF8($v5), $this->toUTF8($v6)); $vcard->addParam('TYPE', 'WORK'); } //// PHONE NUMBERS if (isset($contactprops[PR_HOME_TELEPHONE_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_HOME_TELEPHONE_NUMBER])); $vcard->addParam('TYPE', 'HOME'); } if (isset($contactprops[PR_BUSINESS_TELEPHONE_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_BUSINESS_TELEPHONE_NUMBER])); $vcard->addParam('TYPE', 'WORK'); } if (isset($contactprops[PR_HOME2_TELEPHONE_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_HOME2_TELEPHONE_NUMBER])); $vcard->addParam('TYPE', 'HOME'); } if (isset($contactprops[PR_BUSINESS2_TELEPHONE_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_BUSINESS2_TELEPHONE_NUMBER])); $vcard->addParam('TYPE', 'WORK'); } if (isset($contactprops[PR_MOBILE_TELEPHONE_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_MOBILE_TELEPHONE_NUMBER])); $vcard->addParam('TYPE', 'CELL'); } if (isset($contactprops[PR_HOME_FAX_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_HOME_FAX_NUMBER])); $vcard->addParam('TYPE', 'HOME'); $vcard->addParam('TYPE','FAX'); } if (isset($contactprops[PR_BUSINESS_FAX_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_BUSINESS_FAX_NUMBER])); $vcard->addParam('TYPE', 'WORK'); $vcard->addParam('TYPE','FAX'); } if (isset($contactprops[PR_OTHER_TELEPHONE_NUMBER])) { $vcard->addTelephone ($this->toUTF8($contactprops[PR_OTHER_TELEPHONE_NUMBER])); } return $vcard; } /** * toUTF8 * * @param string A string to encode. * @return string Encoded string. */ function toUTF8($str) { // ZCP 7 supports unicode... if ($this->zarafa["unicode_store"] == true) return $str; // END ZCP 7 supports unicode... return utf8_encode($str); } /** * Get properties for a single file/resource * * @param string resource path * @return array resource properties */ function contactinfo($contactprops) { // create result array $info = array(); $name = $contactprops[PR_DISPLAY_NAME]; $info["path"] = $this->zarafa["root"].sha1($name).$this->extension; $info["props"] = array(); // no special beautified displayname here ... $info["props"][] = $this->mkprop("displayname", $this->toUTF8($name)); $info["props"][] = $this->mkprop("resourcetype", ""); $info["props"][] = $this->mkprop("getcontenttype", $this->mime); $etag = $this->get_etag($contactprops); $info["props"][] = $this->mkprop("getetag", '"'.$etag.'"'); $info["status"] = "HTTP/1.1 201 Created"; return $info; } /** * get_etag * * @param array Contact properties. * @return string Unique Etag for contact object. */ function get_etag($props) { return sha1($this->etag_salt.$props[PR_LAST_MODIFICATION_TIME].$props[PR_DISPLAY_NAME]); } /** * HEAD method handler * * @param array parameter passing array * @return bool true on success */ function HEAD(&$options) { return false; } /** * GET method handler * * @param array parameter passing array * @return bool true on success */ function GET(&$options) { if ($options["path"] == "/") { $store = $this->zarafa["store"]; $contacts = $this->zarafa["contacts"]; $files["files"] = array(); $global_etag = ""; $html_content = "Zarafa Contacts for ".$this->zarafa['user']."
"; foreach ($contacts as $contact) { $message = mapi_msgstore_openentry($store, $contact[PR_ENTRYID]); $props = mapi_getprops($message); $name = $props[PR_DISPLAY_NAME]; $html_content .= "extension."\">".$name."
\n"; } $html_content .= ""; $options['mimetype'] = "text/html"; $options['data'] = $html_content; return true; } $contactprops = $this->get_contact($options["path"]); if ($contactprops === false) return false; $options['mimetype'] = $this->mime; $vcard = $this->build_vcard($contactprops); $vcard = $vcard->fetch(); $etag = $this->get_etag($contactprops); $options['data'] = $vcard; header("ETAG: \"".$etag."\""); return true; } /** * HTTP REPORT Method Wrapper */ function http_REPORT() { $options = Array(); $files = Array(); $options["path"] = $this->path; // search depth from header (default is "infinity) if (isset($this->_SERVER['HTTP_DEPTH'])) { $options["depth"] = $this->_SERVER["HTTP_DEPTH"]; } else { $options["depth"] = "infinity"; } // analyze request payload $propinfo = new _parse_report("php://input"); if (!$propinfo->success) { $this->http_status("400 Error"); return; } $options['props'] = $propinfo->props; error_log(print_r($options['props'],true)); $options['sync-token'] = $propinfo->sync_token; // call user handler if (!$this->REPORT($options, $files)) { $files = array("files" => array()); if ($options['sync-token-refresh']) { // Client specified a sync-token. We do not allow this. // See http://tools.ietf.org/html/draft-daboo-carddav-01#section-2.3 for error specification. //$this->http_status("412 Precondition Failed"); $this->http_status("403 Forbidden"); //$this->http_status("409 Conflict"); header('Content-Type: text/xml; charset="utf-8"'); echo "\n"; echo "\n"; } elseif ($options['path-not-supported']) { // REPORT on root path / $this->http_status("404 Not Found"); } else { $this->http_status("400 Error"); } return; } // collect namespaces here $ns_hash = array(); // Microsoft Clients need this special namespace for date and time values $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\""; // now we loop over all returned file entries foreach ($files["files"] as $filekey => $file) { // nothing to do if no properties were returend for a file if (!isset($file["props"]) || !is_array($file["props"])) { continue; } // now loop over all returned properties foreach ($file["props"] as $key => $prop) { // as a convenience feature we do not require that user handlers // restrict returned properties to the requested ones // here we strip all unrequested entries out of the response switch($options['props']) { case "all": // nothing to remove break; case "names": // only the names of all existing properties were requested // so we remove all values unset($files["files"][$filekey]["props"][$key]["val"]); break; default: $found = false; // search property name in requested properties foreach ((array)$options["props"] as $reqprop) { if (!isset($reqprop["xmlns"])) { $reqprop["xmlns"] = ""; } if ( $reqprop["name"] == $prop["name"] && $reqprop["xmlns"] == $prop["ns"]) { $found = true; break; } } // unset property and continue with next one if not found/requested if (!$found) { $files["files"][$filekey]["props"][$key]=""; continue(2); } break; } // namespace handling if (empty($prop["ns"])) continue; // no namespace $ns = $prop["ns"]; if ($ns == "DAV:") continue; // default namespace if (isset($ns_hash[$ns])) continue; // already known // register namespace $ns_name = "ns".(count($ns_hash) + 1); $ns_hash[$ns] = $ns_name; $ns_defs .= " xmlns:$ns_name=\"$ns\""; } // we also need to add empty entries for properties that were requested // but for which no values where returned by the user handler if (is_array($options['props'])) { foreach ($options["props"] as $reqprop) { if ($reqprop['name']=="") continue; // skip empty entries $found = false; if (!isset($reqprop["xmlns"])) { $reqprop["xmlns"] = ""; } // check if property exists in result foreach ($file["props"] as $prop) { if ( $reqprop["name"] == $prop["name"] && $reqprop["xmlns"] == $prop["ns"]) { $found = true; break; } } if (!$found) { if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") { // lockdiscovery is handled by the base class $files["files"][$filekey]["props"][] = $this->mkprop("DAV:", "lockdiscovery", $this->lockdiscovery($files["files"][$filekey]['path'])); } else { // add empty value for this property $files["files"][$filekey]["noprops"][] = $this->mkprop($reqprop["xmlns"], $reqprop["name"], ""); // register property namespace if not known yet if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) { $ns_name = "ns".(count($ns_hash) + 1); $ns_hash[$reqprop["xmlns"]] = $ns_name; $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\""; } } } } } } // now we generate the reply header ... $this->http_status("207 Multi-Status"); header('Content-Type: text/xml; charset="utf-8"'); // ... and payload echo "\n"; echo "\n"; foreach ($files["files"] as $file) { // ignore empty or incomplete entries if (!is_array($file) || empty($file) || !isset($file["path"])) continue; $path = $file['path']; if (!is_string($path) || $path==="") continue; echo " \n"; /* TODO right now the user implementation has to make sure collections end in a slash, this should be done in here by checking the resource attribute */ $href = $this->_mergePaths($this->_SERVER['SCRIPT_NAME'], $path); /* minimal urlencoding is needed for the resource path */ $href = $this->_urlencode($href); echo " $href\n"; if (isset($file["status"])) { echo " ".$file["status"].""; } // report all found properties and their values (if any) if (isset($file["props"]) && is_array($file["props"])) { echo " \n"; echo " \n"; foreach ($file["props"] as $key => $prop) { if (!is_array($prop)) continue; if (!isset($prop["name"])) continue; if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) { // empty properties (cannot use empty() for check as "0" is a legal value here) if ($prop["ns"]=="DAV:") { echo " <$prop[name]/>\n"; } else if (!empty($prop["ns"])) { echo " <".$ns_hash[$prop["ns"]].":$prop[name]/>\n"; } else { echo " <$prop[name] xmlns=\"\"/>"; } } else if ($prop["ns"] == "DAV:") { // some WebDAV properties need special treatment switch ($prop["name"]) { case "getcontenttype": echo " <$prop[name]>$prop[val]\n"; break; case "getetag": echo " <$prop[name]>" . $this->_prop_encode($prop['val']) . "\n"; break; default: echo " <$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; break; } } else { // properties from namespaces != "DAV:" or without any namespace if ($prop["ns"]) { echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; } else { echo " <$prop[name] xmlns=\"\">" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; } } } echo " \n"; echo " HTTP/1.1 200 OK\n"; echo " \n"; } // now report all properties requested but not found if (isset($file["noprops"])) { echo " \n"; echo " \n"; foreach ($file["noprops"] as $key => $prop) { if ($prop["ns"] == "DAV:") { echo " <$prop[name]/>\n"; } else if ($prop["ns"] == "") { echo " <$prop[name] xmlns=\"\"/>\n"; } else { echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n"; } } echo " \n"; echo " HTTP/1.1 404 Not Found\n"; echo " \n"; } echo " \n"; } if (isset($options['sync-token'])) { echo "".$options['sync-token'].""; } echo "\n"; } /** * PROPFIND method handler * * @brief This is almost an identical copy of the http_PROPFIND() function from Server.php of our anchestor class. Changes are marked with '#MOD' * @param void * @return void */ function http_PROPFIND() { $options = Array(); $files = Array(); $options["path"] = $this->path; // search depth from header (default is "infinity) if (isset($this->_SERVER['HTTP_DEPTH'])) { $options["depth"] = $this->_SERVER["HTTP_DEPTH"]; } else { $options["depth"] = "infinity"; } // analyze request payload $propinfo = new _parse_propfind("php://input"); if (!$propinfo->success) { $this->http_status("400 Error"); return; } $options['props'] = $propinfo->props; // call user handler if (!$this->PROPFIND($options, $files)) { $files = array("files" => array()); if (method_exists($this, "checkLock")) { // is locked? $lock = $this->checkLock($this->path); if (is_array($lock) && count($lock)) { $created = isset($lock['created']) ? $lock['created'] : time(); $modified = isset($lock['modified']) ? $lock['modified'] : time(); $files['files'][] = array("path" => $this->_slashify($this->path), "props" => array($this->mkprop("displayname", $this->path), $this->mkprop("creationdate", $created), $this->mkprop("getlastmodified", $modified), $this->mkprop("resourcetype", ""), $this->mkprop("getcontenttype", ""), $this->mkprop("getcontentlength", 0)) ); } } if (empty($files['files'])) { $this->http_status("404 Not Found"); return; } } // collect namespaces here $ns_hash = array(); // Microsoft Clients need this special namespace for date and time values $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\""; // now we loop over all returned file entries foreach ($files["files"] as $filekey => $file) { // nothing to do if no properties were returend for a file if (!isset($file["props"]) || !is_array($file["props"])) { continue; } // now loop over all returned properties foreach ($file["props"] as $key => $prop) { // as a convenience feature we do not require that user handlers // restrict returned properties to the requested ones // here we strip all unrequested entries out of the response switch($options['props']) { case "all": // nothing to remove break; case "names": // only the names of all existing properties were requested // so we remove all values unset($files["files"][$filekey]["props"][$key]["val"]); break; default: $found = false; // search property name in requested properties foreach ((array)$options["props"] as $reqprop) { if (!isset($reqprop["xmlns"])) { $reqprop["xmlns"] = ""; } if ( $reqprop["name"] == $prop["name"] && $reqprop["xmlns"] == $prop["ns"]) { $found = true; break; } } // unset property and continue with next one if not found/requested if (!$found) { $files["files"][$filekey]["props"][$key]=""; continue(2); } break; } // namespace handling if (empty($prop["ns"])) continue; // no namespace $ns = $prop["ns"]; if ($ns == "DAV:") continue; // default namespace if (isset($ns_hash[$ns])) continue; // already known // register namespace $ns_name = "ns".(count($ns_hash) + 1); $ns_hash[$ns] = $ns_name; $ns_defs .= " xmlns:$ns_name=\"$ns\""; } // we also need to add empty entries for properties that were requested // but for which no values where returned by the user handler if (is_array($options['props'])) { foreach ($options["props"] as $reqprop) { if ($reqprop['name']=="") continue; // skip empty entries $found = false; if (!isset($reqprop["xmlns"])) { $reqprop["xmlns"] = ""; } // check if property exists in result foreach ($file["props"] as $prop) { if ( $reqprop["name"] == $prop["name"] && $reqprop["xmlns"] == $prop["ns"]) { $found = true; break; } } if (!$found) { if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") { // lockdiscovery is handled by the base class $files["files"][$filekey]["props"][] = $this->mkprop("DAV:", "lockdiscovery", $this->lockdiscovery($files["files"][$filekey]['path'])); } else { // add empty value for this property $files["files"][$filekey]["noprops"][] = $this->mkprop($reqprop["xmlns"], $reqprop["name"], ""); // register property namespace if not known yet if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) { $ns_name = "ns".(count($ns_hash) + 1); $ns_hash[$reqprop["xmlns"]] = $ns_name; $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\""; } } } } } } // now we generate the reply header ... $this->http_status("207 Multi-Status"); header('Content-Type: text/xml; charset="utf-8"'); // ... and payload echo "\n"; echo "\n"; foreach ($files["files"] as $file) { // ignore empty or incomplete entries if (!is_array($file) || empty($file) || !isset($file["path"])) continue; $path = $file['path']; if (!is_string($path) || $path==="") continue; echo " \n"; /* TODO right now the user implementation has to make sure collections end in a slash, this should be done in here by checking the resource attribute */ $href = $this->_mergePaths($this->_SERVER['SCRIPT_NAME'], $path); /* minimal urlencoding is needed for the resource path */ $href = $this->_urlencode($href); echo " $href\n"; // report all found properties and their values (if any) if (isset($file["props"]) && is_array($file["props"])) { echo " \n"; echo " \n"; foreach ($file["props"] as $key => $prop) { if (!is_array($prop)) continue; if (!isset($prop["name"])) continue; if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) { // empty properties (cannot use empty() for check as "0" is a legal value here) if ($prop["ns"]=="DAV:") { echo " <$prop[name]/>\n"; } else if (!empty($prop["ns"])) { echo " <".$ns_hash[$prop["ns"]].":$prop[name]/>\n"; } else { echo " <$prop[name] xmlns=\"\"/>"; } } else if ($prop["ns"] == "DAV:") { // some WebDAV properties need special treatment switch ($prop["name"]) { case "creationdate": echo " " . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val']) . "\n"; break; case "getlastmodified": echo " " . gmdate("D, d M Y H:i:s ", $prop['val']) . "GMT\n"; break; case "resourcetype": echo " $prop[val]\n"; break; case "supportedlock": echo " $prop[val]\n"; break; case "lockdiscovery": echo " \n"; echo $prop["val"]; echo " \n"; break; // the following are non-standard Microsoft extensions to the DAV namespace case "lastaccessed": echo " " . gmdate("D, d M Y H:i:s ", $prop['val']) . "GMT\n"; break; case "ishidden": echo " " . is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false') . "\n"; break; // >> #MOD case "getetag": echo " <$prop[name]>" . $this->_prop_encode($prop['val']) . "\n"; break; case "supported-report-set": echo " <$prop[name]>" . $prop['val'] . "\n"; break; // << #MOD default: echo " <$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; break; } } else { // properties from namespaces != "DAV:" or without any namespace if ($prop["ns"]) { echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; } else { echo " <$prop[name] xmlns=\"\">" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; } } } echo " \n"; echo " HTTP/1.1 200 OK\n"; echo " \n"; } // now report all properties requested but not found if (isset($file["noprops"])) { echo " \n"; echo " \n"; foreach ($file["noprops"] as $key => $prop) { if ($prop["ns"] == "DAV:") { echo " <$prop[name]/>\n"; } else if ($prop["ns"] == "") { echo " <$prop[name] xmlns=\"\"/>\n"; } else { echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n"; } } echo " \n"; echo " HTTP/1.1 404 Not Found\n"; echo " \n"; } echo " \n"; } echo "\n"; } } // Create a server object and handle the request. $carddav = new HTTP_WebDAV_Server_Zarafa(); $carddav->ServeRequest(); ?>