PageRenderTime 58ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Sabre/CalDAV/Plugin.php

https://github.com/agilastic/sabre-dav
PHP | 1390 lines | 734 code | 293 blank | 363 comment | 100 complexity | 40f3862254b118331b80d028c8db9264 MD5 | raw file
Possible License(s): BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. namespace Sabre\CalDAV;
  3. use
  4. Sabre\DAV,
  5. Sabre\DAVACL,
  6. Sabre\VObject,
  7. Sabre\HTTP\URLUtil,
  8. Sabre\HTTP\RequestInterface,
  9. Sabre\HTTP\ResponseInterface;
  10. /**
  11. * CalDAV plugin
  12. *
  13. * This plugin provides functionality added by CalDAV (RFC 4791)
  14. * It implements new reports, and the MKCALENDAR method.
  15. *
  16. * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
  17. * @author Evert Pot (http://evertpot.com/)
  18. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  19. */
  20. class Plugin extends DAV\ServerPlugin {
  21. /**
  22. * This is the official CalDAV namespace
  23. */
  24. const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
  25. /**
  26. * This is the namespace for the proprietary calendarserver extensions
  27. */
  28. const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
  29. /**
  30. * The hardcoded root for calendar objects. It is unfortunate
  31. * that we're stuck with it, but it will have to do for now
  32. */
  33. const CALENDAR_ROOT = 'calendars';
  34. /**
  35. * Reference to server object
  36. *
  37. * @var DAV\Server
  38. */
  39. protected $server;
  40. /**
  41. * The email handler for invites and other scheduling messages.
  42. *
  43. * @var Schedule\IMip
  44. */
  45. protected $imipHandler;
  46. /**
  47. * Sets the iMIP handler.
  48. *
  49. * iMIP = The email transport of iCalendar scheduling messages. Setting
  50. * this is optional, but if you want the server to allow invites to be sent
  51. * out, you must set a handler.
  52. *
  53. * Specifically iCal will plain assume that the server supports this. If
  54. * the server doesn't, iCal will display errors when inviting people to
  55. * events.
  56. *
  57. * @param Schedule\IMip $imipHandler
  58. * @return void
  59. */
  60. public function setIMipHandler(Schedule\IMip $imipHandler) {
  61. $this->imipHandler = $imipHandler;
  62. }
  63. /**
  64. * Use this method to tell the server this plugin defines additional
  65. * HTTP methods.
  66. *
  67. * This method is passed a uri. It should only return HTTP methods that are
  68. * available for the specified uri.
  69. *
  70. * @param string $uri
  71. * @return array
  72. */
  73. public function getHTTPMethods($uri) {
  74. // The MKCALENDAR is only available on unmapped uri's, whose
  75. // parents extend IExtendedCollection
  76. list($parent, $name) = URLUtil::splitPath($uri);
  77. $node = $this->server->tree->getNodeForPath($parent);
  78. if ($node instanceof DAV\IExtendedCollection) {
  79. try {
  80. $node->getChild($name);
  81. } catch (DAV\Exception\NotFound $e) {
  82. return array('MKCALENDAR');
  83. }
  84. }
  85. return array();
  86. }
  87. /**
  88. * Returns the path to a principal's calendar home.
  89. *
  90. * The return url must not end with a slash.
  91. *
  92. * @param string $principalUrl
  93. * @return string
  94. */
  95. public function getCalendarHomeForPrincipal($principalUrl) {
  96. // The default is a bit naive, but it can be overwritten.
  97. list(, $nodeName) = URLUtil::splitPath($principalUrl);
  98. return self::CALENDAR_ROOT . '/' . $nodeName;
  99. }
  100. /**
  101. * Returns a list of features for the DAV: HTTP header.
  102. *
  103. * @return array
  104. */
  105. public function getFeatures() {
  106. return array('calendar-access', 'calendar-proxy');
  107. }
  108. /**
  109. * Returns a plugin name.
  110. *
  111. * Using this name other plugins will be able to access other plugins
  112. * using DAV\Server::getPlugin
  113. *
  114. * @return string
  115. */
  116. public function getPluginName() {
  117. return 'caldav';
  118. }
  119. /**
  120. * Returns a list of reports this plugin supports.
  121. *
  122. * This will be used in the {DAV:}supported-report-set property.
  123. * Note that you still need to subscribe to the 'report' event to actually
  124. * implement them
  125. *
  126. * @param string $uri
  127. * @return array
  128. */
  129. public function getSupportedReportSet($uri) {
  130. $node = $this->server->tree->getNodeForPath($uri);
  131. $reports = array();
  132. if ($node instanceof ICalendar || $node instanceof ICalendarObject) {
  133. $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
  134. $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
  135. }
  136. if ($node instanceof ICalendar) {
  137. $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
  138. }
  139. // iCal has a bug where it assumes that sync support is enabled, only
  140. // if we say we support it on the calendar-home, even though this is
  141. // not actually the case.
  142. if ($node instanceof UserCalendars && $this->server->getPlugin('sync')) {
  143. $reports[] = '{DAV:}sync-collection';
  144. }
  145. return $reports;
  146. }
  147. /**
  148. * Initializes the plugin
  149. *
  150. * @param DAV\Server $server
  151. * @return void
  152. */
  153. public function initialize(DAV\Server $server) {
  154. $this->server = $server;
  155. $server->on('method:MKCALENDAR', [$this,'httpMkcalendar']);
  156. $server->on('method:POST', [$this,'httpPost']);
  157. $server->on('method:GET', [$this,'httpGet'], 90);
  158. $server->on('report', [$this,'report']);
  159. $server->on('beforeGetProperties', [$this,'beforeGetProperties']);
  160. $server->on('onHTMLActionsPanel', [$this,'htmlActionsPanel']);
  161. $server->on('onBrowserPostAction', [$this,'browserPostAction']);
  162. $server->on('beforeWriteContent', [$this,'beforeWriteContent']);
  163. $server->on('beforeCreateFile', [$this,'beforeCreateFile']);
  164. $server->xmlNamespaces[self::NS_CALDAV] = 'cal';
  165. $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs';
  166. $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Property\\SupportedCalendarComponentSet';
  167. $server->propertyMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Property\\ScheduleCalendarTransp';
  168. $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
  169. $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox';
  170. $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
  171. $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
  172. $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification';
  173. array_push($server->protectedProperties,
  174. '{' . self::NS_CALDAV . '}supported-calendar-component-set',
  175. '{' . self::NS_CALDAV . '}supported-calendar-data',
  176. '{' . self::NS_CALDAV . '}max-resource-size',
  177. '{' . self::NS_CALDAV . '}min-date-time',
  178. '{' . self::NS_CALDAV . '}max-date-time',
  179. '{' . self::NS_CALDAV . '}max-instances',
  180. '{' . self::NS_CALDAV . '}max-attendees-per-instance',
  181. '{' . self::NS_CALDAV . '}calendar-home-set',
  182. '{' . self::NS_CALDAV . '}supported-collation-set',
  183. '{' . self::NS_CALDAV . '}calendar-data',
  184. // scheduling extension
  185. '{' . self::NS_CALDAV . '}schedule-inbox-URL',
  186. '{' . self::NS_CALDAV . '}schedule-outbox-URL',
  187. '{' . self::NS_CALDAV . '}calendar-user-address-set',
  188. '{' . self::NS_CALDAV . '}calendar-user-type',
  189. // CalendarServer extensions
  190. '{' . self::NS_CALENDARSERVER . '}getctag',
  191. '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
  192. '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for',
  193. '{' . self::NS_CALENDARSERVER . '}notification-URL',
  194. '{' . self::NS_CALENDARSERVER . '}notificationtype'
  195. );
  196. }
  197. /**
  198. * This method handles POST request for the outbox.
  199. *
  200. * @param RequestInterface $request
  201. * @param ResponseInterface $response
  202. * @return bool
  203. */
  204. public function httpPost(RequestInterface $request, ResponseInterface $response) {
  205. // Checking if this is a text/calendar content type
  206. $contentType = $request->getHeader('Content-Type');
  207. if (strpos($contentType, 'text/calendar')!==0) {
  208. return;
  209. }
  210. $path = $request->getPath();
  211. // Checking if we're talking to an outbox
  212. try {
  213. $node = $this->server->tree->getNodeForPath($path);
  214. } catch (DAV\Exception\NotFound $e) {
  215. return;
  216. }
  217. if (!$node instanceof Schedule\IOutbox)
  218. return;
  219. $this->server->transactionType = 'post-caldav-outbox';
  220. $this->outboxRequest($node, $path);
  221. return false;
  222. }
  223. /**
  224. * This functions handles REPORT requests specific to CalDAV
  225. *
  226. * @param string $reportName
  227. * @param \DOMNode $dom
  228. * @return bool
  229. */
  230. public function report($reportName,$dom) {
  231. switch($reportName) {
  232. case '{'.self::NS_CALDAV.'}calendar-multiget' :
  233. $this->server->transactionType = 'report-calendar-multiget';
  234. $this->calendarMultiGetReport($dom);
  235. return false;
  236. case '{'.self::NS_CALDAV.'}calendar-query' :
  237. $this->server->transactionType = 'report-calendar-query';
  238. $this->calendarQueryReport($dom);
  239. return false;
  240. case '{'.self::NS_CALDAV.'}free-busy-query' :
  241. $this->server->transactionType = 'report-free-busy-query';
  242. $this->freeBusyQueryReport($dom);
  243. return false;
  244. }
  245. }
  246. /**
  247. * This function handles the MKCALENDAR HTTP method, which creates
  248. * a new calendar.
  249. *
  250. * @param RequestInterface $request
  251. * @param ResponseInterface $response
  252. * @return bool
  253. */
  254. public function httpMkCalendar(RequestInterface $request, ResponseInterface $response) {
  255. // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
  256. // for clients matching iCal in the user agent
  257. //$ua = $this->server->httpRequest->getHeader('User-Agent');
  258. //if (strpos($ua,'iCal/')!==false) {
  259. // throw new \Sabre\DAV\Exception\Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.');
  260. //}
  261. $body = $request->getBody($asString = true);
  262. $path = $request->getPath();
  263. $properties = array();
  264. if ($body) {
  265. $dom = DAV\XMLUtil::loadDOMDocument($body);
  266. foreach($dom->firstChild->childNodes as $child) {
  267. if (DAV\XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue;
  268. foreach(DAV\XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) {
  269. $properties[$k] = $prop;
  270. }
  271. }
  272. }
  273. $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
  274. $this->server->createCollection($path,$resourceType,$properties);
  275. $this->server->httpResponse->setStatus(201);
  276. $this->server->httpResponse->setHeader('Content-Length',0);
  277. // This breaks the method chain.
  278. return false;
  279. }
  280. /**
  281. * beforeGetProperties
  282. *
  283. * This method handler is invoked before any after properties for a
  284. * resource are fetched. This allows us to add in any CalDAV specific
  285. * properties.
  286. *
  287. * @param string $path
  288. * @param DAV\INode $node
  289. * @param array $requestedProperties
  290. * @param array $returnedProperties
  291. * @return void
  292. */
  293. public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
  294. if ($node instanceof DAVACL\IPrincipal) {
  295. $principalUrl = $node->getPrincipalUrl();
  296. // calendar-home-set property
  297. $calHome = '{' . self::NS_CALDAV . '}calendar-home-set';
  298. if (in_array($calHome,$requestedProperties)) {
  299. $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl) . '/';
  300. unset($requestedProperties[array_search($calHome, $requestedProperties)]);
  301. $returnedProperties[200][$calHome] = new DAV\Property\Href($calendarHomePath);
  302. }
  303. // schedule-outbox-URL property
  304. $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL';
  305. if (in_array($scheduleProp,$requestedProperties)) {
  306. $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl);
  307. $outboxPath = $calendarHomePath . '/outbox';
  308. unset($requestedProperties[array_search($scheduleProp, $requestedProperties)]);
  309. $returnedProperties[200][$scheduleProp] = new DAV\Property\Href($outboxPath);
  310. }
  311. // calendar-user-address-set property
  312. $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
  313. if (in_array($calProp,$requestedProperties)) {
  314. $addresses = $node->getAlternateUriSet();
  315. $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/';
  316. unset($requestedProperties[array_search($calProp, $requestedProperties)]);
  317. $returnedProperties[200][$calProp] = new DAV\Property\HrefList($addresses, false);
  318. }
  319. // These two properties are shortcuts for ical to easily find
  320. // other principals this principal has access to.
  321. $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
  322. $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
  323. if (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) {
  324. $aclPlugin = $this->server->getPlugin('acl');
  325. $membership = $aclPlugin->getPrincipalMembership($path);
  326. $readList = array();
  327. $writeList = array();
  328. foreach($membership as $group) {
  329. $groupNode = $this->server->tree->getNodeForPath($group);
  330. // If the node is either ap proxy-read or proxy-write
  331. // group, we grab the parent principal and add it to the
  332. // list.
  333. if ($groupNode instanceof Principal\IProxyRead) {
  334. list($readList[]) = URLUtil::splitPath($group);
  335. }
  336. if ($groupNode instanceof Principal\IProxyWrite) {
  337. list($writeList[]) = URLUtil::splitPath($group);
  338. }
  339. }
  340. if (in_array($propRead,$requestedProperties)) {
  341. unset($requestedProperties[$propRead]);
  342. $returnedProperties[200][$propRead] = new DAV\Property\HrefList($readList);
  343. }
  344. if (in_array($propWrite,$requestedProperties)) {
  345. unset($requestedProperties[$propWrite]);
  346. $returnedProperties[200][$propWrite] = new DAV\Property\HrefList($writeList);
  347. }
  348. }
  349. // notification-URL property
  350. $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL';
  351. if (($index = array_search($notificationUrl, $requestedProperties)) !== false) {
  352. $principalId = $node->getName();
  353. $notificationPath = $this->getCalendarHomeForPrincipal($principalUrl) . '/notifications/';
  354. unset($requestedProperties[$index]);
  355. $returnedProperties[200][$notificationUrl] = new DAV\Property\Href($notificationPath);
  356. }
  357. } // instanceof IPrincipal
  358. if ($node instanceof Notifications\INode) {
  359. $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype';
  360. if (($index = array_search($propertyName, $requestedProperties)) !== false) {
  361. $returnedProperties[200][$propertyName] =
  362. $node->getNotificationType();
  363. unset($requestedProperties[$index]);
  364. }
  365. } // instanceof Notifications_INode
  366. if ($node instanceof ICalendarObject) {
  367. // The calendar-data property is not supposed to be a 'real'
  368. // property, but in large chunks of the spec it does act as such.
  369. // Therefore we simply expose it as a property.
  370. $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
  371. if (in_array($calDataProp, $requestedProperties)) {
  372. unset($requestedProperties[$calDataProp]);
  373. $val = $node->get();
  374. if (is_resource($val))
  375. $val = stream_get_contents($val);
  376. // Taking out \r to not screw up the xml output
  377. $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
  378. }
  379. }
  380. }
  381. /**
  382. * This function handles the calendar-multiget REPORT.
  383. *
  384. * This report is used by the client to fetch the content of a series
  385. * of urls. Effectively avoiding a lot of redundant requests.
  386. *
  387. * @param \DOMNode $dom
  388. * @return void
  389. */
  390. public function calendarMultiGetReport($dom) {
  391. $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
  392. $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
  393. $xpath = new \DOMXPath($dom);
  394. $xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
  395. $xpath->registerNameSpace('dav','urn:DAV');
  396. $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
  397. if ($expand->length>0) {
  398. $expandElem = $expand->item(0);
  399. $start = $expandElem->getAttribute('start');
  400. $end = $expandElem->getAttribute('end');
  401. if(!$start || !$end) {
  402. throw new DAV\Exception\BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
  403. }
  404. $start = VObject\DateTimeParser::parseDateTime($start);
  405. $end = VObject\DateTimeParser::parseDateTime($end);
  406. if ($end <= $start) {
  407. throw new DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
  408. }
  409. $expand = true;
  410. } else {
  411. $expand = false;
  412. }
  413. $uris = [];
  414. foreach($hrefElems as $elem) {
  415. $uris[] = $this->server->calculateUri($elem->nodeValue);
  416. }
  417. foreach($this->server->getPropertiesForMultiplePaths($uris, $properties) as $uri=>$objProps) {
  418. if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
  419. $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
  420. $vObject->expand($start, $end);
  421. $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
  422. }
  423. $propertyList[]=$objProps;
  424. }
  425. $prefer = $this->server->getHTTPPRefer();
  426. $this->server->httpResponse->setStatus(207);
  427. $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  428. $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
  429. $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal']));
  430. }
  431. /**
  432. * This function handles the calendar-query REPORT
  433. *
  434. * This report is used by clients to request calendar objects based on
  435. * complex conditions.
  436. *
  437. * @param \DOMNode $dom
  438. * @return void
  439. */
  440. public function calendarQueryReport($dom) {
  441. $parser = new CalendarQueryParser($dom);
  442. $parser->parse();
  443. $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
  444. $depth = $this->server->getHTTPDepth(0);
  445. // The default result is an empty array
  446. $result = array();
  447. // The calendarobject was requested directly. In this case we handle
  448. // this locally.
  449. if ($depth == 0 && $node instanceof ICalendarObject) {
  450. $requestedCalendarData = true;
  451. $requestedProperties = $parser->requestedProperties;
  452. if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
  453. // We always retrieve calendar-data, as we need it for filtering.
  454. $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
  455. // If calendar-data wasn't explicitly requested, we need to remove
  456. // it after processing.
  457. $requestedCalendarData = false;
  458. }
  459. $properties = $this->server->getPropertiesForPath(
  460. $this->server->getRequestUri(),
  461. $requestedProperties,
  462. 0
  463. );
  464. // This array should have only 1 element, the first calendar
  465. // object.
  466. $properties = current($properties);
  467. // If there wasn't any calendar-data returned somehow, we ignore
  468. // this.
  469. if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
  470. $validator = new CalendarQueryValidator();
  471. $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
  472. if ($validator->validate($vObject,$parser->filters)) {
  473. // If the client didn't require the calendar-data property,
  474. // we won't give it back.
  475. if (!$requestedCalendarData) {
  476. unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
  477. } else {
  478. if ($parser->expand) {
  479. $vObject->expand($parser->expand['start'], $parser->expand['end']);
  480. $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
  481. }
  482. }
  483. $result = array($properties);
  484. }
  485. }
  486. }
  487. // If we're dealing with a calendar, the calendar itself is responsible
  488. // for the calendar-query.
  489. if ($node instanceof ICalendar && $depth = 1) {
  490. $nodePaths = $node->calendarQuery($parser->filters);
  491. foreach($nodePaths as $path) {
  492. list($properties) =
  493. $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
  494. if ($parser->expand) {
  495. // We need to do some post-processing
  496. $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
  497. $vObject->expand($parser->expand['start'], $parser->expand['end']);
  498. $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
  499. }
  500. $result[] = $properties;
  501. }
  502. }
  503. $prefer = $this->server->getHTTPPRefer();
  504. $this->server->httpResponse->setStatus(207);
  505. $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  506. $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
  507. $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
  508. }
  509. /**
  510. * This method is responsible for parsing the request and generating the
  511. * response for the CALDAV:free-busy-query REPORT.
  512. *
  513. * @param \DOMNode $dom
  514. * @return void
  515. */
  516. protected function freeBusyQueryReport(\DOMNode $dom) {
  517. $start = null;
  518. $end = null;
  519. foreach($dom->firstChild->childNodes as $childNode) {
  520. $clark = DAV\XMLUtil::toClarkNotation($childNode);
  521. if ($clark == '{' . self::NS_CALDAV . '}time-range') {
  522. $start = $childNode->getAttribute('start');
  523. $end = $childNode->getAttribute('end');
  524. break;
  525. }
  526. }
  527. if ($start) {
  528. $start = VObject\DateTimeParser::parseDateTime($start);
  529. }
  530. if ($end) {
  531. $end = VObject\DateTimeParser::parseDateTime($end);
  532. }
  533. if (!$start && !$end) {
  534. throw new DAV\Exception\BadRequest('The freebusy report must have a time-range filter');
  535. }
  536. $acl = $this->server->getPlugin('acl');
  537. if (!$acl) {
  538. throw new DAV\Exception('The ACL plugin must be loaded for free-busy queries to work');
  539. }
  540. $uri = $this->server->getRequestUri();
  541. $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
  542. $calendar = $this->server->tree->getNodeForPath($uri);
  543. if (!$calendar instanceof ICalendar) {
  544. throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
  545. }
  546. // Doing a calendar-query first, to make sure we get the most
  547. // performance.
  548. $urls = $calendar->calendarQuery(array(
  549. 'name' => 'VCALENDAR',
  550. 'comp-filters' => array(
  551. array(
  552. 'name' => 'VEVENT',
  553. 'comp-filters' => array(),
  554. 'prop-filters' => array(),
  555. 'is-not-defined' => false,
  556. 'time-range' => array(
  557. 'start' => $start,
  558. 'end' => $end,
  559. ),
  560. ),
  561. ),
  562. 'prop-filters' => array(),
  563. 'is-not-defined' => false,
  564. 'time-range' => null,
  565. ));
  566. $objects = array_map(function($url) use ($calendar) {
  567. $obj = $calendar->getChild($url)->get();
  568. return $obj;
  569. }, $urls);
  570. $generator = new VObject\FreeBusyGenerator();
  571. $generator->setObjects($objects);
  572. $generator->setTimeRange($start, $end);
  573. $result = $generator->getResult();
  574. $result = $result->serialize();
  575. $this->server->httpResponse->setStatus(200);
  576. $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
  577. $this->server->httpResponse->setHeader('Content-Length', strlen($result));
  578. $this->server->httpResponse->setBody($result);
  579. }
  580. /**
  581. * This method is triggered before a file gets updated with new content.
  582. *
  583. * This plugin uses this method to ensure that CalDAV objects receive
  584. * valid calendar data.
  585. *
  586. * @param string $path
  587. * @param DAV\IFile $node
  588. * @param resource $data
  589. * @param bool $modified Should be set to true, if this event handler
  590. * changed &$data.
  591. * @return void
  592. */
  593. public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) {
  594. if (!$node instanceof ICalendarObject)
  595. return;
  596. $this->validateICalendar($data, $path, $modified);
  597. }
  598. /**
  599. * This method is triggered before a new file is created.
  600. *
  601. * This plugin uses this method to ensure that newly created calendar
  602. * objects contain valid calendar data.
  603. *
  604. * @param string $path
  605. * @param resource $data
  606. * @param DAV\ICollection $parentNode
  607. * @param bool $modified Should be set to true, if this event handler
  608. * changed &$data.
  609. * @return void
  610. */
  611. public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) {
  612. if (!$parentNode instanceof Calendar)
  613. return;
  614. $this->validateICalendar($data, $path, $modified);
  615. }
  616. /**
  617. * This event is triggered before the usual GET request handler.
  618. *
  619. * We use this to intercept GET calls to notification nodes, and return the
  620. * proper response.
  621. *
  622. * @param RequestInterface $request
  623. * @param ResponseInterface $response
  624. * @return void
  625. */
  626. public function httpGet(RequestInterface $request, ResponseInterface $response) {
  627. $path = $request->getPath();
  628. try {
  629. $node = $this->server->tree->getNodeForPath($path);
  630. } catch (DAV\Exception\NotFound $e) {
  631. return;
  632. }
  633. if (!$node instanceof Notifications\INode)
  634. return;
  635. if (!$this->server->checkPreconditions(true)) return false;
  636. $dom = new \DOMDocument('1.0', 'UTF-8');
  637. $dom->formatOutput = true;
  638. $root = $dom->createElement('cs:notification');
  639. foreach($this->server->xmlNamespaces as $namespace => $prefix) {
  640. $root->setAttribute('xmlns:' . $prefix, $namespace);
  641. }
  642. $dom->appendChild($root);
  643. $node->getNotificationType()->serializeBody($this->server, $root);
  644. $response->setHeader('Content-Type','application/xml');
  645. $response->setHeader('ETag',$node->getETag());
  646. $response->setStatus(200);
  647. $response->setBody($dom->saveXML());
  648. // Return false to break the event chain.
  649. return false;
  650. }
  651. /**
  652. * Checks if the submitted iCalendar data is in fact, valid.
  653. *
  654. * An exception is thrown if it's not.
  655. *
  656. * @param resource|string $data
  657. * @param string $path
  658. * @param bool $modified Should be set to true, if this event handler
  659. * changed &$data.
  660. * @return void
  661. */
  662. protected function validateICalendar(&$data, $path, &$modified) {
  663. // If it's a stream, we convert it to a string first.
  664. if (is_resource($data)) {
  665. $data = stream_get_contents($data);
  666. }
  667. $before = md5($data);
  668. // Converting the data to unicode, if needed.
  669. $data = DAV\StringUtil::ensureUTF8($data);
  670. if ($before!==md5($data)) $modified = true;
  671. try {
  672. $vobj = VObject\Reader::read($data);
  673. } catch (VObject\ParseException $e) {
  674. throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
  675. }
  676. if ($vobj->name !== 'VCALENDAR') {
  677. throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
  678. }
  679. // Get the Supported Components for the target calendar
  680. list($parentPath,$object) = URLUtil::splitPath($path);
  681. $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
  682. $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
  683. $foundType = null;
  684. $foundUID = null;
  685. foreach($vobj->getComponents() as $component) {
  686. switch($component->name) {
  687. case 'VTIMEZONE' :
  688. continue 2;
  689. case 'VEVENT' :
  690. case 'VTODO' :
  691. case 'VJOURNAL' :
  692. if (is_null($foundType)) {
  693. $foundType = $component->name;
  694. if (!in_array($foundType, $supportedComponents)) {
  695. throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
  696. }
  697. if (!isset($component->UID)) {
  698. throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
  699. }
  700. $foundUID = (string)$component->UID;
  701. } else {
  702. if ($foundType !== $component->name) {
  703. throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
  704. }
  705. if ($foundUID !== (string)$component->UID) {
  706. throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
  707. }
  708. }
  709. break;
  710. default :
  711. throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
  712. }
  713. }
  714. if (!$foundType)
  715. throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
  716. }
  717. /**
  718. * This method handles POST requests to the schedule-outbox.
  719. *
  720. * Currently, two types of requests are support:
  721. * * FREEBUSY requests from RFC 6638
  722. * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
  723. *
  724. * The latter is from an expired early draft of the CalDAV scheduling
  725. * extensions, but iCal depends on a feature from that spec, so we
  726. * implement it.
  727. *
  728. * @param Schedule\IOutbox $outboxNode
  729. * @param string $outboxUri
  730. * @return void
  731. */
  732. public function outboxRequest(Schedule\IOutbox $outboxNode, $outboxUri) {
  733. // Parsing the request body
  734. try {
  735. $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true));
  736. } catch (VObject\ParseException $e) {
  737. throw new DAV\Exception\BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
  738. }
  739. // The incoming iCalendar object must have a METHOD property, and a
  740. // component. The combination of both determines what type of request
  741. // this is.
  742. $componentType = null;
  743. foreach($vObject->getComponents() as $component) {
  744. if ($component->name !== 'VTIMEZONE') {
  745. $componentType = $component->name;
  746. break;
  747. }
  748. }
  749. if (is_null($componentType)) {
  750. throw new DAV\Exception\BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
  751. }
  752. // Validating the METHOD
  753. $method = strtoupper((string)$vObject->METHOD);
  754. if (!$method) {
  755. throw new DAV\Exception\BadRequest('A METHOD property must be specified in iTIP messages');
  756. }
  757. // So we support two types of requests:
  758. //
  759. // REQUEST with a VFREEBUSY component
  760. // REQUEST, REPLY, ADD, CANCEL on VEVENT components
  761. $acl = $this->server->getPlugin('acl');
  762. if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
  763. $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-query-freebusy');
  764. $this->handleFreeBusyRequest($outboxNode, $vObject);
  765. } elseif ($componentType === 'VEVENT' && in_array($method, array('REQUEST','REPLY','ADD','CANCEL'))) {
  766. $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-post-vevent');
  767. $this->handleEventNotification($outboxNode, $vObject);
  768. } else {
  769. throw new DAV\Exception\NotImplemented('SabreDAV supports only VFREEBUSY (REQUEST) and VEVENT (REQUEST, REPLY, ADD, CANCEL)');
  770. }
  771. }
  772. /**
  773. * This method handles the REQUEST, REPLY, ADD and CANCEL methods for
  774. * VEVENT iTip messages.
  775. *
  776. * @return void
  777. */
  778. protected function handleEventNotification(Schedule\IOutbox $outboxNode, VObject\Component $vObject) {
  779. $originator = $this->server->httpRequest->getHeader('Originator');
  780. $recipients = $this->server->httpRequest->getHeader('Recipient');
  781. if (!$originator) {
  782. throw new DAV\Exception\BadRequest('The Originator: header must be specified when making POST requests');
  783. }
  784. if (!$recipients) {
  785. throw new DAV\Exception\BadRequest('The Recipient: header must be specified when making POST requests');
  786. }
  787. $recipients = explode(',',$recipients);
  788. foreach($recipients as $k=>$recipient) {
  789. $recipient = trim($recipient);
  790. if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
  791. throw new DAV\Exception\BadRequest('Recipients must start with mailto: and must be valid email address');
  792. }
  793. $recipient = substr($recipient, 7);
  794. $recipients[$k] = $recipient;
  795. }
  796. // We need to make sure that 'originator' matches the currently
  797. // authenticated user.
  798. $aclPlugin = $this->server->getPlugin('acl');
  799. if (is_null($aclPlugin)) throw new DAV\Exception('The ACL plugin must be loaded for scheduling to work');
  800. $principal = $aclPlugin->getCurrentUserPrincipal();
  801. $props = $this->server->getProperties($principal,array(
  802. '{' . self::NS_CALDAV . '}calendar-user-address-set',
  803. ));
  804. $addresses = array();
  805. if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
  806. $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
  807. }
  808. $found = false;
  809. foreach($addresses as $address) {
  810. // Trimming the / on both sides, just in case..
  811. if (rtrim(strtolower($originator),'/') === rtrim(strtolower($address),'/')) {
  812. $found = true;
  813. break;
  814. }
  815. }
  816. if (!$found) {
  817. throw new DAV\Exception\Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
  818. }
  819. // If the Originator header was a url, and not a mailto: address..
  820. // we're going to try to pull the mailto: from the vobject body.
  821. if (strtolower(substr($originator,0,7)) !== 'mailto:') {
  822. $originator = (string)$vObject->VEVENT->ORGANIZER;
  823. }
  824. if (strtolower(substr($originator,0,7)) !== 'mailto:') {
  825. throw new DAV\Exception\Forbidden('Could not find mailto: address in both the Orignator header, and the ORGANIZER property in the VEVENT');
  826. }
  827. $originator = substr($originator,7);
  828. $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
  829. $this->server->httpResponse->setStatus(200);
  830. $this->server->httpResponse->setHeader('Content-Type','application/xml');
  831. $this->server->httpResponse->setBody($this->generateScheduleResponse($result));
  832. }
  833. /**
  834. * Sends an iMIP message by email.
  835. *
  836. * This method must return an array with status codes per recipient.
  837. * This should look something like:
  838. *
  839. * array(
  840. * 'user1@example.org' => '2.0;Success'
  841. * )
  842. *
  843. * Formatting for this status code can be found at:
  844. * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
  845. *
  846. * A list of valid status codes can be found at:
  847. * https://tools.ietf.org/html/rfc5546#section-3.6
  848. *
  849. * @param string $originator
  850. * @param array $recipients
  851. * @param VObject\Component $vObject
  852. * @param string $principal Principal url
  853. * @return array
  854. */
  855. protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
  856. if (!$this->imipHandler) {
  857. $resultStatus = '5.2;This server does not support this operation';
  858. } else {
  859. $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
  860. $resultStatus = '2.0;Success';
  861. }
  862. $result = array();
  863. foreach($recipients as $recipient) {
  864. $result[$recipient] = $resultStatus;
  865. }
  866. return $result;
  867. }
  868. /**
  869. * Generates a schedule-response XML body
  870. *
  871. * The recipients array is a key->value list, containing email addresses
  872. * and iTip status codes. See the iMIPMessage method for a description of
  873. * the value.
  874. *
  875. * @param array $recipients
  876. * @return string
  877. */
  878. public function generateScheduleResponse(array $recipients) {
  879. $dom = new \DOMDocument('1.0','utf-8');
  880. $dom->formatOutput = true;
  881. $xscheduleResponse = $dom->createElement('cal:schedule-response');
  882. $dom->appendChild($xscheduleResponse);
  883. foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
  884. $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
  885. }
  886. foreach($recipients as $recipient=>$status) {
  887. $xresponse = $dom->createElement('cal:response');
  888. $xrecipient = $dom->createElement('cal:recipient');
  889. $xrecipient->appendChild($dom->createTextNode($recipient));
  890. $xresponse->appendChild($xrecipient);
  891. $xrequestStatus = $dom->createElement('cal:request-status');
  892. $xrequestStatus->appendChild($dom->createTextNode($status));
  893. $xresponse->appendChild($xrequestStatus);
  894. $xscheduleResponse->appendChild($xresponse);
  895. }
  896. return $dom->saveXML();
  897. }
  898. /**
  899. * This method is responsible for parsing a free-busy query request and
  900. * returning it's result.
  901. *
  902. * @param Schedule\IOutbox $outbox
  903. * @param string $request
  904. * @return string
  905. */
  906. protected function handleFreeBusyRequest(Schedule\IOutbox $outbox, VObject\Component $vObject) {
  907. $vFreeBusy = $vObject->VFREEBUSY;
  908. $organizer = $vFreeBusy->organizer;
  909. $organizer = (string)$organizer;
  910. // Validating if the organizer matches the owner of the inbox.
  911. $owner = $outbox->getOwner();
  912. $caldavNS = '{' . Plugin::NS_CALDAV . '}';
  913. $uas = $caldavNS . 'calendar-user-address-set';
  914. $props = $this->server->getProperties($owner,array($uas));
  915. if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
  916. throw new DAV\Exception\Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
  917. }
  918. if (!isset($vFreeBusy->ATTENDEE)) {
  919. throw new DAV\Exception\BadRequest('You must at least specify 1 attendee');
  920. }
  921. $attendees = array();
  922. foreach($vFreeBusy->ATTENDEE as $attendee) {
  923. $attendees[]= (string)$attendee;
  924. }
  925. if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
  926. throw new DAV\Exception\BadRequest('DTSTART and DTEND must both be specified');
  927. }
  928. $startRange = $vFreeBusy->DTSTART->getDateTime();
  929. $endRange = $vFreeBusy->DTEND->getDateTime();
  930. $results = array();
  931. foreach($attendees as $attendee) {
  932. $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
  933. }
  934. $dom = new \DOMDocument('1.0','utf-8');
  935. $dom->formatOutput = true;
  936. $scheduleResponse = $dom->createElement('cal:schedule-response');
  937. foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
  938. $scheduleResponse->setAttribute('xmlns:' . $prefix,$namespace);
  939. }
  940. $dom->appendChild($scheduleResponse);
  941. foreach($results as $result) {
  942. $response = $dom->createElement('cal:response');
  943. $recipient = $dom->createElement('cal:recipient');
  944. $recipientHref = $dom->createElement('d:href');
  945. $recipientHref->appendChild($dom->createTextNode($result['href']));
  946. $recipient->appendChild($recipientHref);
  947. $response->appendChild($recipient);
  948. $reqStatus = $dom->createElement('cal:request-status');
  949. $reqStatus->appendChild($dom->createTextNode($result['request-status']));
  950. $response->appendChild($reqStatus);
  951. if (isset($result['calendar-data'])) {
  952. $calendardata = $dom->createElement('cal:calendar-data');
  953. $calendardata->appendChild($dom->createTextNode(str_replace("\r\n","\n",$result['calendar-data']->serialize())));
  954. $response->appendChild($calendardata);
  955. }
  956. $scheduleResponse->appendChild($response);
  957. }
  958. $this->server->httpResponse->setStatus(200);
  959. $this->server->httpResponse->setHeader('Content-Type','application/xml');
  960. $this->server->httpResponse->setBody($dom->saveXML());
  961. }
  962. /**
  963. * Returns free-busy information for a specific address. The returned
  964. * data is an array containing the following properties:
  965. *
  966. * calendar-data : A VFREEBUSY VObject
  967. * request-status : an iTip status code.
  968. * href: The principal's email address, as requested
  969. *
  970. * The following request status codes may be returned:
  971. * * 2.0;description
  972. * * 3.7;description
  973. *
  974. * @param string $email address
  975. * @param \DateTime $start
  976. * @param \DateTime $end
  977. * @param VObject\Component $request
  978. * @return array
  979. */
  980. protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
  981. $caldavNS = '{' . Plugin::NS_CALDAV . '}';
  982. $aclPlugin = $this->server->getPlugin('acl');
  983. if (substr($email,0,7)==='mailto:') $email = substr($email,7);
  984. $result = $aclPlugin->principalSearch(
  985. array('{http://sabredav.org/ns}email-address' => $email),
  986. array(
  987. '{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
  988. '{http://sabredav.org/ns}email-address',
  989. )
  990. );
  991. if (!count($result)) {
  992. return array(
  993. 'request-status' => '3.7;Could not find principal',
  994. 'href' => 'mailto:' . $email,
  995. );
  996. }
  997. if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
  998. return array(
  999. 'request-status' => '3.7;No calendar-home-set property found',
  1000. 'href' => 'mailto:' . $email,
  1001. );
  1002. }
  1003. $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
  1004. // Grabbing the calendar list
  1005. $objects = array();
  1006. foreach($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
  1007. if (!$node instanceof ICalendar) {
  1008. continue;
  1009. }
  1010. $aclPlugin->checkPrivileges($homeSet . $node->getName() ,$caldavNS . 'read-free-busy');
  1011. // Getting the list of object uris within the time-range
  1012. $urls = $node->calendarQuery(array(
  1013. 'name' => 'VCALENDAR',
  1014. 'comp-filters' => array(
  1015. array(
  1016. 'name' => 'VEVENT',
  1017. 'comp-filters' => array(),
  1018. 'prop-filters' => array(),
  1019. 'is-not-defined' => false,
  1020. 'time-range' => array(
  1021. 'start' => $start,
  1022. 'end' => $end,
  1023. ),
  1024. ),
  1025. ),
  1026. 'prop-filters' => array(),
  1027. 'is-not-defined' => false,
  1028. 'time-range' => null,
  1029. ));
  1030. $calObjects = array_map(function($url) use ($node) {
  1031. $obj = $node->getChild($url)->get();
  1032. return $obj;
  1033. }, $urls);
  1034. $objects = array_merge($objects,$calObjects);
  1035. }
  1036. $vcalendar = new VObject\Component\VCalendar();
  1037. $vcalendar->VERSION = '2.0';
  1038. $vcalendar->METHOD = 'REPLY';
  1039. $vcalendar->CALSCALE = 'GREGORIAN';
  1040. $vcalendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
  1041. $generator = new VObject\FreeBusyGenerator();
  1042. $generator->setObjects($objects);
  1043. $generator->setTimeRange($start, $end);
  1044. $generator->setBaseObject($vcalendar);
  1045. $result = $generator->getResult();
  1046. $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
  1047. $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
  1048. $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
  1049. return array(
  1050. 'calendar-data' => $result,
  1051. 'request-status' => '2.0;Success',
  1052. 'href' => 'mailto:' . $email,
  1053. );
  1054. }
  1055. /**
  1056. * This method is used to generate HTML output for the
  1057. * DAV\Browser\Plugin. This allows us to generate an interface users
  1058. * can use to create new calendars.
  1059. *
  1060. * @param DAV\INode $node
  1061. * @param string $output
  1062. * @return bool
  1063. */
  1064. public function htmlActionsPanel(DAV\INode $node, &$output) {
  1065. if (!$node instanceof UserCalendars)
  1066. return;
  1067. $output.= '<tr><td colspan="2"><form method="post" action="">
  1068. <h3>Create new calendar</h3>
  1069. <input type="hidden" name="sabreAction" value="mkcalendar" />
  1070. <label>Name (uri):</label> <input type="text" name="name" /><br />
  1071. <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
  1072. <input type="submit" value="create" />
  1073. </form>
  1074. </td></tr>';
  1075. return false;
  1076. }
  1077. /**
  1078. * This method allows us to intercept the 'mkcalendar' sabreAction. This
  1079. * action enables the user to create new calendars from the browser plugin.
  1080. *
  1081. * @param string $uri
  1082. * @param string $action
  1083. * @param array $postVars
  1084. * @return bool
  1085. */
  1086. public function browserPostAction($uri, $action, array $p

Large files files are truncated, but you can click here to view the full file