update sabredav to 3.0.7

This commit is contained in:
Sebijk 2022-02-13 23:22:47 +01:00
parent 3f21950bce
commit 8b8ee9b71b
990 changed files with 109128 additions and 35011 deletions

View file

@ -46,7 +46,7 @@ class BMCalDAVBackend extends Sabre\CalDAV\Backend\AbstractBackend
'uri' => 'calendar',
'principaluri' => $os->getPrincipalURI(),
'{DAV:}displayname' => 'Calendar',
'{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT'))
'{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet(array('VEVENT'))
);
$groupList = $os->calendar->GetGroups();
@ -60,7 +60,7 @@ class BMCalDAVBackend extends Sabre\CalDAV\Backend\AbstractBackend
'uri' => !empty($group['dav_uri']) ? $group['dav_uri'] : 'calendar-' . $key,
'principaluri' => $os->getPrincipalURI(),
'{DAV:}displayname' => $group['title'],
'{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT'))
'{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet(array('VEVENT'))
);
}
@ -72,7 +72,7 @@ class BMCalDAVBackend extends Sabre\CalDAV\Backend\AbstractBackend
'uri' => !empty($list['dav_uri']) ? $list['dav_uri'] : 'tasklist-' . $list['tasklistid'],
'principaluri' => $os->getPrincipalURI(),
'{DAV:}displayname' => $list['title'],
'{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VTODO'))
'{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet(array('VTODO'))
);
}
@ -85,8 +85,8 @@ class BMCalDAVBackend extends Sabre\CalDAV\Backend\AbstractBackend
$key = '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set';
if(!isset($properties[$key]) || !($properties[$key] instanceof Sabre\CalDAV\Property\SupportedCalendarComponentSet))
throw new Sabre\DAV\Exception('The ' . $key . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
if(!isset($properties[$key]) || !($properties[$key] instanceof Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet))
throw new Sabre\DAV\Exception('The ' . $key . ' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet');
$type = strtoupper(implode(',', $properties[$key]->getValue()));
@ -892,7 +892,7 @@ $caldavBackend = new BMCalDAVBackend;
$nodes = array(
new \Sabre\CalDAV\Principal\Collection($principalBackend),
new \Sabre\CalDAV\CalendarRootNode($principalBackend, $caldavBackend)
new \Sabre\CalDAV\CalendarRoot($principalBackend, $caldavBackend)
);
$server = new DAV\Server($nodes);

View file

@ -41,7 +41,7 @@ class BMCardDAVBackend extends Sabre\CardDAV\Backend\AbstractBackend
'uri' => 'main',
'principaluri' => $os->getPrincipalURI(),
'{DAV:}displayname' => 'Addressbook',
'{' . Sabre\CardDAV\Plugin::NS_CARDDAV . '}supported-address-data' => new Sabre\CardDAV\Property\SupportedAddressData()
'{' . Sabre\CardDAV\Plugin::NS_CARDDAV . '}supported-address-data' => new Sabre\CardDAV\Xml\Property\SupportedAddressData()
);
return($result);

File diff suppressed because it is too large Load diff

27
src/serverlib/3rdparty/SabreDAV/LICENSE vendored Normal file
View file

@ -0,0 +1,27 @@
Copyright (C) 2007-2016 fruux GmbH (https://fruux.com/).
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of SabreDAV nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,30 @@
![sabre's logo](http://sabre.io/img/logo.png) SabreDAV
======================================================
Introduction
------------
SabreDAV is the most popular WebDAV framework for PHP. Use it to create WebDAV, CalDAV and CardDAV servers.
Full documentation can be found on the website:
http://sabre.io/
Build status
------------
| branch | status |
| ------------ | ------ |
| master | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=master)](https://travis-ci.org/fruux/sabre-dav) |
| 3.0 | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=3.0)](https://travis-ci.org/fruux/sabre-dav) |
| 2.1 | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=2.1)](https://travis-ci.org/fruux/sabre-dav) |
| 2.0 | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=2.0)](https://travis-ci.org/fruux/sabre-dav) |
| 1.8 | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=1.8)](https://travis-ci.org/fruux/sabre-dav) |
| 1.7 | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=1.7)](https://travis-ci.org/fruux/sabre-dav) |
| 1.6 | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=1.6)](https://travis-ci.org/fruux/sabre-dav) |
| xml-rewrite2 | [![Build Status](https://travis-ci.org/fruux/sabre-dav.svg?branch=xml-rewrite2)](https://travis-ci.org/fruux/sabre-dav) |
Made at fruux
-------------
SabreDAV is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.

View file

@ -0,0 +1,241 @@
#!/usr/bin/env php
<?php
namespace Sabre\VObject;
// This sucks.. we have to try to find the composer autoloader. But chances
// are, we can't find it this way. So we'll do our bestest
$paths = array(
__DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly
__DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency.
);
foreach($paths as $path) {
if (file_exists($path)) {
include $path;
break;
}
}
if (!class_exists('Sabre\\VObject\\Version')) {
fwrite(STDERR, "Composer autoloader could not be properly loaded.\n");
die(1);
}
if ($argc < 2) {
$version = Version::VERSION;
$help = <<<HI
sabre/vobject $version
Usage:
generate_vcards [count]
Options:
count The number of random vcards to generate
Examples:
generate_vcards 1000 > testdata.vcf
HI;
fwrite(STDERR, $help);
exit(2);
}
$count = (int)$argv[1];
if ($count < 1) {
fwrite(STDERR, "Count must be at least 1\n");
exit(2);
}
fwrite(STDERR, "sabre/vobject " . Version::VERSION . "\n");
fwrite(STDERR, "Generating " . $count . " vcards in vCard 4.0 format\n");
/**
* The following list is just some random data we compiled from various
* sources online.
*
* Very little thought went into compiling this list, and certainly nothing
* political or ethical.
*
* We would _love_ more additions to this to add more variation to this list.
*
* Send us PR's and don't be shy adding your own first and last name for fun.
*/
$sets = array(
"nl" => array(
"country" => "Netherlands",
"boys" => array(
"Anno",
"Bram",
"Daan",
"Evert",
"Finn",
"Jayden",
"Jens",
"Jesse",
"Levi",
"Lucas",
"Luuk",
"Milan",
"René",
"Sem",
"Sibrand",
"Willem",
),
"girls" => array(
"Celia",
"Emma",
"Fenna",
"Geke",
"Inge",
"Julia",
"Lisa",
"Lotte",
"Mila",
"Sara",
"Sophie",
"Tess",
"Zoë",
),
"last" => array(
"Bakker",
"Bos",
"De Boer",
"De Groot",
"De Jong",
"De Vries",
"Jansen",
"Janssen",
"Meyer",
"Mulder",
"Peters",
"Smit",
"Van Dijk",
"Van den Berg",
"Visser",
"Vos",
),
),
"us" => array(
"country" => "United States",
"boys" => array(
"Aiden",
"Alexander",
"Charles",
"David",
"Ethan",
"Jacob",
"James",
"Jayden",
"John",
"Joseph",
"Liam",
"Mason",
"Michael",
"Noah",
"Richard",
"Robert",
"Thomas",
"William",
),
"girls" => array(
"Ava",
"Barbara",
"Chloe",
"Dorothy",
"Elizabeth",
"Emily",
"Emma",
"Isabella",
"Jennifer",
"Lily",
"Linda",
"Margaret",
"Maria",
"Mary",
"Mia",
"Olivia",
"Patricia",
"Roxy",
"Sophia",
"Susan",
"Zoe",
),
"last" => array(
"Smith",
"Johnson",
"Williams",
"Jones",
"Brown",
"Davis",
"Miller",
"Wilson",
"Moore",
"Taylor",
"Anderson",
"Thomas",
"Jackson",
"White",
"Harris",
"Martin",
"Thompson",
"Garcia",
"Martinez",
"Robinson",
),
),
);
$current = 0;
$r = function($arr) {
return $arr[mt_rand(0,count($arr)-1)];
};
$bdayStart = strtotime('-85 years');
$bdayEnd = strtotime('-20 years');
while($current < $count) {
$current++;
fwrite(STDERR, "\033[100D$current/$count");
$country = array_rand($sets);
$gender = mt_rand(0,1)?'girls':'boys';
$vcard = new Component\VCard(array(
'VERSION' => '4.0',
'FN' => $r($sets[$country][$gender]) . ' ' . $r($sets[$country]['last']),
'UID' => UUIDUtil::getUUID(),
));
$bdayRatio = mt_rand(0,9);
if($bdayRatio < 2) {
// 20% has a birthday property with a full date
$dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd));
$vcard->add('BDAY', $dt->format('Ymd'));
} elseif ($bdayRatio < 3) {
// 10% we only know the month and date of
$dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd));
$vcard->add('BDAY', '--' . $dt->format('md'));
}
if ($result = $vcard->validate()) {
ob_start();
echo "\nWe produced an invalid vcard somehow!\n";
foreach($result as $message) {
echo " " . $message['message'] . "\n";
}
fwrite(STDERR, ob_get_clean());
}
echo $vcard->serialize();
}
fwrite(STDERR,"\nDone.\n");

View file

@ -0,0 +1,140 @@
#!/usr/bin/env python
#
# Copyright (c) 2009-2010 Evert Pot
# All rights reserved.
# http://www.rooftopsolutions.nl/
#
# This utility is distributed along with SabreDAV
# license: http://sabre.io/license/ Modified BSD License
import os
from optparse import OptionParser
import time
def getfreespace(path):
stat = os.statvfs(path)
return stat.f_frsize * stat.f_bavail
def getbytesleft(path,threshold):
return getfreespace(path)-threshold
def run(cacheDir, threshold, sleep=5, simulate=False, min_erase = 0):
bytes = getbytesleft(cacheDir,threshold)
if (bytes>0):
print "Bytes to go before we hit threshold:", bytes
else:
print "Threshold exceeded with:", -bytes, "bytes"
dir = os.listdir(cacheDir)
dir2 = []
for file in dir:
path = cacheDir + '/' + file
dir2.append({
"path" : path,
"atime": os.stat(path).st_atime,
"size" : os.stat(path).st_size
})
dir2.sort(lambda x,y: int(x["atime"]-y["atime"]))
filesunlinked = 0
gainedspace = 0
# Left is the amount of bytes that need to be freed up
# The default is the 'min_erase setting'
left = min_erase
# If the min_erase setting is lower than the amount of bytes over
# the threshold, we use that number instead.
if left < -bytes :
left = -bytes
print "Need to delete at least:", left;
for file in dir2:
# Only deleting files if we're not simulating
if not simulate: os.unlink(file["path"])
left = int(left - file["size"])
gainedspace = gainedspace + file["size"]
filesunlinked = filesunlinked + 1
if(left<0):
break
print "%d files deleted (%d bytes)" % (filesunlinked, gainedspace)
time.sleep(sleep)
def main():
parser = OptionParser(
version="naturalselection v0.3",
description="Cache directory manager. Deletes cache entries based on accesstime and free space thresholds.\n" +
"This utility is distributed alongside SabreDAV.",
usage="usage: %prog [options] cacheDirectory",
)
parser.add_option(
'-s',
dest="simulate",
action="store_true",
help="Don't actually make changes, but just simulate the behaviour",
)
parser.add_option(
'-r','--runs',
help="How many times to check before exiting. -1 is infinite, which is the default",
type="int",
dest="runs",
default=-1
)
parser.add_option(
'-n','--interval',
help="Sleep time in seconds (default = 5)",
type="int",
dest="sleep",
default=5
)
parser.add_option(
'-l','--threshold',
help="Threshold in bytes (default = 10737418240, which is 10GB)",
type="int",
dest="threshold",
default=10737418240
)
parser.add_option(
'-m', '--min-erase',
help="Minimum number of bytes to erase when the threshold is reached. " +
"Setting this option higher will reduce the amount of times the cache directory will need to be scanned. " +
"(the default is 1073741824, which is 1GB.)",
type="int",
dest="min_erase",
default=1073741824
)
options,args = parser.parse_args()
if len(args)<1:
parser.error("This utility requires at least 1 argument")
cacheDir = args[0]
print "Natural Selection"
print "Cache directory:", cacheDir
free = getfreespace(cacheDir);
print "Current free disk space:", free
runs = options.runs;
while runs!=0 :
run(
cacheDir,
sleep=options.sleep,
simulate=options.simulate,
threshold=options.threshold,
min_erase=options.min_erase
)
if runs>0:
runs = runs - 1
if __name__ == '__main__' :
main()

View file

@ -0,0 +1,2 @@
#!/bin/sh
php -S 0.0.0.0:8080 `dirname $0`/sabredav.php

27
src/serverlib/3rdparty/SabreDAV/bin/vobject vendored Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env php
<?php
namespace Sabre\VObject;
// This sucks.. we have to try to find the composer autoloader. But chances
// are, we can't find it this way. So we'll do our bestest
$paths = array(
__DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly
__DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency.
);
foreach($paths as $path) {
if (file_exists($path)) {
include $path;
break;
}
}
if (!class_exists('Sabre\\VObject\\Version')) {
fwrite(STDERR, "Composer autoloader could not be loaded.\n");
die(1);
}
$cli = new Cli();
exit($cli->main($argv));

View file

