commit ae4f03c1bdebdf94099ae01a4b81a2c275f539c4
Author: ctucx <c@ctu.cx>
Date: Tue, 11 Feb 2020 17:13:37 +0100
Author: ctucx <c@ctu.cx>
Date: Tue, 11 Feb 2020 17:13:37 +0100
init
34 files changed, 4875 insertions(+), 0 deletions(-)
A
|
993
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
510
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
334
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
78
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
113
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
237
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
446
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
105
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
122
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
367
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
127
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
201
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
194
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
162
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
142
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
143
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A
|
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