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();