@ -0,0 +1,57 @@
<?php
/*
Addressbook/CardDAV server example
This server features CardDAV support
*/
// settings
date_default_timezone_set('Canada/Eastern');
// Make sure this setting is turned on and reflect the root url for your WebDAV server.
// This can be for example the root / or a complete path to your server script
$baseUri = '/';
/* Database */
$pdo = new PDO('sqlite:data/db.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//Mapping PHP errors to exceptions
function exception_error_handler($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");
// Autoloader
require_once 'vendor/autoload.php';
// Backends
$authBackend = new Sabre\DAV\Auth\Backend\PDO($pdo);
$principalBackend = new Sabre\DAVACL\PrincipalBackend\PDO($pdo);
$carddavBackend = new Sabre\CardDAV\Backend\PDO($pdo);
//$caldavBackend = new Sabre\CalDAV\Backend\PDO($pdo);
// Setting up the directory tree //
$nodes = [
new Sabre\DAVACL\PrincipalCollection($principalBackend),
// new Sabre\CalDAV\CalendarRoot($authBackend, $caldavBackend),
new Sabre\CardDAV\AddressBookRoot($principalBackend, $carddavBackend),
];
// The object tree needs in turn to be passed to the server class
$server = new Sabre\DAV\Server($nodes);
$server->setBaseUri($baseUri);
// Plugins
$server->addPlugin(new Sabre\DAV\Auth\Plugin($authBackend));
$server->addPlugin(new Sabre\DAV\Browser\Plugin());
//$server->addPlugin(new Sabre\CalDAV\Plugin());
$server->addPlugin(new Sabre\CardDAV\Plugin());
$server->addPlugin(new Sabre\DAVACL\Plugin());
$server->addPlugin(new Sabre\DAV\Sync\Plugin());
// And off we go!
$server->exec();

View file

@ -0,0 +1,76 @@
<?php
/*
CalendarServer example
This server features CalDAV support
*/
// settings
date_default_timezone_set('Canada/Eastern');
// If you want to run the SabreDAV server in a custom location (using mod_rewrite for instance)
// You can override the baseUri here.
// $baseUri = '/';
/* Database */
$pdo = new PDO('sqlite:data/db.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//Mapping PHP errors to exceptions
function exception_error_handler($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");
// Files we need
require_once 'vendor/autoload.php';
// Backends
$authBackend = new Sabre\DAV\Auth\Backend\PDO($pdo);
$calendarBackend = new Sabre\CalDAV\Backend\PDO($pdo);
$principalBackend = new Sabre\DAVACL\PrincipalBackend\PDO($pdo);
// Directory structure
$tree = [
new Sabre\CalDAV\Principal\Collection($principalBackend),
new Sabre\CalDAV\CalendarRoot($principalBackend, $calendarBackend),
];
$server = new Sabre\DAV\Server($tree);
if (isset($baseUri))
$server->setBaseUri($baseUri);
/* Server Plugins */
$authPlugin = new Sabre\DAV\Auth\Plugin($authBackend);
$server->addPlugin($authPlugin);
$aclPlugin = new Sabre\DAVACL\Plugin();
$server->addPlugin($aclPlugin);
/* CalDAV support */
$caldavPlugin = new Sabre\CalDAV\Plugin();
$server->addPlugin($caldavPlugin);
/* Calendar subscription support */
$server->addPlugin(
new Sabre\CalDAV\Subscriptions\Plugin()
);
/* Calendar scheduling support */
$server->addPlugin(
new Sabre\CalDAV\Schedule\Plugin()
);
/* WebDAV-Sync plugin */
$server->addPlugin(new Sabre\DAV\Sync\Plugin());
// Support for html frontend
$browser = new Sabre\DAV\Browser\Plugin();
$server->addPlugin($browser);
// And off we go!
$server->exec();

View file

@ -0,0 +1,56 @@
<?php
/*
This is the best starting point if you're just interested in setting up a fileserver.
Make sure that the 'public' and 'tmpdata' exists, with write permissions
for your server.
*/
// settings
date_default_timezone_set('Canada/Eastern');
$publicDir = 'public';
$tmpDir = 'tmpdata';
// If you want to run the SabreDAV server in a custom location (using mod_rewrite for instance)
// You can override the baseUri here.
// $baseUri = '/';
// Files we need
require_once 'vendor/autoload.php';
// Create the root node
$root = new \Sabre\DAV\FS\Directory($publicDir);
// The rootnode needs in turn to be passed to the server class
$server = new \Sabre\DAV\Server($root);
if (isset($baseUri))
$server->setBaseUri($baseUri);
// Support for LOCK and UNLOCK
$lockBackend = new \Sabre\DAV\Locks\Backend\File($tmpDir . '/locksdb');
$lockPlugin = new \Sabre\DAV\Locks\Plugin($lockBackend);
$server->addPlugin($lockPlugin);
// Support for html frontend
$browser = new \Sabre\DAV\Browser\Plugin();
$server->addPlugin($browser);
// Automatically guess (some) contenttypes, based on extesion
$server->addPlugin(new \Sabre\DAV\Browser\GuessContentType());
// Authentication backend
$authBackend = new \Sabre\DAV\Auth\Backend\File('.htdigest');
$auth = new \Sabre\DAV\Auth\Plugin($authBackend);
$server->addPlugin($auth);
// Temporary file filter
$tempFF = new \Sabre\DAV\TemporaryFileFilterPlugin($tmpDir);
$server->addPlugin($tempFF);
// And off we go!
$server->exec();

View file

@ -0,0 +1,92 @@
<?php
/**
* This server combines both CardDAV and CalDAV functionality into a single
* server. It is assumed that the server runs at the root of a HTTP domain (be
* that a domainname-based vhost or a specific TCP port.
*
* This example also assumes that you're using SQLite and the database has
* already been setup (along with the database tables).
*
* You may choose to use MySQL instead, just change the PDO connection
* statement.
*/
/**
* UTC or GMT is easy to work with, and usually recommended for any
* application.
*/
date_default_timezone_set('UTC');
/**
* Make sure this setting is turned on and reflect the root url for your WebDAV
* server.
*
* This can be for example the root / or a complete path to your server script.
*/
// $baseUri = '/';
/**
* Database
*
* Feel free to switch this to MySQL, it will definitely be better for higher
* concurrency.
*/
$pdo = new \PDO('sqlite:data/db.sqlite');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
/**
* Mapping PHP errors to exceptions.
*
* While this is not strictly needed, it makes a lot of sense to do so. If an
* E_NOTICE or anything appears in your code, this allows SabreDAV to intercept
* the issue and send a proper response back to the client (HTTP/1.1 500).
*/
function exception_error_handler($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");
// Autoloader
require_once 'vendor/autoload.php';
/**
* The backends. Yes we do really need all of them.
*
* This allows any developer to subclass just any of them and hook into their
* own backend systems.
*/
$authBackend = new \Sabre\DAV\Auth\Backend\PDO($pdo);
$principalBackend = new \Sabre\DAVACL\PrincipalBackend\PDO($pdo);
$carddavBackend = new \Sabre\CardDAV\Backend\PDO($pdo);
$caldavBackend = new \Sabre\CalDAV\Backend\PDO($pdo);
/**
* The directory tree
*
* Basically this is an array which contains the 'top-level' directories in the
* WebDAV server.
*/
$nodes = [
// /principals
new \Sabre\CalDAV\Principal\Collection($principalBackend),
// /calendars
new \Sabre\CalDAV\CalendarRoot($principalBackend, $caldavBackend),
// /addressbook
new \Sabre\CardDAV\AddressBookRoot($principalBackend, $carddavBackend),
];
// The object tree needs in turn to be passed to the server class
$server = new \Sabre\DAV\Server($nodes);
if (isset($baseUri)) $server->setBaseUri($baseUri);
// Plugins
$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend));
$server->addPlugin(new \Sabre\DAV\Browser\Plugin());
$server->addPlugin(new \Sabre\CalDAV\Plugin());
$server->addPlugin(new \Sabre\CardDAV\Plugin());
$server->addPlugin(new \Sabre\DAVACL\Plugin());
$server->addPlugin(new \Sabre\DAV\Sync\Plugin());
// And off we go!
$server->exec();

View file

@ -0,0 +1,123 @@
<?php
// !!!! Make sure the Sabre directory is in the include_path !!!
// example:
// set_include_path('lib/' . PATH_SEPARATOR . get_include_path());
/*
This example demonstrates a simple way to create your own virtual filesystems.
By extending the _File and Directory classes, you can easily create a tree
based on various datasources.
The most obvious example is the filesystem itself. A more complete and documented
example can be found in:
lib/Sabre/DAV/FS/Node.php
lib/Sabre/DAV/FS/Directory.php
lib/Sabre/DAV/FS/File.php
*/
// settings
date_default_timezone_set('Canada/Eastern');
$publicDir = 'public';
// Files we need
require_once 'vendor/autoload.php';
class MyCollection extends Sabre\DAV\Collection {
private $myPath;
function __construct($myPath) {
$this->myPath = $myPath;
}
function getChildren() {
$children = [];
// Loop through the directory, and create objects for each node
foreach (scandir($this->myPath) as $node) {
// Ignoring files staring with .
if ($node[0] === '.') continue;
$children[] = $this->getChild($node);
}
return $children;
}
function getChild($name) {
$path = $this->myPath . '/' . $name;
// We have to throw a NotFound exception if the file didn't exist
if (!file_exists($this->myPath)) throw new \Sabre\DAV\Exception\NotFound('The file with name: ' . $name . ' could not be found');
// Some added security
if ($name[0] == '.') throw new \Sabre\DAV\Exception\Forbidden('Access denied');
if (is_dir($path)) {
return new \MyCollection($name);
} else {
return new \MyFile($path);
}
}
function getName() {
return basename($this->myPath);
}
}
class MyFile extends \Sabre\DAV\File {
private $myPath;
function __construct($myPath) {
$this->myPath = $myPath;
}
function getName() {
return basename($this->myPath);
}
function get() {
return fopen($this->myPath, 'r');
}
function getSize() {
return filesize($this->myPath);
}
}
// Make sure there is a directory in your current directory named 'public'. We will be exposing that directory to WebDAV
$rootNode = new \MyCollection($publicDir);
// The rootNode needs to be passed to the server object.
$server = new \Sabre\DAV\Server($rootNode);
// And off we go!
$server->exec();

View file

@ -0,0 +1,28 @@
CREATE TABLE addressbooks (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principaluri VARBINARY(255),
displayname VARCHAR(255),
uri VARBINARY(200),
description TEXT,
synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
UNIQUE(principaluri(100), uri(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE cards (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
addressbookid INT(11) UNSIGNED NOT NULL,
carddata MEDIUMBLOB,
uri VARBINARY(200),
lastmodified INT(11) UNSIGNED,
etag VARBINARY(32),
size INT(11) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE addressbookchanges (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
uri VARBINARY(200) NOT NULL,
synctoken INT(11) UNSIGNED NOT NULL,
addressbookid INT(11) UNSIGNED NOT NULL,
operation TINYINT(1) NOT NULL,
INDEX addressbookid_synctoken (addressbookid, synctoken)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -0,0 +1,64 @@
CREATE TABLE calendarobjects (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
calendardata MEDIUMBLOB,
uri VARBINARY(200),
calendarid INTEGER UNSIGNED NOT NULL,
lastmodified INT(11) UNSIGNED,
etag VARBINARY(32),
size INT(11) UNSIGNED NOT NULL,
componenttype VARBINARY(8),
firstoccurence INT(11) UNSIGNED,
lastoccurence INT(11) UNSIGNED,
uid VARBINARY(200),
UNIQUE(calendarid, uri)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE calendars (
id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principaluri VARBINARY(100),
displayname VARCHAR(100),
uri VARBINARY(200),
synctoken INTEGER UNSIGNED NOT NULL DEFAULT '1',
description TEXT,
calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
calendarcolor VARBINARY(10),
timezone TEXT,
components VARBINARY(21),
transparent TINYINT(1) NOT NULL DEFAULT '0',
UNIQUE(principaluri, uri)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE calendarchanges (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
uri VARBINARY(200) NOT NULL,
synctoken INT(11) UNSIGNED NOT NULL,
calendarid INT(11) UNSIGNED NOT NULL,
operation TINYINT(1) NOT NULL,
INDEX calendarid_synctoken (calendarid, synctoken)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE calendarsubscriptions (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
uri VARBINARY(200) NOT NULL,
principaluri VARBINARY(100) NOT NULL,
source TEXT,
displayname VARCHAR(100),
refreshrate VARCHAR(10),
calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
calendarcolor VARBINARY(10),
striptodos TINYINT(1) NULL,
stripalarms TINYINT(1) NULL,
stripattachments TINYINT(1) NULL,
lastmodified INT(11) UNSIGNED,
UNIQUE(principaluri, uri)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE schedulingobjects (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principaluri VARBINARY(255),
calendardata MEDIUMBLOB,
uri VARBINARY(200),
lastmodified INT(11) UNSIGNED,
etag VARBINARY(32),
size INT(11) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -0,0 +1,12 @@
CREATE TABLE locks (
id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
owner VARCHAR(100),
timeout INTEGER UNSIGNED,
created INTEGER,
token VARBINARY(100),
scope TINYINT,
depth TINYINT,
uri VARBINARY(1000),
INDEX(token),
INDEX(uri(100))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View file

@ -0,0 +1,20 @@
CREATE TABLE principals (
id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
uri VARBINARY(200) NOT NULL,
email VARBINARY(80),
displayname VARCHAR(80),
UNIQUE(uri)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE groupmembers (
id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principal_id INTEGER UNSIGNED NOT NULL,
member_id INTEGER UNSIGNED NOT NULL,
UNIQUE(principal_id, member_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO principals (uri,email,displayname) VALUES
('principals/admin', 'admin@example.org','Administrator'),
('principals/admin/calendar-proxy-read', null, null),
('principals/admin/calendar-proxy-write', null, null);

View file

@ -0,0 +1,9 @@
CREATE TABLE propertystorage (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
path VARBINARY(1024) NOT NULL,
name VARBINARY(100) NOT NULL,
valuetype INT UNSIGNED,
value MEDIUMBLOB
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE UNIQUE INDEX path_property ON propertystorage (path(600), name(100));

View file

@ -0,0 +1,9 @@
CREATE TABLE users (
id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
username VARBINARY(50),
digesta1 VARBINARY(32),
UNIQUE(username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO users (username,digesta1) VALUES
('admin', '87fd274b7b6c01e48d7c2f965da8ddf7');

View file

@ -0,0 +1,52 @@
CREATE TABLE addressbooks (
id SERIAL NOT NULL,
principaluri VARCHAR(255),
displayname VARCHAR(255),
uri VARCHAR(200),
description TEXT,
synctoken INTEGER NOT NULL DEFAULT 1
);
ALTER TABLE ONLY addressbooks
ADD CONSTRAINT addressbooks_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX addressbooks_ukey
ON addressbooks USING btree (principaluri, uri);
CREATE TABLE cards (
id SERIAL NOT NULL,
addressbookid INTEGER NOT NULL,
carddata TEXT,
uri VARCHAR(200),
lastmodified INTEGER,
etag VARCHAR(32),
size INTEGER NOT NULL
);
ALTER TABLE ONLY cards
ADD CONSTRAINT cards_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX cards_ukey
ON cards USING btree (addressbookid, uri);
ALTER TABLE ONLY cards
ADD CONSTRAINT cards_addressbookid_fkey FOREIGN KEY (addressbookid) REFERENCES addressbooks(id)
ON DELETE CASCADE;
CREATE TABLE addressbookchanges (
id SERIAL NOT NULL,
uri VARCHAR(200) NOT NULL,
synctoken INTEGER NOT NULL,
addressbookid INTEGER NOT NULL,
operation SMALLINT NOT NULL
);
ALTER TABLE ONLY addressbookchanges
ADD CONSTRAINT addressbookchanges_pkey PRIMARY KEY (id);
CREATE INDEX addressbookchanges_addressbookid_synctoken_ix
ON addressbookchanges USING btree (addressbookid, synctoken);
ALTER TABLE ONLY addressbookchanges
ADD CONSTRAINT addressbookchanges_addressbookid_fkey FOREIGN KEY (addressbookid) REFERENCES addressbooks(id)
ON DELETE CASCADE;

View file

@ -0,0 +1,93 @@
CREATE TABLE calendars (
id SERIAL NOT NULL,
principaluri VARCHAR(100),
displayname VARCHAR(100),
uri VARCHAR(200),
synctoken INTEGER NOT NULL DEFAULT 1,
description TEXT,
calendarorder INTEGER NOT NULL DEFAULT 0,
calendarcolor VARCHAR(10),
timezone TEXT,
components VARCHAR(20),
uid VARCHAR(200),
transparent SMALLINT NOT NULL DEFAULT '0'
);
ALTER TABLE ONLY calendars
ADD CONSTRAINT calendars_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX calendars_ukey
ON calendars USING btree (principaluri, uri);
CREATE TABLE calendarobjects (
id SERIAL NOT NULL,
calendardata TEXT,
uri VARCHAR(200),
calendarid INTEGER NOT NULL,
lastmodified INTEGER,
etag VARCHAR(32),
size INTEGER NOT NULL,
componenttype VARCHAR(8),
firstoccurence INTEGER,
lastoccurence INTEGER,
uid VARCHAR(200)
);
ALTER TABLE ONLY calendarobjects
ADD CONSTRAINT calendarobjects_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX calendarobjects_ukey
ON calendarobjects USING btree (calendarid, uri);
ALTER TABLE ONLY calendarobjects
ADD CONSTRAINT calendarobjects_calendarid_fkey FOREIGN KEY (calendarid) REFERENCES calendars(id)
ON DELETE CASCADE;
CREATE TABLE calendarsubscriptions (
id SERIAL NOT NULL,
uri VARCHAR(200) NOT NULL,
principaluri VARCHAR(100) NOT NULL,
source TEXT,
displayname VARCHAR(100),
refreshrate VARCHAR(10),
calendarorder INTEGER NOT NULL DEFAULT 0,
calendarcolor VARCHAR(10),
striptodos SMALLINT NULL,
stripalarms SMALLINT NULL,
stripattachments SMALLINT NULL,
lastmodified INTEGER
);
ALTER TABLE ONLY calendarsubscriptions
ADD CONSTRAINT calendarsubscriptions_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX calendarsubscriptions_ukey
ON calendarsubscriptions USING btree (principaluri, uri);
CREATE TABLE calendarchanges (
id SERIAL NOT NULL,
uri VARCHAR(200) NOT NULL,
synctoken INTEGER NOT NULL,
calendarid INTEGER NOT NULL,
operation SMALLINT NOT NULL DEFAULT 0
);
ALTER TABLE ONLY calendarchanges
ADD CONSTRAINT calendarchanges_pkey PRIMARY KEY (id);
CREATE INDEX calendarchanges_calendarid_synctoken_ix
ON calendarchanges USING btree (calendarid, synctoken);
ALTER TABLE ONLY calendarchanges
ADD CONSTRAINT calendarchanges_calendar_fk FOREIGN KEY (calendarid) REFERENCES calendars(id)
ON DELETE CASCADE;
CREATE TABLE schedulingobjects (
id SERIAL NOT NULL,
principaluri VARCHAR(255),
calendardata BYTEA,
uri VARCHAR(200),
lastmodified INTEGER,
etag VARCHAR(32),
size INTEGER NOT NULL
);

View file

@ -0,0 +1,19 @@
CREATE TABLE locks (
id SERIAL NOT NULL,
owner VARCHAR(100),
timeout INTEGER,
created INTEGER,
token VARCHAR(100),
scope SMALLINT,
depth SMALLINT,
uri TEXT
);
ALTER TABLE ONLY locks
ADD CONSTRAINT locks_pkey PRIMARY KEY (id);
CREATE INDEX locks_token_ix
ON locks USING btree (token);
CREATE INDEX locks_uri_ix
ON locks USING btree (uri);

View file

@ -0,0 +1,38 @@
CREATE TABLE principals (
id SERIAL NOT NULL,
uri VARCHAR(200) NOT NULL,
email VARCHAR(80),
displayname VARCHAR(80)
);
ALTER TABLE ONLY principals
ADD CONSTRAINT principals_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX principals_ukey
ON principals USING btree (uri);
CREATE TABLE groupmembers (
id SERIAL NOT NULL,
principal_id INTEGER NOT NULL,
member_id INTEGER NOT NULL
);
ALTER TABLE ONLY groupmembers
ADD CONSTRAINT groupmembers_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX groupmembers_ukey
ON groupmembers USING btree (principal_id, member_id);
ALTER TABLE ONLY groupmembers
ADD CONSTRAINT groupmembers_principal_id_fkey FOREIGN KEY (principal_id) REFERENCES principals(id)
ON DELETE CASCADE;
ALTER TABLE ONLY groupmembers
ADD CONSTRAINT groupmembers_member_id_id_fkey FOREIGN KEY (member_id) REFERENCES principals(id)
ON DELETE CASCADE;
INSERT INTO principals (uri,email,displayname) VALUES
('principals/admin', 'admin@example.org','Administrator'),
('principals/admin/calendar-proxy-read', null, null),
('principals/admin/calendar-proxy-write', null, null);

View file

@ -0,0 +1,13 @@
CREATE TABLE propertystorage (
id SERIAL NOT NULL,
path VARCHAR(1024) NOT NULL,
name VARCHAR(100) NOT NULL,
valuetype INT,
value TEXT
);
ALTER TABLE ONLY propertystorage
ADD CONSTRAINT propertystorage_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX propertystorage_ukey
ON propertystorage (path, name);

View file

@ -0,0 +1,14 @@
CREATE TABLE users (
id SERIAL NOT NULL,
username VARCHAR(50),
digesta1 VARCHAR(32)
);
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX users_ukey
ON users USING btree (username);
INSERT INTO users (username,digesta1) VALUES
('admin', '87fd274b7b6c01e48d7c2f965da8ddf7');

View file

@ -0,0 +1,28 @@
CREATE TABLE addressbooks (
id integer primary key asc,
principaluri text,
displayname text,
uri text,
description text,
synctoken integer
);
CREATE TABLE cards (
id integer primary key asc,
addressbookid integer,
carddata blob,
uri text,
lastmodified integer,
etag text,
size integer
);
CREATE TABLE addressbookchanges (
id integer primary key asc,
uri text,
synctoken integer,
addressbookid integer,
operation integer
);
CREATE INDEX addressbookid_synctoken ON addressbookchanges (addressbookid, synctoken);

View file

@ -0,0 +1,64 @@
CREATE TABLE calendarobjects (
id integer primary key asc,
calendardata blob,
uri text,
calendarid integer,
lastmodified integer,
etag text,
size integer,
componenttype text,
firstoccurence integer,
lastoccurence integer,
uid text
);
CREATE TABLE calendars (
id integer primary key asc,
principaluri text,
displayname text,
uri text,
synctoken integer,
description text,
calendarorder integer,
calendarcolor text,
timezone text,
components text,
transparent bool
);
CREATE TABLE calendarchanges (
id integer primary key asc,
uri text,
synctoken integer,
calendarid integer,
operation integer
);
CREATE INDEX calendarid_synctoken ON calendarchanges (calendarid, synctoken);
CREATE TABLE calendarsubscriptions (
id integer primary key asc,
uri text,
principaluri text,
source text,
displayname text,
refreshrate text,
calendarorder integer,
calendarcolor text,
striptodos bool,
stripalarms bool,
stripattachments bool,
lastmodified int
);
CREATE TABLE schedulingobjects (
id integer primary key asc,
principaluri text,
calendardata blob,
uri text,
lastmodified integer,
etag text,
size integer
);
CREATE INDEX principaluri_uri ON calendarsubscriptions (principaluri, uri);

View file

@ -0,0 +1,12 @@
BEGIN TRANSACTION;
CREATE TABLE locks (
id integer primary key asc,
owner text,
timeout integer,
created integer,
token text,
scope integer,
depth integer,
uri text
);
COMMIT;

View file

@ -0,0 +1,20 @@
CREATE TABLE principals (
id INTEGER PRIMARY KEY ASC,
uri TEXT,
email TEXT,
displayname TEXT,
UNIQUE(uri)
);
CREATE TABLE groupmembers (
id INTEGER PRIMARY KEY ASC,
principal_id INTEGER,
member_id INTEGER,
UNIQUE(principal_id, member_id)
);
INSERT INTO principals (uri,email,displayname) VALUES ('principals/admin', 'admin@example.org','Administrator');
INSERT INTO principals (uri,email,displayname) VALUES ('principals/admin/calendar-proxy-read', null, null);
INSERT INTO principals (uri,email,displayname) VALUES ('principals/admin/calendar-proxy-write', null, null);

View file

@ -0,0 +1,10 @@
CREATE TABLE propertystorage (
id integer primary key asc,
path text,
name text,
valuetype integer,
value string
);
CREATE UNIQUE INDEX path_property ON propertystorage (path, name);

View file

@ -0,0 +1,9 @@
CREATE TABLE users (
id integer primary key asc,
username TEXT,
digesta1 TEXT,
UNIQUE(username)
);
INSERT INTO users (username,digesta1) VALUES
('admin', '87fd274b7b6c01e48d7c2f965da8ddf7');

View file

@ -0,0 +1,16 @@
RewriteEngine On
# This makes every request go to server.php
RewriteRule (.*) server.php [L]
# Output buffering needs to be off, to prevent high memory usage
php_flag output_buffering off
# This is also to prevent high memory usage
php_flag always_populate_raw_post_data off
# This is almost a given, but magic quotes is *still* on on some
# linux distributions
php_flag magic_quotes_gpc off
# SabreDAV is not compatible with mbstring function overloading
php_flag mbstring.func_overload off

View file

@ -0,0 +1,33 @@
# This is a sample configuration to setup a dedicated Apache vhost for WebDAV.
#
# The main thing that should be configured is the servername, and the path to
# your SabreDAV installation (DocumentRoot).
#
# This configuration assumed mod_php5 is used, as it sets a few default php
# settings as well.
<VirtualHost *:*>
# Don't forget to change the server name
# ServerName dav.example.org
# The DocumentRoot is also required
# DocumentRoot /home/sabredav/
RewriteEngine On
# This makes every request go to server.php
RewriteRule ^/(.*)$ /server.php [L]
# Output buffering needs to be off, to prevent high memory usage
php_flag output_buffering off
# This is also to prevent high memory usage
php_flag always_populate_raw_post_data off
# This is almost a given, but magic quotes is *still* on on some
# linux distributions
php_flag magic_quotes_gpc off
# SabreDAV is not compatible with mbstring function overloading
php_flag mbstring.func_overload off
</VirtualHost *:*>

View file

@ -0,0 +1,21 @@
# This is a sample configuration to setup a dedicated Apache vhost for WebDAV.
#
# The main thing that should be configured is the servername, and the path to
# your SabreDAV installation (DocumentRoot).
#
# This configuration assumes CGI or FastCGI is used.
<VirtualHost *:*>
# Don't forget to change the server name
# ServerName dav.example.org
# The DocumentRoot is also required
# DocumentRoot /home/sabredav/
# This makes every request go to server.php. This also makes sure
# the Authentication information is available. If your server script is
# not called server.php, be sure to change it.
RewriteEngine On
RewriteRule ^/(.*)$ /server.php [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</VirtualHost *:*>

View file

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInitd98aa8eda4187f8aba381f2d1a6f1d7d::getLoader();

View file

@ -0,0 +1,413 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative) {
return false;
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
// Remember that this class does not exist.
return $this->classMap[$class] = false;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

View file

@ -0,0 +1,19 @@
Copyright (c) 2015 Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View file

@ -0,0 +1,13 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php',
'3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
'93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
);

View file

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);

View file

@ -0,0 +1,18 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'),
'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'),
'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'),
'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'),
'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'),
'Sabre\\DAV\\' => array($vendorDir . '/sabre/dav/lib/DAV'),
'Sabre\\DAVACL\\' => array($vendorDir . '/sabre/dav/lib/DAVACL'),
'Sabre\\CardDAV\\' => array($vendorDir . '/sabre/dav/lib/CardDAV'),
'Sabre\\CalDAV\\' => array($vendorDir . '/sabre/dav/lib/CalDAV'),
);

View file

@ -0,0 +1,59 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitd98aa8eda4187f8aba381f2d1a6f1d7d
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitd98aa8eda4187f8aba381f2d1a6f1d7d', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitd98aa8eda4187f8aba381f2d1a6f1d7d', 'loadClassLoader'));
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
$loader->register(true);
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequired98aa8eda4187f8aba381f2d1a6f1d7d($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequired98aa8eda4187f8aba381f2d1a6f1d7d($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

View file

@ -0,0 +1,381 @@
[
{
"name": "sabre/uri",
"version": "1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-uri.git",
"reference": "9012116434d84ef6e5e37a89dfdbfbe2204a8704"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-uri/zipball/9012116434d84ef6e5e37a89dfdbfbe2204a8704",
"reference": "9012116434d84ef6e5e37a89dfdbfbe2204a8704",
"shasum": ""
},
"require": {
"php": ">=5.4.7"
},
"require-dev": {
"phpunit/phpunit": "*",
"sabre/cs": "~0.0.1"
},
"time": "2016-03-08 02:29:27",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\Uri\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "Functions for making sense out of URIs.",
"homepage": "http://sabre.io/uri/",
"keywords": [
"rfc3986",
"uri",
"url"
]
},
{
"name": "sabre/event",
"version": "2.0.2",
"version_normalized": "2.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-event.git",
"reference": "337b6f5e10ea6e0b21e22c7e5788dd3883ae73ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-event/zipball/337b6f5e10ea6e0b21e22c7e5788dd3883ae73ff",
"reference": "337b6f5e10ea6e0b21e22c7e5788dd3883ae73ff",
"shasum": ""
},
"require": {
"php": ">=5.4.1"
},
"require-dev": {
"phpunit/phpunit": "*",
"sabre/cs": "~0.0.1"
},
"time": "2015-05-19 10:24:22",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Sabre\\Event\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "sabre/event is a library for lightweight event-based programming",
"homepage": "http://sabre.io/event/",
"keywords": [
"EventEmitter",
"events",
"hooks",
"plugin",
"promise",
"signal"
]
},
{
"name": "sabre/http",
"version": "4.2.1",
"version_normalized": "4.2.1.0",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-http.git",
"reference": "2e93bc8321524c67be4ca5b8415daebd4c8bf85e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-http/zipball/2e93bc8321524c67be4ca5b8415daebd4c8bf85e",
"reference": "2e93bc8321524c67be4ca5b8415daebd4c8bf85e",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.4",
"sabre/event": ">=1.0.0,<4.0.0",
"sabre/uri": "~1.0"
},
"require-dev": {
"phpunit/phpunit": "~4.3",
"sabre/cs": "~0.0.1"
},
"suggest": {
"ext-curl": " to make http requests with the Client class"
},
"time": "2016-01-06 23:00:08",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"lib/functions.php"
],
"psr-4": {
"Sabre\\HTTP\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
"homepage": "https://github.com/fruux/sabre-http",
"keywords": [
"http"
]
},
{
"name": "sabre/xml",
"version": "1.4.1",
"version_normalized": "1.4.1.0",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-xml.git",
"reference": "59998046db252634259a878baf1af18159f508f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-xml/zipball/59998046db252634259a878baf1af18159f508f3",
"reference": "59998046db252634259a878baf1af18159f508f3",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"lib-libxml": ">=2.6.20",
"php": ">=5.4.1",
"sabre/uri": "~1.0"
},
"require-dev": {
"phpunit/phpunit": "*",
"sabre/cs": "~0.0.2"
},
"time": "2016-03-12 22:23:16",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Sabre\\Xml\\": "lib/"
},
"files": [
"lib/Deserializer/functions.php",
"lib/Serializer/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Markus Staab",
"email": "markus.staab@redaxo.de",
"role": "Developer"
}
],
"description": "sabre/xml is an XML library that you may not hate.",
"homepage": "https://sabre.io/xml/",
"keywords": [
"XMLReader",
"XMLWriter",
"dom",
"xml"
]
},
{
"name": "sabre/vobject",
"version": "3.5.1",
"version_normalized": "3.5.1.0",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-vobject.git",
"reference": "5b92c27399a31c860877b9c29f4fcbb241069679"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-vobject/zipball/5b92c27399a31c860877b9c29f4fcbb241069679",
"reference": "5b92c27399a31c860877b9c29f4fcbb241069679",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.3.1"
},
"require-dev": {
"phpunit/phpunit": "*",
"squizlabs/php_codesniffer": "*"
},
"time": "2016-04-07 00:12:31",
"bin": [
"bin/vobject",
"bin/generate_vcards"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Sabre\\VObject\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
},
{
"name": "Dominik Tobschall",
"email": "dominik@fruux.com",
"homepage": "http://tobschall.de/",
"role": "Developer"
}
],
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
"homepage": "http://sabre.io/vobject/",
"keywords": [
"VObject",
"iCalendar",
"jCal",
"jCard",
"vCard"
]
},
{
"name": "sabre/dav",
"version": "3.0.9",
"version_normalized": "3.0.9.0",
"source": {
"type": "git",
"url": "https://github.com/fruux/sabre-dav.git",
"reference": "b42593965211de1ce99f73bd3aede99c41258e08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruux/sabre-dav/zipball/b42593965211de1ce99f73bd3aede99c41258e08",
"reference": "b42593965211de1ce99f73bd3aede99c41258e08",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-dom": "*",
"ext-iconv": "*",
"ext-mbstring": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"ext-spl": "*",
"lib-libxml": ">=2.7.0",
"php": ">=5.4.1",
"sabre/event": "~2.0",
"sabre/http": "~4.0",
"sabre/uri": "~1.0",
"sabre/vobject": "^3.3.4",
"sabre/xml": "~1.0"
},
"require-dev": {
"evert/phpdoc-md": "~0.1.0",
"phpunit/phpunit": "~4.2",
"sabre/cs": "~0.0.2"
},
"suggest": {
"ext-curl": "*",
"ext-pdo": "*"
},
"time": "2016-04-07 00:32:57",
"bin": [
"bin/sabredav",
"bin/naturalselection"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Sabre\\DAV\\": "lib/DAV/",
"Sabre\\DAVACL\\": "lib/DAVACL/",
"Sabre\\CalDAV\\": "lib/CalDAV/",
"Sabre\\CardDAV\\": "lib/CardDAV/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage": "http://evertpot.com/",
"role": "Developer"
}
],
"description": "WebDAV Framework for PHP",
"homepage": "http://sabre.io/",
"keywords": [
"CalDAV",
"CardDAV",
"WebDAV",
"framework",
"iCalendar"
]
}
]

View file

@ -0,0 +1,40 @@
# Unit tests
tests/temp
tests/.sabredav
tests/cov
# ViM
*.swp
# Composer
composer.lock
vendor
# Composer binaries
bin/phing
bin/phpunit
bin/vobject
bin/generate_vcards
bin/phpdocmd
bin/phpunit
bin/php-cs-fixer
bin/sabre-cs-fixer
# Assuming every .php file in the root is for testing
/*.php
# Other testing stuff
/tmpdata
/data
/public
# Build
build
build.properties
# Docs
docs/api
docs/wikidocs
# Mac
.DS_Store

View file

@ -0,0 +1,32 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7
- hhvm
matrix:
fast_finish: true
env:
matrix:
- LOWEST_DEPS="" TEST_DEPS=""
- LOWEST_DEPS="--prefer-lowest" TEST_DEPS="tests/Sabre/"
services:
- mysql
sudo: false
cache: vendor
before_script:
- mysql -e 'create database sabredav'
# - composer self-update
- composer update --prefer-source $LOWEST_DEPS
script:
- ./bin/phpunit --configuration tests/phpunit.xml $TEST_DEPS
- ./bin/sabre-cs-fixer fix lib/ --dry-run --diff

View file

@ -0,0 +1,87 @@
Contributing to sabre projects
==============================
Want to contribute to sabre/dav? Here are some guidelines to ensure your patch
gets accepted.
Building a new feature? Contact us first
----------------------------------------
We may not want to accept every feature that comes our way. Sometimes
features are out of scope for our projects.
We don't want to waste your time, so by having a quick chat with us first,
you may find out quickly if the feature makes sense to us, and we can give
some tips on how to best build the feature.
If we don't accept the feature, it could be for a number of reasons. For
instance, we've rejected features in the past because we felt uncomfortable
assuming responsibility for maintaining the feature.
In those cases, it's often possible to keep the feature separate from the
sabre projects. sabre/dav for instance has a plugin system, and there's no
reason the feature can't live in a project you own.
In that case, definitely let us know about your plugin as well, so we can
feature it on [sabre.io][4].
We are often on [IRC][5], in the #sabredav channel on freenode. If there's
no one there, post a message on the [mailing list][6].
Coding standards
----------------
sabre projects follow:
1. [PSR-1][1]
2. [PSR-4][2]
sabre projects don't follow [PSR-2][3].
In addition to that, here's a list of basic rules:
1. PHP 5.4 array syntax must be used every where. This means you use `[` and
`]` instead of `array(` and `)`.
2. Use PHP namespaces everywhere.
3. Use 4 spaces for indentation.
4. Try to keep your lines under 80 characters. This is not a hard rule, as
there are many places in the source where it felt more sensibile to not
do so. In particular, function declarations are never split over multiple
lines.
5. Opening braces (`{`) are _always_ on the same line as the `class`, `if`,
`function`, etc. they belong to.
6. `public` must be omitted from method declarations. It must also be omitted
for static properties.
7. All files should use unix-line endings (`\n`).
8. Files must omit the closing php tag (`?>`).
9. `true`, `false` and `null` are always lower-case.
10. Constants are always upper-case.
11. Any of the rules stated before may be broken where this is the pragmatic
thing to do.
Unit test requirements
----------------------
Any new feature or change requires unittests. We use [PHPUnit][7] for all our
tests.
Adding unittests will greatly increase the likelyhood of us quickly accepting
your pull request. If unittests are not included though for whatever reason,
we'd still _love_ your pull request.
We may have to write the tests ourselves, which can increase the time it takes
to accept the patch, but we'd still really like your contribution!
To run the testsuite jump into the directory `cd tests` and trigger `phpunit`.
Make sure you did a `composer install` beforehand.
[1]: http://www.php-fig.org/psr/psr-1/
[2]: http://www.php-fig.org/psr/psr-4/
[3]: http://www.php-fig.org/psr/psr-2/
[4]: http://sabre.io/
[5]: irc://freenode.net/#sabredav
[6]: http://groups.google.com/group/sabredav-discuss
[7]: http://phpunit.de/

View file

@ -0,0 +1,177 @@
#!/usr/bin/env php
<?php
$tasks = [
'buildzip' => [
'init', 'test', 'clean',
],
'markrelease' => [
'init', 'test', 'clean',
],
'clean' => [],
'test' => [
'composerupdate',
],
'init' => [],
'composerupdate' => [],
];
$default = 'buildzip';
$baseDir = __DIR__ . '/../';
chdir($baseDir);
$currentTask = $default;
if ($argc > 1) $currentTask = $argv[1];
$version = null;
if ($argc > 2) $version = $argv[2];
if (!isset($tasks[$currentTask])) {
echo "Task not found: ", $currentTask, "\n";
die(1);
}
// Creating the dependency graph
$newTaskList = [];
$oldTaskList = [$currentTask => true];
while(count($oldTaskList)>0) {
foreach($oldTaskList as $task=>$foo) {
if (!isset($tasks[$task])) {
echo "Dependency not found: " . $task, "\n";
die(1);
}
$dependencies = $tasks[$task];
$fullFilled = true;
foreach($dependencies as $dependency) {
if (isset($newTaskList[$dependency])) {
// Already in the fulfilled task list.
continue;
} else {
$oldTaskList[$dependency] = true;
$fullFilled = false;
}
}
if ($fullFilled) {
unset($oldTaskList[$task]);
$newTaskList[$task] = 1;
}
}
}
foreach(array_keys($newTaskList) as $task) {
echo "task: " . $task, "\n";
call_user_func($task);
echo "\n";
}
function init() {
global $version;
if (!$version) {
include __DIR__ . '/../vendor/autoload.php';
$version = Sabre\DAV\Version::VERSION;
}
echo " Building sabre/dav " . $version, "\n";
}
function clean() {
global $baseDir;
echo " Removing build files\n";
$outputDir = $baseDir . '/build/SabreDAV';
if (is_dir($outputDir)) {
system('rm -r ' . $baseDir . '/build/SabreDAV');
}
}
function composerupdate() {
global $baseDir;
echo " Updating composer packages to latest version\n\n";
system('cd ' . $baseDir . '; composer update');
}
function test() {
global $baseDir;
echo " Running all unittests.\n";
echo " This may take a while.\n\n";
system(__DIR__ . '/phpunit --configuration ' . $baseDir . '/tests/phpunit.xml --stop-on-failure', $code);
if ($code != 0) {
echo "PHPUnit reported error code $code\n";
die(1);
}
}
function buildzip() {
global $baseDir, $version;
echo " Generating composer.json\n";
$input = json_decode(file_get_contents(__DIR__ . '/../composer.json'), true);
$newComposer = [
"require" => $input['require'],
"config" => [
"bin-dir" => "./bin",
],
"prefer-stable" => true,
"minimum-stability" => "alpha",
];
unset(
$newComposer['require']['sabre/vobject'],
$newComposer['require']['sabre/http'],
$newComposer['require']['sabre/uri'],
$newComposer['require']['sabre/event']
);
$newComposer['require']['sabre/dav'] = $version;
mkdir('build/SabreDAV');
file_put_contents('build/SabreDAV/composer.json', json_encode($newComposer, JSON_PRETTY_PRINT));
echo " Downloading dependencies\n";
system("cd build/SabreDAV; composer install -n", $code);
if ($code!==0) {
echo "Composer reported error code $code\n";
die(1);
}
echo " Removing pointless files\n";
unlink('build/SabreDAV/composer.json');
unlink('build/SabreDAV/composer.lock');
echo " Moving important files to the root of the project\n";
$fileNames = [
'CHANGELOG.md',
'LICENSE',
'README.md',
'examples',
];
foreach($fileNames as $fileName) {
echo " $fileName\n";
rename('build/SabreDAV/vendor/sabre/dav/' . $fileName, 'build/SabreDAV/' . $fileName);
}
// <zip destfile="build/SabreDAV-${sabredav.version}.zip" basedir="build/SabreDAV" prefix="SabreDAV/" />
echo "\n";
echo "Zipping the sabredav distribution\n\n";
system('cd build; zip -qr sabredav-' . $version . '.zip SabreDAV');
echo "Done.";
}

View file

@ -0,0 +1,248 @@
#!/usr/bin/env python
#
# Copyright 2006, 2007 Google Inc. All Rights Reserved.
# Author: danderson@google.com (David Anderson)
#
# Script for uploading files to a Google Code project.
#
# This is intended to be both a useful script for people who want to
# streamline project uploads and a reference implementation for
# uploading files to Google Code projects.
#
# To upload a file to Google Code, you need to provide a path to the
# file on your local machine, a small summary of what the file is, a
# project name, and a valid account that is a member or owner of that
# project. You can optionally provide a list of labels that apply to
# the file. The file will be uploaded under the same name that it has
# in your local filesystem (that is, the "basename" or last path
# component). Run the script with '--help' to get the exact syntax
# and available options.
#
# Note that the upload script requests that you enter your
# googlecode.com password. This is NOT your Gmail account password!
# This is the password you use on googlecode.com for committing to
# Subversion and uploading files. You can find your password by going
# to http://code.google.com/hosting/settings when logged in with your
# Gmail account. If you have already committed to your project's
# Subversion repository, the script will automatically retrieve your
# credentials from there (unless disabled, see the output of '--help'
# for details).
#
# If you are looking at this script as a reference for implementing
# your own Google Code file uploader, then you should take a look at
# the upload() function, which is the meat of the uploader. You
# basically need to build a multipart/form-data POST request with the
# right fields and send it to https://PROJECT.googlecode.com/files .
# Authenticate the request using HTTP Basic authentication, as is
# shown below.
#
# Licensed under the terms of the Apache Software License 2.0:
# http://www.apache.org/licenses/LICENSE-2.0
#
# Questions, comments, feature requests and patches are most welcome.
# Please direct all of these to the Google Code users group:
# http://groups.google.com/group/google-code-hosting
"""Google Code file uploader script.
"""
__author__ = 'danderson@google.com (David Anderson)'
import httplib
import os.path
import optparse
import getpass
import base64
import sys
def upload(file, project_name, user_name, password, summary, labels=None):
"""Upload a file to a Google Code project's file server.
Args:
file: The local path to the file.
project_name: The name of your project on Google Code.
user_name: Your Google account name.
password: The googlecode.com password for your account.
Note that this is NOT your global Google Account password!
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
Returns: a tuple:
http_status: 201 if the upload succeeded, something else if an
error occurred.
http_reason: The human-readable string associated with http_status
file_url: If the upload succeeded, the URL of the file on Google
Code, None otherwise.
"""
# The login is the user part of user@gmail.com. If the login provided
# is in the full user@domain form, strip it down.
if user_name.endswith('@gmail.com'):
user_name = user_name[:user_name.index('@gmail.com')]
form_fields = [('summary', summary)]
if labels is not None:
form_fields.extend([('label', l.strip()) for l in labels])
content_type, body = encode_upload_request(form_fields, file)
upload_host = '%s.googlecode.com' % project_name
upload_uri = '/files'
auth_token = base64.b64encode('%s:%s'% (user_name, password))
headers = {
'Authorization': 'Basic %s' % auth_token,
'User-Agent': 'Googlecode.com uploader v0.9.4',
'Content-Type': content_type,
}
server = httplib.HTTPSConnection(upload_host)
server.request('POST', upload_uri, body, headers)
resp = server.getresponse()
server.close()
if resp.status == 201:
location = resp.getheader('Location', None)
else:
location = None
return resp.status, resp.reason, location
def encode_upload_request(fields, file_path):
"""Encode the given fields and file into a multipart form body.
fields is a sequence of (name, value) pairs. file is the path of
the file to upload. The file will be uploaded to Google Code with
the same file name.
Returns: (content_type, body) ready for httplib.HTTP instance
"""
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
CRLF = '\r\n'
body = []
# Add the metadata about the upload first
for key, value in fields:
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="%s"' % key,
'',
value,
])
# Now add the file itself
file_name = os.path.basename(file_path)
f = open(file_path, 'rb')
file_content = f.read()
f.close()
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="filename"; filename="%s"'
% file_name,
# The upload server determines the mime-type, no need to set it.
'Content-Type: application/octet-stream',
'',
file_content,
])
# Finalize the form body
body.extend(['--' + BOUNDARY + '--', ''])
return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
def upload_find_auth(file_path, project_name, summary, labels=None,
user_name=None, password=None, tries=3):
"""Find credentials and upload a file to a Google Code project's file server.
file_path, project_name, summary, and labels are passed as-is to upload.
Args:
file_path: The local path to the file.
project_name: The name of your project on Google Code.
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
config_dir: Path to Subversion configuration directory, 'none', or None.
user_name: Your Google account name.
tries: How many attempts to make.
"""
while tries > 0:
if user_name is None:
# Read username if not specified or loaded from svn config, or on
# subsequent tries.
sys.stdout.write('Please enter your googlecode.com username: ')
sys.stdout.flush()
user_name = sys.stdin.readline().rstrip()
if password is None:
# Read password if not loaded from svn config, or on subsequent tries.
print 'Please enter your googlecode.com password.'
print '** Note that this is NOT your Gmail account password! **'
print 'It is the password you use to access Subversion repositories,'
print 'and can be found here: http://code.google.com/hosting/settings'
password = getpass.getpass()
status, reason, url = upload(file_path, project_name, user_name, password,
summary, labels)
# Returns 403 Forbidden instead of 401 Unauthorized for bad
# credentials as of 2007-07-17.
if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
# Rest for another try.
user_name = password = None
tries = tries - 1
else:
# We're done.
break
return status, reason, url
def main():
parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
'-p PROJECT [options] FILE')
parser.add_option('-s', '--summary', dest='summary',
help='Short description of the file')
parser.add_option('-p', '--project', dest='project',
help='Google Code project name')
parser.add_option('-u', '--user', dest='user',
help='Your Google Code username')
parser.add_option('-w', '--password', dest='password',
help='Your Google Code password')
parser.add_option('-l', '--labels', dest='labels',
help='An optional list of comma-separated labels to attach '
'to the file')
options, args = parser.parse_args()
if not options.summary:
parser.error('File summary is missing.')
elif not options.project:
parser.error('Project name is missing.')
elif len(args) < 1:
parser.error('File to upload not provided.')
elif len(args) > 1:
parser.error('Only one file may be specified.')
file_path = args[0]
if options.labels:
labels = options.labels.split(',')
else:
labels = None
status, reason, url = upload_find_auth(file_path, options.project,
options.summary, labels,
options.user, options.password)
if url:
print 'The file was uploaded successfully.'
print 'URL: %s' % url
return 0
else:
print 'An error occurred. Your file was not uploaded.'
print 'Google Code upload server said: %s (%s)' % (reason, status)
return 1
if __name__ == '__main__':
sys.exit(main())

View file

@ -0,0 +1,284 @@
#!/usr/bin/env php
<?php
echo "SabreDAV migrate script for version 1.7\n";
if ($argc<2) {
echo <<<HELLO
This script help you migrate from a pre-1.7 database to 1.7 and later\n
Both the 'calendarobjects' and 'calendars' tables will be upgraded.
If you do not have this table, or don't use the default PDO CalDAV backend
it's pointless to run this script.
Keep in mind that some processing will be done on every single record of this
table and in addition, ALTER TABLE commands will be executed.
If you have a large calendarobjects table, this may mean that this process
takes a while.
Usage:
php {$argv[0]} [pdo-dsn] [username] [password]
For example:
php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
php {$argv[0]} sqlite:data/sabredav.db
HELLO;
exit();
}
// There's a bunch of places where the autoloader could be, so we'll try all of
// them.
$paths = array(
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
);
foreach($paths as $path) {
if (file_exists($path)) {
include $path;
break;
}
}
$dsn = $argv[1];
$user = isset($argv[2])?$argv[2]:null;
$pass = isset($argv[3])?$argv[3]:null;
echo "Connecting to database: " . $dsn . "\n";
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
echo "Validating existing table layout\n";
// The only cross-db way to do this, is to just fetch a single record.
$row = $pdo->query("SELECT * FROM calendarobjects LIMIT 1")->fetch();
if (!$row) {
echo "Error: This database did not have any records in the calendarobjects table, you should just recreate the table.\n";
exit(-1);
}
$requiredFields = array(
'id',
'calendardata',
'uri',
'calendarid',
'lastmodified',
);
foreach($requiredFields as $requiredField) {
if (!array_key_exists($requiredField,$row)) {
echo "Error: The current 'calendarobjects' table was missing a field we expected to exist.\n";
echo "For safety reasons, this process is stopped.\n";
exit(-1);
}
}
$fields17 = array(
'etag',
'size',
'componenttype',
'firstoccurence',
'lastoccurence',
);
$found = 0;
foreach($fields17 as $field) {
if (array_key_exists($field, $row)) {
$found++;
}
}
if ($found === 0) {
echo "The database had the 1.6 schema. Table will now be altered.\n";
echo "This may take some time for large tables\n";
switch($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)) {
case 'mysql' :
$pdo->exec(<<<SQL
ALTER TABLE calendarobjects
ADD etag VARCHAR(32),
ADD size INT(11) UNSIGNED,
ADD componenttype VARCHAR(8),
ADD firstoccurence INT(11) UNSIGNED,
ADD lastoccurence INT(11) UNSIGNED
SQL
);
break;
case 'sqlite' :
$pdo->exec('ALTER TABLE calendarobjects ADD etag text');
$pdo->exec('ALTER TABLE calendarobjects ADD size integer');
$pdo->exec('ALTER TABLE calendarobjects ADD componenttype TEXT');
$pdo->exec('ALTER TABLE calendarobjects ADD firstoccurence integer');
$pdo->exec('ALTER TABLE calendarobjects ADD lastoccurence integer');
break;
default :
die('This upgrade script does not support this driver (' . $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) . ")\n");
}
echo "Database schema upgraded.\n";
} elseif ($found === 5) {
echo "Database already had the 1.7 schema\n";
} else {
echo "The database had $found out of 5 from the changes for 1.7. This is scary and unusual, so we have to abort.\n";
echo "You can manually try to upgrade the schema, and then run this script again.\n";
exit(-1);
}
echo "Now, we need to parse every record and pull out some information.\n";
$result = $pdo->query('SELECT id, calendardata FROM calendarobjects');
$stmt = $pdo->prepare('UPDATE calendarobjects SET etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ? WHERE id = ?');
echo "Total records found: " . $result->rowCount() . "\n";
$done = 0;
$total = $result->rowCount();
while($row = $result->fetch()) {
try {
$newData = getDenormalizedData($row['calendardata']);
} catch (Exception $e) {
echo "===\nException caught will trying to parser calendarobject.\n";
echo "Error message: " . $e->getMessage() . "\n";
echo "Record id: " . $row['id'] . "\n";
echo "This record is ignored, you should inspect it to see if there's anything wrong.\n===\n";
continue;
}
$stmt->execute(array(
$newData['etag'],
$newData['size'],
$newData['componentType'],
$newData['firstOccurence'],
$newData['lastOccurence'],
$row['id'],
));
$done++;
if ($done % 500 === 0) {
echo "Completed: $done / $total\n";
}
}
echo "Completed: $done / $total\n";
echo "Checking the calendars table needs changes.\n";
$row = $pdo->query("SELECT * FROM calendars LIMIT 1")->fetch();
if (array_key_exists('transparent', $row)) {
echo "The calendars table is already up to date\n";
} else {
echo "Adding the 'transparent' field to the calendars table\n";
switch($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)) {
case 'mysql' :
$pdo->exec("ALTER TABLE calendars ADD transparent TINYINT(1) NOT NULL DEFAULT '0'");
break;
case 'sqlite' :
$pdo->exec("ALTER TABLE calendars ADD transparent bool");
break;
default :
die('This upgrade script does not support this driver (' . $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) . ")\n");
}
}
echo "Process completed!\n";
/**
* Parses some information from calendar objects, used for optimized
* calendar-queries.
*
* Blantently copied from Sabre\CalDAV\Backend\PDO
*
* Returns an array with the following keys:
* * etag
* * size
* * componentType
* * firstOccurence
* * lastOccurence
*
* @param string $calendarData
* @return array
*/
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->value));
$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\Recur\EventIterator($vObject, (string)$component->UID);
$maxDate = new DateTime(\Sabre\CalDAV\Backend\PDO::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,
);
}

View file

@ -0,0 +1,453 @@
#!/usr/bin/env php
<?php
echo "SabreDAV migrate script for version 2.0\n";
if ($argc<2) {
echo <<<HELLO
This script help you migrate from a pre-2.0 database to 2.0 and later
The 'calendars', 'addressbooks' and 'cards' tables will be upgraded, and new
tables (calendarchanges, addressbookchanges, propertystorage) will be added.
If you don't use the default PDO CalDAV or CardDAV backend, it's pointless to
run this script.
Keep in mind that ALTER TABLE commands will be executed. If you have a large
dataset this may mean that this process takes a while.
Lastly: Make a back-up first. This script has been tested, but the amount of
potential variants are extremely high, so it's impossible to deal with every
possible situation.
In the worst case, you will lose all your data. This is not an overstatement.
Usage:
php {$argv[0]} [pdo-dsn] [username] [password]
For example:
php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
php {$argv[0]} sqlite:data/sabredav.db
HELLO;
exit();
}
// There's a bunch of places where the autoloader could be, so we'll try all of
// them.
$paths = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
];
foreach($paths as $path) {
if (file_exists($path)) {
include $path;
break;
}
}
$dsn = $argv[1];
$user = isset($argv[2])?$argv[2]:null;
$pass = isset($argv[3])?$argv[3]:null;
echo "Connecting to database: " . $dsn . "\n";
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
switch($driver) {
case 'mysql' :
echo "Detected MySQL.\n";
break;
case 'sqlite' :
echo "Detected SQLite.\n";
break;
default :
echo "Error: unsupported driver: " . $driver . "\n";
die(-1);
}
foreach(['calendar', 'addressbook'] as $itemType) {
$tableName = $itemType . 's';
$tableNameOld = $tableName . '_old';
$changesTable = $itemType . 'changes';
echo "Upgrading '$tableName'\n";
// The only cross-db way to do this, is to just fetch a single record.
$row = $pdo->query("SELECT * FROM $tableName LIMIT 1")->fetch();
if (!$row) {
echo "No records were found in the '$tableName' table.\n";
echo "\n";
echo "We're going to rename the old table to $tableNameOld (just in case).\n";
echo "and re-create the new table.\n";
switch($driver) {
case 'mysql' :
$pdo->exec("RENAME TABLE $tableName TO $tableNameOld");
switch($itemType) {
case 'calendar' :
$pdo->exec("
CREATE TABLE calendars (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principaluri VARCHAR(100),
displayname VARCHAR(100),
uri VARCHAR(200),
synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
description TEXT,
calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
calendarcolor VARCHAR(10),
timezone TEXT,
components VARCHAR(20),
transparent TINYINT(1) NOT NULL DEFAULT '0',
UNIQUE(principaluri, uri)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
");
break;
case 'addressbook' :
$pdo->exec("
CREATE TABLE addressbooks (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principaluri VARCHAR(255),
displayname VARCHAR(255),
uri VARCHAR(200),
description TEXT,
synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
UNIQUE(principaluri, uri)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
");
break;
}
break;
case 'sqlite' :
$pdo->exec("ALTER TABLE $tableName RENAME TO $tableNameOld");
switch($itemType) {
case 'calendar' :
$pdo->exec("
CREATE TABLE calendars (
id integer primary key asc,
principaluri text,
displayname text,
uri text,
synctoken integer,
description text,
calendarorder integer,
calendarcolor text,
timezone text,
components text,
transparent bool
);
");
break;
case 'addressbook' :
$pdo->exec("
CREATE TABLE addressbooks (
id integer primary key asc,
principaluri text,
displayname text,
uri text,
description text,
synctoken integer
);
");
break;
}
break;
}
echo "Creation of 2.0 $tableName table is complete\n";
} else {
// Checking if there's a synctoken field already.
if (array_key_exists('synctoken', $row)) {
echo "The 'synctoken' field already exists in the $tableName table.\n";
echo "It's likely you already upgraded, so we're simply leaving\n";
echo "the $tableName table alone\n";
} else {
echo "1.8 table schema detected\n";
switch($driver) {
case 'mysql' :
$pdo->exec("ALTER TABLE $tableName ADD synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1'");
$pdo->exec("ALTER TABLE $tableName DROP ctag");
$pdo->exec("UPDATE $tableName SET synctoken = '1'");
break;
case 'sqlite' :
$pdo->exec("ALTER TABLE $tableName ADD synctoken integer");
$pdo->exec("UPDATE $tableName SET synctoken = '1'");
echo "Note: there's no easy way to remove fields in sqlite.\n";
echo "The ctag field is no longer used, but it's kept in place\n";
break;
}
echo "Upgraded '$tableName' to 2.0 schema.\n";
}
}
try {
$pdo->query("SELECT * FROM $changesTable LIMIT 1");
echo "'$changesTable' already exists. Assuming that this part of the\n";
echo "upgrade was already completed.\n";
} catch (Exception $e) {
echo "Creating '$changesTable' table.\n";
switch($driver) {
case 'mysql' :
$pdo->exec("
CREATE TABLE $changesTable (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
uri VARCHAR(200) NOT NULL,
synctoken INT(11) UNSIGNED NOT NULL,
{$itemType}id INT(11) UNSIGNED NOT NULL,
operation TINYINT(1) NOT NULL,
INDEX {$itemType}id_synctoken ({$itemType}id, synctoken)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
");
break;
case 'sqlite' :
$pdo->exec("
CREATE TABLE $changesTable (
id integer primary key asc,
uri text,
synctoken integer,
{$itemType}id integer,
operation bool
);
");
$pdo->exec("CREATE INDEX {$itemType}id_synctoken ON $changesTable ({$itemType}id, synctoken);");
break;
}
}
}
try {
$pdo->query("SELECT * FROM calendarsubscriptions LIMIT 1");
echo "'calendarsubscriptions' already exists. Assuming that this part of the\n";
echo "upgrade was already completed.\n";
} catch (Exception $e) {
echo "Creating calendarsubscriptions table.\n";
switch($driver) {
case 'mysql' :
$pdo->exec("
CREATE TABLE calendarsubscriptions (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
uri VARCHAR(200) NOT NULL,
principaluri VARCHAR(100) NOT NULL,
source TEXT,
displayname VARCHAR(100),
refreshrate VARCHAR(10),
calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
calendarcolor VARCHAR(10),
striptodos TINYINT(1) NULL,
stripalarms TINYINT(1) NULL,
stripattachments TINYINT(1) NULL,
lastmodified INT(11) UNSIGNED,
UNIQUE(principaluri, uri)
);
");
break;
case 'sqlite' :
$pdo->exec("
CREATE TABLE calendarsubscriptions (
id integer primary key asc,
uri text,
principaluri text,
source text,
displayname text,
refreshrate text,
calendarorder integer,
calendarcolor text,
striptodos bool,
stripalarms bool,
stripattachments bool,
lastmodified int
);
");
$pdo->exec("CREATE INDEX principaluri_uri ON calendarsubscriptions (principaluri, uri);");
break;
}
}
try {
$pdo->query("SELECT * FROM propertystorage LIMIT 1");
echo "'propertystorage' already exists. Assuming that this part of the\n";
echo "upgrade was already completed.\n";
} catch (Exception $e) {
echo "Creating propertystorage table.\n";
switch($driver) {
case 'mysql' :
$pdo->exec("
CREATE TABLE propertystorage (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
path VARBINARY(1024) NOT NULL,
name VARBINARY(100) NOT NULL,
value MEDIUMBLOB
);
");
$pdo->exec("
CREATE UNIQUE INDEX path_property ON propertystorage (path(600), name(100));
");
break;
case 'sqlite' :
$pdo->exec("
CREATE TABLE propertystorage (
id integer primary key asc,
path TEXT,
name TEXT,
value TEXT
);
");
$pdo->exec("
CREATE UNIQUE INDEX path_property ON propertystorage (path, name);
");
break;
}
}
echo "Upgrading cards table to 2.0 schema\n";
try {
$create = false;
$row = $pdo->query("SELECT * FROM cards LIMIT 1")->fetch();
if (!$row) {
$random = mt_rand(1000,9999);
echo "There was no data in the cards table, so we're re-creating it\n";
echo "The old table will be renamed to cards_old$random, just in case.\n";
$create = true;
switch($driver) {
case 'mysql' :
$pdo->exec("RENAME TABLE cards TO cards_old$random");
break;
case 'sqlite' :
$pdo->exec("ALTER TABLE cards RENAME TO cards_old$random");
break;
}
}
} catch (Exception $e) {
echo "Exception while checking cards table. Assuming that the table does not yet exist.\n";
echo "Debug: ", $e->getMessage(), "\n";
$create = true;
}
if ($create) {
switch($driver) {
case 'mysql' :
$pdo->exec("
CREATE TABLE cards (
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
addressbookid INT(11) UNSIGNED NOT NULL,
carddata MEDIUMBLOB,
uri VARCHAR(200),
lastmodified INT(11) UNSIGNED,
etag VARBINARY(32),
size INT(11) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
");
break;
case 'sqlite' :
$pdo->exec("
CREATE TABLE cards (
id integer primary key asc,
addressbookid integer,
carddata blob,
uri text,
lastmodified integer,
etag text,
size integer
);
");
break;
}
} else {
switch($driver) {
case 'mysql' :
$pdo->exec("
ALTER TABLE cards
ADD etag VARBINARY(32),
ADD size INT(11) UNSIGNED NOT NULL;
");
break;
case 'sqlite' :
$pdo->exec("
ALTER TABLE cards ADD etag text;
ALTER TABLE cards ADD size integer;
");
break;
}
echo "Reading all old vcards and populating etag and size fields.\n";
$result = $pdo->query('SELECT id, carddata FROM cards');
$stmt = $pdo->prepare('UPDATE cards SET etag = ?, size = ? WHERE id = ?');
while($row = $result->fetch(\PDO::FETCH_ASSOC)) {
$stmt->execute([
md5($row['carddata']),
strlen($row['carddata']),
$row['id']
]);
}
}
echo "Upgrade to 2.0 schema completed.\n";

View file

@ -0,0 +1,180 @@
#!/usr/bin/env php
<?php
echo "SabreDAV migrate script for version 2.1\n";
if ($argc<2) {
echo <<<HELLO
This script help you migrate from a pre-2.1 database to 2.1.
Changes:
The 'calendarobjects' table will be upgraded.
'schedulingobjects' will be created.
If you don't use the default PDO CalDAV or CardDAV backend, it's pointless to
run this script.
Keep in mind that ALTER TABLE commands will be executed. If you have a large
dataset this may mean that this process takes a while.
Lastly: Make a back-up first. This script has been tested, but the amount of
potential variants are extremely high, so it's impossible to deal with every
possible situation.
In the worst case, you will lose all your data. This is not an overstatement.
Usage:
php {$argv[0]} [pdo-dsn] [username] [password]
For example:
php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
php {$argv[0]} sqlite:data/sabredav.db
HELLO;
exit();
}
// There's a bunch of places where the autoloader could be, so we'll try all of
// them.
$paths = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
];
foreach($paths as $path) {
if (file_exists($path)) {
include $path;
break;
}
}
$dsn = $argv[1];
$user = isset($argv[2])?$argv[2]:null;
$pass = isset($argv[3])?$argv[3]:null;
echo "Connecting to database: " . $dsn . "\n";
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
switch($driver) {
case 'mysql' :
echo "Detected MySQL.\n";
break;
case 'sqlite' :
echo "Detected SQLite.\n";
break;
default :
echo "Error: unsupported driver: " . $driver . "\n";
die(-1);
}
echo "Upgrading 'calendarobjects'\n";
$addUid = false;
try {
$result = $pdo->query('SELECT * FROM calendarobjects LIMIT 1');
$row = $result->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
echo "No data in table. Going to try to add the uid field anyway.\n";
$addUid = true;
} elseif (array_key_exists('uid', $row)) {
echo "uid field exists. Assuming that this part of the migration has\n";
echo "Already been completed.\n";
} else {
echo "2.0 schema detected.\n";
$addUid = true;
}
} catch (Exception $e) {
echo "Could not find a calendarobjects table. Skipping this part of the\n";
echo "upgrade.\n";
}
if ($addUid) {
switch($driver) {
case 'mysql' :
$pdo->exec('ALTER TABLE calendarobjects ADD uid VARCHAR(200)');
break;
case 'sqlite' :
$pdo->exec('ALTER TABLE calendarobjects ADD uid TEXT');
break;
}
$result = $pdo->query('SELECT id, calendardata FROM calendarobjects');
$stmt = $pdo->prepare('UPDATE calendarobjects SET uid = ? WHERE id = ?');
$counter = 0;
while($row = $result->fetch(\PDO::FETCH_ASSOC)) {
try {
$vobj = \Sabre\VObject\Reader::read($row['calendardata']);
} catch (\Exception $e) {
echo "Warning! Item with id $row[id] could not be parsed!\n";
continue;
}
$uid = null;
$item = $vobj->getBaseComponent();
if (!isset($item->UID)) {
echo "Warning! Item with id $item[id] does NOT have a UID property and this is required.\n";
continue;
}
$uid = (string)$item->UID;
$stmt->execute([$uid, $row['id']]);
$counter++;
}
}
echo "Creating 'schedulingobjects'\n";
switch($driver) {
case 'mysql' :
$pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects
(
id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
principaluri VARCHAR(255),
calendardata MEDIUMBLOB,
uri VARCHAR(200),
lastmodified INT(11) UNSIGNED,
etag VARCHAR(32),
size INT(11) UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
');
break;
case 'sqlite' :
$pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects (
id integer primary key asc,
principaluri text,
calendardata blob,
uri text,
lastmodified integer,
etag text,
size integer
)
');
break;
$pdo->exec('
CREATE INDEX principaluri_uri ON calendarsubscriptions (principaluri, uri);
');
break;
}
echo "Done.\n";
echo "Upgrade to 2.1 schema completed.\n";

View file

@ -0,0 +1,171 @@
#!/usr/bin/env php
<?php
echo "SabreDAV migrate script for version 3.0\n";
if ($argc<2) {
echo <<<HELLO
This script help you migrate from a pre-3.0 database to 3.0 and later
Changes:
* The propertystorage table has changed to allow storage of complex
properties.
* the vcardurl field in the principals table is no more. This was moved to
the propertystorage table.
Keep in mind that ALTER TABLE commands will be executed. If you have a large
dataset this may mean that this process takes a while.
Lastly: Make a back-up first. This script has been tested, but the amount of
potential variants are extremely high, so it's impossible to deal with every
possible situation.
In the worst case, you will lose all your data. This is not an overstatement.
Usage:
php {$argv[0]} [pdo-dsn] [username] [password]
For example:
php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
php {$argv[0]} sqlite:data/sabredav.db
HELLO;
exit();
}
// There's a bunch of places where the autoloader could be, so we'll try all of
// them.
$paths = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
];
foreach($paths as $path) {
if (file_exists($path)) {
include $path;
break;
}
}
$dsn = $argv[1];
$user = isset($argv[2])?$argv[2]:null;
$pass = isset($argv[3])?$argv[3]:null;
echo "Connecting to database: " . $dsn . "\n";
$pdo = new PDO($dsn, $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
switch($driver) {
case 'mysql' :
echo "Detected MySQL.\n";
break;
case 'sqlite' :
echo "Detected SQLite.\n";
break;
default :
echo "Error: unsupported driver: " . $driver . "\n";
die(-1);
}
echo "Upgrading 'propertystorage'\n";
$addValueType = false;
try {
$result = $pdo->query('SELECT * FROM propertystorage LIMIT 1');
$row = $result->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
echo "No data in table. Going to re-create the table.\n";
$random = mt_rand(1000,9999);
echo "Renaming propertystorage -> propertystorage_old$random and creating new table.\n";
switch($driver) {
case 'mysql' :
$pdo->exec('RENAME TABLE propertystorage TO propertystorage_old' . $random);
$pdo->exec('
CREATE TABLE propertystorage (
id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
path VARBINARY(1024) NOT NULL,
name VARBINARY(100) NOT NULL,
valuetype INT UNSIGNED,
value MEDIUMBLOB
);
');
$pdo->exec('CREATE UNIQUE INDEX path_property_' . $random . ' ON propertystorage (path(600), name(100));');
break;
case 'sqlite' :
$pdo->exec('ALTER TABLE propertystorage RENAME TO propertystorage_old' . $random);
$pdo->exec('
CREATE TABLE propertystorage (
id integer primary key asc,
path text,
name text,
valuetype integer,
value blob
);');
$pdo->exec('CREATE UNIQUE INDEX path_property_' . $random . ' ON propertystorage (path, name);');
break;
}
} elseif (array_key_exists('valuetype', $row)) {
echo "valuetype field exists. Assuming that this part of the migration has\n";
echo "Already been completed.\n";
} else {
echo "2.1 schema detected. Going to perform upgrade.\n";
$addValueType = true;
}
} catch (Exception $e) {
echo "Could not find a propertystorage table. Skipping this part of the\n";
echo "upgrade.\n";
echo $e->getMessage(), "\n";
}
if ($addValueType) {
switch($driver) {
case 'mysql' :
$pdo->exec('ALTER TABLE propertystorage ADD valuetype INT UNSIGNED');
break;
case 'sqlite' :
$pdo->exec('ALTER TABLE propertystorage ADD valuetype INT');
break;
}
$pdo->exec('UPDATE propertystorage SET valuetype = 1 WHERE valuetype IS NULL ');
}
echo "Migrating vcardurl\n";
$result = $pdo->query('SELECT id, uri, vcardurl FROM principals WHERE vcardurl IS NOT NULL');
$stmt1 = $pdo->prepare('INSERT INTO propertystorage (path, name, valuetype, value) VALUES (?, ?, 3, ?)');
while($row = $result->fetch(\PDO::FETCH_ASSOC)) {
// Inserting the new record
$stmt1->execute([
'addressbooks/' . basename($row['uri']),
'{http://calendarserver.org/ns/}me-card',
serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl']))
]);
echo serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl']));
}
echo "Done.\n";
echo "Upgrade to 3.0 schema completed.\n";

View file

@ -0,0 +1,140 @@
#!/usr/bin/env python
#
# Copyright (c) 2009-2010 Evert Pot
# All rights reserved.
# http://www.rooftopsolutions.nl/
#
# This utility is distributed along with SabreDAV
# license: http://sabre.io/license/ Modified BSD License
import os
from optparse import OptionParser
import time
def getfreespace(path):
stat = os.statvfs(path)
return stat.f_frsize * stat.f_bavail
def getbytesleft(path,threshold):
return getfreespace(path)-threshold
def run(cacheDir, threshold, sleep=5, simulate=False, min_erase = 0):
bytes = getbytesleft(cacheDir,threshold)
if (bytes>0):
print "Bytes to go before we hit threshold:", bytes
else:
print "Threshold exceeded with:", -bytes, "bytes"
dir = os.listdir(cacheDir)
dir2 = []
for file in dir:
path = cacheDir + '/' + file
dir2.append({
"path" : path,
"atime": os.stat(path).st_atime,
"size" : os.stat(path).st_size
})
dir2.sort(lambda x,y: int(x["atime"]-y["atime"]))
filesunlinked = 0
gainedspace = 0
# Left is the amount of bytes that need to be freed up
# The default is the 'min_erase setting'
left = min_erase
# If the min_erase setting is lower than the amount of bytes over
# the threshold, we use that number instead.
if left < -bytes :
left = -bytes
print "Need to delete at least:", left;
for file in dir2:
# Only deleting files if we're not simulating
if not simulate: os.unlink(file["path"])
left = int(left - file["size"])
gainedspace = gainedspace + file["size"]
filesunlinked = filesunlinked + 1
if(left<0):
break
print "%d files deleted (%d bytes)" % (filesunlinked, gainedspace)
time.sleep(sleep)
def main():
parser = OptionParser(
version="naturalselection v0.3",
description="Cache directory manager. Deletes cache entries based on accesstime and free space thresholds.\n" +
"This utility is distributed alongside SabreDAV.",
usage="usage: %prog [options] cacheDirectory",
)
parser.add_option(
'-s',
dest="simulate",
action="store_true",
help="Don't actually make changes, but just simulate the behaviour",
)
parser.add_option(
'-r','--runs',
help="How many times to check before exiting. -1 is infinite, which is the default",
type="int",
dest="runs",
default=-1
)
parser.add_option(
'-n','--interval',
help="Sleep time in seconds (default = 5)",
type="int",
dest="sleep",
default=5
)
parser.add_option(
'-l','--threshold',
help="Threshold in bytes (default = 10737418240, which is 10GB)",
type="int",
dest="threshold",
default=10737418240
)
parser.add_option(
'-m', '--min-erase',
help="Minimum number of bytes to erase when the threshold is reached. " +
"Setting this option higher will reduce the amount of times the cache directory will need to be scanned. " +
"(the default is 1073741824, which is 1GB.)",
type="int",
dest="min_erase",
default=1073741824
)
options,args = parser.parse_args()
if len(args)<1:
parser.error("This utility requires at least 1 argument")
cacheDir = args[0]
print "Natural Selection"
print "Cache directory:", cacheDir
free = getfreespace(cacheDir);
print "Current free disk space:", free
runs = options.runs;
while runs!=0 :
run(
cacheDir,
sleep=options.sleep,
simulate=options.simulate,
threshold=options.threshold,
min_erase=options.min_erase
)
if runs>0:
runs = runs - 1
if __name__ == '__main__' :
main()

View file

@ -0,0 +1,2 @@
#!/bin/sh
php -S 0.0.0.0:8080 `dirname $0`/sabredav.php

View file

@ -0,0 +1,53 @@
<?php
// SabreDAV test server.
class CliLog {
protected $stream;
function __construct() {
$this->stream = fopen('php://stdout','w');
}
function log($msg) {
fwrite($this->stream, $msg . "\n");
}
}
$log = new CliLog();
if (php_sapi_name()!=='cli-server') {
die("This script is intended to run on the built-in php webserver");
}
// Finding composer
$paths = array(
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
);
foreach($paths as $path) {
if (file_exists($path)) {
include $path;
break;
}
}
use Sabre\DAV;
// Root
$root = new DAV\FS\Directory(getcwd());
// Setting up server.
$server = new DAV\Server($root);
// Browser plugin
$server->addPlugin(new DAV\Browser\Plugin());
$server->exec();

View file

@ -0,0 +1,66 @@
{
"name": "sabre/dav",
"type": "library",
"description": "WebDAV Framework for PHP",
"keywords": ["Framework", "WebDAV", "CalDAV", "CardDAV", "iCalendar"],
"homepage": "http://sabre.io/",
"license" : "BSD-3-Clause",
"authors": [
{
"name": "Evert Pot",
"email": "me@evertpot.com",
"homepage" : "http://evertpot.com/",
"role" : "Developer"
}
],
"require": {
"php": ">=5.4.1",
"sabre/vobject": "^3.3.4",
"sabre/event" : "~2.0",
"sabre/xml" : "~1.0",
"sabre/http" : "~4.0",
"sabre/uri" : "~1.0",
"ext-dom": "*",
"ext-pcre": "*",
"ext-spl": "*",
"ext-simplexml": "*",
"ext-mbstring" : "*",
"ext-ctype" : "*",
"ext-date" : "*",
"ext-iconv" : "*",
"lib-libxml" : ">=2.7.0"
},
"require-dev" : {
"phpunit/phpunit" : "~4.2",
"evert/phpdoc-md" : "~0.1.0",
"sabre/cs" : "~0.0.2"
},
"suggest" : {
"ext-curl" : "*",
"ext-pdo" : "*"
},
"autoload": {
"psr-4" : {
"Sabre\\DAV\\" : "lib/DAV/",
"Sabre\\DAVACL\\" : "lib/DAVACL/",
"Sabre\\CalDAV\\" : "lib/CalDAV/",
"Sabre\\CardDAV\\" : "lib/CardDAV/"
}
},
"support" : {
"forum" : "https://groups.google.com/group/sabredav-discuss",
"source" : "https://github.com/fruux/sabre-dav"
},
"bin" : [
"bin/sabredav",
"bin/naturalselection"
],
"config" : {
"bin-dir" : "./bin"
},
"extra" : {
"branch-alias": {
"dev-master": "3.0.0-dev"
}
}
}

View file

@ -0,0 +1,221 @@
<?php
namespace Sabre\CalDAV\Backend;
use Sabre\VObject;
use Sabre\CalDAV;
/**
* Abstract Calendaring backend. Extend this class to create your own backends.
*
* Checkout the BackendInterface for all the methods that must be implemented.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class AbstractBackend implements BackendInterface {
/**
* Updates properties for a calendar.
*
* 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 updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {
}
/**
* Returns a list of calendar objects.
*
* This method should work identical to getCalendarObject, but instead
* return all the calendar objects in the list as an array.
*
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $calendarId
* @param array $uris
* @return array
*/
function getMultipleCalendarObjects($calendarId, array $uris) {
return array_map(function($uri) use ($calendarId) {
return $this->getCalendarObject($calendarId, $uri);
}, $uris);
}
/**
* 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 either VEVENT or VTODO.
*
* ..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.
*
* @param mixed $calendarId
* @param array $filters
* @return array
*/
function calendarQuery($calendarId, array $filters) {
$result = [];
$objects = $this->getCalendarObjects($calendarId);
foreach ($objects as $object) {
if ($this->validateFilterForObject($object, $filters)) {
$result[] = $object['uri'];
}
}
return $result;
}
/**
* This method validates if a filter (as passed to calendarQuery) matches
* the given object.
*
* @param array $object
* @param array $filters
* @return bool
*/
protected function validateFilterForObject(array $object, array $filters) {
// Unfortunately, setting the 'calendardata' here is optional. If
// it was excluded, we actually need another call to get this as
// well.
if (!isset($object['calendardata'])) {
$object = $this->getCalendarObject($object['calendarid'], $object['uri']);
}
$vObject = VObject\Reader::read($object['calendardata']);
$validator = new CalDAV\CalendarQueryValidator();
return $validator->validate($vObject, $filters);
}
/**
* Searches through all of a users calendars and calendar objects to find
* an object with a specific UID.
*
* This method should return the path to this object, relative to the
* calendar home, so this path usually only contains two parts:
*
* calendarpath/objectpath.ics
*
* If the uid is not found, return null.
*
* This method should only consider * objects that the principal owns, so
* any calendars owned by other principals that also appear in this
* collection should be ignored.
*
* @param string $principalUri
* @param string $uid
* @return string|null
*/
function getCalendarObjectByUID($principalUri, $uid) {
// Note: this is a super slow naive implementation of this method. You
// are highly recommended to optimize it, if your backend allows it.
foreach ($this->getCalendarsForUser($principalUri) as $calendar) {
// We must ignore calendars owned by other principals.
if ($calendar['principaluri'] !== $principalUri) {
continue;
}
// Ignore calendars that are shared.
if (isset($calendar['{http://sabredav.org/ns}owner-principal']) && $calendar['{http://sabredav.org/ns}owner-principal'] !== $principalUri) {
continue;
}
$results = $this->calendarQuery(
$calendar['id'],
[
'name' => 'VCALENDAR',
'prop-filters' => [],
'comp-filters' => [
[
'name' => 'VEVENT',
'is-not-defined' => false,
'time-range' => null,
'comp-filters' => [],
'prop-filters' => [
[
'name' => 'UID',
'is-not-defined' => false,
'time-range' => null,
'text-match' => [
'value' => $uid,
'negate-condition' => false,
'collation' => 'i;octet',
],
'param-filters' => [],
],
]
]
],
]
);
if ($results) {
// We have a match
return $calendar['uri'] . '/' . $results[0];
}
}
}
}

View file

@ -0,0 +1,268 @@
<?php
namespace Sabre\CalDAV\Backend;
/**
* Every CalDAV backend must at least implement this interface.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface BackendInterface {
/**
* 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 is 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.
*
* If you return {http://sabredav.org/ns}read-only and set the value to 1,
* ACL will automatically be put in read-only mode.
*
* @param string $principalUri
* @return array
*/
function getCalendarsForUser($principalUri);
/**
* 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
*/
function createCalendar($principalUri, $calendarUri, array $properties);
/**
* Updates properties for a calendar.
*
* 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 documentation for more info and examples.
*
* @param string $path
* @param \Sabre\DAV\PropPatch $propPatch
* @return void
*/
function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch);
/**
* Delete a calendar and all its objects
*
* @param mixed $calendarId
* @return void
*/
function deleteCalendar($calendarId);
/**
* Returns all calendar objects within a calendar.
*
* Every item contains an array with the following keys:
* * 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"')
* * 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
*/
function getCalendarObjects($calendarId);
/**
* 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
*/
function getCalendarObject($calendarId, $objectUri);
/**
* Returns a list of calendar objects.
*
* This method should work identical to getCalendarObject, but instead
* return all the calendar objects in the list as an array.
*
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $calendarId
* @param array $uris
* @return array
*/
function getMultipleCalendarObjects($calendarId, array $uris);
/**
* Creates a new calendar object.
*
* The object uri is only the basename, or filename and not a full path.
*
* It is possible to 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
*/
function createCalendarObject($calendarId, $objectUri, $calendarData);
/**
* 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
*/
function updateCalendarObject($calendarId, $objectUri, $calendarData);
/**
* 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
*/
function deleteCalendarObject($calendarId, $objectUri);
/**
* 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 either VEVENT or VTODO.
*
* ..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.
*
* @param mixed $calendarId
* @param array $filters
* @return array
*/
function calendarQuery($calendarId, array $filters);
/**
* Searches through all of a users calendars and calendar objects to find
* an object with a specific UID.
*
* This method should return the path to this object, relative to the
* calendar home, so this path usually only contains two parts:
*
* calendarpath/objectpath.ics
*
* If the uid is not found, return null.
*
* This method should only consider * objects that the principal owns, so
* any calendars owned by other principals that also appear in this
* collection should be ignored.
*
* @param string $principalUri
* @param string $uid
* @return string|null
*/
function getCalendarObjectByUID($principalUri, $uid);
}

View file

@ -0,0 +1,46 @@
<?php
namespace Sabre\CalDAV\Backend;
use Sabre\CalDAV\Xml\Notification\NotificationInterface;
/**
* Adds caldav notification support to a backend.
*
* Note: This feature is experimental, and may change in between different
* SabreDAV versions.
*
* Notifications are defined at:
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-notifications.txt
*
* These notifications are basically a list of server-generated notifications
* displayed to the user. Users can dismiss notifications by deleting them.
*
* The primary usecase is to allow for calendar-sharing.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface NotificationSupport extends BackendInterface {
/**
* Returns a list of notifications for a given principal url.
*
* @param string $principalUri
* @return NotificationInterface[]
*/
function getNotificationsForPrincipal($principalUri);
/**
* This deletes a specific notifcation.
*
* This may be called by a client once it deems a notification handled.
*
* @param string $principalUri
* @param NotificationInterface $notification
* @return void
*/
function deleteNotification($principalUri, NotificationInterface $notification);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,65 @@
<?php
namespace Sabre\CalDAV\Backend;
/**
* Implementing this interface adds CalDAV Scheduling support to your caldav
* server, as defined in rfc6638.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface SchedulingSupport extends BackendInterface {
/**
* Returns a single scheduling object for the inbox collection.
*
* The returned array should contain the following elements:
* * uri - A unique basename for the object. This will be used to
* construct a full uri.
* * calendardata - The iCalendar object
* * lastmodified - The last modification date. Can be an int for a unix
* timestamp, or a PHP DateTime object.
* * etag - A unique token that must change if the object changed.
* * size - The size of the object, in bytes.
*
* @param string $principalUri
* @param string $objectUri
* @return array
*/
function getSchedulingObject($principalUri, $objectUri);
/**
* Returns all scheduling objects for the inbox collection.
*
* These objects should be returned as an array. Every item in the array
* should follow the same structure as returned from getSchedulingObject.
*
* The main difference is that 'calendardata' is optional.
*
* @param string $principalUri
* @return array
*/
function getSchedulingObjects($principalUri);
/**
* Deletes a scheduling object from the inbox collection.
*
* @param string $principalUri
* @param string $objectUri
* @return void
*/
function deleteSchedulingObject($principalUri, $objectUri);
/**
* Creates a new scheduling object. This should land in a users' inbox.
*
* @param string $principalUri
* @param string $objectUri
* @param string $objectData
* @return void
*/
function createSchedulingObject($principalUri, $objectUri, $objectData);
}

View file

@ -0,0 +1,527 @@
<?php
namespace Sabre\CalDAV;
use Sabre\DAV;
use Sabre\DAVACL;
use Sabre\DAV\PropPatch;
/**
* This object represents a CalDAV calendar.
*
* A calendar can contain multiple TODO and or Events. These are represented
* as \Sabre\CalDAV\CalendarObject objects.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, DAV\IMultiGet {
/**
* This is an array with calendar information
*
* @var array
*/
protected $calendarInfo;
/**
* CalDAV backend
*
* @var Backend\BackendInterface
*/
protected $caldavBackend;
/**
* Constructor
*
* @param Backend\BackendInterface $caldavBackend
* @param array $calendarInfo
*/
function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
$this->caldavBackend = $caldavBackend;
$this->calendarInfo = $calendarInfo;
}
/**
* Returns the name of the calendar
*
* @return string
*/
function getName() {
return $this->calendarInfo['uri'];
}
/**
* Updates properties on this node.
*
* This method received a PropPatch object, which contains all the
* information about the update.
*
* To update specific properties, call the 'handle' method on this object.
* Read the PropPatch documentation for more information.
*
* @param PropPatch $propPatch
* @return void
*/
function propPatch(PropPatch $propPatch) {
return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch);
}
/**
* Returns the list of properties
*
* @param array $requestedProperties
* @return array
*/
function getProperties($requestedProperties) {
$response = [];
foreach ($this->calendarInfo as $propName => $propValue) {
if ($propName[0] === '{')
$response[$propName] = $this->calendarInfo[$propName];
}
return $response;
}
/**
* Returns a calendar object
*
* The contained calendar objects are for example Events or Todo's.
*
* @param string $name
* @return \Sabre\CalDAV\ICalendarObject
*/
function getChild($name) {
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
if (!$obj) throw new DAV\Exception\NotFound('Calendar object not found');
$obj['acl'] = $this->getChildACL();
return new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
}
/**
* Returns the full list of calendar objects
*
* @return array
*/
function getChildren() {
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
$children = [];
foreach ($objs as $obj) {
$obj['acl'] = $this->getChildACL();
$children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
}
return $children;
}
/**
* This method receives a list of paths in it's first argument.
* It must return an array with Node objects.
*
* If any children are not found, you do not have to return them.
*
* @param string[] $paths
* @return array
*/
function getMultipleChildren(array $paths) {
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
$children = [];
foreach ($objs as $obj) {
$obj['acl'] = $this->getChildACL();
$children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
}
return $children;
}
/**
* Checks if a child-node exists.
*
* @param string $name
* @return bool
*/
function childExists($name) {
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
if (!$obj)
return false;
else
return true;
}
/**
* Creates a new directory
*
* We actually block this, as subdirectories are not allowed in calendars.
*
* @param string $name
* @return void
*/
function createDirectory($name) {
throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
}
/**
* Creates a new file
*
* The contents of the new file must be a valid ICalendar string.
*
* @param string $name
* @param resource $calendarData
* @return string|null
*/
function createFile($name, $calendarData = null) {
if (is_resource($calendarData)) {
$calendarData = stream_get_contents($calendarData);
}
return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'], $name, $calendarData);
}
/**
* Deletes the calendar.
*
* @return void
*/
function delete() {
$this->caldavBackend->deleteCalendar($this->calendarInfo['id']);
}
/**
* Renames the calendar. Note that most calendars use the
* {DAV:}displayname to display a name to display a name.
*
* @param string $newName
* @return void
*/
function setName($newName) {
throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
}
/**
* Returns the last modification date as a unix timestamp.
*
* @return void
*/
function getLastModified() {
return null;
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->calendarInfo['principaluri'];
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup() {
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
$acl = [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
[
'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
'principal' => '{DAV:}authenticated',
'protected' => true,
],
];
if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner(),
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
];
}
return $acl;
}
/**
* This method returns the ACL's for calendar objects in this calendar.
* The result of this method automatically gets passed to the
* calendar-object nodes in the calendar.
*
* @return array
*/
function getChildACL() {
$acl = [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
];
if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner(),
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
];
}
return $acl;
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's.
*
* @param array $acl
* @return void
*/
function setACL(array $acl) {
throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
}
/**
* Returns the list of supported privileges for this node.
*
* The returned data structure is a list of nested privileges.
* See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
* standard structure.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet() {
$default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
// We need to inject 'read-free-busy' in the tree, aggregated under
// {DAV:}read.
foreach ($default['aggregates'] as &$agg) {
if ($agg['privilege'] !== '{DAV:}read') continue;
$agg['aggregates'][] = [
'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
];
}
return $default;
}
/**
* 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.
*
* @param array $filters
* @return array
*/
function calendarQuery(array $filters) {
return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
}
/**
* This method returns the current sync-token for this collection.
* This can be any string.
*
* If null is returned from this function, the plugin assumes there's no
* sync information available.
*
* @return string|null
*/
function getSyncToken() {
if (
$this->caldavBackend instanceof Backend\SyncSupport &&
isset($this->calendarInfo['{DAV:}sync-token'])
) {
return $this->calendarInfo['{DAV:}sync-token'];
}
if (
$this->caldavBackend instanceof Backend\SyncSupport &&
isset($this->calendarInfo['{http://sabredav.org/ns}sync-token'])
) {
return $this->calendarInfo['{http://sabredav.org/ns}sync-token'];
}
}
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken and the current collection.
*
* This function should return an array, such as the following:
*
* [
* 'syncToken' => 'The current synctoken',
* 'added' => [
* 'new.txt',
* ],
* 'modified' => [
* 'modified.txt',
* ],
* 'deleted' => [
* 'foo.php.bak',
* 'old.txt'
* ]
* ];
*
* The syncToken property should reflect the *current* syncToken of the
* collection, as reported getSyncToken(). This is needed here too, to
* ensure the operation is atomic.
*
* If the syncToken is specified as null, this is an initial sync, and all
* members should be reported.
*
* The modified property is an array of nodenames that have changed since
* the last token.
*
* The deleted property is an array with nodenames, that have been deleted
* from collection.
*
* The second argument is basically the 'depth' of the report. If it's 1,
* you only have to report changes that happened only directly in immediate
* descendants. If it's 2, it should also include changes from the nodes
* below the child collections. (grandchildren)
*
* The third (optional) argument allows a client to specify how many
* results should be returned at most. If the limit is not specified, it
* should be treated as infinite.
*
* If the limit (infinite or not) is higher than you're willing to return,
* you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
*
* If the syncToken is expired (due to data cleanup) or unknown, you must
* return null.
*
* The limit is 'suggestive'. You are free to ignore it.
*
* @param string $syncToken
* @param int $syncLevel
* @param int $limit
* @return array
*/
function getChanges($syncToken, $syncLevel, $limit = null) {
if (!$this->caldavBackend instanceof Backend\SyncSupport) {
return null;
}
return $this->caldavBackend->getChangesForCalendar(
$this->calendarInfo['id'],
$syncToken,
$syncLevel,
$limit
);
}
}

View file

@ -0,0 +1,430 @@
<?php
namespace Sabre\CalDAV;
use Sabre\DAV;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\MkCol;
use Sabre\DAVACL;
use Sabre\HTTP\URLUtil;
/**
* The CalendarHome represents a node that is usually in a users'
* calendar-homeset.
*
* It contains all the users' calendars, and can optionally contain a
* notifications collection, calendar subscriptions, a users' inbox, and a
* users' outbox.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL {
/**
* CalDAV backend
*
* @var Sabre\CalDAV\Backend\BackendInterface
*/
protected $caldavBackend;
/**
* Principal information
*
* @var array
*/
protected $principalInfo;
/**
* Constructor
*
* @param Backend\BackendInterface $caldavBackend
* @param mixed $userUri
*/
function __construct(Backend\BackendInterface $caldavBackend, $principalInfo) {
$this->caldavBackend = $caldavBackend;
$this->principalInfo = $principalInfo;
}
/**
* Returns the name of this object
*
* @return string
*/
function getName() {
list(, $name) = URLUtil::splitPath($this->principalInfo['uri']);
return $name;
}
/**
* Updates the name of this object
*
* @param string $name
* @return void
*/
function setName($name) {
throw new DAV\Exception\Forbidden();
}
/**
* Deletes this object
*
* @return void
*/
function delete() {
throw new DAV\Exception\Forbidden();
}
/**
* Returns the last modification date
*
* @return int
*/
function getLastModified() {
return null;
}
/**
* Creates a new file under this object.
*
* This is currently not allowed
*
* @param string $filename
* @param resource $data
* @return void
*/
function createFile($filename, $data = null) {
throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
}
/**
* Creates a new directory under this object.
*
* This is currently not allowed.
*
* @param string $filename
* @return void
*/
function createDirectory($filename) {
throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
}
/**
* Returns a single calendar, by name
*
* @param string $name
* @return Calendar
*/
function getChild($name) {
// Special nodes
if ($name === 'inbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) {
return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']);
}
if ($name === 'outbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) {
return new Schedule\Outbox($this->principalInfo['uri']);
}
if ($name === 'notifications' && $this->caldavBackend instanceof Backend\NotificationSupport) {
return new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
}
// Calendars
foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
if ($calendar['uri'] === $name) {
if ($this->caldavBackend instanceof Backend\SharingSupport) {
if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
return new SharedCalendar($this->caldavBackend, $calendar);
} else {
return new ShareableCalendar($this->caldavBackend, $calendar);
}
} else {
return new Calendar($this->caldavBackend, $calendar);
}
}
}
if ($this->caldavBackend instanceof Backend\SubscriptionSupport) {
foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
if ($subscription['uri'] === $name) {
return new Subscriptions\Subscription($this->caldavBackend, $subscription);
}
}
}
throw new NotFound('Node with name \'' . $name . '\' could not be found');
}
/**
* Checks if a calendar exists.
*
* @param string $name
* @return bool
*/
function childExists($name) {
try {
return !!$this->getChild($name);
} catch (NotFound $e) {
return false;
}
}
/**
* Returns a list of calendars
*
* @return array
*/
function getChildren() {
$calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
$objs = [];
foreach ($calendars as $calendar) {
if ($this->caldavBackend instanceof Backend\SharingSupport) {
if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
$objs[] = new SharedCalendar($this->caldavBackend, $calendar);
} else {
$objs[] = new ShareableCalendar($this->caldavBackend, $calendar);
}
} else {
$objs[] = new Calendar($this->caldavBackend, $calendar);
}
}
if ($this->caldavBackend instanceof Backend\SchedulingSupport) {
$objs[] = new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']);
$objs[] = new Schedule\Outbox($this->principalInfo['uri']);
}
// We're adding a notifications node, if it's supported by the backend.
if ($this->caldavBackend instanceof Backend\NotificationSupport) {
$objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
}
// If the backend supports subscriptions, we'll add those as well,
if ($this->caldavBackend instanceof Backend\SubscriptionSupport) {
foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
$objs[] = new Subscriptions\Subscription($this->caldavBackend, $subscription);
}
}
return $objs;
}
/**
* Creates a new calendar or subscription.
*
* @param string $name
* @param MkCol $mkCol
* @throws DAV\Exception\InvalidResourceType
* @return void
*/
function createExtendedCollection($name, MkCol $mkCol) {
$isCalendar = false;
$isSubscription = false;
foreach ($mkCol->getResourceType() as $rt) {
switch ($rt) {
case '{DAV:}collection' :
case '{http://calendarserver.org/ns/}shared-owner' :
// ignore
break;
case '{urn:ietf:params:xml:ns:caldav}calendar' :
$isCalendar = true;
break;
case '{http://calendarserver.org/ns/}subscribed' :
$isSubscription = true;
break;
default :
throw new DAV\Exception\InvalidResourceType('Unknown resourceType: ' . $rt);
}
}
$properties = $mkCol->getRemainingValues();
$mkCol->setRemainingResultCode(201);
if ($isSubscription) {
if (!$this->caldavBackend instanceof Backend\SubscriptionSupport) {
throw new DAV\Exception\InvalidResourceType('This backend does not support subscriptions');
}
$this->caldavBackend->createSubscription($this->principalInfo['uri'], $name, $properties);
} elseif ($isCalendar) {
$this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties);
} else {
throw new DAV\Exception\InvalidResourceType('You can only create calendars and subscriptions in this collection');
}
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->principalInfo['uri'];
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup() {
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->principalInfo['uri'],
'protected' => true,
],
[
'privilege' => '{DAV:}write',
'principal' => $this->principalInfo['uri'],
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}write',
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->principalInfo['uri'] . '/calendar-proxy-read',
'protected' => true,
],
];
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's.
*
* @param array $acl
* @return void
*/
function setACL(array $acl) {
throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
}
/**
* Returns the list of supported privileges for this node.
*
* The returned data structure is a list of nested privileges.
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
* standard structure.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet() {
return null;
}
/**
* This method is called when a user replied to a request to share.
*
* This method should return the url of the newly created calendar if the
* share was accepted.
*
* @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
*/
function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) {
if (!$this->caldavBackend instanceof Backend\SharingSupport) {
throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.');
}
return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary);
}
/**
* Searches through all of a users calendars and calendar objects to find
* an object with a specific UID.
*
* This method should return the path to this object, relative to the
* calendar home, so this path usually only contains two parts:
*
* calendarpath/objectpath.ics
*
* If the uid is not found, return null.
*
* This method should only consider * objects that the principal owns, so
* any calendars owned by other principals that also appear in this
* collection should be ignored.
*
* @param string $uid
* @return string|null
*/
function getCalendarObjectByUID($uid) {
return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid);
}
}

View file

@ -0,0 +1,290 @@
<?php
namespace Sabre\CalDAV;
/**
* The CalendarObject represents a single VEVENT or VTODO within a Calendar.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\DAVACL\IACL {
/**
* Sabre\CalDAV\Backend\BackendInterface
*
* @var Sabre\CalDAV\Backend\AbstractBackend
*/
protected $caldavBackend;
/**
* Array with information about this CalendarObject
*
* @var array
*/
protected $objectData;
/**
* Array with information about the containing calendar
*
* @var array
*/
protected $calendarInfo;
/**
* Constructor
*
* The following properties may be passed within $objectData:
*
* * calendarid - This must refer to a calendarid from a caldavBackend
* * uri - A unique uri. Only the 'basename' must be passed.
* * calendardata (optional) - The iCalendar data
* * etag - (optional) The etag for this object, MUST be encloded with
* double-quotes.
* * size - (optional) The size of the data in bytes.
* * lastmodified - (optional) format as a unix timestamp.
* * acl - (optional) Use this to override the default ACL for the node.
*
* @param Backend\BackendInterface $caldavBackend
* @param array $calendarInfo
* @param array $objectData
*/
function __construct(Backend\BackendInterface $caldavBackend, array $calendarInfo, array $objectData) {
$this->caldavBackend = $caldavBackend;
if (!isset($objectData['uri'])) {
throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
}
$this->calendarInfo = $calendarInfo;
$this->objectData = $objectData;
}
/**
* Returns the uri for this object
*
* @return string
*/
function getName() {
return $this->objectData['uri'];
}
/**
* Returns the ICalendar-formatted object
*
* @return string
*/
function get() {
// Pre-populating the 'calendardata' is optional, if we don't have it
// already we fetch it from the backend.
if (!isset($this->objectData['calendardata'])) {
$this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri']);
}
return $this->objectData['calendardata'];
}
/**
* Updates the ICalendar-formatted object
*
* @param string|resource $calendarData
* @return string
*/
function put($calendarData) {
if (is_resource($calendarData)) {
$calendarData = stream_get_contents($calendarData);
}
$etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], $calendarData);
$this->objectData['calendardata'] = $calendarData;
$this->objectData['etag'] = $etag;
return $etag;
}
/**
* Deletes the calendar object
*
* @return void
*/
function delete() {
$this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']);
}
/**
* Returns the mime content-type
*
* @return string
*/
function getContentType() {
$mime = 'text/calendar; charset=utf-8';
if (isset($this->objectData['component']) && $this->objectData['component']) {
$mime .= '; component=' . $this->objectData['component'];
}
return $mime;
}
/**
* Returns an ETag for this object.
*
* The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
*
* @return string
*/
function getETag() {
if (isset($this->objectData['etag'])) {
return $this->objectData['etag'];
} else {
return '"' . md5($this->get()) . '"';
}
}
/**
* Returns the last modification date as a unix timestamp
*
* @return int
*/
function getLastModified() {
return $this->objectData['lastmodified'];
}
/**
* Returns the size of this object in bytes
*
* @return int
*/
function getSize() {
if (array_key_exists('size', $this->objectData)) {
return $this->objectData['size'];
} else {
return strlen($this->get());
}
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->calendarInfo['principaluri'];
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup() {
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
// An alternative acl may be specified in the object data.
if (isset($this->objectData['acl'])) {
return $this->objectData['acl'];
}
// The default ACL
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
],
[
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read',
'protected' => true,
],
];
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's.
*
* @param array $acl
* @return void
*/
function setACL(array $acl) {
throw new \Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
}
/**
* Returns the list of supported privileges for this node.
*
* The returned data structure is a list of nested privileges.
* See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
* standard structure.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet() {
return null;
}
}

View file

@ -0,0 +1,375 @@
<?php
namespace Sabre\CalDAV;
use Sabre\VObject;
use DateTime;
/**
* CalendarQuery Validator
*
* This class is responsible for checking if an iCalendar object matches a set
* of filters. The main function to do this is 'validate'.
*
* This is used to determine which icalendar objects should be returned for a
* calendar-query REPORT request.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalendarQueryValidator {
/**
* Verify if a list of filters applies to the calendar data object
*
* The list of filters must be formatted as parsed by \Sabre\CalDAV\CalendarQueryParser
*
* @param VObject\Component $vObject
* @param array $filters
* @return bool
*/
function validate(VObject\Component\VCalendar $vObject, array $filters) {
// The top level object is always a component filter.
// We'll parse it manually, as it's pretty simple.
if ($vObject->name !== $filters['name']) {
return false;
}
return
$this->validateCompFilters($vObject, $filters['comp-filters']) &&
$this->validatePropFilters($vObject, $filters['prop-filters']);
}
/**
* This method checks the validity of comp-filters.
*
* A list of comp-filters needs to be specified. Also the parent of the
* component we're checking should be specified, not the component to check
* itself.
*
* @param VObject\Component $parent
* @param array $filters
* @return bool
*/
protected function validateCompFilters(VObject\Component $parent, array $filters) {
foreach ($filters as $filter) {
$isDefined = isset($parent->{$filter['name']});
if ($filter['is-not-defined']) {
if ($isDefined) {
return false;
} else {
continue;
}
}
if (!$isDefined) {
return false;
}
if ($filter['time-range']) {
foreach ($parent->{$filter['name']} as $subComponent) {
if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
continue 2;
}
}
return false;
}
if (!$filter['comp-filters'] && !$filter['prop-filters']) {
continue;
}
// If there are sub-filters, we need to find at least one component
// for which the subfilters hold true.
foreach ($parent->{$filter['name']} as $subComponent) {
if (
$this->validateCompFilters($subComponent, $filter['comp-filters']) &&
$this->validatePropFilters($subComponent, $filter['prop-filters'])) {
// We had a match, so this comp-filter succeeds
continue 2;
}
}
// If we got here it means there were sub-comp-filters or
// sub-prop-filters and there was no match. This means this filter
// needs to return false.
return false;
}
// If we got here it means we got through all comp-filters alive so the
// filters were all true.
return true;
}
/**
* This method checks the validity of prop-filters.
*
* A list of prop-filters needs to be specified. Also the parent of the
* property we're checking should be specified, not the property to check
* itself.
*
* @param VObject\Component $parent
* @param array $filters
* @return bool
*/
protected function validatePropFilters(VObject\Component $parent, array $filters) {
foreach ($filters as $filter) {
$isDefined = isset($parent->{$filter['name']});
if ($filter['is-not-defined']) {
if ($isDefined) {
return false;
} else {
continue;
}
}
if (!$isDefined) {
return false;
}
if ($filter['time-range']) {
foreach ($parent->{$filter['name']} as $subComponent) {
if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
continue 2;
}
}
return false;
}
if (!$filter['param-filters'] && !$filter['text-match']) {
continue;
}
// If there are sub-filters, we need to find at least one property
// for which the subfilters hold true.
foreach ($parent->{$filter['name']} as $subComponent) {
if (
$this->validateParamFilters($subComponent, $filter['param-filters']) &&
(!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
) {
// We had a match, so this prop-filter succeeds
continue 2;
}
}
// If we got here it means there were sub-param-filters or
// text-match filters and there was no match. This means the
// filter needs to return false.
return false;
}
// If we got here it means we got through all prop-filters alive so the
// filters were all true.
return true;
}
/**
* This method checks the validity of param-filters.
*
* A list of param-filters needs to be specified. Also the parent of the
* parameter we're checking should be specified, not the parameter to check
* itself.
*
* @param VObject\Property $parent
* @param array $filters
* @return bool
*/
protected function validateParamFilters(VObject\Property $parent, array $filters) {
foreach ($filters as $filter) {
$isDefined = isset($parent[$filter['name']]);
if ($filter['is-not-defined']) {
if ($isDefined) {
return false;
} else {
continue;
}
}
if (!$isDefined) {
return false;
}
if (!$filter['text-match']) {
continue;
}
// If there are sub-filters, we need to find at least one parameter
// for which the subfilters hold true.
foreach ($parent[$filter['name']]->getParts() as $paramPart) {
if ($this->validateTextMatch($paramPart, $filter['text-match'])) {
// We had a match, so this param-filter succeeds
continue 2;
}
}
// If we got here it means there was a text-match filter and there
// were no matches. This means the filter needs to return false.
return false;
}
// If we got here it means we got through all param-filters alive so the
// filters were all true.
return true;
}
/**
* This method checks the validity of a text-match.
*
* A single text-match should be specified as well as the specific property
* or parameter we need to validate.
*
* @param VObject\Node|string $check Value to check against.
* @param array $textMatch
* @return bool
*/
protected function validateTextMatch($check, array $textMatch) {
if ($check instanceof VObject\Node) {
$check = $check->getValue();
}
$isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']);
return ($textMatch['negate-condition'] xor $isMatching);
}
/**
* Validates if a component matches the given time range.
*
* This is all based on the rules specified in rfc4791, which are quite
* complex.
*
* @param VObject\Node $component
* @param DateTime $start
* @param DateTime $end
* @return bool
*/
protected function validateTimeRange(VObject\Node $component, $start, $end) {
if (is_null($start)) {
$start = new DateTime('1900-01-01');
}
if (is_null($end)) {
$end = new DateTime('3000-01-01');
}
switch ($component->name) {
case 'VEVENT' :
case 'VTODO' :
case 'VJOURNAL' :
return $component->isInTimeRange($start, $end);
case 'VALARM' :
// If the valarm is wrapped in a recurring event, we need to
// expand the recursions, and validate each.
//
// Our datamodel doesn't easily allow us to do this straight
// in the VALARM component code, so this is a hack, and an
// expensive one too.
if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) {
// Fire up the iterator!
$it = new VObject\Recur\EventIterator($component->parent->parent, (string)$component->parent->UID);
while ($it->valid()) {
$expandedEvent = $it->getEventObject();
// We need to check from these expanded alarms, which
// one is the first to trigger. Based on this, we can
// determine if we can 'give up' expanding events.
$firstAlarm = null;
if ($expandedEvent->VALARM !== null) {
foreach ($expandedEvent->VALARM as $expandedAlarm) {
$effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
if ($expandedAlarm->isInTimeRange($start, $end)) {
return true;
}
if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') {
// This is an alarm with a non-relative trigger
// time, likely created by a buggy client. The
// implication is that every alarm in this
// recurring event trigger at the exact same
// time. It doesn't make sense to traverse
// further.
} else {
// We store the first alarm as a means to
// figure out when we can stop traversing.
if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
$firstAlarm = $effectiveTrigger;
}
}
}
}
if (is_null($firstAlarm)) {
// No alarm was found.
//
// Or technically: No alarm that will change for
// every instance of the recurrence was found,
// which means we can assume there was no match.
return false;
}
if ($firstAlarm > $end) {
return false;
}
$it->next();
}
return false;
} else {
return $component->isInTimeRange($start, $end);
}
case 'VFREEBUSY' :
throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components');
case 'COMPLETED' :
case 'CREATED' :
case 'DTEND' :
case 'DTSTAMP' :
case 'DTSTART' :
case 'DUE' :
case 'LAST-MODIFIED' :
return ($start <= $component->getDateTime() && $end >= $component->getDateTime());
default :
throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component');
}
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Sabre\CalDAV;
use Sabre\DAVACL\PrincipalBackend;
/**
* Calendars collection
*
* This object is responsible for generating a list of calendar-homes for each
* user.
*
* This is the top-most node for the calendars tree. In most servers this class
* represents the "/calendars" path.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalendarRoot extends \Sabre\DAVACL\AbstractPrincipalCollection {
/**
* CalDAV backend
*
* @var Sabre\CalDAV\Backend\BackendInterface
*/
protected $caldavBackend;
/**
* Constructor
*
* This constructor needs both an authentication and a caldav backend.
*
* By default this class will show a list of calendar collections for
* principals in the 'principals' collection. If your main principals are
* actually located in a different path, use the $principalPrefix argument
* to override this.
*
* @param PrincipalBackend\BackendInterface $principalBackend
* @param Backend\BackendInterface $caldavBackend
* @param string $principalPrefix
*/
function __construct(PrincipalBackend\BackendInterface $principalBackend, Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals') {
parent::__construct($principalBackend, $principalPrefix);
$this->caldavBackend = $caldavBackend;
}
/**
* Returns the nodename
*
* We're overriding this, because the default will be the 'principalPrefix',
* and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT
*
* @return string
*/
function getName() {
return Plugin::CALENDAR_ROOT;
}
/**
* This method returns a node for a principal.
*
* The passed array contains principal information, and is guaranteed to
* at least contain a uri item. Other properties may or may not be
* supplied by the authentication backend.
*
* @param array $principal
* @return \Sabre\DAV\INode
*/
function getChildForPrincipal(array $principal) {
return new CalendarHome($this->caldavBackend, $principal);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Sabre\CalDAV\Exception;
use Sabre\DAV;
use Sabre\CalDAV;
/**
* InvalidComponentType
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class InvalidComponentType extends DAV\Exception\Forbidden {
/**
* Adds in extra information in the xml response.
*
* This method adds the {CALDAV:}supported-calendar-component as defined in rfc4791
*
* @param DAV\Server $server
* @param \DOMElement $errorNode
* @return void
*/
function serialize(DAV\Server $server, \DOMElement $errorNode) {
$doc = $errorNode->ownerDocument;
$np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component');
$errorNode->appendChild($np);
}
}

