commit vendor

This commit is contained in:
2025-11-11 14:49:30 +01:00
parent f33121a308
commit 6d03080c00
2436 changed files with 483781 additions and 0 deletions

109
vendor/sabre/dav/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,109 @@
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.
Release process
---------------
Generally, these are the steps taken to do releases.
1. Update the changelog. Every repo will have a `CHANGELOG.md` file. This file
should have a new version, and contain all the changes since the last
release. I generally run a `git diff` to figure out if I missed any changes.
This file should also have the current date.
2. If there were BC breaks, this usually now means a major version bump.
3. Ensure that `lib/Version.php` or `lib/DAV/Version.php` also matches this
version number.
4. Tag the release (Example `git tag 3.0.1` and push the tag (`git push --tags`).
5. (only for the sabre/dav project), create a zip distribution. Run
`php bin/build.php`.
6. For the relevant project, go to github and click the 'releases' tab. On this
tab I create the release with the relevant version. I also set the
description of the release to the same information of the changelog. In the
case of the `sabre/dav` project I also upload the zip distribution here.
7. Write a blog post on sabre.io. This also automatically updates twitter.
[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/

27
vendor/sabre/dav/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.

169
vendor/sabre/dav/bin/build.php vendored Normal file
View File

@ -0,0 +1,169 @@
#!/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.dist --stop-on-failure', $code);
if (0 != $code) {
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 (0 !== $code) {
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())

417
vendor/sabre/dav/bin/migrateto20.php vendored Normal file
View File

@ -0,0 +1,417 @@
#!/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";

166
vendor/sabre/dav/bin/migrateto21.php vendored Normal file
View File

@ -0,0 +1,166 @@
#!/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;
}
echo "Done.\n";
echo "Upgrade to 2.1 schema completed.\n";

161
vendor/sabre/dav/bin/migrateto30.php vendored Normal file
View File

@ -0,0 +1,161 @@
#!/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";

258
vendor/sabre/dav/bin/migrateto32.php vendored Normal file
View File

@ -0,0 +1,258 @@
#!/usr/bin/env php
<?php
echo "SabreDAV migrate script for version 3.2\n";
if ($argc < 2) {
echo <<<HELLO
This script help you migrate from a 3.1 database to 3.2 and later
Changes:
* Created a new calendarinstances table to support calendar sharing.
* Remove a lot of columns from calendars.
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.
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.
Lastly, if you are upgrading from an older version than 3.1, make sure you run
the earlier migration script first. Migration scripts must be ran in order.
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;
$backupPostfix = time();
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 "Creating 'calendarinstances'\n";
$addValueType = false;
try {
$result = $pdo->query('SELECT * FROM calendarinstances LIMIT 1');
$result->fetch(\PDO::FETCH_ASSOC);
echo "calendarinstances exists. Assuming this part of the migration has already been done.\n";
} catch (Exception $e) {
echo "calendarinstances does not yet exist. Creating table and migrating data.\n";
switch ($driver) {
case 'mysql':
$pdo->exec(<<<SQL
CREATE TABLE calendarinstances (
id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
calendarid INTEGER UNSIGNED NOT NULL,
principaluri VARBINARY(100),
access TINYINT(1) NOT NULL DEFAULT '1' COMMENT '1 = owner, 2 = read, 3 = readwrite',
displayname VARCHAR(100),
uri VARBINARY(200),
description TEXT,
calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
calendarcolor VARBINARY(10),
timezone TEXT,
transparent TINYINT(1) NOT NULL DEFAULT '0',
share_href VARBINARY(100),
share_displayname VARCHAR(100),
share_invitestatus TINYINT(1) NOT NULL DEFAULT '2' COMMENT '1 = noresponse, 2 = accepted, 3 = declined, 4 = invalid',
UNIQUE(principaluri, uri),
UNIQUE(calendarid, principaluri),
UNIQUE(calendarid, share_href)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL
);
$pdo->exec('
INSERT INTO calendarinstances
(
calendarid,
principaluri,
access,
displayname,
uri,
description,
calendarorder,
calendarcolor,
transparent
)
SELECT
id,
principaluri,
1,
displayname,
uri,
description,
calendarorder,
calendarcolor,
transparent
FROM calendars
');
break;
case 'sqlite':
$pdo->exec(<<<SQL
CREATE TABLE calendarinstances (
id integer primary key asc NOT NULL,
calendarid integer,
principaluri text,
access integer COMMENT '1 = owner, 2 = read, 3 = readwrite' NOT NULL DEFAULT '1',
displayname text,
uri text NOT NULL,
description text,
calendarorder integer,
calendarcolor text,
timezone text,
transparent bool,
share_href text,
share_displayname text,
share_invitestatus integer DEFAULT '2',
UNIQUE (principaluri, uri),
UNIQUE (calendarid, principaluri),
UNIQUE (calendarid, share_href)
);
SQL
);
$pdo->exec('
INSERT INTO calendarinstances
(
calendarid,
principaluri,
access,
displayname,
uri,
description,
calendarorder,
calendarcolor,
transparent
)
SELECT
id,
principaluri,
1,
displayname,
uri,
description,
calendarorder,
calendarcolor,
transparent
FROM calendars
');
break;
}
}
try {
$result = $pdo->query('SELECT * FROM calendars LIMIT 1');
$row = $result->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
echo "Source table is empty.\n";
$migrateCalendars = true;
}
$columnCount = count($row);
if (3 === $columnCount) {
echo "The calendars table has 3 columns already. Assuming this part of the migration was already done.\n";
$migrateCalendars = false;
} else {
echo 'The calendars table has '.$columnCount." columns.\n";
$migrateCalendars = true;
}
} catch (Exception $e) {
echo "calendars table does not exist. This is a major problem. Exiting.\n";
exit(-1);
}
if ($migrateCalendars) {
$calendarBackup = 'calendars_3_1_'.$backupPostfix;
echo "Backing up 'calendars' to '", $calendarBackup, "'\n";
switch ($driver) {
case 'mysql':
$pdo->exec('RENAME TABLE calendars TO '.$calendarBackup);
break;
case 'sqlite':
$pdo->exec('ALTER TABLE calendars RENAME TO '.$calendarBackup);
break;
}
echo "Creating new calendars table.\n";
switch ($driver) {
case 'mysql':
$pdo->exec(<<<SQL
CREATE TABLE calendars (
id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
synctoken INTEGER UNSIGNED NOT NULL DEFAULT '1',
components VARBINARY(21)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
SQL
);
break;
case 'sqlite':
$pdo->exec(<<<SQL
CREATE TABLE calendars (
id integer primary key asc NOT NULL,
synctoken integer DEFAULT 1 NOT NULL,
components text NOT NULL
);
SQL
);
break;
}
echo "Migrating data from old to new table\n";
$pdo->exec(<<<SQL
INSERT INTO calendars (id, synctoken, components) SELECT id, synctoken, COALESCE(components,"VEVENT,VTODO,VJOURNAL") as components FROM $calendarBackup
SQL
);
}
echo "Upgrade to 3.2 schema completed.\n";

140
vendor/sabre/dav/bin/naturalselection vendored Normal file
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 number 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()

2
vendor/sabre/dav/bin/sabredav vendored Normal file
View File

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

51
vendor/sabre/dav/bin/sabredav.php vendored Normal file
View File

@ -0,0 +1,51 @@
<?php
// SabreDAV test server.
class CliLog
{
protected $stream;
public function __construct()
{
$this->stream = fopen('php://stdout', 'w');
}
public function log($msg)
{
fwrite($this->stream, $msg."\n");
}
}
$log = new CliLog();
if ('cli-server' !== php_sapi_name()) {
die('This script is intended to run on the built-in php webserver');
}
// Finding composer
$paths = [
__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,216 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Backend;
use Sabre\CalDAV;
use Sabre\VObject;
/**
* 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 documentation for more info and examples.
*
* @param mixed $calendarId
*/
public 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
*
* @return array
*/
public 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
*
* @return array
*/
public 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.
*
* @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();
$result = $validator->validate($vObject, $filters);
// Destroy circular references so PHP will GC the object.
$vObject->destroy();
return $result;
}
/**
* 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
*/
public 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,273 @@
<?php
declare(strict_types=1);
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
*/
public 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.
*
* The id can be any type, including ints, strings, objects or array.
*
* @param string $principalUri
* @param string $calendarUri
*
* @return mixed
*/
public 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 mixed $calendarId
*/
public function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch);
/**
* Delete a calendar and all its objects.
*
* @param mixed $calendarId
*/
public 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
*/
public 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
*/
public 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
*
* @return array
*/
public 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
*/
public 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
*/
public 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
*/
public 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
*
* @return array
*/
public 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
*/
public function getCalendarObjectByUID($principalUri, $uid);
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
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[]
*/
public function getNotificationsForPrincipal($principalUri);
/**
* This deletes a specific notifcation.
*
* This may be called by a client once it deems a notification handled.
*
* @param string $principalUri
*/
public function deleteNotification($principalUri, NotificationInterface $notification);
/**
* This method is called when a user replied to a request to share.
*
* If the user chose to accept the share, this method should return the
* newly created calendar url.
*
* @param string $href The sharee who is replying (often a mailto: address)
* @param int $status One of the SharingPlugin::STATUS_* constants
* @param string $calendarUri The url to the calendar thats being shared
* @param string $inReplyTo The unique id this message is a response to
* @param string $summary A description of the reply
*
* @return string|null
*/
public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
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
*/
public 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
*/
public function getSchedulingObjects($principalUri);
/**
* Deletes a scheduling object from the inbox collection.
*
* @param string $principalUri
* @param string $objectUri
*/
public function deleteSchedulingObject($principalUri, $objectUri);
/**
* Creates a new scheduling object. This should land in a users' inbox.
*
* @param string $principalUri
* @param string $objectUri
* @param string|resource $objectData
*/
public function createSchedulingObject($principalUri, $objectUri, $objectData);
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Backend;
/**
* Adds support for sharing features to a CalDAV server.
*
* CalDAV backends that implement this interface, must make the following
* modifications to getCalendarsForUser:
*
* 1. Return shared calendars for users.
* 2. For every calendar, return calendar-resource-uri. This strings is a URI or
* relative URI reference that must be unique for every calendar, but
* identical for every instance of the same shared calendar.
* 3. For every calendar, you must return a share-access element. This element
* should contain one of the Sabre\DAV\Sharing\Plugin:ACCESS_* constants and
* indicates the access level the user has.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface SharingSupport extends BackendInterface
{
/**
* Updates the list of shares.
*
* @param mixed $calendarId
* @param \Sabre\DAV\Xml\Element\Sharee[] $sharees
*/
public function updateInvites($calendarId, array $sharees);
/**
* Returns the list of people whom this calendar is shared with.
*
* Every item in the returned list must be a Sharee object with at
* least the following properties set:
* $href
* $shareAccess
* $inviteStatus
*
* and optionally:
* $properties
*
* @param mixed $calendarId
*
* @return \Sabre\DAV\Xml\Element\Sharee[]
*/
public function getInvites($calendarId);
/**
* Publishes a calendar.
*
* @param mixed $calendarId
* @param bool $value
*/
public function setPublishStatus($calendarId, $value);
}

View File

@ -0,0 +1,289 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Backend;
use Sabre\CalDAV;
use Sabre\DAV;
/**
* Simple PDO CalDAV backend.
*
* This class is basically the most minimum example to get a caldav backend up
* and running. This class uses the following schema (MySQL example):
*
* CREATE TABLE simple_calendars (
* id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
* uri VARBINARY(200) NOT NULL,
* principaluri VARBINARY(200) NOT NULL
* );
*
* CREATE TABLE simple_calendarobjects (
* id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
* calendarid INT UNSIGNED NOT NULL,
* uri VARBINARY(200) NOT NULL,
* calendardata MEDIUMBLOB
* )
*
* To make this class work, you absolutely need to have the PropertyStorage
* plugin enabled.
*
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SimplePDO extends AbstractBackend
{
/**
* pdo.
*
* @var \PDO
*/
protected $pdo;
/**
* Creates the backend.
*/
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* 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. This is just the 'base uri' or 'filename' of the calendar.
* * 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\Xml\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
*/
public function getCalendarsForUser($principalUri)
{
// Making fields a comma-delimited list
$stmt = $this->pdo->prepare('SELECT id, uri FROM simple_calendars WHERE principaluri = ? ORDER BY id ASC');
$stmt->execute([$principalUri]);
$calendars = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$calendars[] = [
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $principalUri,
];
}
return $calendars;
}
/**
* Creates a new calendar for a principal.
*
* If the creation was a success, an id must be returned that can be used
* to reference this calendar in other methods, such as updateCalendar.
*
* @param string $principalUri
* @param string $calendarUri
*
* @return string
*/
public function createCalendar($principalUri, $calendarUri, array $properties)
{
$stmt = $this->pdo->prepare('INSERT INTO simple_calendars (principaluri, uri) VALUES (?, ?)');
$stmt->execute([$principalUri, $calendarUri]);
return $this->pdo->lastInsertId();
}
/**
* Delete a calendar and all it's objects.
*
* @param string $calendarId
*/
public function deleteCalendar($calendarId)
{
$stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ?');
$stmt->execute([$calendarId]);
$stmt = $this->pdo->prepare('DELETE FROM simple_calendars WHERE id = ?');
$stmt->execute([$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 string $calendarId
*
* @return array
*/
public function getCalendarObjects($calendarId)
{
$stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ?');
$stmt->execute([$calendarId]);
$result = [];
foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
$result[] = [
'id' => $row['id'],
'uri' => $row['uri'],
'etag' => '"'.md5($row['calendardata']).'"',
'calendarid' => $calendarId,
'size' => strlen($row['calendardata']),
'calendardata' => $row['calendardata'],
];
}
return $result;
}
/**
* 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 string $calendarId
* @param string $objectUri
*
* @return array|null
*/
public function getCalendarObject($calendarId, $objectUri)
{
$stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?');
$stmt->execute([$calendarId, $objectUri]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
return [
'id' => $row['id'],
'uri' => $row['uri'],
'etag' => '"'.md5($row['calendardata']).'"',
'calendarid' => $calendarId,
'size' => strlen($row['calendardata']),
'calendardata' => $row['calendardata'],
];
}
/**
* Creates a new calendar object.
*
* The object uri is only the basename, or filename and not a full path.
*
* It is possible return an etag from this function, which will be used in
* the response to this PUT request. Note that the ETag must be surrounded
* by double-quotes.
*
* However, you should only really return this ETag if you don't mangle the
* calendar-data. If the result of a subsequent GET to this object is not
* the exact same as this request body, you should omit the ETag.
*
* @param mixed $calendarId
* @param string $objectUri
* @param string $calendarData
*
* @return string|null
*/
public function createCalendarObject($calendarId, $objectUri, $calendarData)
{
$stmt = $this->pdo->prepare('INSERT INTO simple_calendarobjects (calendarid, uri, calendardata) VALUES (?,?,?)');
$stmt->execute([
$calendarId,
$objectUri,
$calendarData,
]);
return '"'.md5($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
*/
public function updateCalendarObject($calendarId, $objectUri, $calendarData)
{
$stmt = $this->pdo->prepare('UPDATE simple_calendarobjects SET calendardata = ? WHERE calendarid = ? AND uri = ?');
$stmt->execute([$calendarData, $calendarId, $objectUri]);
return '"'.md5($calendarData).'"';
}
/**
* Deletes an existing calendar object.
*
* The object uri is only the basename, or filename and not a full path.
*
* @param string $calendarId
* @param string $objectUri
*/
public function deleteCalendarObject($calendarId, $objectUri)
{
$stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?');
$stmt->execute([$calendarId, $objectUri]);
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Backend;
use Sabre\DAV;
/**
* 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 SubscriptionSupport extends BackendInterface
{
/**
* Returns a list of subscriptions for a principal.
*
* Every subscription is an array with the following keys:
* * id, a unique id that will be used by other functions to modify the
* subscription. This can be the same as the uri or a database key.
* * uri. This is just the 'base uri' or 'filename' of the subscription.
* * principaluri. The owner of the subscription. Almost always the same as
* principalUri passed to this method.
*
* Furthermore, all the subscription info must be returned too:
*
* 1. {DAV:}displayname
* 2. {http://apple.com/ns/ical/}refreshrate
* 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
* should not be stripped).
* 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
* should not be stripped).
* 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
* attachments should not be stripped).
* 6. {http://calendarserver.org/ns/}source (Must be a
* Sabre\DAV\Property\Href).
* 7. {http://apple.com/ns/ical/}calendar-color
* 8. {http://apple.com/ns/ical/}calendar-order
* 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
* (should just be an instance of
* Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
* default components).
*
* @param string $principalUri
*
* @return array
*/
public function getSubscriptionsForUser($principalUri);
/**
* Creates a new subscription for a principal.
*
* If the creation was a success, an id must be returned that can be used to reference
* this subscription in other methods, such as updateSubscription.
*
* @param string $principalUri
* @param string $uri
*
* @return mixed
*/
public function createSubscription($principalUri, $uri, array $properties);
/**
* Updates a subscription.
*
* 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 mixed $subscriptionId
* @param \Sabre\DAV\PropPatch $propPatch
*/
public function updateSubscription($subscriptionId, DAV\PropPatch $propPatch);
/**
* Deletes a subscription.
*
* @param mixed $subscriptionId
*/
public function deleteSubscription($subscriptionId);
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Backend;
/**
* WebDAV-sync support for CalDAV backends.
*
* In order for backends to advertise support for WebDAV-sync, this interface
* must be implemented.
*
* Implementing this can result in a significant reduction of bandwidth and CPU
* time.
*
* For this to work, you _must_ return a {http://sabredav.org/ns}sync-token
* property from getCalendarsFromUser.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface SyncSupport extends BackendInterface
{
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified calendar.
*
* 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 returned syncToken property should reflect the *current* syncToken
* of the calendar, as reported in the {http://sabredav.org/ns}sync-token
* property This is * needed here too, to ensure the operation is atomic.
*
* If the $syncToken argument 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 $syncLevel 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 $limit 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 $calendarId
* @param string $syncToken
* @param int $syncLevel
* @param int $limit
*
* @return array
*/
public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null);
}

451
vendor/sabre/dav/lib/CalDAV/Calendar.php vendored Normal file
View File

@ -0,0 +1,451 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV;
use Sabre\DAV;
use Sabre\DAV\PropPatch;
use Sabre\DAVACL;
/**
* 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
{
use DAVACL\ACLTrait;
/**
* This is an array with calendar information.
*
* @var array
*/
protected $calendarInfo;
/**
* CalDAV backend.
*
* @var Backend\BackendInterface
*/
protected $caldavBackend;
/**
* Constructor.
*
* @param array $calendarInfo
*/
public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo)
{
$this->caldavBackend = $caldavBackend;
$this->calendarInfo = $calendarInfo;
}
/**
* Returns the name of the calendar.
*
* @return string
*/
public 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.
*/
public function propPatch(PropPatch $propPatch)
{
return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch);
}
/**
* Returns the list of properties.
*
* @param array $requestedProperties
*
* @return array
*/
public function getProperties($requestedProperties)
{
$response = [];
foreach ($this->calendarInfo as $propName => $propValue) {
if (!is_null($propValue) && '{' === $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
*/
public 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
*/
public 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
*/
public 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
*/
public 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
*/
public 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
*/
public 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.
*/
public 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
*/
public function setName($newName)
{
throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
}
/**
* Returns the last modification date as a unix timestamp.
*/
public 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
*/
public function getOwner()
{
return $this->calendarInfo['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
*/
public 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
*/
public 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;
}
/**
* 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.
*
* @return array
*/
public 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
*/
public 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
*/
public 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,356 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV;
use Sabre\DAV;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\MkCol;
use Sabre\DAVACL;
use Sabre\Uri;
/**
* 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
{
use DAVACL\ACLTrait;
/**
* CalDAV backend.
*
* @var Backend\BackendInterface
*/
protected $caldavBackend;
/**
* Principal information.
*
* @var array
*/
protected $principalInfo;
/**
* Constructor.
*
* @param array $principalInfo
*/
public function __construct(Backend\BackendInterface $caldavBackend, $principalInfo)
{
$this->caldavBackend = $caldavBackend;
$this->principalInfo = $principalInfo;
}
/**
* Returns the name of this object.
*
* @return string
*/
public function getName()
{
list(, $name) = Uri\split($this->principalInfo['uri']);
return $name;
}
/**
* Updates the name of this object.
*
* @param string $name
*/
public function setName($name)
{
throw new DAV\Exception\Forbidden();
}
/**
* Deletes this object.
*/
public function delete()
{
throw new DAV\Exception\Forbidden();
}
/**
* Returns the last modification date.
*
* @return int
*/
public function getLastModified()
{
return null;
}
/**
* Creates a new file under this object.
*
* This is currently not allowed
*
* @param string $filename
* @param resource $data
*/
public 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
*/
public 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
*/
public function getChild($name)
{
// Special nodes
if ('inbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) {
return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']);
}
if ('outbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) {
return new Schedule\Outbox($this->principalInfo['uri']);
}
if ('notifications' === $name && $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) {
return new SharedCalendar($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
*/
public function childExists($name)
{
try {
return (bool) $this->getChild($name);
} catch (NotFound $e) {
return false;
}
}
/**
* Returns a list of calendars.
*
* @return array
*/
public function getChildren()
{
$calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
$objs = [];
foreach ($calendars as $calendar) {
if ($this->caldavBackend instanceof Backend\SharingSupport) {
$objs[] = new SharedCalendar($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
*
* @throws DAV\Exception\InvalidResourceType
*/
public 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 of the calendar home.
*
* @return string
*/
public function getOwner()
{
return $this->principalInfo['uri'];
}
/**
* 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
*/
public 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,
],
];
}
/**
* 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 string|null
*/
public 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
*/
public function getCalendarObjectByUID($uid)
{
return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid);
}
}

View File

@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
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
{
use \Sabre\DAVACL\ACLTrait;
/**
* Sabre\CalDAV\Backend\BackendInterface.
*
* @var 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.
*/
public 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
*/
public function getName()
{
return $this->objectData['uri'];
}
/**
* Returns the ICalendar-formatted object.
*
* @return string
*/
public 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
*/
public 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.
*/
public function delete()
{
$this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']);
}
/**
* Returns the mime content-type.
*
* @return string
*/
public 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
*/
public 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
*/
public function getLastModified()
{
return $this->objectData['lastmodified'];
}
/**
* Returns the size of this object in bytes.
*
* @return int
*/
public 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
*/
public function getOwner()
{
return $this->calendarInfo['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
*/
public 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:}all',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
],
[
'privilege' => '{DAV:}all',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read',
'protected' => true,
],
];
}
}

View File

@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV;
use DateTime;
use Sabre\VObject;
/**
* 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
*
* @return bool
*/
public 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.
*
* @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.
*
* @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.
*
* @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
*
* @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 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 ('VEVENT' === $component->parent->name && $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 (null !== $expandedEvent->VALARM) {
foreach ($expandedEvent->VALARM as $expandedAlarm) {
$effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
if ($expandedAlarm->isInTimeRange($start, $end)) {
return true;
}
if ('DATE-TIME' === (string) $expandedAlarm->TRIGGER['VALUE']) {
// 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);
}
// no break
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,75 @@
<?php
declare(strict_types=1);
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 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 string $principalPrefix
*/
public 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
*/
public 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.
*
* @return \Sabre\DAV\INode
*/
public function getChildForPrincipal(array $principal)
{
return new CalendarHome($this->caldavBackend, $principal);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Exception;
use Sabre\CalDAV;
use Sabre\DAV;
/**
* 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
*/
public 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,377 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV;
use DateTime;
use DateTimeZone;
use Sabre\DAV;
use Sabre\DAV\Exception\BadRequest;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject;
/**
* 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
*/
public 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.
*
* @throws BadRequest
* @throws DAV\Exception\NotFound
* @throws VObject\InvalidDataException
*
* @return bool
*/
public 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']) && (bool) $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\negotiateContentType(
$request->getHeader('Accept'),
[
'text/calendar',
'application/calendar+json',
]
);
if (isset($queryParams['accept'])) {
if ('application/calendar+json' === $queryParams['accept'] || 'jcal' === $queryParams['accept']) {
$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
*
* @throws DAV\Exception\NotFound
* @throws VObject\InvalidDataException
*/
protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response)
{
$calDataProp = '{'.Plugin::NS_CALDAV.'}calendar-data';
$calendarNode = $this->server->tree->getNodeForPath($path);
$blobs = [];
if ($start || $end || $componentType) {
// If there was a start or end filter, we need to enlist
// calendarQuery for speed.
$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();
// Destroy circular references to PHP will GC the object.
$vtimezoneObj->destroy();
unset($vtimezoneObj);
} else {
// Defaulting to UTC.
$calendarTimeZone = new DateTimeZone('UTC');
}
$mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone);
}
$filenameExtension = '.ics';
switch ($format) {
case 'text/calendar':
$mergedCalendar = $mergedCalendar->serialize();
$filenameExtension = '.ics';
break;
case 'application/calendar+json':
$mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
$filenameExtension = '.json';
break;
}
$filename = preg_replace(
'/[^a-zA-Z0-9-_ ]/um',
'',
$calendarNode->getName()
);
$filename .= '-'.date('Y-m-d').$filenameExtension;
$response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
$response->setHeader('Content-Type', $format);
$response->setStatus(200);
$response->setBody($mergedCalendar);
}
/**
* Merges all calendar objects, and builds one big iCalendar blob.
*
* @param array $properties Some CalDAV properties
*
* @return VObject\Component\VCalendar
*/
public 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[] = clone $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)) {
break;
}
$timezones[] = clone $child;
$collectedTimezones[] = $child->TZID;
break;
}
}
// Destroy circular references to PHP will GC the object.
$nodeComp->destroy();
unset($nodeComp);
}
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
*/
public 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
*/
public 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,20 @@
<?php
declare(strict_types=1);
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,23 @@
<?php
declare(strict_types=1);
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
declare(strict_types=1);
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.
*
* @return array
*/
public function calendarQuery(array $filters);
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV;
use Sabre\DAV\Sharing\ISharedNode;
/**
* This interface represents a 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
*/
interface ISharedCalendar extends ISharedNode
{
/**
* Marks this calendar as published.
*
* Publishing a calendar should automatically create a read-only, public,
* subscribable calendar.
*
* @param bool $value
*/
public function setPublishStatus($value);
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Notifications;
use Sabre\CalDAV;
use Sabre\DAV;
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
{
use DAVACL\ACLTrait;
/**
* The notification backend.
*
* @var CalDAV\Backend\NotificationSupport
*/
protected $caldavBackend;
/**
* Principal uri.
*
* @var string
*/
protected $principalUri;
/**
* Constructor.
*
* @param string $principalUri
*/
public function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri)
{
$this->caldavBackend = $caldavBackend;
$this->principalUri = $principalUri;
}
/**
* Returns all notifications for a principal.
*
* @return array
*/
public 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
*/
public 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
*/
public function getOwner()
{
return $this->principalUri;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Notifications;
use Sabre\DAV;
/**
* 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
*/
interface ICollection extends DAV\ICollection
{
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Notifications;
use Sabre\CalDAV\Xml\Notification\NotificationInterface;
/**
* 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.
*
* For a complete example, check out the Notification class, which contains
* some helper functions.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface INode
{
/**
* This method must return an xml element, using the
* Sabre\CalDAV\Xml\Notification\NotificationInterface classes.
*
* @return NotificationInterface
*/
public function getNotificationType();
/**
* Returns the etag for the notification.
*
* The etag must be surrounded by literal double-quotes.
*
* @return string
*/
public function getETag();
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Notifications;
use Sabre\CalDAV;
use Sabre\CalDAV\Xml\Notification\NotificationInterface;
use Sabre\DAV;
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
{
use DAVACL\ACLTrait;
/**
* The notification backend.
*
* @var CalDAV\Backend\NotificationSupport
*/
protected $caldavBackend;
/**
* The actual notification.
*
* @var NotificationInterface
*/
protected $notification;
/**
* Owner principal of the notification.
*
* @var string
*/
protected $principalUri;
/**
* Constructor.
*
* @param string $principalUri
*/
public 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 string
*/
public function getName()
{
return $this->notification->getId().'.xml';
}
/**
* Returns the etag for the notification.
*
* The etag must be surrounded by litteral double-quotes.
*
* @return string
*/
public function getETag()
{
return $this->notification->getETag();
}
/**
* This method must return an xml element, using the
* Sabre\CalDAV\Xml\Notification\NotificationInterface classes.
*
* @return NotificationInterface
*/
public function getNotificationType()
{
return $this->notification;
}
/**
* Deletes this notification.
*/
public 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
*/
public function getOwner()
{
return $this->principalUri;
}
}

View File

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Notifications;
use Sabre\DAV;
use Sabre\DAV\INode as BaseINode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
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
*/
public 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.
*/
public 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.
*/
public 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.
*/
public 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
*/
public 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/',
];
}
}

1011
vendor/sabre/dav/lib/CalDAV/Plugin.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
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.
*
* @return User
*/
public function getChildForPrincipal(array $principalInfo)
{
return new User($this->principalBackend, $principalInfo);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Principal;
use Sabre\DAVACL;
/**
* ProxyRead principal interface.
*
* Any principal node implementing this interface will be picked up as a 'proxy
* principal group'.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface IProxyRead extends DAVACL\IPrincipal
{
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Principal;
use Sabre\DAVACL;
/**
* ProxyWrite principal interface.
*
* Any principal node implementing this interface will be picked up as a 'proxy
* principal group'.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface IProxyWrite extends DAVACL\IPrincipal
{
}

View File

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Principal;
use Sabre\DAV;
use Sabre\DAVACL;
/**
* 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.
*/
public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo)
{
$this->principalInfo = $principalInfo;
$this->principalBackend = $principalBackend;
}
/**
* Returns this principals name.
*
* @return string
*/
public function getName()
{
return 'calendar-proxy-read';
}
/**
* Returns the last modification time.
*/
public function getLastModified()
{
return null;
}
/**
* Deletes the current node.
*
* @throws DAV\Exception\Forbidden
*/
public function delete()
{
throw new DAV\Exception\Forbidden('Permission denied to delete node');
}
/**
* Renames the node.
*
* @param string $name The new name
*
* @throws DAV\Exception\Forbidden
*/
public 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
*/
public function getAlternateUriSet()
{
return [];
}
/**
* Returns the full principal url.
*
* @return string
*/
public 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
*/
public 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
*/
public 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.
*/
public 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
*/
public function getDisplayName()
{
return $this->getName();
}
}

View File

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Principal;
use Sabre\DAV;
use Sabre\DAVACL;
/**
* 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.
*/
public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo)
{
$this->principalInfo = $principalInfo;
$this->principalBackend = $principalBackend;
}
/**
* Returns this principals name.
*
* @return string
*/
public function getName()
{
return 'calendar-proxy-write';
}
/**
* Returns the last modification time.
*/
public function getLastModified()
{
return null;
}
/**
* Deletes the current node.
*
* @throws DAV\Exception\Forbidden
*/
public function delete()
{
throw new DAV\Exception\Forbidden('Permission denied to delete node');
}
/**
* Renames the node.
*
* @param string $name The new name
*
* @throws DAV\Exception\Forbidden
*/
public 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
*/
public function getAlternateUriSet()
{
return [];
}
/**
* Returns the full principal url.
*
* @return string
*/
public 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
*/
public 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
*/
public 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.
*/
public 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
*/
public function getDisplayName()
{
return $this->getName();
}
}

View File

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
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
*/
public 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
*/
public 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
*/
public 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 ('calendar-proxy-read' === $name) {
return new ProxyRead($this->principalBackend, $this->principalProperties);
}
if ('calendar-proxy-write' === $name) {
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[]
*/
public 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
*/
public 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
*/
public 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,17 @@
<?php
declare(strict_types=1);
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,185 @@
<?php
declare(strict_types=1);
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.
*/
public 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
*/
public 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
*/
public function getPluginName()
{
return 'imip';
}
/**
* Event handler for the 'schedule' event.
*/
public 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 ('mailto' !== parse_url($iTipMessage->sender, PHP_URL_SCHEME)) {
return;
}
if ('mailto' !== parse_url($iTipMessage->recipient, PHP_URL_SCHEME)) {
return;
}
$sender = substr($iTipMessage->sender, 7);
$recipient = substr($iTipMessage->recipient, 7);
if ($iTipMessage->senderName) {
$sender = $iTipMessage->senderName.' <'.$sender.'>';
}
if ($iTipMessage->recipientName && $iTipMessage->recipientName != $recipient) {
$recipient = $iTipMessage->recipientName.' <'.$recipient.'>';
}
$subject = 'SabreDAV iTIP message';
switch (strtoupper($iTipMessage->method)) {
case 'REPLY':
$subject = 'Re: '.$summary;
break;
case 'REQUEST':
$subject = 'Invitation: '.$summary;
break;
case 'CANCEL':
$subject = 'Cancelled: '.$summary;
break;
}
$headers = [
'Reply-To: '.$sender,
'From: '.$iTipMessage->senderName.' <'.$this->senderEmail.'>',
'MIME-Version: 1.0',
'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
*/
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
*/
public function getPluginInfo()
{
return [
'name' => $this->getPluginName(),
'description' => 'Email delivery (rfc6047) for CalDAV scheduling',
'link' => 'http://sabre.io/dav/scheduling/',
];
}
}

View File

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

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
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,198 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Schedule;
use Sabre\CalDAV;
use Sabre\CalDAV\Backend;
use Sabre\DAV;
use Sabre\DAVACL;
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
{
use DAVACL\ACLTrait;
/**
* CalDAV backend.
*
* @var Backend\BackendInterface
*/
protected $caldavBackend;
/**
* The principal Uri.
*
* @var string
*/
protected $principalUri;
/**
* Constructor.
*
* @param string $principalUri
*/
public 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
*/
public function getName()
{
return 'inbox';
}
/**
* Returns an array with all the child nodes.
*
* @return \Sabre\DAV\INode[]
*/
public 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 successful 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 string|null
*/
public 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
*/
public function getOwner()
{
return $this->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
*/
public function getACL()
{
return [
[
'privilege' => '{DAV:}read',
'principal' => '{DAV:}authenticated',
'protected' => true,
],
[
'privilege' => '{DAV:}write-properties',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}unbind',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}unbind',
'principal' => $this->getOwner().'/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-deliver',
'principal' => '{DAV:}authenticated',
'protected' => true,
],
];
}
/**
* 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.
*
* @return array
*/
public 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'];
}
// Destroy circular references to PHP will GC the object.
$vObject->destroy();
}
return $result;
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Schedule;
use Sabre\CalDAV;
use Sabre\DAV;
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
{
use DAVACL\ACLTrait;
/**
* The principal Uri.
*
* @var string
*/
protected $principalUri;
/**
* Constructor.
*
* @param string $principalUri
*/
public function __construct($principalUri)
{
$this->principalUri = $principalUri;
}
/**
* Returns the name of the node.
*
* This is used to generate the url.
*
* @return string
*/
public function getName()
{
return 'outbox';
}
/**
* Returns an array with all the child nodes.
*
* @return \Sabre\DAV\INode[]
*/
public 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
*/
public function getOwner()
{
return $this->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
*/
public function getACL()
{
return [
[
'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send',
'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,
],
];
}
}

View File

@ -0,0 +1,999 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Schedule;
use DateTimeZone;
use Sabre\CalDAV\ICalendar;
use Sabre\CalDAV\ICalendarObject;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Sharing;
use Sabre\DAV\Xml\Property\LocalHref;
use Sabre\DAVACL;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\ITip;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Reader;
/**
* 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
*/
public function getFeatures()
{
return ['calendar-auto-schedule', 'calendar-availability'];
}
/**
* Returns the name of the plugin.
*
* Using this name other plugins will be able to access other plugins
* using Server::getPlugin
*
* @return string
*/
public function getPluginName()
{
return 'caldav-schedule';
}
/**
* Initializes the plugin.
*/
public function initialize(Server $server)
{
$this->server = $server;
$server->on('method:POST', [$this, 'httpPost']);
$server->on('propFind', [$this, 'propFind']);
$server->on('propPatch', [$this, 'propPatch']);
$server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
$server->on('beforeUnbind', [$this, 'beforeUnbind']);
$server->on('schedule', [$this, 'scheduleLocalDelivery']);
$server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
$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
*/
public 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.
*
* @return bool
*/
public function httpPost(RequestInterface $request, ResponseInterface $response)
{
// Checking if this is a text/calendar content type
$contentType = $request->getHeader('Content-Type');
if (!$contentType || 0 !== strpos($contentType, 'text/calendar')) {
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.
*/
public function propFind(PropFind $propFind, INode $node)
{
if ($node instanceof DAVACL\IPrincipal) {
$caldavPlugin = $this->server->getPlugin('caldav');
$principalUrl = $node->getPrincipalUrl();
// schedule-outbox-URL property
$propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function () use ($principalUrl, $caldavPlugin) {
$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
if (!$calendarHomePath) {
return null;
}
$outboxPath = $calendarHomePath.'/outbox/';
return new LocalHref($outboxPath);
});
// schedule-inbox-URL property
$propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function () use ($principalUrl, $caldavPlugin) {
$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
if (!$calendarHomePath) {
return null;
}
$inboxPath = $calendarHomePath.'/inbox/';
return new LocalHref($inboxPath);
});
$propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function () use ($principalUrl, $caldavPlugin) {
// We don't support customizing this property yet, so in the
// meantime we just grab the first calendar in the home-set.
$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
if (!$calendarHomePath) {
return null;
}
$sccs = '{'.self::NS_CALDAV.'}supported-calendar-component-set';
$result = $this->server->getPropertiesForPath($calendarHomePath, [
'{DAV:}resourcetype',
'{DAV:}share-access',
$sccs,
], 1);
foreach ($result as $child) {
if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{'.self::NS_CALDAV.'}calendar')) {
// Node is either not a calendar
continue;
}
if (isset($child[200]['{DAV:}share-access'])) {
$shareAccess = $child[200]['{DAV:}share-access']->getValue();
if (Sharing\Plugin::ACCESS_NOTSHARED !== $shareAccess && Sharing\Plugin::ACCESS_SHAREDOWNER !== $shareAccess) {
// Node is a shared node, not owned by the relevant
// user.
continue;
}
}
if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
// Either there is no supported-calendar-component-set
// (which is fine) or we found one that supports VEVENT.
return new LocalHref($child['href']);
}
}
});
// The server currently reports every principal to be of type
// 'INDIVIDUAL'
$propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function () {
return 'INDIVIDUAL';
});
}
// Mapping the old property to the new property.
$propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use ($propFind, $node) {
// In case it wasn't clear, the only difference is that we map the
// old property to a different namespace.
$availProp = '{'.self::NS_CALDAV.'}calendar-availability';
$subPropFind = new PropFind(
$propFind->getPath(),
[$availProp]
);
$this->server->getPropertiesByNode(
$subPropFind,
$node
);
$propFind->set(
'{http://calendarserver.org/ns/}calendar-availability',
$subPropFind->get($availProp),
$subPropFind->getStatus($availProp)
);
});
}
/**
* This method is called during property updates.
*
* @param string $path
*/
public function propPatch($path, PropPatch $propPatch)
{
// Mapping the old property to the new property.
$propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function ($value) use ($path) {
$availProp = '{'.self::NS_CALDAV.'}calendar-availability';
$subPropPatch = new PropPatch([$availProp => $value]);
$this->server->emit('propPatch', [$path, $subPropPatch]);
$subPropPatch->commit();
return $subPropPatch->getResult()[$availProp];
});
}
/**
* 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
*/
public 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);
if ($oldObj) {
// Destroy circular references so PHP will GC the object.
$oldObj->destroy();
}
}
/**
* This method is responsible for delivering the ITip message.
*/
public 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
*/
public function beforeUnbind($path)
{
// FIXME: We shouldn't trigger this functionality when we're issuing a
// MOVE. This is a hack.
if ('MOVE' === $this->server->httpRequest->getMethod()) {
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.
*/
public 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 ('REPLY' === $iTipMessage->method) {
$privilege = 'schedule-deliver-reply';
} else {
$privilege = 'schedule-deliver-invite';
}
if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS.$privilege, DAVACL\Plugin::R_PARENT, false)) {
$iTipMessage->scheduleStatus = '3.8;insufficient privileges: '.$privilege.' is required on the recipient schedule 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 ('REPLY' === $iTipMessage->method) {
$this->processICalendarChange(
$oldICalendarData,
$newObject,
[$iTipMessage->recipient],
[$iTipMessage->sender]
);
}
$objectNode->put($newObject->serialize());
}
$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
}
/**
* This method is triggered whenever a subsystem requests the privileges
* that are supported on a particular node.
*
* We need to add a number of privileges for scheduling purposes.
*/
public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
{
$ns = '{'.self::NS_CALDAV.'}';
if ($node instanceof IOutbox) {
$supportedPrivilegeSet[$ns.'schedule-send'] = [
'abstract' => false,
'aggregates' => [
$ns.'schedule-send-invite' => [
'abstract' => false,
'aggregates' => [],
],
$ns.'schedule-send-reply' => [
'abstract' => false,
'aggregates' => [],
],
$ns.'schedule-send-freebusy' => [
'abstract' => false,
'aggregates' => [],
],
// Privilege from an earlier scheduling draft, but still
// used by some clients.
$ns.'schedule-post-vevent' => [
'abstract' => false,
'aggregates' => [],
],
],
];
}
if ($node instanceof IInbox) {
$supportedPrivilegeSet[$ns.'schedule-deliver'] = [
'abstract' => false,
'aggregates' => [
$ns.'schedule-deliver-invite' => [
'abstract' => false,
'aggregates' => [],
],
$ns.'schedule-deliver-reply' => [
'abstract' => false,
'aggregates' => [],
],
$ns.'schedule-query-freebusy' => [
'abstract' => false,
'aggregates' => [],
],
],
];
}
}
/**
* 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 array $ignore any addresses to not send messages to
* @param bool $modified a marker to indicate that the original object
* modified by this process
*/
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 supported:
* * 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.
*/
public 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 ('VTIMEZONE' !== $component->name) {
$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 ('VFREEBUSY' === $componentType && 'REQUEST' === $method) {
$acl && $acl->checkPrivileges($outboxPath, '{'.self::NS_CALDAV.'}schedule-send-freebusy');
$this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
// Destroy circular references so PHP can GC the object.
$vObject->destroy();
unset($vObject);
} 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.
*
* @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
*
* @return array
*/
protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request)
{
$caldavNS = '{'.self::NS_CALDAV.'}';
$aclPlugin = $this->server->getPlugin('acl');
if ('mailto:' === substr($email, 0, 7)) {
$email = substr($email, 7);
}
$result = $aclPlugin->principalSearch(
['{http://sabredav.org/ns}email-address' => $email],
[
'{DAV:}principal-URL',
$caldavNS.'calendar-home-set',
$caldavNS.'schedule-inbox-URL',
'{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,
];
}
if (!isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) {
return [
'request-status' => '3.7;No schedule-inbox-URL property found',
'href' => 'mailto:'.$email,
];
}
$homeSet = $result[0][200][$caldavNS.'calendar-home-set']->getHref();
$inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref();
// Do we have permission?
$aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy');
// 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]) && ScheduleCalendarTransp::TRANSPARENT == $props[$sct]->getValue()) {
// If a calendar is marked as 'transparent', it means we must
// ignore it for free-busy purposes.
continue;
}
if (isset($props[$ctz])) {
$vtimezoneObj = VObject\Reader::read($props[$ctz]);
$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
// Destroy circular references so PHP can garbage collect the object.
$vtimezoneObj->destroy();
}
// 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);
}
$inboxProps = $this->server->getProperties(
$inboxUrl,
$caldavNS.'calendar-availability'
);
$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);
if ($inboxProps) {
$generator->setVAvailability(
VObject\Reader::read(
$inboxProps[$caldavNS.'calendar-availability']
)
);
}
$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.
*
* @return bool
*/
private function scheduleReply(RequestInterface $request)
{
$scheduleReply = $request->getHeader('Schedule-Reply');
return 'F' !== $scheduleReply;
}
/**
* 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
*/
public function getPluginInfo()
{
return [
'name' => $this->getPluginName(),
'description' => 'Adds calendar-auto-schedule, as defined in rfc6638',
'link' => 'http://sabre.io/dav/scheduling/',
];
}
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
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
{
/**
* 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.
*/
public function __construct(Backend\SchedulingSupport $caldavBackend, array $objectData)
{
parent::__construct($caldavBackend, [], $objectData);
if (!isset($objectData['uri'])) {
throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
}
}
/**
* Returns the ICalendar-formatted object.
*
* @return string
*/
public 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
*/
public function put($calendarData)
{
throw new MethodNotAllowed('Updating scheduling objects is not supported');
}
/**
* Deletes the scheduling message.
*/
public 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
*/
public 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
*/
public 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:}all',
'principal' => '{DAV:}owner',
'protected' => true,
],
[
'privilege' => '{DAV:}all',
'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,219 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV;
use Sabre\DAV\Sharing\Plugin as SPlugin;
/**
* 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
{
/**
* Returns the 'access level' for the instance of this shared resource.
*
* The value should be one of the Sabre\DAV\Sharing\Plugin::ACCESS_
* constants.
*
* @return int
*/
public function getShareAccess()
{
return isset($this->calendarInfo['share-access']) ? $this->calendarInfo['share-access'] : SPlugin::ACCESS_NOTSHARED;
}
/**
* This function must return a URI that uniquely identifies the shared
* resource. This URI should be identical across instances, and is
* also used in several other XML bodies to connect invites to
* resources.
*
* This may simply be a relative reference to the original shared instance,
* but it could also be a urn. As long as it's a valid URI and unique.
*
* @return string
*/
public function getShareResourceUri()
{
return $this->calendarInfo['share-resource-uri'];
}
/**
* Updates the list of sharees.
*
* Every item must be a Sharee object.
*
* @param \Sabre\DAV\Xml\Element\Sharee[] $sharees
*/
public function updateInvites(array $sharees)
{
$this->caldavBackend->updateInvites($this->calendarInfo['id'], $sharees);
}
/**
* Returns the list of people whom this resource is shared with.
*
* Every item in the returned array must be a Sharee object with
* at least the following properties set:
*
* * $href
* * $shareAccess
* * $inviteStatus
*
* and optionally:
*
* * $properties
*
* @return \Sabre\DAV\Xml\Element\Sharee[]
*/
public function getInvites()
{
return $this->caldavBackend->getInvites($this->calendarInfo['id']);
}
/**
* Marks this calendar as published.
*
* Publishing a calendar should automatically create a read-only, public,
* subscribable calendar.
*
* @param bool $value
*/
public function setPublishStatus($value)
{
$this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value);
}
/**
* 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
*/
public function getACL()
{
$acl = [];
switch ($this->getShareAccess()) {
case SPlugin::ACCESS_NOTSHARED:
case SPlugin::ACCESS_SHAREDOWNER:
$acl[] = [
'privilege' => '{DAV:}share',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}share',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
'protected' => true,
];
// no break intentional!
case SPlugin::ACCESS_READWRITE:
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
'protected' => true,
];
// no break intentional!
case SPlugin::ACCESS_READ:
$acl[] = [
'privilege' => '{DAV:}write-properties',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}write-properties',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read',
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
'protected' => true,
];
$acl[] = [
'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy',
'principal' => '{DAV:}authenticated',
'protected' => true,
];
break;
}
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
*/
public function getChildACL()
{
$acl = [];
switch ($this->getShareAccess()) {
case SPlugin::ACCESS_NOTSHARED:
case SPlugin::ACCESS_SHAREDOWNER:
case SPlugin::ACCESS_READWRITE:
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
'protected' => true,
];
// no break intentional
case SPlugin::ACCESS_READ:
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'],
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write',
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read',
'protected' => true,
];
break;
}
return $acl;
}
}

