ctucx.git: tinyDAV

[php] Cal-/ CardDAV server with a simple web-GUI based on SabreDAV

commit ae4f03c1bdebdf94099ae01a4b81a2c275f539c4
Author: ctucx <c@ctu.cx>
Date: Tue, 11 Feb 2020 17:13:37 +0100

init
34 files changed, 4875 insertions(+), 0 deletions(-)
A
docker-compose.yml
|
16
++++++++++++++++
A
docker/Dockerfile
|
60
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
docker/entrypoint.sh
|
38
++++++++++++++++++++++++++++++++++++++
A
docker/infcloud-config.js
|
993
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
docker/nginx.conf
|
46
++++++++++++++++++++++++++++++++++++++++++++++
A
docker/php-settings.ini
|
6
++++++
A
public/composer.json
|
7
+++++++
A
public/composer.lock
|
510
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/dav.php
|
76
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/index.php
|
334
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/install.php
|
25
+++++++++++++++++++++++++
A
public/lib/AddressbookCollectionManager.php
|
47
+++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/CalendarCollectionManager.php
|
78
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Database.php
|
113
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/GenericCollectionManager.php
|
237
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Helpers.php
|
40
++++++++++++++++++++++++++++++++++++++++
A
public/lib/JSONDB.php
|
446
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Router.php
|
105
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Sabre/FilesPlugin.php
|
122
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Sabre/SabreAuthenticationJsonBackend.php
|
34
++++++++++++++++++++++++++++++++++
A
public/lib/Sabre/SabreCalDAVJsonBackend.php
|
367
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Sabre/SabreCardDAVJsonBackend.php
|
127
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Sabre/SabrePrincipalJsonBackend.php
|
201
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Sabre/SabrePropertyStorageJsonBackend.php
|
194
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/Template.php
|
162
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/lib/UserManager.php
|
142
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/main.css
|
7
+++++++
A
public/milligram.min.css
|
12
++++++++++++
A
public/template/footer.tpl
|
6
++++++
A
public/template/header.tpl
|
16
++++++++++++++++
A
public/template/login.tpl
|
18
++++++++++++++++++
A
public/template/message.tpl
|
8
++++++++
A
public/template/overview.tpl
|
143
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
public/template/update.tpl
|
139
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/docker-compose.yml b/docker-compose.yml
@@ -0,0 +1,15 @@
+version: "3"
+
+services:
+  tinydav:
+    build:
+      context: ./
+      dockerfile: ./docker/Dockerfile
+    image: tinydav
+    container_name: ctucx_tinydav
+    user: "1000:1000"
+    restart: always
+    volumes:
+      - ./data:/app/data
+    ports:
+      - "8088:8080"+
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
@@ -0,0 +1,60 @@
+FROM php:7.4-fpm-alpine
+
+ARG UID=1000
+ARG GID=1000
+
+ARG NGINX_VER=1.17.6
+ARG NGINX_CONF="--prefix=/app --with-cc-opt='-static' \
+		--with-ld-opt='-static' --with-cpu-opt=generic --with-pcre \
+		--sbin-path=/app/nginx \
+		--http-log-path=/app/log/access.log \
+		--error-log-path=/app/log/error.log \
+		--pid-path=/app/nginx.pid \
+		--lock-path=/app/nginx.lock \
+		--without-http_gzip_module \
+		--without-http_uwsgi_module \
+		--without-http_scgi_module \
+		--without-http_memcached_module \
+		--without-http_empty_gif_module \
+		--without-http_geo_module \
+		--with-threads"
+
+WORKDIR /tmp
+
+COPY ./public                    /app/www
+COPY ./docker/php-settings.ini	 $PHP_INI_DIR/conf.d/settings.ini
+COPY ./docker/entrypoint.sh      /app/entrypoint.sh
+COPY ./docker/nginx.conf         /app/nginx.conf
+
+RUN apk --update upgrade && \
+    apk add --no-cache --no-progress build-base pcre-dev wget libxml2 libxml2-dev && \
+    wget http://nginx.org/download/nginx-${NGINX_VER}.tar.gz && \
+    wget https://www.inf-it.com/InfCloud_0.13.1.zip && \
+    wget https://getcomposer.org/download/1.9.3/composer.phar -O /tmp/composer.phar && \
+    tar xzf nginx-${NGINX_VER}.tar.gz && \
+    unzip InfCloud_0.13.1.zip && \
+    chmod +x /tmp/composer.phar && \
+    cd /tmp/nginx-${NGINX_VER} && \
+    ./configure ${NGINX_CONF} && \
+    make -j 1 && \
+    make install && \
+    docker-php-ext-install simplexml dom && \
+    apk del --no-cache --no-progress build-base pcre-dev wget libxml2-dev && \
+    mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" && \
+    cd /app/www && /tmp/composer.phar install && \
+    mkdir /app/tmp && \
+    mkdir /app/data && \
+    mkdir /app/logs && \
+    cp -r /tmp/infcloud /app/www/infcloud && \
+    chown -R ${UID}:${GID} /app && \
+    chmod +x /app/entrypoint.sh && \
+    chmod 7777 /app/tmp && \
+    rm -rf /tmp/nginx-${NGINX_VER} /tmp/nginx-${NGINX_VER}.tar.gz /tmp/composer.phar /tmp/InfCloud_0.13.1.zip /tmp/infcloud
+
+COPY ./docker/infcloud-config.js /app/www/infcloud/config.js
+
+EXPOSE 8080
+
+VOLUME [ "/app/data" ]
+
+ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+php /app/www/install.php
+
+# Start the first process
+/app/nginx -c /app/nginx.conf
+status=$?
+if [ $status -ne 0 ]; then
+  echo "Failed to start my_first_process: $status"
+  exit $status
+fi
+
+# Start the second process
+php-fpm
+status=$?
+if [ $status -ne 0 ]; then
+  echo "Failed to start my_second_process: $status"
+  exit $status
+fi
+
+# Naive check runs checks once a minute to see if either of the processes exited.
+# This illustrates part of the heavy lifting you need to do if you want to run
+# more than one service in a container. The container exits with an error
+# if it detects that either of the processes has exited.
+# Otherwise it loops forever, waking up every 60 seconds
+
+while sleep 60; do
+  ps aux |grep my_first_process |grep -q -v grep
+  PROCESS_1_STATUS=$?
+  ps aux |grep my_second_process |grep -q -v grep
+  PROCESS_2_STATUS=$?
+  # If the greps above find anything, they exit with 0 status
+  # If they are not both 0, then something is wrong
+  if [ $PROCESS_1_STATUS -ne 0 -o $PROCESS_2_STATUS -ne 0 ]; then
+    echo "One of the processes has already exited."
+    exit 1
+  fi
+done
diff --git a/docker/infcloud-config.js b/docker/infcloud-config.js
@@ -0,0 +1,993 @@
+var globalNetworkCheckSettings={
+	href: 'https://dav.ctu.cx/dav/principals/',
+	timeOut: 90000,
+	lockTimeOut: 10000,
+	checkContentType: true,
+	settingsAccount: true,
+	delegation: false,
+	additionalResources: [],
+	hrefLabel: null,
+	forceReadOnly: null,
+	ignoreAlarms: false,
+	backgroundCalendars: []
+}
+
+var globalBackgroundSync=true;
+var globalSyncResourcesInterval=120000;
+var globalEnableRefresh=true;
+var globalEnableKbNavigation=true;
+
+
+// globalSettingsType
+// Where to store user settings such as: active view, enabled/selected
+// collections, ... (the client store them into DAV property on the server).
+// NOTE: not all servers support storing DAV properties (some servers support
+// only subset /or none/ of these URLs).
+// Supported values:
+// - 'principal-URL', '', null or undefined (default) => settings are stored
+//   to principal-URL (recommended for most servers)
+// - 'addressbook-home-set' => settings are are stored to addressbook-home-set
+// - 'calendar-home-set' => settings are stored to calendar-home-set
+// Example:
+//var globalSettingsType='';
+
+
+// globalCrossServerSettingsURL
+// Settings such as enabled/selected collections are stored on the server
+// (see the previous option) in form of full URL
+// (e.g.: https://user@server:port/principal/collection/), but even if this
+// approach is "correct" (you can use the same principal URL with multiple
+// different logins, ...) it causes a problem if your server is accessible
+// from multiple URLs (e.g. http://server/ and https://server/). If you want
+// to store only the "principal/collection/" part of the URL (instead of the
+// full URL) then enable this option.
+// Example:
+//var globalCrossServerSettingsURL=false;
+
+
+var globalInterfaceLanguage='de_DE';
+
+
+var globalInterfaceCustomLanguages=['en_US', 'de_DE'];
+
+var globalSortAlphabet=' 0123456789'+
+	'AÀÁÂÄÆÃÅĀBCÇĆČDĎEÈÉÊËĒĖĘĚFGĞHIÌÍÎİÏĪĮJKLŁĹĽMNŃÑŇOÒÓÔÖŐŒØÕŌ'+
+	'PQRŔŘSŚŠȘșŞşẞTŤȚțŢţUÙÚÛÜŰŮŪVWXYÝŸZŹŻŽ'+
+	'aàáâäæãåābcçćčdďeèéêëēėęěfgğhiìíîïīįıjklłĺľmnńñňoòóôöőœøõō'+
+	'pqrŕřsśšßtťuùúûüűůūvwxyýÿzźżžАБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЮЯ'+
+	'Ьабвгґдеєжзиіїйклмнопрстуфхцчшщюяь';
+
+var globalSearchTransformAlphabet={
+	'[ÀàÁáÂâÄäÆæÃãÅåĀā]': 'a', '[ÇçĆćČč]': 'c', '[Ďď]': 'd',
+	'[ÈèÉéÊêËëĒēĖėĘęĚě]': 'e', '[Ğğ]': 'g', '[ÌìÍíÎîİıÏïĪīĮį]': 'i',
+	'[ŁłĹ弾]': 'l', '[ŃńÑñŇň]': 'n', '[ÒòÓóÔôÖöŐőŒœØøÕõŌō]': 'o',
+	'[ŔŕŘř]': 'r', '[ŚśŠšȘșŞşẞß]': 's', '[ŤťȚțŢţ]': 't',
+	'[ÙùÚúÛûÜüŰűŮůŪū]': 'u', '[ÝýŸÿ]': 'y', '[ŹźŻżŽž]': 'z'
+};
+
+// globalResourceAlphabetSorting
+// If more than one resource (server account) is configured, sort the
+// resources alphabetically?
+// Example:
+var globalResourceAlphabetSorting=true;
+
+
+// globalNewVersionNotifyUsers
+// Update notification will be shown only to users with login names defined
+// in this array.
+// If undefined (or empty), update notifications will be shown to all users.
+// Example:
+//   globalNewVersionNotifyUsers=['admin', 'peter'];
+var globalNewVersionNotifyUsers=[];
+
+
+// globalDatepickerFormat
+// Set the datepicker format (see 
+// http://docs.jquery.com/UI/Datepicker/formatDate for valid values).
+// NOTE: date format is predefined for each localization - use this option
+// ONLY if you want to use custom date format (instead of the localization
+// predefined one).
+// Example:
+//var globalDatepickerFormat='dd.mm.yy';
+
+
+// globalDatepickerFirstDayOfWeek
+// Set the datepicker first day of the week: Sunday is 0, Monday is 1, etc.
+// Example:
+var globalDatepickerFirstDayOfWeek=1;
+
+
+// globalHideInfoMessageAfter
+// How long are information messages (such as: success, error) displayed
+// (in miliseconds).
+// Example:
+var globalHideInfoMessageAfter=1800;
+
+
+// globalEditorFadeAnimation
+// Set the editor fade in/out animation duration when editing or saving data
+// (in miliseconds).
+// Example:
+var globalEditorFadeAnimation=666;
+
+
+
+
+// ******* CalDAV (CalDavZAP) related settings ******* //
+
+// globalEventStartPastLimit, globalEventStartFutureLimit, globalTodoPastLimit
+// Number of months pre-loaded from past/future in advance for calendars
+// and todo lists (if null then date range synchronization is disabled).
+// NOTE: interval synchronization is used only if your server supports
+// sync-collection REPORT (e.g. DAViCal).
+// NOTE: if you experience problems with data loading and your server has
+// no time-range filtering support set these variables to null.
+// Example:
+var globalEventStartPastLimit=3;
+var globalEventStartFutureLimit=3;
+var globalTodoPastLimit=1;
+
+
+// globalLoadedCalendarCollections
+// This option sets the list of calendar collections (down)loaded after login.
+// If empty then all calendar collections for the currently logged user are
+// loaded.
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+var globalLoadedCalendarCollections=[];
+
+
+// globalLoadedTodoCollections
+// This option sets the list of todo collections (down)loaded after login.
+// If empty then all todo collections for the currently logged user are loaded.
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+var globalLoadedTodoCollections=[];
+
+
+// globalActiveCalendarCollections
+// This options sets the list of calendar collections checked (enabled
+// checkbox => data visible in the interface) by default after login.
+// If empty then all loaded calendar collections for the currently logged
+// user are checked.
+// NOTE: only already (down)loaded collections can be checked (see 
+// the globalLoadedCalendarCollections option).
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+var globalActiveCalendarCollections=[];
+
+
+// globalActiveTodoCollections
+// This options sets the list of todo collections checked (enabled
+// checkbox => data visible in the interface) by default after login.
+// If empty then all loaded todo collections for the currently logged
+// user are checked.
+// NOTE: only already (down)loaded collections can be checked (see 
+// the globalLoadedTodoCollections option).
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+var globalActiveTodoCollections=[];
+
+
+// globalCalendarSelected
+// This option sets which calendar collection will be pre-selected
+// (if you create a new event) by default after login.
+// The value must be URL encoded path to a calendar collection,
+// for example: 'USER/calendar/'
+// If empty or undefined then the first available calendar collection
+// is selected automatically.
+// NOTE: only already (down)loaded collections can be pre-selected (see
+// the globalLoadedCalendarCollections option).
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+//var globalCalendarSelected='';
+
+
+// globalTodoCalendarSelected
+// This option sets which todo collection will be pre-selected
+// (if you create a new todo) by default after login.
+// The value must be URL encoded path to a todo collection,
+// for example: 'USER/todo_calendar/'
+// If empty or undefined then the first available todo collection
+// is selected automatically.
+// NOTE: only already (down)loaded collections can be pre-selected (see 
+// the globalLoadedTodoCollections option).
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+//var globalTodoCalendarSelected='';
+
+
+// globalActiveView
+// This options sets the default fullcalendar view option (the default calendar
+// view after the first login).
+// Supported values:
+// - 'month'
+// - 'multiWeek'
+// - 'agendaWeek'
+// - 'agendaDay'
+// NOTE: we use custom and enhanced version of fullcalendar!
+// Example:
+var globalActiveView='multiWeek';
+
+
+// globalOpenFormMode
+// Open new event form on 'single' or 'double' click.
+// If undefined or not 'double', then 'single' is used.
+// Example:
+var globalOpenFormMode='double';
+
+
+// globalTodoListFilterSelected
+// This options sets the list of filters in todo list that are selected
+// after login.
+// Supported options:
+// - 'filterAction'
+// - 'filterProgress' (available only if globalAppleRemindersMode is disabled)
+// - 'filterCompleted'
+// - 'filterCanceled' (available only if globalAppleRemindersMode is disabled)
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+var globalTodoListFilterSelected=['filterAction', 'filterProgress'];
+
+
+// globalCalendarStartOfBusiness, globalCalendarEndOfBusiness
+// These options set the start and end of business hours with 0.5 hour
+// precision. Non-business hours are faded out in the calendar interface.
+// If both variables are set to the same value then no fade out occurs.
+// Example:
+var globalCalendarStartOfBusiness=8;
+var globalCalendarEndOfBusiness=17;
+
+
+// globalDefaultEventDuration
+// This option sets the default duration (in minutes) for newly created events.
+// If undefined or null, globalCalendarEndOfBusiness value will be taken as
+// a default end time instead.
+// Example:
+var globalDefaultEventDuration=120;
+
+
+// globalAMPMFormat
+// This option enables to use 12 hours format (AM/PM) for displaying time.
+// NOTE: time format is predefined for each localization - use this option
+// ONLY if you want to use custom time format (instead of the localization
+// predefined one).
+// Example:
+//var globalAMPMFormat=false;
+
+
+// globalTimeFormatBasic
+// This option defines the time format information for events in month and
+// multiweek views. If undefined or null then default value is used.
+// If defined as empty string no time information is shown in these views.
+// See http://arshaw.com/fullcalendar/docs/utilities/formatDate/ for exact
+// formating rules.
+// Example:
+//var globalTimeFormatBasic='';
+
+
+// globalTimeFormatAgenda
+// This option defines the time format information for events in day and
+// week views. If undefined or null then default value is used.
+// If defined as empty string no time information is shown in these views.
+// See http://arshaw.com/fullcalendar/docs/utilities/formatDate/ for exact
+// formating rules.
+// Example:
+//var globalTimeFormatAgenda='';
+
+
+// globalDisplayHiddenEvents
+// This option defined whether events from unechecked calendars are displayed
+// with certain transparency (true) or completely hidden (false).
+// Example:
+var globalDisplayHiddenEvents=false;
+
+
+// globalTimeZoneSupport
+// This option enables timezone support in the client.
+// NOTE: timezone cannot be specified for all-day events because these don't
+// have start and end time.
+// If this option is disabled then local time is used.
+// Example:
+var globalTimeZoneSupport=true;
+
+
+// globalTimeZone
+// If timezone support is enabled, this option sets the default timezone.
+// See timezones.js or use the following command to get the list of supported
+// timezones (defined in timezones.js):
+// grep "'[^']\+': {" timezones.js | sed -Ee "s#(\s*'|':\s*\{)##g"
+// Example:
+var globalTimeZone='Europe/Berlin';
+
+
+// globalTimeZonesEnabled
+// This option sets the list of available timezones in the interface (for the 
+// list of supported timezones see the comment for the previous configuration
+// option).
+// NOTE: if there is at least one event/todo with a certain timezone defined,
+// that timezone is enabled (even if it is not present in this list).
+// Example:
+//   var globalTimeZonesEnabled=['America/New_York', 'Europe/Berlin'];	
+var globalTimeZonesEnabled=[];
+
+
+// globalRewriteTimezoneComponent
+// This options sets whether the client will enhance/replace (if you edit an
+// event or todo) the timezone information using the official IANA timezone
+// database information (recommended).
+// Example:
+var globalRewriteTimezoneComponent=true;
+
+
+// globalRemoveUnknownTimezone
+// This options sets whether the client will remove all non-standard timezone
+// names from events and todos (if you edit an event or todo)
+// (e.g.: /freeassociation.sourceforge.net/Tzfile/Europe/Vienna)
+// Example:
+var globalRemoveUnknownTimezone=false;
+
+
+// globalShowHiddenAlarms
+// This option sets whether the client will show alarm notifications for
+// unchecked calendars. If this option is enabled and you uncheck a calendar
+// in the calendar list, alarm notifications will be temporary disabled for
+// unchecked calendar(s).
+// Example:
+var globalShowHiddenAlarms=false;
+
+
+// globalIgnoreCompletedOrCancelledAlarms
+// This options sets whether the client will show alarm notifications for
+// already completed or cancelled todos. If enabled then alarm notification
+// for completed and cancelled todos are disabled.
+// Example:
+var globalIgnoreCompletedOrCancelledAlarms=true;
+
+
+// globalMozillaSupport
+// Mozilla automatically treats custom repeating event calculations as if
+// the start day of the week is Monday, despite what day is chosen in settings.
+// Set this variable to true to use the same approach, ensuring compatible
+// event rendering in special cases.
+// Example:
+var globalMozillaSupport=false;
+
+
+// globalCalendarColorPropertyXmlns
+// This options sets the namespace used for storing the "calendar-color"
+// property by the client.
+// If true, undefined (or empty) "http://apple.com/ns/ical/" is used (Apple
+// compatible). If false, then the calendar color modification functionality
+// is completely disabled.
+// Example:
+//var globalCalendarColorPropertyXmlns=true;
+
+
+// globalWeekendDays
+// This option sets the list of days considered as weekend days (these
+// are faded out in the calendar interface). Non-weekend days are automatically
+// considered as business days.
+// Sunday is 0, Monday is 1, etc.
+// Example:
+var globalWeekendDays=[0, 6];
+
+
+// globalAppleRemindersMode
+// If this option is enabled then then client will use the same approach
+// for handling repeating reminders (todos) as Apple. It is STRONGLY
+// recommended to enabled this option if you use any Apple clients for
+// reminders (todos).
+// Supported options:
+// - 'iOS6'
+// - 'iOS7'
+// - true (support of the latest iOS version - 'iOS8')
+// - false
+// If this option is enabled:
+// - RFC todo support is SEVERELY limited and the client mimics the behaviour
+//   of Apple Reminders.app (to ensure maximum compatibility)
+// - when a single instance of repeating todo is edited, it becomes an
+//   autonomous non-repeating todo with NO relation to the original repeating
+//   todo
+// - capabilities of repeating todos are limited - only the first instance
+//   is ever visible in the interface
+// - support for todo DTSTART attribute is disabled
+// - support for todo STATUS attribute other than COMPLETED and NEEDS-ACTION
+//   is disabled
+// - [iOS6 only] support for LOCATION and URL attributes is disabled
+// Example:
+var globalAppleRemindersMode=true;
+
+
+// globalSubscribedCalendars
+// This option specifies a list of remote URLs to ics files (e.g.: used
+// for distributing holidays information). Subscribed calendars are
+// ALWAYS read-only. Remote servers where ics files are hosted MUST
+// return proper CORS headers (see readme.txt) otherwise this functionality
+// will not work!
+// NOTE: subsribed calendars are NOT "shared" calendars. For "shared"
+// calendars see the delegation option in globalAccountSettings,
+// globalNetworkCheckSettings and globalNetworkAccountSettings.
+// List of properties used in globalSubscribedCalendars variable:
+// - hrefLabel
+//   This options defines the header string above the subcsribed calendars.
+// - calendars
+//   This option specifies an array of remote calendar objects with the
+//   following properties:
+//   - href
+//     Set this option to the "full URL" of the remote calendar
+//   - userAuth
+//     NOTE: keep empty if remote authentication is not required!
+//     - userName
+//       Set the username you want to login.
+//     - userPassword
+//       Set the password for the given username.
+//   - typeList
+//     Set the list of objects you want to process from remote calendars;
+//     two options are available:
+//     - 'vevent' (show remote events in the interface) 
+//     - 'vtodo' (show remote todos in the interface) 
+//   - ignoreAlarm
+//     Set this option to true if you want to disable alarm notifications
+//     from the remote calendar.
+//   - displayName
+//     Set this option to the name of the calendar you want to see
+//     in the interface.
+//   - color
+//     Set the calendar color you want to see in the interface.
+// Example:
+//var globalSubscribedCalendars={
+//	hrefLabel: 'Subscribed',
+//	calendars: [
+//		{
+//			href: 'http://something.com/calendar.ics',
+//			userAuth: {
+//				userName: '',
+//				userPassword: ''
+//			},
+//			typeList: ['vevent', 'vtodo'],
+//			ignoreAlarm: true,
+//			displayName: 'Remote Calendar 1',
+//			color: '#ff0000'
+//		},
+//		{
+//			href: 'http://calendar.com/calendar2.ics',
+//			...
+//			...
+//		}
+//	]
+//};
+
+
+
+// ******* CardDAV (CardDavMATE) related settings ******* //
+
+
+// globalLoadedAddressbookCollections
+// This option sets the list of addressbook collections (down)loaded after
+// login. If empty then all addressbook collections for the currently logged
+// user are loaded.
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+var globalLoadedAddressbookCollections=[];
+
+
+// globalActiveAddressbookCollections
+// This options sets the list of addressbook collections checked (enabled
+// checkbox => data visible in the interface) by default after login.
+// If empty then all loaded addressbook collections for the currently logged
+// user are checked.
+// NOTE: only already (down)loaded collections can be checked (see
+// the globalLoadedAddressbookCollections option).
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+var globalActiveAddressbookCollections=[];
+
+
+// globalAddressbookSelected
+// This option sets which addressbook collection will be pre-selected
+// (if you create a new contact) by default after login.
+// The value must be URL encoded path to an addressbook collection,
+// for example: 'USER/addressbook/'
+// If empty or undefined then the first available addressbook collection
+// is selected automatically.
+// NOTE: only already (down)loaded collections can be pre-selected (see
+// the globalLoadedAddressbookCollections option).
+// NOTE: settings stored on the server (see settingsAccount) overwrite this
+// option.
+// Example:
+//var globalAddressbookSelected='';
+
+
+// globalCompatibility
+// This options is reserved for various compatibility settings.
+// NOTE: if this option is used the value must be an object.
+// Currently there is only one supported option:
+// - anniversaryOutputFormat
+//   Different clients use different (and incompatible) approach
+//   to store anniversary date in vCards. Apple stores this attribute as:
+//     itemX.X-ABDATE;TYPE=pref:2000-01-01\r\n
+//     itemX.X-ABLabel:_$!<Anniversary>!$_\r\n'
+//   other clients store this attribute as:
+//     X-ANNIVERSARY:2000-01-01\r\n
+//   Choose 'apple' or 'other' (lower case) for your 3rd party client
+//   compatibility. You can chose both: ['apple', 'other'], but it may
+//   cause many problems in the future, for example: duplicate anniversary
+//   dates, invalid/old anniversary date in your clients, ...)
+//   Examples:
+//     anniversaryOutputFormat: ['other']
+//     anniversaryOutputFormat: ['apple', 'other']
+// Example:
+var globalCompatibility={anniversaryOutputFormat: ['apple']};
+
+
+// globalUriHandler{Tel,Email,Url,Profile}
+// These options set the URI handlers for TEL, EMAIL, URL and X-SOCIALPROFILE
+// vCard attributes. Set them to null (or comment out) to disable.
+// NOTE: for globalUriHandlerTel is recommended to use 'tel:', 'callto:'
+// or 'skype:'. The globalUriHandlerUrl value is used only if no URI handler
+// is defined in the URL.
+// NOTE: it is safe to keep these values unchanged!
+// Example:
+var globalUriHandlerTel='tel:';
+var globalUriHandlerEmail='mailto:';
+var globalUriHandlerUrl='http://';
+var globalUriHandlerProfile={
+	'twitter': 'http://twitter.com/%u',
+	'facebook': 'http://www.facebook.com/%u',
+	'flickr': 'http://www.flickr.com/photos/%u',
+	'linkedin': 'http://www.linkedin.com/in/%u',
+	'myspace': 'http://www.myspace.com/%u',
+	'sinaweibo': 'http://weibo.com/n/%u'
+};
+
+
+// globalDefaultAddressCountry
+// This option sets the default country for new address fields.
+// See common.js or use the following command to get the list of
+// all supported country codes (defined in common.js):
+// grep -E "'[a-z]{2}':\s+\[" common.js | sed -Ee 's#^\s+|\s+\[\s+# #g'
+// Example:
+var globalDefaultAddressCountry='us';
+
+
+// globalAddressCountryEquivalence
+// This option sets the processing of the country field specified
+// in the vCard ADR attribute.
+// By default the address field in vCard looks like:
+//   ADR;TYPE=WORK:;;1 Waters Edge;Baytown;LA;30314;USA\r\n
+// what cause a problem, because the country field is a plain
+// text and can contain any value, e.g.:
+//   USA
+//   United States of America
+//   US
+// and because the address format can be completely different for
+// each country, e.g.:
+//   China address example:
+//     [China]
+//     [Province] [City]
+//     [Street]
+//     [Postal]
+//   Japan address example:
+//     [Postal]
+//     [Prefecture] [County/City]
+//     [Further Divisions]
+//     [Japan]
+// the client needs to correctly detect the country from the ADR
+// attribute. Apple solved this problem by using:
+//   item1.ADR;TYPE=WORK:;;1 Waters Edge;Baytown;LA;30314;USA\r\n
+//   item1.X-ABADR:us\r\n
+// where the second "related" attribute defines the country code
+// for the ADR attribute. This client uses the same approach, but
+// if the vCard is created by 3rd party clients and the X-ABADR
+// is missing, it is possible to define additional "rules" for
+// country matching. These rules are specied by the country code
+// (for full list of country codes see the comment for pre previous
+// option) and a case insensitive regular expression (which matches
+// the plain text value in the country field).
+// NOTE: if X-ABADR is not present and the country not matches any
+// country defined in this option, then globalDefaultAddressCountry
+// is used by default.
+// Example:
+var globalAddressCountryEquivalence=[
+	{country: 'de', regex: '^\\W*Deutschland\\W*$'},
+	{country: 'sk', regex: '^\\W*Slovensko\\W*$'}
+];
+
+
+// globalAddressCountryFavorites
+// This option defines the list of countries which are shown at the top
+// of the country list in the interface (for full list of country codes
+// see the comment for pre globalDefaultAddressCountry option).
+// Example:
+//   var globalAddressCountryFavorites=['de','sk'];
+var globalAddressCountryFavorites=[];
+
+
+// globalAddrColorPropertyXmlns
+// This options sets the namespace used for storing the "addressbook-color"
+// property by the client.
+// If true, undefined (or empty) "http://inf-it.com/ns/ab/" is used.
+// If false, then the addressbook color modification functionality
+// is completely disabled, and addressbook colors in the interface are
+// generated automatically.
+// Example:
+//var globalAddrColorPropertyXmlns=true;
+
+
+// globalContactStoreFN
+// This option specifies how the FN (formatted name) is stored into vCard.
+// The value for this options must be an array of strings, that can contain
+// the following variables:
+//   prefix
+//   last
+//   middle
+//   first
+//   suffix
+// The string element of the array can contain any other characters (usually
+// space or colon). Elements are added into FN only if the there is
+// a variable match, for example if:
+//     last='Lastname'
+//     first='Firstname'
+//     middle='' (empty)
+//   and this option is set to:
+//     ['last', ' middle', ' first'] (space in the second and third element)
+//   the resulting value for FN will be: 'Lastname Firstname' and not
+//   'Lastname  Firstname' (two spaces), because the middle name is empty (so
+//   the second element is completely ignored /not added into FN/).
+// NOTE: this attribute is NOT used by this client, and it is also NOT
+// possible to directly edit it in the interface.
+// Examples:
+//   var globalContactStoreFN=[' last', ' middle', ' first'];
+//   var globalContactStoreFN=['last', ', middle', ' ,first'];
+var globalContactStoreFN=['prefix',' last',' middle',' first',' suffix'];
+
+
+// globalGroupContactsByCompanies
+// This options specifies how contacts are grouped in the interface.
+// By default the interface looks like (very simple example):
+//   A
+//    Adams Adam
+//    Anderson Peter
+//   B
+//    Brown John
+//    Baker Josh
+// if grouped by company/deparment the result is:
+//   Company A [Department X]
+//    Adams Adam
+//    Brown John
+//   Company B [Department Y]
+//    Anderson Peter
+//    Baker Josh
+// If this option is set to true contacts are grouped by company/department,
+// otherwise (default) contacts are grouped by letters of the alphabet.
+// If undefined or not true, grouping by alphabet letters is used.
+// NOTE: see also the globalCollectionDisplay option below.
+var globalGroupContactsByCompanies=false;
+
+
+// globalCollectionDisplay
+// This options specifies how data columns in the contact list are displayed.
+//
+// NOTE: columns are displayed ONLY if there is enought horizontal place in
+// the browser window (e.g. if you define 5 columns here, but your browser
+// window is not wide enough, you will see only first 3 columns instead of 5).
+//
+// NOTE: see the globalContactDataMinVisiblePercentage option which defines the
+// width for columns.
+//
+// The value must be an array of columns, where each column is represented by
+// an object with the following properties:
+//   label => the value of this option is a string used as column header
+//     You can use the following localized variables in the label string:
+//     - {Name}
+//     - {FirstName}
+//     - {LastName}
+//     - {MiddleName}
+//     - {NickName}
+//     - {Prefix}
+//     - {Suffix}
+//     - {BirthDay}
+//     - {PhoneticLastName}
+//     - {PhoneticFirstName}
+//     - {JobTitle}
+//     - {Company}
+//     - {Department}
+//     - {Categories}
+//     - {NoteText}
+//     - {Address}, {AddressWork}, {AddressHome}, {AddressOther}
+//     - {Phone}, {PhoneWork}, {PhoneHome}, {PhoneCell}, {PhoneMain},
+//       {PhonePager}, {PhoneFax}, {PhoneIphone}, {PhoneOther}
+//     - {Email}, {EmailWork}, {EmailHome}, {EmailMobileme}, {EmailOther}
+//     - {URL}, {URLWork}, {URLHome}, {URLHomepage}, {URLOther}
+//     - {Dates}, {DatesAnniversary}, {DatesOther}
+//     - {Related}, {RelatedManager}, {RelatedAssistant}, {RelatedFather},
+//       {RelatedMother}, {RelatedParent}, {RelatedBrother}, {RelatedSister},
+//       {RelatedChild}, {RelatedFriend}, {RelatedSpouse}, {RelatedPartner},
+//       {RelatedOther}
+//     - {Profile}, {ProfileTwitter}, {ProfileFacebook}, {ProfileFlickr},
+//       {ProfileLinkedin}, {ProfileMyspace}, {ProfileSinaweibo}
+//     - {IM}, {IMWork}, {IMHome}, {IMMobileme}, {IMOther}, {IMAim}, {IMIcq},
+//       {IMIrc}, {IMJabber}, {IMMsn}, {IMYahoo}, {IMFacebook}, {IMGadugadu},
+//       {IMGoogletalk}, {IMQq}, {IMSkype}
+//   value => the value of this option is an array of format strings, or
+//     an object with the following properties:
+//     - company (used for company contacts)
+//     - personal (used for user contacts)
+//     where the value of these properties is an array of format strings used
+//     for company or user contacts (you can have different values in the same
+//     column for personal and company contacts).
+//     You can use the following simple variables in the format string:
+//     - {FirstName}
+//     - {LastName}
+//     - {MiddleName}
+//     - {NickName}
+//     - {Prefix}
+//     - {Suffix}
+//     - {BirthDay}
+//     - {PhoneticLastName}
+//     - {PhoneticFirstName}
+//     - {JobTitle}
+//     - {Company}
+//     - {Department}
+//     - {Categories}
+//     - {NoteText}
+//     You can also use parametrized variables, where the parameter is enclosed
+//     in square bracket. Paramatrized variables are useful to extract data
+//     such as home phone {Phone[type=home]}, extract the second phone number
+//     {Phone[:1]} (zero based indexing) or extract the third home phone number
+//     {Phone[type=home][:2]} from the vCard.
+//     NOTE: if the parametrized variable matches multiple items, e.g.:
+//     {Phone[type=work]} (if the contact has multiple work phones) then the
+//     first one is used!
+//
+//     The following parametrized variables are supported (note: you can use
+//     all of them also without parameters /the first one will be used/):
+//     - {Address[type=XXX]} or {Address[:NUM]} or {Address[type=XXX][:NUM]}
+//       where supported values for XXX are:
+//       - work
+//       - home
+//       - other
+//       - any other custom value
+//     - {Phone[type=XXX]} or {Phone[:NUM]} or {Phone[type=XXX][:NUM]}
+//       where supported values for XXX are:
+//       - work
+//       - home
+//       - cell
+//       - main
+//       - pager
+//       - fax
+//       - iphone
+//       - other
+//       - any other custom value
+//     - {Email[type=XXX]} or {Email[:NUM]} or {Email[type=XXX][:NUM]}
+//       where supported values for XXX are:
+//       - work
+//       - home
+//       - mobileme
+//       - other
+//       - any other custom value
+//     - {URL[type=XXX]} or {URL[:NUM]} or {URL[type=XXX][:NUM]}
+//       where supported values for XXX are:
+//       - work
+//       - home
+//       - homepage
+//       - other
+//       - any other custom value
+//     - {Dates[type=XXX]} or {Dates[:NUM]} or {Dates[type=XXX][:NUM]}
+//       where supported values for XXX are:
+//       - anniversary
+//       - other
+//       - any other custom value
+//     - {Related[type=XXX]} or {Related[:NUM]} or {Related[type=XXX][:NUM]}
+//       where supported values for XXX are:
+//       - manager
+//       - assistant
+//       - father
+//       - mother
+//       - parent
+//       - brother
+//       - sister
+//       - child
+//       - friend
+//       - spouse
+//       - partner
+//       - other
+//       - any other custom value
+//     - {Profile[type=XXX]} or {Profile[:NUM]} or {Profile[type=XXX][:NUM]}
+//       where supported values for XXX are:
+//       - twitter
+//       - facebook
+//       - flickr
+//       - linkedin
+//       - myspace
+//       - sinaweibo
+//       - any other custom value
+//     - {IM[type=XXX]} or {IM[service-type=YYY]} or {IM[:NUM]}
+//       where supported values for XXX are:
+//       - work
+//       - home
+//       - mobileme
+//       - other
+//       - any other custom value
+//       and supported values for YYY are:
+//       - aim
+//       - icq
+//       - irc
+//       - jabber
+//       - msn
+//       - yahoo
+//       - facebook
+//       - gadugadu
+//       - googletalk
+//       - qq
+//       - skype
+//       - any other custom value
+//
+//   NOTE: if you want to use the "any other custom value" option (for XXX
+//   or YYY above) you MUST double escape the following characters:
+//     =[]{}\
+//   for example:
+//   - for profile type "=XXX=" use: '{Profile[type=\\=XXX\\=]}'
+//   - for profile type "\XXX\" use: '{Profile[type=\\\\XXX\\\\]}'
+//
+//   NOTE: if you want to use curly brackets in the format string you must
+//   double escape it, e.g.: ['{Company}', '\\{{Department}\\}']
+//
+//   The format string (for the value option) is an array to allow full
+//   customization of the interface. For example if:
+//     value: ['{LastName} {MiddleName} {FirstName}']
+//   and the person has no middle name, then the result in the column
+//   will be (without quotes):
+//     "Parker  Peter" (note: two space characters)
+//   but if you use:
+//     value: ['{LastName}', ' {MiddleName}', ' {FirstName}']
+//   then the result will be (without quotes):
+//     "Parker Peter" (note: only one space character)
+//   The reason is that only those elements of the array are appended
+//   into the result where non-empty substitution was performed (so the
+//   ' {MiddleName}' element in this case is ignored, because the person
+//   in the example above has no /more precisely has empty/ middle name).
+//
+// Examples:
+// To specify two columns (named "Company" and "Department / LastName"),
+// where the first will display the company name, and the second will display
+// department for company contacts (with "Dep -" prefix), and lastname for
+// personal contacts (with "Name -" prefix) use:
+// var globalCollectionDisplay=[
+// 	{
+// 		label: 'Company',
+// 		value: ['{Company}']
+// 	},
+// 	{
+// 		label: 'Department / LastName',
+// 		value: {
+// 			company: ['Dep - {Department}'],
+// 			personal: ['Name - {LastName}']
+// 		}
+// 	}
+// ];
+// To specify 3 columns (named "Categories", "URL" and "IM"), where the first
+// will display categories, second will display the third work URL, and third
+// will display ICQ IM use:
+// var globalCollectionDisplay=[
+// 	{
+// 		label: 'Categories',
+// 		value: ['{Categories}']
+// 	},
+// 	{
+// 		label: 'URL',
+// 		value: ['{URL[type=WORK][:2]}']
+// 	},
+// 	{
+// 		label: 'IM',
+// 		value: ['{IM[service-type=ICQ]}']
+// 	}
+// ];
+//
+// Recommended settings if globalGroupContactsByCompanies
+// is set to false:
+// var globalCollectionDisplay=[
+// 	{
+// 		label: '{Name}',
+// 		value: ['{LastName}', ' {MiddleName}', ' {FirstName}']
+// 	},
+// 	{
+// 		label: '{Company} [{Department}]',
+// 		value: ['{Company}', ' [{Department}]']
+// 	},
+// 	{
+// 		label: '{JobTitle}',
+// 		value: ['{JobTitle}']
+// 	},
+// 	{
+// 		label: '{Email}',
+// 		value: ['{Email[:0]}']
+// 	},
+// 	{
+// 		label: '{Phone} 1',
+// 		value: ['{Phone[:0]}']
+// 	},
+// 	{
+// 		label: '{Phone} 2',
+// 		value: ['{Phone[:1]}']
+// 	},
+// 	{
+// 		label: '{NoteText}',
+// 		value: ['{NoteText}']
+// 	}
+// ];
+//
+// Recommended settings if globalGroupContactsByCompanies
+// is set to true:
+// var globalCollectionDisplay=[
+// 	{
+// 		label: '{Name}',
+// 		value: {
+// 			personal: ['{LastName}', ' {MiddleName}', ' {FirstName}'],
+// 			company: ['{Company}', ' [{Department}]']
+// 		}
+// 	},
+// 	{
+// 		label: '{JobTitle}',
+// 		value: ['{JobTitle}']
+// 	},
+// 	{
+// 		label: '{Email}',
+// 		value: ['{Email[:0]}']
+// 	},
+// 	{
+// 		label: '{Phone} 1',
+// 		value: ['{Phone[:0]}']
+// 	},
+// 	{
+// 		label: '{Phone} 2',
+// 		value: ['{Phone[:1]}']
+// 	},
+// 	{
+// 		label: '{NoteText}',
+// 		value: ['{NoteText}']
+// 	}
+// ];
+//
+// NOTE: if left undefined, the recommended settings will be used.
+
+
+// globalCollectionSort
+// This options sets the ordering of contacts in the interface. In general
+// contacts are ordered alphabetically by an internal "sort string" which
+// is created for each contact. Here you can specify how this internal string
+// is created. The value is a simple array holding only the values from the
+// value property defined in the globalCollectionDisplay option.
+// If undefined, the definition from globalCollectionDisplay is used.
+// Example:
+// var globalCollectionSort = [
+// 	['{LastName}'],
+// 	['{FirstName}'],
+// 	['{MiddleName}'],
+// 	{
+// 		company: ['{Categories}'],
+// 		personal: ['{Company}']
+// 	}
+// ];
+
+
+// globalContactDataMinVisiblePercentage
+// This option defines how the width for columns are computed. If you set
+// it to 1 then 100% of all data in the column will be visible (the column
+// width is determined by the longest string in the column). If you set it
+// to 0.95 then 95% of data will fit into the column width, and the remaining
+// 5% will be truncated (" ...").
+// Example:
+var globalContactDataMinVisiblePercentage=0.95;
+
+
diff --git a/docker/nginx.conf b/docker/nginx.conf
@@ -0,0 +1,46 @@
+pid /app/tmp/pid;
+worker_processes auto;
+
+error_log /app/logs/error.log;
+
+events {
+    worker_connections  1024;
+}
+
+http {
+	include       conf/mime.types;
+	default_type  application/octet-stream;
+
+	client_body_temp_path /app/tmp/client_body_temp;
+	fastcgi_temp_path     /app/tmp/fastcgi_temp;
+	proxy_temp_path       /app/tmp/proxy_temp;
+
+	error_log  /app/logs/error.log;
+	access_log /app/logs/access.log;
+
+	sendfile           on;
+	keepalive_timeout  65;
+
+	server {
+		listen       8080;
+		server_name  default;
+
+		port_in_redirect off;
+
+		root /app/www;
+		index index.html index.php;
+
+		try_files $uri $uri/ index.php?$query_string;
+
+		location /dav {
+			try_files $uri $uri/ dav.php;
+		}
+
+		location ~* \.php$ {
+			fastcgi_pass	127.0.0.1:9000;
+			include         conf/fastcgi_params;
+			fastcgi_param   SCRIPT_FILENAME    $document_root/$fastcgi_script_name;
+			fastcgi_param   SCRIPT_NAME        $fastcgi_script_name;
+		}
+	}
+}
diff --git a/docker/php-settings.ini b/docker/php-settings.ini
@@ -0,0 +1,5 @@
+file_uploads = On
+memory_limit = 500M
+upload_max_filesize = 1G
+post_max_size = 512M
+max_execution_time = 600+
\ No newline at end of file
diff --git a/public/composer.json b/public/composer.json
@@ -0,0 +1,7 @@
+{
+    "require": {
+        "php"             : ">=7.0",
+        "sabre/dav"       : "~4.0",
+        "ralouphie/mimey" : "~2.0"
+     }
+}
diff --git a/public/composer.lock b/public/composer.lock
@@ -0,0 +1,510 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "75864b2454e8320f903e52d25a3ca2e5",
+    "packages": [
+        {
+            "name": "psr/log",
+            "version": "1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801",
+                "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "time": "2019-11-01T11:05:21+00:00"
+        },
+        {
+            "name": "ralouphie/mimey",
+            "version": "2.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ralouphie/mimey.git",
+                "reference": "8f74e6da73f9df7bd965e4e123f3d8fb9acb89ba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ralouphie/mimey/zipball/8f74e6da73f9df7bd965e4e123f3d8fb9acb89ba",
+                "reference": "8f74e6da73f9df7bd965e4e123f3d8fb9acb89ba",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.4|^7.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^1.1",
+                "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Mimey\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ralph Khattar",
+                    "email": "ralph.khattar@gmail.com"
+                }
+            ],
+            "description": "PHP package for converting file extensions to MIME types and vice versa.",
+            "time": "2019-03-08T08:49:03+00:00"
+        },
+        {
+            "name": "sabre/dav",
+            "version": "4.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sabre-io/dav.git",
+                "reference": "fd0234d46c045fc9b35ec06bd2e7b490240e6ade"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sabre-io/dav/zipball/fd0234d46c045fc9b35ec06bd2e7b490240e6ade",
+                "reference": "fd0234d46c045fc9b35ec06bd2e7b490240e6ade",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-date": "*",
+                "ext-dom": "*",
+                "ext-iconv": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "ext-pcre": "*",
+                "ext-simplexml": "*",
+                "ext-spl": "*",
+                "lib-libxml": ">=2.7.0",
+                "php": ">=7.0.0",
+                "psr/log": "^1.0",
+                "sabre/event": "^5.0",
+                "sabre/http": "^5.0",
+                "sabre/uri": "^2.0",
+                "sabre/vobject": "^4.2.0-alpha1",
+                "sabre/xml": "^2.0.1"
+            },
+            "require-dev": {
+                "evert/phpdoc-md": "~0.1.0",
+                "monolog/monolog": "^1.18",
+                "phpunit/phpunit": "^6"
+            },
+            "suggest": {
+                "ext-curl": "*",
+                "ext-imap": "*",
+                "ext-pdo": "*"
+            },
+            "bin": [
+                "bin/sabredav",
+                "bin/naturalselection"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Sabre\\DAV\\": "lib/DAV/",
+                    "Sabre\\DAVACL\\": "lib/DAVACL/",
+                    "Sabre\\CalDAV\\": "lib/CalDAV/",
+                    "Sabre\\CardDAV\\": "lib/CardDAV/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Evert Pot",
+                    "email": "me@evertpot.com",
+                    "homepage": "http://evertpot.com/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "WebDAV Framework for PHP",
+            "homepage": "http://sabre.io/",
+            "keywords": [
+                "CalDAV",
+                "CardDAV",
+                "WebDAV",
+                "framework",
+                "iCalendar"
+            ],
+            "time": "2019-10-19T07:17:49+00:00"
+        },
+        {
+            "name": "sabre/event",
+            "version": "5.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sabre-io/event.git",
+                "reference": "f5cf802d240df1257866d8813282b98aee3bc548"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sabre-io/event/zipball/f5cf802d240df1257866d8813282b98aee3bc548",
+                "reference": "f5cf802d240df1257866d8813282b98aee3bc548",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">=6",
+                "sabre/cs": "~1.0.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Sabre\\Event\\": "lib/"
+                },
+                "files": [
+                    "lib/coroutine.php",
+                    "lib/Loop/functions.php",
+                    "lib/Promise/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Evert Pot",
+                    "role": "Developer",
+                    "email": "me@evertpot.com",
+                    "homepage": "http://evertpot.com/"
+                }
+            ],
+            "description": "sabre/event is a library for lightweight event-based programming",
+            "homepage": "http://sabre.io/event/",
+            "keywords": [
+                "EventEmitter",
+                "async",
+                "coroutine",
+                "eventloop",
+                "events",
+                "hooks",
+                "plugin",
+                "promise",
+                "reactor",
+                "signal"
+            ],
+            "time": "2018-03-05T13:55:47+00:00"
+        },
+        {
+            "name": "sabre/http",
+            "version": "5.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sabre-io/http.git",
+                "reference": "f91c7d4437dcbc6f89c8b64e855e1544f4b60250"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sabre-io/http/zipball/f91c7d4437dcbc6f89c8b64e855e1544f4b60250",
+                "reference": "f91c7d4437dcbc6f89c8b64e855e1544f4b60250",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-mbstring": "*",
+                "php": ">=7.0",
+                "sabre/event": ">=4.0 <6.0",
+                "sabre/uri": "^2.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": ">=6.0.0",
+                "sabre/cs": "~1.0.0"
+            },
+            "suggest": {
+                "ext-curl": " to make http requests with the Client class"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "lib/functions.php"
+                ],
+                "psr-4": {
+                    "Sabre\\HTTP\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Evert Pot",
+                    "email": "me@evertpot.com",
+                    "homepage": "http://evertpot.com/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
+            "homepage": "https://github.com/fruux/sabre-http",
+            "keywords": [
+                "http"
+            ],
+            "time": "2018-06-04T21:27:19+00:00"
+        },
+        {
+            "name": "sabre/uri",
+            "version": "2.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sabre-io/uri.git",
+                "reference": "18f454324f371cbcabdad3d0d3755b4b0182095d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sabre-io/uri/zipball/18f454324f371cbcabdad3d0d3755b4b0182095d",
+                "reference": "18f454324f371cbcabdad3d0d3755b4b0182095d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "lib/functions.php"
+                ],
+                "psr-4": {
+                    "Sabre\\Uri\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Evert Pot",
+                    "email": "me@evertpot.com",
+                    "homepage": "http://evertpot.com/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Functions for making sense out of URIs.",
+            "homepage": "http://sabre.io/uri/",
+            "keywords": [
+                "rfc3986",
+                "uri",
+                "url"
+            ],
+            "time": "2019-09-09T23:00:25+00:00"
+        },
+        {
+            "name": "sabre/vobject",
+            "version": "4.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sabre-io/vobject.git",
+                "reference": "6d7476fbd227ae285029c19ad518cd451336038c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sabre-io/vobject/zipball/6d7476fbd227ae285029c19ad518cd451336038c",
+                "reference": "6d7476fbd227ae285029c19ad518cd451336038c",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": ">=5.5",
+                "sabre/xml": ">=1.5 <3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "> 4.8.35, <6.0.0"
+            },
+            "suggest": {
+                "hoa/bench": "If you would like to run the benchmark scripts"
+            },
+            "bin": [
+                "bin/vobject",
+                "bin/generate_vcards"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Sabre\\VObject\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Evert Pot",
+                    "email": "me@evertpot.com",
+                    "homepage": "http://evertpot.com/",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Dominik Tobschall",
+                    "email": "dominik@fruux.com",
+                    "homepage": "http://tobschall.de/",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Ivan Enderlin",
+                    "email": "ivan.enderlin@hoa-project.net",
+                    "homepage": "http://mnt.io/",
+                    "role": "Developer"
+                }
+            ],
+            "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+            "homepage": "http://sabre.io/vobject/",
+            "keywords": [
+                "availability",
+                "freebusy",
+                "iCalendar",
+                "ical",
+                "ics",
+                "jCal",
+                "jCard",
+                "recurrence",
+                "rfc2425",
+                "rfc2426",
+                "rfc2739",
+                "rfc4770",
+                "rfc5545",
+                "rfc5546",
+                "rfc6321",
+                "rfc6350",
+                "rfc6351",
+                "rfc6474",
+                "rfc6638",
+                "rfc6715",
+                "rfc6868",
+                "vCalendar",
+                "vCard",
+                "vcf",
+                "xCal",
+                "xCard"
+            ],
+            "time": "2019-12-18T19:29:43+00:00"
+        },
+        {
+            "name": "sabre/xml",
+            "version": "2.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sabre-io/xml.git",
+                "reference": "f08a58f57e2b0d7df769a432756aa371417ab9eb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sabre-io/xml/zipball/f08a58f57e2b0d7df769a432756aa371417ab9eb",
+                "reference": "f08a58f57e2b0d7df769a432756aa371417ab9eb",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-xmlreader": "*",
+                "ext-xmlwriter": "*",
+                "lib-libxml": ">=2.6.20",
+                "php": ">=7.0",
+                "sabre/uri": ">=1.0,<3.0.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Sabre\\Xml\\": "lib/"
+                },
+                "files": [
+                    "lib/Deserializer/functions.php",
+                    "lib/Serializer/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Evert Pot",
+                    "email": "me@evertpot.com",
+                    "homepage": "http://evertpot.com/",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Markus Staab",
+                    "email": "markus.staab@redaxo.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "sabre/xml is an XML library that you may not hate.",
+            "homepage": "https://sabre.io/xml/",
+            "keywords": [
+                "XMLReader",
+                "XMLWriter",
+                "dom",
+                "xml"
+            ],
+            "time": "2019-08-14T15:41:34+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=7.0"
+    },
+    "platform-dev": []
+}
diff --git a/public/dav.php b/public/dav.php
@@ -0,0 +1,75 @@
+<?php
+error_reporting(E_ALL);
+ini_set("log_errors", true);
+ini_set('display_errors', true);
+ini_set('error_log', './error.log'); 
+ini_set('memory_limit', '1024M');
+
+define("PATH", __DIR__.'/');
+date_default_timezone_set('Europe/Berlin');
+
+if (!file_exists('vendor/')) {
+    die('<h1>Incomplete installation</h1>Dependencies have not been installed.');
+}
+
+require 'vendor/autoload.php';
+require PATH.'lib/JSONDB.php';
+require PATH.'lib/Helpers.php';
+require PATH.'lib/UserManager.php';
+require PATH.'lib/GenericCollectionManager.php';
+require PATH.'lib/AddressbookCollectionManager.php';
+require PATH.'lib/CalendarCollectionManager.php';
+
+require PATH.'lib/Sabre/SabrePrincipalJsonBackend.php';
+require PATH.'lib/Sabre/SabreAuthenticationJsonBackend.php';
+require PATH.'lib/Sabre/SabrePropertyStorageJsonBackend.php';
+require PATH.'lib/Sabre/SabreCardDAVJsonBackend.php';
+require PATH.'lib/Sabre/SabreCalDAVJsonBackend.php';
+require PATH.'lib/Sabre/FilesPlugin.php';
+
+$db      = new JSONDB(PATH.'/../data');
+
+$userMgr = new UserManager($db);
+$cardMgr = new AddressbookCollectionManager($db);
+$calMgr  = new CalendarCollectionManager($db);
+
+$authBackend            = new SabreAuthenticationJsonBackend($userMgr);
+$propertyStorageBackend = new SabrePropertyStorageJsonBackend($db);
+$principalBackend       = new SabrePrincipalJsonBackend($userMgr);
+$cardDavBackend         = new SabreCardDAVJsonBackend($cardMgr);
+$calDavBackend          = new SabreCalDAVJsonBackend($calMgr);
+
+$server = new Sabre\DAV\Server([
+		new Sabre\DAVACL\PrincipalCollection($principalBackend),
+		//CardDAV-Sever
+		new Sabre\CardDAV\AddressBookRoot($principalBackend, $cardDavBackend),
+		//CalDAV-Server
+		new Sabre\CalDAV\CalendarRoot($principalBackend, $calDavBackend),
+		//WebDAV-Server
+		new FilesPlugin\FilesCollection($principalBackend, PATH.'data/files/'),
+	]);
+
+
+$server->setBaseUri('/dav/');
+
+$server->addPlugin(new Sabre\DAV\Auth\Plugin($authBackend, 'tinyDAV'));
+$server->addPlugin(new Sabre\DAVACL\Plugin());
+$server->addPlugin(new Sabre\DAV\PropertyStorage\Plugin($propertyStorageBackend));
+$server->addPlugin(new Sabre\DAV\Sync\Plugin());
+
+//CardDAV-Server
+$server->addPlugin(new Sabre\CardDAV\Plugin());
+$server->addPlugin(new Sabre\CardDAV\VCFExportPlugin());
+
+//CalDAV-Server
+$server->addPlugin(new Sabre\CalDAV\Plugin());
+$server->addPlugin(new Sabre\CalDAV\ICSExportPlugin());
+$server->addPlugin(new Sabre\CalDAV\Schedule\Plugin());
+
+//WebDAV-Server
+$server->addPlugin(new FilesPlugin\Plugin(PATH.'data/files/'));
+
+//Fancy Web
+$server->addPlugin(new Sabre\DAV\Browser\Plugin());
+
+$server->exec();+
\ No newline at end of file
diff --git a/public/index.php b/public/index.php
@@ -0,0 +1,333 @@
+<?php
+//For debuging. remove in production
+error_reporting(E_ALL);
+ini_set("log_errors", true);
+ini_set('display_errors', true);
+ini_set('error_log', './error.log'); 
+
+define('PATH', __DIR__);
+session_start();
+
+require PATH.'/lib/JSONDB.php';
+require PATH.'/lib/Template.php';
+require PATH.'/lib/Router.php';
+require PATH.'/lib/Helpers.php';
+require PATH.'/lib/UserManager.php';
+require PATH.'/lib/GenericCollectionManager.php';
+require PATH.'/lib/AddressbookCollectionManager.php';
+require PATH.'/lib/CalendarCollectionManager.php';
+
+$pagevars = [];
+
+$db      = new JSONDB(PATH.'/../data');
+$userMgr = new UserManager($db);
+$cardMgr = new AddressbookCollectionManager($db);
+$calMgr  = new CalendarCollectionManager($db);
+$tpl     = new Template(PATH.'/template/', [
+		'IS_ADMIN' => $userMgr->isAdmin(),
+	]);
+
+//GET
+Router::add('GET', '/', function(){
+	global $userMgr;
+	
+	if (!$userMgr->isLoggedIn()){
+		header("Location: /login");
+		exit();
+	}
+
+	header("Location: /overview");
+});
+
+Router::add('GET', '/login', function(){
+	global $tpl;
+
+	$tpl->render('login', [
+		'PAGE'		=>	'Login',
+	]);
+});
+
+Router::add('GET', '/logout', function(){
+	global $userMgr;
+	$userMgr->logout();
+	header("Location: /");
+});
+
+Router::add('GET', '/overview', function(){
+	global $tpl, $db, $userMgr, $cardMgr, $calMgr;
+	$userMgr->checkLoggedIn();
+
+	if (!$userMgr->isAdmin()) {
+		$addressbooks = $cardMgr->getCollections('principals/'.$userMgr->getLoggedInAccount()['username']);
+		$calendars    = $calMgr->getCollections('principals/'.$userMgr->getLoggedInAccount()['username']);
+
+		foreach ($addressbooks as $addressbook) {
+			$tpl->blockAssign('addressbooks', [
+					'ID'          => $addressbook['id'],
+					'DISPLAYNAME' => $addressbook['displayname'],
+					'URI'         => $addressbook['uri'],
+				]);
+		}
+
+		foreach ($calendars as $calendar) {
+			$tpl->blockAssign('calendars', [
+					'ID'          => $calendar['id'],
+					'DISPLAYNAME' => $calendar['displayname'],
+					'URI'         => $calendar['uri'],
+				]);
+		}
+
+	} else {
+		$users = $userMgr->getAll();
+
+		foreach ($users as $user) {
+			$tpl->blockAssign('users', [
+					'ID'       => $user->id,
+					'USERNAME' => $user->username,
+					'ACTIVE'   => $user->active,
+				]);
+		}
+	}
+	
+	$tpl->render('overview', [
+		'PAGE'     => 'Overview',
+		'USERNAME' => $userMgr->getLoggedInAccount()['username'],
+	]);
+});
+
+Router::add('GET', '/update', function(){
+	global $tpl, $userMgr;
+	$userMgr->checkLoggedIn();
+
+	$username = strtolower(trim(Helpers::getVar('username')));
+
+	switch(Helpers::getVar('type')) {
+		case 'user':
+		
+			if (!$userMgr->isAdmin()) {
+				header("Location: /overview");
+				exit();
+			}
+
+			if (!$userMgr->exists($username)) {
+				header("Location: /overview");
+				exit();				
+			}
+
+			$user = $userMgr->get($username);
+
+			$tpl->render('update', [
+				'PAGE'       => 'Update User',
+				'TYPE'       => 'user',
+				'USERNAME'   => $user['username'],
+				'ACTIVE'     => $user['active']
+			]);
+
+		default:
+			header("Location: /overview");
+	}
+});
+
+//POST 
+Router::add('POST', '/login', function(){
+	global $tpl, $userMgr;
+
+	try {
+		$userMgr->checkLogin(strtolower(trim(Helpers::postVar('username'))), Helpers::postVar('password'));
+		header("Location: /overview");
+	} catch ( Exception $e ) {
+		$tpl->render('login', [
+			'PAGE' => 'Login',
+			'MSG'  => $e->getMessage(),
+		]);		
+	}
+});
+
+Router::add('POST', '/create', function(){
+	global $tpl, $userMgr, $cardMgr, $calMgr;
+	$userMgr->checkLoggedIn();
+
+	$username = strtolower(trim(Helpers::postVar('username')));
+	$msg      = '';	
+
+	switch(Helpers::postVar('type')) {
+		case 'user':
+			if (!$userMgr->isAdmin()) {
+				header("Location: /overview");
+				exit();
+			}
+				
+			try {
+				$userMgr->create($username, Helpers::postVar('password'), true);
+				$cardMgr->createCollection('principals/'.$username, 'default', ['displayname' => 'Default-Addressbook']);
+				$calMgr->createCollection('principals/'.$username, 'default', ['displayname' => 'Default-Calendar']);
+
+				header("Location: /overview");
+			} catch ( Exception $e ) {
+				$tpl->render('message', [
+					'PAGE' => 'Message',
+					'MSG'  => $e->getMessage(),
+				]);		
+			}
+
+		case 'addressbook':
+		case 'calendar':
+			if ($userMgr->getLoggedInAccount()['username'] !== $username && !$userMgr->isAdmin()) {
+				header("Location: /overview");
+				exit();
+			}
+
+			$uri         = Helpers::postVar('uri');
+			$displayname = Helpers::postVar('displayname');
+
+			try {
+				if (empty($displayname)) throw new Exception('Displayname can\'t be empty.');
+				
+				if (empty($uri)) {
+					$uri = preg_replace("/[^A-Za-z0-9 ]/", '', $displayname);
+				}
+
+				if (empty($uri)) throw new Exception('URI can\'t be empty.');
+
+				if (Helpers::postVar('type') !== 'addressbook') {
+					$calMgr->createCollection('principals/'.$username, $uri, ['displayname' => $displayname]);
+				} else {
+					$cardMgr->createCollection('principals/'.$username, $uri, ['displayname' => $displayname]);
+				}
+
+				header("Location: /overview");
+			} catch ( Exception $e ) {
+				$tpl->render('message', [
+					'PAGE' => 'Message',
+					'MSG'  => $e->getMessage(),
+				]);		
+			}
+
+		default:
+			header("Location: /overview");
+			exit();
+	}
+
+	$tpl->render('message', [
+		'PAGE'     => 'Oofff',
+		'MSG'      => $msg, 
+	]);
+}); 
+
+Router::add('POST', '/update', function(){
+	global $tpl, $userMgr;
+	$userMgr->checkLoggedIn();
+
+	$username = strtolower(trim(Helpers::postVar('username')));
+	$msg      = '';
+
+	switch(Helpers::postVar('type')) {
+		case 'password':
+			$password1 = Helpers::postVar('password1');
+			$password2 = Helpers::postVar('password2');
+
+			if ($userMgr->getLoggedInAccount()['username'] !== $username && !$userMgr->isAdmin()) {
+				header("Location: /overview");
+				exit();
+			} elseif ($password1 !== $password2) {
+				$msg = 'Passwords do not match!';
+			} else {
+				try {
+					$userMgr->updatePassword($username, $password1);
+					header("Location: /overview");
+					
+				} catch (Exception $e) {
+					$msg = $e->getMessage();
+				}
+			}
+
+		case 'user':
+			$active = boolval(Helpers::postVar('active'));
+
+			if (!$userMgr->isAdmin()) {
+				header("Location: /overview");
+				exit();
+			}
+
+			try {
+				if (!$active) {
+					$userMgr->disable($username);
+				} else {
+					$userMgr->enable($username);
+				}
+
+				header("Location: /overview");
+					
+			} catch (Exception $e) {
+				$msg = $e->getMessage();
+			}
+
+		default:
+			header("Location: /overview");
+			exit();
+	}
+
+	$tpl->render('message', [
+		'PAGE'     => 'Oofff',
+		'MSG'      => $msg, 
+	]);
+});
+
+Router::add('POST', '/delete', function(){
+	global $tpl, $userMgr, $cardMgr, $calMgr;
+	$userMgr->checkLoggedIn();
+
+	$username = strtolower(trim(Helpers::postVar('username')));
+	$msg      = '';
+	
+	switch (Helpers::postVar('type')) {
+		case 'user':
+			if ($userMgr->getLoggedInAccount()['username'] !== $username && !$userMgr->isAdmin()) {
+				header("Location: /overview");
+				exit();
+			}
+
+			try {
+				$userMgr->delete($username);
+
+				$addressbooks = $cardMgr->getCollections('principals/'.$username);
+				$calendars    = $calMgr->getCollections('principals/'.$username);
+
+				foreach ($addressbooks as $addressbook) {
+					$cardMgr->deleteCollection($addressbook['id']);
+				}
+
+				foreach ($calendars as $calendar) {
+					$calMgr->deleteCollection($calendar['id']);
+				}
+
+				if ($userMgr->getLoggedInAccount()['username'] !== $username) {
+					header("Location: /overview");			
+				} else {
+					$userMgr->logout();
+					header("Location: /");
+				}					
+			} catch (Exception $e) {
+				$msg = $e->getMessage();
+			}
+
+		default:
+			header("Location: /overview");
+			exit();
+	}
+
+	$tpl->render('message', [
+		'PAGE'     => 'Oofff',
+		'MSG'      => $msg, 
+	]);
+});
+
+Router::pathNotFound(function(){
+	header("Loctaion: /");
+});
+
+Router::methodNotAllowed(function(){
+	header("Loctaion: /");
+});
+
+Router::run('/');+
\ No newline at end of file
diff --git a/public/install.php b/public/install.php
@@ -0,0 +1,24 @@
+<?php
+define('PATH', __DIR__);
+
+require(PATH.'/lib/JSONDB.php');
+
+if (php_sapi_name() !== 'cli') {
+	die("run me on a shell.");
+}
+
+if (!file_exists(PATH.'/../data/users.json')) {
+	mkdir(PATH.'/../data');
+	mkdir(PATH.'/../data/addressbooks');
+	mkdir(PATH.'/../data/calendars');
+	mkdir(PATH.'/../data/files');
+
+
+	$db = new JSONDB(PATH.'/../data');
+	$db->insert( 'users.json', [ 
+			'id'       => 1,
+			'username' => 'admin',
+			'password' => password_hash("admin", PASSWORD_DEFAULT),
+			'active'   => true,
+		]);
+}+
\ No newline at end of file
diff --git a/public/lib/AddressbookCollectionManager.php b/public/lib/AddressbookCollectionManager.php
@@ -0,0 +1,47 @@
+<?php
+
+class AddressbookCollectionManager extends GenericCollectionManager {
+	public $dataFolder         = PATH.'/../data/';
+	public $collectionsFile    = 'addressbooks/addressbooks.json';
+	public $collectionsFolder  = 'addressbooks/';
+	public $collectionType     = 'Addressbook';
+	public $datafield          = 'carddata';
+
+
+	public function newCollection ($username, $uri, array $properties) {
+		$collection = [ 
+				'id'           => $this->getHighestCollectionId()+1,
+				'principaluri' => $username,
+				'displayname'  => NULL,
+				'uri'          => $uri,
+				'description'  => NULL,
+				'synctoken'    => 1,
+			];
+
+		$collection = array_merge($collection, $properties);
+
+		return $collection;
+	}
+
+	public function newObject ($collectionId, $uri, $data, $extraData = NULL) {
+		$object = [
+				'id'            => $this->getHighestObjectId($collectionId)+1,
+				'uri'           => $uri,
+				'etag'          => md5($data),
+				'size'          => strlen($data),
+				'lastmodified'  => time(),
+			];
+
+		return $object;
+	}
+
+	public function newObjectUpdate ($collectionId, $uri, $data, $extraData = NULL) {
+		$update = [
+				'etag'          => md5($data),
+				'size'          => strlen($data),
+				'lastmodified'  => time(),
+			];
+
+		return $update;
+	}
+}
diff --git a/public/lib/CalendarCollectionManager.php b/public/lib/CalendarCollectionManager.php
@@ -0,0 +1,78 @@
+<?php
+
+class CalendarCollectionManager extends GenericCollectionManager {
+	public $dataFolder         = PATH.'/../data/';
+	public $collectionsFile    = 'calendars/calendars.json';
+	public $collectionsFolder  = 'calendars/';
+	public $collectionType     = 'Calendar';
+	public $datafield          = 'calendardata';
+
+
+	public function newCollection ($username, $uri, array $properties) {
+		$collection = [ 
+				'id'            => $this->getHighestCollectionId($username)+1,
+				'principaluri'  => $username,
+				'uri'           => $uri,
+				'displayname'   => NULL,
+				'description'   => NULL,
+				'timezone'      => NULL,
+				'calendarorder' => NULL,
+				'calendarcolor' => NULL,
+				'components'    => 'VEVENT,VTODO',
+				'synctoken'     => 1,
+			];
+
+		$collection = array_merge($collection, $properties);
+
+		return $collection;
+	}
+
+	public function newObject ($collectionId, $uri, $data, $extraData = NULL) {
+		$object = [
+				'id'             => $this->getHighestObjectId($collectionId)+1,
+				'uri'            => $uri,
+				'componenttype'  => NULL,
+				'firstoccurence' => NULL,
+				'lastoccurence'  => NULL,
+				'uid'            => NULL,
+				'etag'           => md5($data),
+				'lastmodified'   => time(),
+				'size'           => strlen($data),
+			];
+
+		if ($extraData !== NULL) {
+			$object = array_merge($object, $extraData);
+		}
+
+		return $object;
+	}
+
+	public function newObjectUpdate ($collectionId, $uri, $data, $extraData = NULL) {
+		$update = [
+				'etag'          => md5($data),
+				'size'          => strlen($data),
+				'lastmodified'  => time(),
+			];
+
+		if ($extraData !== NULL) {
+			$update = array_merge($update, $extraData);
+		}
+
+		return $update;
+	}
+
+	public function getUriByUID($username, $uid) {
+		$collections = $this->getCollections($username);
+
+		foreach ($collections as $collection) {
+			$objects = $this->getObjects($collection['id']);
+
+			foreach ($objects as $object) {
+				if ($object['uid'] == $uid) return $collection['uri'].'/'.$object['uri'];
+			}
+		}
+
+		return null;
+	}
+}
+
diff --git a/public/lib/Database.php b/public/lib/Database.php
@@ -0,0 +1,112 @@
+<?php
+
+class Database {
+	private $db;
+	public $lastQuery;
+
+	public $handleError = false;
+
+	public function __construct(array $options) {
+		try {
+			if ($options['type'] !== 'mysql') {
+				$this->db = new PDO('sqlite:'.$options['file']);
+			} else {
+				$this->db = new PDO('mysql:host='.$options['host'].';dbname='.$options['name'], $options['user'], $options['password']);
+			}
+		} catch (PDOException $e) {
+			die('Connection to database faild: '.$e->getMessage());
+		}
+
+		$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+		$this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
+	}
+
+	public function query($sql, array $args = null) {
+		try {
+			if (!is_null($args)) {
+				$this->lastQuery = $this->db->prepare($sql);
+				$this->lastQuery->execute($args);
+			} else {
+				$this->lastQuery = $this->db->query($sql);
+			}
+
+			return $this->lastQuery;
+		} catch (PDOException $e) {
+			if ($this->handleError) {
+				$this->error($e->getMessage(), $sql, $e->getCode());
+			} else {
+				throw $e;
+			}
+		}
+	}
+
+	public function prepare($sql) {
+		try {
+			return $this->db->prepare($sql);
+		} catch (PDOException $e) {
+			if ($this->handleError) {
+				$this->error($e->getMessage(), $sql, $e->getCode());
+			} else {
+				throw $e;
+			}
+		}
+	}
+
+	public function fetchObject(PDOStatement $stmt = null) {
+		if (!is_null($stmt)) {
+			return $stmt->fetch(PDO::FETCH_OBJ);
+		} else {
+			return $this->lastQuery->fetch(PDO::FETCH_OBJ);
+		}
+	}
+
+	public function fetch(PDOStatement $stmt = null, $fetch_style = FETCH_ASSOC) {
+		if (!is_null($stmt)) {
+			return $stmt->fetch(PDO::$fetch_style);
+		} else {
+			return $this->lastQuery->fetch(PDO::$fetch_style);
+		}
+	}
+
+	public function numRows(PDOStatement $stmt = null) {
+		if (!is_null($stmt)) {
+			return $stmt->rowCount();
+		} else {
+			return $this->lastQuery->rowCount();
+		}
+	}
+
+	function tableExists($table) {
+		try {
+			$result = $this->query('SHOW TABLES LIKE ?', [$table])->numRows();
+		} catch (Exception $e) {
+			return $e->getMessage();
+		}
+
+	    return $result > 0 ? true : false;
+	}
+
+	public function insertID() {
+		return $this->db->lastInsertId();
+	}
+
+	public function beginTransaction() {
+		return $this->db->beginTransaction();
+	}
+
+	public function commit() {
+		return $this->db->commit();
+	}
+
+	public function rollback() {
+		return $this->db->rollBack();
+	}
+
+	public function errorInfo() {
+		return $this->db->errorinfo();
+	}
+		
+	protected function error($msg, $sql, $code) {
+		Helpers::log(Helpers::LOG_ERROR, $msg . '<br><br><code>' . $sql . '</code>');
+	}
+}+
\ No newline at end of file
diff --git a/public/lib/GenericCollectionManager.php b/public/lib/GenericCollectionManager.php
@@ -0,0 +1,237 @@
+<?php
+
+class GenericCollectionManager {
+	private $db;
+
+//	public $dataFolder         = PATH.'/data/';
+//	public $collectionsFile    = 'addressbooks/addressbooks.json';
+//	public $collectionsFolder  = 'addressbooks/';
+//	public $collectionType     = 'Addressbook';
+//	public $datafield          = 'carddata';
+
+
+//	public function newCollection ($username, $uri, array $properties) {}
+//	public function newObject ($collectionId, $uri, $data, $extraData) {}
+//	public function newObjectUpdate ($collectionId, $uri, $data, $extraData) {}
+
+	public function __construct (JSONDB $database) {
+		$this->db = $database;
+	}
+
+	//
+	//  Collections
+	//
+
+	public function collectionExists ($collectionId) {
+		return (!$this->getCollection($collectionId)) ? false : true;
+	}
+
+	public function getCollection ($collectionId) {
+		$result = $this->db->select('*')
+			->from($this->collectionsFile)
+			->where(['id' => $collectionId])
+			->get();
+
+		if (!isset($result[0])) return false;
+
+		return $result[0];
+	}
+
+	public function getCollections ($username) {
+		$result = $this->db->select('*')
+			->from($this->collectionsFile)
+			->where(['principaluri' => $username])
+			->get();
+
+		return $result;
+	}
+
+	public function getHighestCollectionId () {
+		$data = $this->db->select('id')
+			->from($this->collectionsFile)
+			->order_by('id', JSONDB::ASC)
+			->get();
+		
+		return end($data)['id'];
+	}
+
+
+	public function createCollection ($username, $uri, array $properties) {
+		if(!preg_match('/^[\w-]+$/', $uri)) throw new Exception('URI contains not allowed characters.');
+
+		$collections = $this->getCollections($username);
+		foreach ($collections as $collection) {
+			if ($collection['uri'] == $uri) throw new Exception('An '.strtolower($this->collectionType).' with this URI already exist.');
+		}
+
+		$collection = $this->newCollection($username, $uri, $properties);
+		$this->db->insert($this->collectionsFile, $collection);
+		mkdir($this->dataFolder.$this->collectionsFolder.$collection['id']);
+		mkdir($this->dataFolder.$this->collectionsFolder.$collection['id'].'/data');
+
+		return $collection['id'];
+	}
+
+	public function updateCollection ($collectionId, $properties) {
+		if (!$this->exists($id)) throw new Exception($this->collectionType.' doesn\'t exist!');
+
+		$this->db->update($properties)
+			->from($this->collectionsFile)
+			->where(['id' => $collectionId])
+			->trigger();
+
+		$this->addChange($collectionId, '', 2);
+	}
+
+	public function deleteCollection ($collectionId) {
+		$this->db->delete()
+			->from($this->collectionsFile)
+			->where(['id' => $collectionId])
+			->trigger();
+
+		Helpers::delete($this->dataFolder.$this->collectionsFolder.$id);
+	}
+
+
+	//
+	// Objects
+	//
+
+	public function getHighestObjectId ($collectionId) {
+		$data = $this->db->select('id')
+			->from($this->collectionsFolder.$collectionId.'/objects.json')
+			->order_by('id', JSONDB::ASC)
+			->get();
+		
+		return end($data)['id'];
+	}
+
+	public function getObject ($collectionId, $uri) {
+		$objects = $result = $this->db->select('*')
+			->from($this->collectionsFolder.$collectionId.'/objects.json')
+			->where(['uri' => $uri])
+			->get();
+
+		if (!isset($objects[0])) return false;
+
+		$result = $objects[0];
+		$result[$this->datafield] = file_get_contents($this->dataFolder.$this->collectionsFolder.$collectionId.'/data/'.$objects[0]['uri']);
+
+		return $result;
+	}
+
+	public function getObjects ($collectionId) {
+		$objects = $this->db->select('*')
+			->from($this->collectionsFolder.$collectionId.'/objects.json')
+			->get();
+
+		$results = [];
+		foreach ($objects as $object) {
+			$results[] = get_object_vars($object);
+		}
+
+		return $results;
+	}
+
+	public function createObject ($collectionId, $uri, $data, $extraData = NULL) {
+		$object = $this->newObject($collectionId, $uri, $data, $extraData);
+		
+		$this->db->insert($this->collectionsFolder.$collectionId.'/objects.json', $object);
+		file_put_contents($this->dataFolder.$this->collectionsFolder.$collectionId.'/data/'.$uri, $data);
+        $this->addChange($collectionId, $uri, 1);
+
+		return $object['etag'];
+	}
+
+	public function updateObject ($collectionId, $uri, $data, $extraData = NULL) {
+		$update = $this->newObjectUpdate($collectionId, $uri, $data, $extraData);
+
+		$this->db->update($update)
+			->from($this->collectionsFolder.$collectionId.'/objects.json')
+			->where(['uri' => $uri])
+			->trigger();
+
+		file_put_contents($this->dataFolder.$this->collectionsFolder.$collectionId.'/data/'.$uri, $data);
+        $this->addChange($collectionId, $uri, 2);
+
+		return $update['etag'];
+	}
+
+	public function deleteObject ($collectionId, $uri) {
+		$result = $this->db->delete()
+			->from($this->collectionsFolder.$collectionId.'/objects.json')
+			->where(['uri' => $uri])
+			->trigger();
+
+		unlink($this->dataFolder.$this->collectionsFolder.$collectionId.'/data/'.$uri);
+		$this->addChange($collectionId, $uri, 3);
+
+		return $result;
+	}
+
+	public function addChange ($collectionId, $uri, $operation) {
+		$synctoken = $this->getCollection($collectionId)['synctoken'];
+
+		$this->db->insert($this->collectionsFolder.$collectionId.'/changes.json', [
+				'synctoken'     => $synctoken,
+				'operation'     => $operation,
+				'uri'           => $uri,
+			]);
+
+		$this->db->update(['synctoken' => $synctoken+1])
+			->from($this->collectionsFile)
+			->where(['id' => $collectionId])
+			->trigger();
+	}
+
+	public function getChanges ($collectionId, $syncToken, $syncLevel) {
+		$currentToken = $this->getCollection($collectionId)['synctoken'];
+
+		if (is_null($currentToken)) {
+			return null;
+		}
+
+        	$result = [
+				'syncToken' => $currentToken,
+				'added'     => [],
+				'modified'  => [],
+				'deleted'   => [],
+			];
+		
+        if ($syncToken) {
+			$query = $this->db->select('*')
+				->from($this->collectionsFolder.$collectionId.'/changes.json')
+				->order_by('synctoken', JSONDB::ASC)
+				->get();
+
+			$changes = [];
+
+			foreach ($query as $data) {
+				if ($data['synctoken'] >= $syncToken) $changes[$data['uri']] = $data['operation'];
+				if ($data['synctoken'] < $currentToken) $changes[$data['uri']] = $data['operation'];
+			}
+
+            foreach ($changes as $uri => $operation) {
+                switch ($operation) {
+                    case 1:
+                        $result['added'][] = $uri;
+                        break;
+                    case 2:
+                        $result['modified'][] = $uri;
+                        break;
+                    case 3:
+                        $result['deleted'][] = $uri;
+                        break;
+                }
+            }
+        } else {
+		$objects = $this->getObjects($collectionId);
+
+		foreach ($objects as $object) {
+	            $result['added'][] = $object['uri'];
+	        }
+        }
+
+        return $result;
+	}
+}
diff --git a/public/lib/Helpers.php b/public/lib/Helpers.php
@@ -0,0 +1,40 @@
+<?php
+
+abstract class Helpers {
+	public static function requestVar ($name) {
+		return (isset($_REQUEST[$name])) ? trim($_REQUEST[$name]) : NULL;
+	}
+
+	public static function getVar ($name) {
+		return (isset($_GET[$name])) ? trim($_GET[$name]) : NULL;
+	}
+
+	public static function postVar ($name) {
+		return (isset($_POST[$name])) ? trim($_POST[$name]) : NULL;
+	}
+
+	public static function isDateValid ($date, $format) {
+	    $d = DateTime::createFromFormat($format, $date);
+	    return $d && $d->format($format) == $date;
+	}
+
+	public static function startsWith ($string, $startString) {
+		$len = strlen($startString);
+		return (substr($string, 0, $len) === $startString); 
+	}
+
+	public static function delete ($target) {
+		if(is_dir($target)){
+    		$files = glob( $target . '*', GLOB_MARK ); //GLOB_MARK adds a slash to directories returned
+
+			foreach( $files as $file ){
+				Helpers::delete($file);      
+        	}
+
+        	rmdir($target);
+		} elseif(is_file($target)) {
+			unlink($target);  
+		}
+	}
+}
+
diff --git a/public/lib/JSONDB.php b/public/lib/JSONDB.php
@@ -0,0 +1,446 @@
+<?php 
+declare( strict_types = 1 );
+
+class JSONDB {
+	public $file, $content = [];
+	private $where, $select, $merge, $update;
+	private $delete = false;
+	private $last_indexes = [];
+	private $order_by = [];
+	protected $dir;
+	private $json_opts = [];
+	const ASC = 1;
+	const DESC = 0;
+
+	public function __construct( $dir, $json_encode_opt = JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT ) {
+		$this->dir = $dir;
+		$this->json_opts[ 'encode' ] = $json_encode_opt;
+	}
+
+	private function check_file() {
+		/**
+		 * Checks and validates if JSON file exists
+		 *
+		 * @return bool
+		*/
+
+		// Checks if JSON file exists, if not create
+		if( !file_exists( $this->file ) ) {
+			$this->commit();
+		}
+
+		// Read content of JSON file
+		$content = file_get_contents( $this->file );
+		$content = json_decode( $content );
+
+		// Check if its arrays of jSON
+		if( !is_array( $content ) && is_object( $content ) ) {
+			throw new \Exception( 'An array of json is required: Json data enclosed with []' );
+			return false;
+		}
+		// An invalid jSON file
+		elseif( !is_array( $content ) && !is_object( $content ) ) {
+			throw new \Exception( 'json is invalid' );
+			return false;
+		}
+		else
+			return true;
+	}
+
+	public function select( $args = '*' ) {
+		/**
+		 * Explodes the selected columns into array
+		 *
+		 * @param type $args Optional. Default *
+		 * @return type object
+		*/
+
+		// Explode to array
+		$this->select = explode( ',', $args );
+		// Remove whitespaces
+		$this->select = array_map( 'trim', $this->select );
+		// Remove empty values
+		$this->select = array_filter( $this->select );
+
+		return $this;
+	}
+
+	public function from( $file ) {
+		/**
+		 * Loads the jSON file
+		 *
+		 * @param type $file. Accepts file path to jSON file
+		 * @return type object
+		*/
+
+		$this->file = sprintf( '%s/%s.json', $this->dir, str_replace( '.json', '', $file ) ); // Adding .json extension is no longer necessary
+
+		// Reset where
+		$this->where( [] );
+		$this->content = '';
+
+		// Reset order by
+		$this->order_by = [];
+
+		if( $this->check_file() ) {
+			$this->content = ( array ) json_decode( file_get_contents( $this->file ) );
+		}
+		return $this;
+	}
+
+	public function where( array $columns, $merge = 'OR' ) {
+		$this->where = $columns;
+		$this->merge = $merge;
+		return $this;
+	}
+
+	public function delete() {
+		$this->delete = true;
+		return $this;
+	}
+
+	public function update( array $columns ) {
+		$this->update = $columns;
+		return $this;
+	}
+
+	/**
+	 * Inserts data into json file
+	 * 
+	 * @param string $file json filename without extension
+	 * @param array $values Array of columns as keys and values
+	 * 
+	 * @return array $last_indexes Array of last index inserted
+	 */
+	public function insert( $file, array $values ) : array {
+		$this->from( $file );
+
+		if( !empty( $this->content[ 0 ] ) ) {
+			$nulls = array_diff_key( ( array ) $this->content[ 0 ], $values );
+			if( $nulls ) {
+				$nulls = array_map( function() {
+					return '';
+				}, $nulls );
+				$values = array_merge( $values, $nulls );
+			}
+		}
+
+		if( !empty( $this->content ) && array_diff_key( $values, (array ) $this->content[ 0 ] ) ) {
+			throw new Exception( 'Columns must match as of the first row' );
+		}
+		else {
+			$this->content[] = ( object ) $values;
+			$this->last_indexes = [ ( count( $this->content ) - 1 ) ];
+			$this->commit();
+		}
+		return $this->last_indexes;
+	}
+
+	public function commit() {
+		$f = fopen( $this->file, 'w+' );
+		fwrite( $f, ( !$this->content ? '[]' : json_encode( $this->content, $this->json_opts[ 'encode' ] ) ) );
+		fclose( $f );
+	}
+
+	private function _update() {
+		if( !empty( $this->last_indexes ) && !empty( $this->where ) ) {
+			foreach( $this->content as $i => $v ) {
+				if( in_array( $i, $this->last_indexes ) ) {
+					$content = ( array ) $this->content[ $i ];
+					if( !array_diff_key( $this->update, $content ) ) {
+						$this->content[ $i ] = ( object ) array_merge( $content, $this->update );
+					}
+					else 
+						throw new Exception( 'Update method has an off key' );
+				}
+				else 
+					continue;
+			}
+		}
+		elseif( !empty( $this->where ) && empty( $this->last_indexes ) ) {
+			null;
+		}
+		else {
+			foreach( $this->content as $i => $v ) {
+				$content = ( array ) $this->content[ $i ];
+				if( !array_diff_key( $this->update, $content ) ) 
+					$this->content[ $i ] = ( object ) array_merge( $content, $this->update );
+				else 
+					throw new Exception( 'Update method has an off key ' );
+			}
+		}
+	}
+
+	/**
+	 * Prepares data and written to file
+	 * 
+	 * @return object $this 
+	 */
+	public function trigger() {
+		$content = ( !empty( $this->where ) ? $this->where_result() : $this->content );
+		$return = false;
+		if( $this->delete ) {
+			if( !empty( $this->last_indexes ) && !empty( $this->where ) ) {
+				$this->content = array_filter($this->content, function( $index ) {
+					return !in_array( $index, $this->last_indexes );
+				}, ARRAY_FILTER_USE_KEY );
+	
+				$this->content = array_values( $this->content );
+			}
+			elseif( empty( $this->where ) && empty( $this->last_indexes ) ) {
+				$this->content = array();
+			}
+			
+			$return = true;
+			$this->delete = false;
+		}
+		elseif( !empty( $this->update ) ) {
+			$this->_update();
+			$this->update = [];
+		}
+		else 
+			$return = false;
+		$this->commit();
+		return $this;
+	}
+
+	/**
+	 * Flushes indexes they won't be reused on next action
+	 * 
+	 * @return object $this 
+	 */
+	private function flush_indexes( $flush_where = false ) {
+		$this->last_indexes = array();
+		if( $flush_where )
+			$this->where = array();
+	}
+
+	/**
+	 * Validates and fetch out the data for manipulation
+	 * 
+	 * @return array $r Array of rows matching WHERE
+	 */
+	private function where_result() {
+		$this->flush_indexes();
+
+		if( $this->merge == 'AND' ) {
+			return $this->where_and_result();
+		}
+		else {
+			$r = [];
+
+			// Loop through the existing values. Ge the index and row
+			foreach( $this->content as $index => $row ) {
+
+				// Make sure its array data type
+				$row = ( array ) $row;
+
+				// Loop again through each row,  get columns and values
+				foreach( $row as $column => $value ) {
+					// If each of the column is provided in the where statement
+					if( in_array( $column, array_keys( $this->where ) ) ) {
+						// To be sure the where column value and existing row column value matches
+						if( $this->where[ $column ] == $row[ $column ] ) {
+							// Append all to be modified row into a array variable
+							$r[] = $row;
+
+							// Append also each row array key
+							$this->last_indexes[] = $index;
+						}
+						else 
+							continue;
+					}
+				}
+			}
+			return $r;
+		}
+	}
+
+	/**
+	 * Validates and fetch out the data for manipulation for AND
+	 * 
+	 * @return array $r Array of fetched WHERE statement
+	 */
+	private function where_and_result() {
+		/*
+			Validates the where statement values
+		*/
+		$r = [];
+
+		// Loop through the db rows. Ge the index and row
+		foreach( $this->content as $index => $row ) {
+
+			// Make sure its array data type
+			$row = ( array ) $row;
+
+			
+			//check if the row = where['col'=>'val', 'col2'=>'val2']
+			if(!array_diff($this->where,$row)) {
+				$r[] = $row;
+				// Append also each row array key
+				$this->last_indexes[] = $index;			
+				
+			}
+			else continue ;
+			
+
+		}
+		return $r;
+	}	
+
+	public function to_xml( $from, $to ) {
+		$this->from( $from );
+		if( $this->content ) {
+			$element = pathinfo( $from, PATHINFO_FILENAME );
+			$xml = '
+			<?xml version="1.0"?>
+				<' . $element . '>
+';
+			
+			foreach( $this->content as $index => $value ) {
+				$xml .= '
+				<DATA>';
+				foreach( $value as $col => $val ) {
+					$xml .= sprintf( '
+					<%s>%s</%s>', $col, $val, $col );
+				}
+				$xml .= '
+				</DATA>
+				';
+			}
+			$xml .= '</' . $element . '>';
+
+			$xml = trim( $xml );
+			file_put_contents( $to, $xml );
+			return true;
+		}
+		return false;
+	}
+	
+	public function to_mysql( $from, $to, $create_table = true ) {
+		$this->from( $from );
+		if( $this->content ) {
+			$table = pathinfo( $to, PATHINFO_FILENAME );
+
+			$sql = "-- PHP-JSONDB JSON to MySQL Dump
+--\r\n\r\n";
+			if( $create_table ) {
+				$sql .= "
+-- Table Structure for `" . $table . "`
+--
+
+CREATE TABLE `" . $table . "`
+	(
+					";
+				$first_row = ( array ) $this->content[ 0 ];
+				foreach( array_keys( $first_row ) as $column ) {
+					$s = '`' . $column . '` ' . $this->_to_mysql_type( gettype( $first_row[ $column ] ) ) ;
+					$s .= ( next( $first_row ) ? ',' : '' );
+					$sql .= $s;
+				}
+				$sql .= "
+	);\r\n";
+			}
+
+			foreach( $this->content as $values ) {
+				$values = ( array ) $values;
+				$v = array_map( function( $vv ) {
+					$vv = ( is_array( $vv ) || is_object( $vv ) ? serialize( $vv ) : $vv );
+					return "'" . addslashes( $vv ) . "'";
+				}, array_values( $values ) );
+
+				$c = array_map( function( $vv ) {
+					return "`" . $vv . "`";
+				}, array_keys( $values ) );
+				$sql .= sprintf( "INSERT INTO `%s` ( %s ) VALUES ( %s );\n", $table, implode( ', ', $c ), implode( ', ', $v ) );
+			}
+			file_put_contents( $to, $sql );
+			return true;
+		}
+		else 
+			return false;
+	}
+
+	private function _to_mysql_type( $type ) {
+		if( $type == 'bool' ) 
+			$return = 'BOOLEAN';
+		elseif( $type == 'integer' ) 
+			$return = 'INT';
+		elseif( $type == 'double' ) 
+			$return = strtoupper( $type );
+		else
+			$return = 'VARCHAR( 255 )';
+		return $return;
+	}
+
+	public function order_by( $column, $order = self::ASC ) {
+		$this->order_by = [ $column, $order ];
+		return $this;
+	}
+
+	private function _process_order_by( $content ) {
+		if( $this->order_by && $content && in_array( $this->order_by[ 0 ], array_keys( ( array ) $content[ 0 ] ) ) ) {
+			/*
+				* Check if order by was specified
+				* Check if there's actually a result of the query
+				* Makes sure the column  actually exists in the list of columns
+			*/
+
+			list( $sort_column, $order_by ) = $this->order_by;
+			$sort_keys = [];
+			$sorted = [];
+
+			foreach( $content as $index => $value ) {
+				$value = ( array ) $value;
+				// Save the index and value so we can use them to sort
+				$sort_keys[ $index ] = $value[ $sort_column ];
+			}
+			
+			// Let's sort!
+			if( $order_by == self::ASC ) {
+				asort( $sort_keys );
+			}
+			elseif( $order_by == self::DESC ) {
+				arsort( $sort_keys );
+			}
+
+			// We are done with sorting, lets use the sorted array indexes to pull back the original content and return new content
+			foreach( $sort_keys as $index => $value ) {
+				$sorted[ $index ] = ( array ) $content[ $index ];
+			}
+
+			$content = $sorted;
+		}
+
+		return $content;
+	}
+
+	public function get() {
+		if($this->where != null) {
+			$content = $this->where_result();
+		}
+		else 
+			$content = $this->content; 
+		
+		if( $this->select && !in_array( '*', $this->select ) ) {
+			$r = [];
+			foreach( $content as $id => $row ) {
+				$row = ( array ) $row;
+				foreach( $row as $key => $val ) {
+					if( in_array( $key, $this->select ) ) {
+						$r[ $id ][ $key ] = $val;
+					} 
+					else 
+						continue;
+				}
+			}
+			$content = $r;
+		}
+
+		// Finally, lets do sorting :)
+		$content = $this->_process_order_by( $content );
+		
+		$this->flush_indexes( true );
+		return $content;
+	}
+} 
diff --git a/public/lib/Router.php b/public/lib/Router.php
@@ -0,0 +1,104 @@
+<?php
+
+class Router {
+
+  private static $routes = [];
+  private static $pathNotFound = null;
+  private static $methodNotAllowed = null;
+
+  public static function add($method, $expression, $function){
+    array_push(self::$routes, [
+      'expression' => $expression,
+      'function' => $function,
+      'method' => $method
+    ]);
+  }
+
+  public static function pathNotFound($function){
+    self::$pathNotFound = $function;
+  }
+
+  public static function methodNotAllowed($function){
+    self::$methodNotAllowed = $function;
+  }
+
+  public static function run($basepath = '/'){
+
+    // Parse current url
+    $parsed_url = parse_url($_SERVER['REQUEST_URI']);//Parse Uri
+
+    if(isset($parsed_url['path'])){
+      $path = $parsed_url['path'];
+    }else{
+      $path = '/';
+    }
+
+    // Get current request method
+    $method = $_SERVER['REQUEST_METHOD'];
+
+    $path_match_found = false;
+
+    $route_match_found = false;
+
+    foreach(self::$routes as $route){
+
+      // If the method matches check the path
+
+      // Add basepath to matching string
+      if($basepath!=''&&$basepath!='/'){
+        $route['expression'] = '('.$basepath.')'.$route['expression'];
+      }
+
+      // Add 'find string start' automatically
+      $route['expression'] = '^'.$route['expression'];
+
+      // Add 'find string end' automatically
+      $route['expression'] = $route['expression'].'$';
+
+      // echo $route['expression'].'<br/>';
+
+      // Check path match	
+      if(preg_match('#'.$route['expression'].'#',$path,$matches)){
+
+        $path_match_found = true;
+
+        // Check method match
+        if(strtolower($method) == strtolower($route['method'])){
+
+          array_shift($matches);// Always remove first element. This contains the whole string
+
+          if($basepath!=''&&$basepath!='/'){
+            array_shift($matches);// Remove basepath
+          }
+
+          call_user_func_array($route['function'], $matches);
+
+          $route_match_found = true;
+
+          // Do not check other routes
+          break;
+        }
+      }
+    }
+
+    // No matching route was found
+    if(!$route_match_found){
+
+      // But a matching path exists
+      if($path_match_found){
+        header("HTTP/1.0 405 Method Not Allowed");
+        if(self::$methodNotAllowed){
+          call_user_func_array(self::$methodNotAllowed, Array($path,$method));
+        }
+      }else{
+        header("HTTP/1.0 404 Not Found");
+        if(self::$pathNotFound){
+          call_user_func_array(self::$pathNotFound, Array($path));
+        }
+      }
+
+    }
+
+  }
+
+}+
\ No newline at end of file
diff --git a/public/lib/Sabre/FilesPlugin.php b/public/lib/Sabre/FilesPlugin.php
@@ -0,0 +1,121 @@
+<?php
+namespace FilesPlugin;
+
+class Plugin extends \Sabre\DAV\ServerPlugin {
+	protected $server      = null;
+	protected $storagePath = null;
+
+
+	public function __construct($storagePath) {
+		$this->storagePath = $storagePath;
+	}
+
+
+	public function getPluginName() {
+		return 'files';
+	}
+
+	public function getPluginInfo() {
+		return [
+			'name'        => $this->getPluginName(),
+			'description' => 'Files support',
+			'link'        => ''
+		];
+	}
+
+	public function initialize(\Sabre\DAV\Server $server) {
+		$this->server = $server;
+
+		$this->server->on('propFind',    [$this, 'propFind']);
+	}
+
+	public function propFind(\Sabre\DAV\PropFind $propFind, \Sabre\DAV\INode $node) {
+		$propFind->handle(
+			'{DAV:}getcontenttype',
+			function() use ($propFind) {
+				$mimes = new \Mimey\MimeTypes;
+
+				return $mimes->getExtension(pathinfo($propFind->getPath(), PATHINFO_EXTENSION));
+			}
+		);
+	}
+}
+
+class FilesCollection extends \Sabre\DAVACL\FS\HomeCollection {
+	public $collectionName = 'files';
+
+
+	function getChildForPrincipal(array $principalInfo) {
+		$owner = $principalInfo['uri'];
+
+		$acl   = [
+			[
+				'privilege' => '{DAV:}read',
+				'principal' => $owner,
+				'protected' => true,
+			],
+			[
+				'privilege' => '{DAV:}write',
+				'principal' => $owner,
+				'protected' => true,
+			],
+		];
+
+		list(, $principalBaseName) = \Sabre\Uri\split($owner);
+
+		$path = $this->storagePath.'/'.$principalBaseName;
+
+		if (!is_dir($path)) {
+			mkdir($path);
+		}
+
+		$out = new Directory($path, $acl, $owner);
+		$out->setRelativePath($this->storagePath);
+
+		return $out;
+	}
+}
+
+class Directory extends \Sabre\DAVACL\FS\Collection {
+	protected static $relativePath = null;
+
+
+	public function getChild($name) {
+		$path = $this->path.'/'.$name;
+
+		if (!file_exists($path)) {
+			throw new \Sabre\DAV\Exception\NotFound('File does not exist!');
+		}
+
+		if ('.' === $name || '..' === $name) {
+			throw new \Sabre\DAV\Exception\Forbidden('Permission denied to . and ..');
+		}
+
+		if (is_dir($path)) {
+			return new self($path, $this->acl, $this->owner);
+		} else {
+			return new \Sabre\DAVACL\FS\File($path, $this->acl, $this->owner);
+		}
+	}
+
+	public function setRelativePath($relativePath) {
+		self::$relativePath = $relativePath;
+	}
+
+	public static function getRelativePath() {
+		return self::$relativePath;
+	}
+
+	private function getDirSize($path){
+		$totalbytes = 0;
+		$path       = realpath($path);
+
+		if($path!==false && $path!='' && file_exists($path)){
+			foreach(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)) as $object){
+				$totalbytes += $object->getSize();
+			}
+		}
+
+		return $totalbytes;
+	}
+}+
\ No newline at end of file
diff --git a/public/lib/Sabre/SabreAuthenticationJsonBackend.php b/public/lib/Sabre/SabreAuthenticationJsonBackend.php
@@ -0,0 +1,34 @@
+<?php
+class SabreAuthenticationJsonBackend extends \Sabre\DAV\Auth\Backend\AbstractBasic {
+	protected $userMgr;
+
+	function __construct(UserManager $userMgr) {
+		$this->userMgr = $userMgr;
+	}
+
+	protected function validateUserPass($username, $password) {
+		$userAccount = $this->userMgr->get($username);
+ 
+		if (!$userAccount) {
+			return false;
+		}
+
+		if (!password_verify($password, $userAccount['password'])) {
+			return false;
+		}
+
+		if (!$userAccount['active']) {
+			return false;
+		}
+
+		return true;
+	}
+
+	function challenge(\Sabre\HTTP\RequestInterface $request, \Sabre\HTTP\ResponseInterface $response) {
+		parent::challenge($request, $response);
+
+		if ('XMLHttpRequest' === $request->getHeader('X-Requested-With')) {
+			$response->removeHeader('WWW-Authenticate');
+		}
+	}
+}
diff --git a/public/lib/Sabre/SabreCalDAVJsonBackend.php b/public/lib/Sabre/SabreCalDAVJsonBackend.php
@@ -0,0 +1,367 @@
+<?php
+declare(strict_types=1);
+
+use Sabre\CalDAV;
+use Sabre\DAV;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Xml\Element\Sharee;
+use Sabre\VObject;
+
+class SabreCalDAVJsonBackend extends Sabre\CalDAV\Backend\AbstractBackend implements Sabre\CalDAV\Backend\SyncSupport {
+	const MAX_DATE = '2038-01-01';
+
+	protected $calMgr;
+
+
+	public $propertyMap = [
+			'displayname'   => '{DAV:}displayname',
+			'description'   => '{urn:ietf:params:xml:ns:caldav}calendar-description',
+			'timezone'      => '{urn:ietf:params:xml:ns:caldav}calendar-timezone',
+			'calendarorder' => '{http://apple.com/ns/ical/}calendar-order',
+			'calendarcolor' => '{http://apple.com/ns/ical/}calendar-color',
+		];
+
+	public function __construct (CalendarCollectionManager $calMgr) {
+		$this->calMgr = $calMgr;
+	}
+
+	public function getCalendarsForUser ($principalUri) {
+		$calendars = [];
+
+		foreach ($this->calMgr->getCollections($principalUri) as $row) {
+			$components = [];
+		
+			if ($row['components']) {
+				$components = explode(',', $row['components']);
+			}
+	
+			$calendar = [
+	 				   	'id'                                                             => [(int) $row['id'], (int) $row['id']],
+						'uri'                                                            => $row['uri'],
+						'principaluri'                                                   => $row['principaluri'],
+			'{'.CalDAV\Plugin::NS_CALENDARSERVER.'}getctag'                  => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'),
+			'{http://sabredav.org/ns}sync-token'                             => $row['synctoken'] ? $row['synctoken'] : '0',
+			'{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components),
+					];
+
+			foreach ($this->propertyMap as $dbName => $xmlName) {
+				$calendar[$xmlName] = $row[$dbName];
+			}
+
+			$calendars[] = $calendar;
+		}
+
+		return $calendars;
+	}
+
+	public function createCalendar ($principalUri, $calendarUri, array $properties) {
+		$sccs       = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+		$components = 'VEVENT,VTODO';
+
+		if (isset($properties[$sccs])) {
+			if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) {
+				throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet');
+			}
+			
+			$components = implode(',', $properties[$sccs]->getValue());
+		}
+
+		$values = [
+				'components' => $components,				
+			];
+
+		foreach ($this->propertyMap as $xmlName => $dbName) {
+			if (isset($properties[$xmlName])) {
+				$values[$dbName] = $properties[$xmlName];
+			}
+		}
+
+		$calendarId = $this->calMgr->createCollection($principalUri, $calendarUri, $values);
+
+		return [ $calendarId, $calendarId ];
+	}
+
+	public function updateCalendar ($calendarId, \Sabre\DAV\PropPatch $propPatch) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+		list($calendarId) = $calendarId;
+
+		$propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
+			$values = [];
+
+	   		foreach ($this->propertyMap as $xmlName => $dbName) {
+				if (isset($mutations[$xmlName])) {
+					$values[$dbName] = $mutations[$xmlName];
+		   		}
+			}
+
+			$this->calMgr->updateCollection($calendarId, $values);
+			return true;
+		});
+	}
+
+	public function deleteCalendar ($calendarId) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		$this->calMgr->deleteCollection($calendarId);
+	}
+
+	public function getCalendarObjects ($calendarId) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		$result = [];
+		foreach ($this->calMgr->getObjects($calendarId) as $row) {
+			$result[] = [
+				'id'           => $row['id'],
+				'uri'          => $row['uri'],
+				'lastmodified' => (int) $row['lastmodified'],
+				'etag'         => '"'.$row['etag'].'"',
+				'size'         => (int) $row['size'],
+				'component'    => strtolower($row['componenttype']),
+			];
+		}
+
+		return $result;
+	}
+
+	public function getCalendarObject ($calendarId, $objectUri) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+		
+		list($calendarId ) = $calendarId;
+
+		$row = $this->calMgr->getObject($calendarId, $objectUri);
+
+		if (!$row) {
+			return null;
+		}
+
+		return [
+			'id'           => $row['id'],
+			'uri'          => $row['uri'],
+			'lastmodified' => (int) $row['lastmodified'],
+			'etag'         => '"'.$row['etag'].'"',
+			'size'         => (int) $row['size'],
+			'calendardata' => $row['calendardata'],
+			'component'    => strtolower($row['componenttype']),
+		 ];
+	}
+
+	public function getMultipleCalendarObjects($calendarId, array $uris) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		$result = [];
+
+		foreach (array_chunk($uris, 900) as $chunk) {
+			$row = $this->calMgr()->getObject($calendarId, $chunk);
+
+			$result[] = [
+					'id'           => $row['id'],
+					'uri'          => $row['uri'],
+					'lastmodified' => (int) $row['lastmodified'],
+					'etag'         => '"'.$row['etag'].'"',
+					'size'         => (int) $row['size'],
+					'calendardata' => $row['calendardata'],
+					'component'    => strtolower($row['componenttype']),
+				];
+		}
+
+		return $result;
+	}
+
+	public function createCalendarObject ($calendarId, $objectUri, $calendarData) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		return '"'.$this->calMgr->createObject($calendarId, $objectUri, $calendarData, $this->getDenormalizedData($calendarData)).'"';
+	}
+
+	public function updateCalendarObject ($calendarId, $objectUri, $calendarData) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		return '"'.$this->calMgr->updateObject($calendarId, $objectUri, $calendarData, $this->getDenormalizedData($calendarData)).'"';
+	}
+
+	public function deleteCalendarObject ($calendarId, $objectUri) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		$this->calMgr->deleteObject($calendarId, $objectUri);
+	}
+
+	public function calendarQuery ($calendarId, array $filters) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		$componentType     = NULL;
+		$requirePostFilter = true;
+		$timeRange         = NULL;
+
+		// if no filters were specified, we don't need to filter after a query
+		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
+			$requirePostFilter = false;
+		}
+
+		// Figuring out if there's a component filter
+		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
+			$componentType = $filters['comp-filters'][0]['name'];
+
+			// Checking if we need post-filters
+			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
+				$requirePostFilter = false;
+			}
+			// There was a time-range filter
+			if ('VEVENT' == $componentType && isset($filters['comp-filters'][0]['time-range'])) {
+				$timeRange = $filters['comp-filters'][0]['time-range'];
+
+				// If start time OR the end time is not specified, we can do a
+				// 100% accurate mysql query.
+				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
+					$requirePostFilter = false;
+				}
+			}
+		}
+
+		$entries = $this->calMgr->getObjects($calendarId);
+		foreach ($entries as $key => $entry) {
+			if ($componentType && $componentType !== $entry['componenttype']) unset($entries[$key]); continue;
+
+			if ($timeRange) {
+				if ($timeRange['start'] && $entry['lastoccurence'] > $timeRange['start']->getTimeStamp()) unset($entries[$key]); continue;
+				if ($timeRange['end'] && $entry['firstoccurence'] < $timeRange['end']->getTimeStamp()) unset($entries[$key]); continue;
+			}
+		}
+
+		$result = [];
+		foreach ($entries as $row) {
+			if ($requirePostFilter) {
+				$row['calendarid'] = [$calendarId, $calendarId];
+
+				if (!$this->validateFilterForObject($row, $filters)) {
+					continue;
+				}
+			}
+
+			$result[] = $row['uri'];
+		}
+
+		return $result;
+	}
+
+	public function getCalendarObjectByUID ($principalUri, $uid) {
+		return $this->calMgr->getUriByUID($principalUri, $uid);
+	}
+
+	public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
+		if (!is_array($calendarId)) {
+			throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId');
+		}
+
+		list($calendarId) = $calendarId;
+
+		return $this->calMgr->getChanges($calendarid, $syncToken, $syncLevel);		
+	}
+
+
+
+	protected function getDenormalizedData ($calendarData) {
+		$vObject = VObject\Reader::read($calendarData);
+		$componentType = null;
+		$component = null;
+		$firstOccurence = null;
+		$lastOccurence = null;
+		$uid = null;
+
+		foreach ($vObject->getComponents() as $component) {
+			if ('VTIMEZONE' !== $component->name) {
+				$componentType = $component->name;
+				$uid = (string) $component->UID;
+				break;
+			}
+		}
+
+		if (!$componentType) {
+			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+		}
+
+		if ('VEVENT' === $componentType) {
+			$firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
+			// Finding the last occurence is a bit harder
+			if (!isset($component->RRULE)) {
+				if (isset($component->DTEND)) {
+					$lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
+				} elseif (isset($component->DURATION)) {
+					$endDate = clone $component->DTSTART->getDateTime();
+					$endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
+					$lastOccurence = $endDate->getTimeStamp();
+				} elseif (!$component->DTSTART->hasTime()) {
+					$endDate = clone $component->DTSTART->getDateTime();
+					$endDate = $endDate->modify('+1 day');
+					$lastOccurence = $endDate->getTimeStamp();
+				} else {
+					$lastOccurence = $firstOccurence;
+				}
+			} else {
+				$it = new VObject\Recur\EventIterator($vObject, (string) $component->UID);
+				$maxDate = new \DateTime(self::MAX_DATE);
+				if ($it->isInfinite()) {
+					$lastOccurence = $maxDate->getTimeStamp();
+				} else {
+					$end = $it->getDtEnd();
+					while ($it->valid() && $end < $maxDate) {
+						$end = $it->getDtEnd();
+						$it->next();
+					}
+					$lastOccurence = $end->getTimeStamp();
+				}
+			}
+
+			// Ensure Occurence values are positive
+			if ($firstOccurence < 0) {
+				$firstOccurence = 0;
+			}
+			if ($lastOccurence < 0) {
+				$lastOccurence = 0;
+			}
+		}
+
+		// Destroy circular references to PHP will GC the object.
+		$vObject->destroy();
+
+		return [
+			'etag'           => md5($calendarData),
+			'size'           => strlen($calendarData),
+			'componenttype'  => $componentType,
+			'firstoccurence' => $firstOccurence,
+			'lastoccurence'  => $lastOccurence,
+			'uid'            => $uid,
+		];
+	}
+}
diff --git a/public/lib/Sabre/SabreCardDAVJsonBackend.php b/public/lib/Sabre/SabreCardDAVJsonBackend.php
@@ -0,0 +1,127 @@
+<?php
+declare(strict_types=1);
+
+use Sabre\CardDAV;
+use Sabre\DAV;
+
+class SabreCardDAVJsonBackend extends Sabre\CardDAV\Backend\AbstractBackend implements Sabre\CardDAV\Backend\SyncSupport {
+    protected $cardMgr;
+
+    public function __construct(AddressbookCollectionManager $cardMgr) {
+        $this->cardMgr = $cardMgr;
+    }
+
+    public function getAddressBooksForUser ($principalUri) {
+        $addressBooks = [];
+
+        foreach ($this->cardMgr->getCollections($principalUri) as $row) {
+            $addressBooks[] = [
+                'id'                                                      => $row['id'],
+                'uri'                                                     => $row['uri'],
+                'principaluri'                                            => $row['principaluri'],
+                '{DAV:}displayname'                                       => $row['displayname'],
+                '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'],
+                '{http://calendarserver.org/ns/}getctag'                  => $row['synctoken'],
+                '{http://sabredav.org/ns}sync-token'                      => $row['synctoken'],
+            ];
+        }
+
+        return $addressBooks;
+    }
+
+    public function updateAddressBook ($addressBookId, \Sabre\DAV\PropPatch $propPatch) {
+        $supportedProperties = [
+            '{DAV:}displayname',
+            '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description',
+        ];
+
+        $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) {
+            $values = [];
+
+            foreach ($mutations as $property => $newValue) {
+                switch ($property) {
+                    case '{DAV:}displayname':
+                        $values['displayname'] = $newValue;
+                        break;
+                    case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
+                        $values['description'] = $newValue;
+                        break;
+                }
+        	}
+
+			$this->cardMgr->updateCollection($addressBookId, $values);
+            return true;
+        });
+    }
+
+    public function createAddressBook ($principalUri, $url, array $properties) {
+        $displayname = null;
+        $description = null;
+
+        foreach ($properties as $property => $newValue) {
+            switch ($property) {
+                case '{DAV:}displayname':
+                    $displayname = $newValue;
+                    break;
+                case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description':
+                    $description = $newValue;
+                    break;
+                default:
+                    throw new DAV\Exception\BadRequest('Unknown property: '.$property);
+            }
+        }
+
+        return $this->cardMgr->createCollection($principalUri, $url, $displayname, $description);
+    }
+
+    public function deleteAddressBook ($addressBookId) {
+		$this->cardMgr->deleteCollection($addressBookId);
+    }
+
+    public function getCards ($addressbookId) {
+    	$objects = $this->cardMgr->getObjects($addressbookId);
+
+		foreach ($objects as $key => $object) {
+			$objects[$key]['etag'] = '"'.$object['etag'].'"';
+		}
+    	
+        return $objects;
+    }
+
+    public function getCard ($addressBookId, $cardUri) {
+    	$object = $this->cardMgr->getObject($addressBookId, $cardUri);
+		if (isset($object['etag'])) $object['etag'] = '"'.$object['etag'].'"';
+
+    	return $object;
+    }
+
+    public function getMultipleCards ($addressBookId, array $uris) {
+        $result = [];
+
+		foreach ($uris as $uri) {
+			$result[] = $this->getCard($addressBookId, $uri);
+        }
+
+        return $result;
+    }
+
+    public function createCard ($addressBookId, $uri, $cardData) {
+		$etag = $this->cardMgr->createObject($addressBookId, $uri, $cardData);
+
+        return '"'.$etag.'"';
+    }
+
+    public function updateCard ($addressBookId, $uri, $cardData) {
+		$etag = $this->cardMgr->updateObject($addressBookId, $uri, $cardData);
+
+        return '"'.$etag.'"';
+    }
+
+    public function deleteCard ($addressBookId, $uri) {
+		return $this->cardMgr->deleteObject($addressBookId, $uri);
+    }
+
+    public function getChangesForAddressBook ($addressBookId, $syncToken, $syncLevel, $limit = null) {
+		return $this->cardMgr->getChanges($addressBookId, $syncToken, $syncLevel);
+    }
+}
diff --git a/public/lib/Sabre/SabrePrincipalJsonBackend.php b/public/lib/Sabre/SabrePrincipalJsonBackend.php
@@ -0,0 +1,201 @@
+<?php
+declare(strict_types=1);
+
+use Sabre\DAV;
+use Sabre\DAV\MkCol;
+use Sabre\Uri;
+
+class SabrePrincipalJsonBackend extends Sabre\DAVACL\PrincipalBackend\AbstractBackend {
+    protected $userMgr;
+
+    /**
+     * Sets up the backend.
+     *
+     * @param \PDO $pdo
+     */
+    public function __construct (UserManager $userMgr) {
+        $this->userMgr = $userMgr;
+    }
+
+    /**
+     * Returns a list of principals based on a prefix.
+     *
+     * This prefix will often contain something like 'principals'. You are only
+     * expected to return principals that are in this base path.
+     *
+     * You are expected to return at least a 'uri' for every user, you can
+     * return any additional properties if you wish so. Common properties are:
+     *   {DAV:}displayname
+     *   {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+     *     field that's actualy injected in a number of other properties. If
+     *     you have an email address, use this property.
+     *
+     * @param string $prefixPath
+     *
+     * @return array
+     */
+    public function getPrincipalsByPrefix ($prefixPath) {
+		if ($prefixPath !== 'principals') {
+			return [];			
+		}
+
+        $principals = [];
+
+		$users = $this->userMgr->getAll();
+
+		foreach ($users as $user) {
+			if ($user->id == 1) continue;
+
+            $principals[] = [
+                'uri'               => 'principals/'.$user->username,
+                '{DAV:}displayname' => $user->username,
+            ];
+        }
+
+        return $principals;
+    }
+
+    /**
+     * Returns a specific principal, specified by it's path.
+     * The returned structure should be the exact same as from
+     * getPrincipalsByPrefix.
+     *
+     * @param string $path
+     *
+     * @return array
+     */
+    public function getPrincipalByPath ($path) {
+		list($prefix,$username) = explode('/', $path);
+
+		if ($prefix !== 'principals') return;
+
+		$user = $this->userMgr->get($username);
+
+		if (!$user) return;		
+		
+        return [
+				'id'                => $user['id'],
+        		'uri'               => 'principals/'.$user['username'],
+        		'{DAV:}displayname' => $user['username'],
+        	];
+    }
+
+    /**
+     * Updates one ore more webdav properties on a principal.
+     *
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+     * To do the actual updates, you must tell this object which properties
+     * you're going to process with the handle() method.
+     *
+     * Calling the handle method is like telling the PropPatch object "I
+     * promise I can handle updating this property".
+     *
+     * Read the PropPatch documentation for more info and examples.
+     *
+     * @param string        $path
+     * @param DAV\PropPatch $propPatch
+     */
+    public function updatePrincipal ($path, DAV\PropPatch $propPatch) {
+		return false;
+    }
+
+    /**
+     * This method is used to search for principals matching a set of
+     * properties.
+     *
+     * This search is specifically used by RFC3744's principal-property-search
+     * REPORT.
+     *
+     * The actual search should be a unicode-non-case-sensitive search. The
+     * keys in searchProperties are the WebDAV property names, while the values
+     * are the property values to search on.
+     *
+     * By default, if multiple properties are submitted to this method, the
+     * various properties should be combined with 'AND'. If $test is set to
+     * 'anyof', it should be combined using 'OR'.
+     *
+     * This method should simply return an array with full principal uri's.
+     *
+     * If somebody attempted to search on a property the backend does not
+     * support, you should simply return 0 results.
+     *
+     * You can also just return 0 results if you choose to not support
+     * searching at all, but keep in mind that this may stop certain features
+     * from working.
+     *
+     * @param string $prefixPath
+     * @param array  $searchProperties
+     * @param string $test
+     *
+     * @return array
+     */
+    public function searchPrincipals ($prefixPath, array $searchProperties, $test = 'allof') {
+		return [];
+    }
+
+    /**
+     * Finds a principal by its URI.
+     *
+     * This method may receive any type of uri, but mailto: addresses will be
+     * the most common.
+     *
+     * Implementation of this API is optional. It is currently used by the
+     * CalDAV system to find principals based on their email addresses. If this
+     * API is not implemented, some features may not work correctly.
+     *
+     * This method must return a relative principal path, or null, if the
+     * principal was not found or you refuse to find it.
+     *
+     * @param string $uri
+     * @param string $principalPrefix
+     *
+     * @return string
+     */
+    public function findByUri ($uri, $principalPrefix) {
+		return null;
+    }
+
+    /**
+     * Returns the list of members for a group-principal.
+     *
+     * @param string $principal
+     *
+     * @return array
+     */
+    public function getGroupMemberSet ($principal) {
+		return [];
+    }
+
+    /**
+     * Returns the list of groups a principal is a member of.
+     *
+     * @param string $principal
+     *
+     * @return array
+     */
+    public function getGroupMembership ($principal) {
+		return [];
+    }
+
+    /**
+     * Updates the list of group members for a group principal.
+     *
+     * The principals should be passed as a list of uri's.
+     *
+     * @param string $principal
+     * @param array  $members
+     */
+    public function setGroupMemberSet ($principal, array $members) {}
+
+    /**
+     * Creates a new principal.
+     *
+     * This method receives a full path for the new principal. The mkCol object
+     * contains any additional webdav properties specified during the creation
+     * of the principal.
+     *
+     * @param string $path
+     * @param MkCol  $mkCol
+     */
+    public function createPrincipal ($path, MkCol $mkCol) {}
+}
diff --git a/public/lib/Sabre/SabrePropertyStorageJsonBackend.php b/public/lib/Sabre/SabrePropertyStorageJsonBackend.php
@@ -0,0 +1,194 @@
+<?php
+declare(strict_types=1);
+
+use Sabre\DAV\PropFind;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Xml\Property\Complex;
+
+class SabrePropertyStorageJsonBackend implements Sabre\DAV\PropertyStorage\Backend\BackendInterface {
+    const VT_STRING = 1;
+    const VT_XML = 2;
+    const VT_OBJECT = 3;
+
+    protected $db;
+
+    public function __construct (JSONDB $db) {
+        $this->db = $db;
+    }
+
+    /**
+     * Fetches properties for a path.
+     *
+     * This method received a PropFind object, which contains all the
+     * information about the properties that need to be fetched.
+     *
+     * Usually you would just want to call 'get404Properties' on this object,
+     * as this will give you the _exact_ list of properties that need to be
+     * fetched, and haven't yet.
+     *
+     * However, you can also support the 'allprops' property here. In that
+     * case, you should check for $propFind->isAllProps().
+     *
+     * @param string   $path
+     * @param PropFind $propFind
+     */
+    public function propFind($path, PropFind $propFind)
+    {
+        if (!$propFind->isAllProps() && 0 === count($propFind->get404Properties())) {
+            return;
+        }
+
+		$propertys = $this->db->select('*')
+					->from('propertystorage.json')
+					->where(['path' => $path])
+					->get();
+
+		foreach ($propertys as  $property) {
+            if ('resource' === gettype($property['value'])) {
+                $property['value'] = stream_get_contents($property['value']);
+            }
+
+            switch ($property['valuetype']) {
+                case null:
+                case self::VT_STRING:
+                    $propFind->set($property['name'], $property['value']);
+                    break;
+                case self::VT_XML:
+                    $propFind->set($property['name'], new Complex($property['value']));
+                    break;
+                case self::VT_OBJECT:
+                    $propFind->set($property['name'], unserialize($property['value']));
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Updates properties for a path.
+     *
+     * This method received a PropPatch object, which contains all the
+     * information about the update.
+     *
+     * Usually you would want to call 'handleRemaining' on this object, to get;
+     * a list of all properties that need to be stored.
+     *
+     * @param string    $path
+     * @param PropPatch $propPatch
+     */
+    public function propPatch ($path, PropPatch $propPatch) {
+        $propPatch->handleRemaining(function ($properties) use ($path) {
+            foreach ($properties as $name => $value) {
+                if (!is_null($value)) {
+                    if (is_scalar($value)) {
+                        $valueType = self::VT_STRING;
+                    } elseif ($value instanceof Complex) {
+                        $valueType = self::VT_XML;
+                        $value = $value->getXml();
+                    } else {
+                        $valueType = self::VT_OBJECT;
+                        $value = serialize($value);
+                    }
+
+					$result = $this->db->select('*')
+								->from('propertystorage.json')
+								->where(['path' => $path, 'name' => $name])
+								->get();
+                    if (!$result) {
+						$this->db->insert('propertystorage.json', [
+								'path'      => $path,
+								'name'      => $name,
+								'valuetype' => $valueType,
+								'value'     => $value,
+							]);
+                    } else {
+                    	$this->db->update(['valuetype' => $valueType, 'value' => $value])
+                    		->from('propertystorage.json')
+                    		->where(['path' => $path, 'name' => $name])
+                    		->trigger();
+                    }
+
+                } else {
+					$this->db->delete()
+						->from('propertystorage.json')
+						->where(['path' => $path, 'name' => $name])
+						->trigger();
+                }
+            }
+
+            return true;
+        });
+    }
+
+    /**
+     * This method is called after a node is deleted.
+     *
+     * This allows a backend to clean up all associated properties.
+     *
+     * The delete method will get called once for the deletion of an entire
+     * tree.
+     *
+     * @param string $path
+     */
+    public function delete ($path) {
+		$paths = [];
+
+		$results = $this->db->select('path')
+			->from('propertystorage.json')
+			->get();
+
+		foreach ($results as $result) {
+			if (!Helpers::startsWith($path, $result['path'])) continue;
+
+			$paths[] = $result['path'];
+		}
+
+    	foreach ($paths as $path) {
+    		$this->db->delete()
+    			->from('propertystorage.json')
+    			->where(['path' => $path])
+    			->trigger();
+    	}
+    }
+
+    /**
+     * This method is called after a successful MOVE.
+     *
+     * This should be used to migrate all properties from one path to another.
+     * Note that entire collections may be moved, so ensure that all properties
+     * for children are also moved along.
+     *
+     * @param string $source
+     * @param string $destination
+     */
+    public function move ($source, $destination) {
+		$paths = [];
+
+		$results = $this->db->select('path')
+			->from('propertystorage.json')
+			->get();
+
+		foreach ($results as $result) {
+			if (!Helpers::startsWith($source, $result['path'])) continue;
+
+			$paths[] = $result['path'];
+		}
+
+    	foreach ($paths as $path) {
+            if ($row['path'] !== $source && 0 !== strpos($row['path'], $source.'/')) {
+                continue;
+            }
+
+            $trailingPart = substr($row['path'], strlen($source) + 1);
+            $newPath = $destination;
+            if ($trailingPart) {
+                $newPath .= '/'.$trailingPart;
+            }
+            $update->execute([$newPath, $row['id']]);
+
+    		$this->db->update(['path' => $newPath])
+    			->from('propertystorage.json')
+    			->where(['path' => $path])
+    			->trigger();
+    	}
+    }
+}
diff --git a/public/lib/Template.php b/public/lib/Template.php
@@ -0,0 +1,161 @@
+<?php
+
+class Template {
+	public  $vars       = [];
+	public  $blocks     = [];
+	private $pagevars   = [];
+	private $tpl_path   = NULL;
+	private $cache_path = NULL;
+
+	public function __construct ($tpl_path, array $pagevars) {
+		if(!file_exists($tpl_path)){
+			throw new Exception('Error templates folder not found.');
+		} else {
+			$this->tpl_path   = $tpl_path;
+		}
+
+		$this->pagevars = $pagevars;
+	}
+
+	public function assign ($vars, $value = null) {
+		if (is_array($vars)) {
+			$this->vars = array_merge($this->vars, $vars);
+		} else if ($value !== null) {
+			$this->vars[$vars] = $value;
+		}
+	}
+
+	public function blockAssign ($name, $array) {
+		$this->blocks[$name][] = (array)$array;
+	}
+
+	private function compileVars ($var) {
+		$newvar = $this->compileVar($var[1]);
+		return "<?php echo isset(" . $newvar . ") ? " . $newvar . " : '{" . $var[1] . "}' ?>";
+	}
+
+	private function compileVar ($var) {
+		if (strpos($var, '.') === false) {
+			$var = '$this->vars[\'' . $var . '\']';
+		} else {
+			$vars = explode('.', $var);
+			if (!isset($this->blocks[$vars[0]]) && isset($this->vars[$var[0]]) && gettype($this->vars[$var[0]]) == 'array') {
+				$var = '$this->vars[\'' . $vars[0] . '\'][\'' . $vars[1] . '\']';
+			} else {
+				$var = preg_replace("#(.*)\.(.*)#", "\$_$1['$2']", $var);
+			}
+		}
+		return $var;
+	}
+
+	private function compileTags ($match) {
+		switch ($match[1]) {
+			case 'INCLUDE':
+				return "<?php echo \$this->compile('" . $match[2] . "'); ?>";
+				break;
+
+			case 'INCLUDEPHP':
+				return "<?php echo include(" . PATH . $match[2] . "'); ?>";
+				break;
+
+			case 'IF':
+				return $this->compileIf($match[2], false);
+				break;
+
+			case 'ELSEIF':
+				return $this->compileIf($match[2], true);
+				break;
+
+			case 'ELSE':
+				return "<?php } else { ?>";
+				break;
+
+			case 'ENDIF':
+				return "<?php } ?>";
+				break;
+
+			case 'BEGIN':
+				return "<?php if (isset(\$this->blocks['" . $match[2] . "'])) { foreach (\$this->blocks['" . $match[2] . "'] as \$_" . $match[2] . ") { ?>";
+				break;
+
+			case 'BEGINELSE':
+				return "<?php } } else { { ?>";
+				break;
+
+			case 'END':
+				return "<?php } } ?>";
+				break;
+		 }
+	}
+
+	private function compileIf ($code, $elseif) {
+		$ex = explode(' ', trim($code));
+		$code = '';
+
+		foreach ($ex as $value) {
+			$chars = strtolower($value);
+
+			switch ($chars) {
+				case 'and':
+				case '&&':
+				case 'or':
+				case '||':
+				case '==':
+				case '!=':
+				case '!==':
+				case '>':
+				case '<':
+				case '>=':
+				case '<=':
+				case '0':
+				case is_numeric($value):
+					$code .= $value;
+					break;
+
+				case 'not':
+					$code .= '!';
+					break;
+
+				default:
+					if (preg_match('/^[A-Za-z0-9_\-\.]+$/i', $value)) {
+						$var = $this->compileVar($value);
+						$code .= "(isset(" . $var . ") ? " . $var . " : '')";
+					} else {
+						$code .= '\'' . preg_replace("#(\\\\|\'|\")#", '', $value) . '\'';
+					}
+					break;
+			}
+			$code .= ' ';
+		}
+
+		return '<?php ' . (($elseif) ? '} else ' : '') . 'if (' . trim($code) . ") { ?>";
+	}
+
+	private function compile ($file) {
+		$abs_file = $this->tpl_path.'/'.$file;
+
+		$tpl = file_get_contents($abs_file);
+		$tpl = preg_replace("#<\?(.*)\?>#", '', $tpl);
+		$tpl = preg_replace_callback("#<!-- ([A-Z]+) (.*)? ?-->#U", array($this, 'compileTags'), $tpl);
+		$tpl = preg_replace_callback("#{([A-Za-z0-9_\-.]+)}#U", array($this, 'compileVars'), $tpl);
+
+		if (eval(' ?>'.$tpl.'<?php ') === false) {
+			$this->error();
+		}
+	}
+
+	public function error () {
+		exit('Fehler im Template!');
+	}
+
+	public function render ($file, $data = NULL) {
+		$this->assign($this->pagevars);
+
+		if ($data !== NULL) {
+			$this->assign($data);
+		}
+
+		$this->compile($file.'.tpl');
+		exit();
+	}
+}+
\ No newline at end of file
diff --git a/public/lib/UserManager.php b/public/lib/UserManager.php
@@ -0,0 +1,141 @@
+<?php
+
+class UserManager {
+	private $cookie_lifetime = 2678400;
+	private $db;
+
+	public  $userAccount;
+
+	public function __construct (JSONDB $database) {
+		$this->db = $database;
+
+		if (!empty($_SESSION['username'])) {
+			$this->userAccount = $this->get($_SESSION['username']);
+		}
+	}
+
+	public function isLoggedIn () {
+		return (!$this->userAccount) ? false : true;
+	}
+
+	public function checkLoggedIn () {
+		if (!$this->isLoggedIn()) {
+			header("Location: /login");
+			exit();
+		}
+	}
+
+	public function isAdmin () {
+		if (!$this->userAccount) return false;
+		return ($this->userAccount['id'] !== 1) ? false : true; 
+	}
+
+	public function getLoggedInAccount () {
+		return $this->userAccount;
+	}
+
+	public function exists ($username) {
+		return (!$this->get($username)) ? false : true;
+	}
+
+	public function checkLogin ($username, $password) {
+		$userAccount = $this->get($username);
+ 
+		if (!$userAccount || !password_verify($password, $userAccount['password'])) {
+			throw new Exception('Account unknown or password wrong.');
+		}
+
+		if (!$userAccount['active']) {
+			throw new Exception('This account is disabled.');
+		}
+
+		$_SESSION['username'] = $userAccount['username'];
+
+		$this->userAccount = $userAccount;
+
+		return true;
+	}
+
+	public function logout () {
+		$this->userAccount = null;
+		session_destroy();
+
+		return true;
+	}
+
+	public function get ($username) {
+		$result = $this->db->select('*')
+			->from('users.json')
+			->where(['username' => $username])
+			->get();
+
+		if (!isset($result[0])) return false;
+
+		return $result[0];
+	}
+
+	public function getAll () {
+		$result = $this->db->select('*')
+			->from('users.json')
+			->get();
+
+		return $result;
+	}
+
+	public function getHighestUserId () {
+		$data = $this->db->select('id')
+			->from('users.json')
+			->order_by('id', JSONDB::ASC)
+			->get();
+		
+		return end($data)['id'];
+	}
+
+	public function updatePassword ($username, $password) {
+		if (!$this->exists($username)) throw new Exception('User doesn\'t exist!');
+
+		$this->db->update(['password' => password_hash($password, PASSWORD_DEFAULT)])
+			->from('users.json')
+			->where(['username' => $username])
+			->trigger();
+	}
+
+	public function create ($username, $password, $active = true) {
+		if ($this->exists($username)) throw new Exception('This username is already taken.');
+//		if(!preg_match('/^[\w-]+$/', $username)) throw new Exception('URI contains not allowed characters.');
+
+		$this->db->insert('users.json', [ 
+				'id'       => $this->getHighestUserId()+1,
+				'username' => $username,
+				'password' => password_hash($password, PASSWORD_DEFAULT),
+				'active'   => $active,
+			]);
+	}
+
+	public function enable ($username) {
+		if (!$this->exists($username)) throw new Exception('User doesn\'t exist!');
+
+		$this->db->update(['active' => true])
+			->from('users.json')
+			->where(['username' => $username])
+			->trigger();
+	}
+
+	public function disable ($username) {
+		if (!$this->exists($username)) throw new Exception('User doesn\'t exist!');
+
+		$this->db->update(['active' => false])
+			->from('users.json')
+			->where(['username' => $username])
+			->trigger();
+	}
+	
+	public function delete ($username) {
+		if (!$this->exists($username)) throw new Exception('User doesn\'t exist!');
+
+		$this->db->delete()
+			->from('users.json')
+			->where(['username' => $username])
+			->trigger();
+	}
+}+
\ No newline at end of file
diff --git a/public/main.css b/public/main.css
@@ -0,0 +1,6 @@
+.button-small {
+	font-size: .8rem;
+	height: 2.8rem;
+	line-height: 2.8rem;
+	padding: 0 1.5rem;
+}+
\ No newline at end of file
diff --git a/public/milligram.min.css b/public/milligram.min.css
@@ -0,0 +1,11 @@
+/*!
+ * Milligram v1.3.0
+ * https://milligram.github.io
+ *
+ * Copyright (c) 2017 CJ Patoilo
+ * Licensed under the MIT license
+ */
+
+*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}
+
+/*# sourceMappingURL=milligram.min.css.map */+
\ No newline at end of file
diff --git a/public/template/footer.tpl b/public/template/footer.tpl
@@ -0,0 +1,6 @@
+					</div>
+				</div>
+			</div>
+		</main>
+	</body>
+</html>
diff --git a/public/template/header.tpl b/public/template/header.tpl
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>{PAGE} - TinyDAV</title>
+		<link rel="icon" href="images/favicon.ico">
+		<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
+		<link rel="stylesheet" href="milligram.min.css">
+		<link rel="stylesheet" href="main.css">
+	</head>
+	<body>
+		<main class="wrapper">
+			<div class="container">
+				<div class="row">
+					<div class="column column-50 column-offset-25">
diff --git a/public/template/login.tpl b/public/template/login.tpl
@@ -0,0 +1,17 @@
+<!-- INCLUDE header.tpl -->
+
+<h5 class="title">TinyDAV</h5>
+
+<form method="post">
+	<fieldset>
+		<label for="username">User:</label>
+		<input type="text" name="username" id="username">
+
+		<label for="password">Password:</label>
+		<input type="password" name="password" id="password">
+
+		<input class="button-primary" type="submit" value="Login">
+	</fieldset>
+</form>
+
+<!-- INCLUDE footer.tpl -->+
\ No newline at end of file
diff --git a/public/template/message.tpl b/public/template/message.tpl
@@ -0,0 +1,7 @@
+<!-- INCLUDE header.tpl -->
+
+<h5 class="title">TinyDAV</h5>
+
+<p>{MSG}</p>
+
+<!-- INCLUDE footer.tpl -->+
\ No newline at end of file
diff --git a/public/template/overview.tpl b/public/template/overview.tpl
@@ -0,0 +1,142 @@
+<!-- INCLUDE header.tpl -->
+
+<div class="clearfix">
+	<div class="float-left">
+		<h4 class="title">TinyDAV</h4>
+	</div>
+	<div class="float-right">
+		<a class="button button-small" href="/logout">logout</a>
+	</div>
+</div>
+
+<!-- IF not IS_ADMIN -->
+
+<h5>Calendars</h5>
+<table>
+	<thead>
+		<tr>
+			<th>Name</th>
+			<th>URI</th>
+			<th>Actions</th>
+		</tr>
+	</thead>
+
+	<tbody>
+
+		<!-- BEGIN calendars -->
+		<tr>
+			<td>{calendars.DISPLAYNAME}</td>
+			<td>https://{PAGE_URL}/dav/addressbooks/{USERNAME}/{calendars.URI}</td>
+			<td><a href="/update?type=calendar&id={calendars.ID}">edit</a> - <a href="/dav/addressbooks/{USERNAME}/{calendars.URI}?export">export</a></td>
+		</tr>
+		<!-- END calendars -->
+
+	</tbody>
+</table>
+
+<h5>Addressbooks</h5>
+<table>
+	<thead>
+		<tr>
+			<th>Name</th>
+			<th>URI</th>
+			<th>Actions</th>
+		</tr>
+	</thead>
+
+	<tbody>
+
+		<!-- BEGIN addressbooks -->
+		<tr>
+			<td>{addressbooks.DISPLAYNAME}</td>
+			<td>https://{PAGE_URL}/dav/addressbooks/{USERNAME}/{addressbooks.URI}</td>
+			<td><a href="/update?type=addressbook&id={addressbooks.ID}">edit</a> - <a href="/dav/addressbooks/{USERNAME}/{addressbooks.URI}?export">export</a></td>
+		</tr>
+		<!-- END addressbooks -->
+
+	</tbody>
+</table>
+
+<h5>New</h5>
+<form method="post" action="/create">
+	<fieldset>
+		<label for="type">Type</label>
+		<select id="type" name="type">
+			<option value="calendar">Calendar</option>
+			<option value="addressbook">Addressbook</option>
+		</select>
+
+		<label for="displayname">Displayname:</label>
+		<input type="text" name="displayname" id="displayname">
+
+		<label for="displayname">URI: (optional)</label>
+		<input type="text" name="uri" id="uri">
+
+		<input type="hidden" name="username" value="{USERNAME}">
+		<input class="button-primary" type="submit" value="create">
+	</fieldset>
+</form>
+
+<!-- ELSE --> 
+	
+<h5>Users</h5>
+<table>
+	<thead>
+		<tr>
+			<th>Username</th>
+			<th>Actions</th>
+		</tr>
+	</thead>
+	<tbody>
+
+		<!-- BEGIN users -->
+		<tr>
+			<td>{users.USERNAME}</td>
+			<!-- IF users.ID == 1 -->
+
+			<td>admin can't be edited</td>
+
+			<!-- ELSE -->
+
+			<td><a href="/update?type=user&username={users.USERNAME}">edit</a>
+
+			<!-- ENDIF -->
+		</tr>
+		<!-- END users -->
+
+	</tbody>
+</table>
+
+<h5>New user</h5>
+<form method="post" action="/create">
+	<fieldset>
+		<label for="username">Username:</label>
+		<input type="text" name="username" id="username" required>
+
+		<label for="password">Password:</label>
+		<input type="password" name="password" id="password" required>
+
+		<input type="hidden" name="type" value="user">
+		<input class="button-primary" type="submit" value="create">
+	</fieldset>
+</form>
+
+<!-- ENDIF -->
+
+<h5>Change password</h5>
+
+<form method="post" action="/update">
+	<fieldset>
+		<label for="password1">New password:</label>
+		<input type="password" name="password1" id="password1" required>
+
+		<label for="password1">Repeat new password:</label>
+		<input type="password" name="password2" id="password2" required>
+
+		<input type="hidden" name="type" value="password">
+		<input type="hidden" name="username" value="{USERNAME}">
+		<input class="button-primary" type="submit" value="update password">
+	</fieldset>
+</form>
+
+<!-- INCLUDE footer.tpl -->+
\ No newline at end of file
diff --git a/public/template/update.tpl b/public/template/update.tpl
@@ -0,0 +1,138 @@
+<!-- INCLUDE header.tpl -->
+
+<div class="clearfix">
+	<div class="float-left">
+		<h4 class="title">TinyDAV</h4>
+	</div>
+	<div class="float-right">
+		<a class="button button-small" href="/logout">logout</a>
+	</div>
+</div>
+
+<!-- IF TYPE == "user" -->
+
+
+<h4>Update user "{USERNAME}"</h4>
+
+<h5>Change password</h5>
+
+<!-- IF PASSWD_MSG -->
+<p> {PASSWD_MSG} </p>
+<!-- ENDIF -->
+
+<form method="post" action="/update">
+	<fieldset>
+		<label for="password1">New password:</label>
+		<input type="password" name="password1" id="password1" required>
+
+		<label for="password1">Repeat new password:</label>
+		<input type="password" name="password2" id="password2" required>
+
+		<input type="hidden" name="type" value="password">
+		<input type="hidden" name="username" value="{USERNAME}">
+		<input class="button-primary" type="submit" value="update password">
+	</fieldset>
+</form>
+
+<form method="post" action="/delete">
+	<fieldset>
+		<input type="hidden" name="type" value="user">
+		<input type="hidden" name="username" value="{USERNAME}">
+		<input class="button-primary" type="submit" value="delete">
+	</fieldset>
+</form>
+
+<!-- IF not ACTIVE-->
+
+<form method="post" action="/update">
+	<fieldset>
+		<input type="hidden" name="type" value="user">
+		<input type="hidden" name="username" value="{USERNAME}">
+		<input type="hidden" name="active" value="1">
+		
+		<input class="button-primary" type="submit" value="activate">
+	</fieldset>
+</form>
+
+<!-- ELSE -->
+
+<form method="post" action="/update">
+	<fieldset>
+		<input type="hidden" name="type" value="user">
+		<input type="hidden" name="username" value="{USERNAME}">
+		<input type="hidden" name="active" value="0">
+		
+		<input class="button-primary" type="submit" value="deactivate">
+	</fieldset>
+</form>
+
+<!-- ENDIF -->
+<!-- ELSEIF TYPE == "calendar" -->
+
+<h5>Calendars</h5>
+<table>
+	<thead>
+		<tr>
+			<th>Name</th>
+			<th>URI</th>
+			<th>Actions</th>
+		</tr>
+	</thead>
+
+	<tbody>
+		<tr>
+			<td>Default calendar</td>
+			<td>https://dav.ctu.cx/dav/calendars/c/default</td>
+			<td><a href="">rename</a> - <a href="">delete</a></td>
+		</tr>
+		<tr>
+			<td>foobar</td>
+			<td>https://dav.ctu.cx/dav/calendars/c/foobar</td>
+			<td><a href="">rename</a> - <a href="">delete</a></td>
+		</tr>
+	</tbody>
+</table>
+
+<h5>Addressbooks</h5>
+<table>
+	<thead>
+		<tr>
+			<th>Name</th>
+			<th>URI</th>
+			<th>Actions</th>
+		</tr>
+	</thead>
+
+	<tbody>
+		<tr>
+			<td>Default addressbook</td>
+			<td>https://dav.ctu.cx/dav/addressbooks/c/default</td>
+			<td><a href="">rename</a> - <a href="">delete</a></td>
+		</tr>
+		<tr>
+			<td>foobar</td>
+			<td>https://dav.ctu.cx/dav/addressbookss/c/foobar</td>
+			<td><a href="">rename</a> - <a href="">delete</a></td>
+		</tr>
+	</tbody>
+</table>
+
+<h5>New</h5>
+<form method="post" action="/create">
+	<fieldset>
+		<label for="type">Type</label>
+		<select id="type" name="type">
+			<option value="calendar">Calendar</option>
+			<option value="addressbook">Addressbook</option>
+		</select>
+
+
+		<label for="displayname">Displayname:</label>
+		<input type="text" name="displayname" id="displayname">
+
+		<input class="button-primary" type="submit" value="create">
+	</fieldset>
+</form>
+
+<!-- ENDIF -->
+<!-- INCLUDE footer.tpl -->+
\ No newline at end of file