创建版本

This commit is contained in:
2021-02-22 12:17:00 +08:00
commit af555f49c3
2332 changed files with 369202 additions and 0 deletions
@@ -0,0 +1,56 @@
<?php namespace Gdoo\Calendar\Sabre\Connector;
/**
* This is an authentication backend that uses a file to manage passwords.
*
* The backend file must conform to Apache's htdigest format
*
* @package Sabre
* @subpackage DAV
* @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Auth extends \Sabre\DAV\Auth\Backend\AbstractBasic
{
/**
* Validates a username and password
*
* This method should return true or false depending on if login
* succeeded.
*
* @return bool
*/
protected function validateUserPass($username, $password)
{
if (\Auth::check()) {
return true;
} else {
$credentials = [
'username' => $username,
'password' => $password
];
if (\Auth::attempt($credentials)) {
return true;
} else {
return false;
}
}
}
/**
* Returns information about the currently logged in username.
*
* If nobody is currently logged in, this method should return null.
*
* @return string|null
*/
public function getCurrentUser()
{
if (\Auth::id() > 0) {
return \Auth::user()->username;
}
return null;
}
}
@@ -0,0 +1,649 @@
<?php namespace Gdoo\Calendar\Sabre\Connector;
/**
* PDO CalDAV backend
*
* This backend is used to store calendar-data in a PDO database, such as
* sqlite or MySQL
*
* @copyright Copyright (C) 2007-2014 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
use Gdoo\Index\Models\Share;
use Gdoo\Calendar\Models\Calendar;
use Gdoo\Calendar\Models\CalendarObject;
use Gdoo\Index\Services\ShareService;
use Gdoo\Calendar\Services\CalendarService;
use Gdoo\Calendar\Services\CalendarObjectService;
use Illuminate\Support\Arr;
class CalDAV extends \Sabre\CalDAV\Backend\AbstractBackend
{
/**
* We need to specify a max date, because we need to stop *somewhere*
*
* On 32 bit system the maximum for a signed integer is 2147483647, so
* MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
* in 2038-01-19 to avoid problems when the date is converted
* to a unix timestamp.
*/
const MAX_DATE = '2038-01-01';
/**
* List of CalDAV properties, and how they map to database fieldnames
* Add your own properties by simply adding on to this array.
*
* Note that only string-based properties are supported here.
*
* @var array
*/
public $propertyMap = array(
'{DAV:}displayname' => 'displayname',
'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
'{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
'{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
);
/**
* 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'.
*
* @param string $principalUri
* @return array
*/
public function getCalendarsForUser($principalUri)
{
$fields = array_values($this->propertyMap);
$fields[] = 'id';
$fields[] = 'uri';
$fields[] = 'ctag';
$fields[] = 'components';
$fields[] = 'principaluri';
$fields[] = 'transparent';
$rows = Calendar::where('principaluri', $principalUri)->get($fields);
foreach ($rows as $row) {
$components = array();
if ($row->components) {
$components = explode(',', $row->components);
}
$calendar = array(
'id' => $row->id,
'uri' => $row->uri,
'principaluri' => $row->principaluri,
'{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $row->ctag?$row->ctag:'0',
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new \Sabre\CalDAV\Property\SupportedCalendarComponentSet($components),
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new \Sabre\CalDAV\Property\ScheduleCalendarTransp($row->transparent?'transparent':'opaque'),
);
foreach ($this->propertyMap as $xmlName => $dbName) {
$calendar[$xmlName] = $row[$dbName];
}
$calendars[] = $calendar;
}
return $calendars;
}
/**
* 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 string
*/
public function createCalendar($principalUri, $calendarUri, array $properties)
{
$fieldNames = array(
'principaluri',
'uri',
'ctag',
'transparent',
);
$values = array(
':principaluri' => $principalUri,
':uri' => $calendarUri,
':ctag' => 1,
':transparent' => 0,
);
$data = array(
'principaluri' => $principalUri,
'uri' => $calendarUri,
'ctag' => 1,
'transparent' => 0,
);
// Default value
$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
$fieldNames[] = 'components';
if (!isset($properties[$sccs])) {
$values[':components'] = 'VEVENT,VTODO';
} else {
if (!($properties[$sccs] instanceof \Sabre\CalDAV\Property\SupportedCalendarComponentSet)) {
throw new \Sabre\DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
}
$values[':components'] = implode(',', $properties[$sccs]->getValue());
}
$transp = '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
if (isset($properties[$transp])) {
$values[':transparent'] = $properties[$transp]->getValue()==='transparent';
}
foreach ($this->propertyMap as $xmlName => $dbName) {
if (isset($properties[$xmlName])) {
$values[':' . $dbName] = $properties[$xmlName];
$fieldNames[] = $dbName;
$data[$dbName] = $properties[$xmlName];
}
}
return Calendar::insertGetId($data);
}
/**
* Updates properties for a calendar.
*
* The mutations array uses the propertyName in clark-notation as key,
* and the array value for the property value. In the case a property
* should be deleted, the property value will be null.
*
* This method must be atomic. If one property cannot be changed, the
* entire operation must fail.
*
* If the operation was successful, true can be returned.
* If the operation failed, false can be returned.
*
* Deletion of a non-existent property is always successful.
*
* Lastly, it is optional to return detailed information about any
* failures. In this case an array should be returned with the following
* structure:
*
* array(
* 403 => array(
* '{DAV:}displayname' => null,
* ),
* 424 => array(
* '{DAV:}owner' => null,
* )
* )
*
* In this example it was forbidden to update {DAV:}displayname.
* (403 Forbidden), which in turn also caused {DAV:}owner to fail
* (424 Failed Dependency) because the request needs to be atomic.
*
* @param string $calendarId
* @param array $mutations
* @return bool|array
*/
public function updateCalendar($calendarId, array $mutations)
{
$newValues = array();
$result = array(
200 => array(), // Ok
403 => array(), // Forbidden
424 => array(), // Failed Dependency
);
$hasError = false;
foreach ($mutations as $propertyName => $propertyValue) {
switch ($propertyName) {
case '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp':
$fieldName = 'transparent';
$newValues[$fieldName] = $propertyValue->getValue()==='transparent';
break;
default:
// Checking the property map
if (!isset($this->propertyMap[$propertyName])) {
// We don't know about this property.
$hasError = true;
$result[403][$propertyName] = null;
unset($mutations[$propertyName]);
continue;
}
$fieldName = $this->propertyMap[$propertyName];
$newValues[$fieldName] = $propertyValue;
}
}
// If there were any errors we need to fail the request
if ($hasError) {
// Properties has the remaining properties
foreach ($mutations as $propertyName => $propertyValue) {
$result[424][$propertyName] = null;
}
// Removing unused statuscodes for cleanliness
foreach ($result as $status => $properties) {
if (is_array($properties) && count($properties)===0) {
unset($result[$status]);
}
}
return $result;
}
// Success
// Now we're generating the sql query.
Calendar::where('id', $calendarId)->update($newValues);
CalendarService::touchCalendar($calendarId);
return true;
}
/**
* Delete a calendar and all it's objects
*
* @param string $calendarId
* @return void
*/
public function deleteCalendar($calendarId)
{
CalendarService::deleteCalendar($calendarId);
}
/**
* 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.
* * 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.
*
* 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 string $calendarId
* @return array
*/
public function getCalendarObjects($calendarId)
{
$model = CalendarObject::select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size']);
if ($calendarId == 'share-events') {
$shared = ShareService::getItemsSourceBy(['event'], \Auth::id());
if ($shared->isEmpty()) {
return array();
}
$share_id = Arr::pluck($shared, 'source_id');
$rows = $model->whereIn('id', $share_id)->get();
foreach ($rows as $key => $row) {
$rows[$key]['calendarid'] = 'share-events';
}
} elseif ($calendarId > 0) {
$rows = $model->where('calendarid', $calendarId)->get();
}
$result = array();
foreach ($rows as $row) {
$result[] = array(
'id' => $row->id,
'uri' => $row->uri,
'lastmodified' => $row->lastmodified,
'etag' => '"' . $row->etag . '"',
'calendarid' => $row->calendarid,
'size' => (int)$row->size,
);
}
return $result;
}
/**
* Returns information from a single calendar object, based on it's object
* uri.
*
* 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 string $calendarId
* @param string $objectUri
* @return array|null
*/
public function getCalendarObject($calendarId, $objectUri)
{
$model = CalendarObject::where('uri', $objectUri);
if ($calendarId == 'share-events') {
$shared = ShareService::getItemsSourceBy(['event'], \Auth::id());
if ($shared->isEmpty()) {
return array();
}
$shareId = Arr::pluck($shared, 'source_id');
$shareUsersList = Arr::pluck($shared, 'name', 'source_id');
$row = $model->whereIn('id', $shareId)->first();
$row->calendarid = 'share-events';
$vcalendar = \Sabre\VObject\Reader::read($row->calendardata);
$vcalendar->VEvent->SUMMARY->value = '['.$shareUsersList[$row->id].']'.$vcalendar->VEvent->SUMMARY->value;
$row->calendardata = $vcalendar->serialize();
} elseif ($calendarId > 0) {
$row = $model->where('calendarid', $calendarId)->first();
}
if (empty($row)) {
return array();
}
return array(
'id' => $row->id,
'uri' => $row->uri,
'lastmodified' => $row->lastmodified,
'etag' => '"' . $row->etag . '"',
'calendarid' => $row->calendarid,
'size' => (int)$row->size,
'calendardata' => $row->calendardata,
);
}
/**
* Creates a new calendar object.
*
* 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)
{
$extraData = $this->getDenormalizedData($calendarData);
$data = array(
'calendarid' => $calendarId,
'uri' => $objectUri,
'calendardata' => $calendarData,
'lastmodified' => time(),
'etag' => $extraData['etag'],
'size' => $extraData['size'],
'componenttype' => $extraData['componentType'],
'firstoccurence' => $extraData['firstOccurence'],
'lastoccurence' => $extraData['lastOccurence'],
);
CalendarObject::insert($data);
CalendarService::touchCalendar($calendarId);
return '"' . $extraData['etag'] . '"';
}
/**
* Updates an existing calendarobject, based on it's uri.
*
* 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)
{
$extraData = $this->getDenormalizedData($calendarData);
$update = array(
'calendardata' => $calendarData,
'lastmodified' => time(),
'etag' => $extraData['etag'],
'size' => $extraData['size'],
'componenttype' => $extraData['componentType'],
'firstoccurence' => $extraData['firstOccurence'],
'lastoccurence' => $extraData['lastOccurence'],
);
CalendarObject::where('calendarid', $calendarId)->where('uri', $objectUri)->update($update);
CalendarService::touchCalendar($calendarId);
return '"' . $extraData['etag'] . '"';
}
/**
* Parses some information from calendar objects, used for optimized
* calendar-queries.
*
* Returns an array with the following keys:
* * etag
* * size
* * componentType
* * firstOccurence
* * lastOccurence
*
* @param string $calendarData
* @return array
*/
protected function getDenormalizedData($calendarData)
{
$vObject = \Sabre\VObject\Reader::read($calendarData);
$componentType = null;
$component = null;
$firstOccurence = null;
$lastOccurence = null;
foreach ($vObject->getComponents() as $component) {
if ($component->name!=='VTIMEZONE') {
$componentType = $component->name;
break;
}
}
if (!$componentType) {
throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
}
if ($componentType === 'VEVENT') {
$firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
// Finding the last occurence is a bit harder
if (!isset($component->RRULE)) {
if (isset($component->DTEND)) {
$lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
} elseif (isset($component->DURATION)) {
$endDate = clone $component->DTSTART->getDateTime();
$endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->getValue()));
$lastOccurence = $endDate->getTimeStamp();
} elseif (!$component->DTSTART->hasTime()) {
$endDate = clone $component->DTSTART->getDateTime();
$endDate->modify('+1 day');
$lastOccurence = $endDate->getTimeStamp();
} else {
$lastOccurence = $firstOccurence;
}
} else {
$it = new \Sabre\VObject\RecurrenceIterator($vObject, (string)$component->UID);
$maxDate = new \DateTime(self::MAX_DATE);
if ($it->isInfinite()) {
$lastOccurence = $maxDate->getTimeStamp();
} else {
$end = $it->getDtEnd();
while ($it->valid() && $end < $maxDate) {
$end = $it->getDtEnd();
$it->next();
}
$lastOccurence = $end->getTimeStamp();
}
}
}
return array(
'etag' => md5($calendarData),
'size' => strlen($calendarData),
'componentType' => $componentType,
'firstOccurence' => $firstOccurence,
'lastOccurence' => $lastOccurence,
);
}
/**
* Deletes an existing calendar object.
*
* @param string $calendarId
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($calendarId, $objectUri)
{
CalendarObject::where('calendarid', $calendarId)->where('uri', $objectUri)->delete();
CalendarService::touchCalendar($calendarId);
}
/**
* Performs a calendar-query on the contents of this calendar.
*
* The calendar-query is defined in RFC4791 : CalDAV. Using the
* calendar-query it is possible for a client to request a specific set of
* object, based on contents of iCalendar properties, date-ranges and
* iCalendar component types (VTODO, VEVENT).
*
* This method should just return a list of (relative) urls that match this
* query.
*
* The list of filters are specified as an array. The exact array is
* documented by \Sabre\CalDAV\CalendarQueryParser.
*
* Note that it is extremely likely that getCalendarObject for every path
* returned from this method will be called almost immediately after. You
* may want to anticipate this to speed up these requests.
*
* This method provides a default implementation, which parses *all* the
* iCalendar objects in the specified calendar.
*
* This default may well be good enough for personal use, and calendars
* that aren't very large. But if you anticipate high usage, big calendars
* or high loads, you are strongly adviced to optimize certain paths.
*
* The best way to do so is override this method and to optimize
* specifically for 'common filters'.
*
* Requests that are extremely common are:
* * requests for just VEVENTS
* * requests for just VTODO
* * requests with a time-range-filter on a VEVENT.
*
* ..and combinations of these requests. It may not be worth it to try to
* handle every possible situation and just rely on the (relatively
* easy to use) CalendarQueryValidator to handle the rest.
*
* Note that especially time-range-filters may be difficult to parse. A
* time-range filter specified on a VEVENT must for instance also handle
* recurrence rules correctly.
* A good example of how to interprete all these filters can also simply
* be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
* as possible, so it gives you a good idea on what type of stuff you need
* to think of.
*
* This specific implementation (for the PDO) backend optimizes filters on
* specific components, and VEVENT time-ranges.
*
* @param string $calendarId
* @param array $filters
* @return array
*/
public function calendarQuery($calendarId, array $filters)
{
$result = array();
$validator = new \Sabre\CalDAV\CalendarQueryValidator();
$componentType = null;
$requirePostFilter = true;
$timeRange = null;
// if no filters were specified, we don't need to filter after a query
if (!$filters['prop-filters'] && !$filters['comp-filters']) {
$requirePostFilter = false;
}
// Figuring out if there's a component filter
if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
$componentType = $filters['comp-filters'][0]['name'];
// Checking if we need post-filters
if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
$requirePostFilter = false;
}
// There was a time-range filter
if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
$timeRange = $filters['comp-filters'][0]['time-range'];
// If start time OR the end time is not specified, we can do a
// 100% accurate mysql query.
if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
$requirePostFilter = false;
}
}
}
if ($requirePostFilter) {
$model = CalendarObject::select(array('uri', 'calendardata'));
} else {
$model = CalendarObject::select(array('uri'));
}
if ($calendarId == 'share-events') {
$shared = ShareService::getItemsSourceBy(['event'], \Auth::id());
$shareId = Arr::pluck($shared, 'source_id');
$model->whereIn('id', $shareId);
} else {
$model->where('calendarid', $calendarId);
}
if ($componentType) {
$model->where('componenttype', $componentType);
}
if ($timeRange && $timeRange['start']) {
$model->where('lastoccurence', '>', $timeRange['start']->getTimeStamp());
}
if ($timeRange && $timeRange['end']) {
$model->where('firstoccurence', '<', $timeRange['end']->getTimeStamp());
}
$rows = $model->get()->toArray();
foreach ($rows as $row) {
if ($requirePostFilter) {
if (!$this->validateFilterForObject($row, $filters)) {
continue;
}
}
$result[] = $row['uri'];
}
return $result;
}
}
@@ -0,0 +1,406 @@
<?php namespace Gdoo\Calendar\Sabre\Connector;
use Sabre\DAV;
use Sabre\DAVACL;
/**
* PDO principal backend
*
*
* This backend assumes all principals are in a single collection. The default collection
* is 'principals/', but this can be overriden.
*
* @copyright Copyright (C) 2007-2014 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
use Gdoo\User\Models\User;
class Principal extends DAVACL\PrincipalBackend\AbstractBackend
{
/**
* pdo
*
* @var PDO
*/
protected $pdo;
/**
* PDO table name for 'principals'
*
* @var string
*/
protected $tableName;
/**
* PDO table name for 'group members'
*
* @var string
*/
protected $groupMembersTableName;
/**
* A list of additional fields to support
*
* @var array
*/
protected $fieldMap = array(
/**
* This property can be used to display the users' real name.
*/
'{DAV:}displayname' => array(
'dbField' => 'displayname',
),
/**
* This property is actually used by the CardDAV plugin, where it gets
* mapped to {http://calendarserver.orgi/ns/}me-card.
*
* The reason we don't straight-up use that property, is because
* me-card is defined as a property on the users' addressbook
* collection.
*/
'{http://sabredav.org/ns}vcard-url' => array(
'dbField' => 'vcardurl',
),
/**
* This is the users' primary email-address.
*/
'{http://sabredav.org/ns}email-address' => array(
'dbField' => 'email',
),
);
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
* {http://sabredav.org/ns}email-address - This is a custom SabreDAV
* field that's actualy injected in a number of other properties. If
* you have an email address, use this property.
*
* @param string $prefixPath
* @return array
*/
public function getPrincipalsByPrefix($prefixPath)
{
if ($prefixPath == 'principals') {
$users = User::where('status', 1)
->where('id', \Auth::id())
->get(['id', 'username', 'name', 'email']);
foreach ($users as $user) {
$principals[] = array(
'id' => $user['id'],
'uri' => 'principals/'.$user['username'],
'{DAV:}displayname' => $user['name'],
'{http://sabredav.org/ns}email-address' => $user['email'],
);
}
}
return $principals;
}
/**
* 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
*/
public function getPrincipalByPath($path)
{
list($prefix, $username) = explode('/', $path);
$user = User::where('username', $username)->first(['id', 'username', 'name', 'email']);
if ($prefix == 'principals') {
return array(
'id' => $user['id'],
'uri' => 'principals/'.$user['username'],
'{DAV:}displayname' => $user['name'],
'{http://sabredav.org/ns}email-address' => $user['email'],
);
}
$fields = array(
'id',
'uri',
);
foreach ($this->fieldMap as $key => $value) {
$fields[] = $value['dbField'];
}
$stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '. $this->tableName . ' WHERE uri = ?');
$stmt->execute(array($path));
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return;
}
$principal = array(
'id' => $row['id'],
'uri' => $row['uri'],
);
foreach ($this->fieldMap as $key => $value) {
if ($row[$value['dbField']]) {
$principal[$key] = $row[$value['dbField']];
}
}
return $principal;
}
/**
* Updates one ore more webdav properties on a principal.
*
* The list of mutations is supplied as an array. Each key in the array is
* a propertyname, such as {DAV:}displayname.
*
* Each value is the actual value to be updated. If a value is null, it
* must be deleted.
*
* This method should be atomic. It must either completely succeed, or
* completely fail. Success and failure can simply be returned as 'true' or
* 'false'.
*
* It is also possible to return detailed failure information. In that case
* an array such as this should be returned:
*
* array(
* 200 => array(
* '{DAV:}prop1' => null,
* ),
* 201 => array(
* '{DAV:}prop2' => null,
* ),
* 403 => array(
* '{DAV:}prop3' => null,
* ),
* 424 => array(
* '{DAV:}prop4' => null,
* ),
* );
*
* In this previous example prop1 was successfully updated or deleted, and
* prop2 was succesfully created.
*
* prop3 failed to update due to '403 Forbidden' and because of this prop4
* also could not be updated with '424 Failed dependency'.
*
* This last example was actually incorrect. While 200 and 201 could appear
* in 1 response, if there's any error (403) the other properties should
* always fail with 423 (failed dependency).
*
* But anyway, if you don't want to scratch your head over this, just
* return true or false.
*
* @param string $path
* @param array $mutations
* @return array|bool
*/
public function updatePrincipal($path, $mutations)
{
$updateAble = array();
foreach ($mutations as $key => $value) {
// We are not aware of this field, we must fail.
if (!isset($this->fieldMap[$key])) {
$response = array(
403 => array(
$key => null,
),
424 => array(),
);
// Adding the rest to the response as a 424
foreach ($mutations as $subKey => $subValue) {
if ($subKey !== $key) {
$response[424][$subKey] = null;
}
}
return $response;
}
$updateAble[$this->fieldMap[$key]['dbField']] = $value;
}
// No fields to update
$query = "UPDATE " . $this->tableName . " SET ";
$first = true;
foreach ($updateAble as $key => $value) {
if (!$first) {
$query.= ', ';
}
$first = false;
$query.= "$key = :$key ";
}
$query.='WHERE uri = :uri';
$stmt = $this->pdo->prepare($query);
$updateAble['uri'] = $path;
$stmt->execute($updateAble);
return true;
}
/**
* 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
*/
public function searchPrincipals($prefixPath, array $searchProperties)
{
$query = 'SELECT uri FROM ' . $this->tableName . ' WHERE 1=1 ';
$values = array();
foreach ($searchProperties as $property => $value) {
switch ($property) {
case '{DAV:}displayname':
$query.=' AND displayname LIKE ?';
$values[] = '%' . $value . '%';
break;
case '{http://sabredav.org/ns}email-address':
$query.=' AND email LIKE ?';
$values[] = '%' . $value . '%';
break;
default:
// Unsupported property
return array();
}
}
$stmt = $this->pdo->prepare($query);
$stmt->execute($values);
$principals = array();
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
// Checking if the principal is in the prefix
list($rowPrefix) = DAV\URLUtil::splitPath($row['uri']);
if ($rowPrefix !== $prefixPath) {
continue;
}
$principals[] = $row['uri'];
}
return $principals;
}
/**
* Returns the list of members for a group-principal
*
* @param string $principal
* @return array
*/
public function getGroupMemberSet($principal)
{
$principal = $this->getPrincipalByPath($principal);
if (!$principal) {
throw new DAV\Exception('Principal not found');
}
$stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?');
$stmt->execute(array($principal['id']));
$result = array();
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$result[] = $row['uri'];
}
return $result;
}
/**
* Returns the list of groups a principal is a member of
*
* @param string $principal
* @return array
*/
public function getGroupMembership($principal)
{
return array();
$principal = $this->getPrincipalByPath($principal);
if (!$principal) {
throw new DAV\Exception('Principal not found');
}
$stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?');
$stmt->execute(array($principal['id']));
$result = array();
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$result[] = $row['uri'];
}
return $result;
}
/**
* 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
*/
public function setGroupMemberSet($principal, array $members)
{
// Grabbing the list of principal id's.
$stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? ' . str_repeat(', ? ', count($members)) . ');');
$stmt->execute(array_merge(array($principal), $members));
$memberIds = array();
$principalId = null;
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
if ($row['uri'] == $principal) {
$principalId = $row['id'];
} else {
$memberIds[] = $row['id'];
}
}
if (!$principalId) {
throw new DAV\Exception('Principal not found');
}
// Wiping out old members
$stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;');
$stmt->execute(array($principalId));
foreach ($memberIds as $memberId) {
$stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);');
$stmt->execute(array($principalId, $memberId));
}
}
}
@@ -0,0 +1,396 @@
<?php namespace Gdoo\Calendar\Sabre\Connector\Share;
use Gdoo\Calendar\Sabre\Connector;
use Auth;
use Gdoo\Calendar\Models\Calendar;
use Gdoo\Index\Services\ShareService;
use Gdoo\Calendar\Services\CalendarService;
class CalDAV extends Connector\CalDAV implements \Sabre\CalDAV\Backend\SharingSupport
{
/**
* List of properties for the calendar shares table
* This list maps exactly to the field names in the db table
*/
public $sharesProperties = array(
'calendarId',
'member',
'status',
'readOnly',
'summary',
//'displayName',
//'colour'
);
public $sharedEvents = array(
'displayname' => '共享事件',
'color' => '#999999',
);
/**
* Updates the list of shares.
*
* The first array is a list of people that are to be added to the
* calendar.
*
* Every element in the add array has the following properties:
* * href - A url. Usually a mailto: address
* * commonName - Usually a first and last name, or false
* * summary - A description of the share, can also be false
* * readOnly - A boolean value
*
* Every element in the remove array is just the address string.
*
* Note that if the calendar is currently marked as 'not shared' by and
* this method is called, the calendar should be 'upgraded' to a shared
* calendar.
*
* @param mixed $calendarId
* @param array $add
* @param array $remove
* @return void
*/
public function updateShares($calendarId, array $add, array $remove)
{
$fields = array();
$fields[':calendarId'] = $calendarId;
// get the principal based on the supplied email address
$principal = $this->getPrincipalByEmail($add[0]['href']);
$fields[':member'] = $principal['id'];
$fields[':status'] = \Sabre\CalDAV\SharingPlugin::STATUS_NORESPONSE;
// check we have all the required fields
foreach ($this->sharesProperties as $field) {
if (isset($add[0][$field])) {
$fields[':'.$field] = $add[0][$field];
}
}
$stmt = $this->pdo->prepare("INSERT INTO ".$this->calendarSharesTableName." (".implode(', ', $this->sharesProperties).") VALUES (".implode(', ', array_keys($fields)).")");
$stmt->execute($fields);
// are we removing any shares?
if (count($remove)>0) {
$r_ids = array();
foreach ($remove as $r_mailto) {
// get the principalid
$r_principal = $this->getPrincipalByEmail($r_mailto);
$r_ids[] = $r_principal['id'];
}
$stmt = $this->pdo->prepare("DELETE FROM ".$this->calendarSharesTableName." WHERE MEMBER = ?");
$stmt->execute($r_ids);
}
}
/**
* Returns the list of people whom this calendar is shared with.
*
* Every element in this array should have the following properties:
* * href - Often a mailto: address
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
* * summary - Optional, a description for the share
*
* @return array
*/
public function getShares($calendarId)
{
//$fields = implode(',', $this->sharesProperties);
//$shareRows = Share::getItemsSourceId('calendar',$calendarId);
//print_r($shareRows);
//$stmt = $this->pdo->prepare("SELECT * FROM ".$this->calendarSharesTableName." AS calendarShares LEFT JOIN ".$this->principalsTableName." AS principals ON calendarShares.member = principals.id WHERE calendarShares.calendarId = ? ORDER BY calendarShares.calendarId ASC");
//$stmt->execute(array($calendarId));
$shareRows = array();
$shares = array();
foreach ($shareRows as $row) {
//while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$share = array(
'calendarId' => $row['calendarId'],
'principalPath' => $row['uri'],
'readOnly' => $row['readonly'],
'summary' => $row['summary'],
'href' => $row['email'],
'commonName' => $row['displayname'],
'displayName' => $row['displayname'],
'status' => $row['status']
/*
'colour'=>$row['colour'],
'displayName'=>$row['displayName'],
*/
);
// map the status integer to a predefined constant
/*
switch($row['status']) {
case 1:
$share['status'] = 'STATUS_ACCEPTED';
break;
case 2:
$share['status'] = 'STATUS_DECLINED';
break;
case 3:
$share['status'] = 'STATUS_DELETED';
break;
case 4:
$share['status'] = 'STATUS_NORESPONSE';
break;
case 5:
$share['status'] = 'STATUS_INVALID';
break;
}*/
// add it to main array
$shares[] = $share;
}
return $shares;
}
/**
* 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'.
*
* MODIFIED: THIS METHOD NOW NEEDS TO BE ABLE TO RETRIEVE SHARED CALENDARS
*
* @param string $principalUri
* @return array
*/
public function getCalendarsForUser($principalUri)
{
// get the principal id
$principalBackend = $this->getPrincipalBackend();
$principal = $principalBackend->getPrincipalByPath($principalUri);
$fields = array_values($this->propertyMap);
$fields[] = 'id';
$fields[] = 'uri';
$fields[] = 'ctag';
$fields[] = 'components';
$fields[] = 'principaluri';
$fields[] = 'transparent';
$uri = 'principals/'.$principal['id'];
// Making fields a comma-delimited list
$rows = Calendar::where('principaluri', $uri)->get($fields);
foreach ($rows as $row) {
$components = array();
if ($row->components) {
$components = explode(',', $row->components);
}
$calendar = array(
'id' => $row->id,
'uri' => $row->uri,
'principaluri' => $principalUri,
'{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $row->ctag ? $row->ctag : '0',
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new \Sabre\CalDAV\Property\SupportedCalendarComponentSet($components),
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new \Sabre\CalDAV\Property\ScheduleCalendarTransp($row->transparent ? 'transparent' : 'opaque'),
);
foreach ($this->propertyMap as $xmlName => $dbName) {
$calendar[$xmlName] = $row[$dbName];
}
$calendars[] = $calendar;
}
// now let's get any shared calendars
$shareFields = implode(', ', $this->sharesProperties);
$shareRows = ShareService::getItemsSourceBy(['calendar'], $principal['id']);
foreach ($shareRows as $shareRow) {
// get the original calendar
$calendarShareRow = CalendarService::getCalendar($shareRow['source_id'], false);
$shareComponents = array();
if ($calendarShareRow['components']) {
$shareComponents = explode(',', $calendarShareRow['components']);
}
$sharedCalendar = array(
'id' => $calendarShareRow['id'],
'uri' => $calendarShareRow['uri'],
'principaluri' => $principalUri,
'{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $calendarShareRow['ctag']?$calendarShareRow['ctag']:'0',
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new \Sabre\CalDAV\Property\SupportedCalendarComponentSet($shareComponents),
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new \Sabre\CalDAV\Property\ScheduleCalendarTransp($calendarShareRow['transparent']?'transparent':'opaque'),
);
// some specific properies for shared calendars
$shareRow['displayname'] = '['.$shareRow['name'].']'.$calendarShareRow['displayname'];
$sharedCalendar['{http://calendarserver.org/ns/}shared-url'] = $calendarShareRow['uri'];
$sharedCalendar['{http://sabredav.org/ns}owner-principal'] = $calendarShareRow['principaluri'];
$sharedCalendar['{http://sabredav.org/ns}read-only'] = true;
$sharedCalendar['{http://calendarserver.org/ns/}summary'] = $shareRow['summary'];
foreach ($this->propertyMap as $xmlName => $dbName) {
if ($xmlName == '{DAV:}displayname') {
$sharedCalendar[$xmlName] = $shareRow['displayname'] == null ? $calendarShareRow['displayname'] : $shareRow['displayname'];
} elseif ($xmlName == '{http://apple.com/ns/ical/}calendar-color') {
$sharedCalendar[$xmlName] = $shareRow['colour'] == null ? $calendarShareRow['calendarcolor'] : $shareRow['color'];
} else {
$sharedCalendar[$xmlName] = $calendarShareRow[$dbName];
}
}
$calendars[] = $sharedCalendar;
}
// 获取分享记录
$shareSource = ShareService::getItemsSourceBy(['event'], Auth::id());
$sharedEvent = array(
'id' => 'share-events',
'uri' => 'share-events',
'principaluri' => $principalUri,
'{'.\Sabre\CalDAV\Plugin::NS_CALENDARSERVER.'}getctag' => count($shareSource),
'{'.\Sabre\CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new \Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT')),
'{'.\Sabre\CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp' => new \Sabre\CalDAV\Property\ScheduleCalendarTransp('opaque'),
);
$sharedEvent['{http://calendarserver.org/ns/}shared-url'] = 'share-events';
$sharedEvent['{http://sabredav.org/ns}owner-principal'] = 'principals/owner';
$sharedEvent['{http://sabredav.org/ns}read-only'] = true;
$sharedEvent['{DAV:}displayname'] = $this->sharedEvents['displayname'];
$calendars[] = $sharedEvent;
return $calendars;
}
/**
* This method is called when a user replied to a request to share.
*
* If the user chose to accept the share, this method should return the
* newly created calendar url.
*
* @param string href The sharee who is replying (often a mailto: address)
* @param int status One of the SharingPlugin::STATUS_* constants
* @param string $calendarUri The url to the calendar thats being shared
* @param string $inReplyTo The unique id this message is a response to
* @param string $summary A description of the reply
* @return null|string
*/
public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null)
{
}
/**
* Marks this calendar as published.
*
* Publishing a calendar should automatically create a read-only, public,
* subscribable calendar.
*
* @param bool $value
* @return void
*/
public function setPublishStatus($calendarId, $value)
{
}
/**
* Returns a list of notifications for a given principal url.
*
* The returned array should only consist of implementations of
* \Sabre\CalDAV\Notifications\INotificationType.
*
* @param string $principalUri
* @return array
*/
public function getNotificationsForPrincipal($principalUri)
{
// get ALL notifications for the user NB. Any read or out of date notifications should be already deleted.
$stmt = $this->pdo->prepare("SELECT * FROM ".$this->notificationsTableName." WHERE principaluri = ? ORDER BY dtstamp ASC");
$stmt->execute(array($principalUri));
$notifications = array();
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
// we need to return the correct type of notification
switch ($row['notification']) {
case 'Invite':
$values = array();
// sort out the required data
if ($row['id']) {
$values['id'] = $row['id'];
}
if ($row['etag']) {
$values['etag'] = $row['etag'];
}
if ($row['principalUri']) {
$values['href'] = $row['principalUri'];
}
if ($row['dtstamp']) {
$values['dtstamp'] = $row['dtstamp'];
}
if ($row['type']) {
$values['type'] = $row['type'];
}
if ($row['readOnly']) {
$values['readOnly'] = $row['readOnly'];
}
if ($row['hostUrl']) {
$values['hostUrl'] = $row['hostUrl'];
}
if ($row['organizer']) {
$values['organizer'] = $row['organizer'];
}
if ($row['commonName']) {
$values['commonName'] = $row['commonName'];
}
if ($row['firstName']) {
$values['firstName'] = $row['firstName'];
}
if ($row['lastName']) {
$values['lastName'] = $row['lastName'];
}
if ($row['summary']) {
$values['summary'] = $row['summary'];
}
$notifications[] = new \Sabre\CalDAV\Notifications\Notification\Invite($values);
break;
case 'InviteReply':
break;
case 'SystemStatus':
break;
}
}
return $notifications;
}
/**
* This deletes a specific notifcation.
*
* This may be called by a client once it deems a notification handled.
*
* @param string $principalUri
* @param \Sabre\CalDAV\Notifications\INotificationType $notification
* @return void
*/
public function deleteNotification($principalUri, \Sabre\CalDAV\Notifications\INotificationType $notification)
{
}
private function getPrincipalByEmail($email)
{
$principalBackend = $this->getPrincipalBackend();
$principalPath = $principalBackend->searchPrincipals('principals/users', array('{http://sabredav.org/ns}email-address'=>$email));
if ($principalPath == 0) {
throw new \Exception("Unknown email address");
}
// use the path to get the principal
return $principalBackend->getPrincipalByPath($principalPath);
}
private function getPrincipalBackend()
{
return new \Gdoo\Calendar\Sabre\Connector\Principal();
}
}