View File

@ -0,0 +1,350 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV;
use Sabre\DAV;
use Sabre\DAV\Xml\Property\LocalHref;
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
{
/**
* Reference to SabreDAV server object.
*
* @var 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
*/
public 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
*/
public 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.
*/
public function initialize(DAV\Server $server)
{
$this->server = $server;
if (is_null($this->server->getPlugin('sharing'))) {
throw new \LogicException('The generic "sharing" plugin must be loaded before the caldav sharing plugin. Call $server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); before this one.');
}
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.
*/
public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
{
if ($node instanceof ISharedCalendar) {
$propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}invite', function () use ($node) {
return new Xml\Property\Invite(
$node->getInvites()
);
});
}
}
/**
* This method is triggered *after* all properties have been retrieved.
* This allows us to inject the correct resourcetype for calendars that
* have been shared.
*/
public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
{
if ($node instanceof ISharedCalendar) {
$shareAccess = $node->getShareAccess();
if ($rt = $propFind->get('{DAV:}resourcetype')) {
switch ($shareAccess) {
case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER:
$rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared-owner');
break;
case \Sabre\DAV\Sharing\Plugin::ACCESS_READ:
case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE:
$rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared');
break;
}
}
$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
*/
public function propPatch($path, DAV\PropPatch $propPatch)
{
$node = $this->server->tree->getNodeForPath($path);
if (!$node instanceof ISharedCalendar) {
return;
}
if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $node->getShareAccess() || \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED === $node->getShareAccess()) {
$propPatch->handle('{DAV:}resourcetype', function ($value) use ($node) {
if ($value->is('{'.Plugin::NS_CALENDARSERVER.'}shared-owner')) {
return false;
}
$shares = $node->getInvites();
foreach ($shares as $share) {
$share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS;
}
$node->updateInvites($shares);
return true;
});
}
}
/**
* We intercept this to handle POST requests on calendars.
*
* @return bool|null
*/
public function httpPost(RequestInterface $request, ResponseInterface $response)
{
$path = $request->getPath();
// Only handling xml
$contentType = $request->getHeader('Content-Type');
if (null === $contentType) {
return;
}
if (false === strpos($contentType, 'application/xml') && false === strpos($contentType, 'text/xml')) {
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) {
// Both the DAV:share-resource and CALENDARSERVER:share requests
// behave identically.
case '{'.Plugin::NS_CALENDARSERVER.'}share':
$sharingPlugin = $this->server->getPlugin('sharing');
$sharingPlugin->shareResource($path, $message->sharees);
$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();
$writer->contextUri = $request->getUrl();
$writer->openMemory();
$writer->startDocument();
$writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}shared-as');
$writer->write(new LocalHref($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 ISharedCalendar) {
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:}share');
}
$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 ISharedCalendar) {
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:}share');
}
$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
*/
public function getPluginInfo()
{
return [
'name' => $this->getPluginName(),
'description' => 'Adds support for caldav-sharing.',
'link' => 'http://sabre.io/dav/caldav-sharing/',
];
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Subscriptions;
use Sabre\DAV\ICollection;
use Sabre\DAV\IProperties;
/**
* ISubscription.
*
* Nodes implementing this interface represent calendar subscriptions.
*
* The subscription node doesn't do much, other than returning and updating
* subscription-related properties.
*
* The following properties should be supported:
*
* 1. {DAV:}displayname
* 2. {http://apple.com/ns/ical/}refreshrate
* 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
* should not be stripped).
* 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
* should not be stripped).
* 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
* attachments should not be stripped).
* 6. {http://calendarserver.org/ns/}source (Must be a
* Sabre\DAV\Property\Href).
* 7. {http://apple.com/ns/ical/}calendar-color
* 8. {http://apple.com/ns/ical/}calendar-order
*
* It is recommended to support every property.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface ISubscription extends ICollection, IProperties
{
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Subscriptions;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
/**
* This plugin adds calendar-subscription support to your CalDAV server.
*
* Some clients support 'managed subscriptions' server-side. This is basically
* a list of subscription urls a user is using.
*
* @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 initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*/
public function initialize(Server $server)
{
$server->resourceTypeMapping['Sabre\\CalDAV\\Subscriptions\\ISubscription'] =
'{http://calendarserver.org/ns/}subscribed';
$server->xml->elementMap['{http://calendarserver.org/ns/}source'] =
'Sabre\\DAV\\Xml\\Property\\Href';
$server->on('propFind', [$this, 'propFind'], 150);
}
/**
* 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
*/
public function getFeatures()
{
return ['calendarserver-subscribed'];
}
/**
* Triggered after properties have been fetched.
*/
public function propFind(PropFind $propFind, INode $node)
{
// There's a bunch of properties that must appear as a self-closing
// xml-element. This event handler ensures that this will be the case.
$props = [
'{http://calendarserver.org/ns/}subscribed-strip-alarms',
'{http://calendarserver.org/ns/}subscribed-strip-attachments',
'{http://calendarserver.org/ns/}subscribed-strip-todos',
];
foreach ($props as $prop) {
if (200 === $propFind->getStatus($prop)) {
$propFind->set($prop, '', 200);
}
}
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using \Sabre\DAV\Server::getPlugin
*
* @return string
*/
public function getPluginName()
{
return 'subscriptions';
}
/**
* 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
*/
public function getPluginInfo()
{
return [
'name' => $this->getPluginName(),
'description' => 'This plugin allows users to store iCalendar subscriptions in their calendar-home.',
'link' => null,
];
}
}

View File

@ -0,0 +1,204 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Subscriptions;
use Sabre\CalDAV\Backend\SubscriptionSupport;
use Sabre\DAV\Collection;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Xml\Property\Href;
use Sabre\DAVACL\ACLTrait;
use Sabre\DAVACL\IACL;
/**
* Subscription Node.
*
* This node represents a subscription.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Subscription extends Collection implements ISubscription, IACL
{
use ACLTrait;
/**
* caldavBackend.
*
* @var SubscriptionSupport
*/
protected $caldavBackend;
/**
* subscriptionInfo.
*
* @var array
*/
protected $subscriptionInfo;
/**
* Constructor.
*/
public function __construct(SubscriptionSupport $caldavBackend, array $subscriptionInfo)
{
$this->caldavBackend = $caldavBackend;
$this->subscriptionInfo = $subscriptionInfo;
$required = [
'id',
'uri',
'principaluri',
'source',
];
foreach ($required as $r) {
if (!isset($subscriptionInfo[$r])) {
throw new \InvalidArgumentException('The '.$r.' field is required when creating a subscription node');
}
}
}
/**
* Returns the name of the node.
*
* This is used to generate the url.
*
* @return string
*/
public function getName()
{
return $this->subscriptionInfo['uri'];
}
/**
* Returns the last modification time.
*
* @return int
*/
public function getLastModified()
{
if (isset($this->subscriptionInfo['lastmodified'])) {
return $this->subscriptionInfo['lastmodified'];
}
}
/**
* Deletes the current node.
*/
public function delete()
{
$this->caldavBackend->deleteSubscription(
$this->subscriptionInfo['id']
);
}
/**
* Returns an array with all the child nodes.
*
* @return \Sabre\DAV\INode[]
*/
public function getChildren()
{
return [];
}
/**
* 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.
*/
public function propPatch(PropPatch $propPatch)
{
return $this->caldavBackend->updateSubscription(
$this->subscriptionInfo['id'],
$propPatch
);
}
/**
* Returns a list of properties for this nodes.
*
* The properties list is a list of propertynames the client requested,
* encoded in clark-notation {xmlnamespace}tagname.
*
* If the array is empty, it means 'all properties' were requested.
*
* Note that it's fine to liberally give properties back, instead of
* conforming to the list of requested properties.
* The Server class will filter out the extra.
*
* @param array $properties
*
* @return array
*/
public function getProperties($properties)
{
$r = [];
foreach ($properties as $prop) {
switch ($prop) {
case '{http://calendarserver.org/ns/}source':
$r[$prop] = new Href($this->subscriptionInfo['source']);
break;
default:
if (array_key_exists($prop, $this->subscriptionInfo)) {
$r[$prop] = $this->subscriptionInfo[$prop];
}
break;
}
}
return $r;
}
/**
* Returns the owner principal.
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
public function getOwner()
{
return $this->subscriptionInfo['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
*/
public function getACL()
{
return [
[
'privilege' => '{DAV:}all',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}all',
'principal' => $this->getOwner().'/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner().'/calendar-proxy-read',
'protected' => true,
],
];
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Filter;
use Sabre\CalDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\VObject\DateTimeParser;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* CalendarData parser.
*
* This class parses the {urn:ietf:params:xml:ns:caldav}calendar-data XML
* element, as defined in:
*
* https://tools.ietf.org/html/rfc4791#section-9.6
*
* This element is used in three distinct places in the caldav spec, but in
* this case, this element class only implements the calendar-data element as
* it appears in a DAV:prop element, in a calendar-query or calendar-multiget
* REPORT request.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalendarData implements XmlDeserializable
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = [
'contentType' => $reader->getAttribute('content-type') ?: 'text/calendar',
'version' => $reader->getAttribute('version') ?: '2.0',
];
$elems = (array) $reader->parseInnerTree();
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{'.Plugin::NS_CALDAV.'}expand':
$result['expand'] = [
'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null,
'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null,
];
if (!$result['expand']['start'] || !$result['expand']['end']) {
throw new BadRequest('The "start" and "end" attributes are required when expanding calendar-data');
}
if ($result['expand']['end'] <= $result['expand']['start']) {
throw new BadRequest('The end-date must be larger than the start-date when expanding calendar-data');
}
break;
}
}
return $result;
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Filter;
use Sabre\CalDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\VObject\DateTimeParser;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* CompFilter parser.
*
* This class parses the {urn:ietf:params:xml:ns:caldav}comp-filter XML
* element, as defined in:
*
* https://tools.ietf.org/html/rfc4791#section-9.6
*
* The result will be spit out as an array.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CompFilter implements XmlDeserializable
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = [
'name' => null,
'is-not-defined' => false,
'comp-filters' => [],
'prop-filters' => [],
'time-range' => false,
];
$att = $reader->parseAttributes();
$result['name'] = $att['name'];
$elems = $reader->parseInnerTree();
if (is_array($elems)) {
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{'.Plugin::NS_CALDAV.'}comp-filter':
$result['comp-filters'][] = $elem['value'];
break;
case '{'.Plugin::NS_CALDAV.'}prop-filter':
$result['prop-filters'][] = $elem['value'];
break;
case '{'.Plugin::NS_CALDAV.'}is-not-defined':
$result['is-not-defined'] = true;
break;
case '{'.Plugin::NS_CALDAV.'}time-range':
if ('VCALENDAR' === $result['name']) {
throw new BadRequest('You cannot add time-range filters on the VCALENDAR component');
}
$result['time-range'] = [
'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null,
'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null,
];
if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) {
throw new BadRequest('The end-date must be larger than the start-date');
}
break;
}
}
}
return $result;
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Filter;
use Sabre\CalDAV\Plugin;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* PropFilter parser.
*
* This class parses the {urn:ietf:params:xml:ns:caldav}param-filter XML
* element, as defined in:
*
* https://tools.ietf.org/html/rfc4791#section-9.7.3
*
* The result will be spit out as an array.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ParamFilter implements XmlDeserializable
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* Important note 2: You are responsible for advancing the reader to the
* next element. Not doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = [
'name' => null,
'is-not-defined' => false,
'text-match' => null,
];
$att = $reader->parseAttributes();
$result['name'] = $att['name'];
$elems = $reader->parseInnerTree();
if (is_array($elems)) {
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{'.Plugin::NS_CALDAV.'}is-not-defined':
$result['is-not-defined'] = true;
break;
case '{'.Plugin::NS_CALDAV.'}text-match':
$result['text-match'] = [
'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap',
'value' => $elem['value'],
];
break;
}
}
}
return $result;
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Filter;
use Sabre\CalDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\VObject\DateTimeParser;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* PropFilter parser.
*
* This class parses the {urn:ietf:params:xml:ns:caldav}prop-filter XML
* element, as defined in:
*
* https://tools.ietf.org/html/rfc4791#section-9.7.2
*
* The result will be spit out as an array.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class PropFilter implements XmlDeserializable
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = [
'name' => null,
'is-not-defined' => false,
'param-filters' => [],
'text-match' => null,
'time-range' => false,
];
$att = $reader->parseAttributes();
$result['name'] = $att['name'];
$elems = $reader->parseInnerTree();
if (is_array($elems)) {
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{'.Plugin::NS_CALDAV.'}param-filter':
$result['param-filters'][] = $elem['value'];
break;
case '{'.Plugin::NS_CALDAV.'}is-not-defined':
$result['is-not-defined'] = true;
break;
case '{'.Plugin::NS_CALDAV.'}time-range':
$result['time-range'] = [
'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null,
'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null,
];
if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) {
throw new BadRequest('The end-date must be larger than the start-date');
}
break;
case '{'.Plugin::NS_CALDAV.'}text-match':
$result['text-match'] = [
'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap',
'value' => $elem['value'],
];
break;
}
}
}
return $result;
}
}

