Compare commits
No commits in common. "17eddae2b0803f8fe51cb9a0c1d3d684f44e4a20" and "9213ca6df6d451229c1f3d06ed266945bd137573" have entirely different histories.
17eddae2b0
...
9213ca6df6
13 changed files with 1 additions and 610 deletions
11
README.md
11
README.md
|
@ -1,12 +1,3 @@
|
||||||
# wiredav
|
# 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.
|
"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.
|
|
|
@ -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:
|
|
|
@ -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"]
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
composer install --no-interaction --optimize-autoloader
|
|
||||||
composer dump-autoload -o
|
|
||||||
composer migrate
|
|
||||||
|
|
||||||
/usr/sbin/apache2 -DFOREGROUND
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Phinx\Migration\AbstractMigration;
|
|
||||||
use Phinx\Db\Adapter\MysqlAdapter;
|
|
||||||
|
|
||||||
final class CreateUsersAndDavTables extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function change(): void
|
|
||||||
{
|
|
||||||
// === Users table ===
|
|
||||||
$users = $this->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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WireDAV\Auth;
|
|
||||||
|
|
||||||
use Sabre\DAV\Auth\Backend\AbstractDigest;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
class SQLAuthBackend extends AbstractDigest
|
|
||||||
{
|
|
||||||
private PDO $pdo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param PDO $pdo A PDO instance connected to your DB
|
|
||||||
*/
|
|
||||||
public function __construct(PDO $pdo)
|
|
||||||
{
|
|
||||||
$this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace WireDAV\DB;
|
|
||||||
|
|
||||||
use PDO;
|
|
||||||
use PDOException;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
class PDOProvider
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Creates and returns a PDO instance with configured DB connection.
|
|
||||||
*
|
|
||||||
* @return PDO
|
|
||||||
* @throws RuntimeException on connection failure
|
|
||||||
*/
|
|
||||||
public static function getPDO(): PDO
|
|
||||||
{
|
|
||||||
$dbHost = getenv('DB_HOST') ?: 'localhost';
|
|
||||||
$dbPort = getenv('DB_PORT') ?: '3306';
|
|
||||||
$dbName = getenv('DB_NAME') ?: 'sabredav';
|
|
||||||
$dbUser = getenv('DB_USER') ?: 'sabreuser';
|
|
||||||
$dbPass = getenv('DB_PASS') ?: 'sabrepass';
|
|
||||||
$charset = 'utf8mb4';
|
|
||||||
|
|
||||||
$dsn = "mysql:host=$dbHost;port=$dbPort;dbname=$dbName;charset=$charset";
|
|
||||||
|
|
||||||
$options = [
|
|
||||||
PDO::ATTR_ERRMODE => 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
// phinx.php
|
|
||||||
|
|
||||||
return [
|
|
||||||
'paths' => [
|
|
||||||
'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',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
|
@ -1,200 +0,0 @@
|
||||||
<?php
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use WireDAV\DB\PDOProvider;
|
|
||||||
|
|
||||||
// Initiate basics
|
|
||||||
$pdo = (new PDOProvider())->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;
|
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
use Sabre\DAV;
|
|
||||||
use Sabre\CalDAV;
|
|
||||||
use Sabre\CardDAV;
|
|
||||||
use Sabre\DAVACL;
|
|
||||||
use WireDAV\DB\PDOProvider;
|
|
||||||
use WireDAV\Auth\SQLAuthBackend;
|
|
||||||
|
|
||||||
$pdo = PDOProvider::getPDO();
|
|
||||||
|
|
||||||
//Mapping PHP errors to exceptions
|
|
||||||
function exception_error_handler($errno, $errstr, $errfile, $errline ) {
|
|
||||||
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
|
||||||
}
|
|
||||||
set_error_handler("exception_error_handler");
|
|
||||||
|
|
||||||
// Backends
|
|
||||||
$authBackend = new SQLAuthBackend($pdo);
|
|
||||||
$principalBackend = new DAVACL\PrincipalBackend\PDO($pdo);
|
|
||||||
$calendarBackend = new CalDAV\Backend\PDO($pdo);
|
|
||||||
|
|
||||||
// Directory tree
|
|
||||||
$tree = array(
|
|
||||||
new DAVACL\PrincipalCollection($principalBackend),
|
|
||||||
new CalDAV\CalendarRoot($principalBackend, $calendarBackend)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// The object tree needs in turn to be passed to the server class
|
|
||||||
$server = new DAV\Server($tree);
|
|
||||||
|
|
||||||
// You are highly encouraged to set your WebDAV server base url. Without it,
|
|
||||||
// SabreDAV will guess, but the guess is not always correct. Putting the
|
|
||||||
// server on the root of the domain will improve compatibility.
|
|
||||||
$server->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();
|
|
Loading…
Add table
Add a link
Reference in a new issue