View file

@ -0,0 +1,361 @@
<?php
namespace Sabre\CalDAV;
use DateTimeZone;
use Sabre\DAV;
use Sabre\VObject;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\DAV\Exception\BadRequest;
use DateTime;
/**
* ICS Exporter
*
* This plugin adds the ability to export entire calendars as .ics files.
* This is useful for clients that don't support CalDAV yet. They often do
* support ics files.
*
* To use this, point a http client to a caldav calendar, and add ?expand to
* the url.
*
* Further options that can be added to the url:
* start=123456789 - Only return events after the given unix timestamp
* end=123245679 - Only return events from before the given unix timestamp
* expand=1 - Strip timezone information and expand recurring events.
* If you'd like to expand, you _must_ also specify start
* and end.
*
* By default this plugin returns data in the text/calendar format (iCalendar
* 2.0). If you'd like to receive jCal data instead, you can use an Accept
* header:
*
* Accept: application/calendar+json
*
* Alternatively, you can also specify this in the url using
* accept=application/calendar+json, or accept=jcal for short. If the url
* parameter and Accept header is specified, the url parameter wins.
*
* Note that specifying a start or end data implies that only events will be
* returned. VTODO and VJOURNAL will be stripped.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ICSExportPlugin extends DAV\ServerPlugin {
/**
* Reference to Server class
*
* @var \Sabre\DAV\Server
*/
protected $server;
/**
* Initializes the plugin and registers event handlers
*
* @param \Sabre\DAV\Server $server
* @return void
*/
function initialize(DAV\Server $server) {
$this->server = $server;
$server->on('method:GET', [$this, 'httpGet'], 90);
$server->on('browserButtonActions', function($path, $node, &$actions) {
if ($node instanceof ICalendar) {
$actions .= '<a href="' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '?export"><span class="oi" data-glyph="calendar"></span></a>';
}
});
}
/**
* Intercepts GET requests on calendar urls ending with ?export.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
function httpGet(RequestInterface $request, ResponseInterface $response) {
$queryParams = $request->getQueryParameters();
if (!array_key_exists('export', $queryParams)) return;
$path = $request->getPath();
$node = $this->server->getProperties($path, [
'{DAV:}resourcetype',
'{DAV:}displayname',
'{http://sabredav.org/ns}sync-token',
'{DAV:}sync-token',
'{http://apple.com/ns/ical/}calendar-color',
]);
if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) {
return;
}
// Marking the transactionType, for logging purposes.
$this->server->transactionType = 'get-calendar-export';
$properties = $node;
$start = null;
$end = null;
$expand = false;
$componentType = false;
if (isset($queryParams['start'])) {
if (!ctype_digit($queryParams['start'])) {
throw new BadRequest('The start= parameter must contain a unix timestamp');
}
$start = DateTime::createFromFormat('U', $queryParams['start']);
}
if (isset($queryParams['end'])) {
if (!ctype_digit($queryParams['end'])) {
throw new BadRequest('The end= parameter must contain a unix timestamp');
}
$end = DateTime::createFromFormat('U', $queryParams['end']);
}
if (isset($queryParams['expand']) && !!$queryParams['expand']) {
if (!$start || !$end) {
throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.');
}
$expand = true;
$componentType = 'VEVENT';
}
if (isset($queryParams['componentType'])) {
if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) {
throw new BadRequest('You are not allowed to search for components of type: ' . $queryParams['componentType'] . ' here');
}
$componentType = $queryParams['componentType'];
}
$format = \Sabre\HTTP\Util::Negotiate(
$request->getHeader('Accept'),
[
'text/calendar',
'application/calendar+json',
]
);
if (isset($queryParams['accept'])) {
if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') {
$format = 'application/calendar+json';
}
}
if (!$format) {
$format = 'text/calendar';
}
$this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response);
// Returning false to break the event chain
return false;
}
/**
* This method is responsible for generating the actual, full response.
*
* @param string $path
* @param DateTime|null $start
* @param DateTime|null $end
* @param bool $expand
* @param string $componentType
* @param string $format
* @param array $properties
* @param ResponseInterface $response
*/
protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) {
$calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
$blobs = [];
if ($start || $end || $componentType) {
// If there was a start or end filter, we need to enlist
// calendarQuery for speed.
$calendarNode = $this->server->tree->getNodeForPath($path);
$queryResult = $calendarNode->calendarQuery([
'name' => 'VCALENDAR',
'comp-filters' => [
[
'name' => $componentType,
'comp-filters' => [],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => [
'start' => $start,
'end' => $end,
],
],
],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => null,
]);
// queryResult is just a list of base urls. We need to prefix the
// calendar path.
$queryResult = array_map(
function($item) use ($path) {
return $path . '/' . $item;
},
$queryResult
);
$nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]);
unset($queryResult);
} else {
$nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1);
}
// Flattening the arrays
foreach ($nodes as $node) {
if (isset($node[200][$calDataProp])) {
$blobs[$node['href']] = $node[200][$calDataProp];
}
}
unset($nodes);
$mergedCalendar = $this->mergeObjects(
$properties,
$blobs
);
if ($expand) {
$calendarTimeZone = null;
// We're expanding, and for that we need to figure out the
// calendar's timezone.
$tzProp = '{' . Plugin::NS_CALDAV . '}calendar-timezone';
$tzResult = $this->server->getProperties($path, [$tzProp]);
if (isset($tzResult[$tzProp])) {
// This property contains a VCALENDAR with a single
// VTIMEZONE.
$vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
unset($vtimezoneObj);
} else {
// Defaulting to UTC.
$calendarTimeZone = new DateTimeZone('UTC');
}
$mergedCalendar->expand($start, $end, $calendarTimeZone);
}
$response->setHeader('Content-Type', $format);
switch ($format) {
case 'text/calendar' :
$mergedCalendar = $mergedCalendar->serialize();
break;
case 'application/calendar+json' :
$mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
break;
}
$response->setStatus(200);
$response->setBody($mergedCalendar);
}
/**
* Merges all calendar objects, and builds one big iCalendar blob.
*
* @param array $properties Some CalDAV properties
* @param array $inputObjects
* @return VObject\Component\VCalendar
*/
function mergeObjects(array $properties, array $inputObjects) {
$calendar = new VObject\Component\VCalendar();
$calendar->version = '2.0';
if (DAV\Server::$exposeVersion) {
$calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
} else {
$calendar->prodid = '-//SabreDAV//SabreDAV//EN';
}
if (isset($properties['{DAV:}displayname'])) {
$calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname'];
}
if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) {
$calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color'];
}
$collectedTimezones = [];
$timezones = [];
$objects = [];
foreach ($inputObjects as $href => $inputObject) {
$nodeComp = VObject\Reader::read($inputObject);
foreach ($nodeComp->children() as $child) {
switch ($child->name) {
case 'VEVENT' :
case 'VTODO' :
case 'VJOURNAL' :
$objects[] = $child;
break;
// VTIMEZONE is special, because we need to filter out the duplicates
case 'VTIMEZONE' :
// Naively just checking tzid.
if (in_array((string)$child->TZID, $collectedTimezones)) continue;
$timezones[] = $child;
$collectedTimezones[] = $child->TZID;
break;
}
}
}
foreach ($timezones as $tz) $calendar->add($tz);
foreach ($objects as $obj) $calendar->add($obj);
return $calendar;
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using \Sabre\DAV\Server::getPlugin
*
* @return string
*/
function getPluginName() {
return 'ics-export';
}
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function getPluginInfo() {
return [
'name' => $this->getPluginName(),
'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.',
'link' => 'http://sabre.io/dav/ics-export-plugin/',
];
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Sabre\CalDAV;
use Sabre\DAVACL;
/**
* Calendar interface
*
* Implement this interface to allow a node to be recognized as an calendar.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface ICalendar extends ICalendarObjectContainer, DAVACL\IACL {
}

View file

@ -0,0 +1,21 @@
<?php
namespace Sabre\CalDAV;
use Sabre\DAV;
/**
* CalendarObject interface
*
* Extend the ICalendarObject interface to allow your custom nodes to be picked up as
* CalendarObjects.
*
* Calendar objects are resources such as Events, Todo's or Journals.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface ICalendarObject extends DAV\IFile {
}

View file

@ -0,0 +1,39 @@
<?php
namespace Sabre\CalDAV;
/**
* This interface represents a node that may contain calendar objects.
*
* This is the shared parent for both the Inbox collection and calendars
* resources.
*
* In most cases you will likely want to look at ICalendar instead of this
* interface.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface ICalendarObjectContainer extends \Sabre\DAV\ICollection {
/**
* 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.
*
* @param array $filters
* @return array
*/
function calendarQuery(array $filters);
}