View File

@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Notification;
use Sabre\CalDAV;
use Sabre\CalDAV\SharingPlugin as SharingPlugin;
use Sabre\DAV;
use Sabre\Xml\Writer;
/**
* This class represents the cs:invite-notification notification element.
*
* This element is defined here:
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Invite implements NotificationInterface
{
/**
* A unique id for the message.
*
* @var string
*/
protected $id;
/**
* Timestamp of the notification.
*
* @var \DateTime
*/
protected $dtStamp;
/**
* A url to the recipient of the notification. This can be an email
* address (mailto:), or a principal url.
*
* @var string
*/
protected $href;
/**
* The type of message, see the SharingPlugin::STATUS_* constants.
*
* @var int
*/
protected $type;
/**
* True if access to a calendar is read-only.
*
* @var bool
*/
protected $readOnly;
/**
* A url to the shared calendar.
*
* @var string
*/
protected $hostUrl;
/**
* Url to the sharer of the calendar.
*
* @var string
*/
protected $organizer;
/**
* The name of the sharer.
*
* @var string
*/
protected $commonName;
/**
* The name of the sharer.
*
* @var string
*/
protected $firstName;
/**
* The name of the sharer.
*
* @var string
*/
protected $lastName;
/**
* A description of the share request.
*
* @var string
*/
protected $summary;
/**
* The Etag for the notification.
*
* @var string
*/
protected $etag;
/**
* The list of supported components.
*
* @var CalDAV\Xml\Property\SupportedCalendarComponentSet
*/
protected $supportedComponents;
/**
* Creates the Invite notification.
*
* This constructor receives an array with the following elements:
*
* * id - A unique id
* * etag - The etag
* * dtStamp - A DateTime object with a timestamp for the notification.
* * type - The type of notification, see SharingPlugin::STATUS_*
* constants for details.
* * readOnly - This must be set to true, if this is an invite for
* read-only access to a calendar.
* * hostUrl - A url to the shared calendar.
* * organizer - Url to the sharer principal.
* * commonName - The real name of the sharer (optional).
* * firstName - The first name of the sharer (optional).
* * lastName - The last name of the sharer (optional).
* * summary - Description of the share, can be the same as the
* calendar, but may also be modified (optional).
* * supportedComponents - An instance of
* Sabre\CalDAV\Property\SupportedCalendarComponentSet.
* This allows the client to determine which components
* will be supported in the shared calendar. This is
* also optional.
*
* @param array $values All the options
*/
public function __construct(array $values)
{
$required = [
'id',
'etag',
'href',
'dtStamp',
'type',
'readOnly',
'hostUrl',
'organizer',
];
foreach ($required as $item) {
if (!isset($values[$item])) {
throw new \InvalidArgumentException($item.' is a required constructor option');
}
}
foreach ($values as $key => $value) {
if (!property_exists($this, $key)) {
throw new \InvalidArgumentException('Unknown option: '.$key);
}
$this->$key = $value;
}
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
$writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-notification');
}
/**
* This method serializes the entire notification, as it is used in the
* response body.
*/
public function xmlSerializeFull(Writer $writer)
{
$cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}';
$this->dtStamp->setTimezone(new \DateTimeZone('GMT'));
$writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z'));
$writer->startElement($cs.'invite-notification');
$writer->writeElement($cs.'uid', $this->id);
$writer->writeElement('{DAV:}href', $this->href);
switch ($this->type) {
case DAV\Sharing\Plugin::INVITE_ACCEPTED:
$writer->writeElement($cs.'invite-accepted');
break;
case DAV\Sharing\Plugin::INVITE_NORESPONSE:
$writer->writeElement($cs.'invite-noresponse');
break;
}
$writer->writeElement($cs.'hosturl', [
'{DAV:}href' => $writer->contextUri.$this->hostUrl,
]);
if ($this->summary) {
$writer->writeElement($cs.'summary', $this->summary);
}
$writer->startElement($cs.'access');
if ($this->readOnly) {
$writer->writeElement($cs.'read');
} else {
$writer->writeElement($cs.'read-write');
}
$writer->endElement(); // access
$writer->startElement($cs.'organizer');
// If the organizer contains a 'mailto:' part, it means it should be
// treated as absolute.
if ('mailto:' === strtolower(substr($this->organizer, 0, 7))) {
$writer->writeElement('{DAV:}href', $this->organizer);
} else {
$writer->writeElement('{DAV:}href', $writer->contextUri.$this->organizer);
}
if ($this->commonName) {
$writer->writeElement($cs.'common-name', $this->commonName);
}
if ($this->firstName) {
$writer->writeElement($cs.'first-name', $this->firstName);
}
if ($this->lastName) {
$writer->writeElement($cs.'last-name', $this->lastName);
}
$writer->endElement(); // organizer
if ($this->commonName) {
$writer->writeElement($cs.'organizer-cn', $this->commonName);
}
if ($this->firstName) {
$writer->writeElement($cs.'organizer-first', $this->firstName);
}
if ($this->lastName) {
$writer->writeElement($cs.'organizer-last', $this->lastName);
}
if ($this->supportedComponents) {
$writer->writeElement('{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set', $this->supportedComponents);
}
$writer->endElement(); // invite-notification
}
/**
* Returns a unique id for this notification.
*
* This is just the base url. This should generally be some kind of unique
* id.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Returns the ETag for this notification.
*
* The ETag must be surrounded by literal double-quotes.
*
* @return string
*/
public function getETag()
{
return $this->etag;
}
}

