diff --git a/README.md b/README.md index 3713e4f..7e5a849 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,3 @@ # wiredav -"Wires together" Sabre/Dav. A phrase that GPT used which I thought was cute. A simple PHP site that provides WebDAV, CalDAV and CardDAV without bloat. - -Pushing incomplete version up. The WebDAV makes this a little too much work for -me to take on at the moment. Calendar sharing seemed to work fine at one point -however I'm a little lost in the weeds on permissions and haven't tested the -latest database init. - -I'll circle back to this however I was expecting this to be a rather simple -implementation of the protocols, sufficient enough to replace baikal and -nextcloud. Unfortunately that's not the case. +"Wires together" Sabre/Dav. A phrase that GPT used which I thought was cute. A simple PHP site that provides WebDAV, CalDAV and CardDAV without bloat. \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 291dd1c..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,29 +0,0 @@ -services: - sabredav: - build: - context: . - dockerfile: docker/Dockerfile - ports: - - "8080:8080" - environment: - DB_HOST: mariadb - DB_PORT: 3306 - DB_NAME: sabredav - DB_USER: sabreuser - DB_PASS: sabrepass - depends_on: - - mariadb - - mariadb: - image: mariadb:10.11 - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_DATABASE: sabredav - MYSQL_USER: sabreuser - MYSQL_PASSWORD: sabrepass - volumes: - - mariadb-data:/var/lib/mysql - -volumes: - mariadb-data: diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index bae813a..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM registry.gitlab.com/dxcker/apache-php:latest - -# Switch over to root user -USER root - -RUN apt update \ - && apt -y install unzip git curl \ - && apt -y install php-xml php-mbstring php-curl php-pdo php-mysql \ - && apt autoclean - -# Enable mod_rewrite and mod_headers. Should be upstream but hey. -RUN a2enmod rewrite headers - -# Reconfigure apache for this project -RUN echo "DocumentRoot /var/www/public" >> /etc/apache2/apache2.conf -RUN sed -i 's/\/var\/www\/html/\/var\/www\/public/g' /etc/apache2/apache2.conf - -# Install Composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -# Copy over entrypoint -COPY docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Copy over files -WORKDIR /var/www/ - -RUN mkdir -p data/files data && chown -R app:app data -COPY src/ . -RUN chown -R app:app /var/www/ - -USER app - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100644 index a15b0f1..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -composer install --no-interaction --optimize-autoloader -composer dump-autoload -o -composer migrate - -/usr/sbin/apache2 -DFOREGROUND diff --git a/src/carddav/index.php b/src/carddav/index.php deleted file mode 100644 index e69de29..0000000 diff --git a/src/composer.json b/src/composer.json deleted file mode 100644 index e1c8fc3..0000000 --- a/src/composer.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "j/wiredav", - "description": "Minimal SabreDAV server with WebDAV, CalDAV, CardDAV and MariaDB auth", - "type": "project", - "require": { - "php": "^7.4 || ^8.0", - "sabre/dav": "^4.4.0" - }, - "require-dev": { - "robmorgan/phinx": "^0.14" - }, - "autoload": { - "psr-4": { - "WireDAV\\": "libs/" - } - }, - "scripts": { - "migrate": "vendor/bin/phinx migrate", - "rollback": "vendor/bin/phinx rollback" - }, - "minimum-stability": "stable", - "license": "MIT" -} - diff --git a/src/db/migrations/2025063000_create_users_and_dav_tables.php b/src/db/migrations/2025063000_create_users_and_dav_tables.php deleted file mode 100644 index ca4aa72..0000000 --- a/src/db/migrations/2025063000_create_users_and_dav_tables.php +++ /dev/null @@ -1,145 +0,0 @@ -table('users', ['id' => false, 'primary_key' => ['username']]); - $users->addColumn('username', 'string', ['limit' => 50]) - ->addColumn('digesta1', 'string', ['limit' => 255]) - ->create(); - - // === Principals table (users and groups) === - $principals = $this->table('principals'); - $principals->addColumn('uri', 'string', ['limit' => 100]) - ->addColumn('email', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('displayname', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('password', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('calendar_prefix', 'string', ['limit' => 20, 'null' => true]) - ->addColumn('backend', 'string', ['limit' => 20, 'null' => true]) - ->addIndex(['uri'], ['unique' => true]) - ->create(); - - // Foreign key principals.uri → users.username (optional but typical) - $principals->addForeignKey('uri', 'users', 'username', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - - // === Calendars table (CalDAV) === - $calendars = $this->table('calendars'); - $calendars->addColumn('principaluri', 'string', ['limit' => 100]) - ->addColumn('displayname', 'string', ['limit' => 100, 'null' => true]) - ->addColumn('uri', 'string', ['limit' => 200]) - ->addColumn('description', 'text', ['null' => true]) - ->addColumn('calendarorder', 'integer', ['null' => true]) - ->addColumn('calendarcolor', 'string', ['limit' => 10, 'null' => true]) - ->addColumn('timezone', 'text', ['null' => true]) - ->addColumn('components', 'string', ['limit' => 20, 'null' => true]) - ->addColumn('transparent', 'boolean', ['null' => true]) - ->addIndex(['principaluri', 'uri'], ['unique' => true]) - ->create(); - - $calendars->addForeignKey('principaluri', 'principals', 'uri', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - - // === Calendar Objects table (CalDAV) === - $calendarObjects = $this->table('calendarobjects'); - $calendarObjects->addColumn('calendarid', 'integer') - ->addColumn('uri', 'string', ['limit' => 200]) - ->addColumn('calendardata', 'binary', ['limit' => MysqlAdapter::BLOB_MEDIUM, 'null' => true]) - ->addColumn('lastmodified', 'integer', ['null' => true]) - ->addColumn('etag', 'string', ['limit' => 32, 'null' => true]) - ->addColumn('size', 'integer', ['null' => true]) - ->addColumn('componenttype', 'string', ['limit' => 8, 'null' => true]) - ->addColumn('firstoccurence', 'integer', ['null' => true]) - ->addColumn('lastoccurence', 'integer', ['null' => true]) - ->addColumn('uid', 'string', ['limit' => 200, 'null' => true]) - ->addIndex(['calendarid', 'uri'], ['unique' => true]) - ->create(); - - $calendarObjects->addForeignKey('calendarid', 'calendars', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - - // === Addressbooks table (CardDAV) === - $addressbooks = $this->table('addressbooks'); - $addressbooks->addColumn('principaluri', 'string', ['limit' => 100]) - ->addColumn('displayname', 'string', ['limit' => 100, 'null' => true]) - ->addColumn('uri', 'string', ['limit' => 200]) - ->addColumn('description', 'text', ['null' => true]) - ->addIndex(['principaluri', 'uri'], ['unique' => true]) - ->create(); - - $addressbooks->addForeignKey('principaluri', 'principals', 'uri', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - - // === Addressbook Changes table (CardDAV) === - $addressbookChanges = $this->table('addressbookchanges'); - $addressbookChanges->addColumn('addressbookid', 'integer') - ->addColumn('uri', 'string', ['limit' => 200, 'null' => true]) - ->addColumn('synctoken', 'integer', ['null' => true]) - ->create(); - - $addressbookChanges->addForeignKey('addressbookid', 'addressbooks', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - - // === Addressbook Objects table (CardDAV) === - $addressbookObjects = $this->table('addressbookobjects'); - $addressbookObjects->addColumn('addressbookid', 'integer') - ->addColumn('uri', 'string', ['limit' => 200]) - ->addColumn('carddata', 'binary', ['limit' => MysqlAdapter::BLOB_MEDIUM, 'null' => true]) - ->addColumn('lastmodified', 'integer', ['null' => true]) - ->addColumn('etag', 'string', ['limit' => 32, 'null' => true]) - ->addColumn('size', 'integer', ['null' => true]) - ->addColumn('uid', 'string', ['limit' => 200, 'null' => true]) - ->addIndex(['addressbookid', 'uri'], ['unique' => true]) - ->create(); - - $addressbookObjects->addForeignKey('addressbookid', 'addressbooks', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - - // === Locks table (WebDAV) === - $locks = $this->table('locks'); - $locks->addColumn('id', 'string', ['limit' => 32]) - ->addColumn('token', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('principaluri', 'string', ['limit' => 100, 'null' => true]) - ->addColumn('scope', 'string', ['limit' => 10, 'null' => true]) - ->addColumn('depth', 'string', ['limit' => 10, 'null' => true]) - ->addColumn('uri', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('timeout', 'integer', ['null' => true]) - ->addColumn('created', 'integer', ['null' => true]) - ->addColumn('owner', 'text', ['null' => true]) - ->addColumn('exclusive', 'boolean', ['null' => true]) - ->addIndex(['id'], ['unique' => true]) - ->create(); - - // === Calendar Shares table === - $calendarShares = $this->table('calendarshares'); - $calendarShares->addColumn('calendarid', 'integer') - ->addColumn('principaluri', 'string', ['limit' => 100]) - ->addColumn('access', 'integer') // e.g. 1=read, 2=write - ->addColumn('share_access', 'integer', ['null' => true]) - ->addColumn('summary', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('href', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('uid', 'string', ['limit' => 200, 'null' => true]) - ->addColumn('lastupdated', 'integer', ['null' => true]) - ->addIndex(['calendarid', 'principaluri'], ['unique' => true]) - ->create(); - - $calendarShares->addForeignKey('calendarid', 'calendars', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - $calendarShares->addForeignKey('principaluri', 'principals', 'uri', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - - // === Addressbook Shares table === - $addressbookShares = $this->table('addressbookshares'); - $addressbookShares->addColumn('addressbookid', 'integer') - ->addColumn('principaluri', 'string', ['limit' => 100]) - ->addColumn('access', 'integer') - ->addColumn('share_access', 'integer', ['null' => true]) - ->addColumn('summary', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('href', 'string', ['limit' => 255, 'null' => true]) - ->addColumn('uid', 'string', ['limit' => 200, 'null' => true]) - ->addColumn('lastupdated', 'integer', ['null' => true]) - ->addIndex(['addressbookid', 'principaluri'], ['unique' => true]) - ->create(); - - $addressbookShares->addForeignKey('addressbookid', 'addressbooks', 'id', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - $addressbookShares->addForeignKey('principaluri', 'principals', 'uri', ['delete'=> 'CASCADE', 'update'=> 'CASCADE'])->save(); - } -} - diff --git a/src/libs/Auth/SQLAuthBackend.php b/src/libs/Auth/SQLAuthBackend.php deleted file mode 100644 index e6f689b..0000000 --- a/src/libs/Auth/SQLAuthBackend.php +++ /dev/null @@ -1,36 +0,0 @@ -pdo = $pdo; - } - - /** - * This method must return the MD5 digest hash for the user in the format: - * MD5(username:realm:password) - * - * @param string $realm - * @param string $username - * @return string|false The hash or false if user not found - */ - public function getDigestHash($realm, $username) - { - $stmt = $this->pdo->prepare('SELECT digesta1 FROM users WHERE username = ?'); - $stmt->execute([$username]); - $hash = $stmt->fetchColumn(); - return $hash ?: false; - } -} - diff --git a/src/libs/DB/PDOProvider.php b/src/libs/DB/PDOProvider.php deleted file mode 100644 index a0ef5da..0000000 --- a/src/libs/DB/PDOProvider.php +++ /dev/null @@ -1,41 +0,0 @@ - PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - ]; - - try { - return new PDO($dsn, $dbUser, $dbPass, $options); - } catch (PDOException $e) { - throw new RuntimeException('Database connection failed: ' . $e->getMessage()); - } - } -} - diff --git a/src/phinx.php b/src/phinx.php deleted file mode 100644 index 87f7005..0000000 --- a/src/phinx.php +++ /dev/null @@ -1,22 +0,0 @@ - [ - 'migrations' => 'db/migrations', - ], - 'environments' => [ - 'default_migration_table' => 'phinxlog', - 'default_environment' => 'development', - 'development' => [ - 'adapter' => 'mysql', - 'host' => getenv('DB_HOST') ?: 'localhost', - 'name' => getenv('DB_NAME') ?: 'sabredav', - 'user' => getenv('DB_USER') ?: 'root', - 'pass' => getenv('DB_PASS') ?: '', - 'port' => getenv('DB_PORT') ?: '3306', - 'charset' => 'utf8mb4', - ], - ], -]; - diff --git a/src/public/admin/index.php b/src/public/admin/index.php deleted file mode 100644 index 198fd78..0000000 --- a/src/public/admin/index.php +++ /dev/null @@ -1,200 +0,0 @@ -getPDO(); -$method = $_SERVER['REQUEST_METHOD']; -$path = $_GET['action'] ?? ''; - - -// Helper Functions -function getBasicAuthUserAndPass() { - if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { - header('WWW-Authenticate: Basic realm="Admin Area"'); - header('HTTP/1.0 401 Unauthorized'); - exit('Authentication required'); - } - return [$_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']]; -} - -function isAdmin(PDO $pdo, $username, $password, $realm = 'SabreDAV') { - $stmt = $pdo->prepare("SELECT digesta1 FROM users WHERE username = ?"); - $stmt->execute([$username]); - $hash = $stmt->fetchColumn(); - if (!$hash) return false; - - $digest = md5("$username:$realm:$password"); - return $digest === $hash; // Only checking password; add is_admin column if you want real admin check -} - -function anyUsersExist(PDO $pdo) { - $stmt = $pdo->query("SELECT COUNT(*) FROM users"); - return $stmt->fetchColumn() > 0; -} - - -// Request Routing -if ($method === 'POST' && $path === 'create_user') { - list($user, $pass) = getBasicAuthUserAndPass(); - - if (anyUsersExist($pdo) && !isAdmin($pdo, $user, $pass)) { - http_response_code(403); - exit(json_encode(['error' => 'Forbidden: admin required'])); - } - - $input = json_decode(file_get_contents('php://input'), true); - if (empty($input['username']) || empty($input['password'])) { - http_response_code(400); - exit(json_encode(['error' => 'Missing username or password'])); - } - - $newUser = $input['username']; - $newPass = $input['password']; - $realm = 'SabreDAV'; - - $stmt = $pdo->prepare("SELECT COUNT(*) FROM users WHERE username = ?"); - $stmt->execute([$newUser]); - if ($stmt->fetchColumn() > 0) { - http_response_code(409); - exit(json_encode(['error' => 'User already exists'])); - } - - $digest = md5("$newUser:$realm:$newPass"); - - $stmt = $pdo->prepare("INSERT INTO users (username, digesta1) VALUES (?, ?)"); - $stmt->execute([$newUser, $digest]); - - echo json_encode(['success' => true, 'message' => "User $newUser created"]); - exit; -} - -if ($method === 'POST' && $path === 'change_password') { - list($user, $pass) = getBasicAuthUserAndPass(); - - if (!isAdmin($pdo, $user, $pass)) { - http_response_code(403); - exit(json_encode(['error' => 'Forbidden: admin required'])); - } - - $input = json_decode(file_get_contents('php://input'), true); - if (empty($input['username']) || empty($input['password'])) { - http_response_code(400); - exit(json_encode(['error' => 'Missing username or password'])); - } - - $targetUser = $input['username']; - $newPass = $input['password']; - $realm = 'SabreDAV'; - - $digest = md5("$targetUser:$realm:$newPass"); - - $stmt = $pdo->prepare("UPDATE users SET digesta1 = ? WHERE username = ?"); - $stmt->execute([$digest, $targetUser]); - - echo json_encode(['success' => true, 'message' => "Password changed for $targetUser"]); - exit; -} - -if ($method === 'POST' && $path === 'delete_user') { - list($user, $pass) = getBasicAuthUserAndPass(); - - if (!isAdmin($pdo, $user, $pass)) { - http_response_code(403); - exit(json_encode(['error' => 'Forbidden: admin required'])); - } - - $input = json_decode(file_get_contents('php://input'), true); - if (empty($input['username'])) { - http_response_code(400); - exit(json_encode(['error' => 'Missing username'])); - } - - $targetUser = $input['username']; - - $stmt = $pdo->prepare("DELETE FROM users WHERE username = ?"); - $stmt->execute([$targetUser]); - - echo json_encode(['success' => true, 'message' => "User $targetUser deleted"]); - exit; -} - -if ($method === 'POST' && $path === 'create_calendar') { - list($username, $password) = getBasicAuthUserAndPass(); - - // Authenticate user (no admin check, calendar owned by user) - $stmt = $pdo->prepare("SELECT digesta1 FROM users WHERE username = ?"); - $stmt->execute([$username]); - $hash = $stmt->fetchColumn(); - $realm = 'SabreDAV'; - if (!$hash || md5("$username:$realm:$password") !== $hash) { - http_response_code(403); - exit(json_encode(['error' => 'Invalid credentials'])); - } - - $input = json_decode(file_get_contents('php://input'), true); - if (empty($input['uri']) || empty($input['displayname'])) { - http_response_code(400); - exit(json_encode(['error' => 'Missing uri or displayname'])); - } - - $calendarUri = $input['uri']; - $displayName = $input['displayname']; - $principalUri = "principals/$username"; - - // Check if calendar URI already exists for user - $stmt = $pdo->prepare("SELECT COUNT(*) FROM calendars WHERE principaluri = ? AND uri = ?"); - $stmt->execute([$principalUri, $calendarUri]); - if ($stmt->fetchColumn() > 0) { - http_response_code(409); - exit(json_encode(['error' => 'Calendar URI already exists'])); - } - - $stmt = $pdo->prepare("INSERT INTO calendars (principaluri, displayname, uri) VALUES (?, ?, ?)"); - $stmt->execute([$principalUri, $displayName, $calendarUri]); - - echo json_encode(['success' => true, 'message' => "Calendar '$displayName' created"]); - exit; -} - -if ($method === 'POST' && $path === 'delete_calendar') { - list($username, $password) = getBasicAuthUserAndPass(); - - // Authenticate user - $stmt = $pdo->prepare("SELECT digesta1 FROM users WHERE username = ?"); - $stmt->execute([$username]); - $hash = $stmt->fetchColumn(); - $realm = 'SabreDAV'; - if (!$hash || md5("$username:$realm:$password") !== $hash) { - http_response_code(403); - exit(json_encode(['error' => 'Invalid credentials'])); - } - - $input = json_decode(file_get_contents('php://input'), true); - if (empty($input['uri'])) { - http_response_code(400); - exit(json_encode(['error' => 'Missing calendar uri'])); - } - - $calendarUri = $input['uri']; - $principalUri = "principals/$username"; - - $stmt = $pdo->prepare("DELETE FROM calendars WHERE principaluri = ? AND uri = ?"); - $stmt->execute([$principalUri, $calendarUri]); - - if ($stmt->rowCount() === 0) { - http_response_code(404); - exit(json_encode(['error' => 'Calendar not found'])); - } - - echo json_encode(['success' => true, 'message' => "Calendar '$calendarUri' deleted"]); - exit; -} - - -// Default fallback if no route matched -http_response_code(404); -echo json_encode(['error' => 'Unknown endpoint']); -exit; - diff --git a/src/public/index.php b/src/public/index.php deleted file mode 100644 index 655b130..0000000 --- a/src/public/index.php +++ /dev/null @@ -1,61 +0,0 @@ -setBaseUri('/'); - -// Authentication plugin -$authPlugin = new DAV\Auth\Plugin($authBackend,'SabreDAV'); -$server->addPlugin($authPlugin); - -// CalDAV plugin -$caldavPlugin = new CalDAV\Plugin(); -$server->addPlugin($caldavPlugin); - -// CardDAV plugin -$carddavPlugin = new CardDAV\Plugin(); -$server->addPlugin($carddavPlugin); - -// ACL plugin -$aclPlugin = new DAVACL\Plugin(); -$server->addPlugin($aclPlugin); - -// Support for html frontend -$browser = new DAV\Browser\Plugin(); -$server->addPlugin($browser); - -// And off we go! -$server->exec(); diff --git a/src/webdav/index.php b/src/webdav/index.php deleted file mode 100644 index e69de29..0000000