View file

@ -0,0 +1,173 @@
<?php
namespace Sabre\CalDAV\Notifications;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\DAVACL;
/**
* This node represents a list of notifications.
*
* It provides no additional functionality, but you must implement this
* interface to allow the Notifications plugin to mark the collection
* as a notifications collection.
*
* This collection should only return Sabre\CalDAV\Notifications\INode nodes as
* its children.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Collection extends DAV\Collection implements ICollection, DAVACL\IACL {
/**
* The notification backend
*
* @var Sabre\CalDAV\Backend\NotificationSupport
*/
protected $caldavBackend;
/**
* Principal uri
*
* @var string
*/
protected $principalUri;
/**
* Constructor
*
* @param CalDAV\Backend\NotificationSupport $caldavBackend
* @param string $principalUri
*/
function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri) {
$this->caldavBackend = $caldavBackend;
$this->principalUri = $principalUri;
}
/**
* Returns all notifications for a principal
*
* @return array
*/
function getChildren() {
$children = [];
$notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri);
foreach ($notifications as $notification) {
$children[] = new Node(
$this->caldavBackend,
$this->principalUri,
$notification
);
}
return $children;
}
/**
* Returns the name of this object
*
* @return string
*/
function getName() {
return 'notifications';
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->principalUri;
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup() {
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
return [
[
'principal' => $this->getOwner(),
'privilege' => '{DAV:}read',
'protected' => true,
],
[
'principal' => $this->getOwner(),
'privilege' => '{DAV:}write',
'protected' => true,
]
];
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's as an array argument.
*
* @param array $acl
* @return void
*/
function setACL(array $acl) {
throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
}
/**
* Returns the list of supported privileges for this node.
*
* The returned data structure is a list of nested privileges.
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
* standard structure.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet() {
return null;
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace Sabre\CalDAV\Notifications;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\CalDAV\Xml\Notification\NotificationInterface;
use Sabre\DAVACL;
/**
* This node represents a single notification.
*
* The signature is mostly identical to that of Sabre\DAV\IFile, but the get() method
* MUST return an xml document that matches the requirements of the
* 'caldav-notifications.txt' spec.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Node extends DAV\File implements INode, DAVACL\IACL {
/**
* The notification backend
*
* @var Sabre\CalDAV\Backend\NotificationSupport
*/
protected $caldavBackend;
/**
* The actual notification
*
* @var Sabre\CalDAV\Notifications\INotificationType
*/
protected $notification;
/**
* Owner principal of the notification
*
* @var string
*/
protected $principalUri;
/**
* Constructor
*
* @param CalDAV\Backend\NotificationSupport $caldavBackend
* @param string $principalUri
* @param NotificationInterface $notification
*/
function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri, NotificationInterface $notification) {
$this->caldavBackend = $caldavBackend;
$this->principalUri = $principalUri;
$this->notification = $notification;
}
/**
* Returns the path name for this notification
*
* @return id
*/
function getName() {
return $this->notification->getId() . '.xml';
}
/**
* Returns the etag for the notification.
*
* The etag must be surrounded by litteral double-quotes.
*
* @return string
*/
function getETag() {
return $this->notification->getETag();
}
/**
* This method must return an xml element, using the
* Sabre\CalDAV\Notifications\INotificationType classes.
*
* @return INotificationType
*/
function getNotificationType() {
return $this->notification;
}
/**
* Deletes this notification
*
* @return void
*/
function delete() {
$this->caldavBackend->deleteNotification($this->getOwner(), $this->notification);
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->principalUri;
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup() {
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
return [
[
'principal' => $this->getOwner(),
'privilege' => '{DAV:}read',
'protected' => true,
],
[
'principal' => $this->getOwner(),
'privilege' => '{DAV:}write',
'protected' => true,
]
];
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's as an array argument.
*
* @param array $acl
* @return void
*/
function setACL(array $acl) {
throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
}
/**
* Returns the list of supported privileges for this node.
*
* The returned data structure is a list of nested privileges.
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
* standard structure.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet() {
return null;
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace Sabre\CalDAV\Notifications;
use Sabre\DAV;
use Sabre\DAV\PropFind;
use Sabre\DAV\INode as BaseINode;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Server;
use Sabre\DAVACL;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Notifications plugin
*
* This plugin implements several features required by the caldav-notification
* draft specification.
*
* Before version 2.1.0 this functionality was part of Sabre\CalDAV\Plugin but
* this has since been split up.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Plugin extends ServerPlugin {
/**
* This is the namespace for the proprietary calendarserver extensions
*/
const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
/**
* Reference to the main server object.
*
* @var Server
*/
protected $server;
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using \Sabre\DAV\Server::getPlugin
*
* @return string
*/
function getPluginName() {
return 'notifications';
}
/**
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param Server $server
* @return void
*/
function initialize(Server $server) {
$this->server = $server;
$server->on('method:GET', [$this, 'httpGet'], 90);
$server->on('propFind', [$this, 'propFind']);
$server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
$server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification';
array_push($server->protectedProperties,
'{' . self::NS_CALENDARSERVER . '}notification-URL',
'{' . self::NS_CALENDARSERVER . '}notificationtype'
);
}
/**
* 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']
);
}
}
/**
* This event is triggered before the usual GET request handler.
*
* We use this to intercept GET calls to notification nodes, and return the
* proper response.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return void
*/
function httpGet(RequestInterface $request, ResponseInterface $response) {
$path = $request->getPath();
try {
$node = $this->server->tree->getNodeForPath($path);
} catch (DAV\Exception\NotFound $e) {
return;
}
if (!$node instanceof INode)
return;
$writer = $this->server->xml->getWriter();
$writer->contextUri = $this->server->getBaseUri();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('{http://calendarserver.org/ns/}notification');
$node->getNotificationType()->xmlSerializeFull($writer);
$writer->endElement();
$response->setHeader('Content-Type', 'application/xml');
$response->setHeader('ETag', $node->getETag());
$response->setStatus(200);
$response->setBody($writer->outputMemory());
// Return false to break the event chain.
return false;
}
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function getPluginInfo() {
return [
'name' => $this->getPluginName(),
'description' => 'Adds support for caldav-notifications, which is required to enable caldav-sharing.',
'link' => 'http://sabre.io/dav/caldav-sharing/',
];
}
}

View file

@ -0,0 +1,992 @@
<?php
namespace Sabre\CalDAV;
use DateTimeZone;
use Sabre\DAV;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\MkCol;
use Sabre\DAV\Xml\Property\Href;
use Sabre\DAVACL;
use Sabre\VObject;
use Sabre\HTTP;
use Sabre\Uri;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* CalDAV plugin
*
* This plugin provides functionality added by CalDAV (RFC 4791)
* It implements new reports, and the MKCALENDAR method.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Plugin extends DAV\ServerPlugin {
/**
* This is the official CalDAV namespace
*/
const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
/**
* This is the namespace for the proprietary calendarserver extensions
*/
const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
/**
* The hardcoded root for calendar objects. It is unfortunate
* that we're stuck with it, but it will have to do for now
*/
const CALENDAR_ROOT = 'calendars';
/**
* Reference to server object
*
* @var DAV\Server
*/
protected $server;
/**
* The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data,
* which can hold up to 2^24 = 16777216 bytes. This is plenty. We're
* capping it to 10M here.
*/
protected $maxResourceSize = 10000000;
/**
* Use this method to tell the server this plugin defines additional
* HTTP methods.
*
* This method is passed a uri. It should only return HTTP methods that are
* available for the specified uri.
*
* @param string $uri
* @return array
*/
function getHTTPMethods($uri) {
// The MKCALENDAR is only available on unmapped uri's, whose
// parents extend IExtendedCollection
list($parent, $name) = Uri\split($uri);
$node = $this->server->tree->getNodeForPath($parent);
if ($node instanceof DAV\IExtendedCollection) {
try {
$node->getChild($name);
} catch (DAV\Exception\NotFound $e) {
return ['MKCALENDAR'];
}
}
return [];
}
/**
* Returns the path to a principal's calendar home.
*
* The return url must not end with a slash.
*
* @param string $principalUrl
* @return string
*/
function getCalendarHomeForPrincipal($principalUrl) {
// The default is a bit naive, but it can be overwritten.
list(, $nodeName) = Uri\split($principalUrl);
return self::CALENDAR_ROOT . '/' . $nodeName;
}
/**
* Returns a list of features for the DAV: HTTP header.
*
* @return array
*/
function getFeatures() {
return ['calendar-access', 'calendar-proxy'];
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using DAV\Server::getPlugin
*
* @return string
*/
function getPluginName() {
return 'caldav';
}
/**
* Returns a list of reports this plugin supports.
*
* This will be used in the {DAV:}supported-report-set property.
* Note that you still need to subscribe to the 'report' event to actually
* implement them
*
* @param string $uri
* @return array
*/
function getSupportedReportSet($uri) {
$node = $this->server->tree->getNodeForPath($uri);
$reports = [];
if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) {
$reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
$reports[] = '{' . self::NS_CALDAV . '}calendar-query';
}
if ($node instanceof ICalendar) {
$reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
}
// iCal has a bug where it assumes that sync support is enabled, only
// if we say we support it on the calendar-home, even though this is
// not actually the case.
if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) {
$reports[] = '{DAV:}sync-collection';
}
return $reports;
}
/**
* Initializes the plugin
*
* @param DAV\Server $server
* @return void
*/
function initialize(DAV\Server $server) {
$this->server = $server;
$server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']);
$server->on('report', [$this, 'report']);
$server->on('propFind', [$this, 'propFind']);
$server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
$server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
$server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
$server->on('afterMethod:GET', [$this, 'httpAfterGET']);
$server->xml->namespaceMap[self::NS_CALDAV] = 'cal';
$server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
$server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport';
$server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport';
$server->xml->elementMap['{' . self::NS_CALDAV . '}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport';
$server->xml->elementMap['{' . self::NS_CALDAV . '}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar';
$server->xml->elementMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp';
$server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
$server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
$server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
$server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
array_push($server->protectedProperties,
'{' . self::NS_CALDAV . '}supported-calendar-component-set',
'{' . self::NS_CALDAV . '}supported-calendar-data',
'{' . self::NS_CALDAV . '}max-resource-size',
'{' . self::NS_CALDAV . '}min-date-time',
'{' . self::NS_CALDAV . '}max-date-time',
'{' . self::NS_CALDAV . '}max-instances',
'{' . self::NS_CALDAV . '}max-attendees-per-instance',
'{' . self::NS_CALDAV . '}calendar-home-set',
'{' . self::NS_CALDAV . '}supported-collation-set',
'{' . self::NS_CALDAV . '}calendar-data',
// CalendarServer extensions
'{' . self::NS_CALENDARSERVER . '}getctag',
'{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
'{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'
);
if ($aclPlugin = $server->getPlugin('acl')) {
$aclPlugin->principalSearchPropertySet['{' . self::NS_CALDAV . '}calendar-user-address-set'] = 'Calendar address';
}
}
/**
* This functions handles REPORT requests specific to CalDAV
*
* @param string $reportName
* @param mixed $report
* @return bool
*/
function report($reportName, $report) {
switch ($reportName) {
case '{' . self::NS_CALDAV . '}calendar-multiget' :
$this->server->transactionType = 'report-calendar-multiget';
$this->calendarMultiGetReport($report);
return false;
case '{' . self::NS_CALDAV . '}calendar-query' :
$this->server->transactionType = 'report-calendar-query';
$this->calendarQueryReport($report);
return false;
case '{' . self::NS_CALDAV . '}free-busy-query' :
$this->server->transactionType = 'report-free-busy-query';
$this->freeBusyQueryReport($report);
return false;
}
}
/**
* This function handles the MKCALENDAR HTTP method, which creates
* a new calendar.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
function httpMkCalendar(RequestInterface $request, ResponseInterface $response) {
$body = $request->getBodyAsString();
$path = $request->getPath();
$properties = [];
if ($body) {
try {
$mkcalendar = $this->server->xml->expect(
'{urn:ietf:params:xml:ns:caldav}mkcalendar',
$body
);
} catch (\Sabre\Xml\ParseException $e) {
throw new BadRequest($e->getMessage(), null, $e);
}
$properties = $mkcalendar->getProperties();
}
// iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored
// subscriptions. Before that it used MKCOL which was the correct way
// to do this.
//
// If the body had a {DAV:}resourcetype, it means we stumbled upon this
// request, and we simply use it instead of the pre-defined list.
if (isset($properties['{DAV:}resourcetype'])) {
$resourceType = $properties['{DAV:}resourcetype']->getValue();
} else {
$resourceType = ['{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar'];
}
$this->server->createCollection($path, new MkCol($resourceType, $properties));
$this->server->httpResponse->setStatus(201);
$this->server->httpResponse->setHeader('Content-Length', 0);
// This breaks the method chain.
return false;
}
/**
* 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);
});
}
}
/**
* This function handles the calendar-multiget REPORT.
*
* This report is used by the client to fetch the content of a series
* of urls. Effectively avoiding a lot of redundant requests.
*
* @param CalendarMultiGetReport $report
* @return void
*/
function calendarMultiGetReport($report) {
$needsJson = $report->contentType === 'application/calendar+json';
$timeZones = [];
$propertyList = [];
$paths = array_map(
[$this->server, 'calculateUri'],
$report->hrefs
);
foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) {
if (($needsJson || $report->expand) && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
$vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
if ($report->expand) {
// We're expanding, and for that we need to figure out the
// calendar's timezone.
list($calendarPath) = Uri\split($uri);
if (!isset($timeZones[$calendarPath])) {
// Checking the calendar-timezone property.
$tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
$tzResult = $this->server->getProperties($calendarPath, [$tzProp]);
if (isset($tzResult[$tzProp])) {
// This property contains a VCALENDAR with a single
// VTIMEZONE.
$vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
$timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
} else {
// Defaulting to UTC.
$timeZone = new DateTimeZone('UTC');
}
$timeZones[$calendarPath] = $timeZone;
}
$vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]);
}
if ($needsJson) {
$objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
} else {
$objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
}
}
$propertyList[] = $objProps;
}
$prefer = $this->server->getHTTPPrefer();
$this->server->httpResponse->setStatus(207);
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
$this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
$this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return'] === 'minimal'));
}
/**
* This function handles the calendar-query REPORT
*
* This report is used by clients to request calendar objects based on
* complex conditions.
*
* @param Xml\Request\CalendarQueryReport $report
* @return void
*/
function calendarQueryReport($report) {
$path = $this->server->getRequestUri();
$needsJson = $report->contentType === 'application/calendar+json';
$node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
$depth = $this->server->getHTTPDepth(0);
// The default result is an empty array
$result = [];
$calendarTimeZone = null;
if ($report->expand) {
// We're expanding, and for that we need to figure out the
// calendar's timezone.
$tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
$tzResult = $this->server->getProperties($path, [$tzProp]);
if (isset($tzResult[$tzProp])) {
// This property contains a VCALENDAR with a single
// VTIMEZONE.
$vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
unset($vtimezoneObj);
} else {
// Defaulting to UTC.
$calendarTimeZone = new DateTimeZone('UTC');
}
}
// The calendarobject was requested directly. In this case we handle
// this locally.
if ($depth == 0 && $node instanceof ICalendarObject) {
$requestedCalendarData = true;
$requestedProperties = $report->properties;
if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
// We always retrieve calendar-data, as we need it for filtering.
$requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
// If calendar-data wasn't explicitly requested, we need to remove
// it after processing.
$requestedCalendarData = false;
}
$properties = $this->server->getPropertiesForPath(
$path,
$requestedProperties,
0
);
// This array should have only 1 element, the first calendar
// object.
$properties = current($properties);
// If there wasn't any calendar-data returned somehow, we ignore
// this.
if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
$validator = new CalendarQueryValidator();
$vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
if ($validator->validate($vObject, $report->filters)) {
// If the client didn't require the calendar-data property,
// we won't give it back.
if (!$requestedCalendarData) {
unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
} else {
if ($report->expand) {
$vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
}
if ($needsJson) {
$properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
} elseif ($report->expand) {
$properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
}
}
$result = [$properties];
}
}
}
if ($node instanceof ICalendarObjectContainer && $depth === 0) {
if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'MSFT-') === 0) {
// Microsoft clients incorrectly supplied depth as 0, when it actually
// should have set depth to 1. We're implementing a workaround here
// to deal with this.
//
// This targets at least the following clients:
// Windows 10
// Windows Phone 8, 10
$depth = 1;
} else {
throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1');
}
}
// If we're dealing with a calendar, the calendar itself is responsible
// for the calendar-query.
if ($node instanceof ICalendarObjectContainer && $depth == 1) {
$nodePaths = $node->calendarQuery($report->filters);
foreach ($nodePaths as $path) {
list($properties) =
$this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $report->properties);
if (($needsJson || $report->expand)) {
$vObject = VObject\Reader::read($properties[200]['{' . self::NS_CALDAV . '}calendar-data']);
if ($report->expand) {
$vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
}
if ($needsJson) {
$properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
} else {
$properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
}
}
$result[] = $properties;
}
}
$prefer = $this->server->getHTTPPrefer();
$this->server->httpResponse->setStatus(207);
$this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
$this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
$this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal'));
}
/**
* This method is responsible for parsing the request and generating the
* response for the CALDAV:free-busy-query REPORT.
*
* @param Xml\Request\FreeBusyQueryReport $report
* @return void
*/
protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) {
$uri = $this->server->getRequestUri();
$acl = $this->server->getPlugin('acl');
if ($acl) {
$acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy');
}
$calendar = $this->server->tree->getNodeForPath($uri);
if (!$calendar instanceof ICalendar) {
throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
}
$tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
// Figuring out the default timezone for the calendar, for floating
// times.
$calendarProps = $this->server->getProperties($uri, [$tzProp]);
if (isset($calendarProps[$tzProp])) {
$vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]);
$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
} else {
$calendarTimeZone = new DateTimeZone('UTC');
}
// Doing a calendar-query first, to make sure we get the most
// performance.
$urls = $calendar->calendarQuery([
'name' => 'VCALENDAR',
'comp-filters' => [
[
'name' => 'VEVENT',
'comp-filters' => [],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => [
'start' => $report->start,
'end' => $report->end,
],
],
],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => null,
]);
$objects = array_map(function($url) use ($calendar) {
$obj = $calendar->getChild($url)->get();
return $obj;
}, $urls);
$generator = new VObject\FreeBusyGenerator();
$generator->setObjects($objects);
$generator->setTimeRange($report->start, $report->end);
$generator->setTimeZone($calendarTimeZone);
$result = $generator->getResult();
$result = $result->serialize();
$this->server->httpResponse->setStatus(200);
$this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
$this->server->httpResponse->setHeader('Content-Length', strlen($result));
$this->server->httpResponse->setBody($result);
}
/**
* This method is triggered before a file gets updated with new content.
*
* This plugin uses this method to ensure that CalDAV objects receive
* valid calendar data.
*
* @param string $path
* @param DAV\IFile $node
* @param resource $data
* @param bool $modified Should be set to true, if this event handler
* changed &$data.
* @return void
*/
function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) {
if (!$node instanceof ICalendarObject)
return;
// We're onyl interested in ICalendarObject nodes that are inside of a
// real calendar. This is to avoid triggering validation and scheduling
// for non-calendars (such as an inbox).
list($parent) = Uri\split($path);
$parentNode = $this->server->tree->getNodeForPath($parent);
if (!$parentNode instanceof ICalendar)
return;
$this->validateICalendar(
$data,
$path,
$modified,
$this->server->httpRequest,
$this->server->httpResponse,
false
);
}
/**
* This method is triggered before a new file is created.
*
* This plugin uses this method to ensure that newly created calendar
* objects contain valid calendar data.
*
* @param string $path
* @param resource $data
* @param DAV\ICollection $parentNode
* @param bool $modified Should be set to true, if this event handler
* changed &$data.
* @return void
*/
function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) {
if (!$parentNode instanceof ICalendar)
return;
$this->validateICalendar(
$data,
$path,
$modified,
$this->server->httpRequest,
$this->server->httpResponse,
true
);
}
/**
* Checks if the submitted iCalendar data is in fact, valid.
*
* An exception is thrown if it's not.
*
* @param resource|string $data
* @param string $path
* @param bool $modified Should be set to true, if this event handler
* changed &$data.
* @param RequestInterface $request The http request.
* @param ResponseInterface $response The http response.
* @param bool $isNew Is the item a new one, or an update.
* @return void
*/
protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) {
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
$before = md5($data);
// Converting the data to unicode, if needed.
$data = DAV\StringUtil::ensureUTF8($data);
if ($before !== md5($data)) $modified = true;
try {
// If the data starts with a [, we can reasonably assume we're dealing
// with a jCal object.
if (substr($data, 0, 1) === '[') {
$vobj = VObject\Reader::readJson($data);
// Converting $data back to iCalendar, as that's what we
// technically support everywhere.
$data = $vobj->serialize();
$modified = true;
} else {
$vobj = VObject\Reader::read($data);
}
} catch (VObject\ParseException $e) {
throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
}
if ($vobj->name !== 'VCALENDAR') {
throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
}
$sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
// Get the Supported Components for the target calendar
list($parentPath) = Uri\split($path);
$calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
if (isset($calendarProperties[$sCCS])) {
$supportedComponents = $calendarProperties[$sCCS]->getValue();
} else {
$supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
}
$foundType = null;
$foundUID = null;
foreach ($vobj->getComponents() as $component) {
switch ($component->name) {
case 'VTIMEZONE' :
continue 2;
case 'VEVENT' :
case 'VTODO' :
case 'VJOURNAL' :
if (is_null($foundType)) {
$foundType = $component->name;
if (!in_array($foundType, $supportedComponents)) {
throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
}
if (!isset($component->UID)) {
throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
}
$foundUID = (string)$component->UID;
} else {
if ($foundType !== $component->name) {
throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
}
if ($foundUID !== (string)$component->UID) {
throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
}
}
break;
default :
throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
}
}
if (!$foundType)
throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
// We use an extra variable to allow event handles to tell us wether
// the object was modified or not.
//
// This helps us determine if we need to re-serialize the object.
$subModified = false;
$this->server->emit(
'calendarObjectChange',
[
$request,
$response,
$vobj,
$parentPath,
&$subModified,
$isNew
]
);
if ($subModified) {
// An event handler told us that it modified the object.
$data = $vobj->serialize();
// Using md5 to figure out if there was an *actual* change.
if (!$modified && $before !== md5($data)) {
$modified = true;
}
}
}
/**
* This method is used to generate HTML output for the
* DAV\Browser\Plugin. This allows us to generate an interface users
* can use to create new calendars.
*
* @param DAV\INode $node
* @param string $output
* @return bool
*/
function htmlActionsPanel(DAV\INode $node, &$output) {
if (!$node instanceof CalendarHome)
return;
$output .= '<tr><td colspan="2"><form method="post" action="">
<h3>Create new calendar</h3>
<input type="hidden" name="sabreAction" value="mkcol" />
<input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CALDAV . '}calendar" />
<label>Name (uri):</label> <input type="text" name="name" /><br />
<label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
<input type="submit" value="create" />
</form>
</td></tr>';
return false;
}
/**
* This event is triggered after GET requests.
*
* This is used to transform data into jCal, if this was requested.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return void
*/
function httpAfterGet(RequestInterface $request, ResponseInterface $response) {
if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) {
return;
}
$result = HTTP\Util::negotiate(
$request->getHeader('Accept'),
['text/calendar', 'application/calendar+json']
);
if ($result !== 'application/calendar+json') {
// Do nothing
return;
}
// Transforming.
$vobj = VObject\Reader::read($response->getBody());
$jsonBody = json_encode($vobj->jsonSerialize());
$response->setBody($jsonBody);
$response->setHeader('Content-Type', 'application/calendar+json');
$response->setHeader('Content-Length', strlen($jsonBody));
}
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function getPluginInfo() {
return [
'name' => $this->getPluginName(),
'description' => 'Adds support for CalDAV (rfc4791)',
'link' => 'http://sabre.io/dav/caldav/',
];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Sabre\CalDAV\Principal;
use Sabre\DAVACL;
/**
* Principal collection
*
* This is an alternative collection to the standard ACL principal collection.
* This collection adds support for the calendar-proxy-read and
* calendar-proxy-write sub-principals, as defined by the caldav-proxy
* specification.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Collection extends DAVACL\PrincipalCollection {
/**
* Returns a child object based on principal information
*
* @param array $principalInfo
* @return User
*/
function getChildForPrincipal(array $principalInfo) {
return new User($this->principalBackend, $principalInfo);
}
}

