🏗️(caldav) migrate from davical to sabre/dav

This commit is contained in:
Sylvain Zimmer
2026-01-11 02:28:04 +01:00
parent a36348ead1
commit bc801d3007
35 changed files with 1059 additions and 549 deletions

View File

@@ -1,39 +0,0 @@
# DAViCal CalDAV Server
# Based on Debian with Apache and PHP
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
# Install dependencies
RUN apt-get update && apt-get install -y \
apache2 \
libapache2-mod-php \
php-pgsql \
php-xml \
php-curl \
php-imap \
php-ldap \
davical \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Enable required Apache modules
RUN a2enmod rewrite
# Copy Apache configuration
COPY davical.conf /etc/apache2/sites-available/davical.conf
RUN a2dissite 000-default && a2ensite davical
# Copy DAViCal configuration
COPY config.php /etc/davical/config.php
# Copy and setup entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Set permissions
RUN chown -R www-data:www-data /var/log/apache2
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,64 +0,0 @@
<?php
/**
* DAViCal Configuration
* This file is mounted as /etc/davical/config.php
* Overrides the default config generated by the fintechstudios/davical image
*/
// Database connection - uses shared calendars database in public schema
// The image will set these from PGHOST, PGDATABASE, PGUSER, PGPASSWORD
$c->pg_connect[] = 'host=' . getenv('PGHOST') . ' port=' . (getenv('PGPORT') ?: '5432') . ' dbname=' . getenv('PGDATABASE') . ' user=' . getenv('PGUSER') . ' password=' . getenv('PGPASSWORD');
// System name
$c->system_name = 'Calendars DAViCal Server';
// Admin email
$c->admin_email = 'admin@example.com';
// Allow public access for CalDAV discovery
$c->public_freebusy_url = true;
// Default locale
$c->default_locale = 'en_US.UTF-8';
// Logging - enable for debugging authentication issues
$c->log_caldav_queries = true;
// Trust proxy headers for auth
$c->trust_x_forwarded = true;
// Configure base path when behind reverse proxy
// Override SCRIPT_NAME so DAViCal generates correct URLs
// DAViCal uses $_SERVER['SCRIPT_NAME'] to determine the base path for URLs
// We set it to the proxy path WITHOUT /caldav.php since DAViCal will add that itself
if (isset($_SERVER['HTTP_X_FORWARDED_PREFIX'])) {
$_SERVER['SCRIPT_NAME'] = rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/');
} elseif (isset($_SERVER['HTTP_X_SCRIPT_NAME'])) {
$_SERVER['SCRIPT_NAME'] = rtrim($_SERVER['HTTP_X_SCRIPT_NAME'], '/');
}
// Custom authentication function to use X-Forwarded-User header
// This function is called by DAViCal's authentication system
function authenticate_via_forwarded_user( $username, $password ) {
// Check if X-Forwarded-User header is present
if (isset($_SERVER['HTTP_X_FORWARDED_USER'])) {
$forwarded_user = trim($_SERVER['HTTP_X_FORWARDED_USER']);
// If the username from Basic Auth matches X-Forwarded-User, authenticate
// Users with password '*' are externally authenticated
if (strtolower($username) === strtolower($forwarded_user)) {
// Return the username to authenticate as this user
// DAViCal will check if user exists and has password '*'
return $forwarded_user;
}
}
// Fall back to standard authentication
return false;
}
// Use custom authentication hook
$c->authenticate_hook = array(
'call' => 'authenticate_via_forwarded_user',
'config' => array()
);

View File

@@ -1,22 +0,0 @@
<VirtualHost *:80>
ServerName localhost
DocumentRoot /usr/share/davical/htdocs
DirectoryIndex index.php
Alias /images/ /usr/share/davical/htdocs/images/
<Directory /usr/share/davical/htdocs>
AllowOverride All
Require all granted
</Directory>
AcceptPathInfo On
# CalDAV principal URL
RewriteEngine On
RewriteRule ^/caldav/(.*)$ /caldav.php/$1 [L]
RewriteRule ^/\.well-known/caldav /caldav.php [R=301,L]
ErrorLog ${APACHE_LOG_DIR}/davical_error.log
CustomLog ${APACHE_LOG_DIR}/davical_access.log combined
</VirtualHost>

View File

