From 539616f7b7c6d066379bfd4ad6cd77e81d39230e Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Sat, 15 Nov 2014 21:34:38 +0100 Subject: [PATCH] Add a principal and a caldav backend for sabre to work with. Ticket #31 --- .../libs/CalciferCaldavBackend.php | 305 ++++++++++++++++++ .../libs/CalciferPrincipalBackend.php | 158 +++++++++ 2 files changed, 463 insertions(+) create mode 100644 src/Hackspace/Bundle/CalciferBundle/libs/CalciferCaldavBackend.php create mode 100644 src/Hackspace/Bundle/CalciferBundle/libs/CalciferPrincipalBackend.php diff --git a/src/Hackspace/Bundle/CalciferBundle/libs/CalciferCaldavBackend.php b/src/Hackspace/Bundle/CalciferBundle/libs/CalciferCaldavBackend.php new file mode 100644 index 0000000..cf5f95e --- /dev/null +++ b/src/Hackspace/Bundle/CalciferBundle/libs/CalciferCaldavBackend.php @@ -0,0 +1,305 @@ +controller = $controller; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri, which the basename of the uri with which the calendar is + * accessed. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet. + * + * @param string $principalUri + * @return array + */ + public function getCalendarsForUser($principalUri) + { + return [[ + 'id' => 1, + 'uri' => 'calendar', + 'principaluri' => '/caldav/calcifer', + ]]; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return void + */ + public function createCalendar($principalUri, $calendarUri, array $properties) + { + throw new \Exception('Not implemented'); + } + + /** + * Delete a calendar and all it's objects + * + * @param mixed $calendarId + * @return void + */ + public function deleteCalendar($calendarId) + { + throw new \Exception('Not implemented'); + } + + private function FormatCalendarEvent(CalendarEvent $event) + { + $stream = new CalendarStream(); + $formatter = new Formatter(); + $stream->addItem('BEGIN:VEVENT') + ->addItem('UID:' . $event->getUid()) + ->addItem('DTSTART:' . $formatter->getFormattedUTCDateTime($event->getStart())) + ->addItem('DTEND:' . $formatter->getFormattedUTCDateTime($event->getEnd())) + ->addItem('SUMMARY:' . $event->getSummary()) + ->addItem('DESCRIPTION:' . $event->getDescription()); + + if ($event->getClass()) + $stream->addItem('CLASS:' . $event->getClass()); + + /* @var $location Location */ + foreach ($event->getLocations() as $location) { + $stream + ->addItem('LOCATION' . $location->getUri() . $location->getLanguage() . ':' . $location->getName()); + } + + if ($event->getGeo()) + $stream->addItem('GEO:' . $event->getGeo()->getLatitude() . ';' . $event->getGeo()->getLongitude()); + + if ($event->getUrl()) + $stream->addItem('URL:' . $event->getUrl()); + + if ($event->getCreated()) + $stream->addItem('CREATED:' . $formatter->getFormattedUTCDateTime($event->getCreated())); + + if ($event->getLastModified()) + $stream->addItem('LAST-MODIFIED:' . $formatter->getFormattedUTCDateTime($event->getLastModified())); + + foreach ($event->getAttendees() as $attendee) { + $stream->addItem($attendee->__toString()); + } + + if ($event->getOrganizer()) + $stream->addItem($event->getOrganizer()->__toString()); + + $stream->addItem('END:VEVENT'); + + return $stream->getStream(); + } + + private function formatEvent(Event $event) + { + /** @var CalendarEvent $calendar_event */ + $calendar_event = $event->ConvertToCalendarEvent(); + $calendar_data = $this->FormatCalendarEvent($calendar_event); + + $event_data = [ + 'id' => $event->id, + 'uri' => $event->slug, + 'lastmodified' => $event->startdate, + 'etag' => '"' . sha1($calendar_data) . '"', + 'calendarid' => 1, + 'calendardata' => $calendar_data, + 'size' => strlen($calendar_data), + 'component' => 'VEVENT', + ]; + return $event_data; + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * id - unique identifier which will be used for subsequent updates + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * '"abcdef"') + * * calendarid - The calendarid as it was passed to this function. + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * @return array + */ + public function getCalendarObjects($calendarId) + { + /** @var EntityManager $em */ + $em = $this->controller->getDoctrine()->getManager(); + + $now = new \DateTime(); + $now->setTime(0, 0, 0); + /** @var QueryBuilder $qb */ + $qb = $em->createQueryBuilder(); + $qb->select(array('e')) + ->from('CalciferBundle:Event', 'e') + ->where('e.startdate >= :startdate') + ->orderBy('e.startdate') + ->setParameter('startdate', $now); + $entities = $qb->getQuery()->execute(); + + if (count($entities) > 0) { + $events = []; + foreach ($entities as $event) { + /** @var Event $event */ + $events[] = $this->formatEvent($event); + } + return $events; + } + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + /** @var EntityManager $em */ + $em = $this->controller->getDoctrine()->getManager(); + + /** @var EntityRepository $repo */ + $repo = $em->getRepository('CalciferBundle:Event'); + + /** @var Event $entity */ + $event = $repo->findOneBy(['slug' => $objectUri]); + + if (!($event instanceof Event)) { + throw $this->controller->createNotFoundException('Unable to find Event entity.'); + } + + return $this->formatEvent($event); + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + // TODO: Implement createCalendarObject() method. + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + throw new \Exception('Not implemented'); + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + * @return void + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + throw new \Exception('Not implemented'); + } +} \ No newline at end of file diff --git a/src/Hackspace/Bundle/CalciferBundle/libs/CalciferPrincipalBackend.php b/src/Hackspace/Bundle/CalciferBundle/libs/CalciferPrincipalBackend.php new file mode 100644 index 0000000..5ee84de --- /dev/null +++ b/src/Hackspace/Bundle/CalciferBundle/libs/CalciferPrincipalBackend.php @@ -0,0 +1,158 @@ + 'calcifer', + '{http://sabredav.org/ns}email-address' => 'calcifer@example.com', + 'uri' => '/caldav/calcifer', + ] + ]; + } + + /** + * Returns a specific principal, specified by it's path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * @return array + */ + function getPrincipalByPath($path) + { + return [ + '{DAV:}displayname' => 'calcifer', + '{http://sabredav.org/ns}email-address' => 'calcifer@example.com', + 'uri' => '/caldav/calcifer', + ]; + } + + /** + * Updates one ore more webdav properties on a principal. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documenation for more info and examples. + * + * @param string $path + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch) + { + throw new \Exception('Not implemented'); + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. You should at least allow searching on + * http://sabredav.org/ns}email-address. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * If multiple properties are being searched on, the search should be + * AND'ed. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param array $searchProperties + * @return array + */ + function searchPrincipals($prefixPath, array $searchProperties) + { + return [ + [ + '{DAV:}displayname' => 'calcifer', + '{http://sabredav.org/ns}email-address' => 'calcifer@example.com', + 'uri' => '/caldav/calcifer', + ] + ]; + } + + /** + * Returns the list of members for a group-principal + * + * @param string $principal + * @return array + */ + function getGroupMemberSet($principal) + { + throw new \Exception('Not implemented'); + } + + /** + * Returns the list of groups a principal is a member of + * + * @param string $principal + * @return array + */ + function getGroupMembership($principal) + { + throw new \Exception('Not implemented'); + } + + /** + * Updates the list of group members for a group principal. + * + * The principals should be passed as a list of uri's. + * + * @param string $principal + * @param array $members + * @return void + */ + function setGroupMemberSet($principal, array $members) + { + throw new \Exception('Not implemented'); + } +} \ No newline at end of file