View file

@ -0,0 +1,181 @@
<?php
namespace Sabre\CalDAV\Principal;
use Sabre\DAVACL;
use Sabre\DAV;
/**
* ProxyRead principal
*
* This class represents a principal group, hosted under the main principal.
* This is needed to implement 'Calendar delegation' support. This class is
* instantiated by User.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ProxyRead implements IProxyRead {
/**
* Principal information from the parent principal.
*
* @var array
*/
protected $principalInfo;
/**
* Principal backend
*
* @var DAVACL\PrincipalBackend\BackendInterface
*/
protected $principalBackend;
/**
* Creates the object.
*
* Note that you MUST supply the parent principal information.
*
* @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
* @param array $principalInfo
*/
function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
$this->principalInfo = $principalInfo;
$this->principalBackend = $principalBackend;
}
/**
* Returns this principals name.
*
* @return string
*/
function getName() {
return 'calendar-proxy-read';
}
/**
* Returns the last modification time
*
* @return null
*/
function getLastModified() {
return null;
}
/**
* Deletes the current node
*
* @throws DAV\Exception\Forbidden
* @return void
*/
function delete() {
throw new DAV\Exception\Forbidden('Permission denied to delete node');
}
/**
* Renames the node
*
* @throws DAV\Exception\Forbidden
* @param string $name The new name
* @return void
*/
function setName($name) {
throw new DAV\Exception\Forbidden('Permission denied to rename file');
}
/**
* Returns a list of alternative urls for a principal
*
* This can for example be an email address, or ldap url.
*
* @return array
*/
function getAlternateUriSet() {
return [];
}
/**
* Returns the full principal url
*
* @return string
*/
function getPrincipalUrl() {
return $this->principalInfo['uri'] . '/' . $this->getName();
}
/**
* Returns the list of group members
*
* If this principal is a group, this function should return
* all member principal uri's for the group.
*
* @return array
*/
function getGroupMemberSet() {
return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
}
/**
* Returns the list of groups this principal is member of
*
* If this principal is a member of a (list of) groups, this function
* should return a list of principal uri's for it's members.
*
* @return array
*/
function getGroupMembership() {
return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
}
/**
* Sets a list of group members
*
* If this principal is a group, this method sets all the group members.
* The list of members is always overwritten, never appended to.
*
* This method should throw an exception if the members could not be set.
*
* @param array $principals
* @return void
*/
function setGroupMemberSet(array $principals) {
$this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
}
/**
* Returns the displayname
*
* This should be a human readable name for the principal.
* If none is available, return the nodename.
*
* @return string
*/
function getDisplayName() {
return $this->getName();
}
}