@@ -1,93 +0,0 @@
#!/bin/bash
###
# Run DB migrations necessary to use Davical.
# Will create the database on first-run, and only run necessary migrations on subsequent runs.
#
# Requires the following environment variables in addition to the container variables.
# - ROOT_PGUSER
# - ROOT_PGPASSWORD
# - DAVICAL_ADMIN_PASS
###
set -e
if [ -z ${ROOT_PGUSER+x} ]; then
echo "ROOT_PGUSER must be set"
exit 1
fi
if [ -z ${ROOT_PGPASSWORD+x} ]; then
echo "ROOT_PGPASSWORD must be set"
exit 1
fi
if [ -z ${DAVICAL_ADMIN_PASS+x} ]; then
echo "DAVICAL_ADMIN_PASS must be set"
exit 1
fi
if [ -z ${DBA_PGPASSWORD+x} ]; then
DBA_PGPASSWORD=$PGPASSWORD
fi
if [ -z ${DAVICAL_SCHEMA+x} ]; then
DAVICAL_SCHEMA=$DBA_PGUSER
fi
# store PG environment so it can be overridden as-needed
DAVICAL_PGUSER=$PGUSER
DAVICAL_PGPASSWORD=$PGPASSWORD
DAVICAL_PGDATABASE=$PGDATABASE
run_migrations() {
echo "Running dba/update-davical-database, which should automatically apply any necessary DB migrations."
/usr/share/davical/dba/update-davical-database \
--dbname $DAVICAL_PGDATABASE \
--dbuser $DBA_PGUSER \
--dbhost $PGHOST \
--dbpass $DBA_PGPASSWORD \
--appuser $DAVICAL_PGUSER \
--owner $DBA_PGUSER
}
export PGUSER=$ROOT_PGUSER
export PGPASSWORD=$ROOT_PGPASSWORD
export PGDATABASE=
# Wait for PG connection
retries=10
until pg_isready -q -t 3; do
[[ retries -eq 0 ]] && echo "Could not connect to Postgres" && exit 1
echo "Waiting for Postgres to be available"
retries=$((retries-1))
sleep 1
done
# Check whether the database has already been setup, with awl tables.
tables=$(psql -d $DAVICAL_PGDATABASE -c "\\dt")
if echo "$tables" | grep -q "awl_db_revision"; then
# The database already exists - just run any outstanding migrations
run_migrations
exit 0
fi
echo "Database has not been created - running first-time database setup"
# the rest of the commands are run as the dba superuser
export PGUSER=$DBA_PGUSER
export PGPASSWORD=$DBA_PGPASSWORD
export PGDATABASE=$DAVICAL_PGDATABASE
psql -qXAt -f /usr/share/awl/dba/awl-tables.sql
psql -qXAt -f /usr/share/awl/dba/schema-management.sql
psql -qXAt -f /usr/share/davical/dba/davical.sql
run_migrations
psql -qXAt -f /usr/share/davical/dba/base-data.sql
# DAViCal only uses salted SHA1 at-best, but it's better than storing the password in plaintext!
# see https://wiki.davical.org/index.php?title=Force_Admin_Password
# from https://gitlab.com/davical-project/awl/-/blob/3f044e2dc8435c2eeba61a3c41ec11c820711ab3/inc/DataUpdate.php#L48-58
salted_password=$(php -r 'require "/usr/share/awl/inc/AWLUtilities.php"; echo session_salted_sha1($argv[1]);' "$DAVICAL_ADMIN_PASS")
psql -qX \
-v pw="'$salted_password'" \
<<EOF
UPDATE usr SET password = :pw WHERE user_no = 1;
EOF

View File

@@ -37,10 +37,4 @@ EOSQL
# Create databases for all services
# The main 'calendar' database is created by default via POSTGRES_DB
# DAViCal database
create_database "davical" "davical" "davical_pass"
# Keycloak database
create_database "keycloak" "keycloak" "keycloak_pass"
echo "All databases initialized successfully!"

View File

@@ -1,6 +1,6 @@
#!/bin/sh
# Initialize shared calendars database for local development
# All services (Django, DAViCal, Keycloak) use the same database in public schema
# All services (Django, Caldav, Keycloak) use the same database in public schema
# This script runs as POSTGRES_USER on first database initialization
set -e

View File

