/** * PropFind * * This method handler is invoked before any after properties for a * resource are fetched. This allows us to add in any CalDAV specific * properties. * * @param DAV\PropFind $propFind * @param DAV\INode $node * @return void */ function propFind(DAV\PropFind $propFind, DAV\INode $node) { $ns = '{' . self::NS_CALDAV . '}'; if ($node instanceof ICalendarObjectContainer) { $propFind->handle($ns . 'max-resource-size', $this->maxResourceSize); $propFind->handle($ns . 'supported-calendar-data', function () { return new Xml\Property\SupportedCalendarData(); }); $propFind->handle($ns . 'supported-collation-set', function () { return new Xml\Property\SupportedCollationSet(); }); } if ($node instanceof DAVACL\IPrincipal) { $principalUrl = $node->getPrincipalUrl(); $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function () use($principalUrl) { $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl) . '/'; return new Href($calendarHomePath); }); // The calendar-user-address-set property is basically mapped to // the {DAV:}alternate-URI-set property. $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-address-set', function () use($node) { $addresses = $node->getAlternateUriSet(); $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/'; return new Href($addresses, false); }); // For some reason somebody thought it was a good idea to add // another one of these properties. We're supporting it too. $propFind->handle('{' . self::NS_CALENDARSERVER . '}email-address-set', function () use($node) { $addresses = $node->getAlternateUriSet(); $emails = []; foreach ($addresses as $address) { if (substr($address, 0, 7) === 'mailto:') { $emails[] = substr($address, 7); } } return new Xml\Property\EmailAddressSet($emails); }); // These two properties are shortcuts for ical to easily find // other principals this principal has access to. $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for'; $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'; if ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) { $aclPlugin = $this->server->getPlugin('acl'); $membership = $aclPlugin->getPrincipalMembership($propFind->getPath()); $readList = []; $writeList = []; foreach ($membership as $group) { $groupNode = $this->server->tree->getNodeForPath($group); $listItem = Uri\split($group)[0] . '/'; // If the node is either ap proxy-read or proxy-write // group, we grab the parent principal and add it to the // list. if ($groupNode instanceof Principal\IProxyRead) { $readList[] = $listItem; } if ($groupNode instanceof Principal\IProxyWrite) { $writeList[] = $listItem; } } $propFind->set($propRead, new Href($readList)); $propFind->set($propWrite, new Href($writeList)); } } // instanceof IPrincipal if ($node instanceof ICalendarObject) { // The calendar-data property is not supposed to be a 'real' // property, but in large chunks of the spec it does act as such. // Therefore we simply expose it as a property. $propFind->handle('{' . self::NS_CALDAV . '}calendar-data', function () use($node) { $val = $node->get(); if (is_resource($val)) { $val = stream_get_contents($val); } // Taking out \r to not screw up the xml output return str_replace("\r", "", $val); }); } }
/** * PropFind * * @param PropFind $propFind * @param BaseINode $node * @return void */ function propFind(PropFind $propFind, BaseINode $node) { $caldavPlugin = $this->server->getPlugin('caldav'); if ($node instanceof DAVACL\IPrincipal) { $principalUrl = $node->getPrincipalUrl(); // notification-URL property $propFind->handle('{' . self::NS_CALENDARSERVER . '}notification-URL', function () use($principalUrl, $caldavPlugin) { $notificationPath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl) . '/notifications/'; return new DAV\Xml\Property\Href($notificationPath); }); } if ($node instanceof INode) { $propFind->handle('{' . self::NS_CALENDARSERVER . '}notificationtype', [$node, 'getNotificationType']); } }
/** * Triggered before properties are looked up in specific nodes. * * @param DAV\PropFind $propFind * @param DAV\INode $node * @param array $requestedProperties * @param array $returnedProperties * @TODO really should be broken into multiple methods, or even a class. * @return bool */ function propFind(DAV\PropFind $propFind, DAV\INode $node) { $path = $propFind->getPath(); // Checking the read permission if (!$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false)) { // User is not allowed to read properties // Returning false causes the property-fetching system to pretend // that the node does not exist, and will cause it to be hidden // from listings such as PROPFIND or the browser plugin. if ($this->hideNodesFromListings) { return false; } // Otherwise we simply mark every property as 403. foreach ($propFind->getRequestedProperties() as $requestedProperty) { $propFind->set($requestedProperty, null, 403); } return; } /* Adding principal properties */ if ($node instanceof IPrincipal) { $propFind->handle('{DAV:}alternate-URI-set', function () use($node) { return new DAV\Property\HrefList($node->getAlternateUriSet()); }); $propFind->handle('{DAV:}principal-URL', function () use($node) { return new DAV\Property\Href($node->getPrincipalUrl() . '/'); }); $propFind->handle('{DAV:}group-member-set', function () use($node) { $members = $node->getGroupMemberSet(); foreach ($members as $k => $member) { $members[$k] = rtrim($member, '/') . '/'; } return new DAV\Property\HrefList($members); }); $propFind->handle('{DAV:}group-membership', function () use($node) { $members = $node->getGroupMembership(); foreach ($members as $k => $member) { $members[$k] = rtrim($member, '/') . '/'; } return new DAV\Property\HrefList($members); }); $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']); } $propFind->handle('{DAV:}principal-collection-set', function () { $val = $this->principalCollectionSet; // Ensuring all collections end with a slash foreach ($val as $k => $v) { $val[$k] = $v . '/'; } return new DAV\Property\HrefList($val); }); $propFind->handle('{DAV:}current-user-principal', function () { if ($url = $this->getCurrentUserPrincipal()) { return new Property\Principal(Property\Principal::HREF, $url . '/'); } else { return new Property\Principal(Property\Principal::UNAUTHENTICATED); } }); $propFind->handle('{DAV:}supported-privilege-set', function () use($node) { return new Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node)); }); $propFind->handle('{DAV:}current-user-privilege-set', function () use($node, $propFind, $path) { if (!$this->checkPrivileges($path, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) { $propFind->set('{DAV:}current-user-privilege-set', null, 403); } else { $val = $this->getCurrentUserPrivilegeSet($node); if (!is_null($val)) { return new Property\CurrentUserPrivilegeSet($val); } } }); $propFind->handle('{DAV:}acl', function () use($node, $propFind, $path) { /* The ACL property contains all the permissions */ if (!$this->checkPrivileges($path, '{DAV:}read-acl', self::R_PARENT, false)) { $propFind->set('{DAV:}acl', null, 403); } else { $acl = $this->getACL($node); if (!is_null($acl)) { return new Property\Acl($this->getACL($node)); } } }); $propFind->handle('{DAV:}acl-restrictions', function () { return new Property\AclRestrictions(); }); /* Adding ACL properties */ if ($node instanceof IACL) { $propFind->handle('{DAV:}owner', function () use($node) { return new DAV\Property\Href($node->getOwner() . '/'); }); } }
/** * This method handler is invoked during fetching of properties. * * We use this event to add calendar-auto-schedule-specific properties. * * @param PropFind $propFind * @param INode $node * @return void */ function propFind(PropFind $propFind, INode $node) { if (!$node instanceof DAVACL\IPrincipal) { return; } $caldavPlugin = $this->server->getPlugin('caldav'); $principalUrl = $node->getPrincipalUrl(); // schedule-outbox-URL property $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function () use($principalUrl, $caldavPlugin) { $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); $outboxPath = $calendarHomePath . '/outbox/'; return new Href($outboxPath); }); // schedule-inbox-URL property $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function () use($principalUrl, $caldavPlugin) { $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); $inboxPath = $calendarHomePath . '/inbox/'; return new Href($inboxPath); }); $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function () use($principalUrl, $caldavPlugin) { // We don't support customizing this property yet, so in the // meantime we just grab the first calendar in the home-set. $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set'; $result = $this->server->getPropertiesForPath($calendarHomePath, ['{DAV:}resourcetype', $sccs], 1); foreach ($result as $child) { if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar') || $child[200]['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared')) { // Node is either not a calendar or a shared instance. continue; } if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) { // Either there is no supported-calendar-component-set // (which is fine) or we found one that supports VEVENT. return new Href($child['href']); } } }); // The server currently reports every principal to be of type // 'INDIVIDUAL' $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function () { return 'INDIVIDUAL'; }); }
/** * Triggered before properties are looked up in specific nodes. * * @param string $uri * @param DAV\INode $node * @param array $requestedProperties * @param array $returnedProperties * @TODO really should be broken into multiple methods, or even a class. * @return bool */ public function beforeGetProperties($uri, DAV\INode $node, &$requestedProperties, &$returnedProperties) { // Checking the read permission if (!$this->checkPrivileges($uri, '{DAV:}read', self::R_PARENT, false)) { // User is not allowed to read properties if ($this->hideNodesFromListings) { return false; } // Marking all requested properties as '403'. foreach ($requestedProperties as $key => $requestedProperty) { unset($requestedProperties[$key]); $returnedProperties[403][$requestedProperty] = null; } return; } /* Adding principal properties */ if ($node instanceof IPrincipal) { if (false !== ($index = array_search('{DAV:}alternate-URI-set', $requestedProperties))) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}alternate-URI-set'] = new DAV\Property\HrefList($node->getAlternateUriSet()); } if (false !== ($index = array_search('{DAV:}principal-URL', $requestedProperties))) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}principal-URL'] = new DAV\Property\Href($node->getPrincipalUrl() . '/'); } if (false !== ($index = array_search('{DAV:}group-member-set', $requestedProperties))) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}group-member-set'] = new DAV\Property\HrefList($node->getGroupMemberSet()); } if (false !== ($index = array_search('{DAV:}group-membership', $requestedProperties))) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}group-membership'] = new DAV\Property\HrefList($node->getGroupMembership()); } if (false !== ($index = array_search('{DAV:}displayname', $requestedProperties))) { $returnedProperties[200]['{DAV:}displayname'] = $node->getDisplayName(); } } if (false !== ($index = array_search('{DAV:}principal-collection-set', $requestedProperties))) { unset($requestedProperties[$index]); $val = $this->principalCollectionSet; // Ensuring all collections end with a slash foreach ($val as $k => $v) { $val[$k] = $v . '/'; } $returnedProperties[200]['{DAV:}principal-collection-set'] = new DAV\Property\HrefList($val); } if (false !== ($index = array_search('{DAV:}current-user-principal', $requestedProperties))) { unset($requestedProperties[$index]); if ($url = $this->getCurrentUserPrincipal()) { $returnedProperties[200]['{DAV:}current-user-principal'] = new Property\Principal(Property\Principal::HREF, $url . '/'); } else { $returnedProperties[200]['{DAV:}current-user-principal'] = new Property\Principal(Property\Principal::UNAUTHENTICATED); } } if (false !== ($index = array_search('{DAV:}supported-privilege-set', $requestedProperties))) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}supported-privilege-set'] = new Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node)); } if (false !== ($index = array_search('{DAV:}current-user-privilege-set', $requestedProperties))) { if (!$this->checkPrivileges($uri, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) { $returnedProperties[403]['{DAV:}current-user-privilege-set'] = null; unset($requestedProperties[$index]); } else { $val = $this->getCurrentUserPrivilegeSet($node); if (!is_null($val)) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}current-user-privilege-set'] = new Property\CurrentUserPrivilegeSet($val); } } } /* The ACL property contains all the permissions */ if (false !== ($index = array_search('{DAV:}acl', $requestedProperties))) { if (!$this->checkPrivileges($uri, '{DAV:}read-acl', self::R_PARENT, false)) { unset($requestedProperties[$index]); $returnedProperties[403]['{DAV:}acl'] = null; } else { $acl = $this->getACL($node); if (!is_null($acl)) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}acl'] = new Property\Acl($this->getACL($node)); } } } /* The acl-restrictions property contains information on how privileges * must behave. */ if (false !== ($index = array_search('{DAV:}acl-restrictions', $requestedProperties))) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}acl-restrictions'] = new Property\AclRestrictions(); } /* Adding ACL properties */ if ($node instanceof IACL) { if (false !== ($index = array_search('{DAV:}owner', $requestedProperties))) { unset($requestedProperties[$index]); $returnedProperties[200]['{DAV:}owner'] = new DAV\Property\Href($node->getOwner() . '/'); } } }
/** * beforeGetProperties * * This method handler is invoked before any after properties for a * resource are fetched. This allows us to add in any CalDAV specific * properties. * * @param string $path * @param DAV\INode $node * @param array $requestedProperties * @param array $returnedProperties * @return void */ public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) { if ($node instanceof DAVACL\IPrincipal) { // calendar-home-set property $calHome = '{' . self::NS_CALDAV . '}calendar-home-set'; if (in_array($calHome, $requestedProperties)) { $principalId = $node->getName(); $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/'; unset($requestedProperties[array_search($calHome, $requestedProperties)]); $returnedProperties[200][$calHome] = new DAV\Property\Href($calendarHomePath); } // schedule-outbox-URL property $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL'; if (in_array($scheduleProp, $requestedProperties)) { $principalId = $node->getName(); $outboxPath = self::CALENDAR_ROOT . '/' . $principalId . '/outbox'; unset($requestedProperties[array_search($scheduleProp, $requestedProperties)]); $returnedProperties[200][$scheduleProp] = new DAV\Property\Href($outboxPath); } // calendar-user-address-set property $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set'; if (in_array($calProp, $requestedProperties)) { $addresses = $node->getAlternateUriSet(); $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/'; unset($requestedProperties[array_search($calProp, $requestedProperties)]); $returnedProperties[200][$calProp] = new DAV\Property\HrefList($addresses, false); } // These two properties are shortcuts for ical to easily find // other principals this principal has access to. $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for'; $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'; if (in_array($propRead, $requestedProperties) || in_array($propWrite, $requestedProperties)) { $aclPlugin = $this->server->getPlugin('acl'); $membership = $aclPlugin->getPrincipalMembership($path); $readList = array(); $writeList = array(); foreach ($membership as $group) { $groupNode = $this->server->tree->getNodeForPath($group); // If the node is either ap proxy-read or proxy-write // group, we grab the parent principal and add it to the // list. if ($groupNode instanceof Principal\IProxyRead) { list($readList[]) = DAV\URLUtil::splitPath($group); } if ($groupNode instanceof Principal\IProxyWrite) { list($writeList[]) = DAV\URLUtil::splitPath($group); } } if (in_array($propRead, $requestedProperties)) { unset($requestedProperties[$propRead]); $returnedProperties[200][$propRead] = new DAV\Property\HrefList($readList); } if (in_array($propWrite, $requestedProperties)) { unset($requestedProperties[$propWrite]); $returnedProperties[200][$propWrite] = new DAV\Property\HrefList($writeList); } } // notification-URL property $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL'; if (($index = array_search($notificationUrl, $requestedProperties)) !== false) { $principalId = $node->getName(); $calendarHomePath = 'calendars/' . $principalId . '/notifications/'; unset($requestedProperties[$index]); $returnedProperties[200][$notificationUrl] = new DAV\Property\Href($calendarHomePath); } } // instanceof IPrincipal if ($node instanceof Notifications\INode) { $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype'; if (($index = array_search($propertyName, $requestedProperties)) !== false) { $returnedProperties[200][$propertyName] = $node->getNotificationType(); unset($requestedProperties[$index]); } } // instanceof Notifications_INode if ($node instanceof ICalendarObject) { // The calendar-data property is not supposed to be a 'real' // property, but in large chunks of the spec it does act as such. // Therefore we simply expose it as a property. $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data'; if (in_array($calDataProp, $requestedProperties)) { unset($requestedProperties[$calDataProp]); $val = $node->get(); if (is_resource($val)) { $val = stream_get_contents($val); } // Taking out \r to not screw up the xml output $returnedProperties[200][$calDataProp] = str_replace("\r", "", $val); } } }
/** * This method handler is invoked during fetching of properties. * * We use this event to add calendar-auto-schedule-specific properties. * * @param PropFind $propFind * @param INode $node * @return void */ function propFind(PropFind $propFind, INode $node) { if ($node instanceof DAVACL\IPrincipal) { $caldavPlugin = $this->server->getPlugin('caldav'); $principalUrl = $node->getPrincipalUrl(); // schedule-outbox-URL property $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function () use($principalUrl, $caldavPlugin) { $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); if (!$calendarHomePath) { return null; } $outboxPath = $calendarHomePath . '/outbox/'; return new LocalHref($outboxPath); }); // schedule-inbox-URL property $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function () use($principalUrl, $caldavPlugin) { $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); if (!$calendarHomePath) { return null; } $inboxPath = $calendarHomePath . '/inbox/'; return new LocalHref($inboxPath); }); $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function () use($principalUrl, $caldavPlugin) { // We don't support customizing this property yet, so in the // meantime we just grab the first calendar in the home-set. $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); if (!$calendarHomePath) { return null; } $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set'; $result = $this->server->getPropertiesForPath($calendarHomePath, ['{DAV:}resourcetype', '{DAV:}share-access', $sccs], 1); foreach ($result as $child) { if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar')) { // Node is either not a calendar continue; } if (isset($child[200]['{DAV:}share-access'])) { $shareAccess = $child[200]['{DAV:}share-access']->getValue(); if ($shareAccess !== Sharing\Plugin::ACCESS_NOTSHARED && $shareAccess !== Sharing\Plugin::ACCESS_SHAREDOWNER) { // Node is a shared node, not owned by the relevant // user. continue; } } if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) { // Either there is no supported-calendar-component-set // (which is fine) or we found one that supports VEVENT. return new LocalHref($child['href']); } } }); // The server currently reports every principal to be of type // 'INDIVIDUAL' $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function () { return 'INDIVIDUAL'; }); } // Mapping the old property to the new property. $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use($propFind, $node) { // In case it wasn't clear, the only difference is that we map the // old property to a different namespace. $availProp = '{' . self::NS_CALDAV . '}calendar-availability'; $subPropFind = new PropFind($propFind->getPath(), [$availProp]); $this->server->getPropertiesByNode($subPropFind, $node); $propFind->set('{http://calendarserver.org/ns/}calendar-availability', $subPropFind->get($availProp), $subPropFind->getStatus($availProp)); }); }
/** * This method handler is invoked during fetching of properties. * * We use this event to add calendar-auto-schedule-specific properties. * * @param PropFind $propFind * @param INode $node * @return void */ function propFind(PropFind $propFind, INode $node) { if (!$node instanceof DAVACL\IPrincipal) { return; } $caldavPlugin = $this->server->getPlugin('caldav'); $principalUrl = $node->getPrincipalUrl(); // schedule-outbox-URL property $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function () use($principalUrl, $caldavPlugin) { $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); $outboxPath = $calendarHomePath . '/outbox/'; return new Href($outboxPath); }); // schedule-inbox-URL property $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function () use($principalUrl, $caldavPlugin) { $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); $inboxPath = $calendarHomePath . '/inbox/'; return new Href($inboxPath); }); $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function () use($principalUrl, $caldavPlugin) { // We don't support customizing this property yet, so in the // meantime we just grab the first calendar in the home-set. $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); $nodes = $this->server->tree->getNodeForPath($calendarHomePath)->getChildren(); foreach ($nodes as $node) { if ($node instanceof ICalendar) { return new Href($calendarHomePath . '/' . $node->getName()); } } }); // The server currently reports every principal to be of type // 'INDIVIDUAL' $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function () { return 'INDIVIDUAL'; }); }