View file

@ -0,0 +1,181 @@
<?php
namespace Sabre\CalDAV\Principal;
use Sabre\DAVACL;
use Sabre\DAV;
/**
* ProxyWrite principal
*
* This class represents a principal group, hosted under the main principal.
* This is needed to implement 'Calendar delegation' support. This class is
* instantiated by User.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ProxyWrite implements IProxyWrite {
/**
* Parent principal information
*
* @var array
*/
protected $principalInfo;
/**
* Principal Backend
*
* @var DAVACL\PrincipalBackend\BackendInterface
*/
protected $principalBackend;
/**
* Creates the object
*
* Note that you MUST supply the parent principal information.
*
* @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
* @param array $principalInfo
*/
function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
$this->principalInfo = $principalInfo;
$this->principalBackend = $principalBackend;
}
/**
* Returns this principals name.
*
* @return string
*/
function getName() {
return 'calendar-proxy-write';
}
/**
* Returns the last modification time
*
* @return null
*/
function getLastModified() {
return null;
}
/**
* Deletes the current node
*
* @throws DAV\Exception\Forbidden
* @return void
*/
function delete() {
throw new DAV\Exception\Forbidden('Permission denied to delete node');
}
/**
* Renames the node
*
* @throws DAV\Exception\Forbidden
* @param string $name The new name
* @return void
*/
function setName($name) {
throw new DAV\Exception\Forbidden('Permission denied to rename file');
}
/**
* Returns a list of alternative urls for a principal
*
* This can for example be an email address, or ldap url.
*
* @return array
*/
function getAlternateUriSet() {
return [];
}
/**
* Returns the full principal url
*
* @return string
*/
function getPrincipalUrl() {
return $this->principalInfo['uri'] . '/' . $this->getName();
}
/**
* Returns the list of group members
*
* If this principal is a group, this function should return
* all member principal uri's for the group.
*
* @return array
*/
function getGroupMemberSet() {
return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
}
/**
* Returns the list of groups this principal is member of
*
* If this principal is a member of a (list of) groups, this function
* should return a list of principal uri's for it's members.
*
* @return array
*/
function getGroupMembership() {
return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
}
/**
* Sets a list of group members
*
* If this principal is a group, this method sets all the group members.
* The list of members is always overwritten, never appended to.
*
* This method should throw an exception if the members could not be set.
*
* @param array $principals
* @return void
*/
function setGroupMemberSet(array $principals) {
$this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
}
/**
* Returns the displayname
*
* This should be a human readable name for the principal.
* If none is available, return the nodename.
*
* @return string
*/
function getDisplayName() {
return $this->getName();
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace Sabre\CalDAV\Principal;
use Sabre\DAV;
use Sabre\DAVACL;
/**
* CalDAV principal
*
* This is a standard user-principal for CalDAV. This principal is also a
* collection and returns the caldav-proxy-read and caldav-proxy-write child
* principals.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class User extends DAVACL\Principal implements DAV\ICollection {
/**
* Creates a new file in the directory
*
* @param string $name Name of the file
* @param resource $data Initial payload, passed as a readable stream resource.
* @throws DAV\Exception\Forbidden
* @return void
*/
function createFile($name, $data = null) {
throw new DAV\Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
}
/**
* Creates a new subdirectory
*
* @param string $name
* @throws DAV\Exception\Forbidden
* @return void
*/
function createDirectory($name) {
throw new DAV\Exception\Forbidden('Permission denied to create directory');
}
/**
* Returns a specific child node, referenced by its name
*
* @param string $name
* @return DAV\INode
*/
function getChild($name) {
$principal = $this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/' . $name);
if (!$principal) {
throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
}
if ($name === 'calendar-proxy-read')
return new ProxyRead($this->principalBackend, $this->principalProperties);
if ($name === 'calendar-proxy-write')
return new ProxyWrite($this->principalBackend, $this->principalProperties);
throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
}
/**
* Returns an array with all the child nodes
*
* @return DAV\INode[]
*/
function getChildren() {
$r = [];
if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-read')) {
$r[] = new ProxyRead($this->principalBackend, $this->principalProperties);
}
if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-write')) {
$r[] = new ProxyWrite($this->principalBackend, $this->principalProperties);
}
return $r;
}
/**
* Returns whether or not the child node exists
*
* @param string $name
* @return bool
*/
function childExists($name) {
try {
$this->getChild($name);
return true;
} catch (DAV\Exception\NotFound $e) {
return false;
}
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
$acl = parent::getACL();
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->principalProperties['uri'] . '/calendar-proxy-read',
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->principalProperties['uri'] . '/calendar-proxy-write',
'protected' => true,
];
return $acl;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace Sabre\CalDAV\Schedule;
/**
* Implement this interface to have a node be recognized as a CalDAV scheduling
* inbox.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface IInbox extends \Sabre\CalDAV\ICalendarObjectContainer, \Sabre\DAVACL\IACL {
}

View file

@ -0,0 +1,190 @@
<?php
namespace Sabre\CalDAV\Schedule;
use Sabre\DAV;
use Sabre\VObject\ITip;
/**
* iMIP handler.
*
* This class is responsible for sending out iMIP messages. iMIP is the
* email-based transport for iTIP. iTIP deals with scheduling operations for
* iCalendar objects.
*
* If you want to customize the email that gets sent out, you can do so by
* extending this class and overriding the sendMessage method.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class IMipPlugin extends DAV\ServerPlugin {
/**
* Email address used in From: header.
*
* @var string
*/
protected $senderEmail;
/**
* ITipMessage
*
* @var ITip\Message
*/
protected $itipMessage;
/**
* Creates the email handler.
*
* @param string $senderEmail. The 'senderEmail' is the email that shows up
* in the 'From:' address. This should
* generally be some kind of no-reply email
* address you own.
*/
function __construct($senderEmail) {
$this->senderEmail = $senderEmail;
}
/*
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param DAV\Server $server
* @return void
*/
function initialize(DAV\Server $server) {
$server->on('schedule', [$this, 'schedule'], 120);
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using \Sabre\DAV\Server::getPlugin
*
* @return string
*/
function getPluginName() {
return 'imip';
}
/**
* Event handler for the 'schedule' event.
*
* @param ITip\Message $iTipMessage
* @return void
*/
function schedule(ITip\Message $iTipMessage) {
// Not sending any emails if the system considers the update
// insignificant.
if (!$iTipMessage->significantChange) {
if (!$iTipMessage->scheduleStatus) {
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
}
return;
}
$summary = $iTipMessage->message->VEVENT->SUMMARY;
if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto')
return;
if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto')
return;
$sender = substr($iTipMessage->sender, 7);
$recipient = substr($iTipMessage->recipient, 7);
if ($iTipMessage->senderName) {
$sender = $iTipMessage->senderName . ' <' . $sender . '>';
}
if ($iTipMessage->recipientName) {
$recipient = $iTipMessage->recipientName . ' <' . $recipient . '>';
}
$subject = 'SabreDAV iTIP message';
switch (strtoupper($iTipMessage->method)) {
case 'REPLY' :
$subject = 'Re: ' . $summary;
break;
case 'REQUEST' :
$subject = $summary;
break;
case 'CANCEL' :
$subject = 'Cancelled: ' . $summary;
break;
}
$headers = [
'Reply-To: ' . $sender,
'From: ' . $this->senderEmail,
'Content-Type: text/calendar; charset=UTF-8; method=' . $iTipMessage->method,
];
if (DAV\Server::$exposeVersion) {
$headers[] = 'X-Sabre-Version: ' . DAV\Version::VERSION;
}
$this->mail(
$recipient,
$subject,
$iTipMessage->message->serialize(),
$headers
);
$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
}
// @codeCoverageIgnoreStart
// This is deemed untestable in a reasonable manner
/**
* This function is responsible for sending the actual email.
*
* @param string $to Recipient email address
* @param string $subject Subject of the email
* @param string $body iCalendar body
* @param array $headers List of headers
* @return void
*/
protected function mail($to, $subject, $body, array $headers) {
mail($to, $subject, $body, implode("\r\n", $headers));
}
// @codeCoverageIgnoreEnd
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function getPluginInfo() {
return [
'name' => $this->getPluginName(),
'description' => 'Email delivery (rfc6037) for CalDAV scheduling',
'link' => 'http://sabre.io/dav/scheduling/',
];
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Sabre\CalDAV\Schedule;
/**
* The SchedulingObject represents a scheduling object in the Inbox collection
*
* @license http://sabre.io/license/ Modified BSD License
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
*/
interface ISchedulingObject extends \Sabre\CalDAV\ICalendarObject {
}

View file

@ -0,0 +1,268 @@
<?php
namespace Sabre\CalDAV\Schedule;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\DAVACL;
use Sabre\CalDAV\Backend;
use Sabre\VObject;
/**
* The CalDAV scheduling inbox
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Inbox extends DAV\Collection implements IInbox {
/**
* CalDAV backend
*
* @var Backend\BackendInterface
*/
protected $caldavBackend;
/**
* The principal Uri
*
* @var string
*/
protected $principalUri;
/**
* Constructor
*
* @param Backend\SchedulingSupport $caldavBackend
* @param string $principalUri
*/
function __construct(Backend\SchedulingSupport $caldavBackend, $principalUri) {
$this->caldavBackend = $caldavBackend;
$this->principalUri = $principalUri;
}
/**
* Returns the name of the node.
*
* This is used to generate the url.
*
* @return string
*/
function getName() {
return 'inbox';
}
/**
* Returns an array with all the child nodes
*
* @return \Sabre\DAV\INode[]
*/
function getChildren() {
$objs = $this->caldavBackend->getSchedulingObjects($this->principalUri);
$children = [];
foreach ($objs as $obj) {
//$obj['acl'] = $this->getACL();
$obj['principaluri'] = $this->principalUri;
$children[] = new SchedulingObject($this->caldavBackend, $obj);
}
return $children;
}
/**
* Creates a new file in the directory
*
* Data will either be supplied as a stream resource, or in certain cases
* as a string. Keep in mind that you may have to support either.
*
* After succesful creation of the file, you may choose to return the ETag
* of the new file here.
*
* The returned ETag must be surrounded by double-quotes (The quotes should
* be part of the actual string).
*
* If you cannot accurately determine the ETag, you should not return it.
* If you don't store the file exactly as-is (you're transforming it
* somehow) you should also not return an ETag.
*
* This means that if a subsequent GET to this new file does not exactly
* return the same contents of what was submitted here, you are strongly
* recommended to omit the ETag.
*
* @param string $name Name of the file
* @param resource|string $data Initial payload
* @return null|string
*/
function createFile($name, $data = null) {
$this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data);
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->principalUri;
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup() {
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}write-properties',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}unbind',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}unbind',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-invite',
'principal' => '{DAV:}authenticated',
'protected' => true,
],
[
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-reply',
'principal' => '{DAV:}authenticated',
'protected' => true,
],
];
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's.
*
* @param array $acl
* @return void
*/
function setACL(array $acl) {
throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
}
/**
* Returns the list of supported privileges for this node.
*
* The returned data structure is a list of nested privileges.
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
* standard structure.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet() {
$ns = '{' . CalDAV\Plugin::NS_CALDAV . '}';
$default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
$default['aggregates'][] = [
'privilege' => $ns . 'schedule-deliver',
'aggregates' => [
['privilege' => $ns . 'schedule-deliver-invite'],
['privilege' => $ns . 'schedule-deliver-reply'],
],
];
return $default;
}
/**
* 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.
*
* @param array $filters
* @return array
*/
function calendarQuery(array $filters) {
$result = [];
$validator = new CalDAV\CalendarQueryValidator();
$objects = $this->caldavBackend->getSchedulingObjects($this->principalUri);
foreach ($objects as $object) {
$vObject = VObject\Reader::read($object['calendardata']);
if ($validator->validate($vObject, $filters)) {
$result[] = $object['uri'];
}
}
return $result;
}
}