View File

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Notification;
use Sabre\CalDAV;
use Sabre\CalDAV\SharingPlugin;
use Sabre\DAV;
use Sabre\Xml\Writer;
/**
* This class represents the cs:invite-reply notification element.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class InviteReply implements NotificationInterface
{
/**
* A unique id for the message.
*
* @var string
*/
protected $id;
/**
* Timestamp of the notification.
*
* @var \DateTime
*/
protected $dtStamp;
/**
* The unique id of the notification this was a reply to.
*
* @var string
*/
protected $inReplyTo;
/**
* A url to the recipient of the original (!) notification.
*
* @var string
*/
protected $href;
/**
* The type of message, see the SharingPlugin::STATUS_ constants.
*
* @var int
*/
protected $type;
/**
* A url to the shared calendar.
*
* @var string
*/
protected $hostUrl;
/**
* A description of the share request.
*
* @var string
*/
protected $summary;
/**
* Notification Etag.
*
* @var string
*/
protected $etag;
/**
* Creates the Invite Reply Notification.
*
* This constructor receives an array with the following elements:
*
* * id - A unique id
* * etag - The etag
* * dtStamp - A DateTime object with a timestamp for the notification.
* * inReplyTo - This should refer to the 'id' of the notification
* this is a reply to.
* * type - The type of notification, see SharingPlugin::STATUS_*
* constants for details.
* * hostUrl - A url to the shared calendar.
* * summary - Description of the share, can be the same as the
* calendar, but may also be modified (optional).
*/
public function __construct(array $values)
{
$required = [
'id',
'etag',
'href',
'dtStamp',
'inReplyTo',
'type',
'hostUrl',
];
foreach ($required as $item) {
if (!isset($values[$item])) {
throw new \InvalidArgumentException($item.' is a required constructor option');
}
}
foreach ($values as $key => $value) {
if (!property_exists($this, $key)) {
throw new \InvalidArgumentException('Unknown option: '.$key);
}
$this->$key = $value;
}
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
$writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-reply');
}
/**
* This method serializes the entire notification, as it is used in the
* response body.
*/
public function xmlSerializeFull(Writer $writer)
{
$cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}';
$this->dtStamp->setTimezone(new \DateTimeZone('GMT'));
$writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z'));
$writer->startElement($cs.'invite-reply');
$writer->writeElement($cs.'uid', $this->id);
$writer->writeElement($cs.'in-reply-to', $this->inReplyTo);
$writer->writeElement('{DAV:}href', $this->href);
switch ($this->type) {
case DAV\Sharing\Plugin::INVITE_ACCEPTED:
$writer->writeElement($cs.'invite-accepted');
break;
case DAV\Sharing\Plugin::INVITE_DECLINED:
$writer->writeElement($cs.'invite-declined');
break;
}
$writer->writeElement($cs.'hosturl', [
'{DAV:}href' => $writer->contextUri.$this->hostUrl,
]);
if ($this->summary) {
$writer->writeElement($cs.'summary', $this->summary);
}
$writer->endElement(); // invite-reply
}
/**
* Returns a unique id for this notification.
*
* This is just the base url. This should generally be some kind of unique
* id.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Returns the ETag for this notification.
*
* The ETag must be surrounded by literal double-quotes.
*
* @return string
*/
public function getETag()
{
return $this->etag;
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Notification;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* This interface reflects a single notification type.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface NotificationInterface extends XmlSerializable
{
/**
* This method serializes the entire notification, as it is used in the
* response body.
*/
public function xmlSerializeFull(Writer $writer);
/**
* Returns a unique id for this notification.
*
* This is just the base url. This should generally be some kind of unique
* id.
*
* @return string
*/
public function getId();
/**
* Returns the ETag for this notification.
*
* The ETag must be surrounded by literal double-quotes.
*
* @return string
*/
public function getETag();
}

View File

@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Notification;
use Sabre\CalDAV\Plugin;
use Sabre\Xml\Writer;
/**
* SystemStatus notification.
*
* This notification can be used to indicate to the user that the system is
* down.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SystemStatus implements NotificationInterface
{
const TYPE_LOW = 1;
const TYPE_MEDIUM = 2;
const TYPE_HIGH = 3;
/**
* A unique id.
*
* @var string
*/
protected $id;
/**
* The type of alert. This should be one of the TYPE_ constants.
*
* @var int
*/
protected $type;
/**
* A human-readable description of the problem.
*
* @var string
*/
protected $description;
/**
* A url to a website with more information for the user.
*
* @var string
*/
protected $href;
/**
* Notification Etag.
*
* @var string
*/
protected $etag;
/**
* Creates the notification.
*
* Some kind of unique id should be provided. This is used to generate a
* url.
*
* @param string $id
* @param string $etag
* @param int $type
* @param string $description
* @param string $href
*/
public function __construct($id, $etag, $type = self::TYPE_HIGH, $description = null, $href = null)
{
$this->id = $id;
$this->type = $type;
$this->description = $description;
$this->href = $href;
$this->etag = $etag;
}
/**
* The serialize method is called during xml writing.
*
* It should use the $writer argument to encode this object into XML.
*
* Important note: it is not needed to create the parent element. The
* parent element is already created, and we only have to worry about
* attributes, child elements and text (if any).
*
* Important note 2: If you are writing any new elements, you are also
* responsible for closing them.
*/
public function xmlSerialize(Writer $writer)
{
switch ($this->type) {
case self::TYPE_LOW:
$type = 'low';
break;
case self::TYPE_MEDIUM:
$type = 'medium';
break;
default:
case self::TYPE_HIGH:
$type = 'high';
break;
}
$writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}systemstatus');
$writer->writeAttribute('type', $type);
$writer->endElement();
}
/**
* This method serializes the entire notification, as it is used in the
* response body.
*/
public function xmlSerializeFull(Writer $writer)
{
$cs = '{'.Plugin::NS_CALENDARSERVER.'}';
switch ($this->type) {
case self::TYPE_LOW:
$type = 'low';
break;
case self::TYPE_MEDIUM:
$type = 'medium';
break;
default:
case self::TYPE_HIGH:
$type = 'high';
break;
}
$writer->startElement($cs.'systemstatus');
$writer->writeAttribute('type', $type);
if ($this->description) {
$writer->writeElement($cs.'description', $this->description);
}
if ($this->href) {
$writer->writeElement('{DAV:}href', $this->href);
}
$writer->endElement(); // systemstatus
}
/**
* Returns a unique id for this notification.
*
* This is just the base url. This should generally be some kind of unique
* id.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/*
* Returns the ETag for this notification.
*
* The ETag must be surrounded by literal double-quotes.
*
* @return string
*/
public function getETag()
{
return $this->etag;
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Property;
use Sabre\CalDAV\Plugin;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* AllowedSharingModes.
*
* This property encodes the 'allowed-sharing-modes' property, as defined by
* the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
* namespace.
*
* This property is a representation of the supported-calendar_component-set
* property in the CalDAV namespace. It simply requires an array of components,
* such as VEVENT, VTODO
*
* @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AllowedSharingModes implements XmlSerializable
{
/**
* Whether or not a calendar can be shared with another user.
*
* @var bool
*/
protected $canBeShared;
/**
* Whether or not the calendar can be placed on a public url.
*
* @var bool
*/
protected $canBePublished;
/**
* Constructor.
*
* @param bool $canBeShared
* @param bool $canBePublished
*/
public function __construct($canBeShared, $canBePublished)
{
$this->canBeShared = $canBeShared;
$this->canBePublished = $canBePublished;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
if ($this->canBeShared) {
$writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-shared');
}
if ($this->canBePublished) {
$writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-published');
}
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Property;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* email-address-set property.
*
* This property represents the email-address-set property in the
* http://calendarserver.org/ns/ namespace.
*
* It's a list of email addresses associated with a user.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class EmailAddressSet implements XmlSerializable
{
/**
* emails.
*
* @var array
*/
private $emails;
/**
* __construct.
*/
public function __construct(array $emails)
{
$this->emails = $emails;
}
/**
* Returns the email addresses.
*
* @return array
*/
public function getValue()
{
return $this->emails;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
foreach ($this->emails as $email) {
$writer->writeElement('{http://calendarserver.org/ns/}email-address', $email);
}
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Property;
use Sabre\CalDAV\Plugin;
use Sabre\DAV;
use Sabre\DAV\Xml\Element\Sharee;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* Invite property.
*
* This property encodes the 'invite' property, as defined by
* the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
* namespace.
*
* @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Invite implements XmlSerializable
{
/**
* The list of users a calendar has been shared to.
*
* @var Sharee[]
*/
protected $sharees;
/**
* Creates the property.
*
* @param Sharee[] $sharees
*/
public function __construct(array $sharees)
{
$this->sharees = $sharees;
}
/**
* Returns the list of users, as it was passed to the constructor.
*
* @return array
*/
public function getValue()
{
return $this->sharees;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
$cs = '{'.Plugin::NS_CALENDARSERVER.'}';
foreach ($this->sharees as $sharee) {
if (DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $sharee->access) {
$writer->startElement($cs.'organizer');
} else {
$writer->startElement($cs.'user');
switch ($sharee->inviteStatus) {
case DAV\Sharing\Plugin::INVITE_ACCEPTED:
$writer->writeElement($cs.'invite-accepted');
break;
case DAV\Sharing\Plugin::INVITE_DECLINED:
$writer->writeElement($cs.'invite-declined');
break;
case DAV\Sharing\Plugin::INVITE_NORESPONSE:
$writer->writeElement($cs.'invite-noresponse');
break;
case DAV\Sharing\Plugin::INVITE_INVALID:
$writer->writeElement($cs.'invite-invalid');
break;
}
$writer->startElement($cs.'access');
switch ($sharee->access) {
case DAV\Sharing\Plugin::ACCESS_READWRITE:
$writer->writeElement($cs.'read-write');
break;
case DAV\Sharing\Plugin::ACCESS_READ:
$writer->writeElement($cs.'read');
break;
}
$writer->endElement(); // access
}
$href = new DAV\Xml\Property\Href($sharee->href);
$href->xmlSerialize($writer);
if (isset($sharee->properties['{DAV:}displayname'])) {
$writer->writeElement($cs.'common-name', $sharee->properties['{DAV:}displayname']);
}
if ($sharee->comment) {
$writer->writeElement($cs.'summary', $sharee->comment);
}
$writer->endElement(); // organizer or user
}
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Property;
use Sabre\CalDAV\Plugin;
use Sabre\Xml\Deserializer;
use Sabre\Xml\Element;
use Sabre\Xml\Reader;
use Sabre\Xml\Writer;
/**
* schedule-calendar-transp property.
*
* This property is a representation of the schedule-calendar-transp property.
* This property is defined in:
*
* http://tools.ietf.org/html/rfc6638#section-9.1
*
* Its values are either 'transparent' or 'opaque'. If it's transparent, it
* means that this calendar will not be taken into consideration when a
* different user queries for free-busy information. If it's 'opaque', it will.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ScheduleCalendarTransp implements Element
{
const TRANSPARENT = 'transparent';
const OPAQUE = 'opaque';
/**
* value.
*
* @var string
*/
protected $value;
/**
* Creates the property.
*
* @param string $value
*/
public function __construct($value)
{
if (self::TRANSPARENT !== $value && self::OPAQUE !== $value) {
throw new \InvalidArgumentException('The value must either be specified as "transparent" or "opaque"');
}
$this->value = $value;
}
/**
* Returns the current value.
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
switch ($this->value) {
case self::TRANSPARENT:
$writer->writeElement('{'.Plugin::NS_CALDAV.'}transparent');
break;
case self::OPAQUE:
$writer->writeElement('{'.Plugin::NS_CALDAV.'}opaque');
break;
}
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = Deserializer\enum($reader, Plugin::NS_CALDAV);
if (in_array('transparent', $elems)) {
$value = self::TRANSPARENT;
} else {
$value = self::OPAQUE;
}
return new self($value);
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Property;
use Sabre\CalDAV\Plugin;
use Sabre\Xml\Element;
use Sabre\Xml\ParseException;
use Sabre\Xml\Reader;
use Sabre\Xml\Writer;
/**
* SupportedCalendarComponentSet property.
*
* This class represents the
* {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set property, as
* defined in:
*
* https://tools.ietf.org/html/rfc4791#section-5.2.3
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SupportedCalendarComponentSet implements Element
{
/**
* List of supported components.
*
* This array will contain values such as VEVENT, VTODO and VJOURNAL.
*
* @var array
*/
protected $components = [];
/**
* Creates the property.
*/
public function __construct(array $components)
{
$this->components = $components;
}
/**
* Returns the list of supported components.
*
* @return array
*/
public function getValue()
{
return $this->components;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
foreach ($this->components as $component) {
$writer->startElement('{'.Plugin::NS_CALDAV.'}comp');
$writer->writeAttributes(['name' => $component]);
$writer->endElement();
}
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = $reader->parseInnerTree();
$components = [];
foreach ((array) $elems as $elem) {
if ($elem['name'] === '{'.Plugin::NS_CALDAV.'}comp') {
$components[] = $elem['attributes']['name'];
}
}
if (!$components) {
throw new ParseException('supported-calendar-component-set must have at least one CALDAV:comp element');
}
return new self($components);
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Property;
use Sabre\CalDAV\Plugin;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* Supported-calendar-data property.
*
* This property is a representation of the supported-calendar-data property
* in the CalDAV namespace. SabreDAV only has support for text/calendar;2.0
* so the value is currently hardcoded.
*
* This property is defined in:
* http://tools.ietf.org/html/rfc4791#section-5.2.4
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SupportedCalendarData implements XmlSerializable
{
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
$writer->startElement('{'.Plugin::NS_CALDAV.'}calendar-data');
$writer->writeAttributes([
'content-type' => 'text/calendar',
'version' => '2.0',
]);
$writer->endElement(); // calendar-data
$writer->startElement('{'.Plugin::NS_CALDAV.'}calendar-data');
$writer->writeAttributes([
'content-type' => 'application/calendar+json',
]);
$writer->endElement(); // calendar-data
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Property;
use Sabre\CalDAV\Plugin;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* supported-collation-set property.
*
* This property is a representation of the supported-collation-set property
* in the CalDAV namespace.
*
* This property is defined in:
* http://tools.ietf.org/html/rfc4791#section-7.5.1
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SupportedCollationSet implements XmlSerializable
{
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
$collations = [
'i;ascii-casemap',
'i;octet',
'i;unicode-casemap',
];
foreach ($collations as $collation) {
$writer->writeElement('{'.Plugin::NS_CALDAV.'}supported-collation', $collation);
}
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Request;
use Sabre\CalDAV\Plugin;
use Sabre\Uri;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* CalendarMultiGetReport request parser.
*
* This class parses the {urn:ietf:params:xml:ns:caldav}calendar-multiget
* REPORT, as defined in:
*
* https://tools.ietf.org/html/rfc4791#section-7.9
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalendarMultiGetReport implements XmlDeserializable
{
/**
* An array with requested properties.
*
* @var array
*/
public $properties;
/**
* This is an array with the urls that are being requested.
*
* @var array
*/
public $hrefs;
/**
* If the calendar data must be expanded, this will contain an array with 2
* elements: start and end.
*
* Each may be a DateTime or null.
*
* @var array|null
*/
public $expand = null;
/**
* The mimetype of the content that should be returend. Usually
* text/calendar.
*
* @var string
*/
public $contentType = null;
/**
* The version of calendar-data that should be returned. Usually '2.0',
* referring to iCalendar 2.0.
*
* @var string
*/
public $version = null;
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = $reader->parseInnerTree([
'{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData',
'{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
]);
$newProps = [
'hrefs' => [],
'properties' => [],
];
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{DAV:}prop':
$newProps['properties'] = array_keys($elem['value']);
if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) {
$newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'];
}
break;
case '{DAV:}href':
$newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']);
break;
}
}
$obj = new self();
foreach ($newProps as $key => $value) {
$obj->$key = $value;
}
return $obj;
}
}

View File

@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Request;
use Sabre\CalDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* CalendarQueryReport request parser.
*
* This class parses the {urn:ietf:params:xml:ns:caldav}calendar-query
* REPORT, as defined in:
*
* https://tools.ietf.org/html/rfc4791#section-7.9
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class CalendarQueryReport implements XmlDeserializable
{
/**
* An array with requested properties.
*
* @var array
*/
public $properties;
/**
* List of property/component filters.
*
* @var array
*/
public $filters;
/**
* If the calendar data must be expanded, this will contain an array with 2
* elements: start and end.
*
* Each may be a DateTime or null.
*
* @var array|null
*/
public $expand = null;
/**
* The mimetype of the content that should be returend. Usually
* text/calendar.
*
* @var string
*/
public $contentType = null;
/**
* The version of calendar-data that should be returned. Usually '2.0',
* referring to iCalendar 2.0.
*
* @var string
*/
public $version = null;
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = $reader->parseInnerTree([
'{urn:ietf:params:xml:ns:caldav}comp-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter',
'{urn:ietf:params:xml:ns:caldav}prop-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter',
'{urn:ietf:params:xml:ns:caldav}param-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter',
'{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData',
'{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
]);
$newProps = [
'filters' => null,
'properties' => [],
];
if (!is_array($elems)) {
$elems = [];
}
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{DAV:}prop':
$newProps['properties'] = array_keys($elem['value']);
if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) {
$newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'];
}
break;
case '{'.Plugin::NS_CALDAV.'}filter':
foreach ($elem['value'] as $subElem) {
if ($subElem['name'] === '{'.Plugin::NS_CALDAV.'}comp-filter') {
if (!is_null($newProps['filters'])) {
throw new BadRequest('Only one top-level comp-filter may be defined');
}
$newProps['filters'] = $subElem['value'];
}
}
break;
}
}
if (is_null($newProps['filters'])) {
throw new BadRequest('The {'.Plugin::NS_CALDAV.'}filter element is required for this request');
}
$obj = new self();
foreach ($newProps as $key => $value) {
$obj->$key = $value;
}
return $obj;
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Request;
use Sabre\CalDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\VObject\DateTimeParser;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* FreeBusyQueryReport.
*
* This class parses the {DAV:}free-busy-query REPORT, as defined in:
*
* http://tools.ietf.org/html/rfc3253#section-3.8
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class FreeBusyQueryReport implements XmlDeserializable
{
/**
* Starttime of report.
*
* @var \DateTime|null
*/
public $start;
/**
* End time of report.
*
* @var \DateTime|null
*/
public $end;
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$timeRange = '{'.Plugin::NS_CALDAV.'}time-range';
$start = null;
$end = null;
foreach ((array) $reader->parseInnerTree([]) as $elem) {
if ($elem['name'] !== $timeRange) {
continue;
}
$start = empty($elem['attributes']['start']) ?: $elem['attributes']['start'];
$end = empty($elem['attributes']['end']) ?: $elem['attributes']['end'];
}
if (!$start && !$end) {
throw new BadRequest('The freebusy report must have a time-range element');
}
if ($start) {
$start = DateTimeParser::parseDateTime($start);
}
if ($end) {
$end = DateTimeParser::parseDateTime($end);
}
$result = new self();
$result->start = $start;
$result->end = $end;
return $result;
}
}

View File

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Request;
use Sabre\CalDAV\Plugin;
use Sabre\CalDAV\SharingPlugin;
use Sabre\DAV;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Element\KeyValue;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* Invite-reply POST request parser.
*
* This class parses the invite-reply POST request, as defined in:
*
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class InviteReply implements XmlDeserializable
{
/**
* The sharee calendar user address.
*
* This is the address that the original invite was set to
*
* @var string
*/
public $href;
/**
* The uri to the calendar that was being shared.
*
* @var string
*/
public $calendarUri;
/**
* The id of the invite message that's being responded to.
*
* @var string
*/
public $inReplyTo;
/**
* An optional message.
*
* @var string
*/
public $summary;
/**
* Either SharingPlugin::STATUS_ACCEPTED or SharingPlugin::STATUS_DECLINED.
*
* @var int
*/
public $status;
/**
* Constructor.
*
* @param string $href
* @param string $calendarUri
* @param string $inReplyTo
* @param string $summary
* @param int $status
*/
public function __construct($href, $calendarUri, $inReplyTo, $summary, $status)
{
$this->href = $href;
$this->calendarUri = $calendarUri;
$this->inReplyTo = $inReplyTo;
$this->summary = $summary;
$this->status = $status;
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = KeyValue::xmlDeserialize($reader);
$href = null;
$calendarUri = null;
$inReplyTo = null;
$summary = null;
$status = null;
foreach ($elems as $name => $value) {
switch ($name) {
case '{'.Plugin::NS_CALENDARSERVER.'}hosturl':
foreach ($value as $bla) {
if ('{DAV:}href' === $bla['name']) {
$calendarUri = $bla['value'];
}
}
break;
case '{'.Plugin::NS_CALENDARSERVER.'}invite-accepted':
$status = DAV\Sharing\Plugin::INVITE_ACCEPTED;
break;
case '{'.Plugin::NS_CALENDARSERVER.'}invite-declined':
$status = DAV\Sharing\Plugin::INVITE_DECLINED;
break;
case '{'.Plugin::NS_CALENDARSERVER.'}in-reply-to':
$inReplyTo = $value;
break;
case '{'.Plugin::NS_CALENDARSERVER.'}summary':
$summary = $value;
break;
case '{DAV:}href':
$href = $value;
break;
}
}
if (is_null($calendarUri)) {
throw new BadRequest('The {http://calendarserver.org/ns/}hosturl/{DAV:}href element must exist');
}
return new self($href, $calendarUri, $inReplyTo, $summary, $status);
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Request;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* MKCALENDAR parser.
*
* This class parses the MKCALENDAR request, as defined in:
*
* https://tools.ietf.org/html/rfc4791#section-5.3.1
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class MkCalendar implements XmlDeserializable
{
/**
* The list of properties that will be set.
*
* @var array
*/
public $properties = [];
/**
* Returns the list of properties the calendar will be initialized with.
*
* @return array
*/
public function getProperties()
{
return $this->properties;
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$self = new self();
$elementMap = $reader->elementMap;
$elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop';
$elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue';
$elems = $reader->parseInnerTree($elementMap);
foreach ($elems as $elem) {
if ('{DAV:}set' === $elem['name']) {
$self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']);
}
}
return $self;
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Sabre\CalDAV\Xml\Request;
use Sabre\CalDAV\Plugin;
use Sabre\DAV\Xml\Element\Sharee;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* Share POST request parser.
*
* This class parses the share POST request, as defined in:
*
* http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Share implements XmlDeserializable
{
/**
* The list of new people added or updated or removed from the share.
*
* @var Sharee[]
*/
public $sharees = [];
/**
* Constructor.
*
* @param Sharee[] $sharees
*/
public function __construct(array $sharees)
{
$this->sharees = $sharees;
}
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = $reader->parseGetElements([
'{'.Plugin::NS_CALENDARSERVER.'}set' => 'Sabre\\Xml\\Element\\KeyValue',
'{'.Plugin::NS_CALENDARSERVER.'}remove' => 'Sabre\\Xml\\Element\\KeyValue',
]);
$sharees = [];
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{'.Plugin::NS_CALENDARSERVER.'}set':
$sharee = $elem['value'];
$sumElem = '{'.Plugin::NS_CALENDARSERVER.'}summary';
$commonName = '{'.Plugin::NS_CALENDARSERVER.'}common-name';
$properties = [];
if (isset($sharee[$commonName])) {
$properties['{DAV:}displayname'] = $sharee[$commonName];
}
$access = array_key_exists('{'.Plugin::NS_CALENDARSERVER.'}read-write', $sharee)
? \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE
: \Sabre\DAV\Sharing\Plugin::ACCESS_READ;
$sharees[] = new Sharee([
'href' => $sharee['{DAV:}href'],
'properties' => $properties,
'access' => $access,
'comment' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null,
]);
break;
case '{'.Plugin::NS_CALENDARSERVER.'}remove':
$sharees[] = new Sharee([
'href' => $elem['value']['{DAV:}href'],
'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS,
]);
break;
}
}
return new self($sharees);
}
}

View File

@ -0,0 +1,335 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAV;
use Sabre\DAVACL;
/**
* The AddressBook class represents a CardDAV addressbook, owned by a specific user.
*
* The AddressBook can contain multiple vcards
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AddressBook extends DAV\Collection implements IAddressBook, DAV\IProperties, DAVACL\IACL, DAV\Sync\ISyncCollection, DAV\IMultiGet
{
use DAVACL\ACLTrait;
/**
* This is an array with addressbook information.
*
* @var array
*/
protected $addressBookInfo;
/**
* CardDAV backend.
*
* @var Backend\BackendInterface
*/
protected $carddavBackend;
/**
* Constructor.
*/
public function __construct(Backend\BackendInterface $carddavBackend, array $addressBookInfo)
{
$this->carddavBackend = $carddavBackend;
$this->addressBookInfo = $addressBookInfo;
}
/**
* Returns the name of the addressbook.
*
* @return string
*/
public function getName()
{
return $this->addressBookInfo['uri'];
}
/**
* Returns a card.
*
* @param string $name
*
* @return Card
*/
public function getChild($name)
{
$obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name);
if (!$obj) {
throw new DAV\Exception\NotFound('Card not found');
}
return new Card($this->carddavBackend, $this->addressBookInfo, $obj);
}
/**
* Returns the full list of cards.
*
* @return array
*/
public function getChildren()
{
$objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
$children = [];
foreach ($objs as $obj) {
$obj['acl'] = $this->getChildACL();
$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $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
*/
public function getMultipleChildren(array $paths)
{
$objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths);
$children = [];
foreach ($objs as $obj) {
$obj['acl'] = $this->getChildACL();
$children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj);
}
return $children;
}
/**
* Creates a new directory.
*
* We actually block this, as subdirectories are not allowed in addressbooks.
*
* @param string $name
*/
public function createDirectory($name)
{
throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed');
}
/**
* Creates a new file.
*
* The contents of the new file must be a valid VCARD.
*
* This method may return an ETag.
*
* @param string $name
* @param resource $vcardData
*
* @return string|null
*/
public function createFile($name, $vcardData = null)
{
if (is_resource($vcardData)) {
$vcardData = stream_get_contents($vcardData);
}
// Converting to UTF-8, if needed
$vcardData = DAV\StringUtil::ensureUTF8($vcardData);
return $this->carddavBackend->createCard($this->addressBookInfo['id'], $name, $vcardData);
}
/**
* Deletes the entire addressbook.
*/
public function delete()
{
$this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']);
}
/**
* Renames the addressbook.
*
* @param string $newName
*/
public function setName($newName)
{
throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported');
}
/**
* Returns the last modification date as a unix timestamp.
*/
public function getLastModified()
{
return null;
}
/**
* 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.
*/
public function propPatch(DAV\PropPatch $propPatch)
{
return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $propPatch);
}
/**
* Returns a list of properties for this nodes.
*
* The properties list is a list of propertynames the client requested,
* encoded in clark-notation {xmlnamespace}tagname
*
* If the array is empty, it means 'all properties' were requested.
*
* @param array $properties
*
* @return array
*/
public function getProperties($properties)
{
$response = [];
foreach ($properties as $propertyName) {
if (isset($this->addressBookInfo[$propertyName])) {
$response[$propertyName] = $this->addressBookInfo[$propertyName];
}
}
return $response;
}
/**
* Returns the owner principal.
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
public function getOwner()
{
return $this->addressBookInfo['principaluri'];
}
/**
* This method returns the ACL's for card nodes in this address book.
* The result of this method automatically gets passed to the
* card nodes in this address book.
*
* @return array
*/
public function getChildACL()
{
return [
[
'privilege' => '{DAV:}all',
'principal' => $this->getOwner(),
'protected' => true,
],
];
}
/**
* 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
*/
public function getSyncToken()
{
if (
$this->carddavBackend instanceof Backend\SyncSupport &&
isset($this->addressBookInfo['{DAV:}sync-token'])
) {
return $this->addressBookInfo['{DAV:}sync-token'];
}
if (
$this->carddavBackend instanceof Backend\SyncSupport &&
isset($this->addressBookInfo['{http://sabredav.org/ns}sync-token'])
) {
return $this->addressBookInfo['{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
*/
public function getChanges($syncToken, $syncLevel, $limit = null)
{
if (!$this->carddavBackend instanceof Backend\SyncSupport) {
return null;
}
return $this->carddavBackend->getChangesForAddressBook(
$this->addressBookInfo['id'],
$syncToken,
$syncLevel,
$limit
);
}
}

View File

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAV;
use Sabre\DAV\MkCol;
use Sabre\DAVACL;
use Sabre\Uri;
/**
* AddressBook Home class.
*
* This collection contains a list of addressbooks associated with one user.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AddressBookHome extends DAV\Collection implements DAV\IExtendedCollection, DAVACL\IACL
{
use DAVACL\ACLTrait;
/**
* Principal uri.
*
* @var array
*/
protected $principalUri;
/**
* carddavBackend.
*
* @var Backend\BackendInterface
*/
protected $carddavBackend;
/**
* Constructor.
*
* @param string $principalUri
*/
public function __construct(Backend\BackendInterface $carddavBackend, $principalUri)
{
$this->carddavBackend = $carddavBackend;
$this->principalUri = $principalUri;
}
/**
* Returns the name of this object.
*
* @return string
*/
public function getName()
{
list(, $name) = Uri\split($this->principalUri);
return $name;
}
/**
* Updates the name of this object.
*
* @param string $name
*/
public function setName($name)
{
throw new DAV\Exception\MethodNotAllowed();
}
/**
* Deletes this object.
*/
public function delete()
{
throw new DAV\Exception\MethodNotAllowed();
}
/**
* Returns the last modification date.
*
* @return int
*/
public function getLastModified()
{
return null;
}
/**
* Creates a new file under this object.
*
* This is currently not allowed
*
* @param string $filename
* @param resource $data
*/
public 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
*/
public function createDirectory($filename)
{
throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
}
/**
* Returns a single addressbook, by name.
*
* @param string $name
*
* @todo needs optimizing
*
* @return AddressBook
*/
public function getChild($name)
{
foreach ($this->getChildren() as $child) {
if ($name == $child->getName()) {
return $child;
}
}
throw new DAV\Exception\NotFound('Addressbook with name \''.$name.'\' could not be found');
}
/**
* Returns a list of addressbooks.
*
* @return array
*/
public function getChildren()
{
$addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri);
$objs = [];
foreach ($addressbooks as $addressbook) {
$objs[] = new AddressBook($this->carddavBackend, $addressbook);
}
return $objs;
}
/**
* Creates a new address book.
*
* @param string $name
*
* @throws DAV\Exception\InvalidResourceType
*/
public function createExtendedCollection($name, MkCol $mkCol)
{
if (!$mkCol->hasResourceType('{'.Plugin::NS_CARDDAV.'}addressbook')) {
throw new DAV\Exception\InvalidResourceType('Unknown resourceType for this collection');
}
$properties = $mkCol->getRemainingValues();
$mkCol->setRemainingResultCode(201);
$this->carddavBackend->createAddressBook($this->principalUri, $name, $properties);
}
/**
* Returns the owner principal.
*
* This must be a url to a principal, or null if there's no owner
*
* @return string|null
*/
public function getOwner()
{
return $this->principalUri;
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAVACL;
/**
* AddressBook rootnode.
*
* This object lists a collection of users, which can contain addressbooks.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AddressBookRoot extends DAVACL\AbstractPrincipalCollection
{
/**
* Principal Backend.
*
* @var DAVACL\PrincipalBackend\BackendInterface
*/
protected $principalBackend;
/**
* CardDAV backend.
*
* @var Backend\BackendInterface
*/
protected $carddavBackend;
/**
* Constructor.
*
* This constructor needs both a principal and a carddav backend.
*
* By default this class will show a list of addressbook 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 string $principalPrefix
*/
public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, Backend\BackendInterface $carddavBackend, $principalPrefix = 'principals')
{
$this->carddavBackend = $carddavBackend;
parent::__construct($principalBackend, $principalPrefix);
}
/**
* Returns the name of the node.
*
* @return string
*/
public function getName()
{
return Plugin::ADDRESSBOOK_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.
*
* @return \Sabre\DAV\INode
*/
public function getChildForPrincipal(array $principal)
{
return new AddressBookHome($this->carddavBackend, $principal['uri']);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Backend;
/**
* CardDAV abstract Backend.
*
* This class serves as a base-class for addressbook backends
*
* This class doesn't do much, but it was added for consistency.
*
* @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
{
/**
* Returns a list of cards.
*
* This method should work identical to getCard, but instead return all the
* cards in the list as an array.
*
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $addressBookId
*
* @return array
*/
public function getMultipleCards($addressBookId, array $uris)
{
return array_map(function ($uri) use ($addressBookId) {
return $this->getCard($addressBookId, $uri);
}, $uris);
}
}

View File

@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Backend;
/**
* CardDAV Backend Interface.
*
* Any CardDAV backend must implement this interface.
*
* Note that there are references to 'addressBookId' scattered throughout the
* class. The value of the addressBookId is completely up to you, it can be any
* arbitrary value you can use as an unique identifier.
*
* @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 the list of addressbooks for a specific user.
*
* Every addressbook should have the following properties:
* id - an arbitrary unique id
* uri - the 'basename' part of the url
* principaluri - Same as the passed parameter
*
* Any additional clark-notation property may be passed besides this. Some
* common ones are :
* {DAV:}displayname
* {urn:ietf:params:xml:ns:carddav}addressbook-description
* {http://calendarserver.org/ns/}getctag
*
* @param string $principalUri
*
* @return array
*/
public function getAddressBooksForUser($principalUri);
/**
* Updates properties for an address book.
*
* 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 $addressBookId
*/
public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch);
/**
* Creates a new address book.
*
* This method should return the id of the new address book. The id can be
* in any format, including ints, strings, arrays or objects.
*
* @param string $principalUri
* @param string $url just the 'basename' of the url
*
* @return mixed
*/
public function createAddressBook($principalUri, $url, array $properties);
/**
* Deletes an entire addressbook and all its contents.
*
* @param mixed $addressBookId
*/
public function deleteAddressBook($addressBookId);
/**
* Returns all cards for a specific addressbook id.
*
* This method should return the following properties for each card:
* * carddata - raw vcard data
* * uri - Some unique url
* * lastmodified - A unix timestamp
*
* It's recommended to also return the following properties:
* * etag - A unique etag. This must change every time the card changes.
* * size - The size of the card in bytes.
*
* If these last two properties are provided, less time will be spent
* calculating them. If they are specified, you can also ommit carddata.
* This may speed up certain requests, especially with large cards.
*
* @param mixed $addressbookId
*
* @return array
*/
public function getCards($addressbookId);
/**
* Returns a specfic card.
*
* The same set of properties must be returned as with getCards. The only
* exception is that 'carddata' is absolutely required.
*
* If the card does not exist, you must return false.
*
* @param mixed $addressBookId
* @param string $cardUri
*
* @return array
*/
public function getCard($addressBookId, $cardUri);
/**
* Returns a list of cards.
*
* This method should work identical to getCard, but instead return all the
* cards in the list as an array.
*
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $addressBookId
*
* @return array
*/
public function getMultipleCards($addressBookId, array $uris);
/**
* Creates a new card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag is for the
* newly created resource, and must be enclosed with double quotes (that
* is, the string itself must contain the double quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
*
* @return string|null
*/
public function createCard($addressBookId, $cardUri, $cardData);
/**
* Updates a card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag should
* match that of the updated resource, and must be enclosed with double
* quotes (that is: the string itself must contain the actual quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
*
* @return string|null
*/
public function updateCard($addressBookId, $cardUri, $cardData);
/**
* Deletes a card.
*
* @param mixed $addressBookId
* @param string $cardUri
*
* @return bool
*/
public function deleteCard($addressBookId, $cardUri);
}

View File

@ -0,0 +1,538 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Backend;
use Sabre\CardDAV;
use Sabre\DAV;
/**
* PDO CardDAV backend.
*
* This CardDAV backend uses PDO to store addressbooks
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class PDO extends AbstractBackend implements SyncSupport
{
/**
* PDO connection.
*
* @var PDO
*/
protected $pdo;
/**
* The PDO table name used to store addressbooks.
*/
public $addressBooksTableName = 'addressbooks';
/**
* The PDO table name used to store cards.
*/
public $cardsTableName = 'cards';
/**
* The table name that will be used for tracking changes in address books.
*
* @var string
*/
public $addressBookChangesTableName = 'addressbookchanges';
/**
* Sets up the object.
*/
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
/**
* Returns the list of addressbooks for a specific user.
*
* @param string $principalUri
*
* @return array
*/
public function getAddressBooksForUser($principalUri)
{
$stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, synctoken FROM '.$this->addressBooksTableName.' WHERE principaluri = ?');
$stmt->execute([$principalUri]);
$addressBooks = [];
foreach ($stmt->fetchAll() as $row) {
$addressBooks[] = [
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $row['principaluri'],
'{DAV:}displayname' => $row['displayname'],
'{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
'{http://calendarserver.org/ns/}getctag' => $row['synctoken'],
'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0',
];
}
return $addressBooks;
}
/**
* Updates properties for an address book.
*
* 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 $addressBookId
*/
public function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch)
{
$supportedProperties = [
'{DAV:}displayname',
'{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description',
];
$propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
$updates = [];
foreach ($mutations as $property => $newValue) {
switch ($property) {
case '{DAV:}displayname':
$updates['displayname'] = $newValue;
break;
case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
$updates['description'] = $newValue;
break;
}
}
$query = 'UPDATE '.$this->addressBooksTableName.' SET ';
$first = true;
foreach ($updates as $key => $value) {
if ($first) {
$first = false;
} else {
$query .= ', ';
}
$query .= ' '.$key.' = :'.$key.' ';
}
$query .= ' WHERE id = :addressbookid';
$stmt = $this->pdo->prepare($query);
$updates['addressbookid'] = $addressBookId;
$stmt->execute($updates);
$this->addChange($addressBookId, '', 2);
return true;
});
}
/**
* Creates a new address book.
*
* @param string $principalUri
* @param string $url just the 'basename' of the url
*
* @return int Last insert id
*/
public function createAddressBook($principalUri, $url, array $properties)
{
$values = [
'displayname' => null,
'description' => null,
'principaluri' => $principalUri,
'uri' => $url,
];
foreach ($properties as $property => $newValue) {
switch ($property) {
case '{DAV:}displayname':
$values['displayname'] = $newValue;
break;
case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
$values['description'] = $newValue;
break;
default:
throw new DAV\Exception\BadRequest('Unknown property: '.$property);
}
}
$query = 'INSERT INTO '.$this->addressBooksTableName.' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)';
$stmt = $this->pdo->prepare($query);
$stmt->execute($values);
return $this->pdo->lastInsertId(
$this->addressBooksTableName.'_id_seq'
);
}
/**
* Deletes an entire addressbook and all its contents.
*
* @param int $addressBookId
*/
public function deleteAddressBook($addressBookId)
{
$stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
$stmt->execute([$addressBookId]);
$stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBooksTableName.' WHERE id = ?');
$stmt->execute([$addressBookId]);
$stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBookChangesTableName.' WHERE addressbookid = ?');
$stmt->execute([$addressBookId]);
}
/**
* Returns all cards for a specific addressbook id.
*
* This method should return the following properties for each card:
* * carddata - raw vcard data
* * uri - Some unique url
* * lastmodified - A unix timestamp
*
* It's recommended to also return the following properties:
* * etag - A unique etag. This must change every time the card changes.
* * size - The size of the card in bytes.
*
* If these last two properties are provided, less time will be spent
* calculating them. If they are specified, you can also ommit carddata.
* This may speed up certain requests, especially with large cards.
*
* @param mixed $addressbookId
*
* @return array
*/
public function getCards($addressbookId)
{
$stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ?');
$stmt->execute([$addressbookId]);
$result = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$row['etag'] = '"'.$row['etag'].'"';
$row['lastmodified'] = (int) $row['lastmodified'];
$result[] = $row;
}
return $result;
}
/**
* Returns a specific card.
*
* The same set of properties must be returned as with getCards. The only
* exception is that 'carddata' is absolutely required.
*
* If the card does not exist, you must return false.
*
* @param mixed $addressBookId
* @param string $cardUri
*
* @return array
*/
public function getCard($addressBookId, $cardUri)
{
$stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ? LIMIT 1');
$stmt->execute([$addressBookId, $cardUri]);
$result = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$result) {
return false;
}
$result['etag'] = '"'.$result['etag'].'"';
$result['lastmodified'] = (int) $result['lastmodified'];
return $result;
}
/**
* Returns a list of cards.
*
* This method should work identical to getCard, but instead return all the
* cards in the list as an array.
*
* If the backend supports this, it may allow for some speed-ups.
*
* @param mixed $addressBookId
*
* @return array
*/
public function getMultipleCards($addressBookId, array $uris)
{
$query = 'SELECT id, uri, lastmodified, etag, size, carddata FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri IN (';
// Inserting a whole bunch of question marks
$query .= implode(',', array_fill(0, count($uris), '?'));
$query .= ')';
$stmt = $this->pdo->prepare($query);
$stmt->execute(array_merge([$addressBookId], $uris));
$result = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$row['etag'] = '"'.$row['etag'].'"';
$row['lastmodified'] = (int) $row['lastmodified'];
$result[] = $row;
}
return $result;
}
/**
* Creates a new card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag is for the
* newly created resource, and must be enclosed with double quotes (that
* is, the string itself must contain the double quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
*
* @return string|null
*/
public function createCard($addressBookId, $cardUri, $cardData)
{
$stmt = $this->pdo->prepare('INSERT INTO '.$this->cardsTableName.' (carddata, uri, lastmodified, addressbookid, size, etag) VALUES (?, ?, ?, ?, ?, ?)');
$etag = md5($cardData);
$stmt->execute([
$cardData,
$cardUri,
time(),
$addressBookId,
strlen($cardData),
$etag,
]);
$this->addChange($addressBookId, $cardUri, 1);
return '"'.$etag.'"';
}
/**
* Updates a card.
*
* The addressbook id will be passed as the first argument. This is the
* same id as it is returned from the getAddressBooksForUser method.
*
* The cardUri is a base uri, and doesn't include the full path. The
* cardData argument is the vcard body, and is passed as a string.
*
* It is possible to return an ETag from this method. This ETag should
* match that of the updated resource, and must be enclosed with double
* quotes (that is: the string itself must contain the actual quotes).
*
* You should only return the ETag if you store the carddata as-is. If a
* subsequent GET request on the same card does not have the same body,
* byte-by-byte and you did return an ETag here, clients tend to get
* confused.
*
* If you don't return an ETag, you can just return null.
*
* @param mixed $addressBookId
* @param string $cardUri
* @param string $cardData
*
* @return string|null
*/
public function updateCard($addressBookId, $cardUri, $cardData)
{
$stmt = $this->pdo->prepare('UPDATE '.$this->cardsTableName.' SET carddata = ?, lastmodified = ?, size = ?, etag = ? WHERE uri = ? AND addressbookid =?');
$etag = md5($cardData);
$stmt->execute([
$cardData,
time(),
strlen($cardData),
$etag,
$cardUri,
$addressBookId,
]);
$this->addChange($addressBookId, $cardUri, 2);
return '"'.$etag.'"';
}
/**
* Deletes a card.
*
* @param mixed $addressBookId
* @param string $cardUri
*
* @return bool
*/
public function deleteCard($addressBookId, $cardUri)
{
$stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ?');
$stmt->execute([$addressBookId, $cardUri]);
$this->addChange($addressBookId, $cardUri, 3);
return 1 === $stmt->rowCount();
}
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified address book.
*
* This function should return an array, such as the following:
*
* [
* 'syncToken' => 'The current synctoken',
* 'added' => [
* 'new.txt',
* ],
* 'modified' => [
* 'updated.txt',
* ],
* 'deleted' => [
* 'foo.php.bak',
* 'old.txt'
* ]
* ];
*
* The returned syncToken property should reflect the *current* syncToken
* of the addressbook, as reported in the {http://sabredav.org/ns}sync-token
* property. This is needed here too, to ensure the operation is atomic.
*
* If the $syncToken argument 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 $syncLevel 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 $limit 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 $addressBookId
* @param string $syncToken
* @param int $syncLevel
* @param int $limit
*
* @return array
*/
public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null)
{
// Current synctoken
$stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->addressBooksTableName.' WHERE id = ?');
$stmt->execute([$addressBookId]);
$currentToken = $stmt->fetchColumn(0);
if (is_null($currentToken)) {
return null;
}
$result = [
'syncToken' => $currentToken,
'added' => [],
'modified' => [],
'deleted' => [],
];
if ($syncToken) {
$query = 'SELECT uri, operation FROM '.$this->addressBookChangesTableName.' WHERE synctoken >= ? AND synctoken < ? AND addressbookid = ? ORDER BY synctoken';
if ($limit > 0) {
$query .= ' LIMIT '.(int) $limit;
}
// Fetching all changes
$stmt = $this->pdo->prepare($query);
$stmt->execute([$syncToken, $currentToken, $addressBookId]);
$changes = [];
// This loop ensures that any duplicates are overwritten, only the
// last change on a node is relevant.
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$changes[$row['uri']] = $row['operation'];
}
foreach ($changes as $uri => $operation) {
switch ($operation) {
case 1:
$result['added'][] = $uri;
break;
case 2:
$result['modified'][] = $uri;
break;
case 3:
$result['deleted'][] = $uri;
break;
}
}
} else {
// No synctoken supplied, this is the initial sync.
$query = 'SELECT uri FROM '.$this->cardsTableName.' WHERE addressbookid = ?';
$stmt = $this->pdo->prepare($query);
$stmt->execute([$addressBookId]);
$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
}
return $result;
}
/**
* Adds a change record to the addressbookchanges table.
*
* @param mixed $addressBookId
* @param string $objectUri
* @param int $operation 1 = add, 2 = modify, 3 = delete
*/
protected function addChange($addressBookId, $objectUri, $operation)
{
$stmt = $this->pdo->prepare('INSERT INTO '.$this->addressBookChangesTableName.' (uri, synctoken, addressbookid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->addressBooksTableName.' WHERE id = ?');
$stmt->execute([
$objectUri,
$addressBookId,
$operation,
$addressBookId,
]);
$stmt = $this->pdo->prepare('UPDATE '.$this->addressBooksTableName.' SET synctoken = synctoken + 1 WHERE id = ?');
$stmt->execute([
$addressBookId,
]);
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Backend;
/**
* WebDAV-sync support for CardDAV backends.
*
* In order for backends to advertise support for WebDAV-sync, this interface
* must be implemented.
*
* Implementing this can result in a significant reduction of bandwidth and CPU
* time.
*
* For this to work, you _must_ return a {http://sabredav.org/ns}sync-token
* property from getAddressBooksForUser.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface SyncSupport extends BackendInterface
{
/**
* The getChanges method returns all the changes that have happened, since
* the specified syncToken in the specified address book.
*
* 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 returned syncToken property should reflect the *current* syncToken
* of the calendar, as reported in the {http://sabredav.org/ns}sync-token
* property. This is needed here too, to ensure the operation is atomic.
*
* If the $syncToken argument 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 $syncLevel 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 $limit 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 $addressBookId
* @param string $syncToken
* @param int $syncLevel
* @param int $limit
*
* @return array
*/
public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null);
}

