Ultraworked with Sisyphus (https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
269 lines
8.1 KiB
PHP
269 lines
8.1 KiB
PHP
<?php
|
|
/**
|
|
* Save as: ./sabredav/server.php
|
|
*
|
|
* SabreDAV CardDAV and CalDAV server implementation with PostgreSQL
|
|
*/
|
|
|
|
use Sabre\DAV;
|
|
use Sabre\CalDAV;
|
|
use Sabre\CardDAV;
|
|
use Sabre\DAVACL;
|
|
|
|
require 'vendor/autoload.php';
|
|
|
|
// PostgreSQL connection
|
|
$dbHost = getenv('POSTGRES_HOST') ?: 'postgres';
|
|
$dbPort = getenv('POSTGRES_PORT') ?: '5432';
|
|
$dbName = getenv('POSTGRES_DB') ?: 'mydb';
|
|
$dbUser = getenv('POSTGRES_USER') ?: 'myuser';
|
|
$dbPass = getenv('POSTGRES_PASSWORD') ?: 'mypassword';
|
|
|
|
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $dbHost, $dbPort, $dbName);
|
|
|
|
try {
|
|
$pdo = new PDO($dsn, $dbUser, $dbPass, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
|
]);
|
|
} catch (PDOException $e) {
|
|
error_log("Database connection failed: " . $e->getMessage());
|
|
http_response_code(500);
|
|
die("Database connection failed");
|
|
}
|
|
|
|
// Create tables if they don't exist
|
|
$pdo->exec("
|
|
CREATE TABLE IF NOT EXISTS dav_principals (
|
|
id SERIAL PRIMARY KEY,
|
|
uri VARCHAR(255) NOT NULL UNIQUE,
|
|
email VARCHAR(255),
|
|
displayname VARCHAR(255)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dav_calendars (
|
|
id SERIAL PRIMARY KEY,
|
|
principaluri VARCHAR(255) NOT NULL,
|
|
displayname VARCHAR(255),
|
|
uri VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
calendarorder INT DEFAULT 0,
|
|
calendarcolor VARCHAR(10),
|
|
timezone TEXT,
|
|
components VARCHAR(255),
|
|
transparent BOOLEAN DEFAULT FALSE,
|
|
UNIQUE(principaluri, uri)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dav_calendarobjects (
|
|
id SERIAL PRIMARY KEY,
|
|
calendardata TEXT,
|
|
uri VARCHAR(255) NOT NULL,
|
|
calendarid INTEGER NOT NULL REFERENCES dav_calendars(id) ON DELETE CASCADE,
|
|
lastmodified INTEGER,
|
|
etag VARCHAR(32),
|
|
size INTEGER,
|
|
componenttype VARCHAR(8),
|
|
firstoccurence INTEGER,
|
|
lastoccurence INTEGER,
|
|
uid VARCHAR(255),
|
|
UNIQUE(calendarid, uri)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dav_addressbooks (
|
|
id SERIAL PRIMARY KEY,
|
|
principaluri VARCHAR(255) NOT NULL,
|
|
displayname VARCHAR(255),
|
|
uri VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
synctoken INTEGER DEFAULT 1,
|
|
UNIQUE(principaluri, uri)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dav_cards (
|
|
id SERIAL PRIMARY KEY,
|
|
addressbookid INTEGER NOT NULL REFERENCES dav_addressbooks(id) ON DELETE CASCADE,
|
|
carddata TEXT,
|
|
uri VARCHAR(255) NOT NULL,
|
|
lastmodified INTEGER,
|
|
etag VARCHAR(32),
|
|
size INTEGER,
|
|
UNIQUE(addressbookid, uri)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dav_addressbookchanges (
|
|
id SERIAL PRIMARY KEY,
|
|
uri VARCHAR(255) NOT NULL,
|
|
synctoken INTEGER NOT NULL,
|
|
addressbookid INTEGER NOT NULL REFERENCES dav_addressbooks(id) ON DELETE CASCADE,
|
|
operation SMALLINT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dav_calendarchanges (
|
|
id SERIAL PRIMARY KEY,
|
|
uri VARCHAR(255) NOT NULL,
|
|
synctoken INTEGER NOT NULL,
|
|
calendarid INTEGER NOT NULL REFERENCES dav_calendars(id) ON DELETE CASCADE,
|
|
operation SMALLINT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS dav_users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(255) NOT NULL UNIQUE,
|
|
digesta1 VARCHAR(32) NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_calendarobjects_calendarid ON dav_calendarobjects(calendarid);
|
|
CREATE INDEX IF NOT EXISTS idx_cards_addressbookid ON dav_cards(addressbookid);
|
|
");
|
|
|
|
// Create default principal if not exists
|
|
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_principals");
|
|
if ($stmt->fetchColumn() == 0) {
|
|
$pdo->exec("
|
|
INSERT INTO dav_principals (uri, email, displayname)
|
|
VALUES ('principals/default', 'admin@example.com', 'Default User')
|
|
");
|
|
}
|
|
|
|
// Create default addressbook if not exists
|
|
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_addressbooks");
|
|
if ($stmt->fetchColumn() == 0) {
|
|
$pdo->exec("
|
|
INSERT INTO dav_addressbooks (principaluri, displayname, uri, description, synctoken)
|
|
VALUES ('principals/default', 'Contacts', 'default', 'Default address book', 1)
|
|
");
|
|
}
|
|
|
|
// Create default calendar if not exists
|
|
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_calendars");
|
|
if ($stmt->fetchColumn() == 0) {
|
|
$pdo->exec("
|
|
INSERT INTO dav_calendars (principaluri, displayname, uri, description, components, transparent)
|
|
VALUES ('principals/default', 'Default Calendar', 'default', 'Default calendar', 'VEVENT,VTODO', false)
|
|
");
|
|
}
|
|
|
|
// Create default user if not exists (username: admin, password from DAV_ADMIN_PASSWORD env)
|
|
$davPassword = getenv('DAV_ADMIN_PASSWORD') ?: 'admin';
|
|
$stmt = $pdo->query("SELECT COUNT(*) FROM dav_users");
|
|
if ($stmt->fetchColumn() == 0) {
|
|
$digest = md5('admin:SabreDAV:' . $davPassword);
|
|
$pdo->exec("
|
|
INSERT INTO dav_users (username, digesta1)
|
|
VALUES ('admin', '$digest')
|
|
");
|
|
}
|
|
|
|
// Custom auth backend for PostgreSQL
|
|
class PostgreSQLAuthBackend extends DAV\Auth\Backend\AbstractDigest {
|
|
protected $pdo;
|
|
|
|
public function __construct(PDO $pdo) {
|
|
$this->pdo = $pdo;
|
|
$this->realm = 'SabreDAV';
|
|
}
|
|
|
|
public function getDigestHash($realm, $username) {
|
|
$stmt = $this->pdo->prepare('SELECT digesta1 FROM dav_users WHERE username = ?');
|
|
$stmt->execute([$username]);
|
|
return $stmt->fetchColumn() ?: null;
|
|
}
|
|
}
|
|
|
|
// Custom principal backend for PostgreSQL
|
|
class PostgreSQLPrincipalBackend extends DAVACL\PrincipalBackend\AbstractBackend {
|
|
protected $pdo;
|
|
protected $tableName = 'dav_principals';
|
|
|
|
public function __construct(PDO $pdo) {
|
|
$this->pdo = $pdo;
|
|
}
|
|
|
|
public function getPrincipalsByPrefix($prefixPath) {
|
|
$stmt = $this->pdo->query('SELECT uri, email, displayname FROM ' . $this->tableName);
|
|
$principals = [];
|
|
|
|
while ($row = $stmt->fetch()) {
|
|
if (strpos($row['uri'], $prefixPath) !== 0) continue;
|
|
|
|
$principals[] = [
|
|
'uri' => $row['uri'],
|
|
'{DAV:}displayname' => $row['displayname'],
|
|
'{http://sabredav.org/ns}email-address' => $row['email'],
|
|
];
|
|
}
|
|
return $principals;
|
|
}
|
|
|
|
public function getPrincipalByPath($path) {
|
|
$stmt = $this->pdo->prepare('SELECT uri, email, displayname FROM ' . $this->tableName . ' WHERE uri = ?');
|
|
$stmt->execute([$path]);
|
|
$row = $stmt->fetch();
|
|
|
|
if (!$row) return null;
|
|
|
|
return [
|
|
'uri' => $row['uri'],
|
|
'{DAV:}displayname' => $row['displayname'],
|
|
'{http://sabredav.org/ns}email-address' => $row['email'],
|
|
];
|
|
}
|
|
|
|
public function updatePrincipal($path, $mutations) {
|
|
return 0;
|
|
}
|
|
|
|
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
|
|
return [];
|
|
}
|
|
|
|
public function getGroupMemberSet($principal) {
|
|
return [];
|
|
}
|
|
|
|
public function getGroupMembership($principal) {
|
|
return [];
|
|
}
|
|
|
|
public function setGroupMemberSet($principal, array $members) {
|
|
throw new DAV\Exception('Setting group members is not supported');
|
|
}
|
|
}
|
|
|
|
// Backends
|
|
$authBackend = new PostgreSQLAuthBackend($pdo);
|
|
$principalBackend = new PostgreSQLPrincipalBackend($pdo);
|
|
|
|
// Use SabreDAV's PostgreSQL backends for CardDAV and CalDAV
|
|
$carddavBackend = new CardDAV\Backend\PDO($pdo);
|
|
$carddavBackend->tableName = 'dav_cards';
|
|
$carddavBackend->addressBooksTableName = 'dav_addressbooks';
|
|
$carddavBackend->addressBookChangesTableName = 'dav_addressbookchanges';
|
|
|
|
$caldavBackend = new CalDAV\Backend\PDO($pdo);
|
|
$caldavBackend->tableName = 'dav_calendarobjects';
|
|
$caldavBackend->calendarTableName = 'dav_calendars';
|
|
$caldavBackend->calendarChangesTableName = 'dav_calendarchanges';
|
|
|
|
// Directory structure
|
|
$tree = [
|
|
new DAVACL\PrincipalCollection($principalBackend),
|
|
new CalDAV\CalendarRoot($principalBackend, $caldavBackend),
|
|
new CardDAV\AddressBookRoot($principalBackend, $carddavBackend),
|
|
];
|
|
|
|
$server = new DAV\Server($tree);
|
|
$server->setBaseUri('/dav/');
|
|
|
|
// Plugins
|
|
$server->addPlugin(new DAV\Auth\Plugin($authBackend));
|
|
$server->addPlugin(new DAVACL\Plugin());
|
|
$server->addPlugin(new DAV\Browser\Plugin());
|
|
$server->addPlugin(new CalDAV\Plugin());
|
|
$server->addPlugin(new CardDAV\Plugin());
|
|
$server->addPlugin(new CalDAV\Schedule\Plugin());
|
|
$server->addPlugin(new DAV\Sync\Plugin());
|
|
|
|
// Run the server
|
|
$server->exec(); |