@@ -0,0 +1,53 @@
# sabre/dav CalDAV Server
# Based on Debian with Apache and PHP
FROM php:8.2-apache-bookworm
ENV DEBIAN_FRONTEND=noninteractive
# Install dependencies
RUN apt-get update && apt-get install -y \
libpq-dev \
postgresql-client \
git \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-install pdo pdo_pgsql
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Create application directory
WORKDIR /var/www/sabredav
# Copy composer files
COPY composer.json ./
# Install sabre/dav and dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Copy server configuration
COPY server.php ./
COPY sabredav.conf /etc/apache2/sites-available/sabredav.conf
COPY init-database.sh /usr/local/bin/init-database.sh
# Copy SQL schema files for database initialization
COPY sql/ ./sql/
# Copy custom principal backend
COPY src/ ./src/
# Enable Apache modules and site
RUN a2enmod rewrite headers \
&& a2dissite 000-default \
&& a2ensite sabredav \
&& chmod +x /usr/local/bin/init-database.sh
# Set permissions
RUN chown -R www-data:www-data /var/www/sabredav \
&& chmod -R 755 /var/www/sabredav
EXPOSE 80
CMD ["apache2-foreground"]

View File

@@ -0,0 +1,16 @@
{
"name": "calendars/sabredav-server",
"description": "sabre/dav CalDAV server for calendars",
"type": "project",
"require": {
"php": ">=8.1",
"sabre/dav": "^4.5",
"ext-pdo": "*",
"ext-pdo_pgsql": "*"
},
"autoload": {
"psr-4": {
"Calendars\\SabreDav\\": "src/"
}
}
}

View File

@@ -0,0 +1,87 @@
#!/bin/bash
###
# Initialize sabre/dav database schema in PostgreSQL
# This script creates all necessary tables for sabre/dav to work
###
set -e
if [ -z ${PGHOST+x} ]; then
echo "PGHOST must be set"
exit 1
fi
if [ -z ${PGDATABASE+x} ]; then
echo "PGDATABASE must be set"
exit 1
fi
if [ -z ${PGUSER+x} ]; then
echo "PGUSER must be set"
exit 1
fi
if [ -z ${PGPASSWORD+x} ]; then
echo "PGPASSWORD must be set"
exit 1
fi
export PGHOST
export PGPORT=${PGPORT:-5432}
export PGDATABASE
export PGUSER
export PGPASSWORD
# Wait for PostgreSQL to be ready
retries=30
until pg_isready -q -h "$PGHOST" -p "$PGPORT" -U "$PGUSER"; do
[[ retries -eq 0 ]] && echo "Could not connect to Postgres" && exit 1
echo "Waiting for Postgres to be available..."
retries=$((retries-1))
sleep 1
done
echo "PostgreSQL is ready. Initializing sabre/dav database schema..."
# SQL files directory (will be copied into container)
SQL_DIR="/var/www/sabredav/sql"
# Check if tables already exist
TABLES_EXIST=$(psql -tAc "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('users', 'principals', 'calendars')" 2>/dev/null || echo "0")
if [ "$TABLES_EXIST" -gt "0" ]; then
echo "sabre/dav tables already exist, skipping initialization"
exit 0
fi
# Create tables
echo "Creating sabre/dav tables..."
if [ -f "$SQL_DIR/pgsql.users.sql" ]; then
psql -f "$SQL_DIR/pgsql.users.sql"
echo "Created users table"
fi
if [ -f "$SQL_DIR/pgsql.principals.sql" ]; then
psql -f "$SQL_DIR/pgsql.principals.sql"
echo "Created principals table"
fi
if [ -f "$SQL_DIR/pgsql.calendars.sql" ]; then
psql -f "$SQL_DIR/pgsql.calendars.sql"
echo "Created calendars table"
fi
if [ -f "$SQL_DIR/pgsql.addressbooks.sql" ]; then
psql -f "$SQL_DIR/pgsql.addressbooks.sql"
echo "Created addressbooks table"
fi
if [ -f "$SQL_DIR/pgsql.locks.sql" ]; then
psql -f "$SQL_DIR/pgsql.locks.sql"
echo "Created locks table"
fi
if [ -f "$SQL_DIR/pgsql.propertystorage.sql" ]; then
psql -f "$SQL_DIR/pgsql.propertystorage.sql"
echo "Created propertystorage table"
fi
echo "sabre/dav database schema initialized successfully!"

View File