202
vendor/sabre/dav/lib/CardDAV/Card.php vendored Normal file
View File

@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAV;
use Sabre\DAVACL;
/**
* The Card object represents a single Card from an addressbook.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class Card extends DAV\File implements ICard, DAVACL\IACL
{
use DAVACL\ACLTrait;
/**
* CardDAV backend.
*
* @var Backend\BackendInterface
*/
protected $carddavBackend;
/**
* Array with information about this Card.
*
* @var array
*/
protected $cardData;
/**
* Array with information about the containing addressbook.
*
* @var array
*/
protected $addressBookInfo;
/**
* Constructor.
*/
public function __construct(Backend\BackendInterface $carddavBackend, array $addressBookInfo, array $cardData)
{
$this->carddavBackend = $carddavBackend;
$this->addressBookInfo = $addressBookInfo;
$this->cardData = $cardData;
}
/**
* Returns the uri for this object.
*
* @return string
*/
public function getName()
{
return $this->cardData['uri'];
}
/**
* Returns the VCard-formatted object.
*
* @return string
*/
public function get()
{
// Pre-populating 'carddata' is optional. If we don't yet have it
// already, we fetch it from the backend.
if (!isset($this->cardData['carddata'])) {
$this->cardData = $this->carddavBackend->getCard($this->addressBookInfo['id'], $this->cardData['uri']);
}
return $this->cardData['carddata'];
}
/**
* Updates the VCard-formatted object.
*
* @param string $cardData
*
* @return string|null
*/
public function put($cardData)
{
if (is_resource($cardData)) {
$cardData = stream_get_contents($cardData);
}
// Converting to UTF-8, if needed
$cardData = DAV\StringUtil::ensureUTF8($cardData);
$etag = $this->carddavBackend->updateCard($this->addressBookInfo['id'], $this->cardData['uri'], $cardData);
$this->cardData['carddata'] = $cardData;
$this->cardData['etag'] = $etag;
return $etag;
}
/**
* Deletes the card.
*/
public function delete()
{
$this->carddavBackend->deleteCard($this->addressBookInfo['id'], $this->cardData['uri']);
}
/**
* Returns the mime content-type.
*
* @return string
*/
public function getContentType()
{
return 'text/vcard; charset=utf-8';
}
/**
* Returns an ETag for this object.
*
* @return string
*/
public function getETag()
{
if (isset($this->cardData['etag'])) {
return $this->cardData['etag'];
} else {
$data = $this->get();
if (is_string($data)) {
return '"'.md5($data).'"';
} else {
// We refuse to calculate the md5 if it's a stream.
return null;
}
}
}
/**
* Returns the last modification date as a unix timestamp.
*
* @return int
*/
public function getLastModified()
{
return isset($this->cardData['lastmodified']) ? $this->cardData['lastmodified'] : null;
}
/**
* Returns the size of this object in bytes.
*
* @return int
*/
public function getSize()
{
if (array_key_exists('size', $this->cardData)) {
return $this->cardData['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
*/
public function getOwner()
{
return $this->addressBookInfo['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
*/
public function getACL()
{
// An alternative acl may be specified through the cardData array.
if (isset($this->cardData['acl'])) {
return $this->cardData['acl'];
}
return [
[
'privilege' => '{DAV:}all',
'principal' => $this->addressBookInfo['principaluri'],
'protected' => true,
],
];
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAV;
/**
* AddressBook interface.
*
* Implement this interface to allow a node to be recognized as an addressbook.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface IAddressBook extends DAV\ICollection
{
}

21
vendor/sabre/dav/lib/CardDAV/ICard.php vendored Normal file
View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAV;
/**
* Card interface.
*
* Extend the ICard interface to allow your custom nodes to be picked up as
* 'Cards'.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface ICard extends DAV\IFile
{
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
/**
* IDirectory interface.
*
* Implement this interface to have an addressbook marked as a 'directory'. A
* directory is an (often) global addressbook.
*
* A full description can be found in the IETF draft:
* - draft-daboo-carddav-directory-gateway
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
interface IDirectory extends IAddressBook
{
}

879
vendor/sabre/dav/lib/CardDAV/Plugin.php vendored Normal file
View File

@ -0,0 +1,879 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAV;
use Sabre\DAV\Exception\ReportNotSupported;
use Sabre\DAV\Xml\Property\LocalHref;
use Sabre\DAVACL;
use Sabre\HTTP;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\Uri;
use Sabre\VObject;
/**
* CardDAV plugin.
*
* The CardDAV plugin adds CardDAV functionality to the WebDAV server
*
* @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
{
/**
* Url to the addressbooks.
*/
const ADDRESSBOOK_ROOT = 'addressbooks';
/**
* xml namespace for CardDAV elements.
*/
const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
/**
* Add urls to this property to have them automatically exposed as
* 'directories' to the user.
*
* @var array
*/
public $directories = [];
/**
* Server class.
*
* @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;
/**
* Initializes the plugin.
*/
public function initialize(DAV\Server $server)
{
/* Events */
$server->on('propFind', [$this, 'propFindEarly']);
$server->on('propFind', [$this, 'propFindLate'], 150);
$server->on('report', [$this, 'report']);
$server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
$server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
$server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
$server->on('afterMethod:GET', [$this, 'httpAfterGet']);
$server->xml->namespaceMap[self::NS_CARDDAV] = 'card';
$server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport';
$server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport';
/* Mapping Interfaces to {DAV:}resourcetype values */
$server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{'.self::NS_CARDDAV.'}addressbook';
$server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{'.self::NS_CARDDAV.'}directory';
/* Adding properties that may never be changed */
$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-address-data';
$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}max-resource-size';
$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}addressbook-home-set';
$server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-collation-set';
$server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href';
$this->server = $server;
}
/**
* Returns a list of supported features.
*
* This is used in the DAV: header in the OPTIONS and PROPFIND requests.
*
* @return array
*/
public function getFeatures()
{
return ['addressbook'];
}
/**
* 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
*/
public function getSupportedReportSet($uri)
{
$node = $this->server->tree->getNodeForPath($uri);
if ($node instanceof IAddressBook || $node instanceof ICard) {
return [
'{'.self::NS_CARDDAV.'}addressbook-multiget',
'{'.self::NS_CARDDAV.'}addressbook-query',
];
}
return [];
}
/**
* Adds all CardDAV-specific properties.
*/
public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
{
$ns = '{'.self::NS_CARDDAV.'}';
if ($node instanceof IAddressBook) {
$propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
$propFind->handle($ns.'supported-address-data', function () {
return new Xml\Property\SupportedAddressData();
});
$propFind->handle($ns.'supported-collation-set', function () {
return new Xml\Property\SupportedCollationSet();
});
}
if ($node instanceof DAVACL\IPrincipal) {
$path = $propFind->getPath();
$propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) {
return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/');
});
if ($this->directories) {
$propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () {
return new LocalHref($this->directories);
});
}
}
if ($node instanceof ICard) {
// The address-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_CARDDAV.'}address-data', function () use ($node) {
$val = $node->get();
if (is_resource($val)) {
$val = stream_get_contents($val);
}
return $val;
});
}
}
/**
* This functions handles REPORT requests specific to CardDAV.
*
* @param string $reportName
* @param \DOMNode $dom
* @param mixed $path
*
* @return bool
*/
public function report($reportName, $dom, $path)
{
switch ($reportName) {
case '{'.self::NS_CARDDAV.'}addressbook-multiget':
$this->server->transactionType = 'report-addressbook-multiget';
$this->addressbookMultiGetReport($dom);
return false;
case '{'.self::NS_CARDDAV.'}addressbook-query':
$this->server->transactionType = 'report-addressbook-query';
$this->addressBookQueryReport($dom);
return false;
default:
return;
}
}
/**
* Returns the addressbook home for a given principal.
*
* @param string $principal
*
* @return string
*/
protected function getAddressbookHomeForPrincipal($principal)
{
list(, $principalId) = Uri\split($principal);
return self::ADDRESSBOOK_ROOT.'/'.$principalId;
}
/**
* This function handles the addressbook-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 Xml\Request\AddressBookMultiGetReport $report
*/
public function addressbookMultiGetReport($report)
{
$contentType = $report->contentType;
$version = $report->version;
if ($version) {
$contentType .= '; version='.$version;
}
$vcardType = $this->negotiateVCard(
$contentType
);
$propertyList = [];
$paths = array_map(
[$this->server, 'calculateUri'],
$report->hrefs
);
foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) {
if (isset($props['200']['{'.self::NS_CARDDAV.'}address-data'])) {
$props['200']['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
$props[200]['{'.self::NS_CARDDAV.'}address-data'],
$vcardType
);
}
$propertyList[] = $props;
}
$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, 'minimal' === $prefer['return']));
}
/**
* This method is triggered before a file gets updated with new content.
*
* This plugin uses this method to ensure that Card nodes receive valid
* vcard data.
*
* @param string $path
* @param resource $data
* @param bool $modified should be set to true, if this event handler
* changed &$data
*/
public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
{
if (!$node instanceof ICard) {
return;
}
$this->validateVCard($data, $modified);
}
/**
* This method is triggered before a new file is created.
*
* This plugin uses this method to ensure that Card nodes receive valid
* vcard data.
*
* @param string $path
* @param resource $data
* @param bool $modified should be set to true, if this event handler
* changed &$data
*/
public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
{
if (!$parentNode instanceof IAddressBook) {
return;
}
$this->validateVCard($data, $modified);
}
/**
* Checks if the submitted iCalendar data is in fact, valid.
*
* An exception is thrown if it's not.
*
* @param resource|string $data
* @param bool $modified should be set to true, if this event handler
* changed &$data
*/
protected function validateVCard(&$data, &$modified)
{
// If it's a stream, we convert it to a string first.
if (is_resource($data)) {
$data = stream_get_contents($data);
}
$before = $data;
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 vCard or jCard data. Parse error: '.$e->getMessage());
}
if ('VCARD' !== $vobj->name) {
throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
}
$options = VObject\Node::PROFILE_CARDDAV;
$prefer = $this->server->getHTTPPrefer();
if ('strict' !== $prefer['handling']) {
$options |= VObject\Node::REPAIR;
}
$messages = $vobj->validate($options);
$highestLevel = 0;
$warningMessage = null;
// $messages contains a list of problems with the vcard, along with
// their severity.
foreach ($messages as $message) {
if ($message['level'] > $highestLevel) {
// Recording the highest reported error level.
$highestLevel = $message['level'];
$warningMessage = $message['message'];
}
switch ($message['level']) {
case 1:
// Level 1 means that there was a problem, but it was repaired.
$modified = true;
break;
case 2:
// Level 2 means a warning, but not critical
break;
case 3:
// Level 3 means a critical error
throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: '.$message['message']);
}
}
if ($warningMessage) {
$this->server->httpResponse->setHeader(
'X-Sabre-Ew-Gross',
'vCard validation warning: '.$warningMessage
);
// Re-serializing object.
$data = $vobj->serialize();
if (!$modified && 0 !== strcmp($data, $before)) {
// This ensures that the system does not send an ETag back.
$modified = true;
}
}
// Destroy circular references to PHP will GC the object.
$vobj->destroy();
}
/**
* This function handles the addressbook-query REPORT.
*
* This report is used by the client to filter an addressbook based on a
* complex query.
*
* @param Xml\Request\AddressBookQueryReport $report
*/
protected function addressbookQueryReport($report)
{
$depth = $this->server->getHTTPDepth(0);
if (0 == $depth) {
$candidateNodes = [
$this->server->tree->getNodeForPath($this->server->getRequestUri()),
];
if (!$candidateNodes[0] instanceof ICard) {
throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0');
}
} else {
$candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
}
$contentType = $report->contentType;
if ($report->version) {
$contentType .= '; version='.$report->version;
}
$vcardType = $this->negotiateVCard(
$contentType
);
$validNodes = [];
foreach ($candidateNodes as $node) {
if (!$node instanceof ICard) {
continue;
}
$blob = $node->get();
if (is_resource($blob)) {
$blob = stream_get_contents($blob);
}
if (!$this->validateFilters($blob, $report->filters, $report->test)) {
continue;
}
$validNodes[] = $node;
if ($report->limit && $report->limit <= count($validNodes)) {
// We hit the maximum number of items, we can stop now.
break;
}
}
$result = [];
foreach ($validNodes as $validNode) {
if (0 == $depth) {
$href = $this->server->getRequestUri();
} else {
$href = $this->server->getRequestUri().'/'.$validNode->getName();
}
list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0);
if (isset($props[200]['{'.self::NS_CARDDAV.'}address-data'])) {
$props[200]['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
$props[200]['{'.self::NS_CARDDAV.'}address-data'],
$vcardType,
$report->addressDataProperties
);
}
$result[] = $props;
}
$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, 'minimal' === $prefer['return']));
}
/**
* Validates if a vcard makes it throught a list of filters.
*
* @param string $vcardData
* @param string $test anyof or allof (which means OR or AND)
*
* @return bool
*/
public function validateFilters($vcardData, array $filters, $test)
{
if (!$filters) {
return true;
}
$vcard = VObject\Reader::read($vcardData);
foreach ($filters as $filter) {
$isDefined = isset($vcard->{$filter['name']});
if ($filter['is-not-defined']) {
if ($isDefined) {
$success = false;
} else {
$success = true;
}
} elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
// We only need to check for existence
$success = $isDefined;
} else {
$vProperties = $vcard->select($filter['name']);
$results = [];
if ($filter['param-filters']) {
$results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
}
if ($filter['text-matches']) {
$texts = [];
foreach ($vProperties as $vProperty) {
$texts[] = $vProperty->getValue();
}
$results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
}
if (1 === count($results)) {
$success = $results[0];
} else {
if ('anyof' === $filter['test']) {
$success = $results[0] || $results[1];
} else {
$success = $results[0] && $results[1];
}
}
} // else
// There are two conditions where we can already determine whether
// or not this filter succeeds.
if ('anyof' === $test && $success) {
// Destroy circular references to PHP will GC the object.
$vcard->destroy();
return true;
}
if ('allof' === $test && !$success) {
// Destroy circular references to PHP will GC the object.
$vcard->destroy();
return false;
}
} // foreach
// Destroy circular references to PHP will GC the object.
$vcard->destroy();
// If we got all the way here, it means we haven't been able to
// determine early if the test failed or not.
//
// This implies for 'anyof' that the test failed, and for 'allof' that
// we succeeded. Sounds weird, but makes sense.
return 'allof' === $test;
}
/**
* Validates if a param-filter can be applied to a specific property.
*
* @todo currently we're only validating the first parameter of the passed
* property. Any subsequence parameters with the same name are
* ignored.
*
* @param string $test
*
* @return bool
*/
protected function validateParamFilters(array $vProperties, array $filters, $test)
{
foreach ($filters as $filter) {
$isDefined = false;
foreach ($vProperties as $vProperty) {
$isDefined = isset($vProperty[$filter['name']]);
if ($isDefined) {
break;
}
}
if ($filter['is-not-defined']) {
if ($isDefined) {
$success = false;
} else {
$success = true;
}
// If there's no text-match, we can just check for existence
} elseif (!$filter['text-match'] || !$isDefined) {
$success = $isDefined;
} else {
$success = false;
foreach ($vProperties as $vProperty) {
// If we got all the way here, we'll need to validate the
// text-match filter.
$success = DAV\StringUtil::textMatch($vProperty[$filter['name']]->getValue(), $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']);
if ($success) {
break;
}
}
if ($filter['text-match']['negate-condition']) {
$success = !$success;
}
} // else
// There are two conditions where we can already determine whether
// or not this filter succeeds.
if ('anyof' === $test && $success) {
return true;
}
if ('allof' === $test && !$success) {
return false;
}
}
// If we got all the way here, it means we haven't been able to
// determine early if the test failed or not.
//
// This implies for 'anyof' that the test failed, and for 'allof' that
// we succeeded. Sounds weird, but makes sense.
return 'allof' === $test;
}
/**
* Validates if a text-filter can be applied to a specific property.
*
* @param string $test
*
* @return bool
*/
protected function validateTextMatches(array $texts, array $filters, $test)
{
foreach ($filters as $filter) {
$success = false;
foreach ($texts as $haystack) {
$success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
// Breaking on the first match
if ($success) {
break;
}
}
if ($filter['negate-condition']) {
$success = !$success;
}
if ($success && 'anyof' === $test) {
return true;
}
if (!$success && 'allof' == $test) {
return false;
}
}
// If we got all the way here, it means we haven't been able to
// determine early if the test failed or not.
//
// This implies for 'anyof' that the test failed, and for 'allof' that
// we succeeded. Sounds weird, but makes sense.
return 'allof' === $test;
}
/**
* This event is triggered when fetching properties.
*
* This event is scheduled late in the process, after most work for
* propfind has been done.
*/
public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
{
// If the request was made using the SOGO connector, we must rewrite
// the content-type property. By default SabreDAV will send back
// text/x-vcard; charset=utf-8, but for SOGO we must strip that last
// part.
if (false === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird')) {
return;
}
$contentType = $propFind->get('{DAV:}getcontenttype');
if (null !== $contentType) {
list($part) = explode(';', $contentType);
if ('text/x-vcard' === $part || 'text/vcard' === $part) {
$propFind->set('{DAV:}getcontenttype', 'text/x-vcard');
}
}
}
/**
* This method is used to generate HTML output for the
* Sabre\DAV\Browser\Plugin. This allows us to generate an interface users
* can use to create new addressbooks.
*
* @param string $output
*
* @return bool
*/
public function htmlActionsPanel(DAV\INode $node, &$output)
{
if (!$node instanceof AddressBookHome) {
return;
}
$output .= '<tr><td colspan="2"><form method="post" action="">
<h3>Create new address book</h3>
<input type="hidden" name="sabreAction" value="mkcol" />
<input type="hidden" name="resourceType" value="{DAV:}collection,{'.self::NS_CARDDAV.'}addressbook" />
<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.
*/
public function httpAfterGet(RequestInterface $request, ResponseInterface $response)
{
$contentType = $response->getHeader('Content-Type');
if (null === $contentType || false === strpos($contentType, 'text/vcard')) {
return;
}
$target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType);
$newBody = $this->convertVCard(
$response->getBody(),
$target
);
$response->setBody($newBody);
$response->setHeader('Content-Type', $mimeType.'; charset=utf-8');
$response->setHeader('Content-Length', strlen($newBody));
}
/**
* This helper function performs the content-type negotiation for vcards.
*
* It will return one of the following strings:
* 1. vcard3
* 2. vcard4
* 3. jcard
*
* It defaults to vcard3.
*
* @param string $input
* @param string $mimeType
*
* @return string
*/
protected function negotiateVCard($input, &$mimeType = null)
{
$result = HTTP\negotiateContentType(
$input,
[
// Most often used mime-type. Version 3
'text/x-vcard',
// The correct standard mime-type. Defaults to version 3 as
// well.
'text/vcard',
// vCard 4
'text/vcard; version=4.0',
// vCard 3
'text/vcard; version=3.0',
// jCard
'application/vcard+json',
]
);
$mimeType = $result;
switch ($result) {
default:
case 'text/x-vcard':
case 'text/vcard':
case 'text/vcard; version=3.0':
$mimeType = 'text/vcard';
return 'vcard3';
case 'text/vcard; version=4.0':
return 'vcard4';
case 'application/vcard+json':
return 'jcard';
// @codeCoverageIgnoreStart
}
// @codeCoverageIgnoreEnd
}
/**
* Converts a vcard blob to a different version, or jcard.
*
* @param string|resource $data
* @param string $target
* @param array $propertiesFilter
*
* @return string
*/
protected function convertVCard($data, $target, array $propertiesFilter = null)
{
if (is_resource($data)) {
$data = stream_get_contents($data);
}
$input = VObject\Reader::read($data);
if (!empty($propertiesFilter)) {
$propertiesFilter = array_merge(['UID', 'VERSION', 'FN'], $propertiesFilter);
$keys = array_unique(array_map(function ($child) {
return $child->name;
}, $input->children()));
$keys = array_diff($keys, $propertiesFilter);
foreach ($keys as $key) {
unset($input->$key);
}
$data = $input->serialize();
}
$output = null;
try {
switch ($target) {
default:
case 'vcard3':
if (VObject\Document::VCARD30 === $input->getDocumentType()) {
// Do nothing
return $data;
}
$output = $input->convert(VObject\Document::VCARD30);
return $output->serialize();
case 'vcard4':
if (VObject\Document::VCARD40 === $input->getDocumentType()) {
// Do nothing
return $data;
}
$output = $input->convert(VObject\Document::VCARD40);
return $output->serialize();
case 'jcard':
$output = $input->convert(VObject\Document::VCARD40);
return json_encode($output);
}
} finally {
// Destroy circular references to PHP will GC the object.
$input->destroy();
if (!is_null($output)) {
$output->destroy();
}
}
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using DAV\Server::getPlugin
*
* @return string
*/
public function getPluginName()
{
return 'carddav';
}
/**
* 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
*/
public function getPluginInfo()
{
return [
'name' => $this->getPluginName(),
'description' => 'Adds support for CardDAV (rfc6352)',
'link' => 'http://sabre.io/dav/carddav/',
];
}
}

View File

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV;
use Sabre\DAV;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject;
/**
* VCF Exporter.
*
* This plugin adds the ability to export entire address books as .vcf files.
* This is useful for clients that don't support CardDAV yet. They often do
* support vcf files.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @author Thomas Tanghus (http://tanghus.net/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VCFExportPlugin extends DAV\ServerPlugin
{
/**
* Reference to Server class.
*
* @var DAV\Server
*/
protected $server;
/**
* Initializes the plugin and registers event handlers.
*/
public function initialize(DAV\Server $server)
{
$this->server = $server;
$this->server->on('method:GET', [$this, 'httpGet'], 90);
$server->on('browserButtonActions', function ($path, $node, &$actions) {
if ($node instanceof IAddressBook) {
$actions .= '<a href="'.htmlspecialchars($path, ENT_QUOTES, 'UTF-8').'?export"><span class="oi" data-glyph="book"></span></a>';
}
});
}
/**
* Intercepts GET requests on addressbook urls ending with ?export.
*
* @return bool
*/
public function httpGet(RequestInterface $request, ResponseInterface $response)
{
$queryParams = $request->getQueryParameters();
if (!array_key_exists('export', $queryParams)) {
return;
}
$path = $request->getPath();
$node = $this->server->tree->getNodeForPath($path);
if (!($node instanceof IAddressBook)) {
return;
}
$this->server->transactionType = 'get-addressbook-export';
// Checking ACL, if available.
if ($aclPlugin = $this->server->getPlugin('acl')) {
$aclPlugin->checkPrivileges($path, '{DAV:}read');
}
$nodes = $this->server->getPropertiesForPath($path, [
'{'.Plugin::NS_CARDDAV.'}address-data',
], 1);
$format = 'text/directory';
$output = null;
$filenameExtension = null;
switch ($format) {
case 'text/directory':
$output = $this->generateVCF($nodes);
$filenameExtension = '.vcf';
break;
}
$filename = preg_replace(
'/[^a-zA-Z0-9-_ ]/um',
'',
$node->getName()
);
$filename .= '-'.date('Y-m-d').$filenameExtension;
$response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"');
$response->setHeader('Content-Type', $format);
$response->setStatus(200);
$response->setBody($output);
// Returning false to break the event chain
return false;
}
/**
* Merges all vcard objects, and builds one big vcf export.
*
* @return string
*/
public function generateVCF(array $nodes)
{
$output = '';
foreach ($nodes as $node) {
if (!isset($node[200]['{'.Plugin::NS_CARDDAV.'}address-data'])) {
continue;
}
$nodeData = $node[200]['{'.Plugin::NS_CARDDAV.'}address-data'];
// Parsing this node so VObject can clean up the output.
$vcard = VObject\Reader::read($nodeData);
$output .= $vcard->serialize();
// Destroy circular references to PHP will GC the object.
$vcard->destroy();
}
return $output;
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using \Sabre\DAV\Server::getPlugin
*
* @return string
*/
public function getPluginName()
{
return 'vcf-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
*/
public function getPluginInfo()
{
return [
'name' => $this->getPluginName(),
'description' => 'Adds the ability to export CardDAV addressbooks as a single vCard file.',
'link' => 'http://sabre.io/dav/vcf-export-plugin/',
];
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Xml\Filter;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* AddressData parser.
*
* This class parses the {urn:ietf:params:xml:ns:carddav}address-data XML
* element, as defined in:
*
* http://tools.ietf.org/html/rfc6352#section-10.4
*
* This element is used in two distinct places, but this one specifically
* encodes the address-data element as it appears in the addressbook-query
* adressbook-multiget REPORT requests.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AddressData implements XmlDeserializable
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = [
'contentType' => $reader->getAttribute('content-type') ?: 'text/vcard',
'version' => $reader->getAttribute('version') ?: '3.0',
];
$elems = (array) $reader->parseInnerTree();
$elems = array_filter($elems, function ($element) {
return '{urn:ietf:params:xml:ns:carddav}prop' === $element['name'] &&
isset($element['attributes']['name']);
});
$result['addressDataProperties'] = array_map(function ($element) {
return $element['attributes']['name'];
}, $elems);
return $result;
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Xml\Filter;
use Sabre\CardDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Element;
use Sabre\Xml\Reader;
/**
* ParamFilter parser.
*
* This class parses the {urn:ietf:params:xml:ns:carddav}param-filter XML
* element, as defined in:
*
* http://tools.ietf.org/html/rfc6352#section-10.5.2
*
* The result will be spit out as an array.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class ParamFilter implements Element
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = [
'name' => null,
'is-not-defined' => false,
'text-match' => null,
];
$att = $reader->parseAttributes();
$result['name'] = $att['name'];
$elems = $reader->parseInnerTree();
if (is_array($elems)) {
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{'.Plugin::NS_CARDDAV.'}is-not-defined':
$result['is-not-defined'] = true;
break;
case '{'.Plugin::NS_CARDDAV.'}text-match':
$matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains';
if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) {
throw new BadRequest('Unknown match-type: '.$matchType);
}
$result['text-match'] = [
'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap',
'value' => $elem['value'],
'match-type' => $matchType,
];
break;
}
}
}
return $result;
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Xml\Filter;
use Sabre\CardDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* PropFilter parser.
*
* This class parses the {urn:ietf:params:xml:ns:carddav}prop-filter XML
* element, as defined in:
*
* http://tools.ietf.org/html/rfc6352#section-10.5.1
*
* The result will be spit out as an array.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class PropFilter implements XmlDeserializable
{
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$result = [
'name' => null,
'test' => 'anyof',
'is-not-defined' => false,
'param-filters' => [],
'text-matches' => [],
];
$att = $reader->parseAttributes();
$result['name'] = $att['name'];
if (isset($att['test']) && 'allof' === $att['test']) {
$result['test'] = 'allof';
}
$elems = $reader->parseInnerTree();
if (is_array($elems)) {
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{'.Plugin::NS_CARDDAV.'}param-filter':
$result['param-filters'][] = $elem['value'];
break;
case '{'.Plugin::NS_CARDDAV.'}is-not-defined':
$result['is-not-defined'] = true;
break;
case '{'.Plugin::NS_CARDDAV.'}text-match':
$matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains';
if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) {
throw new BadRequest('Unknown match-type: '.$matchType);
}
$result['text-matches'][] = [
'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'],
'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap',
'value' => $elem['value'],
'match-type' => $matchType,
];
break;
}
}
}
return $result;
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Xml\Property;
use Sabre\CardDAV\Plugin;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* Supported-address-data property.
*
* This property is a representation of the supported-address-data property
* in the CardDAV namespace.
*
* This property is defined in:
*
* http://tools.ietf.org/html/rfc6352#section-6.2.2
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SupportedAddressData implements XmlSerializable
{
/**
* supported versions.
*
* @var array
*/
protected $supportedData = [];
/**
* Creates the property.
*/
public function __construct(array $supportedData = null)
{
if (is_null($supportedData)) {
$supportedData = [
['contentType' => 'text/vcard', 'version' => '3.0'],
['contentType' => 'text/vcard', 'version' => '4.0'],
['contentType' => 'application/vcard+json', 'version' => '4.0'],
];
}
$this->supportedData = $supportedData;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
foreach ($this->supportedData as $supported) {
$writer->startElement('{'.Plugin::NS_CARDDAV.'}address-data-type');
$writer->writeAttributes([
'content-type' => $supported['contentType'],
'version' => $supported['version'],
]);
$writer->endElement(); // address-data-type
}
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Xml\Property;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
/**
* supported-collation-set property.
*
* This property is a representation of the supported-collation-set property
* in the CardDAV namespace.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class SupportedCollationSet implements XmlSerializable
{
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializable should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*/
public function xmlSerialize(Writer $writer)
{
foreach (['i;ascii-casemap', 'i;octet', 'i;unicode-casemap'] as $coll) {
$writer->writeElement('{urn:ietf:params:xml:ns:carddav}supported-collation', $coll);
}
}
}

View File

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Xml\Request;
use Sabre\CardDAV\Plugin;
use Sabre\Uri;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* AddressBookMultiGetReport request parser.
*
* This class parses the {urn:ietf:params:xml:ns:carddav}addressbook-multiget
* REPORT, as defined in:
*
* http://tools.ietf.org/html/rfc6352#section-8.7
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AddressBookMultiGetReport implements XmlDeserializable
{
/**
* An array with requested properties.
*
* @var array
*/
public $properties;
/**
* This is an array with the urls that are being requested.
*
* @var array
*/
public $hrefs;
/**
* The mimetype of the content that should be returend. Usually
* text/vcard.
*
* @var string
*/
public $contentType = null;
/**
* The version of vcard data that should be returned. Usually 3.0,
* referring to vCard 3.0.
*
* @var string
*/
public $version = null;
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = $reader->parseInnerTree([
'{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData',
'{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
]);
$newProps = [
'hrefs' => [],
'properties' => [],
];
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{DAV:}prop':
$newProps['properties'] = array_keys($elem['value']);
if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) {
$newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'];
}
break;
case '{DAV:}href':
$newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']);
break;
}
}
$obj = new self();
foreach ($newProps as $key => $value) {
$obj->$key = $value;
}
return $obj;
}
}