View file

@ -0,0 +1,184 @@
<?php
namespace Sabre\CalDAV\Schedule;
use Sabre\DAV;
use Sabre\CalDAV;
use Sabre\DAVACL;
/**
* The CalDAV scheduling outbox
*
* The outbox is mainly used as an endpoint in the tree for a client to do
* free-busy requests. This functionality is completely handled by the
* Scheduling plugin, so this object is actually mostly static.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Outbox extends DAV\Collection implements IOutbox {
/**
* The principal Uri
*
* @var string
*/
protected $principalUri;
/**
* Constructor
*
* @param string $principalUri
*/
function __construct($principalUri) {
$this->principalUri = $principalUri;
}
/**
* Returns the name of the node.
*
* This is used to generate the url.
*
* @return string
*/
function getName() {
return 'outbox';
}
/**
* Returns an array with all the child nodes
*
* @return \Sabre\DAV\INode[]
*/
function getChildren() {
return [];
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->principalUri;
}
/**
* Returns a group principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getGroup() {
return null;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
return [
[
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
];
}
/**
* Updates the ACL
*
* This method will receive a list of new ACE's.
*
* @param array $acl
* @return void
*/
function setACL(array $acl) {
throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
}
/**
* Returns the list of supported privileges for this node.
*
* The returned data structure is a list of nested privileges.
* See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
* standard structure.
*
* If null is returned from this method, the default privilege set is used,
* which is fine for most common usecases.
*
* @return array|null
*/
function getSupportedPrivilegeSet() {
$default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
$default['aggregates'][] = [
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
];
$default['aggregates'][] = [
'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
];
return $default;
}
}

View file