@@ -0,0 +1,28 @@
<VirtualHost *:80>
ServerName localhost
DocumentRoot /var/www/sabredav
<Directory /var/www/sabredav>
AllowOverride All
Require all granted
Options -Indexes +FollowSymLinks
</Directory>
# Set REMOTE_USER from X-Forwarded-User header (set by Django proxy)
# This allows sabre/dav to use Apache auth backend
<IfModule mod_headers.c>
RequestHeader set REMOTE_USER %{HTTP:X-Forwarded-User}e env=HTTP_X_FORWARDED_USER
</IfModule>
# Rewrite rules for CalDAV
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ server.php [QSA,L]
# Well-known CalDAV discovery
RewriteRule ^\.well-known/caldav / [R=301,L]
ErrorLog ${APACHE_LOG_DIR}/sabredav_error.log
CustomLog ${APACHE_LOG_DIR}/sabredav_access.log combined
</VirtualHost>

View File

@@ -0,0 +1,79 @@
<?php
/**
* sabre/dav CalDAV Server
* Configured to use PostgreSQL backend and Apache authentication
*/
use Sabre\DAV\Auth;
use Sabre\DAVACL;
use Sabre\CalDAV;
use Sabre\CardDAV;
use Sabre\DAV;
use Calendars\SabreDav\AutoCreatePrincipalBackend;
// Composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
// Set REMOTE_USER from X-Forwarded-User header (set by Django proxy)
// This allows sabre/dav Apache auth backend to work with proxied requests
if (isset($_SERVER['HTTP_X_FORWARDED_USER']) && !isset($_SERVER['REMOTE_USER'])) {
$_SERVER['REMOTE_USER'] = $_SERVER['HTTP_X_FORWARDED_USER'];
}
// Get base URI from environment variable (set by compose.yaml)
// This ensures sabre/dav generates URLs with the correct proxy path
$baseUri = getenv('CALENDARS_BASE_URI') ?: '/';
// Database connection from environment variables
$dbHost = getenv('PGHOST') ?: 'postgresql';
$dbPort = getenv('PGPORT') ?: '5432';
$dbName = getenv('PGDATABASE') ?: 'calendars';
$dbUser = getenv('PGUSER') ?: 'pgroot';
$dbPass = getenv('PGPASSWORD') ?: 'pass';
// Create PDO connection
$pdo = new PDO(
"pgsql:host={$dbHost};port={$dbPort};dbname={$dbName}",
$dbUser,
$dbPass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
// Create backend
$authBackend = new Auth\Backend\Apache();
// Create authentication plugin
$authPlugin = new Auth\Plugin($authBackend);
// Create CalDAV backend
$caldavBackend = new CalDAV\Backend\PDO($pdo);
// Create CardDAV backend (optional, for future use)
$carddavBackend = new CardDAV\Backend\PDO($pdo);
// Create principal backend with auto-creation support
$principalBackend = new AutoCreatePrincipalBackend($pdo);
// Create directory tree
$nodes = [
new CalDAV\Principal\Collection($principalBackend),
new CalDAV\CalendarRoot($principalBackend, $caldavBackend),
new CardDAV\AddressBookRoot($principalBackend, $carddavBackend),
];
// Create server
$server = new DAV\Server($nodes);
$server->setBaseUri($baseUri);
// Add plugins
$server->addPlugin($authPlugin);
$server->addPlugin(new CalDAV\Plugin());
$server->addPlugin(new CardDAV\Plugin());
$server->addPlugin(new DAVACL\Plugin());
$server->addPlugin(new DAV\Browser\Plugin());
// Start server
$server->start();

View File

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

View File

@@ -0,0 +1,108 @@
CREATE TABLE calendarobjects (
id SERIAL NOT NULL,
calendardata BYTEA,
uri VARCHAR(200),
calendarid INTEGER NOT NULL,
lastmodified INTEGER,
etag VARCHAR(32),
size INTEGER NOT NULL,
componenttype VARCHAR(8),
firstoccurence INTEGER,
lastoccurence INTEGER,
uid VARCHAR(200)
);
ALTER TABLE ONLY calendarobjects
ADD CONSTRAINT calendarobjects_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX calendarobjects_ukey
ON calendarobjects USING btree (calendarid, uri);
CREATE TABLE calendars (
id SERIAL NOT NULL,
synctoken INTEGER NOT NULL DEFAULT 1,
components VARCHAR(21)
);
ALTER TABLE ONLY calendars
ADD CONSTRAINT calendars_pkey PRIMARY KEY (id);
CREATE TABLE calendarinstances (
id SERIAL NOT NULL,
calendarid INTEGER NOT NULL,
principaluri VARCHAR(100),
access SMALLINT NOT NULL DEFAULT '1', -- '1 = owner, 2 = read, 3 = readwrite'
displayname VARCHAR(100),
uri VARCHAR(200),
description TEXT,
calendarorder INTEGER NOT NULL DEFAULT 0,
calendarcolor VARCHAR(10),
timezone TEXT,
transparent SMALLINT NOT NULL DEFAULT '0',
share_href VARCHAR(100),
share_displayname VARCHAR(100),
share_invitestatus SMALLINT NOT NULL DEFAULT '2' -- '1 = noresponse, 2 = accepted, 3 = declined, 4 = invalid'
);
ALTER TABLE ONLY calendarinstances
ADD CONSTRAINT calendarinstances_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX calendarinstances_principaluri_uri
ON calendarinstances USING btree (principaluri, uri);
CREATE UNIQUE INDEX calendarinstances_principaluri_calendarid
ON calendarinstances USING btree (principaluri, calendarid);
CREATE UNIQUE INDEX calendarinstances_principaluri_share_href
ON calendarinstances USING btree (principaluri, share_href);
CREATE TABLE calendarsubscriptions (
id SERIAL NOT NULL,
uri VARCHAR(200) NOT NULL,
principaluri VARCHAR(100) NOT NULL,
source TEXT,
displayname VARCHAR(100),
refreshrate VARCHAR(10),
calendarorder INTEGER NOT NULL DEFAULT 0,
calendarcolor VARCHAR(10),
striptodos SMALLINT NULL,
stripalarms SMALLINT NULL,
stripattachments SMALLINT NULL,
lastmodified INTEGER
);
ALTER TABLE ONLY calendarsubscriptions
ADD CONSTRAINT calendarsubscriptions_pkey PRIMARY KEY (id);
CREATE UNIQUE INDEX calendarsubscriptions_ukey
ON calendarsubscriptions USING btree (principaluri, uri);
CREATE TABLE calendarchanges (
id SERIAL NOT NULL,
uri VARCHAR(200) NOT NULL,
synctoken INTEGER NOT NULL,
calendarid INTEGER NOT NULL,
operation SMALLINT NOT NULL DEFAULT 0
);
ALTER TABLE ONLY calendarchanges
ADD CONSTRAINT calendarchanges_pkey PRIMARY KEY (id);
CREATE INDEX calendarchanges_calendarid_synctoken_ix
ON calendarchanges USING btree (calendarid, synctoken);
CREATE TABLE schedulingobjects (
id SERIAL NOT NULL,
principaluri VARCHAR(255),
calendardata BYTEA,
uri VARCHAR(200),
lastmodified INTEGER,
etag VARCHAR(32),
size INTEGER NOT NULL
);
ALTER TABLE ONLY schedulingobjects
ADD CONSTRAINT schedulingobjects_pkey PRIMARY KEY (id);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
<?php
/**
* Custom principal backend that auto-creates principals when they don't exist.
* This allows Apache authentication to work without pre-creating principals.
*/
namespace Calendars\SabreDav;
use Sabre\DAVACL\PrincipalBackend\PDO as BasePDO;
use Sabre\DAV\MkCol;
class AutoCreatePrincipalBackend extends BasePDO
{
/**
* Returns a specific principal, specified by it's path.
* Auto-creates the principal if it doesn't exist.
*
* @param string $path
*
* @return array|null
*/
public function getPrincipalByPath($path)
{
$principal = parent::getPrincipalByPath($path);
// If principal doesn't exist, create it automatically
if (!$principal && strpos($path, 'principals/') === 0) {
// Extract username from path (e.g., "principals/user@example.com" -> "user@example.com")
$username = substr($path, strlen('principals/'));
// Create principal directly in database
// Access protected pdo property from parent
$pdo = $this->pdo;
$tableName = $this->tableName;
try {
$stmt = $pdo->prepare(
'INSERT INTO ' . $tableName . ' (uri, email, displayname) VALUES (?, ?, ?) ON CONFLICT (uri) DO NOTHING'
);
$stmt->execute([$path, $username, $username]);
// Retry getting the principal
$principal = parent::getPrincipalByPath($path);
} catch (\Exception $e) {
// If creation fails, return null
error_log("Failed to auto-create principal: " . $e->getMessage());
return null;
}
}
return $principal;
}
}