View File

@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Sabre\CardDAV\Xml\Request;
use Sabre\CardDAV\Plugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* AddressBookQueryReport request parser.
*
* This class parses the {urn:ietf:params:xml:ns:carddav}addressbook-query
* REPORT, as defined in:
*
* http://tools.ietf.org/html/rfc6352#section-8.6
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://sabre.io/license/ Modified BSD License
*/
class AddressBookQueryReport implements XmlDeserializable
{
/**
* An array with requested properties.
*
* @var array
*/
public $properties;
/**
* An array with requested vcard properties.
*
* @var array
*/
public $addressDataProperties = [];
/**
* List of property/component filters.
*
* This is an array with filters. Every item is a property filter. Every
* property filter has the following keys:
* * name - name of the component to filter on
* * test - anyof or allof
* * is-not-defined - Test for non-existence
* * param-filters - A list of parameter filters on the property
* * text-matches - A list of text values the filter needs to match
*
* Each param-filter has the following keys:
* * name - name of the parameter
* * is-not-defined - Test for non-existence
* * text-match - Match the parameter value
*
* Each text-match in property filters, and the single text-match in
* param-filters have the following keys:
*
* * value - value to match
* * match-type - contains, starts-with, ends-with, equals
* * negate-condition - Do the opposite match
* * collation - Usually i;unicode-casemap
*
* @var array
*/
public $filters;
/**
* The number of results the client wants.
*
* null means it wasn't specified, which in most cases means 'all results'.
*
* @var int|null
*/
public $limit;
/**
* Either 'anyof' or 'allof'.
*
* @var string
*/
public $test;
/**
* The mimetype of the content that should be returend. Usually
* text/vcard.
*
* @var string
*/
public $contentType = null;
/**
* The version of vcard data that should be returned. Usually 3.0,
* referring to vCard 3.0.
*
* @var string
*/
public $version = null;
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @return mixed
*/
public static function xmlDeserialize(Reader $reader)
{
$elems = (array) $reader->parseInnerTree([
'{urn:ietf:params:xml:ns:carddav}prop-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter',
'{urn:ietf:params:xml:ns:carddav}param-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter',
'{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData',
'{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
]);
$newProps = [
'filters' => null,
'properties' => [],
'test' => 'anyof',
'limit' => null,
];
if (!is_array($elems)) {
$elems = [];
}
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{DAV:}prop':
$newProps['properties'] = array_keys($elem['value']);
if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) {
$newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'];
}
break;
case '{'.Plugin::NS_CARDDAV.'}filter':
if (!is_null($newProps['filters'])) {
throw new BadRequest('You can only include 1 {'.Plugin::NS_CARDDAV.'}filter element');
}
if (isset($elem['attributes']['test'])) {
$newProps['test'] = $elem['attributes']['test'];
if ('allof' !== $newProps['test'] && 'anyof' !== $newProps['test']) {
throw new BadRequest('The "test" attribute must be one of "allof" or "anyof"');
}
}
$newProps['filters'] = [];
foreach ((array) $elem['value'] as $subElem) {
if ($subElem['name'] === '{'.Plugin::NS_CARDDAV.'}prop-filter') {
$newProps['filters'][] = $subElem['value'];
}
}
break;
case '{'.Plugin::NS_CARDDAV.'}limit':
foreach ($elem['value'] as $child) {
if ($child['name'] === '{'.Plugin::NS_CARDDAV.'}nresults') {
$newProps['limit'] = (int) $child['value'];
}
}
break;
}
}
if (is_null($newProps['filters'])) {
/*
* We are supposed to throw this error, but KDE sometimes does not
* include the filter element, and we need to treat it as if no
* filters are supplied
*/
//throw new BadRequest('The {' . Plugin::NS_CARDDAV . '}filter element is required for this request');
$newProps['filters'] = [];
}
$obj = new self();
foreach ($newProps as $key => $value) {
$obj->$key = $value;
}
return $obj;
}
}

