diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1f03e5d..57b6ebf 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,7 +1,3 @@ -# See -# - https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions -# - https://docs.github.com/en/actions/learn-github-actions/contexts -# - https://github.com/docker/build-push-action/blob/master/docs/advanced/multi-platform.md name: docker build on: @@ -10,209 +6,52 @@ on: - master paths: - .github/workflows/docker-build.yml - - "*.dockerfile" + - Dockerfile - files/* release: types: - published -jobs: - build_apache: - name: Build Apache variant - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - # See https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - # See https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - # See https://github.com/docker/login-action - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - # Build Apache httpd images - - name: Build and push experimental image - if: github.event_name == 'push' - uses: docker/build-push-action@v2 - with: - file: apache.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 - push: true - tags: | - ckulka/baikal:experimental-apache - ckulka/baikal:experimental - - # Get the release version by stripping build metadata from the release name - - name: Parse release tag - if: github.event_name == 'release' - run: echo DOCKER_RELEASE_TAG=${GITHUB_REF_NAME/+*/} >> $GITHUB_ENV - - - name: Build and push release image - if: github.event_name == 'release' - uses: docker/build-push-action@v2 - with: - file: apache.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 - push: true - tags: | - ckulka/baikal:${{ env.DOCKER_RELEASE_TAG }}-apache - ckulka/baikal:${{ env.DOCKER_RELEASE_TAG }} - ckulka/baikal:apache - ckulka/baikal:latest - - build_apache_php80: - name: Build Apache variant (PHP 8.0) - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - # See https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - # See https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - # See https://github.com/docker/login-action - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} - # Build Apache httpd images - - name: Build and push experimental image - if: github.event_name == 'push' - uses: docker/build-push-action@v2 - with: - file: apache-php8.0.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 - push: true - tags: | - ckulka/baikal:experimental-apache-php8.0 - ckulka/baikal:experimental-php8.0 - - # Get the release version by stripping build metadata from the release name - - name: Parse release tag - if: github.event_name == 'release' - run: echo DOCKER_RELEASE_TAG=${GITHUB_REF_NAME/+*/} >> $GITHUB_ENV - - - name: Build and push release image - if: github.event_name == 'release' - uses: docker/build-push-action@v2 - with: - file: apache-php8.0.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 - push: true - tags: | - ckulka/baikal:${{ env.DOCKER_RELEASE_TAG }}-apache-php8.0 - ckulka/baikal:${{ env.DOCKER_RELEASE_TAG }}-php8.0 - ckulka/baikal:apache-php8.0 - ckulka/baikal:latest-php8.0 - - build_nginx: - name: Build nginx variant +jobs: + build: + name: Build runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - # See https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - # See https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - # See https://github.com/docker/login-action - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - # Build nginx images - - name: Build and push experimental image - if: github.event_name == 'push' - uses: docker/build-push-action@v2 - with: - file: nginx.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 - push: true - tags: ckulka/baikal:experimental-nginx - - # Get the release version by stripping build metadata from the release name - - name: Parse release tag - if: github.event_name == 'release' - run: echo DOCKER_RELEASE_TAG=${GITHUB_REF_NAME/+*/} >> $GITHUB_ENV - - - name: Build and push release image - if: github.event_name == 'release' - uses: docker/build-push-action@v2 - with: - file: nginx.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 - push: true - tags: | - ckulka/baikal:${{ env.DOCKER_RELEASE_TAG }}-nginx - ckulka/baikal:nginx - - build_nginx_php80: - name: Build nginx variant (PHP 8.0) - runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v4 - # See https://github.com/docker/setup-qemu-action - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 - # See https://github.com/docker/setup-buildx-action - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - # See https://github.com/docker/login-action - - name: Login to DockerHub - uses: docker/login-action@v1 + - name: Login to Github Registry + uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - # Build nginx images - - name: Build and push experimental image - if: github.event_name == 'push' - uses: docker/build-push-action@v2 + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 with: - file: nginx-php8.0.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 - push: true - tags: ckulka/baikal:experimental-nginx-php8.0 - - # Get the release version by stripping build metadata from the release name - - name: Parse release tag - if: github.event_name == 'release' - run: echo DOCKER_RELEASE_TAG=${GITHUB_REF_NAME/+*/} >> $GITHUB_ENV + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - name: Build and push release image - if: github.event_name == 'release' - uses: docker/build-push-action@v2 + - name: Build and push image + uses: docker/build-push-action@v5 with: - file: nginx-php8.0.dockerfile - platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386 + context: . push: true - tags: | - ckulka/baikal:${{ env.DOCKER_RELEASE_TAG }}-nginx-php8.0 - ckulka/baikal:nginx-php8.0 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml deleted file mode 100644 index 3c07cd8..0000000 --- a/.github/workflows/docker-test.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: test - -on: - push: - branches: - - master - paths: - - .github/workflows/docker-test.yml - - "*.dockerfile" - - cypress/* - - cypress.config.ts - - files/* - - package.json - - package-lock.json - pull_request: - paths: - - .github/workflows/docker-test.yml - - "*.dockerfile" - - cypress/* - - cypress.config.ts - - files/* - - package.json - - package-lock.json - -jobs: - test: - name: Test Docker image - runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - dockerfile: [apache, apache-php8.0, nginx, nginx-php8.0] - - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v3 - with: - node-version: "16" - - - name: Install Cypress dependencies - run: npm ci - - - name: Build and start Baikal - run: docker run --rm -dp 80:80 $(docker build -qf ${{ matrix.dockerfile }}.dockerfile .) - - - name: Run Cypress tests - run: npm run test - - - name: Archive test results - if: always() - uses: actions/upload-artifact@v3 - with: - name: cypress-results-${{ matrix.dockerfile }} - path: | - cypress/screenshots - cypress/videos diff --git a/.github/workflows/dockerhub-description.yml b/.github/workflows/dockerhub-description.yml deleted file mode 100644 index 00fbfec..0000000 --- a/.github/workflows/dockerhub-description.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Updates the Docker Hub description with the contents of the README.md file -name: Docker Hub Description -on: - release: - types: - - published - paths: - - README.md - - .github/workflows/dockerhub-description.yml - -jobs: - update: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - repository: ckulka/baikal - short-description: ${{ github.event.repository.description }} diff --git a/nginx.dockerfile b/Dockerfile similarity index 88% rename from nginx.dockerfile rename to Dockerfile index 13ac78e..f8a51af 100644 --- a/nginx.dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Multi-stage build, see https://docs.docker.com/develop/develop-images/multistage-build/ -FROM alpine AS builder +FROM alpine:3.18.3 AS builder ENV VERSION 0.9.3 @@ -7,11 +7,11 @@ ADD https://github.com/sabre-io/Baikal/releases/download/$VERSION/baikal-$VERSIO RUN apk add unzip && unzip -q baikal-$VERSION.zip # Final Docker image -FROM nginx:1 +FROM nginx:1.25.2 LABEL description="Baikal is a Cal and CardDAV server, based on sabre/dav, that includes an administrative interface for easy management." LABEL version="0.9.3" -LABEL repository="https://github.com/ckulka/baikal-docker" +LABEL repository="https://github.com/MrAlucardDante/baikal-docker-hass" LABEL website="http://sabre.io/baikal/" # Install dependencies: PHP (with libffi6 dependency) & SQLite3 @@ -38,6 +38,7 @@ RUN curl -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg COPY files/docker-entrypoint.d/*.sh files/docker-entrypoint.d/nginx/ /docker-entrypoint.d/ COPY --from=builder --chown=nginx:nginx baikal /var/www/baikal COPY files/nginx.conf /etc/nginx/conf.d/default.conf +COPY --chown=nginx:nginx files/Plugin.php /var/www/baikal/vendor/sabre/dav/lib/CalDAV/Plugin.php VOLUME /var/www/baikal/config VOLUME /var/www/baikal/Specific diff --git a/README.md b/README.md index 2f3239a..1ad171e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Baikal -![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/ckulka/baikal) [![docker build](https://github.com/ckulka/baikal-docker/actions/workflows/docker-build.yml/badge.svg)](https://github.com/ckulka/baikal-docker/actions/workflows/docker-build.yml) ![Docker Pulls](https://img.shields.io/docker/pulls/ckulka/baikal) ![Docker Architectures](https://img.shields.io/badge/arch-amd64%20%7C%20arm32v7%20%7C%20arm64v8%20%7C%20i386-informational) +![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/MrAlucardDante/baikal) [![docker build](https://github.com/MrAlucardDante/baikal-docker-hass/actions/workflows/docker-build.yml/badge.svg)](https://github.com/MrAlucardDante/baikal-docker-hass/actions/workflows/docker-build.yml) ![Docker Pulls](https://img.shields.io/docker/pulls/MrAlucardDante/baikal) ![Docker Architectures](https://img.shields.io/badge/arch-amd64%20%7C%20arm32v7%20%7C%20arm64v8%20%7C%20i386-informational) This dockerfile provides a ready-to-go [Baikal server](http://sabre.io/baikal/). @@ -12,37 +12,7 @@ I follow the same version naming scheme as [Baikal](http://sabre.io/baikal/) the The following tags support multiple architectures, e.g. `amd64`, `arm32v7`, `arm64v8` and `i386`. -- [`0.9.3`, `0.9.3-apache`, `apache`, `latest`](https://github.com/ckulka/baikal-docker/blob/0.9.3/apache.dockerfile) -- [`0.9.3-php8.0`, `0.9.3-apache-php8.0`, `apache-php8.0`, `latest-php8.0`](https://github.com/ckulka/baikal-docker/blob/0.9.3/apache-php8.0.dockerfile) -- [`0.9.3-nginx`, `nginx`](https://github.com/ckulka/baikal-docker/blob/0.9.3/nginx.dockerfile) -- [`0.9.3-nginx-php8.0`, `nginx-php8.0`](https://github.com/ckulka/baikal-docker/blob/0.9.3/nginx-php8.0.dockerfile) -- [`0.9.2`, `0.9.2-apache`](https://github.com/ckulka/baikal-docker/blob/0.9.2/apache.dockerfile) -- [`0.9.2-php8.0`, `0.9.2-apache-php8.0`, `apache-php8.0`, `latest-php8.0`](https://github.com/ckulka/baikal-docker/blob/0.9.2/apache-php8.0.dockerfile) -- [`0.9.2-nginx`](https://github.com/ckulka/baikal-docker/blob/0.9.2/nginx.dockerfile) -- [`0.9.2-nginx-php8.0`, `nginx-php8.0`](https://github.com/ckulka/baikal-docker/blob/0.9.2/nginx-php8.0.dockerfile) -- [`0.9.1`, `0.9.1-apache`](https://github.com/ckulka/baikal-docker/blob/0.9.1/apache.dockerfile) -- [`0.9.1-php8.0`, `0.9.1-apache-php8.0`](https://github.com/ckulka/baikal-docker/blob/0.9.1/apache-php8.0.dockerfile) -- [`0.9.1-nginx`](https://github.com/ckulka/baikal-docker/blob/0.9.1/nginx.dockerfile) -- [`0.9.1-nginx-php8.0`](https://github.com/ckulka/baikal-docker/blob/0.9.1/nginx-php8.0.dockerfile) -- [`0.9.0`, `0.9.0-apache`](https://github.com/ckulka/baikal-docker/blob/0.9.0/apache.dockerfile) -- [`0.9.0-nginx`](https://github.com/ckulka/baikal-docker/blob/0.9.0/nginx.dockerfile) -- [`0.8.0`, `0.8.0-apache`](https://github.com/ckulka/baikal-docker/blob/0.8.0/apache.dockerfile) -- [`0.8.0-nginx`](https://github.com/ckulka/baikal-docker/blob/0.8.0/nginx.dockerfile) - -For earlier versions all the way back to version 0.2.7, please search in the [tags](https://hub.docker.com/r/ckulka/baikal/tags) tab. Version 0.4.5 and older are only available for `amd64`. Version 0.9.0 and older do not support `i386`. - -The `*-php8.0` images address compatibility issue in some edge cases with version 0.9.1 and PHP 8.1, see [ckulka/baikal-docker #52](https://github.com/ckulka/baikal-docker/issues/52) and [sabre-io/vobject #561](https://github.com/sabre-io/vobject/pull/561). - -## Quick reference - -- **Where to file issues**: - [https://github.com/ckulka/baikal-docker/issues](https://github.com/ckulka/baikal-docker/issues) -- **Supported architectures** ([more info](https://github.com/docker-library/official-images#architectures-other-than-amd64)): - `amd64`, `arm32v7`, `arm64v8`, `i386` -- **Image updates**: - [PRs for ckulka/baikal-docker](https://github.com/ckulka/baikal-docker/pulls) -- **Source of this description**: - [https://github.com/ckulka/baikal-docker](https://github.com/ckulka/baikal-docker) +- [`0.9.3`, `latest`](https://github.com/MrAlucardDante/baikal-docker-hass/blob/0.9.3/Dockerfile) ## What is Baikal? @@ -62,7 +32,7 @@ The following command will start Baikal: docker run --rm -it -p 80:80 ckulka/baikal:nginx ``` -Alternatively, use the provided [examples/docker-compose.yaml](https://github.com/ckulka/baikal-docker/blob/master/examples/docker-compose.yaml) from the Git repository: +Alternatively, use the provided [examples/docker-compose.yaml](https://github.com/MrAlucardDante/baikal-docker-hass/blob/master/examples/docker-compose.yaml) from the Git repository: ```bash docker-compose up @@ -74,47 +44,13 @@ You can now open [http://localhost](http://localhost) or [http://host-ip](http:/ The image exposes the `/var/www/baikal/Specific` and `/var/www/baikal/config` folders, which contain the persistent data. These folders should be part of a regular backup. -If you want to use local folders instead of Docker volumes, see [examples/docker-compose.localvolumes.yaml](https://github.com/ckulka/baikal-docker/blob/master/examples/docker-compose.localvolumes.yaml) to avoid file permission issues. +If you want to use local folders instead of Docker volumes, see [examples/docker-compose.localvolumes.yaml](https://github.com/MrAlucardDante/baikal-docker-hass/blob/master/examples/docker-compose.localvolumes.yaml) to avoid file permission issues. ### Further Guides You can find more installation and configuration guides here: -- [Email Guide](https://github.com/ckulka/baikal-docker/blob/master/docs/email-guide.md) -- [SSL Certificate Guide](https://github.com/ckulka/baikal-docker/blob/master/docs/ssl-certificates-guide.md) -- [systemd Guide](https://github.com/ckulka/baikal-docker/blob/master/docs/systemd-guide.md) -- [Unraid Installation Guide](https://github.com/ckulka/baikal-docker/blob/master/docs/unraid-installation-guide.md) - -## Image Variants - -The `ckulka/baikal` images come in several flavors, each designed for a specific use case. - -### `ckulka/baikal:` - -This is the defacto image and follows the official guidelines the closest using Apache httpd. - -With that being said, it's worth checking out the `nginx` variant as it requires fewer resources and produces no warning messages out-of-the-box. - -If you are unsure about what your needs are, you probably want to use this one though. - -### `ckulka/baikal:apache` - -This image relies on Apache httpd and uses the [official PHP image](https://hub.docker.com/_/php/) that's packaged with the Apache web server. - -It also ships with HTTPS support and self-signed certificates, which can be replaced by user-provided certificates - for more details, see the [SSL Certificate Guide](https://github.com/ckulka/baikal-docker/blob/master/docs/ssl-certificates-guide.md). - -This image uses environment variables to set Apache's `ServerName` and `ServerAlias` directives to avoid Apache httpd's warnings in the logs. - -The `BAIKAL_SERVERNAME` environment variable is used to set the global `ServerName` directive, e.g. `dav.example.io`. For more details, see [Apache Core Features: ServerName Directive](https://httpd.apache.org/docs/2.4/mod/core.html#servername). - -The `BAIKAL_SERVERALIAS` environment variable is used to set the `ServerAlias` directive of the `VirtualHost`s, e.g. `dav.example.org dav.example.com`. For more details, see [Apache Core Features: ServerAlias Directive](https://httpd.apache.org/docs/2.4/mod/core.html#serveralias). - -### `ckulka/baikal:experimental` - -This image has the latest code from the source repository [ckulka/baikal-docker](https://github.com/ckulka/baikal-docker), mainly used for testing before a version is released. Use this at your own risk. - -### `ckulka/baikal:nginx` - -This image relies on [nginx](https://www.nginx.com/) and uses the [official nginx image](https://hub.docker.com/_/nginx/). - -Compared to the Apache variant, it is significantly smaller (less than half the size) and produces no warning messages out-of-the-box. +- [Email Guide](https://github.com/MrAlucardDante/baikal-docker-hass/blob/master/docs/email-guide.md) +- [SSL Certificate Guide](https://github.com/MrAlucardDante/baikal-docker-hass/blob/master/docs/ssl-certificates-guide.md) +- [systemd Guide](https://github.com/MrAlucardDante/baikal-docker-hass/blob/master/docs/systemd-guide.md) +- [Unraid Installation Guide](https://github.com/MrAlucardDante/baikal-docker-hass/blob/master/docs/unraid-installation-guide.md) diff --git a/apache-php8.0.dockerfile b/apache-php8.0.dockerfile deleted file mode 100644 index 29a732e..0000000 --- a/apache-php8.0.dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Multi-stage build, see https://docs.docker.com/develop/develop-images/multistage-build/ -FROM alpine AS builder - -ENV VERSION 0.9.3 - -ADD https://github.com/sabre-io/Baikal/releases/download/$VERSION/baikal-$VERSION.zip . -RUN apk add unzip && unzip -q baikal-$VERSION.zip - -# Final Docker image -FROM php:8.0-apache - -LABEL description="Baikal is a Cal and CardDAV server, based on sabre/dav, that includes an administrative interface for easy management." -LABEL version="0.9.3" -LABEL repository="https://github.com/ckulka/baikal-docker" -LABEL website="http://sabre.io/baikal/" - -# Install Baikal and required dependencies -COPY --from=builder --chown=www-data:www-data baikal /var/www/baikal -RUN apt-get update &&\ - apt-get install -y \ - libcurl4-openssl-dev \ - msmtp msmtp-mta &&\ - rm -rf /var/lib/apt/lists/* &&\ - docker-php-ext-install curl pdo pdo_mysql - -# Configure Apache + HTTPS -COPY files/apache.conf /etc/apache2/sites-enabled/000-default.conf -RUN a2enmod rewrite ssl && openssl req -x509 -newkey rsa:2048 -subj "/C= " -keyout /etc/ssl/private/baikal.private.pem -out /etc/ssl/private/baikal.public.pem -days 3650 -nodes - -# Expose HTTPS & data directory -EXPOSE 443 -VOLUME /var/www/baikal/config -VOLUME /var/www/baikal/Specific - -COPY files/docker-entrypoint.sh /docker-entrypoint.sh -COPY files/docker-entrypoint.d/*.sh files/docker-entrypoint.d/httpd/ /docker-entrypoint.d/ -ENTRYPOINT [ "/docker-entrypoint.sh" ] -CMD [ "apache2-foreground" ] diff --git a/apache.dockerfile b/apache.dockerfile deleted file mode 100644 index c40c95d..0000000 --- a/apache.dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Multi-stage build, see https://docs.docker.com/develop/develop-images/multistage-build/ -FROM alpine AS builder - -ENV VERSION 0.9.3 - -ADD https://github.com/sabre-io/Baikal/releases/download/$VERSION/baikal-$VERSION.zip . -RUN apk add unzip && unzip -q baikal-$VERSION.zip - -# Final Docker image -FROM php:8.1-apache - -LABEL description="Baikal is a Cal and CardDAV server, based on sabre/dav, that includes an administrative interface for easy management." -LABEL version="0.9.3" -LABEL repository="https://github.com/ckulka/baikal-docker" -LABEL website="http://sabre.io/baikal/" - -# Install Baikal and required dependencies -COPY --from=builder --chown=www-data:www-data baikal /var/www/baikal -RUN apt-get update &&\ - apt-get install -y \ - libcurl4-openssl-dev \ - msmtp msmtp-mta &&\ - rm -rf /var/lib/apt/lists/* &&\ - docker-php-ext-install curl pdo pdo_mysql - -# Configure Apache + HTTPS -COPY files/apache.conf /etc/apache2/sites-enabled/000-default.conf -RUN a2enmod rewrite ssl && openssl req -x509 -newkey rsa:2048 -subj "/C= " -keyout /etc/ssl/private/baikal.private.pem -out /etc/ssl/private/baikal.public.pem -days 3650 -nodes - -# Expose HTTPS & data directory -EXPOSE 443 -VOLUME /var/www/baikal/config -VOLUME /var/www/baikal/Specific - -COPY files/docker-entrypoint.sh /docker-entrypoint.sh -COPY files/docker-entrypoint.d/*.sh files/docker-entrypoint.d/httpd/ /docker-entrypoint.d/ -ENTRYPOINT [ "/docker-entrypoint.sh" ] -CMD [ "apache2-foreground" ] diff --git a/examples/docker-compose.apache.yaml b/examples/docker-compose.apache.yaml deleted file mode 100644 index 5ba259d..0000000 --- a/examples/docker-compose.apache.yaml +++ /dev/null @@ -1,20 +0,0 @@ -version: "2" - -services: - baikal: - image: ckulka/baikal:apache - restart: always - environment: - BAIKAL_SERVERNAME: dav.example.io - BAIKAL_SERVERALIAS: dav.example.org dav.example.com - ports: - - "80:80" - - "443:443" - volumes: - - config:/var/www/baikal/config - - data:/var/www/baikal/Specific - # - /etc/ssl/private/baikal:/etc/ssl/private - -volumes: - config: - data: diff --git a/examples/docker-compose.awss3.yaml b/examples/docker-compose.awss3.yaml deleted file mode 100644 index 93838b6..0000000 --- a/examples/docker-compose.awss3.yaml +++ /dev/null @@ -1,34 +0,0 @@ -version: "2" - -services: - baikal: - image: ckulka/baikal:nginx - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - config:/var/www/baikal/config - - data:/var/www/baikal/Specific - - backup: - image: ckulka/awscli:alpine - environment: - - AWS_ACCESS_KEY_ID=my-access-key-id - - AWS_SECRET_ACCESS_KEY=my-access-key-secret - - AWS_DEFAULT_REGION=eu-central-1 - - BACKUP_LOCAL=/mnt - - BACKUP_S3=s3://my-bucket - # Specifying id of www-data, since the user doesn't exist in ckulka/awscli - - BACKUP_CHOWN=33:33 - volumes: - - config:/mnt/config - - data:/mnt/Specific - command: sh /opt/aws-s3-sync-backup.sh - -# To manually backup, run docker-compose run --rm backup -# To manually restore, run docker-compose run --rm backup sh /opt/aws-s3-sync-restore.sh - -volumes: - config: - data: diff --git a/files/Plugin.php b/files/Plugin.php new file mode 100644 index 0000000..e4f9ef5 --- /dev/null +++ b/files/Plugin.php @@ -0,0 +1,1004 @@ +server->tree->getNodeForPath($parent); + + if ($node instanceof DAV\IExtendedCollection) { + try { + $node->getChild($name); + } catch (DAV\Exception\NotFound $e) { + return ['MKCALENDAR']; + } + } + + return []; + } + + /** + * Returns the path to a principal's calendar home. + * + * The return url must not end with a slash. + * This function should return null in case a principal did not have + * a calendar home. + * + * @param string $principalUrl + * + * @return string + */ + public function getCalendarHomeForPrincipal($principalUrl) + { + // The default behavior for most sabre/dav servers is that there is a + // principals root node, which contains users directly under it. + // + // This function assumes that there are two components in a principal + // path. If there's more, we don't return a calendar home. This + // excludes things like the calendar-proxy-read principal (which it + // should). + $parts = explode('/', trim($principalUrl, '/')); + if (2 !== count($parts)) { + return; + } + if ('principals' !== $parts[0]) { + return; + } + + return self::CALENDAR_ROOT.'/'.$parts[1]; + } + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + public function getFeatures() + { + return ['calendar-access', 'calendar-proxy']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'caldav'; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + + $reports = []; + if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) { + $reports[] = '{'.self::NS_CALDAV.'}calendar-multiget'; + $reports[] = '{'.self::NS_CALDAV.'}calendar-query'; + } + if ($node instanceof ICalendar) { + $reports[] = '{'.self::NS_CALDAV.'}free-busy-query'; + } + // iCal has a bug where it assumes that sync support is enabled, only + // if we say we support it on the calendar-home, even though this is + // not actually the case. + if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) { + $reports[] = '{DAV:}sync-collection'; + } + + return $reports; + } + + /** + * Initializes the plugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + + $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']); + $server->on('report', [$this, 'report']); + $server->on('propFind', [$this, 'propFind']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $server->on('afterMethod:GET', [$this, 'httpAfterGET']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + + $server->xml->namespaceMap[self::NS_CALDAV] = 'cal'; + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write'; + + array_push($server->protectedProperties, + '{'.self::NS_CALDAV.'}supported-calendar-component-set', + '{'.self::NS_CALDAV.'}supported-calendar-data', + '{'.self::NS_CALDAV.'}max-resource-size', + '{'.self::NS_CALDAV.'}min-date-time', + '{'.self::NS_CALDAV.'}max-date-time', + '{'.self::NS_CALDAV.'}max-instances', + '{'.self::NS_CALDAV.'}max-attendees-per-instance', + '{'.self::NS_CALDAV.'}calendar-home-set', + '{'.self::NS_CALDAV.'}supported-collation-set', + '{'.self::NS_CALDAV.'}calendar-data', + + // CalendarServer extensions + '{'.self::NS_CALENDARSERVER.'}getctag', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for' + ); + + if ($aclPlugin = $server->getPlugin('acl')) { + $aclPlugin->principalSearchPropertySet['{'.self::NS_CALDAV.'}calendar-user-address-set'] = 'Calendar address'; + } + } + + /** + * This functions handles REPORT requests specific to CalDAV. + * + * @param string $reportName + * @param mixed $report + * @param mixed $path + * + * @return bool|null + */ + public function report($reportName, $report, $path) + { + switch ($reportName) { + case '{'.self::NS_CALDAV.'}calendar-multiget': + $this->server->transactionType = 'report-calendar-multiget'; + $this->calendarMultiGetReport($report); + + return false; + case '{'.self::NS_CALDAV.'}calendar-query': + $this->server->transactionType = 'report-calendar-query'; + $this->calendarQueryReport($report); + + return false; + case '{'.self::NS_CALDAV.'}free-busy-query': + $this->server->transactionType = 'report-free-busy-query'; + $this->freeBusyQueryReport($report); + + return false; + } + } + + /** + * This function handles the MKCALENDAR HTTP method, which creates + * a new calendar. + * + * @return bool + */ + public function httpMkCalendar(RequestInterface $request, ResponseInterface $response) + { + $body = $request->getBodyAsString(); + $path = $request->getPath(); + + $properties = []; + + if ($body) { + try { + $mkcalendar = $this->server->xml->expect( + '{urn:ietf:params:xml:ns:caldav}mkcalendar', + $body + ); + } catch (\Sabre\Xml\ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + $properties = $mkcalendar->getProperties(); + } + + // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored + // subscriptions. Before that it used MKCOL which was the correct way + // to do this. + // + // If the body had a {DAV:}resourcetype, it means we stumbled upon this + // request, and we simply use it instead of the pre-defined list. + if (isset($properties['{DAV:}resourcetype'])) { + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + } else { + $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar']; + } + + $this->server->createCollection($path, new MkCol($resourceType, $properties)); + + $response->setStatus(201); + $response->setHeader('Content-Length', 0); + + // This breaks the method chain. + return false; + } + + /** + * PropFind. + * + * This method handler is invoked before any after properties for a + * resource are fetched. This allows us to add in any CalDAV specific + * properties. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $ns = '{'.self::NS_CALDAV.'}'; + + if ($node instanceof ICalendarObjectContainer) { + $propFind->handle($ns.'max-resource-size', $this->maxResourceSize); + $propFind->handle($ns.'supported-calendar-data', function () { + return new Xml\Property\SupportedCalendarData(); + }); + $propFind->handle($ns.'supported-collation-set', function () { + return new Xml\Property\SupportedCollationSet(); + }); + } + + if ($node instanceof DAVACL\IPrincipal) { + $principalUrl = $node->getPrincipalUrl(); + + $propFind->handle('{'.self::NS_CALDAV.'}calendar-home-set', function () use ($principalUrl) { + $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl); + if (is_null($calendarHomePath)) { + return null; + } + + return new LocalHref($calendarHomePath.'/'); + }); + // The calendar-user-address-set property is basically mapped to + // the {DAV:}alternate-URI-set property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $addresses[] = $this->server->getBaseUri().$node->getPrincipalUrl().'/'; + + return new LocalHref($addresses); + }); + // For some reason somebody thought it was a good idea to add + // another one of these properties. We're supporting it too. + $propFind->handle('{'.self::NS_CALENDARSERVER.'}email-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $emails = []; + foreach ($addresses as $address) { + if ('mailto:' === substr($address, 0, 7)) { + $emails[] = substr($address, 7); + } + } + + return new Xml\Property\EmailAddressSet($emails); + }); + + // These two properties are shortcuts for ical to easily find + // other principals this principal has access to. + $propRead = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for'; + $propWrite = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for'; + + if (404 === $propFind->getStatus($propRead) || 404 === $propFind->getStatus($propWrite)) { + $aclPlugin = $this->server->getPlugin('acl'); + $membership = $aclPlugin->getPrincipalMembership($propFind->getPath()); + $readList = []; + $writeList = []; + + foreach ($membership as $group) { + $groupNode = $this->server->tree->getNodeForPath($group); + + $listItem = Uri\split($group)[0].'/'; + + // If the node is either ap proxy-read or proxy-write + // group, we grab the parent principal and add it to the + // list. + if ($groupNode instanceof Principal\IProxyRead) { + $readList[] = $listItem; + } + if ($groupNode instanceof Principal\IProxyWrite) { + $writeList[] = $listItem; + } + } + + $propFind->set($propRead, new LocalHref($readList)); + $propFind->set($propWrite, new LocalHref($writeList)); + } + } // instanceof IPrincipal + + if ($node instanceof ICalendarObject) { + // The calendar-data property is not supposed to be a 'real' + // property, but in large chunks of the spec it does act as such. + // Therefore we simply expose it as a property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-data', function () use ($node) { + $val = $node->get(); + if (is_resource($val)) { + $val = stream_get_contents($val); + } + + // Taking out \r to not screw up the xml output + return str_replace("\r", '', $val); + }); + } + } + + /** + * This function handles the calendar-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param CalendarMultiGetReport $report + */ + public function calendarMultiGetReport($report) + { + $needsJson = 'application/calendar+json' === $report->contentType; + + $timeZones = []; + $propertyList = []; + + $paths = array_map( + [$this->server, 'calculateUri'], + $report->hrefs + ); + + foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) { + if (($needsJson || $report->expand) && isset($objProps[200]['{'.self::NS_CALDAV.'}calendar-data'])) { + $vObject = VObject\Reader::read($objProps[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + list($calendarPath) = Uri\split($uri); + if (!isset($timeZones[$calendarPath])) { + // Checking the calendar-timezone property. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($calendarPath, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + } else { + // Defaulting to UTC. + $timeZone = new DateTimeZone('UTC'); + } + $timeZones[$calendarPath] = $timeZone; + } + + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]); + } + if ($needsJson) { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + + $propertyList[] = $objProps; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return'])); + } + + /** + * This function handles the calendar-query REPORT. + * + * This report is used by clients to request calendar objects based on + * complex conditions. + * + * @param Xml\Request\CalendarQueryReport $report + */ + public function calendarQueryReport($report) + { + $path = $this->server->getRequestUri(); + + $needsJson = 'application/calendar+json' === $report->contentType; + + $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); + $depth = $this->server->getHTTPDepth(0); + + // The default result is an empty array + $result = []; + + $calendarTimeZone = null; + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + $calendarTimeZone = new DateTimeZone($tzResult[$tzProp]); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + } + + // The calendarobject was requested directly. In this case we handle + // this locally. + if (0 == $depth && $node instanceof ICalendarObject) { + $requestedCalendarData = true; + $requestedProperties = $report->properties; + + if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { + // We always retrieve calendar-data, as we need it for filtering. + $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + + // If calendar-data wasn't explicitly requested, we need to remove + // it after processing. + $requestedCalendarData = false; + } + + $properties = $this->server->getPropertiesForPath( + $path, + $requestedProperties, + 0 + ); + + // This array should have only 1 element, the first calendar + // object. + $properties = current($properties); + + // If there wasn't any calendar-data returned somehow, we ignore + // this. + if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) { + $validator = new CalendarQueryValidator(); + + $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + if ($validator->validate($vObject, $report->filters)) { + // If the client didn't require the calendar-data property, + // we won't give it back. + if (!$requestedCalendarData) { + unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + } else { + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } elseif ($report->expand) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + } + + $result = [$properties]; + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + } + + if ($node instanceof ICalendarObjectContainer && 0 === $depth) { + if (0 === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'MSFT-')) { + // Microsoft clients incorrectly supplied depth as 0, when it actually + // should have set depth to 1. We're implementing a workaround here + // to deal with this. + // + // This targets at least the following clients: + // Windows 10 + // Windows Phone 8, 10 + $depth = 1; + } else { + throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1'); + } + } + + // If we're dealing with a calendar, the calendar itself is responsible + // for the calendar-query. + if ($node instanceof ICalendarObjectContainer && 1 == $depth) { + $nodePaths = $node->calendarQuery($report->filters); + + foreach ($nodePaths as $path) { + list($properties) = + $this->server->getPropertiesForPath($this->server->getRequestUri().'/'.$path, $report->properties); + + if (($needsJson || $report->expand)) { + $vObject = VObject\Reader::read($properties[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + $result[] = $properties; + } + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * This method is responsible for parsing the request and generating the + * response for the CALDAV:free-busy-query REPORT. + */ + protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) + { + $uri = $this->server->getRequestUri(); + + $acl = $this->server->getPlugin('acl'); + if ($acl) { + $acl->checkPrivileges($uri, '{'.self::NS_CALDAV.'}read-free-busy'); + } + + $calendar = $this->server->tree->getNodeForPath($uri); + if (!$calendar instanceof ICalendar) { + throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars'); + } + + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + + // Figuring out the default timezone for the calendar, for floating + // times. + $calendarProps = $this->server->getProperties($uri, [$tzProp]); + + if (isset($calendarProps[$tzProp])) { + $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references so PHP will garbage collect the object. + $vtimezoneObj->destroy(); + } else { + $calendarTimeZone = new DateTimeZone('UTC'); + } + + // Doing a calendar-query first, to make sure we get the most + // performance. + $urls = $calendar->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $report->start, + 'end' => $report->end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $objects = array_map(function ($url) use ($calendar) { + $obj = $calendar->getChild($url)->get(); + + return $obj; + }, $urls); + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($report->start, $report->end); + $generator->setTimeZone($calendarTimeZone); + $result = $generator->getResult(); + $result = $result->serialize(); + + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); + $this->server->httpResponse->setHeader('Content-Length', strlen($result)); + $this->server->httpResponse->setBody($result); + } + + /** + * This method is triggered before a file gets updated with new content. + * + * This plugin uses this method to ensure that CalDAV objects receive + * valid calendar data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) + { + if (!$node instanceof ICalendarObject) { + return; + } + + // We're onyl interested in ICalendarObject nodes that are inside of a + // real calendar. This is to avoid triggering validation and scheduling + // for non-calendars (such as an inbox). + list($parent) = Uri\split($path); + $parentNode = $this->server->tree->getNodeForPath($parent); + + if (!$parentNode instanceof ICalendar) { + return; + } + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + false + ); + } + + /** + * This method is triggered before a new file is created. + * + * This plugin uses this method to ensure that newly created calendar + * objects contain valid calendar data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) + { + if (!$parentNode instanceof ICalendar) { + return; + } + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + true + ); + } + + /** + * Checks if the submitted iCalendar data is in fact, valid. + * + * An exception is thrown if it's not. + * + * @param resource|string $data + * @param string $path + * @param bool $modified should be set to true, if this event handler + * changed &$data + * @param RequestInterface $request the http request + * @param ResponseInterface $response the http response + * @param bool $isNew is the item a new one, or an update + */ + protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) + { + // If it's a stream, we convert it to a string first. + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + $before = $data; + + try { + // If the data starts with a [, we can reasonably assume we're dealing + // with a jCal object. + if ('[' === substr($data, 0, 1)) { + $vobj = VObject\Reader::readJson($data); + + // Converting $data back to iCalendar, as that's what we + // technically support everywhere. + $data = $vobj->serialize(); + $modified = true; + } else { + $vobj = VObject\Reader::read($data); + } + } catch (VObject\ParseException $e) { + throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: '.$e->getMessage()); + } + + if ('VCALENDAR' !== $vobj->name) { + throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.'); + } + + $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + + // Get the Supported Components for the target calendar + list($parentPath) = Uri\split($path); + $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); + + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } else { + $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; + } + + $foundType = null; + + foreach ($vobj->getComponents() as $component) { + switch ($component->name) { + case 'VTIMEZONE': + continue 2; + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + $foundType = $component->name; + break; + } + } + + if (!$foundType || !in_array($foundType, $supportedComponents)) { + throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type '.implode(', ', $supportedComponents)); + } + + $options = VObject\Node::PROFILE_CALDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ('strict' !== $prefer['handling']) { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; + } + switch ($message['level']) { + case 1: + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2: + // Level 2 means a warning, but not critical + break; + case 3: + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: '.$message['message']); + } + } + if ($warningMessage) { + $response->setHeader( + 'X-Sabre-Ew-Gross', + 'iCalendar validation warning: '.$warningMessage + ); + } + + // We use an extra variable to allow event handles to tell us whether + // the object was modified or not. + // + // This helps us determine if we need to re-serialize the object. + $subModified = false; + + $this->server->emit( + 'calendarObjectChange', + [ + $request, + $response, + $vobj, + $parentPath, + &$subModified, + $isNew, + ] + ); + + if ($modified || $subModified) { + // An event handler told us that it modified the object. + $data = $vobj->serialize(); + + // Using md5 to figure out if there was an *actual* change. + if (!$modified && 0 !== strcmp($data, $before)) { + $modified = true; + } + } + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + } + + /** + * This method is triggered whenever a subsystem reqeuests the privileges + * that are supported on a particular node. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + if ($node instanceof ICalendar) { + $supportedPrivilegeSet['{DAV:}read']['aggregates']['{'.self::NS_CALDAV.'}read-free-busy'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new calendars. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof CalendarHome) { + return; + } + + $output .= '
+