@ -0,0 +1,897 @@
<?php
namespace Sabre\CalDAV\Schedule;
use DateTimeZone;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\PropFind;
use Sabre\DAV\INode;
use Sabre\DAV\Xml\Property\Href;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject;
use Sabre\VObject\Reader;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\ITip;
use Sabre\VObject\ITip\Message;
use Sabre\DAVACL;
use Sabre\CalDAV\ICalendar;
use Sabre\CalDAV\ICalendarObject;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\NotImplemented;
/**
* CalDAV scheduling plugin.
* =========================
*
* This plugin provides the functionality added by the "Scheduling Extensions
* to CalDAV" standard, as defined in RFC6638.
*
* calendar-auto-schedule largely works by intercepting a users request to
* update their local calendar. If a user creates a new event with attendees,
* this plugin is supposed to grab the information from that event, and notify
* the attendees of this.
*
* There's 3 possible transports for this:
* * local delivery
* * delivery through email (iMip)
* * server-to-server delivery (iSchedule)
*
* iMip is simply, because we just need to add the iTip message as an email
* attachment. Local delivery is harder, because we both need to add this same
* message to a local DAV inbox, as well as live-update the relevant events.
*
* iSchedule is something for later.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Plugin extends ServerPlugin {
/**
* This is the official CalDAV namespace
*/
const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
/**
* Reference to main Server object.
*
* @var Server
*/
protected $server;
/**
* Returns a list of features for the DAV: HTTP header.
*
* @return array
*/
function getFeatures() {
return ['calendar-auto-schedule'];
}
/**
* Returns the name of the plugin.
*
* Using this name other plugins will be able to access other plugins
* using Server::getPlugin
*
* @return string
*/
function getPluginName() {
return 'caldav-schedule';
}
/**
* Initializes the plugin
*
* @param Server $server
* @return void
*/
function initialize(Server $server) {
$this->server = $server;
$server->on('method:POST', [$this, 'httpPost']);
$server->on('propFind', [$this, 'propFind']);
$server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
$server->on('beforeUnbind', [$this, 'beforeUnbind']);
$server->on('schedule', [$this, 'scheduleLocalDelivery']);
$ns = '{' . self::NS_CALDAV . '}';
/**
* This information ensures that the {DAV:}resourcetype property has
* the correct values.
*/
$server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns . 'schedule-outbox';
$server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns . 'schedule-inbox';
/**
* Properties we protect are made read-only by the server.
*/
array_push($server->protectedProperties,
$ns . 'schedule-inbox-URL',
$ns . 'schedule-outbox-URL',
$ns . 'calendar-user-address-set',
$ns . 'calendar-user-type',
$ns . 'schedule-default-calendar-URL'
);
}
/**
* Use this method to tell the server this plugin defines additional
* HTTP methods.
*
* This method is passed a uri. It should only return HTTP methods that are
* available for the specified uri.
*
* @param string $uri
* @return array
*/
function getHTTPMethods($uri) {
try {
$node = $this->server->tree->getNodeForPath($uri);
} catch (NotFound $e) {
return [];
}
if ($node instanceof IOutbox) {
return ['POST'];
}
return [];
}
/**
* This method handles POST request for the outbox.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
function httpPost(RequestInterface $request, ResponseInterface $response) {
// Checking if this is a text/calendar content type
$contentType = $request->getHeader('Content-Type');
if (strpos($contentType, 'text/calendar') !== 0) {
return;
}
$path = $request->getPath();
// Checking if we're talking to an outbox
try {
$node = $this->server->tree->getNodeForPath($path);
} catch (NotFound $e) {
return;
}
if (!$node instanceof IOutbox)
return;
$this->server->transactionType = 'post-caldav-outbox';
$this->outboxRequest($node, $request, $response);
// Returning false breaks the event chain and tells the server we've
// handled the request.
return false;
}
/**
* 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';
});
}
/**
* This method is triggered whenever there was a calendar object gets
* created or updated.
*
* @param RequestInterface $request HTTP request
* @param ResponseInterface $response HTTP Response
* @param VCalendar $vCal Parsed iCalendar object
* @param mixed $calendarPath Path to calendar collection
* @param mixed $modified The iCalendar object has been touched.
* @param mixed $isNew Whether this was a new item or we're updating one
* @return void
*/
function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
if (!$this->scheduleReply($this->server->httpRequest)) {
return;
}
$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
$addresses = $this->getAddressesForPrincipal(
$calendarNode->getOwner()
);
if (!$isNew) {
$node = $this->server->tree->getNodeForPath($request->getPath());
$oldObj = Reader::read($node->get());
} else {
$oldObj = null;
}
$this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
}
/**
* This method is responsible for delivering the ITip message.
*
* @param ITip\Message $itipMessage
* @return void
*/
function deliver(ITip\Message $iTipMessage) {
$this->server->emit('schedule', [$iTipMessage]);
if (!$iTipMessage->scheduleStatus) {
$iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
}
// In case the change was considered 'insignificant', we are going to
// remove any error statuses, if any. See ticket #525.
list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
$iTipMessage->scheduleStatus = null;
}
}
/**
* This method is triggered before a file gets deleted.
*
* We use this event to make sure that when this happens, attendees get
* cancellations, and organizers get 'DECLINED' statuses.
*
* @param string $path
* @return void
*/
function beforeUnbind($path) {
// FIXME: We shouldn't trigger this functionality when we're issuing a
// MOVE. This is a hack.
if ($this->server->httpRequest->getMethod() === 'MOVE') return;
$node = $this->server->tree->getNodeForPath($path);
if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
return;
}
if (!$this->scheduleReply($this->server->httpRequest)) {
return;
}
$addresses = $this->getAddressesForPrincipal(
$node->getOwner()
);
$broker = new ITip\Broker();
$messages = $broker->parseEvent(null, $addresses, $node->get());
foreach ($messages as $message) {
$this->deliver($message);
}
}
/**
* Event handler for the 'schedule' event.
*
* This handler attempts to look at local accounts to deliver the
* scheduling object.
*
* @param ITip\Message $iTipMessage
* @return void
*/
function scheduleLocalDelivery(ITip\Message $iTipMessage) {
$aclPlugin = $this->server->getPlugin('acl');
// Local delivery is not available if the ACL plugin is not loaded.
if (!$aclPlugin) {
return;
}
$caldavNS = '{' . self::NS_CALDAV . '}';
$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
if (!$principalUri) {
$iTipMessage->scheduleStatus = '3.7;Could not find principal.';
return;
}
// We found a principal URL, now we need to find its inbox.
// Unfortunately we may not have sufficient privileges to find this, so
// we are temporarily turning off ACL to let this come through.
//
// Once we support PHP 5.5, this should be wrapped in a try..finally
// block so we can ensure that this privilege gets added again after.
$this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
$result = $this->server->getProperties(
$principalUri,
[
'{DAV:}principal-URL',
$caldavNS . 'calendar-home-set',
$caldavNS . 'schedule-inbox-URL',
$caldavNS . 'schedule-default-calendar-URL',
'{http://sabredav.org/ns}email-address',
]
);
// Re-registering the ACL event
$this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
if (!isset($result[$caldavNS . 'schedule-inbox-URL'])) {
$iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
return;
}
if (!isset($result[$caldavNS . 'calendar-home-set'])) {
$iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
return;
}
if (!isset($result[$caldavNS . 'schedule-default-calendar-URL'])) {
$iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
return;
}
$calendarPath = $result[$caldavNS . 'schedule-default-calendar-URL']->getHref();
$homePath = $result[$caldavNS . 'calendar-home-set']->getHref();
$inboxPath = $result[$caldavNS . 'schedule-inbox-URL']->getHref();
if ($iTipMessage->method === 'REPLY') {
$privilege = 'schedule-deliver-reply';
} else {
$privilege = 'schedule-deliver-invite';
}
if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) {
$iTipMessage->scheduleStatus = '3.8;organizer did not have the ' . $privilege . ' privilege on the attendees inbox';
return;
}
// Next, we're going to find out if the item already exits in one of
// the users' calendars.
$uid = $iTipMessage->uid;
$newFileName = 'sabredav-' . \Sabre\DAV\UUIDUtil::getUUID() . '.ics';
$home = $this->server->tree->getNodeForPath($homePath);
$inbox = $this->server->tree->getNodeForPath($inboxPath);
$currentObject = null;
$objectNode = null;
$isNewNode = false;
$result = $home->getCalendarObjectByUID($uid);
if ($result) {
// There was an existing object, we need to update probably.
$objectPath = $homePath . '/' . $result;
$objectNode = $this->server->tree->getNodeForPath($objectPath);
$oldICalendarData = $objectNode->get();
$currentObject = Reader::read($oldICalendarData);
} else {
$isNewNode = true;
}
$broker = new ITip\Broker();
$newObject = $broker->processMessage($iTipMessage, $currentObject);
$inbox->createFile($newFileName, $iTipMessage->message->serialize());
if (!$newObject) {
// We received an iTip message referring to a UID that we don't
// have in any calendars yet, and processMessage did not give us a
// calendarobject back.
//
// The implication is that processMessage did not understand the
// iTip message.
$iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
return;
}
// Note that we are bypassing ACL on purpose by calling this directly.
// We may need to look a bit deeper into this later. Supporting ACL
// here would be nice.
if ($isNewNode) {
$calendar = $this->server->tree->getNodeForPath($calendarPath);
$calendar->createFile($newFileName, $newObject->serialize());
} else {
// If the message was a reply, we may have to inform other
// attendees of this attendees status. Therefore we're shooting off
// another itipMessage.
if ($iTipMessage->method === 'REPLY') {
$this->processICalendarChange(
$oldICalendarData,
$newObject,
[$iTipMessage->recipient],
[$iTipMessage->sender]
);
}
$objectNode->put($newObject->serialize());
}
$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
}
/**
* This method looks at an old iCalendar object, a new iCalendar object and
* starts sending scheduling messages based on the changes.
*
* A list of addresses needs to be specified, so the system knows who made
* the update, because the behavior may be different based on if it's an
* attendee or an organizer.
*
* This method may update $newObject to add any status changes.
*
* @param VCalendar|string $oldObject
* @param VCalendar $newObject
* @param array $addresses
* @param array $ignore Any addresses to not send messages to.
* @param bool $modified A marker to indicate that the original object
* modified by this process.
* @return void
*/
protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) {
$broker = new ITip\Broker();
$messages = $broker->parseEvent($newObject, $addresses, $oldObject);
if ($messages) $modified = true;
foreach ($messages as $message) {
if (in_array($message->recipient, $ignore)) {
continue;
}
$this->deliver($message);
if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
if ($message->scheduleStatus) {
$newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
}
unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
} else {
if (isset($newObject->VEVENT->ATTENDEE)) foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
if ($attendee->getNormalizedValue() === $message->recipient) {
if ($message->scheduleStatus) {
$attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
}
unset($attendee['SCHEDULE-FORCE-SEND']);
break;
}
}
}
}
}
/**
* Returns a list of addresses that are associated with a principal.
*
* @param string $principal
* @return array
*/
protected function getAddressesForPrincipal($principal) {
$CUAS = '{' . self::NS_CALDAV . '}calendar-user-address-set';
$properties = $this->server->getProperties(
$principal,
[$CUAS]
);
// If we can't find this information, we'll stop processing
if (!isset($properties[$CUAS])) {
return;
}
$addresses = $properties[$CUAS]->getHrefs();
return $addresses;
}
/**
* This method handles POST requests to the schedule-outbox.
*
* Currently, two types of requests are support:
* * FREEBUSY requests from RFC 6638
* * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
*
* The latter is from an expired early draft of the CalDAV scheduling
* extensions, but iCal depends on a feature from that spec, so we
* implement it.
*
* @param IOutbox $outboxNode
* @param RequestInterface $request
* @param ResponseInterface $response
* @return void
*/
function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) {
$outboxPath = $request->getPath();
// Parsing the request body
try {
$vObject = VObject\Reader::read($request->getBody());
} catch (VObject\ParseException $e) {
throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
}
// The incoming iCalendar object must have a METHOD property, and a
// component. The combination of both determines what type of request
// this is.
$componentType = null;
foreach ($vObject->getComponents() as $component) {
if ($component->name !== 'VTIMEZONE') {
$componentType = $component->name;
break;
}
}
if (is_null($componentType)) {
throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
}
// Validating the METHOD
$method = strtoupper((string)$vObject->METHOD);
if (!$method) {
throw new BadRequest('A METHOD property must be specified in iTIP messages');
}
// So we support one type of request:
//
// REQUEST with a VFREEBUSY component
$acl = $this->server->getPlugin('acl');
if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
$acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy');
$this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
} else {
throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
}
}
/**
* This method is responsible for parsing a free-busy query request and
* returning it's result.
*
* @param IOutbox $outbox
* @param VObject\Component $vObject
* @param RequestInterface $request
* @param ResponseInterface $response
* @return string
*/
protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) {
$vFreeBusy = $vObject->VFREEBUSY;
$organizer = $vFreeBusy->organizer;
$organizer = (string)$organizer;
// Validating if the organizer matches the owner of the inbox.
$owner = $outbox->getOwner();
$caldavNS = '{' . self::NS_CALDAV . '}';
$uas = $caldavNS . 'calendar-user-address-set';
$props = $this->server->getProperties($owner, [$uas]);
if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
}
if (!isset($vFreeBusy->ATTENDEE)) {
throw new BadRequest('You must at least specify 1 attendee');
}
$attendees = [];
foreach ($vFreeBusy->ATTENDEE as $attendee) {
$attendees[] = (string)$attendee;
}
if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
throw new BadRequest('DTSTART and DTEND must both be specified');
}
$startRange = $vFreeBusy->DTSTART->getDateTime();
$endRange = $vFreeBusy->DTEND->getDateTime();
$results = [];
foreach ($attendees as $attendee) {
$results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
}
$dom = new \DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$scheduleResponse = $dom->createElement('cal:schedule-response');
foreach ($this->server->xml->namespaceMap as $namespace => $prefix) {
$scheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
}
$dom->appendChild($scheduleResponse);
foreach ($results as $result) {
$xresponse = $dom->createElement('cal:response');
$recipient = $dom->createElement('cal:recipient');
$recipientHref = $dom->createElement('d:href');
$recipientHref->appendChild($dom->createTextNode($result['href']));
$recipient->appendChild($recipientHref);
$xresponse->appendChild($recipient);
$reqStatus = $dom->createElement('cal:request-status');
$reqStatus->appendChild($dom->createTextNode($result['request-status']));
$xresponse->appendChild($reqStatus);
if (isset($result['calendar-data'])) {
$calendardata = $dom->createElement('cal:calendar-data');
$calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
$xresponse->appendChild($calendardata);
}
$scheduleResponse->appendChild($xresponse);
}
$response->setStatus(200);
$response->setHeader('Content-Type', 'application/xml');
$response->setBody($dom->saveXML());
}
/**
* Returns free-busy information for a specific address. The returned
* data is an array containing the following properties:
*
* calendar-data : A VFREEBUSY VObject
* request-status : an iTip status code.
* href: The principal's email address, as requested
*
* The following request status codes may be returned:
* * 2.0;description
* * 3.7;description
*
* @param string $email address
* @param \DateTime $start
* @param \DateTime $end
* @param VObject\Component $request
* @return array
*/
protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
$caldavNS = '{' . self::NS_CALDAV . '}';
$aclPlugin = $this->server->getPlugin('acl');
if (substr($email, 0, 7) === 'mailto:') $email = substr($email, 7);
$result = $aclPlugin->principalSearch(
['{http://sabredav.org/ns}email-address' => $email],
[
'{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
'{http://sabredav.org/ns}email-address',
]
);
if (!count($result)) {
return [
'request-status' => '3.7;Could not find principal',
'href' => 'mailto:' . $email,
];
}
if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
return [
'request-status' => '3.7;No calendar-home-set property found',
'href' => 'mailto:' . $email,
];
}
$homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
// Grabbing the calendar list
$objects = [];
$calendarTimeZone = new DateTimeZone('UTC');
foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
if (!$node instanceof ICalendar) {
continue;
}
$sct = $caldavNS . 'schedule-calendar-transp';
$ctz = $caldavNS . 'calendar-timezone';
$props = $node->getProperties([$sct, $ctz]);
if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) {
// If a calendar is marked as 'transparent', it means we must
// ignore it for free-busy purposes.
continue;
}
$aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy');
if (isset($props[$ctz])) {
$vtimezoneObj = VObject\Reader::read($props[$ctz]);
$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
}
// Getting the list of object uris within the time-range
$urls = $node->calendarQuery([
'name' => 'VCALENDAR',
'comp-filters' => [
[
'name' => 'VEVENT',
'comp-filters' => [],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => [
'start' => $start,
'end' => $end,
],
],
],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => null,
]);
$calObjects = array_map(function($url) use ($node) {
$obj = $node->getChild($url)->get();
return $obj;
}, $urls);
$objects = array_merge($objects, $calObjects);
}
$vcalendar = new VObject\Component\VCalendar();
$vcalendar->METHOD = 'REPLY';
$generator = new VObject\FreeBusyGenerator();
$generator->setObjects($objects);
$generator->setTimeRange($start, $end);
$generator->setBaseObject($vcalendar);
$generator->setTimeZone($calendarTimeZone);
$result = $generator->getResult();
$vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
$vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
$vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
return [
'calendar-data' => $result,
'request-status' => '2.0;Success',
'href' => 'mailto:' . $email,
];
}
/**
* This method checks the 'Schedule-Reply' header
* and returns false if it's 'F', otherwise true.
*
* @param RequestInterface $request
* @return bool
*/
private function scheduleReply(RequestInterface $request) {
$scheduleReply = $request->getHeader('Schedule-Reply');
return $scheduleReply !== 'F';
}
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function getPluginInfo() {
return [
'name' => $this->getPluginName(),
'description' => 'Adds calendar-auto-schedule, as defined in rf6868',
'link' => 'http://sabre.io/dav/scheduling/',
];
}
}

View file

@ -0,0 +1,165 @@
<?php
namespace Sabre\CalDAV\Schedule;
use Sabre\CalDAV\Backend;
use Sabre\DAV\Exception\MethodNotAllowed;
/**
* The SchedulingObject represents a scheduling object in the Inbox collection
*
* @author Brett (https://github.com/bretten)
* @license http://sabre.io/license/ Modified BSD License
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
*/
class SchedulingObject extends \Sabre\CalDAV\CalendarObject implements ISchedulingObject {
/**
/* The CalDAV backend
*
* @var Backend\SchedulingSupport
*/
protected $caldavBackend;
/**
* Array with information about this SchedulingObject
*
* @var array
*/
protected $objectData;
/**
* Constructor
*
* The following properties may be passed within $objectData:
*
* * uri - A unique uri. Only the 'basename' must be passed.
* * principaluri - the principal that owns the object.
* * calendardata (optional) - The iCalendar data
* * etag - (optional) The etag for this object, MUST be encloded with
* double-quotes.
* * size - (optional) The size of the data in bytes.
* * lastmodified - (optional) format as a unix timestamp.
* * acl - (optional) Use this to override the default ACL for the node.
*
* @param Backend\BackendInterface $caldavBackend
* @param array $objectData
*/
function __construct(Backend\SchedulingSupport $caldavBackend, array $objectData) {
$this->caldavBackend = $caldavBackend;
if (!isset($objectData['uri'])) {
throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
}
$this->objectData = $objectData;
}
/**
* Returns the ICalendar-formatted object
*
* @return string
*/
function get() {
// Pre-populating the 'calendardata' is optional, if we don't have it
// already we fetch it from the backend.
if (!isset($this->objectData['calendardata'])) {
$this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
}
return $this->objectData['calendardata'];
}
/**
* Updates the ICalendar-formatted object
*
* @param string|resource $calendarData
* @return string
*/
function put($calendarData) {
throw new MethodNotAllowed('Updating scheduling objects is not supported');
}
/**
* Deletes the scheduling message
*
* @return void
*/
function delete() {
$this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->objectData['principaluri'];
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
// An alternative acl may be specified in the object data.
//
if (isset($this->objectData['acl'])) {
return $this->objectData['acl'];
}
// The default ACL
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->objectData['principaluri'],
'protected' => true,
],
[
'privilege' => '{DAV:}write',
'principal' => $this->objectData['principaluri'],
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}write',
'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->objectData['principaluri'] . '/calendar-proxy-read',
'protected' => true,
],
];
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace Sabre\CalDAV;
/**
* This object represents a CalDAV calendar that is shared by a different user.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SharedCalendar extends Calendar implements ISharedCalendar {
/**
* Constructor
*
* @param Backend\BackendInterface $caldavBackend
* @param array $calendarInfo
*/
function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
$required = [
'{http://calendarserver.org/ns/}shared-url',
'{http://sabredav.org/ns}owner-principal',
'{http://sabredav.org/ns}read-only',
];
foreach ($required as $r) {
if (!isset($calendarInfo[$r])) {
throw new \InvalidArgumentException('The ' . $r . ' property must be specified for SharedCalendar(s)');
}
}
parent::__construct($caldavBackend, $calendarInfo);
}
/**
* This method should return the url of the owners' copy of the shared
* calendar.
*
* @return string
*/
function getSharedUrl() {
return $this->calendarInfo['{http://calendarserver.org/ns/}shared-url'];
}
/**
* Returns the owner principal
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
function getOwner() {
return $this->calendarInfo['{http://sabredav.org/ns}owner-principal'];
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
function getACL() {
// The top-level ACL only contains access information for the true
// owner of the calendar, so we need to add the information for the
// sharee.
$acl = parent::getACL();
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
if ($this->calendarInfo['{http://sabredav.org/ns}read-only']) {
$acl[] = [
'privilege' => '{DAV:}write-properties',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
} else {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
}
return $acl;
}
/**
* This method returns the ACL's for calendar objects in this calendar.
* The result of this method automatically gets passed to the
* calendar-object nodes in the calendar.
*
* @return array
*/
function getChildACL() {
$acl = parent::getChildACL();
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
if (!$this->calendarInfo['{http://sabredav.org/ns}read-only']) {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
}
return $acl;
}
/**
* 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
*/
function getShares() {
return $this->caldavBackend->getShares($this->calendarInfo['id']);
}
}

View file

@ -0,0 +1,426 @@
<?php
namespace Sabre\CalDAV;
use Sabre\DAV;
use Sabre\DAV\Xml\Property\Href;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* This plugin implements support for caldav sharing.
*
* This spec is defined at:
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
*
* See:
* Sabre\CalDAV\Backend\SharingSupport for all the documentation.
*
* Note: This feature is experimental, and may change in between different
* SabreDAV versions.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SharingPlugin extends DAV\ServerPlugin {
/**
* These are the various status constants used by sharing-messages.
*/
const STATUS_ACCEPTED = 1;
const STATUS_DECLINED = 2;
const STATUS_DELETED = 3;
const STATUS_NORESPONSE = 4;
const STATUS_INVALID = 5;
/**
* Reference to SabreDAV server object.
*
* @var Sabre\DAV\Server
*/
protected $server;
/**
* This method should return a list of server-features.
*
* This is for example 'versioning' and is added to the DAV: header
* in an OPTIONS response.
*
* @return array
*/
function getFeatures() {
return ['calendarserver-sharing'];
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using Sabre\DAV\Server::getPlugin
*
* @return string
*/
function getPluginName() {
return 'caldav-sharing';
}
/**
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param DAV\Server $server
* @return void
*/
function initialize(DAV\Server $server) {
$this->server = $server;
$server->resourceTypeMapping['Sabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared';
array_push(
$this->server->protectedProperties,
'{' . Plugin::NS_CALENDARSERVER . '}invite',
'{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes',
'{' . Plugin::NS_CALENDARSERVER . '}shared-url'
);
$this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share';
$this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply';
$this->server->on('propFind', [$this, 'propFindEarly']);
$this->server->on('propFind', [$this, 'propFindLate'], 150);
$this->server->on('propPatch', [$this, 'propPatch'], 40);
$this->server->on('method:POST', [$this, 'httpPost']);
}
/**
* This event is triggered when properties are requested for a certain
* node.
*
* This allows us to inject any properties early.
*
* @param DAV\PropFind $propFind
* @param DAV\INode $node
* @return void
*/
function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) {
if ($node instanceof IShareableCalendar) {
$propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) {
return new Xml\Property\Invite(
$node->getShares()
);
});
}
if ($node instanceof ISharedCalendar) {
$propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}shared-url', function() use ($node) {
return new Href(
$node->getSharedUrl()
);
});
$propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) {
// Fetching owner information
$props = $this->server->getPropertiesForPath($node->getOwner(), [
'{http://sabredav.org/ns}email-address',
'{DAV:}displayname',
], 0);
$ownerInfo = [
'href' => $node->getOwner(),
];
if (isset($props[0][200])) {
// We're mapping the internal webdav properties to the
// elements caldav-sharing expects.
if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) {
$ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address'];
}
if (isset($props[0][200]['{DAV:}displayname'])) {
$ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname'];
}
}
return new Xml\Property\Invite(
$node->getShares(),
$ownerInfo
);
});
}
}
/**
* This method is triggered *after* all properties have been retrieved.
* This allows us to inject the correct resourcetype for calendars that
* have been shared.
*
* @param DAV\PropFind $propFind
* @param DAV\INode $node
* @return void
*/
function propFindLate(DAV\PropFind $propFind, DAV\INode $node) {
if ($node instanceof IShareableCalendar) {
if ($rt = $propFind->get('{DAV:}resourcetype')) {
if (count($node->getShares()) > 0) {
$rt->add('{' . Plugin::NS_CALENDARSERVER . '}shared-owner');
}
}
$propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', function() {
return new Xml\Property\AllowedSharingModes(true, false);
});
}
}
/**
* This method is trigged when a user attempts to update a node's
* properties.
*
* A previous draft of the sharing spec stated that it was possible to use
* PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
* the calendar.
*
* Even though this is no longer in the current spec, we keep this around
* because OS X 10.7 may still make use of this feature.
*
* @param string $path
* @param DAV\PropPatch $propPatch
* @return void
*/
function propPatch($path, DAV\PropPatch $propPatch) {
$node = $this->server->tree->getNodeForPath($path);
if (!$node instanceof IShareableCalendar)
return;
$propPatch->handle('{DAV:}resourcetype', function($value) use ($node) {
if ($value->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return false;
$shares = $node->getShares();
$remove = [];
foreach ($shares as $share) {
$remove[] = $share['href'];
}
$node->updateShares([], $remove);
return true;
});
}
/**
* We intercept this to handle POST requests on calendars.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return null|bool
*/
function httpPost(RequestInterface $request, ResponseInterface $response) {
$path = $request->getPath();
// Only handling xml
$contentType = $request->getHeader('Content-Type');
if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false)
return;
// Making sure the node exists
try {
$node = $this->server->tree->getNodeForPath($path);
} catch (DAV\Exception\NotFound $e) {
return;
}
$requestBody = $request->getBodyAsString();
// If this request handler could not deal with this POST request, it
// will return 'null' and other plugins get a chance to handle the
// request.
//
// However, we already requested the full body. This is a problem,
// because a body can only be read once. This is why we preemptively
// re-populated the request body with the existing data.
$request->setBody($requestBody);
$message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
switch ($documentType) {
// Dealing with the 'share' document, which modified invitees on a
// calendar.
case '{' . Plugin::NS_CALENDARSERVER . '}share' :
// We can only deal with IShareableCalendar objects
if (!$node instanceof IShareableCalendar) {
return;
}
$this->server->transactionType = 'post-calendar-share';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
$node->updateShares($message->set, $message->remove);
$response->setStatus(200);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
// Breaking the event chain
return false;
// The invite-reply document is sent when the user replies to an
// invitation of a calendar share.
case '{' . Plugin::NS_CALENDARSERVER . '}invite-reply' :
// This only works on the calendar-home-root node.
if (!$node instanceof CalendarHome) {
return;
}
$this->server->transactionType = 'post-invite-reply';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
$url = $node->shareReply(
$message->href,
$message->status,
$message->calendarUri,
$message->inReplyTo,
$message->summary
);
$response->setStatus(200);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
if ($url) {
$writer = $this->server->xml->getWriter($this->server->getBaseUri());
$writer->openMemory();
$writer->startDocument();
$writer->startElement('{' . Plugin::NS_CALENDARSERVER . '}shared-as');
$writer->write(new Href($url));
$writer->endElement();
$response->setHeader('Content-Type', 'application/xml');
$response->setBody($writer->outputMemory());
}
// Breaking the event chain
return false;
case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' :
// We can only deal with IShareableCalendar objects
if (!$node instanceof IShareableCalendar) {
return;
}
$this->server->transactionType = 'post-publish-calendar';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
$node->setPublishStatus(true);
// iCloud sends back the 202, so we will too.
$response->setStatus(202);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
// Breaking the event chain
return false;
case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' :
// We can only deal with IShareableCalendar objects
if (!$node instanceof IShareableCalendar) {
return;
}
$this->server->transactionType = 'post-unpublish-calendar';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
$acl->checkPrivileges($path, '{DAV:}write');
}
$node->setPublishStatus(false);
$response->setStatus(200);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
// Breaking the event chain
return false;
}
}
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function getPluginInfo() {
return [
'name' => $this->getPluginName(),
'description' => 'Adds support for caldav-sharing.',
'link' => 'http://sabre.io/dav/caldav-sharing/',
];
}
}

Some files were not shown because too many files have changed in this diff Show more