View File

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Sabre\DAV\Auth\Backend;
use Sabre\HTTP;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* HTTP Basic authentication backend class.
*
* This class can be used by authentication objects wishing to use HTTP Basic
* Most of the digest logic is handled, implementors just need to worry about
* the validateUserPass method.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author James David Low (http://jameslow.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class AbstractBasic implements BackendInterface
{
/**
* Authentication Realm.
*
* The realm is often displayed by browser clients when showing the
* authentication dialog.
*
* @var string
*/
protected $realm = 'sabre/dav';
/**
* This is the prefix that will be used to generate principal urls.
*
* @var string
*/
protected $principalPrefix = 'principals/';
/**
* Validates a username and password.
*
* This method should return true or false depending on if login
* succeeded.
*
* @param string $username
* @param string $password
*
* @return bool
*/
abstract protected function validateUserPass($username, $password);
/**
* Sets the authentication realm for this backend.
*
* @param string $realm
*/
public function setRealm($realm)
{
$this->realm = $realm;
}
/**
* When this method is called, the backend must check if authentication was
* successful.
*
* The returned value must be one of the following
*
* [true, "principals/username"]
* [false, "reason for failure"]
*
* If authentication was successful, it's expected that the authentication
* backend returns a so-called principal url.
*
* Examples of a principal url:
*
* principals/admin
* principals/user1
* principals/users/joe
* principals/uid/123457
*
* If you don't use WebDAV ACL (RFC3744) we recommend that you simply
* return a string such as:
*
* principals/users/[username]
*
* @return array
*/
public function check(RequestInterface $request, ResponseInterface $response)
{
$auth = new HTTP\Auth\Basic(
$this->realm,
$request,
$response
);
$userpass = $auth->getCredentials();
if (!$userpass) {
return [false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"];
}
if (!$this->validateUserPass($userpass[0], $userpass[1])) {
return [false, 'Username or password was incorrect'];
}
return [true, $this->principalPrefix.$userpass[0]];
}
/**
* This method is called when a user could not be authenticated, and
* authentication was required for the current request.
*
* This gives you the opportunity to set authentication headers. The 401
* status code will already be set.
*
* In this case of Basic Auth, this would for example mean that the
* following header needs to be set:
*
* $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
*
* Keep in mind that in the case of multiple authentication backends, other
* WWW-Authenticate headers may already have been set, and you'll want to
* append your own WWW-Authenticate header instead of overwriting the
* existing one.
*/
public function challenge(RequestInterface $request, ResponseInterface $response)
{
$auth = new HTTP\Auth\Basic(
$this->realm,
$request,
$response
);
$auth->requireLogin();
}
}

View File

@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Sabre\DAV\Auth\Backend;
use Sabre\HTTP;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* HTTP Bearer authentication backend class.
*
* This class can be used by authentication objects wishing to use HTTP Bearer
* Most of the digest logic is handled, implementors just need to worry about
* the validateBearerToken method.
*
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
* @author François Kooman (https://tuxed.net/)
* @author James David Low (http://jameslow.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class AbstractBearer implements BackendInterface
{
/**
* Authentication Realm.
*
* The realm is often displayed by browser clients when showing the
* authentication dialog.
*
* @var string
*/
protected $realm = 'sabre/dav';
/**
* Validates a Bearer token.
*
* This method should return the full principal url, or false if the
* token was incorrect.
*
* @param string $bearerToken
*
* @return string|false
*/
abstract protected function validateBearerToken($bearerToken);
/**
* Sets the authentication realm for this backend.
*
* @param string $realm
*/
public function setRealm($realm)
{
$this->realm = $realm;
}
/**
* When this method is called, the backend must check if authentication was
* successful.
*
* The returned value must be one of the following
*
* [true, "principals/username"]
* [false, "reason for failure"]
*
* If authentication was successful, it's expected that the authentication
* backend returns a so-called principal url.
*
* Examples of a principal url:
*
* principals/admin
* principals/user1
* principals/users/joe
* principals/uid/123457
*
* If you don't use WebDAV ACL (RFC3744) we recommend that you simply
* return a string such as:
*
* principals/users/[username]
*
* @return array
*/
public function check(RequestInterface $request, ResponseInterface $response)
{
$auth = new HTTP\Auth\Bearer(
$this->realm,
$request,
$response
);
$bearerToken = $auth->getToken($request);
if (!$bearerToken) {
return [false, "No 'Authorization: Bearer' header found. Either the client didn't send one, or the server is mis-configured"];
}
$principalUrl = $this->validateBearerToken($bearerToken);
if (!$principalUrl) {
return [false, 'Bearer token was incorrect'];
}
return [true, $principalUrl];
}
/**
* This method is called when a user could not be authenticated, and
* authentication was required for the current request.
*
* This gives you the opportunity to set authentication headers. The 401
* status code will already be set.
*
* In this case of Bearer Auth, this would for example mean that the
* following header needs to be set:
*
* $response->addHeader('WWW-Authenticate', 'Bearer realm=SabreDAV');
*
* Keep in mind that in the case of multiple authentication backends, other
* WWW-Authenticate headers may already have been set, and you'll want to
* append your own WWW-Authenticate header instead of overwriting the
* existing one.
*/
public function challenge(RequestInterface $request, ResponseInterface $response)
{
$auth = new HTTP\Auth\Bearer(
$this->realm,
$request,
$response
);
$auth->requireLogin();
}
}

View File

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Sabre\DAV\Auth\Backend;
use Sabre\DAV;
use Sabre\HTTP;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* HTTP Digest authentication backend class.
*
* This class can be used by authentication objects wishing to use HTTP Digest
* Most of the digest logic is handled, implementors just need to worry about
* the getDigestHash method
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
abstract class AbstractDigest implements BackendInterface
{
/**
* Authentication Realm.
*
* The realm is often displayed by browser clients when showing the
* authentication dialog.
*
* @var string
*/
protected $realm = 'SabreDAV';
/**
* This is the prefix that will be used to generate principal urls.
*
* @var string
*/
protected $principalPrefix = 'principals/';
/**
* Sets the authentication realm for this backend.
*
* Be aware that for Digest authentication, the realm influences the digest
* hash. Choose the realm wisely, because if you change it later, all the
* existing hashes will break and nobody can authenticate.
*
* @param string $realm
*/
public function setRealm($realm)
{
$this->realm = $realm;
}
/**
* Returns a users digest hash based on the username and realm.
*
* If the user was not known, null must be returned.
*
* @param string $realm
* @param string $username
*
* @return string|null
*/
abstract public function getDigestHash($realm, $username);
/**
* When this method is called, the backend must check if authentication was
* successful.
*
* The returned value must be one of the following
*
* [true, "principals/username"]
* [false, "reason for failure"]
*
* If authentication was successful, it's expected that the authentication
* backend returns a so-called principal url.
*
* Examples of a principal url:
*
* principals/admin
* principals/user1
* principals/users/joe
* principals/uid/123457
*
* If you don't use WebDAV ACL (RFC3744) we recommend that you simply
* return a string such as:
*
* principals/users/[username]
*
* @return array
*/
public function check(RequestInterface $request, ResponseInterface $response)
{
$digest = new HTTP\Auth\Digest(
$this->realm,
$request,
$response
);
$digest->init();
$username = $digest->getUsername();
// No username was given
if (!$username) {
return [false, "No 'Authorization: Digest' header found. Either the client didn't send one, or the server is misconfigured"];
}
$hash = $this->getDigestHash($this->realm, $username);
// If this was false, the user account didn't exist
if (false === $hash || is_null($hash)) {
return [false, 'Username or password was incorrect'];
}
if (!is_string($hash)) {
throw new DAV\Exception('The returned value from getDigestHash must be a string or null');
}
// If this was false, the password or part of the hash was incorrect.
if (!$digest->validateA1($hash)) {
return [false, 'Username or password was incorrect'];
}
return [true, $this->principalPrefix.$username];
}
/**
* This method is called when a user could not be authenticated, and
* authentication was required for the current request.
*
* This gives you the opportunity to set authentication headers. The 401
* status code will already be set.
*
* In this case of Basic Auth, this would for example mean that the
* following header needs to be set:
*
* $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV');
*
* Keep in mind that in the case of multiple authentication backends, other
* WWW-Authenticate headers may already have been set, and you'll want to
* append your own WWW-Authenticate header instead of overwriting the
* existing one.
*/
public function challenge(RequestInterface $request, ResponseInterface $response)
{
$auth = new HTTP\Auth\Digest(
$this->realm,
$request,
$response
);
$auth->init();
$oldStatus = $response->getStatus() ?: 200;
$auth->requireLogin();
// Preventing the digest utility from modifying the http status code,
// this should be handled by the main plugin.
$response->setStatus($oldStatus);
}
}

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