Create new calendar

+ + +
+
+ +
+ '; + + return false; + } + + /** + * This event is triggered after GET requests. + * + * This is used to transform data into jCal, if this was requested. + */ + public function httpAfterGet(RequestInterface $request, ResponseInterface $response) + { + $contentType = $response->getHeader('Content-Type'); + if (null === $contentType || false === strpos($contentType, 'text/calendar')) { + return; + } + + $result = HTTP\negotiateContentType( + $request->getHeader('Accept'), + ['text/calendar', 'application/calendar+json'] + ); + + if ('application/calendar+json' !== $result) { + // Do nothing + return; + } + + // Transforming. + $vobj = VObject\Reader::read($response->getBody()); + + $jsonBody = json_encode($vobj->jsonSerialize()); + $response->setBody($jsonBody); + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + + $response->setHeader('Content-Type', 'application/calendar+json'); + $response->setHeader('Content-Length', strlen($jsonBody)); + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for CalDAV (rfc4791)', + 'link' => 'http://sabre.io/dav/caldav/', + ]; + } +} diff --git a/files/apache.conf b/files/apache.conf deleted file mode 100644 index b684dc8..0000000 --- a/files/apache.conf +++ /dev/null @@ -1,59 +0,0 @@ -# InjectedServerName dav.example.com - -# For more details, see http://sabre.io/baikal/install/ - - - # InjectedServerAlias dav.example.org dav.example.io - DocumentRoot /var/www/baikal/html - - RewriteEngine On - RewriteRule /.well-known/carddav /dav.php [R,L] - RewriteRule /.well-known/caldav /dav.php [R,L] - - - Options None - Options +FollowSymlinks - AllowOverride All - - # Confiugration for apache-2.2: - Order allow,deny - Allow from all - - # Confiugration for apache-2.4: - Require all granted - - - - - - - # InjectedServerAlias dav.example.org dav.example.io - DocumentRoot /var/www/baikal/html - - RewriteEngine On - RewriteRule /.well-known/carddav /dav.php [R,L] - RewriteRule /.well-known/caldav /dav.php [R,L] - - - Options None - Options +FollowSymlinks - AllowOverride All - - # Confiugration for apache-2.2: - Order allow,deny - Allow from all - - # Confiugration for apache-2.4: - Require all granted - - - # For more details, see https://bettercrypto.org/#_apache - SSLEngine on - SSLCertificateFile /etc/ssl/private/baikal.public.pem - SSLCertificateKeyFile /etc/ssl/private/baikal.private.pem - SSLProtocol All -SSLv2 -SSLv3 - SSLHonorCipherOrder On - SSLCompression off - SSLCipherSuite 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA' - - diff --git a/files/docker-entrypoint.d/httpd/40-fix-baikal-file-permissions.sh b/files/docker-entrypoint.d/httpd/40-fix-baikal-file-permissions.sh deleted file mode 100755 index cb07c1c..0000000 --- a/files/docker-entrypoint.d/httpd/40-fix-baikal-file-permissions.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -chown -R www-data:www-data /var/www/baikal diff --git a/files/docker-entrypoint.d/httpd/40-inject-httpd-server.sh b/files/docker-entrypoint.d/httpd/40-inject-httpd-server.sh deleted file mode 100755 index dffbfd1..0000000 --- a/files/docker-entrypoint.d/httpd/40-inject-httpd-server.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -# Inject ServerName and ServerAlias (if specified) into the Apache httpd configuration - -APACHE_CONFIG="/etc/apache2/sites-available/000-default.conf" -if [ ! -z ${BAIKAL_SERVERNAME+x} ] -then - sed -i "s/# InjectedServerName .*/ServerName $BAIKAL_SERVERNAME/g" $APACHE_CONFIG -fi - -if [ ! -z ${BAIKAL_SERVERALIAS+x} ] -then - sed -i "s/# InjectedServerAlias .*/ServerAlias $BAIKAL_SERVERALIAS/g" $APACHE_CONFIG -fi diff --git a/nginx-php8.0.dockerfile b/nginx-php8.0.dockerfile deleted file mode 100644 index d36b62d..0000000 --- a/nginx-php8.0.dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# Multi-stage build, see https://docs.docker.com/develop/develop-images/multistage-build/ -FROM alpine AS builder - -ENV VERSION 0.9.3 - -ADD https://github.com/sabre-io/Baikal/releases/download/$VERSION/baikal-$VERSION.zip . -RUN apk add unzip && unzip -q baikal-$VERSION.zip - -# Final Docker image -FROM nginx:1 - -LABEL description="Baikal is a Cal and CardDAV server, based on sabre/dav, that includes an administrative interface for easy management." -LABEL version="0.9.3" -LABEL repository="https://github.com/ckulka/baikal-docker" -LABEL website="http://sabre.io/baikal/" - -# Install dependencies: PHP (with libffi6 dependency) & SQLite3 -RUN curl -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg &&\ - apt update &&\ - apt install -y lsb-release &&\ - echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list &&\ - apt remove -y lsb-release &&\ - apt update &&\ - apt install -y \ - php8.0-curl \ - php8.0-fpm \ - php8.0-mbstring \ - php8.0-mysql \ - php8.0-sqlite3 \ - php8.0-xml \ - sqlite3 \ - msmtp msmtp-mta &&\ - rm -rf /var/lib/apt/lists/* &&\ - sed -i 's/www-data/nginx/' /etc/php/8.0/fpm/pool.d/www.conf &&\ - sed -i 's/^listen = .*/listen = \/var\/run\/php-fpm.sock/' /etc/php/8.0/fpm/pool.d/www.conf - -# Add Baikal & nginx configuration -COPY --from=builder --chown=nginx:nginx baikal /var/www/baikal -COPY files/docker-entrypoint.d/*.sh files/docker-entrypoint.d/nginx/ /docker-entrypoint.d/ -COPY files/nginx.conf /etc/nginx/conf.d/default.conf - -VOLUME /var/www/baikal/config -VOLUME /var/www/baikal/Specific