<?php /** * EGroupware: iSchedule server * * @link http://www.egroupware.org * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage groupdav * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> * @copyright (c) 2012-13 by Ralf Becker <RalfBecker-AT-outdoor-training.de> * @version $Id$ */ /** * iSchedule server: serverside of iSchedule * * @link https://tools.ietf.org/html/draft-desruisseaux-ischedule-03 iSchedule draft from 2013-01-22 * * groupdav get's extended here to get it's logging, should separate that out ... */ class ischedule_server extends groupdav { /** * iSchedule xml namespace */ const ISCHEDULE = 'urn:ietf:params:xml:ns:ischedule'; /** * Own iSchedule version */ const VERSION = '1.0'; /** * Supported versions for capablities * * Might be more then current version above * * @var array */ static $supported_versions = array(self::VERSION); /** * Required headers in DKIM signature (DKIM-Signature is always a required header!) */ const REQUIRED_DKIM_HEADERS = 'iSchedule-Version:Content-Type:Originator:Recipient'; /** * Constructor */ public function __construct() { // install our own exception handler sending exceptions as http status set_exception_handler(array(__CLASS__, 'exception_handler')); self::$instance = $this; } /** * Serve an iSchedule request */ public function ServeRequest() { self::$log_level = $GLOBALS['egw_info']['user']['preferences']['groupdav']['debug_level']; self::$log_level = 'f'; if (self::$log_level === 'r' || self::$log_level === 'f' || $this->debug) { self::$request_starttime = microtime(true); $this->store_request = true; ob_start(); } // get raw request body $this->request = file_get_contents('php://input'); switch($_SERVER['REQUEST_METHOD']) { case 'GET': $this->get(); break; case 'POST': $this->post(); break; default: error_log(__METHOD__."() invalid iSchedule request using {$_SERVER['REQUEST_METHOD']}!"); header("HTTP/1.1 400 Bad Request"); } if (self::$request_starttime) self::log_request(); } /** * List of supported components and methods * * @var array */ static $supported_components = array( 'VFREEBUSY' => array('REQUEST'), 'VEVENT' => array('REQUEST', 'REPLY', 'CANCEL'), //'VTODO' => array('REQUEST', 'REPLY', 'CANCEL'), ); /** * List of supported calendar-data-types * * @var array */ static $supported_calendar_data_types = array( array('content-type' => 'text/calendar', 'version' => '2.0'), ); /** * Supported attachment types for capabilities * * @var array */ static $supported_attachments = array('external', 'inline'); /** * Other capablities * * @var array name => value pairs */ static $capablities = array( 'max-content-length' => 102400, 'min-date-time' => '19910101T000000Z', 'max-date-time' => '20381231T000000Z', 'max-instances' => 150, 'max-recipients' => 250, // for server config: 'administrator' => 'mailto:ischedule-admin@example.com</administrator', ); /** * Requiremnt for originator to match depending on method * * @var array method => array('ORGANIZER','ATTENDEE') * @link https://tools.ietf.org/html/draft-desruisseaux-ischedule-03#section-6.1 */ static $supported_method2origin_requirement = array( //'PUBLISH' => null, // no requirement 'REQUEST' => array('ORGANIZER', 'ATTENDEE'), 'REPLY' => array('ATTENDEE'), 'ADD' => array('ORGANIZER'), 'CANCEL' => array('ORGANIZER'), //'REFRESH' => null, //'COUNTER' => array('ATTENDEE'), //'DECLINECOUNTER' => array('ORGANIZER'), ); /** * Serve an iSchedule POST request */ public function post() { // get and verify required headers $headers = array(); foreach($_SERVER as $name => $value) { $name = strtolower(str_replace('_', '-', $name)); list($first, $rest) = explode('-', $name, 2); switch($first) { case 'content': $headers[$name] = $value; break; case 'http': $headers[$rest] = $value; break; } } if (($missing = array_diff(explode(':', strtolower(self::REQUIRED_DKIM_HEADERS.':DKIM-Signature')), array_keys($headers)))) { //error_log('headers='.array2string(array_keys($headers)).', required='.self::REQUIRED_DKIM_HEADERS.', missing='.array($missing)); if (in_array('originator', $missing)) { $error = 'originator-missing'; } elseif(in_array('recipient', $missing)) { $error = 'recipient-missing'; } else { $error = 'invalid-scheduling-message'; } throw new Exception ("Bad Request: $error: missing required headers: ".implode(', ', $missing), 400); } // validate dkim signature // for multivalued Recipient header: as PHP engine agregates them ", " separated, // we cant tell it apart from ", " separated recipients in one header, therefore we try to validate both. // It will fail if multiple recipients in a single header are also ", " separated (just comma works fine) if (!self::dkim_validate($headers, $this->request, $error)) { throw new Exception('Bad Request: verification-failed: DKIM signature invalid: '.$error, 400); } // check if recipient is a user // todo: multiple recipients, currently we use last recipient for EGroupware enviroment // it is no error, to not specify a correct user, in that case we return just a request-status of "3.7;Invalid Calendar User"! //foreach(preg_split('/, */', $headers['recipient']) as $recipient) /*{ if (!stripos($recipient, 'mailto:') === 0 || !($account_id = $GLOBALS['egw']->accounts->name2id(substr($recipient, 7), 'account_email'))) { throw new Exception("Bad Request: recipient-missing: unknown recipient '$recipient'", 400); } } // create enviroment for recipient user, as we act on his behalf $GLOBALS['egw']->session->account_id = $account_id; $GLOBALS['egw']->session->account_lid = $GLOBALS['egw']->accounts->id2name($account_id); //$GLOBALS['egw']->session->account_domain = $domain; $GLOBALS['egw_info']['user'] = $GLOBALS['egw']->session->read_repositories(); translation::init(); */ // check originator is allowed to iSchedule with recipient // ToDo: preference for user/admin to specify with whom to iSchedule: $allowed_origins $allowed_origins = preg_split('/, ?/', $GLOBALS['egw_info']['user']['preferences']['groupdav']['ischedule_allowed_origins']); /* disabled 'til UI is ready to specifiy * ToDo: this should be no error but a response-status of "3.8;No authority" list(,$originator_domain) = explode('@', $headers['Originator']); if (!in_array($headers['Originator'], $allowed_orgins) && !in_array($originator_domain, $allowed_origins)) { throw new Exception('Forbidden', 403); }*/ // check method and component of Content-Type are valid // ToDo: no component or method in Content-Type should give an "invalid-scheduling-message" error // only unsupported (not in capablities) component should give "invalid-caledar-data-type" if (!preg_match('/component=([^;]+)/i', $headers['content-type'], $matches) || (!in_array($component=strtoupper($matches[1]), self::$supported_components))) { throw new Exception ('Bad Request: invalid-calendar-data-type: missing or unsupported component in Content-Type header', 400); } if (!preg_match('/method=([^;]+)/i', $headers['content-type'], $matches) || (!isset(self::$supported_method2origin_requirement[$method=strtoupper($matches[1])])) || $component == 'VFREEBUSY' && $method != 'REQUEST') { throw new Exception ('Bad Request: missing or unsupported method in Content-Type header', 400); } // parse iCal // code copied from calendar_groupdav::outbox_freebusy_request for now include_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php'; $vcal = new Horde_iCalendar(); if (!$vcal->parsevCalendar($this->request, 'VCALENDAR', 'utf-8')) { throw new Exception('Bad Request: invalid-calendar-data: Failed parsing iCal', 400); } $version = $vcal->getAttribute('VERSION'); $handler = new calendar_ical(); $handler->setSupportedFields('GroupDAV',$this->agent); $handler->calendarOwner = $handler->user = 0; // to NOT default owner/organizer to something if (!($vcal_comp = $vcal->getComponent(0)) || !($event = $handler->vevent2egw($vcal_comp, $version, $handler->supportedFields, $principalURL='', $check_component='Horde_iCalendar_'.strtolower($component)))) { throw new Exception('Bad Request: invalid-calendar-data: Failed converting iCal', 400); } // validate originator matches organizer or attendee $originator_requirement = self::$supported_method2origin_requirement[$method]; if (isset($originator_requirement)) { $matches = false; foreach($originator_requirement as $requirement) { $originator = $headers['originator']; if (stripos($originator, 'mailto:') === 0) $originator = substr($originator, 7); if ($requirement == 'ORGANIZER' && ($event['organizer'] == $originator || strpos($event['organizer'], '<'.$originator.'>') !== false) || $requirement == 'ATTENDEE' && (in_array('e'.$originator, $event['participants']) || // ToDO: Participant could have CN, need to check that too $originator_account_id = $GLOBALS['egw']->accounts->name2id($originator, 'account_email') && in_array($originator_account_id, $event['participants']))) { $matches = true; break; // no need to try further as we OR } } if (!$matches) { throw new Exception("Bad Request: originator-invalid: originator '$originator' invalid for given $component component!", 400); } } $xml = new XMLWriter; $xml->openMemory(); $xml->setIndent(true); $xml->startDocument('1.0', 'UTF-8'); $xml->startElementNs(null, 'schedule-response', self::ISCHEDULE); // null = no prefix switch($component) { case 'VFREEBUSY': $this->vfreebusy($event, $handler, $vcal_comp, $xml); break; case 'VEVENT': $this->vevent($event, $handler, $component, $xml); break; default: throw new Exception ('Bad Request: invalid-calendar-data-type: not implemented', 400); } $xml->endElement(); // schedule-response $xml->endDocument(); header('Content-Type: text/xml; charset=UTF-8'); header('iSchedule-Version: '.self::VERSION); header('iSchedule-Capabilities: '.self::SERIAL); echo $xml->outputMemory(); } /** * Handle VEVENT component * * @param array $event * @param calendar_ical $handler * @param string $component * @param XMLWriter $xml */ function vevent(array $event, calendar_ical $handler, Horde_iCalendar_vevent $component, XMLWriter $xml) { $organizer = $component->getAttribute('ORGANIZER'); $attendees = (array)$component->getAttribute('ATTENDEE'); $handler->importVCal($vCalendar, $eventId, self::etag2value($this->http_if_match), false, 0, $this->groupdav->current_user_principal, $user, $charset, $id); foreach($event['participants'] as $uid => $status) { $xml->startElement('response'); $xml->writeElement('recipient', $attendee=array_shift($attendees)); // iSchedule has not DAV:href! if (is_numeric($uid)) { $xml->writeElement('request-status', '2.0;Success'); $xml->writeElement('responsedescription', 'Delivered to recipient'); } else { $xml->writeElement('request-status', '3.7;Invalid Calendar User'); $xml->writeElement('responsedescription', 'Recipient not a local user'); } $xml->endElement(); // response } } /** * Handle VFREEBUSY component * * @param array $event * @param calendar_ical $handler * @param Horde_iCalendar_vfreebusy $component * @param XMLWriter $xml */ function vfreebusy(array $event, calendar_ical $handler, Horde_iCalendar_vfreebusy $component, XMLWriter $xml) { $organizer = $component->getAttribute('ORGANIZER'); $attendees = (array)$component->getAttribute('ATTENDEE'); foreach($attendees as $attendee) { $xml->startElement('response'); $xml->writeElement('recipient', $attendee); // iSchedule has not DAV:href! if (stripos($attendee, 'mailto:') === 0 && ($uid = $GLOBALS['egw']->accounts->name2id(substr($attendee, 7), 'account_email'))) { $xml->writeElement('request-status', '2.0;Success'); $xml->writeElement('calendar-data', $handler->freebusy($uid, $event['end'], true, 'utf-8', $event['start'], 'REPLY', array( 'UID' => $event['uid'], 'ORGANIZER' => $organizer, 'ATTENDEE' => $attendee, ))); } else { $xml->writeElement('request-status', '3.7;Invalid Calendar User'); } $xml->endElement(); // response } } /** * Required DKIM tags * * @link https://tools.ietf.org/html/rfc6376#section-3.5 * * @var array */ public static $required_dkim_tags = array('v', 'a', 'b', 'bh', 'd', 'h', 's'); /** * Validate DKIM signature * * For multivalued and multiple Recipient header(s): PHP engine agregates them ", " separated. * ischedule-relaxed canonisation takes care of that. * * @param array $headers header-name in lowercase(!) as key * @param string $body * @param string &$error=null error if false returned * @return boolean true if signature could be validated, false otherwise */ public static function dkim_validate(array $headers, $body, &$error=null) { // parse dkim signature, after unfolding it if (!isset($headers['dkim-signature']) || !preg_match_all('/[\t\s]*([a-z]+)\s*=\s*([^;]+);?/i', preg_replace("|\r\n\s+|", "", $headers['dkim-signature']), $matches)) { $error = "Can't parse DKIM signature"; return false; } $dkim = array_combine($matches[1], $matches[2]); // check required DKIM tags if (($missing = array_diff(self::$required_dkim_tags, array_keys($dkim)))) { $error = 'missing required DKIM tags: '.implode(', ', $missing); return false; } // check dkim version, have to fail if it's not 1 if ($dkim['v'] !== '1') { $error = "Wrong DKIM version '$dkim[v]'"; return false; } // create headers array, h tag can contain whitespace, which need to be removed $dkim_headers = array(); $check = $headers; foreach(explode(':', strtolower(preg_replace("|\s|", "", $dkim['h']))) as $header) { // dkim oversigning: ommit not existing headers in signing if (!isset($check[$header])) continue; $value = $check[$header]; unset($check[$header]); $dkim_headers[] = $header.': '.$value; } // dkim signature is obvious without content of signature, but must not necessarly be last tag $dkim_unsigned = 'DKIM-Signature: '.str_replace($dkim['b'], '', $headers['dkim-signature']); // c defaults to 'simple/simple', check on valid canonicalization methods is performed further down list($header_canon, $body_canon) = explode('/', isset($dkim['c']) ? $dkim['c'] : 'simple/simple'); // Canonicalization for Body switch($body_canon) { case 'relaxed': $_b = mailDomainSigner::bodyRelaxCanon($body); break; case 'simple': $_b = mailDomainSigner::bodySimpleCanon($body); break; default: $error = "Unknown body canonicalization '$body_canon'"; return false; } // check signing and hashing algorithms list($sign_algo, $hash_algo) = explode('-', $dkim['a']); if ($sign_algo != 'rsa' || !in_array($hash_algo, array('sha1', 'sha256', 'sha384', 'sha512'))) { $error = "Unknown or unimplemented algorithm a='$dkim[a]'"; return false; } // Hash of the canonicalized body [tag:bh] $_bh = base64_encode(hash($hash_algo, $_b, true)); // check body hash, need to remove optional whitespace if ($_bh != preg_replace("|\s|", "", $dkim['bh'])) { $error = 'Body hash does NOT verify'; error_log(__METHOD__."() body-hash='$_bh' != '$dkim[bh]'=dkim-bh $error"); return false; } // Canonicalization Header Data switch($header_canon) { case 'relaxed': $_unsigned = mailDomainSigner::headRelaxCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned); break; case 'ischedule-relaxed': $_unsigned = mailDomainSigner::headIScheduleRelaxCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned); break; case 'simple': $_unsigned = mailDomainSigner::headSimpleCanon(implode("\r\n", $dkim_headers). "\r\n".$dkim_unsigned); break; default: $error = "Unknown header canonicalization '$header_canon'"; return false; } // fetch public key using method in dkim q foreach(explode(':', $dkim['q']) as $method) { switch($method) { case 'dns/txt': $public_key = self::dns_txt_pubkey($dkim['d'], $dkim['s']); break; case 'http/well-known': $public_key = self::well_known_pubkey($dkim['d'], $dkim['s']); break; case 'private-exchange': $public_key = self::private_exchange_pubkey($dkim['d'], $dkim['s']); break; default: // not understood q method $public_key = false; break; } if ($public_key) break; } // for testing purpose allways try private-exchange, if none other match if (!$public_key) $public_key = self::private_exchange_pubkey($dkim['d'], $dkim['s']); if (!$public_key) { $error = "No public key for d='$dkim[d]' and s='$dkim[s]' using methods q='$dkim[q]'"; return false; } // value of b tag can contain whitespace, which need to be removed $ok = openssl_verify($_unsigned, base64_decode(preg_replace("|\s|", "", $dkim['b'])), $public_key, $hash_algo); if ($ok != 1) error_log(__METHOD__."() openssl_verify('$_unsigned', ..., '$public_key', '$hash_algo') returned ".array2string($ok)); switch($ok) { case -1: $error = 'Error while verifying DKIM'; return false; case 0: $error = 'DKIM signature does NOT verify'; return false; } // verify optional x tag, unix timestamp when signature expires if (isset($dkim['x']) && (!is_numeric($dkim['x']) || $dkim['x'] < time())) { $error = "DKIM signature is expired x=$dkim[x] < ".time(); return false; } return true; } /** * Provisional private-exchange public keys * * @var array domain => selector => public key */ static $private_exchange = array( 'example.com' => array( 'ischedule' => '-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDtocSHvSS1Nn0uIL4Sg+0wp6Kc W31WRC4Fww8P+jvsVAazVOxvxkShNSd18EvApiNa55P8WgKVEu02OQePjnjKNqfg JPeajkWy/0CJn+d6rX/ncPMGX2EYzqXy/CyVqpcnVAosToymo6VHL6ufhzlyLJFD znLtV121CZLUZlAySQIDAQAB -----END PUBLIC KEY-----', ), 'mysite.edu' => array( 'selector' => '-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuv+6UtGUdPerJ3s0HCng2sv3c R3ttma0JB6rMFfOTi1oHgk+h328MfGzhZK+SA9tsRPBcrJE/3uxs4SS2XNG9qRCG 0YMmNFOmubht4RhQhS9drSNyMZbhy2MPVbl9lHAJULFdaDdLj1hc3xTMWy8sDa8s M8r0gHvp/sPSe9CQQQIDAQAB -----END PUBLIC KEY-----', ), 'ken.name' => array( 'ischedule' => '-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCfAWRKjVWS/W4F3nUYnj4XWYsy +DsJdTjMKiHilUgQoz5MLnWpE/rt0LZklMG4Vz0io82pvnUzRrhmiaTTynlEwkj7 gDABr/WCxv2j5vKNayz13my8z+D6efYkuHsPP8z5iQp9yzbKa8FXKb+O7AXbK5fS 3U7S1OJgMpBeCrcpdwIDAQAB -----END PUBLIC KEY-----', ), 'caldav.egroupware.net' => array( 'calendar' => '-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiawhLuTSVhnl1zz5pXs1A748y N3aNE181dni8nsYqIQB1h4H32J4dZurEiAnP9nflQRjCmmg1NTvFcNz11Bem4zo1 K4r4mcfbjlheorK2Mwoh445HR3fo/pP7uV6CcXTNboBJLTxs6ZHswmQjxyuKBKmx yXUKsIQVi3qPyPdB3QIDAQAB -----END PUBLIC KEY-----', ), 'outdoor-training.de' => array( 'calendar' => '-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiawhLuTSVhnl1zz5pXs1A748y N3aNE181dni8nsYqIQB1h4H32J4dZurEiAnP9nflQRjCmmg1NTvFcNz11Bem4zo1 K4r4mcfbjlheorK2Mwoh445HR3fo/pP7uV6CcXTNboBJLTxs6ZHswmQjxyuKBKmx yXUKsIQVi3qPyPdB3QIDAQAB -----END PUBLIC KEY-----', ), ); /** * Fetch public key from dns txt recored dkim q=private-exchange * * @param string $d domain * @param string $s selector * @return string|boolean string with (full) public key or false if not found or other error retrieving it */ public static function private_exchange_pubkey($d, $s) { if (!isset(self::$private_exchange[$d]) || !isset(self::$private_exchange[$d][$s])) { return false; } return self::$private_exchange[$d][$s]; } /** * Fetch public key via http for q=http/well-known * * GET request to https://$domain/.well-known/domainkey/$selector * Content should be identical to txt record for dns/txt. * * @param string $d domain * @param string $s selector * @return string|boolean string with (full) public key or false if not found or other error retrieving it */ public static function well_known_pubkey($d, $s) { if (!($keys = @file_get_contents('https://'.$d.'/.well-known/domainkey/'.$s)) || preg_match('/p=([^;]+)/i', $keys, $matches)) { return false; } return "-----BEGIN PUBLIC KEY-----\n".chunk_split($matches[1], 64, "\n")."-----END PUBLIC KEY-----\n"; } /** * Fetch public key from dns txt recored dkim q=dns/txt * * @param string $d domain * @param string $s selector * @return string|boolean string with (full) public key or false if not found or other error retrieving it */ public function dns_txt_pubkey($d, $s) { if (!($dns = self::fetch_dns($d, $s))) { return false; } return "-----BEGIN PUBLIC KEY-----\n".chunk_split($dns['p'], 64, "\n")."-----END PUBLIC KEY-----\n"; } /** * Fetch dns record and return parsed array * * @param string $domain * @param string $selector * @return array with values for keys parsed from eg. "v=DKIM1\;k=rsa\;h=sha1\;s=calendar\;t=s\;p=..." */ public static function fetch_dns($domain, $selector='calendar') { if (!($records = dns_get_record($host=$selector.'._domainkey.'.$domain, DNS_TXT))) return false; if (!isset($records[0]['text']) && !preg_match_all('/[\t\s]*([a-z]+)=([^;]+);?/i', $records[0]['txt'], $matches)) { return false; } return array_combine($matches[1], $matches[2]); } const SERIAL = '124'; /** * Serve an iSchedule GET request, currently only action=capabilities * * GET /.well-known/ischedule?action=capabilities HTTP/1.1 * Host: cal.example.com * * HTTP/1.1 200 OK * Date: Mon, 15 Dec 2008 09:32:12 GMT * Content-Type: application/xml; charset=utf-8 * Content-Length: xxxx * iSchedule-Version: 1.0 * iSchedule-Capabilities: 123 * ETag: "afasdf-132afds" * * <?xml version="1.0" encoding="utf-8" ?> * <query-result xmlns="urn:ietf:params:xml:ns:ischedule"> * <capabilities> * <serial-number>123</serial-number> * <versions> * <version>1.0</version> * </versions> * <scheduling-messages> * <component name="VEVENT"> * <method name="REQUEST"/> * <method name="ADD"/> * <method name="REPLY"/> * <method name="CANCEL"/> * </component> * <component name="VTODO"/> * <component name="VFREEBUSY"/> * </scheduling-messages> * <calendar-data-types> * <calendar-data-type content-type="text/calendar" version="2.0"/> * </calendar-data-types> * <attachmens> * <inline/> * <external/> * </attachments> * <max-content-length>102400</max-content-length> * <min-date-time>19910101T000000Z</min-date-time> * <max-date-time>20381231T000000Z</max-date-time> * <max-instances>150</max-instances> * <max-recipients>250</max-recipients> * <administrator>mailto:ischedule-admin@example.com</administrator> * </capabilities> * </query-result> */ public function get() { if (!isset($_GET['action']) || $_GET['action'] !== 'capabilities') { error_log(__METHOD__."() invalid iSchedule request using GET without action=capabilities!"); header("HTTP/1.1 400 Bad Request"); echo "<h1>Invalid iSchedule request using GET without action=capabilities!</h1>\n"; return; } // generate capabilities $xml = new XMLWriter; $xml->openMemory(); $xml->setIndent(true); $xml->startDocument('1.0', 'UTF-8'); $xml->startElementNs(null, 'query-result', self::ISCHEDULE); $xml->startElement('capabilities'); $xml->writeElement('serial-number', self::SERIAL); $xml->startElement('versions'); foreach(self::$supported_versions as $version) { $xml->writeElement('version', $version); } $xml->endElement(); // versions $xml->startElement('scheduling-messages'); foreach(self::$supported_components as $component => $methods) { $xml->startElement('component'); $xml->writeAttribute('name', $component); foreach($methods as $method) { $xml->startElement('method'); $xml->writeAttribute('name', $method); $xml->endElement(); // method } $xml->endElement(); // component } $xml->endElement(); // scheduling-messages $xml->startElement('calendar-data-types'); foreach(self::$supported_calendar_data_types as $data) { $xml->startElement('calendar-data-type'); foreach($data as $name => $value) { $xml->writeAttribute($name, $value); } $xml->endElement(); // calendar-data-type } $xml->endElement(); // calendar-data-types $xml->startElement('attachments'); foreach(self::$supported_attachments as $type) { $xml->writeElement($type, ''); } $xml->endElement(); // attachments if (!empty($GLOBALS['egw_info']['server']['admin_mails'])) { self::$capablities['administrator'] = 'mailto:'.$GLOBALS['egw_info']['server']['admin_mails']; } foreach(self::$capablities as $name => $value) { $xml->writeElement($name, $value); } $xml->endElement(); // capabilities $xml->endElement(); // query-result $xml->endDocument(); $capabilities = $xml->outputMemory(); // returning capabilities header('Content-Type: application/xml; charset=utf-8'); header('iSchedule-Version: '.self::VERSION); header('iSchedule-Capabilities: '.self::SERIAL); header('Content-Length: '.bytes($capabilites)); header('ETag: "'.md5($capabilites).'"'); echo $capabilities; common::egw_exit(); } /** * Exception handler, which additionally logs the request (incl. a trace) * * Does NOT return and get installed in constructor. * * @param Exception $e */ public static function exception_handler(Exception $e) { // logging exception as regular egw_execption_hander does _egw_log_exception($e,$headline); // exception handler sending message back to the client as http status $code = $e->getCode(); $msg = $e->getMessage(); list($http_status, $error, $description) = explode(': ', $msg, 3); // check if we have a valid iSchedule error element, if not we use invalid-scheduling-message if (!empty($error) && strpos($error, ' ') !== false) { $description = $error.($description ? ': '.$description : ''); $error = 'invalid-scheduling-message'; } if (!in_array($code, array(400, 403, 407, 503))) $code = 500; header('HTTP/1.1 '.$code.' '.$http_status, true, $code); header('Content-Type: text/xml; charset=UTF-8'); header('iSchedule-Version: '.self::VERSION); header('iSchedule-Capabilities: '.self::SERIAL); if ($error) { echo '<?xml version="1.0" encoding="utf-8" ?> <error xmlns="urn:ietf:params:xml:ns:ischedule"> <'.$error.' /> <response-description>'.htmlspecialchars($description).'</response-description> </error> '; } // if our groupdav logging is active, log the request plus a trace, if enabled in server-config if (groupdav::$request_starttime && isset(self::$instance)) { self::$instance->_http_status = $code.' '.$http_status; if ($GLOBALS['egw_info']['server']['exception_show_trace']) { self::$instance->log_request("\n".$e->getTraceAsString()."\n"); } else { self::$instance->log_request(); } } if (is_object($GLOBALS['egw'])) { common::egw_exit(); } exit; } }