]> git.plutz.net Git - confetti/commitdiff
auto complete in attendant fields master
authorPaul Hänsch <paul@plutz.net>
Thu, 18 Apr 2024 18:25:04 +0000 (20:25 +0200)
committerPaul Hänsch <paul@plutz.net>
Thu, 18 Apr 2024 18:25:04 +0000 (20:25 +0200)
94 files changed:
.gitmodules [deleted file]
Makefile [new file with mode: 0644]
actions/export_vcard.sh [deleted file]
actions/generate_courselist.sh [deleted file]
actions/new_prescription.sh [deleted file]
actions/update_bookmarks.sh [deleted file]
actions/update_card.sh [deleted file]
actions/update_course.sh [deleted file]
actions/update_prescription.sh [deleted file]
actions/update_therapy.sh [deleted file]
cards/edit_card.sh [moved from actions/new_card.sh with 56% similarity]
cards/export_card.sh [moved from actions/edit_card.sh with 69% similarity]
cards/export_csv.sh [new file with mode: 0755]
cards/filter_card.sh [new file with mode: 0755]
cards/index.cgi [new file with mode: 0755]
cards/l10n.sh [new file with mode: 0755]
cards/list.sh [new file with mode: 0755]
cards/new_card.sh [new file with mode: 0755]
cards/update_card.sh [new file with mode: 0755]
cards/widgets.sh [new file with mode: 0755]
categories/edit_categories.sh [moved from actions/edit_categories.sh with 71% similarity]
categories/index.cgi [new file with mode: 0755]
categories/l10n.sh [moved from actions/export_ical.sh with 63% similarity]
categories/update_categories.sh [moved from actions/update_categories.sh with 54% similarity]
cgilite/.gitignore [new file with mode: 0644]
cgilite/cgilite.awk [new file with mode: 0644]
cgilite/cgilite.sh [new file with mode: 0755]
cgilite/common.css [new file with mode: 0644]
cgilite/db23.sh [new file with mode: 0755]
cgilite/file.sh [new file with mode: 0755]
cgilite/html-sh.sed [new file with mode: 0755]
cgilite/json.sh [new file with mode: 0755]
cgilite/logging.sh [new file with mode: 0755]
cgilite/markdown.awk [new file with mode: 0755]
cgilite/session.sh [new file with mode: 0755]
cgilite/storage.sh [new file with mode: 0755]
cgilite/users.sh [new file with mode: 0755]
courses/edit_course.sh [moved from actions/filter_card.sh with 60% similarity]
courses/export_ical.sh [moved from actions/edit_course.sh with 68% similarity]
courses/export_pdf.sh [new file with mode: 0755]
courses/index.cgi [new file with mode: 0755]
courses/l10n.sh [new file with mode: 0755]
courses/list.sh [new file with mode: 0755]
courses/new_course.sh [moved from actions/new_course.sh with 51% similarity]
courses/update_course.sh [new file with mode: 0755]
courses/widgets.sh [new file with mode: 0755]
globals.sh [deleted file]
index.cgi [new file with mode: 0755]
l10n.sh [new file with mode: 0755]
ledgers/csv_upload.awk [new file with mode: 0755]
ledgers/csv_upload.sh [new file with mode: 0755]
ledgers/iban_assign.awk [new file with mode: 0755]
ledgers/index.cgi [new file with mode: 0755]
multipart.sh [new file with mode: 0755]
pages/cards.sh [deleted file]
pages/categories.sh [deleted file]
pages/courses.sh [deleted file]
pages/email.sh [deleted file]
pages/prescriptions.sh [deleted file]
pages/therapy.sh [deleted file]
pdiread.sh [new file with mode: 0755]
session_lock.sh [new file with mode: 0644]
shcgi [deleted submodule]
static/cards.css [deleted file]
static/categories.css [deleted file]
static/common.css [deleted file]
static/courses.css [deleted file]
static/email.css [deleted file]
static/prescriptions.css [deleted file]
static/therapy.css [deleted file]
static/therapy_background.png [deleted file]
static/therapy_background.xcf [deleted file]
static/therapy_draw.js [deleted file]
style.css [new file with mode: 0644]
templates/cards.html.sh [deleted file]
templates/categories.html.sh [deleted file]
templates/course_print.sh [deleted file]
templates/courses.html.sh [deleted file]
templates/edit_card.sh [deleted file]
templates/edit_course.sh [deleted file]
templates/edit_prescription.sh [deleted file]
templates/email.html.sh [deleted file]
templates/frame.html.sh [deleted file]
templates/prescriptions.html.sh [deleted file]
templates/text_cards.sh [deleted file]
templates/text_categories.sh [deleted file]
templates/text_courses.sh [deleted file]
templates/text_frame.sh [deleted file]
templates/text_prescriptions.sh [deleted file]
templates/text_therapy.sh [deleted file]
templates/therapy.html.sh [deleted file]
templates/view_card.sh [deleted file]
templates/view_course.sh [deleted file]
templates/view_prescription.sh [deleted file]

diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644 (file)
index fb39709..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "shcgi"]
-       path = shcgi
-       url = http://git.plutz.net/git/shcgi
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..24781a9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,9 @@
+.PHONY: _subtrees
+
+_subtrees: _cgilite
+
+cgilite:
+       git subtree add --squash -P $@ https://git.plutz.net/git/$@ master
+
+_cgilite: cgilite
+       git subtree pull --squash -P $< https://git.plutz.net/git/$< master
diff --git a/actions/export_vcard.sh b/actions/export_vcard.sh
deleted file mode 100755 (executable)
index d6ea293..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014,2015 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-echo -n "Content-Type: text/vcard;charset=utf-8\n\n"
-card="${_GET[card]}"
-cat "$_DATA/vcard/$card"
diff --git a/actions/generate_courselist.sh b/actions/generate_courselist.sh
deleted file mode 100755 (executable)
index d777536..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014, 2016, 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-pdflatex="$(where pdflatex |head -n1 || echo false)"
-course="${_GET[course]}"
-fromdate="${_GET[fromdate]}"
-fromdate="$(date -d "$fromdate" +%s)" 2>/dev/null
-[ -z "$fromdate" ] && fromdate=$(date +%s)
-
-. ${_EXEC}/pages/courses.sh
-. ${_EXEC}/pages/cards.sh
-
-tex_clean() { #in dire need for improvement
-  printf %s "$*" |tr -d '{&}\\"'
-}
-
-
-list_attendee() {  #Parameter: Cardfile
-  id="$1"
-  cardfile="$_DATA/vcard/${id}"
-
-  declare -A values
-
-  if [ -r "$cardfile" ]; then
-    vcf_parse "$cardfile"
-
-    n=$(printf %s "$values[N]" \
-        | sed -rn 's:^([^;]*)(;[^;]*)(;[^;]*)?(;[^;]*)?(;[^;]*)?$:\4 \2 \3 \1 \5:gp' \
-        | sed -r 's:,: :;s:;: :g;s: +: :g;s:^ $::;'
-       )
-    fullname="${n:-${values[FN]:-${values[NICKNAME]}}}"
-
-    tel=''
-    for n in TEL TEL{0..10}; do if (echo "$values[$n]" |grep -Eq '[0-9]'); then
-      [ -n "$tel" ] && tel="$tel\\newline $(tex_clean "$values[$n]")" || tel="$(tex_clean "$values[$n]")"
-    fi; done
-
-    note=''
-    for n in NOTE NOTE{0..10}; do if [ -n "$values[$n]" ]; then
-      [ -n "$note" ] && note="$note\\newline $(tex_clean "$values[$n]")" || note="$(tex_clean "$values[$n]")"
-    fi; done
-    printf '%s & %s & %s & %s\n' \
-           "$(tex_clean $fullname)" "$(tex_clean $values[BDAY])" "$tel" "$note" \
-    | sed -r ':X;N;$!bX; s;\n;\\newline ;g'
-  fi
-}
-
-get_dates() {  #Parameter: Calendarfile
-  calendarfile="$_DATA/ical/$course"
-
-  declare -A values
-  ics_parse "$calendarfile"
-
-  dtstart="$values[DTSTART]"
-  [ -z "$dtstart" ] && dtstart=$(date +%Y%m%dT%H%M%S)
-  echo "$dtstart" |case "$dtstart" in
-    *Z)    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$:\1-\2-\3 \4\:\5\:\6 UTC:p';;
-    TZID*) sed -rn 's:^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:TZ="\1" \2-\3-\4 \5\:\6\:\7:p';;
-    *)     sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:\1-\2-\3 \4\:\5\:\6:p';;
-  esac |read dts_date
-  rrule="$values[RRULE]"
-  rr_int="$(echo $rrule |sed -rn 's:^.*INTERVAL=([0-9]+)(;.*)?$:\1:p')"
-  rr_freq="$(echo $rrule |sed -rn 's:^.*FREQ=(YEARLY|MONTHLY|WEEKLY|DAILY)(;.*)?$:\1:p')"
-  case "$rr_freq" in
-    YEARLY) rec="$rr_int year";;
-    MONTHLY) rec="$rr_int month";;
-    DAILY) rec="$rr_int day";;
-    *) rec="$rr_int week";;
-  esac
-
-  next_date="$dts_date"
-  n=10
-  while [ $n -gt 0 ]; do
-    if [ "$(date -d "$next_date" +%s)" -gt "$(date +%s)" ]; then
-      dtlist="$dtlist & $(date -d "$next_date" +"%d. %b.")"
-      n=$(($n - 1))
-    fi
-    next_date="$(date -d "$next_date + $rec" +%Y-%m-%d)"
-  done
-
-  echo "$dtlist"
-}
-
-if [ -r "${_DATA}/ical/${course}" ]; then
-  . ${_EXEC}/templates/course_print.sh >"${_DATA}/temp/courselist_${course}.tex"
-  [ -e "${_DATA}/temp/courselist_${course}.pdf" ] && rm "${_DATA}/temp/courselist_${course}.pdf"
-  "$pdflatex" -halt-on-error -output-directory "${_DATA}/temp/" "${_DATA}/temp/courselist_${course}.tex" |debug >/dev/null
-  "$pdflatex" -halt-on-error -output-directory "${_DATA}/temp/" "${_DATA}/temp/courselist_${course}.tex" |debug >/dev/null
-fi
-if [ -r "${_DATA}/temp/courselist_${course}.pdf" ]; then
-  echo 'Content-Type: application/x-pdf\n'
-  cat "${_DATA}/temp/courselist_${course}.pdf"
-fi
diff --git a/actions/new_prescription.sh b/actions/new_prescription.sh
deleted file mode 100755 (executable)
index 26173af..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-cgi_refdata
-
-client="${_POST[client]:-${_GET[client]}}"
-
-uid=$(uuidgenerator)
-prescription="${client%.vcf}.${uid}.mpx"
-
-cardfile="$_DATA/vcard/$client"
-tempfile="$_DATA/temp/$prescription"
-
-cat >"$tempfile" <<EOF
-prescription:${prescription}
-insurance:$(sed -nr 's;^X-HEALTH-INSURANCE:(.*)\;.*\;\r?$;\1;p' "$cardfile")
-bday:$(sed -nr 's;^BDAY:(.*)\r?$;\1;p' "$cardfile")
-name:$(sed -rn '/^N[\;:]/{s;^N(\;[^:]*)?:([^\;]*)(\;[^\;]*)(\;[^\;]*)?(\;[^\;]*)?(\;[^\;]*)?\r?$;\5 \3 \4 \2 \6;;s;[\;,]; ;g;s; +; ;g;s;^ | $;;g;p}' "$cardfile")\n$(sed -nr 's;^ADR:(.*)\r?$;\1;p' "$cardfile")
-date:
-EOF
-
-echo -n "Location: ?p=prescriptions&edit=$prescription\n\n"
diff --git a/actions/update_bookmarks.sh b/actions/update_bookmarks.sh
deleted file mode 100755 (executable)
index 1670840..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-bmfile="${_DATA}/mappings/bookmarks"
- url="$(validate "${_POST[bm_url]}" '/.+' '/')"
-name="$(validate "${_POST[bm_name]}" '.+' "$url")"
-
-case "${_POST[submit]}" in
-  add) printf '%s\t%s\n' "${url}" "${name}" >>"${bmfile}"
-    ;;
-  del) cp "${bmfile}" "${bmfile}.temp"
-       grep -vF "${url}        ${name}" "${bmfile}.temp" >"${bmfile}"
-       rm "${bmfile}.temp"
-    ;;
-esac
-
-redirect "${url}#CONFIGURE"
diff --git a/actions/update_card.sh b/actions/update_card.sh
deleted file mode 100755 (executable)
index a1143c3..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014, 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-cgi_refdata
-
-filter="&filter=${_REF[filter]}"
-filtertype="&filtertype=${_REF[filtertype]}"
-order="&order=${_REF[order]}"
-
-card="${_POST[card]}"
-tempfile="$_DATA/temp/$card"
-cardfile="$_DATA/vcard/$card"
-attfile="$_DATA/mappings/attendance"
-
-vcf_escape(){
-  for each in "$@"; do
-    printf %s\\n "$each" \
-    | sed -r ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;'
-  done \
-  | sed -r ':X;$!{N;bX}; s;\n;\;;g'
-}
-
-[ "${_POST[hi_select]}" = "list" ] || _POST[hi_company]="${_POST[hi_other]}"
-[ -n "${_POST[hi_company]}${_POST[hi_number]}${_POST[hi_status]}" ] \
-&& _POST[X-HEALTH-INSURANCE]="$(vcf_escape "${_POST[hi_company]}" "${_POST[hi_number]}" "${_POST[hi_status]}")"
-
-sed -r 's;$;\r;' >"$tempfile" <<EOF
-BEGIN:VCARD
-VERSION:4.0
-N:$(vcf_escape "${_POST[0N]}" "${_POST[1N]}" "${_POST[2N]}" "${_POST[3N]}" "${_POST[4N]}")
-UID:${_POST[UID]}
-$(
-  for field in $VCF_FIELDS; do for key in $field $field{0..100}; do
-    [ -z "${_POST[$key]+x}" ] && break
-    [ -z "${_POST[$key]}" ] && continue
-    case "$key" in
-      (TEL[0-9]*)
-        printf '%s;TYPE=%s:%s\r\n' "${field}" "${_POST[phonetype${key#TEL}]}" "$(vcf_escape "${_POST[$key]}")"
-        ;;
-      (X-HEALTH-INSURANCE)
-        printf '%s:%s\r\n' "${field}" "${_POST[$key]}"
-        ;;
-      (*)
-        printf '%s:%s\r\n' "${field}" "$(vcf_escape "${_POST[$key]}")"
-        ;;
-    esac
-  done; done
-  [ "${_POST[action]}" = addfield ] && printf '%s:\r\n' "${_POST[newfield]}"
-)
-END:VCARD
-EOF
-
-case "${_POST[action]}" in
-  addfield)
-    redirect "?p=cards${filter}${filtertype}${order}&edit=$card"
-    ;;
-  update)
-    attendance=()
-    for att in attendance attendance{0..100}; do
-      [ -n "${_POST[$att]}" ] && attendance+=("${_POST[$att]}")
-    done
-    sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
-      touch "$_DATA/ical/$course"
-    done
-    sed -i -r '/^(.+)\t'$card'$/d' "$attfile"
-    for each in $attendance; do
-      echo "$each\t$card"
-    done >>"$attfile"
-    sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
-      touch "$_DATA/ical/$course"
-    done
-
-    mv "$tempfile" "$cardfile"
-    redirect "?p=cards${filter}${filtertype}${order}#$card"
-    ;;
-  cancel)
-    rm "$tempfile"
-    [ -f "$cardfile" ] \
-    && redirect "?p=cards${filter}${filtertype}${order}#$card" \
-    || redirect "?p=cards${filter}${filtertype}${order}"
-    ;;
-  delete)
-    rm "$tempfile" "$cardfile"
-    redirect "?p=cards${filter}${filtertype}${order}"
-    ;;
-esac
diff --git a/actions/update_course.sh b/actions/update_course.sh
deleted file mode 100755 (executable)
index a5ba8ce..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014, 2015 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-cgi_post
-
-course="${_POST[course]}"
-tempfile="temp/$course"
-coursefile="ical/$course"
-attfile="$_DATA/mappings/attendance"
-
-# DURATION:
-uid="${_POST[UID]}"
-
-tzid=$(cat /etc/timezone)
-tstamp=$(TZ="$tzid" date +%Y%m%dT%H%M%S)
-
-dts_year="${_POST[DTSYEAR]}"
-dts_month="${_POST[DTSMONTH]}"
-dts_day="${_POST[DTSDAY]}"
-[ -n "${_POST[DTSDAY0]}" ] && dts_day="${_POST[DTSDAY0]}"
-dts_hour="${_POST[DTSHOUR]}"
-dts_minute="${_POST[DTSMINUTE]}"
-
-[ -z $dts_year ] && dts_year=$(date +%Y)
-[ -z $dts_month ] && dts_month=$(date +%m)
-[ -z $dts_day ] && dts_day=$(date +%d)
-date -d ${dts_year}-${dts_month}-${dts_day} >/dev/null 2>/dev/null || dts_day="01"
-[ -z $dts_hour ] && dts_hour=$(date +%H)
-[ -z $dts_minute ] && dts_minute=$(date +%M)
-
-dtstart="TZID=${tzid}:${dts_year}${dts_month}${dts_day}T${dts_hour}${dts_minute}00"
-
-rr_int="${_POST[RRULE_INTERVAL]}"
-rr_freq="${_POST[RRULE_FREQ]}"
-rr_limit="${_POST[RRULE_LIMIT]}"
-case "$rr_limit" in
-  ETERN)
-    rrule="FREQ=$rr_freq;INTERVAL=$rr_int"
-    ;;
-  COUNT)
-    t="${_POST[RRULE_COUNT]}"
-    rrule="FREQ=$rr_freq;INTERVAL=$rr_int;COUNT=$t"
-    ;;
-  UNTIL)
-    uy="${_POST[RRULE_UYEAR]}"
-    um="${_POST[RRULE_UMONTH]}"
-    ud="${_POST[RRULE_UDAY]}"
-    rrule="FREQ=$rr_freq;INTERVAL=$rr_int;UNTIL=${uy}${um}${ud}T000000Z"
-    ;;
-esac
-
-echo "BEGIN:VCALENDAR\r" >"$tempfile"
-echo "VERSION:2.0\r" >>"$tempfile"
-echo "PRODID:Berlin RAW Confetti\r" >>"$tempfile"
-echo "BEGIN:VEVENT\r" >>"$tempfile"
-echo "UID:$uid\r" >>"$tempfile"
-echo "DTSTAMP:TZID=${tzid}:${tstamp}\r" >>"$tempfile"
-echo "DTSTART:${dtstart}\r" >>"$tempfile"
-echo "RRULE:${rrule}\r" >>"$tempfile"
-for field in SUMMARY COMMENT; do
-  value="${_POST[$field]}"
-  n=0
-  while [ -n "$value" ]; do
-    value="$(echo "$value" |sed -r ':a;N;$!ba;s:\n:\\\\n:g;s:\r:\\\\r:g')"
-    echo "${field}:${value}\r"
-    value="${_POST[$field$n]}"
-    n=$(($n + 1))
-  done
-done >>"$tempfile"
-
-case "${_POST[action]}" in
-  addfield)
-    echo "${_POST[newfield]}:\r" >>"$tempfile"
-    echo "END:VEVENT\r" >>"$tempfile"
-    echo "END:VCALENDAR\r" >>"$tempfile"
-    echo -n "Location: ?p=courses&edit=$course\n\n"
-    ;;
-  update)
-    attendance=()
-    for att in attendance attendance{0..100}; do
-      [ -n "${_POST[$att]}" ] && attendance+=("${_POST[$att]}")
-    done
-    sed -rn 's:^'$course'\t(.+)$:\1:p' "$attfile" |while read card; do
-      touch "$_DATA/vcard/$card"
-    done
-    sed -i -r '/^'$course'\t(.+)$/d' "$attfile"
-    for each in $attendance; do
-      echo "$course\t$each"
-    done >>"$attfile"
-    sed -rn 's:^'$course'\t(.+)$:\1:p' "$attfile" |while read card; do
-      touch "$_DATA/vcard/$card"
-    done
-
-    echo "END:VEVENT\r" >>"$tempfile"
-    echo "END:VCALENDAR\r" >>"$tempfile"
-    mv "$tempfile" "$coursefile"
-    echo -n "Location: ?p=courses#$course\n\n"
-    ;;
-  cancel)
-    rm "$tempfile"
-    [ -f "$coursefile" ] \
-      && echo -n "Location: ?p=courses#$course\n\n" \
-      || echo -n "Location: ?p=courses\n\n"
-    ;;
-  delete)
-    rm "$tempfile" "$coursefile"
-    echo -n "Location: ?p=courses\n\n"
-    ;;
-  *)
-    echo "END:VEVENT\r" >>"$tempfile"
-    echo "END:VCALENDAR\r" >>"$tempfile"
-    echo -n "Location: ?p=courses&edit=$course\n\n"
-    ;;
-esac
diff --git a/actions/update_prescription.sh b/actions/update_prescription.sh
deleted file mode 100755 (executable)
index aeb4993..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-BR='
-'
-prescription="${_POST[prescription]}"
-
-tempfile="$_DATA/temp/$prescription"
-prescfile="$_DATA/prescriptions/$prescription"
-client="${prescription%.*.mpx}.vcf"
-clientfile="$_DATA/vcard/$client"
-
-if [ -z "$prescription" -o \! -f "$clientfile" ]; then
-  redirect "?p=error"
-  exit 0
-fi
-
-[ "$_POST[issuer_select]" = "other" ] && _POST[issuer]="${_POST[issuer_other]}"
-
-# serialize POST array into file
-for key in ${(k)_POST}; do
-  printf %s:%s\\n "$key" "${_POST[$key]//$BR/\\n}"
-done >"$tempfile"
-
-case "${_POST[action]}" in
-  save)
-    mv "$tempfile" "$prescfile"
-    touch "$clientfile"
-    ;;
-  cancel)
-    rm "$tempfile"
-    ;;
-  delete)
-    rm "$tempfile" "$prescfile"
-    touch "$clientfile"
-    ;;
-esac
-
-redirect "?p=prescriptions&client=${client}#${prescription}"
diff --git a/actions/update_therapy.sh b/actions/update_therapy.sh
deleted file mode 100755 (executable)
index 6ba06cb..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-BR='
-'
-tpy="${_POST[id]}"
-
-tpyfile="$_DATA/therapies/$tpy"
-tempfile="$_DATA/temp/$tpy"
-
-# serialize POST array into file
-for key in ${(k)_POST}; do
-  [ "$key" != imagedata ] && printf %s:%s\\n "$key" "${_POST[$key]//$BR/\\n}"
-done >"$tempfile"
-
-if [ -n "$_POST[delete_session]" ]; then
-  n="$_POST[delete_session]"
-  sed -i -r '/^session'$n'[_:]/d' "$tempfile"
-  rm "${tpyfile%.tpy}_session${n}.png"
-
-  while grep -Eq '^session'$(($n + 1))'_' "$tempfile"; do
-    sed -i -r 's;^session'$(($n + 1))'(_|:);session'$n'\1;' "$tempfile"
-    mv "${tpyfile%.tpy}_session$(($n+1)).png" "${tpyfile%.tpy}_session${n}.png"
-    n=$(($n+1))
-  done
-
-elif [ -n "$_POST[new_session]" ]; then
-  sid="$_POST[new_session]"
-
-  identify "$_EXEC/static/therapy_background.png" \
-  | sed -r 's;^.* ([0-9]+x[0-9]+) .*$;\1;' \
-  | read dim
-
-  convert -size "$dim" xc:transparent "${tpyfile%.tpy}_${sid}.png"
-
-  printf '%s:exists\n' "$sid" >>"$tempfile"
-  printf '%s_open:checked\n' "$sid" >>"$tempfile"
-
-elif [ -n "$_POST[imagedata]" ]; then
-  sed -rn 's;^(session[0-9]+)_open:checked$;\1;p' "$tempfile" \
-  | sort -n \
-  | tail -n1 \
-  | read sid
-
-  convert "${tpyfile%.tpy}_${sid}.png" \
-          -strokewidth 2 -fill '#00000000' \
-          -draw "${_POST[imagedata]}" -transparent white \
-          "${tpyfile%.tpy}_${sid}.png"
-  sync
-fi
-
-mv "$tempfile" "$tpyfile"
-
-redirect "?p=therapy&id=${tpy}"
similarity index 56%
rename from actions/new_card.sh
rename to cards/edit_card.sh
index 2d6c39b3ff4200f0952697e051e43a7b73e37107..897bcc28df0109af1bf7a969d5d6506dba464f6f 100755 (executable)
@@ -1,6 +1,6 @@
-#!/bin/zsh
+#!/bin/sh
 
-# Copyright 2014 Paul Hänsch
+# Copyright 2019 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-cgi_post
-cgi_refdata
+locktimeout=900
+. "$_EXEC"/session_lock.sh
 
-filter="&filter=${_REF[filter]}"
-filtertype="&filtertype=${_REF[filtertype]}"
-order="&order=${_REF[order]}"
+card="$(GET card |PATH)"
+cardfile="$_DATA/vcard/${card##*/}"
+filter="$(REF f)"
+order="$(REF o)"
 
-uid=$(uuidgenerator)
-card="${uid}.vcf"
-
-tempfile="$_DATA/temp/$card"
-
-cat >"$tempfile" <<EOF
-BEGIN:VCARD
-VERSION:4.0
-N:;;;;
-BDAY:
-TEL:
-EMAIL:
-ADR:
-NOTE:
-UID:$uid
-END:VCARD
-EOF
-
-echo -n "Location: ?p=cards${filter}${filtertype}${order}&edit=$card\n\n"
+if tempfile="$(SLOCK "$cardfile" "$locktimeout")"; then
+  REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}"
+elif [ -f "$tempfile" ]; then
+  SET_COOKIE session message="SESSLOCK"
+  REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}"
+else
+  SET_COOKIE session message="EDITLOCK"
+  REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}"
+fi
similarity index 69%
rename from actions/edit_card.sh
rename to cards/export_card.sh
index f90b326d2cca3a38dfa6b668fe3bc689c7556a9b..0918032c6019fc8da3d3be9a0711b539fdb01365 100755 (executable)
@@ -1,6 +1,6 @@
-#!/bin/zsh
+#!/bin/sh
 
-# Copyright 2014 Paul Hänsch
+# Copyright 2014, 2015, 2021 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-cgi_refdata
+card="$(GET card |PATH)"
+cardfile="$_DATA/vcard/${card##*/}"
 
-card="${_GET[card]}"
-filter="&filter=${_REF[filter]}"
-filtertype="&filtertype=${_REF[filtertype]}"
-order="&order=${_REF[order]}"
+. $_EXEC/pdiread.sh
+. $_EXEC/cgilite/file.sh
 
-echo -n "Location: ?p=cards${filter}${filtertype}${order}&edit=$card\n\n"
+printf 'Content-Disposition: inline; filename="%s.vcf"\r\n' "$(pdi_value "$(pdi_load "$cardfile")" FN)"
+
+FILE "$cardfile" "text/vcard; charset=utf-8"
diff --git a/cards/export_csv.sh b/cards/export_csv.sh
new file mode 100755 (executable)
index 0000000..b566c03
--- /dev/null
@@ -0,0 +1,64 @@
+#!/bin/sh
+
+. $_EXEC/pdiread.sh
+. $_EXEC/cards/l10n.sh
+. $_EXEC/cards/list.sh
+
+filter="$(GET f)"
+order="$(GET o)"
+
+[ "$order" ] || order=firstname
+
+list_attendance() {
+  grep -F "    ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do
+    { pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY || l10n "(unnamed course)"; } |unescape
+  done \
+  | sed -E 's;";\\";g;'
+}
+
+list_item() {
+  local item="$1"
+  local cnt="$(pdi_count "$card" "$item")"
+  local ret=''
+  
+  seq 1 $cnt |while read n; do case $item in
+    TEL)
+      tel="$(pdi_value "$card" "$item" "$n" |unescape)"
+      ttype="$(pdi_attrib "$card" "$item" "$n" TYPE)"
+      if [ "$tel" -a "$ttype" ]; then 
+        printf '%s: %s\n' "$(l10n "TYPE=$ttype")" "$tel"
+      elif [ "$tel" ]; then
+        printf '%s\n' "$tel"
+      fi
+      ;;
+    GENDER)
+      gen="$(pdi_value "$card" "$item" "$n" |unescape)"
+      [ "$gen" ] && l10n "gender_$gen"
+      ;;
+    *) pdi_value "$card" "$item" "$n" |unescape
+      ;;
+  esac; done \
+  | sed -E 's;";\\";g;'
+}
+
+printf '%s\r\n' \
+       'Content-Type: text/csv; charset=utf-8' \
+       'Content-Disposition: inline; filename="confetti_export_'$(date +%F_%T)'.csv"' \
+       ''
+
+printf '"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s"\n' \
+       "$(l10n FN)" "$(l10n GENDER)" "$(l10n BDAY)" \
+       "$(l10n TEL)" "$(l10n EMAIL)" "$(l10n ADR)" \
+       "$(l10n NOTE)" "$(l10n courses)" "$(l10n CATEGORIES)" \
+| sed -E 's;&shy\;;;g;'
+
+
+filter_cards \
+| order_cards \
+| while read cardfile; do
+  card="$(pdi_load "$cardfile")"
+  printf '"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s";"%s"\n' \
+  "$(list_item FN)" "$(list_item GENDER)" "$(list_item BDAY)" \
+  "$(list_item TEL)" "$(list_item EMAIL)" "$(list_item ADR)" \
+  "$(list_item NOTE)" "$(list_attendance)" "$(list_item CATEGORIES)"
+done
diff --git a/cards/filter_card.sh b/cards/filter_card.sh
new file mode 100755 (executable)
index 0000000..504fb08
--- /dev/null
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+# Copyright 2014, 2017, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+filter="$(
+  seq 0 100 |while read n; do
+    filter_type="$(POST "filter_type${n}")"
+    filter_text="$(POST "filter_text${n}")"
+    if [ ! "$filter_type" -a ! "$filter_text"  ]; then
+      break
+    elif [ "$filter_type" = CATEGORIES ]; then
+      printf '^CATEGORIES:'
+      seq 0 $(POST_COUNT filter_cat$n) |while read m; do
+        printf '|%s' "$(POST filter_cat$n $m)"
+      done
+    elif [ "$filter_type" = course ]; then
+      printf '^course:'
+      seq 0 $(POST_COUNT filter_course$n) |while read m; do
+        printf '|%s' "$(POST filter_course$n $m)"
+      done
+    else
+      printf '^%s:%s' "$filter_type" "$filter_text"
+    fi
+  done | sed -E \
+         's;\|+;\|;g;   s;\^+;\^;g;   s;:\|;:;g;
+          :X;   s;\^[^:]*:\^;\^;g;   /\^[^:]*:\^/bX;
+          s;^\^;;;   s;\^[^:]*:$;;;'
+)"
+
+case $(POST choice) in
+  filter)
+    REDIRECT "${_BASE}/cards/?o=$(POST order)&f=${filter}"
+    ;;
+  new_filter)
+    REDIRECT "${_BASE}/cards/?o=$(POST order)&f=${filter}&newfilter=yes"
+    ;;
+  export_csv)
+    REDIRECT "${_BASE}/cards/export_csv.sh?o=$(POST order)&f=${filter}"
+    ;;
+  *)
+    REDIRECT "${_BASE}/cards/"
+    ;;
+esac
diff --git a/cards/index.cgi b/cards/index.cgi
new file mode 100755 (executable)
index 0000000..5babc9f
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+. $_EXEC/pdiread.sh
+. $_EXEC/cards/l10n.sh
+. $_EXEC/cards/widgets.sh
+. $_EXEC/cards/list.sh
+
+filter="$(GET f)"
+order="$(GET o)"
+edit="$(GET e |PATH)"
+
+[ "$order" ] || order=firstname
+edit="${edit##*/}"
+
+{ w_filter_diag
+  printf '
+  [form class="newcard" action="%s/cards/new_card.sh" method="POST"
+    [button type="submit" %s]
+    [input name="seed" placeholder="%s"]
+  ]' "${_BASE}" "$(l10n newcard)" "$(l10n vcf_seed_label)"
+  [ "$edit" ] && edit_card "$edit"
+  list_cards
+} | yield_page cards #/cards/cards.css
diff --git a/cards/l10n.sh b/cards/l10n.sh
new file mode 100755 (executable)
index 0000000..2d9dc06
--- /dev/null
@@ -0,0 +1,63 @@
+# Copyright 2014, 2016, 2019, 2021 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+l10n(){
+  local word
+  [ $# -eq 0 ] && read -r word || word="$*"
+  case $word in
+    newcard) printf %s "Neuen Eintrag anlegen";;
+
+    X-HEALTH-INSURANCE) printf %s "Kran&shy;ken&shy;ver&shy;sich&shy;er&shy;ung";;
+    hi_from_list) printf %s "Aus Liste";;
+    hi_other) printf %s "Andere";;
+    hi_company) printf %s "Ver&shy;sich&shy;er&shy;ungs&shy;ge&shy;sell&shy;schaft";;
+    hi_number) printf %s "Ver&shy;sich&shy;er&shy;ten&shy;num&shy;mer";;
+    hi_status) printf %s "Ver&shy;sich&shy;er&shy;ten&shy;sta&shy;tus";;
+    X-HEALTH-INSURANCE-NOCONTRIB) printf %s "Zu&shy;zahl&shy;ungs&shy;be&shy;frei&shy;ung";;
+    X-CLIENT-REFERRAL) printf %s "Empfehl&shy;ung durch";;
+    prescriptions) printf %s "Verord&shy;nungen";;
+    new_prescription) printf %s "Neue Verord&shy;nung";;
+    no_icd) printf %s "Kein ICD Code";;
+  
+    X-ZACK-JOINDATE)  printf %s "Anmelde&shy;datum";;
+    X-ZACK-LEAVEDATE) printf %s "Abmelde&shy;datum";;
+    X-ZACK-JOINDATE_short)  printf %s "Anm.";;
+    X-ZACK-LEAVEDATE_short) printf %s "Abm.";;
+
+    *) l10n_global "$word";;
+  esac
+}
+
+# BEGIN) printf %s "";;
+# CALADRURI) printf %s "";;
+# CALURI) printf %s "";;
+# CLASS) printf %s "";;
+# CLIENTPIDMAP) printf %s "";;
+# END) printf %s "";;
+# FBURL) printf %s "";;
+# GEO) printf %s "";;
+# MAILER) printf %s "";;
+# NAME) printf %s "";;
+# PRODID) printf %s "";;
+# PROFILE) printf %s "";;
+# REV) printf %s "";;
+# SORT-STRING) printf %s "";;
+# SOURCE) printf %s "";;
+# TZ) printf %s "";;
+# UID) printf %s "";;
+# VERSION) printf %s "";;
+# XML) printf %s "";;
diff --git a/cards/list.sh b/cards/list.sh
new file mode 100755 (executable)
index 0000000..519005c
--- /dev/null
@@ -0,0 +1,212 @@
+#!/bin/sh
+
+. "${_EXEC}"/pdiread.sh
+
+edit_card(){
+  local cardfile="$_DATA/vcard/$1" 
+  local tempfile card
+
+  . $_EXEC/session_lock.sh
+
+  if ! tempfile="$(CHECK_SLOCK "$cardfile")"; then
+    printf '[div .message %s]' "$(l10n "This card is not set up for editing within this session.")"
+  else
+    card="$(pdi_load "$tempfile")"
+    cat <<-EOF
+       [form .card #${cardfile##*/} action="${_BASE}/cards/update_card.sh" method="POST"
+         [input type="hidden" name="tid" value="$(transid ${tempfile})"]
+         [div .section .basic $(
+           edit_item "$card" N GENDER
+           [ "$(pdi_count "$card" NICKNAME)" -gt 0 ] \
+           && edit_item "$card" NICKNAME
+           edit_item "$card" BDAY
+           edit_item "$card" X-ZACK-JOINDATE
+           [ "$(pdi_count "$card" X-ZACK-LEAVEDATE)" -gt 0 ] \
+           && edit_item "$card" X-ZACK-LEAVEDATE
+           card_item "$card" SOUND PHOTO LOGO
+         )]
+         [div .section .phone   $(edit_item "$card" TEL)]
+         [div .section .message $(
+           edit_item "$card" EMAIL
+           [ $(pdi_count "$card" IMPP) -gt 0 ] && edit_item "$card" IMPP
+           [ $(pdi_count "$card" URL ) -gt 0 ] && edit_item "$card" URL
+         )]
+         [div .section .address $(edit_item "$card" ADR)]
+         [div .section .note    $(edit_item "$card" NOTE)]
+         [div .section .attendance
+           [h3 $(l10n course_attendance) ] [div .attendance $(
+            list_courses |while IFS=/ read course coursename; do
+              courseH="$(HTML "$course")"
+             printf '[input type="checkbox" id="cour%s" name="attendance" value="%s" %s][label for="cour%s" . %s]' \
+                     "$courseH" "$courseH" \
+                    "$(grep -qF "${course}     ${cardfile##*/}" "$_DATA/mappings/attendance" \
+                       && printf 'checked="checked"'
+                      )" \
+                     "$courseH" "$coursename"
+           done)]
+           [h3 $(l10n CATEGORIES) ] [div .categories $(
+           grep -xE '[^ ]+' "$_DATA"/mappings/categories |while read -r cat; do
+              catH="$(HTML "$cat")"
+             printf '[input type="checkbox" id="cat%s" name="CATEGORIES" value="%s" %s][label for="cat%s" . %s]' \
+                    "$catH" "$catH" \
+                    "$(seq 1 $(pdi_count "$card" CATEGORIES) |while read c; do
+                      pdi_value "$card" CATEGORIES $c |grep -qxF "$cat" \
+                      && printf 'checked="checked"' && break
+                    done)" \
+                    "$catH" "$catH"
+           done)]
+         ]
+         [div .control
+           [div .item .delete label="$(l10n edit_delete)"
+              [input type="checkbox" #delete]
+              [label for="delete" $(l10n edit_delete)]
+             [button type="submit" name="action" value="delete" $(l10n edit_delete)]
+            ]
+            [div .item .newfield
+              [select name="newfield"
+               [option value="" disabled="disabled" selected="selected" $(l10n edit_addfieldtext)]
+               $(for f in NICKNAME EMAIL TEL IMPP ADR URL NOTE X-ZACK-LEAVEDATE; do
+                 printf '[option value="%s" %s] ' "$f" "$(l10n "$f")"
+               done)
+             ][button type="submit" name="action" value="addfield" $(l10n edit_addfield)]
+            ]
+           [button .item type="submit" name="action" value="update"   $(l10n edit_update)]
+           [button .item type="submit" name="action" value="cancel"   $(l10n edit_cancel)]
+         ]
+         [input type="hidden" name="UID" value="$(pdi_value "$card" UID |HTML)"]
+         [input type="hidden" name="card" value="${cardfile##*/}"]
+       ]
+       EOF
+  fi
+}
+
+print_card(){
+  local cardfile="$1"
+  local card="$(pdi_load "$cardfile")"
+  cat <<-EOF
+    [div .card #${cardfile##*/}
+      [div .section .basic . $(
+        card_item "$card" FN GENDER NICKNAME BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE SOUND PHOTO LOGO
+      )]
+      [div .section .phone   . $(card_item "$card" TEL)]
+      [div .section .message . $(card_item "$card" EMAIL IMPP URL)]
+      [div .section .address . $(card_item "$card" ADR)]
+      [div .section .note    . $(card_item "$card" NOTE)]
+      [div .section .attendance [h3 $(l10n course_attendance) ] [ul
+        $(grep -F "    ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do
+          printf '[li [a .item .attendance href="%s/courses#%s" . %s]]' \
+                 "${_BASE}" "$each" \
+                 "$(pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY || l10n "(unnamed course)" |unescape |HTML)"
+        done |sort -k7)]
+        $(card_item "$card" CATEGORIES)
+      ]
+      [div .control
+        [a .button .item href="${_BASE}/cards/edit_card.sh?card=${cardfile##*/}" $(l10n edit)]
+        [a .button .item href="${_BASE}/cards/export_card.sh?card=${cardfile##*/}" $(l10n vcf_export)]
+      ]
+    ]
+       EOF
+}
+
+print_cards(){
+  local cardfile cachefile date size name ldate=0 lsize lname
+
+  while read cardfile; do
+    cachefile="${_DATA}/cache/${cardfile##*/}.cache"
+    if [ -s "$cachefile" -a "$cachefile" -nt "$cardfile" ]; then
+      cat "$cachefile"
+    elif [ -s "$cardfile" ]; then
+      print_card "$cardfile" |tee "$cachefile"
+    fi
+  done
+}
+
+filter_attendance(){
+  fatt="$1"
+  attfile="$_DATA/mappings/attendance"
+
+  if [ ! "$fatt" ]; then
+    # debug 'list all'
+    printf '%s\n' "$_DATA/vcard"/*.vcf
+  elif [ "${fatt#* }" = "${fatt}" ]; then
+    # debug "list $fatt"
+    grep -xiE "(${fatt})       .+vcf" "$attfile" \
+    | while read vcf; do
+      printf '%s/vcard/%s\n' "$_DATA" "${vcf##*        }"
+    done
+  else
+    # debug "filter ${fatt%% *}"
+    filter_attendance "${fatt#* }" \
+    | while read vcf; do
+      grep -xiE "(${fatt%% *}) ${vcf##*/}" "$attfile"
+    done \
+    | while read vcf; do
+      printf '%s/vcard/%s\n' "$_DATA" "${vcf##*        }"
+    done
+  fi
+}
+
+filter_cards(){
+  local filter f fex='x;p;'
+  local upcase=' y;abcdefghijklmnopqrstuvwxyzäöüé;ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉ;; '
+
+  filter="$(printf %s "${filter}" \
+            | sed -E 's;[]\/\(\)\\\$\?\.\+\*\;\[\{\}];\\&;g;
+                      '"$upcase"
+           )^"
+
+  while [ "$filter" ]; do
+    f="${filter%%^*}" filter="${filter#*^}"
+    case $f in
+      '') break
+        ;;
+      COURSE:*) fatt="${fatt}${fatt:+ }${f#*:}"
+        ;;
+      ANY:*) fex="/\n.*(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}"
+        ;;
+      NAME:*) fex="/\n(N|FN|NICKNAME)(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}"
+        ;;
+      STREET:*|ZIP:*) fex="/\nADR(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}"
+        ;;
+      *) fex="/\n${f%%:*}(\;[^:]*)?:[^\n]*(${f#*:})[^\n]*\r?\n/{${fex}}"
+        ;;
+    esac
+  done
+
+  # for cardfile in "${_DATA}"/vcard/*.vcf; do
+  filter_attendance "$fatt" |while read cardfile; do
+    printf '%s\n' "$cardfile"
+    cat "$cardfile"
+  done \
+  | sed -nE ':X; /\nEND\;?:VCARD\r?$/!{ N; bX; }; h; s;\n.*$;;; x; s;^[^\n]+\n;;;
+             '"$upcase""$fex"
+}
+
+order_cards() {
+  local cardfile card
+
+  while read cardfile; do
+    card="$(pdi_load "$cardfile")"
+
+    case $order in
+      firstname)
+        printf '%s     %s\n' "$(pdi_value "$card" FN)" "$cardfile"
+        ;;
+      lastname)
+        printf '%s     %s\n' "$(pdi_value "$card" N || pdi_value "$card" FN)" "$cardfile"
+        ;;
+      bdate)
+        printf '%s     %s\n' "$(pdi_value "$card" BDAY || printf 0000-00-00)" "$cardfile"
+        ;;
+    esac
+  done \
+  | sort \
+  | sed -E 's;^.*\t;;g'
+}
+
+list_cards(){
+  filter_cards \
+  | order_cards \
+  | grep -xvF "$edit" \
+  | print_cards
+}
diff --git a/cards/new_card.sh b/cards/new_card.sh
new file mode 100755 (executable)
index 0000000..69f89c6
--- /dev/null
@@ -0,0 +1,75 @@
+#!/bin/sh
+
+# Copyright 2014, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+filter="$(REF f)"
+order="$(REF o)"
+
+uid="$(timeid)$(randomid)"  # 32 Octets UID, starting with timestamp
+card="${uid}.vcf"
+
+vcf_escape(){
+  for each in "$@"; do
+    printf %s\\n "$each" \
+    | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;'
+  done \
+  | sed -E ':X;$!{N;bX}; s;\n;\;;g'
+}
+
+IFS='|' read -r date fn ln bday bmonth byear tel tcell junk1 email junk2 note <<-EOF
+       $(POST seed |tr \\t \|)
+       EOF
+
+[ ${#byear} = 1 ] && byear="200$byear"
+[ ${#byear} = 2 ] && byear="20$byear"
+[ ${#bmonth} = 1 ] && bmonth="0$bmonth"
+[ ${#bday} = 1 ] && bday="0$bday"
+
+mn=""
+case $fn in
+  *\ *)
+    mn="${fn#* }"
+    fn="${fn%% *}"
+    ;;
+esac
+
+mkdir -p "${_DATA}/lock/vcard/"
+lockdir="${_DATA}/lock/vcard/${card}/"
+lockfile=${lockdir}/${SESSION_ID}
+
+if mkdir "$lockdir"; then
+  cat >"$lockfile" <<-EOF
+       BEGIN:VCARD
+       VERSION:4.0
+       N:$(vcf_escape "$ln" "$fn" "$mn" "" "")
+       FN:$(vcf_escape "${fn}${mn:+ }${mn} ${ln}")
+       BDAY:$(parse_date "${byear}-${bmonth}-${bday}")
+       TEL:$(vcf_escape "$tel")
+       TEL;TYPE=CELL:$(vcf_escape "$tcell")
+       EMAIL:$(vcf_escape "$email")
+       X-ZACK-JOINDATE:$(parse_date "$date")
+       ADR:
+       NOTE:$(vcf_escape "$note")
+       UID:${uid}
+       END:VCARD
+       EOF
+  REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}"
+else
+  SET_COOKIE session message="EDITLOCK"
+  REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}"
+fi
diff --git a/cards/update_card.sh b/cards/update_card.sh
new file mode 100755 (executable)
index 0000000..2b87632
--- /dev/null
@@ -0,0 +1,151 @@
+#!/bin/sh
+
+# Copyright 2014, 2016, 2019, 2020, 2021 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. "$_EXEC/pdiread.sh"
+. "$_EXEC/session_lock.sh"
+. "$_EXEC/cgilite/storage.sh"
+
+unset filter order card action newfield
+unset cardfile attfile tempfile
+unset vcf field cnt delete_key
+
+filter="$(REF f)"
+order="$(REF o)"
+
+card="$(POST card |PATH)"; card="${card##*/}"
+cardfile="$_DATA/vcard/${card}"
+attfile="$_DATA/mappings/attendance"
+
+action="$(POST action)"
+newfield="$(POST newfield |grep -m 1 -xE '[A-Z][A-Z0-9-]*')"
+
+if printf '%s\n' "$action" |grep -qxE 'addfield [A-Z][A-Z0-9]*'; then
+  newfield="${action##* }"
+  action=addfield
+fi
+
+if ! tempfile=$(CHECK_SLOCK "$cardfile"); then
+  SET_COOKIE 0 message="NO VALID FILE LOCK"
+  REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}"
+  exit 0
+elif [ "$(POST tid)" != "$(transid "$tempfile")" ]; then
+  SET_COOKIE 0 message="INVALID TRANSACTION ID"
+  REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}"
+  exit 0
+fi
+
+vcf_escape(){
+  for each in "$@"; do
+    printf %s\\n "$each" \
+    | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;'
+  done \
+  | sed -E ':X;$!{N;bX}; s;\n;\;;g'
+}
+
+# [ "${_POST[hi_select]}" = "list" ] || _POST[hi_company]="${_POST[hi_other]}"
+# [ -n "${_POST[hi_company]}${_POST[hi_number]}${_POST[hi_status]}" ] \
+# && _POST[X-HEALTH-INSURANCE]="$(vcf_escape "${_POST[hi_company]}" "${_POST[hi_number]}" "${_POST[hi_status]}")"
+
+vcf="$(pdi_load "$tempfile")"
+
+n1="$(POST 1N)" n2="$(POST 2N)" n3="$(POST 3N)" n4="$(POST 4N)" n5="$(POST 5N)"
+# 3N (Middle Names) is not actually used
+n3="${n2#${n2%% *}}"
+
+vcf="$(pdi_update_value "$vcf"  N 1 "$(vcf_escape "$n1" "${n2%% *}" "${n3# }" "$n4" "$n5")")"
+vcf="$(pdi_update_value "$vcf" FN 1 "$(vcf_escape "$n4 $n2 $n1 $n5" |sed -E 's;(^ +| +$);;g; s; +; ;g;')")"
+vcf="$(printf '%s\n' "$vcf" |sed -E "/^CATEGORIES;[^:]*:.*$/d")"
+
+for field in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*'); do
+  for cnt in $(seq 1 $(POST_COUNT "$field")); do
+    case "$field" in
+      # (TEL)
+      #   printf '%s;TYPE=%s:%s\r\n' "${field}" "${_POST[phonetype${key#TEL}]}" "$(vcf_escape "$(POST "$field" "$cnt")")"
+      #   ;;
+      TEL)
+         vcf="$(pdi_update_attrib "$vcf" TEL $cnt TYPE="$(POST teltype $cnt |grep -Exm1 'HOME|WORK|CELL|FAX')")"
+         vcf="$(pdi_update_value "$vcf" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")"
+         ;;
+      *)
+         vcf="$(pdi_update_value "$vcf" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")"
+        ;;
+    esac
+done; done
+
+# delete fields, first mark for deletion using delete_key
+# this way the field enumeration is preserved during the process
+# finally filter marked lines
+delete_key="$(randomid)"
+for delete in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*_delete_[0-9]+'); do
+  f="${delete%%_*}"; c="${delete##*_}";
+  [ "$(POST "$delete")" = "true" ] && vcf="$(pdi_update_value "$vcf" "$f" "$c" "delete=${delete_key}")"
+done
+vcf="$(printf '%s\n' "$vcf" |sed -E "/^[^:]+:delete=${delete_key}\$/d")"
+
+if [ "$action" = addfield ]; then
+  vcf="$(pdi_update_value "$vcf" "$newfield" $(( $(pdi_count "$vcf" "$newfield") + 1 )) '')"
+fi
+printf '%s' "$vcf" |grep -vx '' >"$tempfile"
+
+case "$action" in
+  addfield)
+    REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}&e=${card}"
+    ;;
+  update)
+    if LOCK "$attfile"; then
+      grep -F "        ${card}" "$attfile" |while read course junk; do
+        touch "$_DATA/ical/${course}"
+      done
+      sed -i -E "/^.+  ${card}\$/d" "$attfile"
+      seq 1 $(POST_COUNT attendance) |while read n; do
+        printf '%s     %s\n' "$(POST attendance $n)" "$card"
+      done >>"$attfile"
+      grep -F "        ${card}" "$attfile" |while read course junk; do
+        touch "$_DATA/ical/${course}"
+      done
+      RELEASE "$attfile"
+    else
+      SET_COOKIE 0 message="COULD NOT UPDATE COURSE MAPPINGS"
+    fi
+
+    cp "$tempfile" "$cardfile"
+    RELEASE_SLOCK "$cardfile"
+    REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}"
+    ;;
+  cancel)
+    RELEASE_SLOCK "$cardfile"
+    [ -f "$cardfile" ] \
+    && REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}#${card}" \
+    || REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}"
+    ;;
+  delete)
+    rm "$cardfile"
+    RELEASE_SLOCK "$cardfile"
+    if LOCK "$attfile"; then
+      grep -F "        ${card}" "$attfile" |while read course junk; do
+        touch "$_DATA/ical/${course}"
+      done
+      sed -i -E "/^.+  ${card}\$/d" "$attfile"
+      RELEASE "$attfile"
+    else
+      SET_COOKIE 0 message="COULD NOT UPDATE COURSE MAPPINGS"
+    fi
+    REDIRECT "${_BASE}/cards/?o=${order}&f=${filter}"
+    ;;
+esac
diff --git a/cards/widgets.sh b/cards/widgets.sh
new file mode 100755 (executable)
index 0000000..eb9d0fd
--- /dev/null
@@ -0,0 +1,284 @@
+# Copyright 2014 - 2019, 2021 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+list_categories() {
+  grep -vxE '^[        ]*$' "${_DATA}/mappings/categories"
+}
+
+list_courses() {
+  local file name cachefile="${_DATA}/cache/courses.ui.cache"
+  if [ $cachefile -nt "${_DATA}/ical" ]; then
+    cat "$cachefile"
+  else
+    for file in "$_DATA/ical"/*.ics; do
+      name="$(pdi_value "$(pdi_load "$file")" SUMMARY || l10n "(unnamed course)" |unescape |HTML)"
+      printf '%s/%s\n' "${file##*/}" "$name"
+    done \
+    | sort -t/ -k2 |tee "$cachefile"
+  fi
+}
+
+w_filter_item() {
+n=$3
+cat <<EOF
+  [fieldset .item
+    [legend $(l10n filter_item):]
+
+    $(for field in any name street zip TEL BDAY CATEGORIES course; do
+      printf '[input .tab id="%s%i" type="radio" name="filter_type%i" value="%s" %s][label for="%s%i" %s ]' \
+              "$field" "$n" "$n" "$field" "$([ "$1" = "$field" ] && printf checked )" \
+              "$field" "$n" "$(l10n filter_$field)"
+    done)
+    [input .tab type="text" name="filter_text$n" value="$([ "$1" = CATEGORIES -o "$1" = course ] || HTML "$2")" placeholder="$(l10n filter_placeholder)"]
+    [fieldset .tab .categories
+      $(list_categories | while read cat; do
+          printf '[label [checkbox "filter_cat%i" "|%s" %s] %s ] ' \
+                 "$n" "$(HTML "$cat")" \
+                 "$(printf %s "$cat" |grep -qxEe "$2" && printf checked )" \
+                 "$(HTML "$cat")"
+      done)
+      [a href="${_BASE}/categories/" $(l10n edit_categories)]
+    ]
+    [fieldset .tab .courses
+      $(list_courses | while IFS=/ read course coursename; do
+        printf '[label [checkbox "filter_course%i" "|%s" %s] %s ] ' \
+               "$n" "$(HTML "${course##*/}")" \
+               "$(printf %s "${course##*/}" |grep -qxEe "$2" && printf checked )" \
+               "$coursename"
+      done)
+    ]
+  ]
+EOF
+}
+
+w_filter_diag(){
+  cat <<EOF
+  [form .filter action="${_BASE}/cards/filter_card.sh" method="POST"
+    [h1 $(l10n filter_label)]
+    [input type="hidden" name="page" value="cards"]
+  
+    $(n=0; filter="${filter}^"
+      while [ "${filter#^}" ]; do
+        fil="${filter%%^*}" filter="${filter#*^}"
+        w_filter_item "${fil%%:*}" "${fil#*:}" $n
+        n=$((n + 1))
+      done
+      [ "$n" -eq 0 -o "$(GET newfilter)" ] && w_filter_item any '' $n
+    )
+    [button type="submit" name="choice" value="new_filter" $(l10n filter_more)]
+    [fieldset class="order"
+      [legend $(l10n filter_order):]
+      [label [radio "order" "firstname" $( [ "$order" = firstname ] && printf checked )] $(l10n filter_firstname)]
+      [label [radio "order" "lastname"  $( [ "$order" = lastname  ] && printf checked )] $(l10n filter_lastname)]
+      [label [radio "order" "bdate"     $( [ "$order" = bdate     ] && printf checked )] $(l10n filter_bdate)]
+    ]
+    [button type="submit" name="choice" value="filter" $(l10n filter_apply)]
+    [button type="submit" name="choice" value="del_filter" $(l10n filter_cancel)]
+    [button type="submit" name="choice" value="export_csv" $(l10n export_csv)]
+  ]
+EOF
+}
+
+# listcards |grep ${edit:+-v} "$edit" \
+# | while read card; do
+#   "${_EXEC}"/cgilite/html-sh.sed <<-ENDCARD
+#      [div #${card} .card
+#        $(view_card "$card")[!--
+#        --][div .control
+#          [a "?action=edit_card&card=${card}" .item $(l10n edit)]
+#          [a "?action=export_vcard&card=${card}".item $(l10n vcf_export)]
+#          ${profile_medical:+[a "?action=new_prescription&client=${card}" .item $(l10n new_prescription)]}
+#      ]]
+#      ENDCARD
+# done
+
+#!/bin/sh
+
+card_item(){
+  local card="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$card" "$item")"
+
+    case $item in
+      FN) printf '[h2 .item .FN . %s]' "$(pdi_value "$card" FN |unescape |HTML)"
+        ;;
+      GENDER) printf '[span .item .GENDER . %s]' "$(pdi_value "$card" GENDER |l10n)"
+        ;;
+      NICKNAME) seq 1 $cnt |while read c; do
+          printf '[span .item .NICKNAME . aka. "%s"]' \
+                 "$(pdi_value "$card" NICKNAME $c |unescape |HTML)"
+        done
+        ;;
+      X-ZACK-JOINDATE|X-ZACK-LEAVEDATE) if [ $cnt -gt 0 ]; then
+          printf '[span .item .%s [b %s:] %s]' \
+                 "$item" "$(l10n "${item}_short")" \
+                 "$(pdi_value "$card" "$item" |HTML)"
+        fi
+        ;;
+      BDAY)
+       [ $cnt -gt 0 ] && printf '[span .item .BDAY [b *:] %s]' \
+                                "$(pdi_value "$card" BDAY |grep -xE '[0-9-]+')"
+        ;;
+      SOUND)
+        [ $cnt -gt 0 ] && printf '[audio .item .SOUND controls="controls"
+                                    [source type="audio/ogg" src="data:audio/ogg;base64,%s"]
+                                  ]' \
+                                  "$(pdi_value "$card" SOUND |grep -xE '[a-zA-Z0-9/+=]+')"
+        ;;
+      PHOTO|LOGO)
+        [ $cnt -gt 0 ] && printf '[img .item .%s src="data:image/%s;base64,%s"]' "$item" \
+                                 "$(pdi_attrib "$card" "$item" |sed -E 's;^(.*;)?TYPE="?(.+)"?(;.*)?$;\2;')" \
+                                 "$(pdi_value "$card" "$item" |grep -xE '[a-zA-Z0-9/+=]+')"
+        ;;
+      EMAIL) 
+        [ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n EMAIL)"
+        seq 1 $cnt |while read c; do
+          printf '[a .item .EMAIL href="mailto:%s" . %s]' \
+                 "$(pdi_value "$card" EMAIL $c |unescape |HTML)" \
+                 "$(pdi_value "$card" EMAIL $c |unescape |HTML)"
+        done
+        ;;
+      TEL)
+        [ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n TEL)"
+        seq 1 $cnt |while read c; do
+          teltype="$(pdi_attrib "$card" TEL $c TYPE)"
+          [ "$teltype" ] \
+          && printf '[span .item .TEL [span .type . %s:] %s]' \
+                    "$(l10n "TYPE=$teltype" |HTML)" \
+                    "$(pdi_value "$card" TEL $c |unescape |HTML)" \
+          || printf '[span .item .TEL . %s]' \
+                    "$(pdi_value "$card" TEL $c |unescape |HTML)"
+        done
+        ;;
+      ADR)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")"
+        shy="$(printf '\302\255')"
+        seq 1 $cnt |while read c; do
+          printf '[span .item .%s . %s]' "$item" \
+                 "$(pdi_value "$card" "$item" $c |sed -r "s;(straße|weg|damm|allee|ufer);${shy}\1;g" |unescape |HTML)"
+        done
+        ;;
+      *)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[span .item .%s . %s]' "$item" \
+                 "$(pdi_value "$card" "$item" $c |unescape |HTML)"
+        done
+        ;;
+    esac
+  done
+}
+
+edit_item(){
+  local card="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$card" "$item")"
+    [ $cnt -lt 1 ] && cnt=1
+
+    case $item in
+      N)N="$(pdi_value "$card" N)"
+        if [ "$N" ]; then
+          IFS=\; read n1 n2 n3 n4 n5 <<-EOF
+               $N
+               EOF
+        else
+         N="$(pdi_value "$card" FN |unescape)"
+          n1="${N##* }"
+          n2="${N%$n1}"
+        fi
+        printf '
+        [h3 %s]
+        [input .item .N name="4N" placeholder="%s" value="%s"]
+        [input .item .N name="2N" placeholder="%s" value="%s"]
+        [input .item .N name="1N" placeholder="%s" value="%s"]
+        [input .item .N name="5N" placeholder="%s" value="%s"]
+        ' "$(l10n "$item")" \
+        "$(l10n n_pre)"   "$(HTML "$n4")" \
+        "$(l10n n_first)" "$(HTML "${n2}$([ "$n2" -a "$n3" ] && printf ' ')${n3}")" \
+        "$(l10n n_last)"  "$(HTML "$n1")" \
+        "$(l10n n_post)"  "$(HTML "$n5")"
+        ;;
+      GENDER)
+        gender="$(pdi_value "$card" GENDER)"
+        printf '
+        [select .item .GENDER name="GENDER"
+          [option value="" disabled="disabled" %s %s]
+          [option value="female" %s %s]
+          [option value="male"   %s %s]
+          [option value="other"  %s %s]
+          [option value="none"   %s %s]
+        ]\n' \
+        "$([ "$gender" = ''       ] && printf 'selected="selected"')" "$(l10n GENDER)" \
+        "$([ "$gender" = 'female' ] && printf 'selected="selected"')" "$(l10n gender_female)" \
+        "$([ "$gender" = 'male'   ] && printf 'selected="selected"')" "$(l10n gender_male)" \
+        "$([ "$gender" = 'other'  ] && printf 'selected="selected"')" "$(l10n gender_other)" \
+        "$([ "$gender" = 'none'   ] && printf 'selected="selected"')" "$(l10n gender_none)"
+        ;;
+      BDAY|X-ZACK-JOINDATE|X-ZACK-LEAVEDATE)
+        printf '[h3 %s]
+        [input .item .%s name="%s" value="%s" placeholder="YYYY-MM-DD"]' \
+        "$(l10n "$item")" "$item" "$item" "$(pdi_value "$card" "$item" |grep -xE '[0-9-]+')"
+        ;;
+      ADR|NOTE)
+        printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '<textarea class="item %s" name="%s">%s</textarea>' \
+            "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+      TEL) printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          teltype="$(pdi_attrib "$card" TEL $c TYPE)"
+          printf '[select .item .teltype name="teltype"
+                    [option value="" disabled="disabled" %s %s]
+                    [option value="HOME" %s %s]
+                    [option value="WORK" %s %s]
+                    [option value="CELL" %s %s]
+                    [option value="FAX"  %s %s]
+                  ]\n' \
+                  "$([ "$teltype" = ''     ] && printf 'selected="selected"')" "$(l10n teltype)" \
+                  "$([ "$teltype" = 'HOME' ] && printf 'selected="selected"')" "$(l10n TYPE=HOME)" \
+                  "$([ "$teltype" = 'WORK' ] && printf 'selected="selected"')" "$(l10n TYPE=WORK)" \
+                  "$([ "$teltype" = 'CELL' ] && printf 'selected="selected"')" "$(l10n TYPE=CELL)" \
+                  "$([ "$teltype" = 'FAX'  ] && printf 'selected="selected"')" "$(l10n TYPE=FAX)"
+
+          printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \
+            "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)" "$(l10n "$item")"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+      *)printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \
+            "$item" "$item" "$(pdi_value "$card" "$item" $c |unescape |HTML)" "$(l10n "$item")"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+    esac
+  done
+}
similarity index 71%
rename from actions/edit_categories.sh
rename to categories/edit_categories.sh
index 2c34d264d224e4d0ec65369eb5de4ddc3786dbb9..2eef152ad851126578dd73a85e0d4be689ae3653 100755 (executable)
 
 catfile="${_DATA}/mappings/categories"
 
-remove="${_POST[remove]}"
-newcat="${_POST[newcat]}"
+remove="$(POST remove)"
+newcat="$(POST newcat)"
 
-if [ "${_POST[add]}" = "add" ]; then
-  printf %s\\n "$newcat" >>"$catfile"
-elif [ -n "$remove" ]; then
-  sed -ri '/^'"${remove}"'$/d' $catfile
+if [ "$(POST add)" = "add" ]; then
+  categories="$( {
+    cat "$catfile"
+    printf %s\\n "$newcat"
+  } |sort -u )"
+  printf %s\\n "$categories" >"$catfile"
+elif [ "$remove" ]; then
+  sed -E -i '/^'"${remove}"'$/d' "$catfile"
 fi
 
-redirect "?p=categories"
+REDIRECT "${_BASE}/categories/"
diff --git a/categories/index.cgi b/categories/index.cgi
new file mode 100755 (executable)
index 0000000..236b1e3
--- /dev/null
@@ -0,0 +1,71 @@
+#!/bin/sh
+# Copyright 2015, 2017, 2018, 2021 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. $_EXEC/pdiread.sh
+. $_EXEC/categories/l10n.sh
+
+catfile="${_DATA}/mappings/categories"
+
+list_categories() {
+  grep -vxE '[  ]*' "$catfile" |sort -u
+}
+
+list_catsel(){
+  local vcf="$1" card="$2" n=1 cats="${BR}"
+  while cats="${cats}${BR}$(pdi_value "$vcf" CATEGORIES $n)"; do n=$((n + 1)); done
+
+  list_categories |while read cat; do
+    printf '[li [label [input %s type="checkbox" name="%s" value="%s"] %s]]' \
+          "$([ "${cats%*${BR}${cat}${BR}*}" != "$cats" ] && printf checked=checked)" \
+           "$(HTML "$card")" "$(HTML "$cat")" "$(HTML "$cat")" 
+  done
+}
+
+cat <<EOF |yield_page categories
+[form .categories action="edit_categories.sh" method="POST"
+  [h1 $(l10n categories_label)]
+  [input type="hidden" name="page" value="categories"]
+  [ul
+    $(list_categories | while read cat; do
+      printf '[li . %s [button type="submit" name="remove" value="%s" . %s]]\n' \
+             "$(HTML "$cat")" "$(HTML "$cat")" "$(l10n cat_remove)"
+    done)
+    [li
+      [input type="text" name="newcat" placeholder="$(l10n cat_newlabel)"]
+      [button type="submit" name="add" value="add" . $(l10n cat_add)]
+    ]
+  ]
+]
+
+[form .namelist action="update_categories.sh" method="POST"
+  [fieldset
+    [button type="submit" name="submit" value="submit" . $(l10n cat_update)]
+  ]
+  [ul .namelist
+  $(for vcffile in "$_DATA"/vcard/*vcf; do
+    vcf="$(pdi_load "$vcffile")"
+    printf '    [li [h2 . %s][ul ' "$(pdi_value "$vcf" FN)"
+    list_catsel "$vcf" "${vcffile##*/}"
+    printf ']]\n'
+  done |sort)
+  ]
+  [fieldset
+    [button type="submit" name="submit" value="submit" . $(l10n cat_update)]
+  ]
+]
+EOF
similarity index 63%
rename from actions/export_ical.sh
rename to categories/l10n.sh
index e595cb68fb9a4d8af629de657f4cf0376bd9ae44..c0e64216e48838cb5cc8b2e56ee7b50470910ed8 100755 (executable)
@@ -1,6 +1,4 @@
-#!/bin/zsh
-
-# Copyright 2014,2015 Paul Hänsch
+# Copyright 2014, 2015, 2016, 2019, 2021 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-echo -n "Content-Type: text/calendar;charset=utf-8\n\n"
-course="${_GET[course]}"
-cat "$_DATA/ical/$course"
+l10n(){
+  local word
+  [ $# -eq 0 ] && read -r word || word="$*"
+  case $word in
+    cat_remove) printf %s "&#45;";;
+    cat_add) printf %s "+";;
+    cat_newlabel) printf %s "neue Kategorie";;
+    cat_update) printf %s "Zuweisungen übernehmen";;
+    categories_label) printf %s "Kategorien";;
+
+    *) l10n_global "$word";;
+  esac
+}
similarity index 54%
rename from actions/update_categories.sh
rename to categories/update_categories.sh
index c3b2e4c3f71dc9709b16b21322134e27cf31d0ef..a26f93d3818359875df92066edc90ffec3af64d0 100755 (executable)
@@ -1,6 +1,6 @@
-#!/bin/zsh
+#!/bin/sh
 
-# Copyright 2016 Paul Hänsch
+# Copyright 2016, 2021 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-. "$_EXEC"/pages/categories.sh
+. "$_EXEC"/cgilite/storage.sh
+. "$_EXEC"/pdiread.sh
 
 catfile="${_DATA}/mappings/categories"
 
-set_categories(){
-  card="${_DATA}/vcard/$1"
-  cats="$2"
-  cardcats="$(sed -nr 's;^CATEGORIES(\;[^":]+|\;"[^"]+")*:([^\r]+)\r?$;\2;p' "$card")"
+for card in "${_DATA}"/vcard/*.vcf; do
+  n='' postcats='' cardcats=''
+  vcf="$(pdi_load "$card")"
 
-  debug "CARD: $card"
-  debug "CATS: $cardcats"
-  debug "NEW:  $cats"
-  if [ "$cats" != "$cardcats" ]; then
-    sed -ri '
+  n=1; while postcats="${postcats}${postcats:+,}$(POST "${card##*/}" $n)"; do n=$((n+1)); done
+  n=1; while cardcats="${cardcats}${cardcats:+,}$(pdi_value "$vcf" CATEGORIES $n)"; do n=$((n+1)); done
+
+  if [ "${postcats}" != "${cardcats}" ] && LOCK "$card"; then
+    sed -E -i '
       /^CATEGORIES[;:]/d
-      /^END:VCARD *\r?$/iCATEGORIES:'"$cats"'\r
+      /^END;?:VCARD *\r?$/iCATEGORIES:'"${postcats%,}"'\r
     ' "${card}"
+    RELEASE "$card"
   fi
-}
-
-listcards |while read card; do
-  cardcats=''
-  for n in "$card" "$card"{0..100}; do
-    if [ -z "${_POST[$n]}" ]; then
-      set_categories "$card" "$cardcats"
-      break
-    fi
-    cardcats="${cardcats}${cardcats:+,}${_POST[$n]}"
-  done
 done
 
-redirect "?p=categories"
+REDIRECT "${_BASE}/categories/"
diff --git a/cgilite/.gitignore b/cgilite/.gitignore
new file mode 100644 (file)
index 0000000..5c9950a
--- /dev/null
@@ -0,0 +1,3 @@
+cgilite
+serverkey
+users.db
diff --git a/cgilite/cgilite.awk b/cgilite/cgilite.awk
new file mode 100644 (file)
index 0000000..f16ed6a
--- /dev/null
@@ -0,0 +1,158 @@
+#!/bin/env awk -f
+
+function PATH( str,    seg, out ) {
+  while ( str ) {
+    seg = str;
+    sub( /\/.*$/, "", seg);
+    sub( /^[^\/]*\//, "", str);
+    if      ( seg == ".." )  sub(/\/[^\/]*\/?$/, "", out);
+    else if ( seg ~ /^\.?$/) sub(/\/?$/, "/", out);
+    else sub(/\/?$/, "/" seg, out);
+
+    if (seg == str) break;
+  }
+  if (!(str && out)) sub(/\/?$/,"/" out);
+  return out;
+}
+
+function HEX_DECODE( pfx, inp,    out, n, k ) {
+  k = length(pfx);
+  gsub(/[].*+?^${}()|\\[]/,"\\\\&",pfx);
+  while ( inp ) if ( n = match(inp, pfx "[0-9a-fA-F][0-9a-fA-F]") ) {
+    out = out substr(inp, 1, n - 1);
+    inp = substr(inp, n + k);
+    if      (inp ~  /^[0-9]/) n = 16 * substr(inp, 1, 1);
+    else if (inp ~  /^[aA]/)  n = 160;
+    else if (inp ~  /^[bB]/)  n = 176;
+    else if (inp ~  /^[cC]/)  n = 192;
+    else if (inp ~  /^[dD]/)  n = 208;
+    else if (inp ~  /^[eE]/)  n = 224;
+    else if (inp ~  /^[fF]/)  n = 240;
+    if      (inp ~ /^.[0-9]/) n += substr(inp, 2, 1);
+    else if (inp ~ /^.[aA]/)  n += 10;
+    else if (inp ~ /^.[bB]/)  n += 11;
+    else if (inp ~ /^.[cC]/)  n += 12;
+    else if (inp ~ /^.[dD]/)  n += 13;
+    else if (inp ~ /^.[eE]/)  n += 14;
+    else if (inp ~ /^.[fF]/)  n += 15;
+    out = out sprintf("%c", n);
+    inp = substr(inp, 3);
+  } else {
+    out = out inp;
+    break;
+  }
+  return out;
+}
+
+function HTML( text ) {
+  gsub( /&/,  "\\&amp;",  text );
+  gsub( /</,  "\\&lt;",   text );
+  gsub( />/,  "\\&gt;",   text );
+  gsub( /"/,  "\\&quot;", text );
+  gsub( /'/,  "\\&#x27;", text );
+  gsub( /\[/, "\\&#x5B;", text );
+  gsub( /\]/, "\\&#x5D;", text );
+  gsub( /\r/, "\\&#x0D;", text );
+  gsub( /\n/, "\\&#x0A;", text );
+  gsub( /\\/, "\\&#x5C;", text );
+  return text;
+}
+
+function URL( text ) {
+  gsub( /&/,  "%26", text );
+  gsub( /"/,  "%22", text );
+  gsub( /'/,  "%27", text );
+  gsub( /`/,  "%60", text );
+  gsub( /\?/, "%3F", text );
+  gsub( /#/,  "%23", text );
+  gsub( /\[/, "%5B", text );
+  gsub( /\]/, "%5D", text );
+  gsub( / /,  "%20", text );
+  gsub( /\t/, "%09", text );
+  gsub( /\r/, "%0D", text );
+  gsub( /\n/, "%0A", text );
+  gsub( /%/,  "%25", text );
+  gsub( /\\/, "%5C", text );
+  return text;
+}
+
+function _cgilite_urldecode( str, arr, spl,   form, k, n, key) {
+  if (! spl) spl="&"
+  split(str, form, spl);
+  for ( k in form ) {
+    key = form[k]; sub(/=.*$/, "", key);
+    sub(/^[^=]*=/, "", form[k]);
+    if ( key in arr ) {
+      n = 1; while ( (key, n) in arr ) n++;
+      arr[key,n] = HEX_DECODE( "%", form[k]);
+    } else {
+      arr[key] = HEX_DECODE( "%", form[k]);
+    }
+  }
+}
+
+function _cgilite_request(    key, val) {
+  # Read request from client connection
+
+  # Read Headers
+  getline; REQUEST_METHOD = $1; REQUEST_URI = $2; SERVER_PROTOCOL = $3;
+  while ( getline ) {
+    if ($0 ~ /^\r?$/) break;
+    else if ($0 ~ /^[a-zA-Z][0-9a-zA-Z_-]+: .*/) {
+      key = toupper($0);
+      sub(/:.*$/, "", key);
+      gsub(/-/, "_", key);
+      _HEADER[key] = $0;
+      sub(/^[^:]:[\t ]*/, "", _HEADER[key]);
+      sub(/[\t ]*\r?$/, "", _HEADER[key]);
+    }
+  }
+  CONTENT_LENGTH = _HEADER["CONTENT_LENGTH"];
+  CONTENT_TYPE   = _HEADER["CONTENT_TYPE"];
+
+  PATH_INFO = REQUEST_URI; gsub(/\?.*$/, "", PATH_INFO)
+  PATH_INFO = PATH( HEX_DECODE( "%", PATH_INFO ) );
+  QUERY_STRING = REQUEST_URI;
+  if ( !gsub(/^[^?]+\?/, "", QUERY_STRING) ) QUERY_STRING = "";
+
+  # Set up _GET[]-Array
+  _cgilite_urldecode(QUERY_STRING, _GET);
+
+  if ( _HEADER["CONTENT_TYPE"] == "application/x-www-form-urlencoded" \
+       && _HEADER["CONTENT_LENGTH"] ) {
+    # Set up _POST[]-Array
+
+    val = ""; key = "head -c " _HEADER["CONTENT_LENGTH"];
+    while (key |getline) val = val $0; close(key);
+    _cgilite_urldecode(val, _POST);
+  }
+
+  if ( _HEADER["COOKIE"] ) {
+    # Set up _COOKIE[]-Array
+    _cgilite_urldecode(_HEADER["COOKIE"], _COOKIE, "; ?");
+  }
+
+  if ( _HEADER["REFERER"] ) {
+    key = HEADER["REFERER"];
+    if (! sub(/^[^\?]+?/, "", key)) key = ""
+    _cgilite_urldecode(key, _REF);
+  }
+
+}
+
+function _cgilite_headers() {
+  # Import request data from webserver environment variables
+}
+
+BEGIN {
+  REQUEST_METHOD=""; REQUEST_URI=""; SERVER_PROTOCOL="";
+  PATH_INFO=""; QUERY_STRING=""; CONTENT_LENGTH=""; CONTENT_TYPE="";
+  split("", _GET); split("", _POST); split("", _REF);
+  split("", _HEADER); split("", _COOKIE);
+
+  if ( ENVIRON["REQUEST_METHOD"] )
+    _cgilite_headers();
+  else
+    _cgilite_request();
+}
diff --git a/cgilite/cgilite.sh b/cgilite/cgilite.sh
new file mode 100755 (executable)
index 0000000..b2467c3
--- /dev/null
@@ -0,0 +1,356 @@
+#!/bin/sh
+
+# This is CGIlite.
+# A collection of posix shell functions for writing CGI scripts.
+
+# Copyright 2017 - 2023 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+[ -n "$include_cgilite" ] && return 0
+# guard set after webserver part
+
+# ksh and zsh workaround
+# set -o posix # ksh, not portable
+setopt -o OCTAL_ZEROES 2>&-
+
+# Integrated webserver request timeout
+cgilite_timeout=2
+
+# General environment variables
+# $_EXEC - directory containing application itself
+# $_DATA - direcotry where application data may be stored
+# $_BASE - optional prefix for http path, e.g. "/myapp"
+#
+# Programmers should take care to use those variables throughout the
+# application.
+# Variables may be set via CLI argument, in environment, or left as default.
+
+for cgilite_arg in "$@"; do case $cgilite_arg in
+  --exec=*) _EXEC="${cgilite_arg#*=}";;
+  --data=*) _DATA="${cgilite_arg#*=}";;
+  --base=*) _BASE="${cgilite_arg#*=}";;
+esac; done
+unset cgilite_arg
+
+_EXEC="${_EXEC:-${0%/*}}"
+_DATA="${_DATA:-.}"
+_EXEC="${_EXEC%/}" _DATA="${_DATA%/}" _BASE="${_BASE%/}"
+
+export _EXEC _DATA _BASE
+
+# Carriage Return and Line Break characters for convenience
+CR="\r"
+BR='
+'
+
+PATH(){ 
+  local str seg out
+  # normalize path
+  # read from stdin if no arguments are provided
+
+  [ $# -eq 0 ] && str="$(cat)" || str="$*"
+  while [ "$str" ]; do
+    seg=${str%%/*}; str="${str#*/}"
+    case $seg in
+      ..) out="${out%/}"; out="${out%/*}/";;
+    .|'') out="${out%/}/";;
+       *) out="${out%/}/${seg}";;
+    esac;
+    [ "$seg" = "$str" ] && break
+  done
+  [ "${str}" -a "${out}" ] && printf %s "$out" || printf %s/ "${out%/}"
+}
+
+HEX_DECODE(){
+  local pfx="$1" in="$2" out
+  # Print out Data encoded as Hex
+  #
+  # Arguments:
+  # pfx - required, prefix for a hex tupel, e.g. "\x", "%" "\", may be empty
+  # in - required, string to be decoded
+  #
+  # anything that does not constitute a tupel of valid Hex numerals
+  # will be copied to the output literally
+
+  while [ "$in" ]; do
+    [ "$pfx" ] || case $in in
+      [0-9a-fA-F][0-9a-fA-F]*):;;
+      ?*) out="${out}${in%%"${in#?}"}"
+          in="${in#?}"; continue;;
+    esac
+
+    case $in in
+      "$pfx"[0-9a-fA-F][0-9a-fA-F]*) in="${in#"${pfx}"}";;
+      \\*) in="${in#?}"; out="${out}\\\\"; continue;;
+       %*) in="${in#?}"; out="${out}%%";  continue;;
+        *) att="${in%%"${pfx}"*}"; att="${att%%%*}"; att="${att%%\\*}"
+           out="${out}${att}"; in="${in#"${att}"}"; continue;;
+    esac;
+
+    # Hex escapes for printf (e.g. \x41) are not portable 
+    # The portable way for Hex output is transforming Hex to Octal
+    # (e.g. \x41 = \101)
+    case $in in
+        [0123]?*) out="${out}\\0";;
+        [4567]?*) out="${out}\\1";;
+      [89aAbB]?*) out="${out}\\2";;
+      [c-fC-F]?*) out="${out}\\3";;
+    esac
+    case $in in
+            [048cC][0-7]*) out="${out}0";;
+       [048cC][89a-fA-F]*) out="${out}1";;
+            [159dD][0-7]*) out="${out}2";;
+       [159dD][89a-fA-F]*) out="${out}3";;
+           [26aAeE][0-7]*) out="${out}4";;
+      [26aAeE][89a-fA-F]*) out="${out}5";;
+           [37bBfF][0-7]*) out="${out}6";;
+      [37bBfF][89a-fA-F]*) out="${out}7";;
+    esac
+    case $in in
+       ?[08]*) out="${out}0";;
+       ?[19]*) out="${out}1";;
+      ?[2aA]*) out="${out}2";;
+      ?[3bB]*) out="${out}3";;
+      ?[4cC]*) out="${out}4";;
+      ?[5dD]*) out="${out}5";;
+      ?[6eE]*) out="${out}6";;
+      ?[7fF]*) out="${out}7";;
+    esac
+    in="${in#?}"
+    in="${in#?}"
+  done
+  printf -- "$out"
+}
+
+if [ -z "$REQUEST_METHOD" ]; then
+  # no webserver variables means we are running via inetd / ncat
+  # so use builtin web server
+
+  # Use env from inetd as webserver variables
+  REMOTE_ADDR="${TCPREMOTEIP}"
+  SERVER_NAME="${TCPLOCALIP}"
+  SERVER_PORT="${TCPLOCALPORT}"
+
+  # Wait 2 seconds for request or kill connection through watchdog.
+  # Once Request is received the watchdog will be suspended (killed).
+  # At the end of the loop the watchdog will be restarted to enable
+  # timeout for the subsequent request.
+
+  (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$!
+  while read REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL; do
+    unset PATH_INFO QUERY_STRING cgilite_headers CONTENT_LENGTH CONTENT_TYPE
+
+    [ "${SERVER_PROTOCOL#HTTP/1.[01]${CR}}" ] && break
+    kill $cgilite_watchdog
+
+    SERVER_PROTOCOL="${SERVER_PROTOCOL%${CR}}"
+    PATH_INFO="$(HEX_DECODE % "${REQUEST_URI%\?*}" |PATH)"
+    [ "${REQUEST_URI}" = "${REQUEST_URI#*\?}" ] \
+    && QUERY_STRING='' \
+    || QUERY_STRING="${REQUEST_URI#*\?}"
+    while read -r hl; do
+      hl="${hl%${CR}}"; [ "$hl" ] || break
+      case $hl in
+        'Content-Length: '*) CONTENT_LENGTH="${hl#*: }";;
+          'Content-Type: '*)   CONTENT_TYPE="${hl#*: }";;
+      esac
+      cgilite_headers="${cgilite_headers}${hl}${BR}"
+    done
+
+    export REMOTE_ADDR SERVER_NAME SERVER_PORT REQUEST_METHOD REQUEST_URI SERVER_PROTOCOL \
+           PATH_INFO QUERY_STRING CONTENT_TYPE CONTENT_LENGTH cgilite_headers
+
+    # Try to serve multiple requests, provided that script serves a
+    # Content-Length header.
+    # Without Content-Length header, connection will terminate after
+    # script.
+
+    cgilite_status='200 OK'; cgilite_response=''; cgilite_cl="Connection: close${CR}${BR}";
+    . "$0" | while read -r l; do case $l in
+      Status:*)
+        cgilite_status="${l#Status: }";;
+      Content-Length:*)
+        cgilite_cl=""
+        cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";;
+      Connection:*)
+        cgilite_cl="${l}${BR}";;
+      $CR) printf '%s %s\r\n%s%s\r\n' \
+             'HTTP/1.1' "${cgilite_status%${CR}}" \
+             "${cgilite_response}${cgilite_response:+${BR}}" "${cgilite_cl}"
+           cat || kill $$
+           [ "${cgilite_cl#Connection}" = "${cgilite_cl}" ]; exit;;
+      *) cgilite_response="${cgilite_response:+${cgilite_response}${BR}}${l}";;
+    esac; done || exit 0;
+    (sleep $cgilite_timeout && kill $$) & cgilite_watchdog=$!
+  done
+  kill $cgilite_watchdog
+  exit 0
+fi
+
+include_cgilite="$0"
+
+if [ "${REQUEST_METHOD}" = POST -a "${CONTENT_LENGTH:-0}" -gt 0 -a \
+     "${CONTENT_TYPE}" = "application/x-www-form-urlencoded" ]; then
+  cgilite_post="$(head -c "$CONTENT_LENGTH")"
+fi
+
+PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")"
+
+debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; }
+[ "${DEBUG+x}" ] && env >&2
+
+# general helper functions, see GET, POST, and REF below
+
+cgilite_count(){
+  printf %s "&$1" \
+  | grep -oE '&'"$2"'=[^&]*' \
+  | wc -l
+}
+
+cgilite_value(){
+  local str="&$1" name="$2" cnt="${3:-1}"
+  while [ $cnt -gt 0 ]; do
+    [ "${str}" = "${str#*&${name}=}" ] && return 1
+    str="${str#*&${name}=}"
+    cnt=$((cnt - 1))
+  done
+  HEX_DECODE % "$(printf %s "${str%%&*}" |tr + \  )"
+}
+
+cgilite_keys(){
+  local str="&$1"
+  while [ "${str#*&}" != "${str}" ]; do
+    str="${str#*&}"
+    printf '%s\n' "${str%%=*}"
+  done \
+  | sort -u
+}
+
+# Read arguments from GET, POST, or the query string of the referrer (REF).
+# Example:
+# GET varname n
+#
+# where n is number for the Nth occurence of a variable and defaults to 1
+#
+# *_COUNT varname
+# -> returns number of ocurences
+# *_KEYS
+# -> returns list of available varnames
+
+GET(){ cgilite_value "${QUERY_STRING}" "$@"; }
+GET_COUNT(){ cgilite_count "${QUERY_STRING}" $1; }
+GET_KEYS(){ cgilite_keys "${QUERY_STRING}"; }
+
+POST(){ cgilite_value "${cgilite_post}" "$@"; }
+POST_COUNT(){ cgilite_count "${cgilite_post}" $1; }
+POST_KEYS(){ cgilite_keys "${cgilite_post}"; }
+
+REF(){ cgilite_value "${HTTP_REFERER#*\?}" "$@"; }
+REF_COUNT(){ cgilite_count "${HTTP_REFERER#*\?}" $1; }
+REF_KEYS(){ cgilite_keys "${HTTP_REFERER#*\?}"; }
+
+HEADER(){
+  # Read value of header line. Use this instead of
+  # referencing HTTP_* environment variables.
+  if [ -n "${cgilite_headers+x}" ]; then
+    local str="${BR}${cgilite_headers}"
+    [ "${str}" = "${str#*${BR}${1}: }" ] && return 1
+    str="${str#*${BR}${1}: }"
+    printf %s "${str%%${BR}*}"
+  else
+    local var="HTTP_$(printf %s "$1" |tr a-z- A-Z_)"
+    eval "[ \"\$$var\" ] && printf %s \"\$$var\" || return 1"
+    # eval "printf %s \"\$HTTP_$(printf %s "${1}" |tr a-z A-Z |tr -c A-Z _)\""
+  fi
+}
+
+COOKIE(){
+  # Read value of cookie
+  HEX_DECODE % "$(
+    HEADER Cookie \
+    | grep -oE '(^|; ?)'"$1"'=[^;]*' \
+    | sed -En "${2:-1}"'{s;^[^=]+=;;; s;\+; ;g; p;}'
+  )"
+}
+
+HTML(){
+  # Escape HTML cahracters
+  # Also escape [, ], and \n for use in html-sh
+  local str out
+  [ $# -eq 0 ] && str="$(cat)" || str="$*"
+  while [ "$str" ]; do case $str in
+    \&*) out="${out}&amp;";       str="${str#?}";;
+    \<*) out="${out}&lt;";        str="${str#?}";;
+    \>*) out="${out}&gt;";        str="${str#?}";;
+    \"*) out="${out}&quot;";      str="${str#?}";;
+    \'*) out="${out}&#x27;";      str="${str#?}";;
+    \[*) out="${out}&#x5B;";      str="${str#?}";;
+    \]*) out="${out}&#x5D;";      str="${str#?}";;
+    "${CR}"*) out="${out}&#x0D;"; str="${str#?}";;
+    "${BR}"*) out="${out}&#x0A;"; str="${str#?}";;
+    *) out="${out}${str%%[]&<>\"\'${CR}${BR}[]*}"; str="${str#"${str%%[]&<>\"\'${CR}${BR}[]*}"}";;
+  esac; done
+  printf %s "$out"
+}
+
+URL(){
+  # Escape pathes, so they can be used in link tags and HTTP Headers
+  local str out
+  [ $# -eq 0 ] && str="$(cat)" || str="$*"
+  while [ "$str" ]; do case $str in
+    \&*) out="${out}%26"; str="${str#?}";;
+    \"*) out="${out}%22"; str="${str#?}";;
+    \'*) out="${out}%27"; str="${str#?}";;
+    \`*) out="${out}%60"; str="${str#?}";;
+    \?*) out="${out}%3F"; str="${str#?}";;
+    \#*) out="${out}%23"; str="${str#?}";;
+    \[*) out="${out}%5B"; str="${str#?}";;
+    \]*) out="${out}%5D"; str="${str#?}";;
+    \ *) out="${out}%20"; str="${str#?}";;
+    "  "*) out="${out}%09"; str="${str#?}";;
+    "${CR}"*) out="${out}%0D"; str="${str#?}";;
+    "${BR}"*) out="${out}%0A"; str="${str#?}";;
+    %*) out="${out}%25"; str="${str#?}";;
+    *) out="${out}${str%%[]&\"\'\?#    ${CR}${BR}%[]*}"; str="${str#"${str%%[]&\"\'\?#         ${CR}${BR}%[]*}"}";;
+  esac; done
+  printf %s "$out"
+}
+
+SET_COOKIE(){
+  # Param: session | +seconds | [date]
+  # Param: name=value
+  # Param: Path= | Domain= | Secure
+  local expire cookie
+  case "$1" in
+    ''|0|session) expire='';;
+    [+-][0-9]*)   expire="$(date -R -d @$(($(date +%s) + $1)))";;
+    *)            expire="$(date -R -d "$1")";;
+  esac
+  cookie="$2"
+
+  printf 'Set-Cookie: %s; HttpOnly; SameSite=Lax' "$cookie"
+  [ -n "$expire" ] && printf '; Expires=%s' "${expire%+????}${expire:+GMT}"
+  [ $# -ge 3 ] && shift 2 && printf '; %s' "$@"
+  printf '\r\n'
+}
+
+REDIRECT(){
+  # Trigger redirct and terminate script
+  printf '%s: %s\r\n' \
+    Status "303 See Other" \
+    Content-Length 0 \
+    Location "$*"
+  printf '\r\n'
+  exit 0
+}
diff --git a/cgilite/common.css b/cgilite/common.css
new file mode 100644 (file)
index 0000000..30c3942
--- /dev/null
@@ -0,0 +1,191 @@
+/* ======= GENERIC HTML STYLES ======= */
+
+* {
+  box-sizing: border-box;
+  position: relative;
+  font: inherit;
+  text-decoration: inherit;
+  color: inherit; background: transparent;
+  max-width: 100%;
+  margin: 0; padding: 0;
+  border: none;
+}
+
+body {
+  font: normal normal normal medium/1.5em sans-serif;
+  color: #000; background: #FFF;
+}
+
+ul, ol, dl, table, pre, p { margin-bottom: .5em; }
+p:only-child { margin-bottom: 0; }
+
+table {
+  max-width: 100%;
+  overflow-x: auto;
+}
+th, td { padding: .25em .75em; }
+
+a {
+  font-style: italic;
+  text-decoration: underline;
+  color: #068;
+  word-break: break-word;
+}
+a.button {
+  font-style: inherit;
+  text-decoration: inherit;
+  color: inherit;
+}
+
+sup { vertical-align: super; }
+sub { vertical-align: sub; }
+small { font-size: .75em; }
+big   { font-size: 1.25em; }
+strike, del, s { text-decoration: line-through; }
+u {text-decoration: underline; }
+i, em { font-style: italic; }
+b, strong { font-weight: bolder; }
+tt, code, var, samp, kbd { font-family: monospace; }
+kbd { font-style: italic; }
+
+blockquote {
+  background-color: #EEE;
+  margin: .5em 0;
+  padding: 1em 2em;
+  white-space: pre-line;
+}
+
+ul, ol { padding-left: 1.5em; }
+dl dt { font-weight: bolder; }
+dl dd {
+  margin: 0 2em;
+  background-color: #EEE;
+}
+table th { font-weight: bold; }
+
+li p + ul, li p + ol {
+  margin-top: -.25em;
+}
+
+hr { border-bottom: 1pt solid; }
+
+h1, h2, h3 {
+  font-weight: bold;
+  margin-top: .75em;
+  margin-bottom: .5em;
+}
+
+h4, h5, h6, form legend {
+  font-weight: bolder;
+  margin-bottom: .25em;
+}
+
+h1 {
+  text-align: center;
+  font-size: 1.5em;
+}
+h2 { font-size: 1.125em; }
+
+select, input, button, textarea, a.button {
+  display: inline-block;
+  color: #000; background-color: #FFF;
+  border: .5pt solid;
+  padding: .25em .75em;
+  vertical-align: text-bottom;
+  border: .5pt solid #000;
+  border-radius: 2pt;
+}
+select { padding: .375em 0; }
+textarea { min-height: 7em; }
+
+input[type=radio], input[type=checkbox] {
+  vertical-align: baseline;
+}
+input[type=number] { text-align: right; padding-right: 0; }
+
+button, input[type=button], a.button {
+  box-shadow: .125em .125em .25em;
+  cursor: pointer;
+}
+input[type=radio], input[type=checkbox], label[for] {
+  cursor: pointer;
+}
+
+label { margin-right: .75em; }
+input + label {
+  margin-left: .375em;
+}
+
+input[type="search"] + button.search,
+input.search + button.search {
+  width: 2.5em;
+  color: transparent;
+  background-color: #CCC;
+  margin-left: -2pt;
+  border-left: none;
+  border-radius: 0 2pt 2pt 0;
+  white-space: nowrap;
+  overflow: hidden;
+}
+input[type="search"] + button.search:before,
+input.search + button.search:before {
+  content: '\1f50d';
+  color: #000;
+  font-weight: bold;
+}
+
+@media print {
+  @page { margin: 20mm; }
+
+  h1, h2, h3, h4, h5, h6, form legend {
+    page-break-inside: avoid;
+    page-break-after: avoid;
+    page-break-before: auto;
+  }
+  li { page-break-inside: avoid; }
+  th, dt { page-break-after: avoid; }
+}
+
+/* ======= End Generic Styles ======= */
+
+/* ======= Common Styles ======= */
+
+*[tooltip]:hover:after {
+  display: block;
+  position: absolute;
+  min-width: 12em;
+  bottom: 100%; left: 50%; transform: translate(-50%, 0);
+  content: attr(tooltip);
+  padding: .5em;
+  color: #000; background-color: #FFC;
+  border: .5pt solid;
+  z-index: 1;
+}
+
+input[type=radio].tab { display: none; }
+input[type=radio].tab + label {
+  display: table-cell;
+  padding: .5em 1em;
+  color: #000; background-color: #EEE;
+  border: .5pt solid;
+}
+input[type=radio].tab:checked + label {
+  background-color: #FFF;
+  border-bottom: none;
+  box-shadow: .125em -.125em .125em #888;
+  z-index: 1;
+}
+input[type=radio].tab ~ *.tab {
+  display: none;
+  width: 100%;
+  margin-top: -.5pt; padding: .25em .75em;
+  border: .5pt solid;
+  border-radius: 0;
+  box-shadow: .125em .125em .125em #888;
+}
+
+/* Markdown line-block */
+.line-block { white-space: pre-wrap; }
+.line-block br { display: none; }
+
+/* ======= End Common Styles ======= */
diff --git a/cgilite/db23.sh b/cgilite/db23.sh
new file mode 100755 (executable)
index 0000000..e8a0d64
--- /dev/null
@@ -0,0 +1,114 @@
+#!/bin/sh
+
+# Copyright 2023, 2024 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+[ -n "$include_db23" ] && return 0
+include_db23="$0"
+
+. "${_EXEC:-.}/cgilite/storage.sh"
+
+DB2() {
+  local call data file key val seq
+  data="${BR}${1}${BR}" call="$2"
+  shift 2
+
+  case $call in
+    new|discard)
+      printf ''
+      ;;
+    open|load) file="$1"
+      cat "$file" || return 1
+      ;;
+    check|contains) key="$(STRING "$1")" val=''
+      val="${data##*"${BR}${key}"      }" val="${val%%"${BR}"*}"
+      [ "$val" = '' ] && return 1
+      ;;
+    count) key="$(STRING "$1")" val='' seq=0
+      val="${data##*"${BR}${key}"      }" val="${val%%"${BR}"*}"
+      [ "$val" = '' ] || val="${val}   "
+      while [ "$val" != '' ]; do
+        seq=$((seq + 1)) val="${val#*  }"
+      done
+      printf "%i\n" "$seq"
+      [ $seq = 0 ] && return 1
+      ;;
+    get) key="$(STRING "$1")" seq="${2:-1}"
+      val="${data##*"${BR}${key}"      }" val="${val%%"${BR}"*}"
+      [ "$val" = '' ] && return 1 || val="${val}       "
+      while [ $seq -gt 1 ]; do
+        seq=$((seq - 1)) val="${val#*  }"
+      done
+      [ "$val" = '' ] && return 1
+      UNSTRING "${val%%        *}"
+      ;;
+    iterate|raw) key="$(STRING "$1")"
+      val="${data##*"${BR}${key}"      }" val="${val%%"${BR}"*}"
+      [ "$val" = '' ] && return 1
+      printf '%s\n' $val
+      ;;
+    delete|remove) key="$(STRING "$1")"
+      val="${data#*"${BR}${key}"       *"${BR}"}"
+      key="${data%"${BR}${key}"        *"${BR}"*}"
+      [ "${key}${BR}${val}" = "${data}" ] && return 1
+      printf '%s' "${key#"${BR}"}${BR}${val%"${BR}"}"
+      ;;
+    set|store) key="$(STRING "$1")" val=""
+      shift 1
+      val="$(for v in "$@"; do STRING "$v"; printf \\t; done)"
+      if [ "${data#*"${BR}${key}"      *}" != "$data" ]; then
+        data="${data%"${BR}${key}"     *"${BR}"*}${BR}${key}   ${val%  }${BR}${data#*"${BR}${key}"     *"${BR}"}"
+        data="${data#"${BR}"}" data="${data%"${BR}"}"
+      else
+        data="${data#"${BR}"}${key}    ${val%  }${BR}"
+        data="${data#"${BR}"}"
+      fi
+      printf %s\\n "${data}"
+      ;;
+    append) key="$(STRING "$1")" val=""
+      val="${data##*"${BR}${key}"      }" val="${val%%"${BR}"*}"
+      if [ "$val" = '' ]; then
+        printf %s\\n "${data}"
+        return 1
+      else
+        shift 1
+        val="${val}$(for v in "$@"; do printf \\t; STRING "$v"; done)"
+        data="${data%"${BR}${key}"     *"${BR}"*}${BR}${key}   ${val%  }${BR}${data#*"${BR}${key}"     *"${BR}"}"
+        data="${data#"${BR}"}" data="${data%"${BR}"}"
+        printf %s\\n "${data}"
+      fi
+      ;;
+    flush|save|write) file="$1"
+      data="${data#"${BR}"}" data="${data%"${BR}"}"
+      printf '%s\n' "$data" >"$file" || return 1
+      ;;
+  esac
+  return 0
+}
+
+DB3() {
+  # wrapper function that allows easyer use of DB2
+  # by always keeping file data in $db3_data
+
+  case "$1" in
+    new|discard|open|load|delete|remove|set|store|append)
+      db3_data="$(DB2 "$db3_data" "$@")"
+      return "$?"
+      ;;
+    get|count|check|contains|iterate|raw|flush|save|write)
+      DB2 "$db3_data" "$@"
+      return "$?"
+      ;;
+  esac
+}
diff --git a/cgilite/file.sh b/cgilite/file.sh
new file mode 100755 (executable)
index 0000000..c66b17d
--- /dev/null
@@ -0,0 +1,144 @@
+#!/bin/sh
+
+# Copyright 2016 - 2024 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+[ -n "$include_fileserve" ] && return 0
+include_fileserve="$0"
+
+file_type(){
+  case ${1##*.} in
+    css)       printf 'text/css';;
+    gif)       printf 'image/gif';;
+    html|html) printf 'text/html';;
+    jpg|jpeg)  printf 'image/jpeg';;
+    js)        printf 'text/javascript';;
+    m3u8)      printf 'application/x-mpegURL';;
+    m4a)       printf 'audio/mp4';;
+    m4s)       printf 'video/iso.segment';;
+    m4v|mp4)   printf 'video/mp4';;
+    mpd)       printf 'application/dash+xml';;
+    ogg)       printf 'audio/ogg';;
+    pdf)       printf 'application/pdf';;
+    png)       printf 'image/png';;
+    sh)        printf 'text/x-shellscript';;
+    svg)       printf 'image/svg+xml';;
+    tex)       printf 'text/x-tex';;
+    txt)       printf 'text/plain';;
+    short)     printf 'text/prs.shorthand';;
+    ts)        printf 'video/MP2T';;
+    webm)      printf 'video/webm';;
+    xml)       printf 'application/xml';;
+    *)         printf 'application/octet-stream';;
+  esac
+}
+
+FILE(){
+  local file="$1" mime="$2"
+  local file_size file_date http_date cachedate range
+
+  if ! [ -f "$file" ]; then
+    printf 'Content-Length: 0\r\nStatus: 404 Not Found\r\n\r\n'
+    return 0
+  elif ! [ -r "$file" ]; then
+    printf 'Content-Length: 0\r\nStatus: 403 Forbidden\r\n\r\n'
+    return 0
+  fi
+
+  read file_size file_date <<-EOF
+       $(stat -Lc "%s  %Y" "$file")
+       EOF
+  http_date="$(date -ud "@$file_date" +"%a, %d %b %Y %T GMT")"
+
+  [ ! "$HTTP_IF_MODIFIED_SINCE" -a "$cgilite_headers" ] \
+  &&    HTTP_IF_MODIFIED_SINCE="$(HEADER If-Modified-Since)"
+  [ ! "$HTTP_RANGE"             -a "$cgilite_headers" ] \
+  &&    HTTP_RANGE="$(HEADER Range)"
+
+  cachedate="$(
+    # Parse the allowable date formats from Section 3.3.1 of
+    # https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
+    # HEADER If-Modified-Since \
+    printf %s "$HTTP_IF_MODIFIED_SINCE" \
+    | sed -E 's;^[^ ]+, ([0-9]{2}) (...) ([0-9]{4}) (..:..:..) GMT$;\3-\2-\1 \4;;
+              s;^[^ ]+, ([0-9]{2})-(...)-([789][0-9]) (..:..:..) GMT$;19\3-\2-\1 \4;;
+              s;^[^ ]+, ([0-9]{2})-(...)-([0-6][0-9]) (..:..:..) GMT$;20\3-\2-\1 \4;;
+              s;^[^ ]+ (...) ([0-9]{2}) (..:..:..) ([0-9]{4})$;\4-\1-\2 \3;;
+              s;^[^ ]+ (...)  ([0-9]) (..:..:..) ([0-9]{4})$;\4-\1-\2 \3;;
+              s;Jan;01;; s;Feb;02;; s;Mar;03;; s;Apr;04;; s;May;05;; s;Jun;06;;
+              s;Jul;07;; s;Aug;08;; s;Sep;09;; s;Oct;10;; s;Nov;11;; s;Dec;12;;' \
+    | xargs -r0 date +%s -ud 2>&-
+  )"
+
+  range="${HTTP_RANGE#bytes=}"
+  case "$range" in
+    *[!0-9]*-*|*-*[!0-9]*)
+      range=""
+      ;;
+    *-)
+      range="${range}$((file_size - 1))"
+      ;;
+    -*)
+      [ ${range#-} -le $file_size ] \
+      && range="$((file_size - ${range#-}))-$((file_size - 1))" \
+      || range="0-$((file_size - 1))"
+      ;;
+    *-*)
+      [ ${range#*-} -ge $file_size ] \
+      && range="${range%-*}-$((file_size - 1))"
+      ;;
+    *) range=""
+      ;;
+  esac
+
+  if [ "$file_date" -lt "$cachedate" ] 2>&-; then
+    printf '%s: %s\r\n' \
+      Status '304 Not Modified' \
+      Content-Length 0 \
+      Last-Modified "$http_date"
+    printf '\r\n'
+  
+  elif [ -z "$range" ]; then
+    printf '%s: %s\r\n' \
+      Status "200 OK" \
+      Accept-Ranges bytes \
+      Last-Modified "$http_date" \
+      Content-Type "${mime:-$(file_type "$file")}" \
+      Content-Length $file_size
+    printf '\r\n'
+  
+    [ "$REQUEST_METHOD" != HEAD ] && cat "$file"
+
+  elif [ "${range%-*}" -le "${range#*-}" ]; then
+    printf '%s: %s\r\n' \
+      Status "206 Partial Content" \
+      Accept-Ranges bytes \
+      Last-Modified "$http_date" \
+      Content-Type "${mime:-$(file_type "$file")}" \
+      Content-Range "bytes ${range}/${file_size}" \
+      Content-Length "$((${range#*-} - ${range%-*} + 1))"
+    printf '\r\n'
+  
+    [ "$REQUEST_METHOD" != HEAD ] \
+    && tail -c+$((${range%-*} + 1)) "$file" \
+     | head -c "$((${range#*-} - ${range%-*} + 1))"
+
+  elif [ "${range%-*}" -gt "${range#*-}" ]; then
+    printf '%s: %s\r\n' \
+      Status "216 Range Not Satisfiable" \
+      Content-Length 0 \
+      Content-Range \*/${file_size}
+    printf '\r\n'
+  fi
+}
diff --git a/cgilite/html-sh.sed b/cgilite/html-sh.sed
new file mode 100755 (executable)
index 0000000..1a0f2b4
--- /dev/null
@@ -0,0 +1,83 @@
+#!/bin/sed -nEf
+
+# Copyright 2018 - 2019 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+:Escapes
+s,\\\\,\&#92;,g; s,\\&,\&amp;,g;
+s,\\<,\&lt;,g; s,\\>,\&gt;,g;
+s,\\",\&quot;,g; s,\\',\&apos;,g;
+s,\\\[,\&#91;,g; s,\\\],\&#93;,g;
+s,\\\.,\&#46;,g; s,\\#,\&#35;,g;
+s,\\,,g;
+
+:CommentHandle
+x; /^<\/!-->/{
+  x; /--]/{
+    H; s;^(.*)--].*$;\1-->;p;
+    g; s;^.*--]([^\n]*)$;\1;
+    x; s;^</!-->\n(.*)\n[^\n]*$;\1;; x;
+    bCommentEnd
+  }
+  p; b;
+}
+x;
+:CommentEnd
+
+:shortcuts
+s;\[hidden[ \t]+"([^"]*)"[ \t]+"([^"]*)";[input type="hidden" name="\1" value="\2";g;
+s;\[checkbox[ \t]+"([^"]*)"[ \t]+"([^"]*)";[input type="checkbox" name="\1" value="\2";g;
+s;\[radio[ \t]+"([^"]*)"[ \t]+"([^"]*)";[input type="radio" name="\1" value="\2";g;
+s;\[submit[ \t]+"([^"]*)"[ \t]+"([^"]*)";[button type="submit" name="\1" value="\2";g;
+s;\[a[ \t]+"([^"]*)";[a href="\1";g;
+s;\[img[ \t]+"([^"]*)"[ \t]+"([^"]*)";[img src="\1" alt="\2";g;
+
+s;\[!([^]\[]*)\];<!\1>;g;
+s;\[!--([^]\[]*)--\];<!--\1-->;g;
+
+:tags
+s;\[([^]\[< \t]+)([^]\[]*)\];<\1>\2</\1>;g;
+t tags;
+
+G;
+:tagclose
+s;^([^]\n]*)\]([^\n]*)\n([^\n]+);\1\3\2;
+t tagclose;
+h; s;^([^\n]*)\n;;; x; s;\n.*$;;;
+
+:tagopen
+s;^([^\[\n]*)\[([^]\[< \t\n]+)([^\n]*);\1<\2>\3\n</\2>;
+t tagopen;
+G; h; s;^[^\n]*\n+;;; x; s;\n.*$;;;
+
+:attribs
+s;class="([^>]+)>[ \t]*\.([^< \t]+);class="\2 \1>;g; t attribs;
+s;(<[^/][^>]*)>[ \t]*\.([^< \t]+);\1 class="\2">;g;
+s;(<[^/][^>]*)>[ \t]*#([^< \t]+);\1 id="\2">;g;
+s;(<[^/][^>]*)>[ \t]*([^ \t=<]+=("[^"]*"|'[^']*'|[^< \t]*));\1 \2>;g;
+t attribs;
+s;(<input ([^>]+ )?type=(radio|"radio"|'radio')( [^>]+)?)>[ \t]*(checked|selected);\1 checked="checked">;g;
+s;(<input ([^>]+ )?type=(checkbox|"checkbox"|'checkbox')( [^>]+)?)>[ \t]*(checked|selected);\1 checked="checked">;g;
+s;(<option( [^>]+)?)>[ \t]*(checked|selected);\1 selected="selected">;g;
+s;(<select( [^>]+)?)>[ \t]*multiple;\1 multiple="multiple">;g;
+t attribs;
+s;(<[^/][^>]*>)[ \t]*\.[ \t];\1;g;
+
+s;(<[^/][^>]*>)[ \t]*;\1;g;
+# s;(<[^/][^>]*)>[ \t]*</[^>]+>;\1/>;g;
+s;(<(br|hr|img|input|link|meta|area|base|col|command|embed|keygen|param|source|track|wbr)[^>]*)>[ \t]*</\1>;\1>;g;
+
+s;<!-->;<!--;;
+
+p; ${g; s;\n+;;g; p;}
diff --git a/cgilite/json.sh b/cgilite/json.sh
new file mode 100755 (executable)
index 0000000..12afdc4
--- /dev/null
@@ -0,0 +1,360 @@
+#!/bin/sh
+
+[ -n "$include_json" ] && return 0
+include_json="$0"
+
+. "${_EXEC:-.}/cgilite/db23.sh"
+
+# debug(){ [ $# -gt 0 ] && printf '%s\n' "$@" >&2 || tee -a /dev/stderr; }
+
+json_except() {
+  printf '%s\n' "$@" >&2;
+  printf 'Exc: %s\n' "$json_document" >&2
+}
+
+json_space() {
+  while true; do case "$json_document" in
+    [" ${BR}${CR}      "]*) json_document="${json_document#?}";;
+    *) break ;;
+  esac; done
+}
+
+json_string() {
+  local string json_document="$json_document" end=0
+
+  json_space
+  case $json_document in
+    \"*) json_document="${json_document#?}"
+      ;;
+    *) json_except "Expected string specifyer starting with (\")"
+      return 1
+      ;;
+  esac
+  while [ "$json_document" ]; do case $json_document in
+    \\?*)
+      string="${string}${json_document%"${json_document#??}"}"
+      json_document="${json_document#??}"
+      ;;
+    \"*)
+      json_document="${json_document#?}"
+      end=1
+      break
+      ;;
+    *) 
+      string="${string}${json_document%"${json_document#?}"}"
+      json_document="${json_document#?}"
+      ;;
+  esac; done
+
+  if [ $end -eq 0 ]; then
+    json_except "Document ended mid-string"
+    return 1
+  fi
+
+  printf "%s   %s\n" "$(STRING "$string")" "$json_document"
+}
+
+json_key() {
+  local key json_document="$json_document"
+
+  json_space
+  case $json_document in
+    \"*)
+      key="$(json_string)" || return 1
+      json_document="${key#*   }"
+      key="${key%%     *}"
+      ;;
+    *) json_except "Expected key specifyer starting with '\"'"
+      return 1
+      ;;
+  esac
+  json_space
+  case $json_document in
+    :*) json_document="${json_document#?}"
+      ;;
+    *) json_except "Expected value separator \":\""
+      return 1
+      ;;
+  esac
+
+  printf '%s   %s\n' "$key" "$json_document"
+}
+
+json_number() {
+  local number json_document="$json_document"
+
+  json_space
+  number="${json_document%%["  ${BR}${CR} ,}]"]*}"
+  json_document="${json_document#"$number"}"
+  if ! number="$(printf %f "$number")"; then
+    json_except "Invalid number format"
+    return 1
+  fi
+
+  printf '%s   %s\n' "${number%.000000}" "$json_document"
+}
+
+json_array() {
+  local struct="$(DB2 "" new)" value json_document="$json_document"
+
+  json_space
+  case $json_document in
+    "["*) json_document="${json_document#?}"
+      ;;
+    *) json_except "Expected array starting with \"[\""
+      return 1
+      ;;
+  esac
+
+  json_space
+  case $json_document in
+    "]"*)
+      printf "%s       %s\n" "" "${json_document#?}"
+      return 0
+      ;;
+  esac
+
+  while :; do
+    json_space
+
+    value="$(json_value)" || return 1
+    json_document="${value#*   }"
+    value="$(UNSTRING "${value%%       *}")"
+
+       struct="$(DB2 "$struct" append "@" "$value")" \
+    || struct="$(DB2 "$struct" set    "@" "$value")"
+
+    json_space
+    case $json_document in
+      ,*) json_document="${json_document#?}"
+        ;;
+      "]"*) json_document="${json_document#?}"
+        break
+        ;;
+      *) json_except "Unexpected character mid-array"
+        return 1
+        ;;
+    esac
+  done
+
+  printf "%s   %s\n" "$(STRING "$struct")" "$json_document"
+}
+
+json_object() {
+  local struct="$(DB2 "" new)" key value json_document="$json_document"
+
+  json_space
+  case $json_document in
+    "{"*) json_document="${json_document#?}"
+      ;;
+    *) json_except "Expected object starting with \"{\""
+      return 1
+      ;;
+  esac
+
+  json_space
+  case $json_document in
+    "}"*)
+      printf "%s       %s\n" "" "${json_document#?}"
+      return 0
+      ;;
+  esac
+
+  while :; do
+    json_space
+
+    key="$(json_key)" || return 1
+    json_document="${key#*     }"
+    key="$(UNSTRING "${key%%   *}")"
+
+    value="$(json_value)" || return 1
+    json_document="${value#*   }"
+    value="$(UNSTRING "${value%%       *}")"
+
+    struct="$(DB2 "$struct" set "$key" "$value")"
+
+    json_space
+    case $json_document in
+      ,*) json_document="${json_document#?}"
+        ;;
+      "}"*) json_document="${json_document#?}"
+        break
+        ;;
+      *) json_except "Unexpected character mid-object"
+        return 1
+        ;;
+    esac
+  done
+
+  printf "%s   %s\n" "$(STRING "$struct")" "$json_document"
+}
+
+json_value() {
+  local value json_document="$json_document"
+  json_type=""
+
+  json_space
+  case $json_document in
+    \"*)
+      value="$(json_string)" || return 1
+      json_document="${value#* }"
+      value="str:${value%%     *}"
+      json_type=string
+      ;;
+    [+-.0-9]*)
+      value="$(json_number)" || return 1
+      json_document="${value#* }"
+      value="num:${value%%     *}"
+      json_type=number
+      ;;
+    "{"*)
+      value="$(json_object)" || return 1
+      json_document="${value#* }"
+      value="obj:${value%%     *}"
+      json_type=object
+      ;;
+    "["*)
+      value="$(json_array)" || return 1
+      json_document="${value#* }"
+      value="arr:${value%%     *}"
+      json_type=array
+      ;;
+    null*)
+      json_document="${json_document#null}"
+      value="null"
+      json_type=null
+      ;;
+    true*)
+      json_document="${json_document#true}"
+      value="true"
+      json_type=boolean
+      ;;
+    false*)
+      json_document="${json_document#false}"
+      value="false"
+      json_type=boolean
+      ;;
+  esac
+
+  printf "%s   %s\n" "$value" "$json_document"
+}
+
+json_load() {
+  local json_document="$1" json
+
+  json_value |UNSTRING
+}
+
+json_get() {
+  local json="$1" jpath="${2#.}" key idx
+  json_type=''
+
+  case $json in
+    str:*) json_type="string";;
+    arr:*) json_type="array";;
+    obj:*) json_type="object";;
+    num:*) json_type="number";;
+    true|false)
+           json_type="boolean";;
+    null)  json_type="null";;
+  esac
+
+  case $jpath in
+    "")
+      printf %s\\n "${json#???:}"
+      return 0
+      ;;
+    "["[0-9]*"]"*)
+      idx="${jpath%%"]"*}" idx="${idx#"["}"
+      jpath="${jpath#"["*"]"}"
+      ;;
+    "['"*"']"*)
+      key="${jpath%%"']"*}" key="${key#"['"}"
+      jpath="${jpath#"['"*"']"}"
+      ;;
+    "$"*)
+      jpath="${jpath#?}"
+      ;;
+    *) key="${jpath%%[".["]*}"
+      jpath="${jpath#"$key"}"
+      ;;
+  esac
+
+  if   [ "$key" -a "$json_type" = object ]; then
+    if ! json="$(DB2 "${json#obj:}" get "$key")"; then
+      debug "Key not found: \"$key\""
+      return 1
+    fi
+  elif [ "$idx" -a "$json_type" = array ]; then
+    if ! json="$(DB2 "${json#arr:}" get @ "$(( idx + 1 ))")"; then
+      debug "Array index not found: \"$idx\""
+      return 1
+    fi
+  elif [ "$key" ]; then
+    debug "Cannot select key (\"$key\") from value of type \"$json_type\""
+    return 1
+  elif [ "$idx" ]; then
+    debug "Cannot select index ($idx) from value of type \"$json_type\""
+    return 1
+  fi
+  json_get "$json" "$jpath"
+  return $?
+}
+
+json_dump_string() {
+  local in="$1" out=''
+  while [ "$in" ]; do case $in in
+    \\*) out="${out}\\\\"; in="${in#\\}" ;;
+    "$BR"*) out="${out}\\n"; in="${in#${BR}}" ;;
+    "$CR"*) out="${out}\\r"; in="${in#${CR}}" ;;
+    "   "*) out="${out}\\t"; in="${in#  }" ;;
+    \"*) out="${out}\\\""; in="${in#\"}" ;;
+    *) out="${out}${in%%[\\${CR}${BR}  \"]*}"; in="${in#"${in%%[\\${BR}${CR}   \"]*}"}" ;;
+  esac; done
+  printf '"%s"' "${out}"
+}
+
+json_dump_array() {
+  local json="$1" value out=''
+
+  for value in $(DB2 "$json" iterate @); do
+    out="${out},$(json_dump "$(UNSTRING "$value")")"
+  done
+  printf '[%s]' "${out#,}"
+}
+
+json_dump_object() {
+  local json="$1" key value out=''
+
+  while read -r key value; do
+    out="${out},$(json_dump_string "$(UNSTRING "$key")"):$(json_dump "$(UNSTRING "$value")")"
+  done <<-EOF
+       ${json}
+       EOF
+  printf '{%s}' "${out#,}"
+}
+
+json_dump() {
+  local json="$1"
+
+  case $json in
+    str:*)
+      json_dump_string "${json#str:}"
+      ;;
+    arr:*)
+      json_dump_array "${json#arr:}"
+      ;;
+    obj:*)
+      json_dump_object "${json#obj:}"
+      ;;
+    num:*)
+      printf "${json#num:}"
+      ;;
+    true|false|null)
+      printf %s\\n "$json"
+      ;;
+    *)
+      json_dump_string "${json}"
+      ;;
+  esac
+}
diff --git a/cgilite/logging.sh b/cgilite/logging.sh
new file mode 100755 (executable)
index 0000000..31bb24d
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+# LOGLEVEL 1: Crash condition
+# LOGLEVEL 2: Unexpected condition
+# LOGLEVEL 3: Failed action (i.e. due to config error)
+# LOGLEVEL 4: Debug
+
+[ -n "$include_logging" ] && return 0
+include_logging="$0"
+
+LOGLEVEL="${LOGLEVEL:-3}"
+LOGFILE="${LOGFILE:-/dev/stderr}"
+
+logmsg(){
+  local ll="${1:-3}"
+  shift 1
+  if [ "$ll" -le "$LOGLEVEL" -a "$#" -gt 0 ]; then
+    printf %s\\n "$*" >>"${LOGFILE}"
+  elif [ "$ll" -le "$LOGLEVEL" ]; then
+    tee -a "${LOGFILE}"
+  elif [ ! "$#" -gt 0 ]; then
+    cat
+  fi
+}
+
+die(){
+  [ "$#" -gt 0 ] && logmsg 1 "$@"
+  exit 1
+}
+panic(){ logmsg 2 "$@"; }
+error(){ logmsg 3 "$@"; }
+debug(){ logmsg 4 "$@"; }
diff --git a/cgilite/markdown.awk b/cgilite/markdown.awk
new file mode 100755 (executable)
index 0000000..34879d2
--- /dev/null
@@ -0,0 +1,866 @@
+#!/bin/awk -f
+#!/opt/busybox/awk -f
+
+# EXPERIMENTAL Markdown processor with minimal dependencies.
+# Meant to support all features of John Grubers basic Markdown
+# + a number of common extensions, mostly inspired by Pandoc Markdown
+
+# Copyright 2021 - 2023 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# Supported Features / TODO:
+# ==========================
+# [x] done    [ ] todo    [-] not planned    ? unsure
+#
+# Basic Markdown - Block elements:
+# -------------------------------
+# - [x] Paragraphs
+#   - [x] Double space line breaks
+# - [x] Proper block element nesting
+# - [x] Headings
+# - [x] ATX-Style Headings
+# - [x] Blockquotes
+# - [x] Lists (ordered, unordered)
+# - [x] Code blocks (using indention)
+# - [x] Horizontal rules
+# - [x] Verbatim HTML block (disabled by default)
+#
+# Basic Markdown - Inline elements:
+# ---------------------------------
+# - [x] Links
+# - [x] Reference style links
+# - [x] Emphasis *em*/**strong** (*Asterisk*, _Underscore_)
+# - [x] `code`, also ``code containing `backticks` ``
+# - [x] Images / reference style images
+# - [x] <automatic links>
+# - [x] backslash escapes
+# - [x] Verbatim HTML inline (disabled by default)
+# - [x] HTML escaping
+#
+# NOTE: Set the environment variable MD_HTML=true to enable verbatim HTML
+#
+# Extensions - Block elements:
+# ----------------------------
+# - [x] Automatic <section>-wrapping (custom)
+# -  ?  Heading identifiers (php md, pandoc)
+#   - [x] Heading attributes (custom)
+#   - [ ] <hr> terminates section
+# - [x] Automatic heading identifiers (custom)
+# - [x] Fenced code blocks (php md, pandoc)
+#   - [x] Fenced code attributes
+# - [x] Images (as block elements, <figure>-wrapped) (custom)
+#   - [x] reference style block images
+# - [/] Tables
+#   -  ?  Simple table (pandoc)
+#   -  ?  Multiline table (pandoc)
+#   - [x] Grid table (pandoc)
+#     - [x] Headerless
+#   - [x] Pipe table (php md, pandoc)
+# - [x] Line blocks (pandoc)
+# - [x] Task lists (pandoc, custom)
+# - [x] Definition lists (php md, pandoc)
+# - [-] Numbered example lists (pandoc)
+# - [-] Metadata blocks (pandoc)
+# - [x] Metadata blocks (custom)
+# - [x] Fenced Divs (pandoc)
+#
+# Extensions - Inline elements:
+# ----------------------------
+# - [x] Ignore embedded_underscores (php md, pandoc)
+# - [x] ~~strikeout~~ (pandoc)
+# - [x] ^Superscript^ ~Subscript~ (pandoc)
+# - [-] Bracketed spans (pandoc)
+#   - [-] Inline attributes (pandoc)
+# - [x] Image attributes (custom, pandoc inspired, not for reference style)
+# - [x] Wiki style links [[PageName]] / [[PageName|Link Text]]
+# - [-] TEX-Math (pandoc)
+# -  ?  Footnotes (php md)
+# -  ?  Abbreviations (php md)
+# -  ?  "Curly quotes" (smartypants)
+# - [ ] em-dashes (--) (smartypants old)
+# -  ?  ... three-dot ellipsis (smartypants)
+# - [-] en-dash (smartypants)
+# - [ ] Automatic em-dash / en-dash
+# - [x] Automatic -> Arrows <- (custom)
+
+function debug(text) { printf "\n---\n%s\n---\n", text > "/dev/stderr"; }
+
+function HTML ( text ) {
+  gsub( /&/,  "\\&amp;",  text );
+  gsub( /</,  "\\&lt;",   text );
+  gsub( />/,  "\\&gt;",   text );
+  gsub( /"/,  "\\&quot;", text );
+  gsub( /'/,  "\\&#x27;", text );
+  gsub( /\\/, "\\&#x5C;", text );
+  return text;
+}
+
+function URL ( text, sharp ) {
+  gsub( /&/,  "%26",  text );
+  gsub( /"/,  "%22", text );
+  gsub( /'/,  "%27", text );
+  gsub( /`/,  "%60", text );
+  gsub( /\?/,  "%3F", text );
+  if (sharp) gsub( /#/,  "%23", text );
+  gsub( /\[/,  "%5B", text );
+  gsub( /\]/,  "%5D", text );
+  gsub( / /,  "%20", text );
+  gsub( /      /,  "%09", text );
+  gsub( /\\/, "%5C", text );
+  return text;
+}
+
+function inline( line, LOCAL, len, text, code, href, guard ) {
+  if ( line ~ /^$/ ) {  # Recursion End
+    return "";
+
+  # omit processing of escaped characters
+  } else if ( line ~ /^\\./) {
+    return HTML(substr(line, 2, 1)) inline( substr(line, 3) );
+
+  # hard brakes
+  } else if ( match(line, /^  \n/) ) {
+    return "<br>\n" inline( substr(line, RLENGTH + 1) );
+
+  #  ``code spans``
+  } else if ( match( line, /^`+/) ) {
+    len = RLENGTH
+    guard = substr( line, 1, len )
+    if ( match(line, guard ".*" guard) ) {
+      code = substr( line, len + 1, match( substr(line, len + 1), guard ) - 1)
+      len = 2 * length(guard) + length(code)
+      #  strip single surrounding white spaces
+      gsub( /^ | $/, "", code)
+      #  escape HTML within code span
+      gsub( /&/, "\\&amp;", code ); gsub( /</, "\\&lt;", code ); gsub( />/, "\\&gt;", code );
+      return "<code>" code "</code>" inline( substr( line, len + 1 ) )
+    }
+
+  # Macros
+  } else if ( match( line, /^<<([^>]|>[^>])+>>/ ) ) {
+    len = RLENGTH;
+    return "<code class=\"macro\">" HTML( substr( line, 3, len - 4 ) ) "</code>" inline(substr(line, len + 1));
+
+  # Wiki style links
+  } else if ( match( line, /^\[\[([^]|]+)(\|[^]]+)?\]\]/) ) {
+    len = RLENGTH;
+    href = gensub(/^\[\[([^]|]+)(\|([^]]+))?\]\]/, "\\1", 1, substr(line, 1, len) );
+    text = gensub(/^\[\[([^]|]+)(\|([^]]+))?\]\]/, "\\3", 1, substr(line, 1, len) );
+    if ( ! text ) text = href;
+    return "<a href=\"" HTML(href) "\">" HTML(text) "</a>" inline( substr( line, len + 1) );
+
+  #  quick links ("automatic links" in md doc)
+  } else if ( match( line, /^<[a-zA-Z]+:\/\/([-\.[:alnum:]]+)(:[0-9]*)?(\/[^>]*)?>/ ) ) {
+    len = RLENGTH;
+    href = HTML( substr( line, 2, len - 2) );
+    return "<a href=\"" href "\">" href "</a>" inline( substr( line, len + 1) );
+
+  # quick link email
+  } else if ( match( line, /^<[a-zA-Z0-9.!#$%&'\''*+\/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>/ ) ) {
+    len = RLENGTH;
+    href = HTML( substr( line, 2, len - 2) );
+    return "<a href=\"mailto:" href "\">" href "</a>" inline( substr( line, len + 1) );
+
+  # Verbatim inline HTML
+  } else if ( AllowHTML && match( line, /^(<!--([^-]|-[^-]|--[^>])*-->|<\?([^\?]|\?[^>])*\?>|<![A-Z][^>]*>|<!\[CDATA\[([^\]]|\][^\]]|\]\][^>])*\]\]>|<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)/) ) {
+    len = RLENGTH;
+    return substr( line, 1, len) inline(substr(line, len + 1));
+
+  # inline links
+  } else if ( match(line, "^" lii "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)") ) {
+    len = RLENGTH;
+    text = href = title = substr( line, 1, len);
+    sub("^\\[", "", text); sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)$", "", text);
+    sub("^" lii "\\([\n\t ]*", "", href); sub("([\n\t ]+" lit ")?[\n\t ]*\\)$", "", href);
+    sub("^" lii "\\([\n\t ]*" lid, "", title); sub("[\n\t ]*\\)$", "", title); sub("^[\n\t ]+", "", title);
+
+    if ( match(href, /^<.*>$/) ) { sub(/^</, "", href); sub(/>$/, "", href); }
+         if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); }
+    else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); }
+    else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); }
+
+    gsub(/\\/, "", href); gsub(/\\/, "", title); gsub(/[\n\t]+/, " ", title);
+
+    return "<a href=\"" HTML(href) "\"" (title?" title=\"" HTML(title) "\"":"") ">" \
+           inline( text ) "</a>" inline( substr( line, len + 1) );
+
+  # reference style links
+  } else if ( match(line, /^\[([^]]+)\] ?\[([^]]*)\]/ ) ) {
+    len = RLENGTH;
+    text = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\1", 1, substr(line, 1, len) );
+      id = gensub(/^\[([^\n]+)\] ?\[([^\n]*)\].*/, "\\2", 1, substr(line, 1, len) );
+    if ( ! id ) id = text;
+    if ( rl_href[id] && rl_title[id] ) {
+      return "<a href=\"" HTML(rl_href[id]) "\" title=\"" HTML(rl_title[id]) "\">" inline(text) "</a>" inline( substr( line, len + 1) );
+    } else if ( rl_href[id] ) {
+      return "<a href=\"" HTML(rl_href[id]) "\">" inline(text) "</a>" inline( substr( line, len + 1) );
+    } else {
+      return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) );
+    }
+
+  # inline images
+  } else if ( match(line, "^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?") ) {
+    len = RLENGTH; text = href = title = attrib = substr( line, 1, len);
+
+    sub("^!\\[", "", text);
+    sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", text);
+
+    sub("^!" lix "\\([\n\t ]*", "", href);
+    sub("([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", href);
+
+    sub("^!" lix "\\([\n\t ]*" lid, "", title);
+    sub("[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?$", "", title);
+    sub("^[\n\t ]+", "", title);
+
+    sub("^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)", "", attrib);
+    sub(/^\{[ \t]*/, "", attrib); sub(/[ \t]*\}$/, "", attrib); gsub(/[ \t]+/, " ", attrib);
+
+    if ( match(href, /^<.*>$/) ) { sub(/^</, "", href); sub(/>$/, "", href); }
+         if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); }
+    else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); }
+    else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); }
+
+    gsub(/^[\t ]+$/, "", text); gsub(/\\/, "", href);
+    gsub(/\\/, "", title); gsub(/[\n\t]+/, " ", title);
+
+    return "<img src=\"" HTML(href, 1) "\" alt=\"" HTML(text?text:title?title:href) "\"" \
+           (title?" title=\"" HTML(title) "\"":"") (attrib?" class=\"" HTML(attrib) "\"":"") \
+           ">" inline( substr( line, len + 1) );
+
+  # reference style images
+  } else if ( match(line, /^!\[([^]]*)\] ?\[([^]]*)\]/ ) ) {
+    len = RLENGTH;
+    text = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\].*/, "\\1", 1, substr(line, 1, len) );
+      id = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\].*/, "\\2", 1, substr(line, 1, len) );
+    if ( ! id ) id = text;
+    if ( rl_href[id] && rl_title[id] ) {
+      return "<img src=\"" HTML(rl_href[id], 1) "\" alt=\"" HTML(text) "\" title=\"" HTML(rl_title[id]) "\">" \
+             inline( substr( line, len + 1) );
+    } else if ( rl_href[id] ) {
+      return "<img src=\"" HTML(rl_href[id], 1) "\" alt=\"" HTML(text) "\">" \
+             inline( substr( line, len + 1) );
+    } else {
+      return "" HTML(substr(line, 1, len)) inline( substr(line, len + 1) );
+    }
+
+  #  ~~strikeout~~ (pandoc)
+  } else if ( match(line, /^~~([[:graph:]]|[[:graph:]]([^~]|~[^~])*[[:graph:]])~~/) ) {
+    len = RLENGTH;
+    return "<del>" inline( substr( line, 3, len - 4 ) ) "</del>" inline( substr( line, len + 1 ) );
+
+  #  ^superscript^ (pandoc)
+  } else if ( match(line, /^\^([^[:space:]^]|\\[ ^])+\^/) ) {
+    len = RLENGTH;
+    return "<sup>" inline( substr( line, 2, len - 2 ) ) "</sup>" inline( substr( line, len + 1 ) );
+
+  #  ~subscript~ (pandoc)
+  } else if ( match(line, /^~([^[:space:]~]|\\[ ~])+~/) ) {
+    len = RLENGTH;
+    return "<sub>" inline( substr( line, 2, len - 2 ) ) "</sub>" inline( substr( line, len + 1 ) );
+
+  # ignore embedded underscores (pandoc, php md)
+  } else if ( match(line, "^[[:alnum:]](__|_)") ) {
+    return HTML(substr( line, 1, RLENGTH)) inline( substr(line, RLENGTH + 1) );
+
+  #  __strong__$
+  } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__$") ) {
+    len = RLENGTH;
+    return "<strong>" inline( substr( line, 3, len - 4 ) ) "</strong>" inline( substr( line, len + 1 ) );
+
+  #  __strong__
+  } else if ( match(line, "^__(([^_[:space:]]|" ieu ")|([^_[:space:]]|" ieu ")(" nu "|" ieu ")*([^_[:space:]]|" ieu "))__[[:space:][:punct:]]") ) {
+    len = RLENGTH;
+    return "<strong>" inline( substr( line, 3, len - 5 ) ) "</strong>" inline( substr( line, len) );
+
+  #  **strong**
+  } else if ( match(line, "^\\*\\*(([^\\*[:space:]]|" iea ")|([^\\*[:space:]]|" iea ")(" na "|" iea ")*([^\\*[:space:]]|" iea "))\\*\\*") ) {
+    len = RLENGTH;
+    return "<strong>" inline( substr( line, 3, len - 4 ) ) "</strong>" inline( substr( line, len + 1 ) );
+
+  #  _em_$
+  } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_$") ) {
+    len = RLENGTH;
+    return "<em>" inline( substr( line, 2, len - 2 ) ) "</em>" inline( substr( line, len + 1 ) );
+
+  #  _em_
+  } else if ( match(line, "^_(([^_[:space:]]|" isu ")|([^_[:space:]]|" isu ")(" nu "|" isu ")*([^_[:space:]]|" isu "))_[[:space:][:punct:]]") ) {
+    len = RLENGTH;
+    return "<em>" inline( substr( line, 2, len - 3 ) ) "</em>" inline( substr( line, len ) );
+
+  #  *em*
+  } else if ( match(line, "^\\*(([^\\*[:space:]]|" isa ")|([^\\*[:space:]]|" isa ")(" na "|" isa ")*([^\\*[:space:]]|" isa "))\\*") ) {
+    len = RLENGTH;
+    return "<em>" inline( substr( line, 2, len - 2 ) ) "</em>" inline( substr( line, len + 1 ) );
+
+  # Literal HTML entities
+  } else if ( match( line, /^&([a-zA-Z]{2,32}|#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6});/) ) {
+    len = RLENGTH;
+    return substr( line, 1, len ) inline(substr(line, len + 1));
+
+  # Arrows
+  } else if ( line ~ /^-->( |$)/) {  # ignore multidash-arrow
+    return "--&gt;" inline( substr(line, 4) );
+  } else if ( line ~ /^<-( |$)/) {
+    return "&larr;" inline( substr(line, 3) );
+  } else if ( line ~ /^->( |$)/) {
+    return "&rarr;" inline( substr(line, 3) );
+
+  # Escape lone HTML character
+  } else if ( match( line, /^[&<>"']/) ) {
+    return HTML(substr(line, 1, 1)) inline(substr(line, 2));
+
+  #  continue walk over string
+  } else {
+    return substr(line, 1, 1) inline( substr(line, 2) );
+  }
+}
+
+function headline( hlvl, htxt, attrib, LOCAL, sec, n, HL) {
+  match(hstack, /([0-9]+( [0-9]+){5})$/); split( substr(hstack, RSTART),  HL);
+
+  for ( n = hlvl; n <= 6; n++ ) { sec = sec (HL[n]?"</section>":""); }
+  HL[hlvl]++; for ( n = hlvl + 1; n <= 6; n++) { HL[n] = 0;}
+
+  hid = ""; for ( n = 2; n <= blvl; n++) { hid = hid BL[n] "/"; }
+  hid = hid HL[1]; for ( n = 2; n <= hlvl; n++) { hid = hid "." HL[n] ; }
+  hid = hid ":" URL(htxt, 1);
+
+  sub(/([0-9]+( [0-9]+){5})$/, "", hstack);
+  hstack = hstack HL[1] " " HL[2] " " HL[3] " " HL[4] " " HL[5] " " HL[6];
+
+  return sec "<section class=\"" (attrib ? "h" hlvl " " attrib : "h" hlvl)  "\" id=\"" hid "\">" \
+         "<h" hlvl (attrib ? " class=\"" attrib "\"" : "") ">" inline( htxt ) \
+         "<a class=\"anchor\" href=\"#" hid "\"></a>" \
+         "</h" hlvl ">\n";
+}
+
+# Nested Block, resets heading counters
+function _nblock( block, LOCAL, sec, n ) {
+  hstack = hstack " 0 0 0 0 0 0";
+
+  # Block Level
+  blvl++; BL[blvl]++;
+  for ( n = blvl + 1; n in BL; n++) { delete BL[n]; }
+
+  block = _block( block );
+  match(hstack, /([0-9]+( [0-9]+){5})$/); split( substr(hstack, RSTART),  HL);
+  sec = ""; for ( n = 1; n <= 6; n++ ) { sec = sec (HL[n]?"</section>":""); }
+
+  sub("( +[0-9]+){6} *$", "", hstack); blvl--;
+  return block sec;
+}
+
+function _block( block, LOCAL, st, len, text, title, attrib, href, guard, code, indent, list ) {
+  gsub( "(^\n+|\n+$)", "", block );
+
+  if ( block == "" ) {
+    return "";
+
+  # HTML #2 #3 #4 $5
+  } else if ( AllowHTML && match( block, /(^|\n) ? ? ?(<!--([^-]|-[^-]|--[^>])*(-->|$)|<\?([^\?]|\?[^>])*(\?>|$)|<![A-Z][^>]*(>|$)|<!\[CDATA\[([^\]]|\][^\]]|\]\][^>])*(\]\]>|$))/) ) {
+    len = RLENGTH; st = RSTART;
+    return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len));
+
+  # HTML #6
+  } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<\/?(address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)([[:space:]\n>]|\/>)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) {
+    len = RLENGTH; st = RSTART;
+    return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len));
+
+  # HTML #1
+  } else if ( AllowHTML && match( tolower(block), /(^|\n) ? ? ?<(script|pre|style)([[:space:]\n>]).*(<\/script>|<\/pre>|<\/style>|$)/) ) {
+    len = RLENGTH; st = RSTART;
+    match( tolower(substr(block, st, len)), /(<\/script>|<\/pre>|<\/style>)/);
+    len = RSTART + RLENGTH;
+    return _block(substr(block, 1, st - 1)) substr(block, st, len) _block(substr(block, st + len));
+
+  # HTML #7
+  } else if ( AllowHTML && match( block, /^ ? ? ?(<\/[A-Za-z][A-Za-z0-9-]*[[:space:]]*>|<[A-Za-z][A-Za-z0-9-]*([[:space:]]+[A-Za-z_:][A-Za-z0-9_\.:-]*([[:space:]]*=[[:space:]]*([[:space:]"'=<>`]+|"[^"]*"|'[^']*'))?)*[[:space:]]*\/?>)([[:space:]]*\n)([^\n]|\n[ \t]*[^\n])*(\n[[:space:]]*\n|$)/) ) {
+    len = RLENGTH; st = RSTART;
+    return substr(block, st, len) _block(substr(block, st + len));
+
+  # Metadata (custom, block starting with %something)
+  # Metadata is ignored but can be interpreted externally
+  } else if ( match(block, /^%[a-zA-Z-]+([[:space:]][^\n]*)?(\n|$)(%[a-zA-Z-]+([[:space:]][^\n]*)?(\n|$)|%([[:space:]][^\n]*)?(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) {
+    len = RLENGTH; st = RSTART;
+    return  _block( substr( block, len + 1) );
+  # Blockquote (leading >)
+  } else if ( match( block, /^> /) ) {
+    match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match(block, /$/);
+    len = RLENGTH; st = RSTART;
+    text = substr(block, 1, st - 1); gsub( /(^|\n)> /, "\n", text );
+    text = _nblock( text ); gsub( /^\n|\n$/, "", text )
+    return "<blockquote>" text "</blockquote>\n\n" _block( substr(block, st + len) );
+
+  # Pipe Tables (pandoc / php md / gfm )
+  } else if ( match(block, "^((\\|)?([^\n]+\\|)+[^\n]+(\\|)?)\n" \
+                           "((\\|)?(:?-+:?[\\|+])+:?-+:?(\\|)?)\n" \
+                           "((\\|)?([^\n]+\\|)+[^\n]+(\\|)?(\n|$))+" ) ) {
+    len = RLENGTH; st = RSTART;
+    #initialize empty arrays
+    split("", talign); split("", tarray);
+    cols = 0; cnt=0; ttext = "";
+
+    # table header and alignment
+    split( gensub( /(^\||\|$)/, "", "g", \
+           gensub( /(^|[^\\])\\\|/, "\\1\\&#x7C;", "g", \
+           substr(block, 1, match(block, /(\n|$)/)) \
+    )), tarray, /\|/);
+    block = substr(block, match(block, /(\n|$)/) + 1 );
+    cols = split( \
+           gensub( /(^\||\|$)/, "", "g", \
+           substr(block, 1, match(block, /(\n|$)/)) \
+    ), talign, /[+\|]/);
+    block = substr(block, match(block, /(\n|$)/) + 1 );
+
+    for( cnt = 1; cnt < cols; cnt++ ) {
+           if (match(talign[cnt], /:-+:/)) talign[cnt]="center";
+      else if (match(talign[cnt],  /-+:/)) talign[cnt]="right";
+      else if (match(talign[cnt],  /:-+/)) talign[cnt]="left";
+      else talign[cnt]="";
+    }
+
+    ttext = "<thead>\n<tr>"
+    for (cnt = 1; cnt < cols; cnt++)
+      ttext = ttext "<th align=\"" talign[cnt] "\">" inline(tarray[cnt]) "</th>"
+    ttext = ttext "</tr>\n</thead><tbody>\n"
+
+    while ( match(block, "^((\\|)?([^\n]+\\|)+[^\n]+(\\|)?(\n|$))+" ) ){
+      split( gensub( /(^\||\|$)/, "", "g", \
+             gensub( /(^|[^\\])\\\|/, "\\1\\&#x7C;", "g", \
+             substr(block, 1, match(block, /(\n|$)/)) \
+      )), tarray, /\|/);
+      block = substr(block, match(block, /(\n|$)/) + 1 );
+
+      ttext = ttext "<tr>"
+      for (cnt = 1; cnt < cols; cnt++)
+        ttext = ttext "<td align=\"" talign[cnt] "\">" inline(tarray[cnt]) "</td>"
+      ttext = ttext "</tr>\n"
+    }
+    return "<table>" ttext "</tbody></table>\n" _block(block);
+
+  # Grid Tables (pandoc)
+  # (with, and without header)
+  } else if ( match( block, "^\\+(-+\\+)+\n" \
+                            "(\\|([^\n]+\\|)+\n)+" \
+                            "(\\+(:?=+:?\\+)+)\n" \
+                           "((\\|([^\n]+\\|)+\n)+" \
+                             "\\+(-+\\+)+(\n|$))+" \
+                   ) || \
+              match( block, "^()()()" \
+                            "(\\+(:?-+:?\\+)+)\n" \
+                           "((\\|([^\n]+\\|)+\n)+" \
+                             "\\+(-+\\+)+(\n|$))+" \
+  ) ) {
+    len = RLENGTH; st = RSTART;
+    #initialize empty arrays
+    split("", talign); split("", tarray); split("", tread);
+    cols = 0; cnt=0; ttext = "";
+
+    # Column Count
+    cols = split(   gensub( "^(\\+(:?-+:?\\+)+)(\n.*)*$", "\\1", 1, block), tread, /\+/) - 2;
+    # debug(" Cols: " gensub( "^(\\+(:?-+:?\\+)+)(\n.*)*$", "\\1", 1, block ));
+
+    # table alignment
+    split( gensub( "^(.*\n)?\\+((:?=+:?\\+|(:-+|-+:|:-+:)\\+)+)(\n.*)$", "\\2", "g", block ), talign, /\+/ );
+    # debug("Align: " gensub( "^(.*\n)?\\+((:?=+:?\\+|(:-+|-+:|:-+:)\\+)+)(\n.*)$", "\\2", "g", block ));
+
+    for (cnt = 1; cnt <= cols; cnt++) {
+           if (match(talign[cnt], /:(-+|=+):/)) talign[cnt]="center";
+      else if (match(talign[cnt],  /(-+|=+):/)) talign[cnt]="right";
+      else if (match(talign[cnt], /:(-+|=+)/ )) talign[cnt]="left";
+      else talign[cnt]="";
+    }
+
+    if ( match(block, "^\\+(-+\\+)+\n" \
+                      "(\\|([^\n]+\\|)+\n)+" \
+                       "\\+(:?=+:?\\+)+\n" \
+                     "((\\|([^\n]+\\|)+\n)+" \
+                       "\\+(-+\\+)+(\n|$))+" \
+    ) ) {
+      # table header
+      block = substr(block, match(block, /(\n|$)/) + 1 );
+      while ( match(block, "^\\|([^\n]+\\|)+\n") ) {
+        split( gensub( /(^\||\|$)/, "", "g", \
+                 gensub( /(^|[^\\])\\\|/, "\\1\\&#x7C;", "g", \
+                   substr(block, 1, match(block, /(\n|$)/)) \
+        )), tread, /\|/);
+        block = substr(block, match(block, /(\n|$)/) + 1 );
+        for (cnt = 1; cnt <= cols; cnt++)
+          tarray[cnt] = tarray[cnt] "\n" tread[cnt];
+      }
+
+      ttext = "<thead>\n<tr>"
+      for (cnt = 1; cnt <= cols; cnt++)
+        ttext = ttext "<th align=\"" talign[cnt] "\">" _nblock(tarray[cnt]) "</th>"
+      ttext = ttext "</tr>\n</thead>"
+    }
+
+    # table body
+    block = substr(block, match(block, /(\n|$)/) + 1 );
+    ttext = ttext "<tbody>\n"
+
+    while ( match(block, /^((\|([^\n]+\|)+\n)+\+(-+\+)+(\n|$))+/ ) ){
+      split("", tarray);
+      while ( match(block, /^\|([^\n]+\|)+\n/) ) {
+        split( gensub( /(^\||\|$)/, "", "g", \
+               gensub( /(^|[^\\])\\\|/, "\\1\\&#x7C;", "g", \
+               substr(block, 1, match(block, /(\n|$)/)) \
+        )), tread, /\|/);
+        block = substr(block, match(block, /(\n|$)/) + 1 );
+        for (cnt = 1; cnt <= cols; cnt++)
+          tarray[cnt] = tarray[cnt] "\n" tread[cnt];
+      }
+      block = substr(block, match(block, /(\n|$)/) + 1 );
+
+      ttext = ttext "<tr>"
+      for (cnt = 1; cnt <= cols; cnt++)
+        ttext = ttext "<td align=\"" talign[cnt] "\">" _nblock(tarray[cnt]) "</td>"
+      ttext = ttext "</tr>\n"
+    }
+    return "<table>" ttext "</tbody></table>\n" _nblock(block);
+
+  # Line Blocks (pandoc)
+  } else if ( match(block, /^\| [^\n]*(\n|$)(\| [^\n]*(\n|$)|[ \t]+[^\n[:space:]][^\n]*(\n|$))*/) ) {
+    len = RLENGTH; st = RSTART;
+
+    text = substr(block, 1, len); gsub(/\n[[:space:]]+/, " ", text);
+    gsub(/\n\| /, "\n", text); gsub(/^\| |\n$/, "", text);
+    text = inline(text); gsub(/\n/, "<br>\n", text);
+
+    return "<div class=\"line-block\">" text "</div>\n" _block( substr( block, len + 1) );
+
+  # Indented Code Block
+  } else if ( match(block, /^(    |\t)( *\t*[^ \t\n]+ *\t*)+(\n|$)((    |\t)[^\n]+(\n|$)|[ \t]*(\n|$))*/) ) {
+    len = RLENGTH; st = RSTART;
+    code = substr(block, 1, len);
+    gsub(/(^|\n)(    |\t)/, "\n", code);
+    gsub(/^\n|\n+$/, "", code);
+    return "<pre><code>" HTML( code ) "</code></pre>\n" \
+           _block( substr( block, len + 1 ) );
+
+  # Fenced Divs (pandoc, custom)
+  } else if ( match( block, /^(:::+)/ ) ) {
+    guard = substr( block, 1, RLENGTH );
+    code = block; sub(/^[^\n]+\n/, "", code);
+    attrib = gensub(/^:::+[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\1", 1, block);
+    gsub(/[^a-zA-Z0-9_-]+/, " ", attrib);
+    gsub(/(^ | $)/, "", attrib);
+    if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) {
+      len = RLENGTH; st = RSTART;
+      return "<div class=\"" attrib "\">" _nblock( substr(code, 1, st - 1) ) "</div>\n" \
+             _block( substr( code, st + len ) );
+    } else {
+      match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ );
+      len = RLENGTH; st = RSTART;
+      return "<p>" inline( substr(block, 1, st - 1) ) "</p>\n" \
+             _block( substr(block, st + len) );
+    }
+
+  # Fenced Code Block (pandoc)
+  } else if ( match( block, /^(~~~+|```+)/ ) ) {
+    guard = substr( block, 1, RLENGTH );
+    code = gensub(/^[^\n]+\n/, "", 1, block);
+    attrib = gensub(/^(~~~+|```+)[ \t]*\{?[ \t]*([^\}\n]*)\}?[ \t]*\n.*$/, "\\2", 1, block);
+    gsub(/[^a-zA-Z0-9_-]+/, " ", attrib);
+    gsub(/(^ | $)/, "", attrib);
+    if ( match(code, "(^|\n)" guard "+(\n|$)" ) ) {
+      len = RLENGTH; st = RSTART;
+      return "<pre><code class=\"" attrib "\">" HTML( substr(code, 1, st - 1) ) "</code></pre>\n" \
+             _block( substr( code, st + len ) );
+    } else {
+      match( block, /(^|\n)[[:space:]]*(\n|$)/ ) || match( block, /$/ );
+      len = RLENGTH; st = RSTART;
+      return "<p>" inline( substr(block, 1, st - 1) ) "</p>\n" \
+             _block( substr(block, st + len) );
+    }
+
+  # First Order Heading H1 + Attrib
+  } else if ( match( block, /^([^\n]+)([ \t]*\{([^\}\n]+)\})\n===+(\n|$)/ ) ) {
+    len = RLENGTH; text = attrib = block;
+    sub(/([ \t]*\{([^\}\n]+)\})\n===+(\n.*)?$/, "", text);
+    sub(/\}\n===+(\n.*)?$/, "", attrib); sub(/^([^\n]+)[ \t]*\{/, "", attrib);
+    gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib);
+
+    return headline(1, text, attrib) _block( substr( block, len + 1 ) );
+
+  # First Order Heading H1
+  } else if ( match( block, /^([^\n]+)\n===+(\n|$)/ ) ) {
+    len = RLENGTH; text = substr(block, 1, len);
+    sub(/\n===+(\n.*)?$/, "", text);
+
+    return headline(1, text, 0) _block( substr( block, len + 1 ) );
+
+  # Second Order Heading H2 + Attrib
+  } else if ( match( block, /^([^\n]+)([ \t]*\{([^\}\n]+)\})\n---+(\n|$)/ ) ) {
+    len = RLENGTH; text = attrib = block;
+    sub(/([ \t]*\{([^\}\n]+)\})\n---+(\n.*)?$/, "", text);
+    sub(/\}\n---+(\n.*)?$/, "", attrib); sub(/^([^\n]+)[ \t]*\{/, "", attrib);
+    gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib);
+
+    return headline(2, text, attrib) _block( substr( block, len + 1) );
+
+  # Second Order Heading H2
+  } else if ( match( block, /^([^\n]+)\n---+(\n|$)/ ) ) {
+    len = RLENGTH; text = substr(block, 1, len);
+    sub(/\n---+(\n.*)?$/, "", text);
+
+    return headline(2, text, 0) _block( substr( block, len + 1) );
+
+  # Nth Order Heading H1 H2 H3 H4 H5 H6 + Attrib
+  } else if ( match( block, /^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*([ \t]*\{([a-zA-Z \t-]*)\})(\n|$)/ ) ) {
+    len = RLENGTH; text = attrib = substr(block, 1, len);
+    match(block, /^#{1,6}/); n = RLENGTH;
+
+    sub(/^(#{1,6})[ \t]*/, "", text);   sub(/[ \t]*#*([ \t]*\{([a-zA-Z \t-]*)\})(\n.*)?$/, "", text);
+    sub(/^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*[ \t]*\{/, "", attrib);
+    sub(/\})(\n.*)?$/, "", attrib);
+    gsub(/[^a-zA-Z0-9_-]+/, " ", attrib); gsub(/(^ | $)/, "", attrib);
+
+    return headline( n, text, attrib ) _block( substr( block, len + 1) );
+
+  # Nth Order Heading H1 H2 H3 H4 H5 H6
+  } else if ( match( block, /^(#{1,6})[ \t]*(([^ \t\n]+|[ \t]+[^ \t\n#]|[ \t]+#+[ \t]*[^ \t\n#])+)[ \t]*#*(\n|$)/ ) ) {
+    len = RLENGTH; text = substr(block, 1, len);
+    match(block, /^#{1,6}/); n = RLENGTH;
+    sub(/^(#{1,6})[ \t]*/, "", text); sub(/[ \t]*#*(\n.*)?$/, "", text);
+
+    return headline( n, text, 0 ) _block( substr( block, len + 1) );
+
+  # block images (wrapped in <figure>)
+  } else if ( match(block, "^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n|$)") ) {
+    len = RLENGTH; text = href = title = attrib = substr( block, 1, len);
+
+    sub("^!\\[", "", text);
+    sub("\\]\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", text);
+
+    sub("^!" lix "\\([\n\t ]*", "", href);
+    sub("([\n\t ]+" lit ")?[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", href);
+
+    sub("^!" lix "\\([\n\t ]*" lid, "", title);
+    sub("[\n\t ]*\\)(\\{[a-zA-Z \t-]*\\})?(\n.*)?$", "", title);
+    sub("^[\n\t ]+", "", title);
+
+    sub("^!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\)", "", attrib);
+    sub("(\n.*)?$", "", attrib);
+    sub(/^\{[ \t]*/, "", attrib); sub(/[ \t]*\}$/, "", attrib); gsub(/[ \t]+/, " ", attrib);
+
+    if ( match(href, /^<.*>$/) ) { sub(/^</, "", href); sub(/>$/, "", href); }
+         if ( match(title, /^".*"$/) ) { sub(/^"/, "", title); sub(/"$/, "", title); }
+    else if ( match(title, /^'.*'$/) ) { sub(/^'/, "", title); sub(/'$/, "", title); }
+    else if ( match(title, /^\(.*\)$/) ) { sub(/^\(/, "", title); sub(/\)$/, "", title); }
+
+    gsub(/^[\t ]+$/, "", text); gsub(/\\/, "", href);
+
+    return "<figure data-src=\"" HTML(href, 1) "\"" (attrib?" class=\"" HTML(attrib) "\"":"") ">" \
+           "<img src=\"" HTML(href, 1) "\" alt=\"" HTML(text?text:title?title:href) "\"" \
+           (attrib?" class=\"" HTML(attrib) "\"":"") ">" \
+           (title?"<figcaption>" inline(title) "</figcaption>":"") \
+           "</figure>\n\n" \
+           _block( substr( block, len + 1) );
+
+  # reference style images (block)
+  } else if ( match(line, /^!\[([^]]*)\] ?\[([^]]*)\](\n|$)/ ) ) {
+    len = RLENGTH;
+    text = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\](\n.*)?$/, "\\1", 1, block);
+      id = gensub(/^!\[([^\n]*)\] ?\[([^\n]*)\](\n.*)?$/, "\\2", 1, block);
+    if ( ! id ) id = text;
+    if ( rl_href[id] && rl_title[id] ) {
+      return "<figure data-src=\"" HTML(rl_href[id], 1) "\">" \
+               "<img src=\"" HTML(rl_href[id], 1) "\" alt=\"" HTML(text) "\">" \
+               "<figcaption>" inline(rl_title[id]) "</figcaption>" \
+             "</figure>\n\n" \
+             _block( substr( block, len + 1) );
+    } else if ( rl_href[id] ) {
+      return "<figure data-src=\"" HTML(rl_href[id], 1) "\">" \
+               "<img src=\"" HTML(rl_href[id], 1) "\" alt=\"" HTML(text) "\">" \
+             "</figure>\n\n" \
+             _block( substr( block, len + 1) );
+    } else {
+      return "<p>" HTML(substr(block, 1, len)) "</p>\n" _block( substr(block, len + 1) );
+    }
+
+  # Macros (standalone <<macro>> calls handled as block, so they are not wrapped in paragraph)
+  } else if ( match( block, /^<<(([^>]|>[^>])+)>>(\n|$)/ ) ) {
+    len = RLENGTH;
+    text = gensub(/^<<(([^>]|>[^>])+)>>(\n.*)?$/, "\\1", 1, block);
+    return "<code class=\"macro\">" HTML(text) "</code>" _block(substr(block, len + 1) );
+
+  # Definition list
+  } else if (match( block, "^(([ \t]*\n)*[^:\n \t][^\n]+\n" \
+                           "([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \
+                          "(([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \
+                           "|[^:\n \t][^\n]+(\n|$)" \
+                           "|( ? ? ?\t|  +)[^\n]+(\n|$)" \
+                           "|([ \t]*\n)+( ? ? ?\t|  +)[^\n]+(\n|$))*)+" \
+  )) {
+    list = substr( block, 1, RLENGTH); block = substr( block, RLENGTH + 1);
+    return "\n<dl>\n" _dlist( list ) "</dl>\n" _block( block );
+
+  # Unordered list types
+  } else if ( text = _startlist( block, "ul", "-",   "([+*•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) {
+    return text;
+  } else if ( text = _startlist( block, "ul", "\\+", "([-*•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) {
+    return text;
+  } else if ( text = _startlist( block, "ul", "\\*", "([-+•]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) {
+    return text;
+  } else if ( text = _startlist( block, "ul", "•", "([-+*]|[0-9]+\\.|#\\.|[0-9]+\\)|#\\))") ) {
+    return text;
+
+  # Ordered list types
+  } else if ( text = _startlist( block, "ol", "[0-9]+\\.", "([-+*•]|#\\.|[0-9]+\\)|#\\))") ) {
+    return text;
+  } else if ( text = _startlist( block, "ol", "[0-9]+\\)", "([-+*•]|[0-9]+\\.|#\\.|#\\))") ) {
+    return text;
+  } else if ( text = _startlist( block, "ol", "#\\.", "([-+*•]|[0-9]+\\.|[0-9]+\\)|#\\))") ) {
+    return text;
+  } else if ( text = _startlist( block, "ol", "#\\)", "([-+*•]|[0-9]+\\.|#\\.|[0-9]+\\))") ) {
+    return text;
+
+  # Split paragraphs
+  } else if ( match( block, /(^|\n)[[:space:]]*(\n|$)/) ) {
+    len = RLENGTH; st = RSTART;
+    return _block( substr(block, 1, st - 1) ) "\n" \
+           _block( substr(block, st + len) );
+
+  # Horizontal rule
+  } else if ( match( block, /(^|\n) ? ? ?((\* *){3,}|(- *){3,}|(_ *){3,})($|\n)/) ) {
+    len = RLENGTH; st = RSTART;
+    return _block(substr(block, 1, st - 1)) "<hr>\n" _block(substr(block, st + len));
+
+  # Plain paragraph
+  } else {
+    return "<p>" inline(block) "</p>\n";
+  }
+}
+
+function _startlist(block, type, mark, exclude, LOCAL, st, len, list, indent, text) {
+  if (match( block, "(^|\n) ? ? ?" mark "[ \t][^\n]+(\n|$)" \
+                                   "(([ \t]*\n)* ? ? ?" mark "[ \t][^\n]+(\n|$)" \
+                                   "|([ \t]*\n)*( ? ? ?\t|  +)[^\n]+(\n|$)" \
+                                   "|[^\n \t][^\n]+(\n|$))*" ) ) {
+    st = RSTART; len = RLENGTH; list = substr( block, st, len);
+
+    sub("^\n", "", list); match(list, "^ ? ? ?"); indent = RLENGTH;
+    gsub( "(^|\n) {0," indent "}", "\n", list); sub("^\n", "", list);
+
+    text = substr(block, 1, st - 1); block = substr(block, st + len);
+    if (match(text, /\n[[:space:]]*\n/)) return 0;
+    if (match(text, "(^|\n) ? ? ?" exclude "[ \t][^\n]+")) return 0;
+    if (match( list, "\n" exclude "[ \t]" )) {
+      block = substr(list, RSTART + 1) block;
+      list = substr(list, 1, RSTART);
+    }
+
+    return _block( text ) "<" type ">\n" _list( list, mark ) "</" type ">\n" _block( block );
+  } else return 0;
+}
+
+function _list (block, mark, p, LOCAL, len, st, text, indent, task) {
+  if ( match(block, "^([ \t]*\n)*$")) return;
+
+  match(block, "^" mark "[ \t]"); indent = RLENGTH;
+  sub("^" mark "[ \t]", "", block);
+
+  if (match(block, /\n[ \t]*\n/)) p = 1;
+
+  match( block, "\n" mark "[ \t][^\n]+(\n|$)" );
+  st = (RLENGTH == -1) ? length(block) + 1 : RSTART;
+  text = substr(block, 1, st); block = substr(block, st + 1);
+
+  gsub("\n {0," indent "}", "\n", text);
+
+  task = match( text, /^\[ \]/   ) ? "<li class=\"task pending\"><input type=checkbox disabled>"      : \
+         match( text, /^\[-\]/   ) ? "<li class=\"task negative\"><input type=checkbox disabled>"     : \
+         match( text, /^\[\/\]/  ) ? "<li class=\"task partial\"><input type=checkbox disabled>"      : \
+         match( text, /^\[\?\]/  ) ? "<li class=\"task unsure\"><input type=checkbox disabled>"       : \
+         match( text, /^\[[xX]\]/) ? "<li class=\"task done\"><input type=checkbox disabled checked>" : "<li>";
+  sub(/^\[[-? \/xX]\]/, "", text);
+
+  text = _nblock( text );
+  if ( ! p && match( text, "^<p>(</p[^>]|</[^p]|<[^/]|[^<])*</p>\n$" ))
+     gsub( "(^<p>|</p>\n$)", "", text);
+
+  return task text "</li>\n" _list(block, mark, p);
+}
+
+function _dlist (block, LOCAL, len, st, text, indent, p) {
+  if (match( block, "^([ \t]*\n)*[^:\n \t][^\n]+\n" )) {
+    len = RLENGTH; text = substr(block, 1, len);
+    gsub( "(^\n*|\n*$)", "", text );
+    return "<dt>" inline( text ) "</dt>\n" _dlist( substr(block, len + 1) );
+  } else if (match( block, "^([ \t]*\n)* ? ? ?:[ \t][^\n]+(\n|$)" \
+                         "([^:\n \t][^\n]+(\n|$)" \
+                         "|( ? ? ?\t|  +)[^\n]+(\n|$)" \
+                         "|([ \t]*\n)+( ? ? ?\t|  +)[^\n]+(\n|$))*" \
+  )) {
+    len = RLENGTH; text = substr(block, 1, len);
+    sub( "^([ \t]*\n)*", "", text);
+    match(text, "^ ? ? ?:(\t| +)"); indent = RLENGTH;
+    sub( "^ ? ? ?:(\t| +)", "", text);
+    gsub( "(^|\n) {0," indent "}", "\n", text );
+
+    text = _nblock(text);
+    if (match( text, "^<p>(</p[^>]|</[^p]|<[^/]|[^<])*</p>\n$" ))
+       gsub( "(^<p>|</p>\n$)", "", text);
+
+    return "<dd>" text "</dd>\n" _dlist( substr(block, len + 1) );
+  }
+}
+
+BEGIN {
+  # Global Vars
+  file = ""; rl_href[""] = ""; rl_title[""] = "";
+  if (ENVIRON["MD_HTML"] == "true") { AllowHTML = "true"; }
+  HL[1] = 0; HL[2] = 0; HL[3] = 0; HL[4] = 0; HL[5] = 0; HL[6] = 0;
+  # hls = "0 0 0 0 0 0";
+
+  # Universal Patterns
+  nu = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\_]|_[[:alnum:]])*"    # not underline (except when escaped)
+  na = "(\\\\\\\\|\\\\[^\\\\]|[^\\\\\\*])*"  # not asterisk (except when escaped)
+  ieu =  "_([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])_"                 # inner <em> (underline)
+  isu = "__([^_[:space:]]|[^_[:space:]]" nu "[^_[:space:]])__"                # inner <strong> (underline)
+  iea =    "\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*"     # inner <em> (asterisk)
+  isa = "\\*\\*([^\\*[:space:]]|[^\\*[:space:]]" na "[^\\*[:space:]])\\*\\*"  # inner <strong> (asterisk)
+
+  lix="\\[(\\\\[^\n]|[^]\n\\\\[])*\\]"  # link text
+  lid="(<(\\\\[^\n]|[^\n<>\\\\])*>|(\\\\.|[^()\"'\\\\])+|([^<\n\t ()\\\\]|\\\\[^\n])(\\\\[\n]|[^\n\t \\(\\)\\\\])*)"  # link dest
+  lit="(\"(\\\\.|[^\"\\\\])*\"|'(\\\\.|[^'\\\\])*'|\\((\\\\.|[^\\(\\)\\\\])*\\))"  # link text
+  # link text with image def
+  lii="\\[(\\\\[^\n]|[^]\n\\\\[])*(!" lix "\\([\n\t ]*" lid "([\n\t ]+" lit ")?[\n\t ]*\\))?(\\\\[^\n]|[^]\n\\\\[])*\\]"
+
+  # Buffering of full file ist necessary, e.g. to find reference links
+  while (getline) { file = file $0 "\n"; }
+  # Clean up MS-DOS line breaks
+  gsub(/\r\n/, "\n", file);
+
+  # Fill array of reference links
+  f = file; rl_id;
+  re_reflink = "(^|\n) ? ? ?\\[([^]\n]+)\\]: ([^ \t\n]+)(\n?[ \t]+(\"([^\"]+)\"|'([^']+)'|\\(([^)]+)\\)))?(\n|$)";
+  # /(^|\n) ? ? ?\[([^]\n]+)\]: ([^ \t\n]+)(\n?[ \t]+("([^"]+)"|'([^']+)'|\(([^)]+)\)))?(\n|$)/
+  while ( match(f, re_reflink ) ) {
+    rl_id           = gensub( re_reflink, "\\2", 1, substr(f, RSTART, RLENGTH) );
+    rl_href[rl_id]  = gensub( re_reflink, "\\3", 1, substr(f, RSTART, RLENGTH) );
+    rl_title[rl_id] = gensub( re_reflink, "\\5", 1, substr(f, RSTART, RLENGTH) );
+    f = substr(f, RSTART + RLENGTH);
+    rl_title[rl_id] = substr( rl_title[rl_id], 2, length(rl_title[rl_id]) - 2 );
+    if ( rl_href[rl_id] ~ /<.*>/ ) rl_href[rl_id] = substr( rl_href[rl_id], 2, length(rl_href[rl_id]) - 2 );
+  }
+  # Clear reflinks from File
+  while( gsub(re_reflink, "\n", file ) );
+  # for (n in rl_href) { debug(n " | " rl_href[n] " | " rl_title[n] ); }
+
+  # Run Block Processing -> The Actual Markdown!
+  printf "%s", _nblock( file );
+}
diff --git a/cgilite/session.sh b/cgilite/session.sh
new file mode 100755 (executable)
index 0000000..c3a44e8
--- /dev/null
@@ -0,0 +1,152 @@
+#!/bin/sh
+
+# Copyright 2018 - 2022 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+[ -n "$include_session" ] && return 0
+include_session="$0"
+
+export _DATE="$(date +%s)"
+SESSION_TIMEOUT="${SESSION_TIMEOUT:-7200}"
+
+if ! which uuencode >/dev/null; then
+  uuencode() { busybox uuencode "$@"; }
+fi
+if ! which sha256sum >/dev/null; then
+  sha256sum() { busybox sha256sum "$@"; }
+fi
+
+if which openssl >/dev/null; then
+  session_mac(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | openssl dgst -sha1 -hmac "$(server_key)" -binary |slopecode; }
+else
+  # Gonzo MAC if openssl is unavailable
+  session_mac(){
+    { server_key | dd status=none bs=256 count=1 skip=1
+      { server_key | dd status=none bs=256 count=1
+        [ $# -gt 0 ] && printf %s "$*" || cat
+      } \
+      | sha256sum -;
+    } \
+    | sha256sum | cut -d\  -f1
+  }
+fi
+
+server_key(){
+  IDFILE="${IDFILE:-${_DATA:-.}/serverkey}"
+  if [ "$(stat -c %s "$IDFILE")" -ne 512 ] || ! cat "$IDFILE"; then
+    dd count=1 bs=512 if=/dev/urandom \
+    | tee "$IDFILE"
+  fi 2>&-
+}
+
+slopecode(){
+  # 6-Bit Code that retains sort order of input data, while beeing safe to use
+  # in ascii transmissions, unix file names, HTTP URLs, and HTML attributes
+
+  { [ $# -gt 0 ] && printf %s "$*" || cat; } \
+  | uuencode -m - | sed '
+    1d;$d; 
+    y;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/;0123456789:=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz;
+  '
+}
+
+randomid(){
+  dd bs=12 count=1 if=/dev/urandom 2>&- \
+  | slopecode
+}
+
+timeid(){
+  d=$(($_DATE % 4294967296))
+  { printf "$(
+      printf \\%o \
+        $((d / 16777216 % 256)) \
+        $((d / 65536 % 256)) \
+        $((d / 256 % 256)) \
+        $((d % 256))
+    )"
+    dd bs=8 count=1 if=/dev/urandom 2>&-
+  } | slopecode
+}
+
+transid(){
+  # transaction ID to modify a given file
+  local file="$1"
+  session_mac "$(stat -c %F%i%n%N%s%Y "$file" 2>&-)" "$SESSION_ID"
+}
+
+checkid(){ { [ $# -gt 0 ] && printf %s "$*" || cat; } | grep -m 1 -xE '[0-9a-zA-Z:=]{16}'; }
+
+update_session(){
+  local session sid time sig checksig
+  unset SESSION_KEY SESSION_ID
+
+  read -r sid time sig <<-END
+       $(POST session_key || COOKIE session)
+       END
+  
+  checksig="$(session_mac "$sid" "$time")"
+  
+  if [ "$checksig" = "$sig" \
+       -a "$time" -ge "$_DATE" \
+       -a "$(checkid "$sid")" ] 2>&-
+  then
+    time=$(( $_DATE + $SESSION_TIMEOUT ))
+    sig="$(session_mac "$sid" "$time")"
+
+    SESSION_KEY="${sid} ${time} ${sig}"
+    SESSION_ID="${sid}"
+    return 0
+  else
+    return 1
+  fi
+
+}
+
+new_session(){
+  local sid time sig
+
+  debug "Setting up new session"
+  sid="$(randomid)"
+  time=$(( $_DATE + $SESSION_TIMEOUT ))
+  sig="$(session_mac "$sid" "$time")"
+
+  SESSION_KEY="${sid} ${time} ${sig}"
+  SESSION_ID="${sid}"
+}
+
+SESSION_BIND() {
+  # Set tamper-proof authenticated cookie
+  local key="$1" value="$2"
+  SET_COOKIE session "$key"="${value} $(session_mac "$value" "$SESSION_ID")" Path="/${_BASE#/}" SameSite=Strict HttpOnly
+}
+
+SESSION_VAR() {
+  # read authenticated cookie
+  # fail if value has been tampered with
+  local key="$1" value sig
+  value="$(COOKIE "$key")"
+  sig="${value##* }" value="${value% *}"
+  if [ "$sig" = "$(session_mac "$value" "$SESSION_ID")" ]; then
+    printf %s\\n "$value"
+  else
+    return 1
+  fi
+}
+
+SESSION_COOKIE() {
+  [ "$1" = new ] && new_session
+  SET_COOKIE 0 session="$SESSION_KEY" Path="/${_BASE#/}" SameSite=Strict HttpOnly
+}
+
+update_session || new_session
diff --git a/cgilite/storage.sh b/cgilite/storage.sh
new file mode 100755 (executable)
index 0000000..17ea0d0
--- /dev/null
@@ -0,0 +1,188 @@
+#!/bin/sh
+
+# Copyright 2018 - 2021 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+[ -n "$include_storage" ] && return 0
+include_storage="$0"
+
+CR="\r"
+BR='
+'
+
+LOCK(){
+  local lock="${1}.lock" timeout="${2-20}" block
+
+  if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -f "$lock" ]; then
+    debug "Impossible to get lock: $lock"
+    return 1
+  fi
+
+  while [ $timeout -gt 0 ]; do
+    printf '%i\n' $$ >>"${lock}"
+    read block <"$lock"
+    if [ "$block" = $$ ]; then
+      return 0
+    elif ! { ps -eo pid |grep -qwF "$block"; }; then
+      debug "Trying to override stale lock: $lock"
+      if LOCK "$lock" 1; then
+        rm -- "$lock"
+        RELEASE "$lock"
+      fi
+    else
+      timeout=$((timeout - 1))
+      [ $timeout -gt 0 ] && sleep 1
+    fi
+  done
+
+  debug "Timeout while trying to get lock: $lock"
+  return 1
+}
+
+RELEASE(){
+  local lock="${1}.lock" block
+
+  read block <"$lock"
+  if [ "$block" = $$ ]; then
+    rm -- "$lock"
+    return 0
+  else
+    debug "Refusing to release foreign lock: $lock"
+    return 1
+  fi
+}
+
+STRING(){
+  local in out=''
+  [ $# -gt 0 ] && in="$*" || in="$(cat)"
+  while [ "$in" ]; do case $in in
+    \\*) out="${out}\\\\"; in="${in#\\}" ;;
+    "$BR"*) out="${out}\\n"; in="${in#${BR}}" ;;
+    "$CR"*) out="${out}\\r"; in="${in#${CR}}" ;;
+    "  "*) out="${out}\\t"; in="${in#  }" ;;
+    +*) out="${out}\\+"; in="${in#+}" ;;
+    " "*) out="${out}+"; in="${in# }" ;;
+    *) out="${out}${in%%[\\${CR}${BR}  + ]*}"; in="${in#"${in%%[\\${BR}${CR}   + ]*}"}" ;;
+  esac; done
+  printf '%s' "${out:-\\}"
+}
+
+UNSTRING(){
+  local in out=''
+  [ $# -gt 0 ] && in="$*" || in="$(cat)"
+  while [ "$in" ]; do case $in in
+    \\\\*) out="${out}\\"; in="${in#\\\\}" ;;
+    \\n*) out="${out}${BR}"; in="${in#\\n}" ;;
+    \\r*) out="${out}${CR}"; in="${in#\\r}" ;;
+    \\t*) out="${out}  "; in="${in#\\t}" ;;
+    \\+*) out="${out}+"; in="${in#\\+}" ;;
+    +*) out="${out} "; in="${in#+}" ;;
+    \\*) in="${in#\\}" ;;
+    *) out="${out}${in%%[\\+]*}"; in="${in#"${in%%[\\+]*}"}" ;;
+  esac; done
+  printf '%s\n' "$out"
+}
+
+DBM() {
+  local file="$1" cmd="$2"
+  local k v key value
+  shift 2;
+
+  case "$cmd" in
+    check|contains)
+      key="$(STRING "$1")"
+      while read -r k v; do if [ "$k" = "$key" ]; then
+        return 0
+      fi; done <"$file" 2>&-
+      return 1
+      ;;
+    get)
+      key="$(STRING "$1")"
+      while read -r k v; do if [ "$k" = "$key" ]; then
+        UNSTRING "$v"
+        return 0
+      fi; done <"$file" 2>&-
+      return 1
+      ;;
+    set|store)
+      key="$(STRING "$1")" value="$(STRING "$2")"
+      LOCK "$file" || return 1
+      { while read -r k v; do
+          [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v"
+        done <"$file" 2>&-
+        printf '%s\t%s\n' "$key" "$value"
+      } >"${file}.$$.tmp"
+      mv "${file}.$$.tmp" "${file}"
+      RELEASE "$file"
+      return 0
+      ;;
+    add|insert)
+      k="$1" key="$(STRING "$1")" value="$(STRING "$2")"
+      LOCK "$file" || return 1
+      if DBM "$file" check "$k"; then
+        RELEASE "$file"
+        return 1
+      else
+        printf '%s\t%s\n' "$key" "$value" >>"${file}"
+        RELEASE "$file"
+        return 0
+      fi
+      ;;
+    update|replace)
+      k="$1" key="$(STRING "$1")" value="$(STRING "$2")"
+      LOCK "$file" || return 1
+      if ! DBM "$file" check "$k"; then
+        RELEASE "$file"
+        return 1
+      fi
+      { while read -r k v; do
+          [ "$k" = "$key" ] \
+          && printf '%s\t%s\n' "$key" "$value" \
+          || printf '%s\t%s\n' "$k" "$v"
+        done <"$file" 2>&-
+      } >"${file}.$$.tmp"
+      mv "${file}.$$.tmp" "${file}"
+      RELEASE "$file"
+      return 0
+      ;;
+    append)
+      key="$(STRING "$1")" value="$(STRING "$2")"
+      LOCK "$file" || return 1
+      if ! DBM "$file" check "$1"; then
+        RELEASE "$file"
+        return 1
+      fi
+      { while read -r k v; do
+          [ "$k" = "$key" ] \
+          && printf '%s\t%s\n' "$key" "$v$value" \
+          || printf '%s\t%s\n' "$k" "$v"
+        done <"$file" 2>&-
+      } >"${file}.$$.tmp"
+      mv "${file}.$$.tmp" "${file}"
+      RELEASE "$file"
+      return 0
+      ;;
+    delete|remove)
+      key="$(STRING "$1")"
+      LOCK "$file" || return 1
+      { while read -r k v; do
+          [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v"
+        done <"$file" 2>&-
+      } >"${file}.$$.tmp"
+      mv "${file}.$$.tmp" "${file}"
+      RELEASE "$file"
+      return 0
+      ;;
+  esac
+}
diff --git a/cgilite/users.sh b/cgilite/users.sh
new file mode 100755 (executable)
index 0000000..32299ff
--- /dev/null
@@ -0,0 +1,661 @@
+#!/bin/sh
+
+# Copyright 2021 - 2024 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+[ -n "$include_users" ] && return 0
+include_users="$0"
+
+. "${_EXEC:-.}/cgilite/session.sh"
+. "${_EXEC:-.}/cgilite/storage.sh"
+
+SENDMAIL=${SENDMAIL-sendmail}
+
+USER_REGISTRATION="${USER_REGISTRATION-true}"
+USER_REQUIREEMAIL="${USER_REQUIREEMAIL-true}"
+USER_ACCOUNTPAGE="${USER_ACCOUNTPAGE}"
+
+USER_ACCOUNTEXPIRE="${USER_ACCOUNTEXPIRE:-$((86400 * 730))}"
+USER_CONFIRMEXPIRE="${USER_CONFIRMEXPIRE:-86400}"
+
+HTTP_HOST="$(HEADER Host)"
+MAILFROM="noreply@${HTTP_HOST%:*}"
+
+[ "$HTTPS" ] && SCHEMA=https || SCHEMA=http
+
+# == FILE FORMAT ==
+# UID  UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+#              (pending|active|deleted)
+
+# == GLOBALS ==
+UNSET_USER='unset \
+  USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+  USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+'
+
+LOCAL_USER='local \
+  USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+  USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+'
+
+# == TRANSLATIONS ==
+# override all functions marked with "TRANSLATION"
+# sed -n '/TRANSLATION$/,/^}/p;' <cgilite/users.sh
+
+unset USER_IDMAP
+eval "$UNSET_USER"
+
+user_db="${user_db:-${_DATA}/users.db}"
+
+read_user() {
+  local user="$1"
+
+  # Global exports
+  USER_ID='' USER_NAME='' USER_STATUS='' USER_EMAIL='' USER_PWSALT=''
+  USER_PWHASH='' USER_EXPIRE='' USER_DEVICES='' USER_FUTUREUSE=''
+
+  if [ $# -eq 0 ]; then
+    read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+            USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+  elif [ "$user" -a -f "$user_db" -a -r "$user_db" ]; then
+    read -r USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+            USER_EXPIRE USER_DEVICES USER_FUTUREUSE <<-EOF
+       $(grep "^${user}        " "${user_db}")
+       EOF
+  fi
+  if [ "$USER_ID" -a "${USER_EXPIRE:-0}" -gt "$_DATE" ]; then
+       USER_NAME="$(UNSTRING "$USER_NAME")"
+      USER_EMAIL="$(UNSTRING "$USER_EMAIL")"
+    USER_DEVICES="$(UNSTRING "$USER_DEVICES")"
+    unset USER_PWSALT USER_PWHASH
+  else
+    eval "$UNSET_USER"
+    return 1
+  fi
+}
+
+update_user() {
+  # internal function for user update
+  local uid="$1" uname status email pwsalt pwhash expire devices futureuse
+  local UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES FUTUREUSE
+  local arg
+
+  for arg in "$@"; do case $arg in
+    uname=*) uname="${arg#*=}";;
+    status=*) status="${arg#*=}";;
+    email=*) email="${arg#*=}";;
+    password=*) pwsalt="$(randomid)"; pwhash="$(user_pwhash "$pwsalt" "${arg#*=}")";;
+    expire=*) expire="${arg#*=}";;
+    devices=*) devices="${arg#*=}";;
+  esac; done
+
+  if LOCK "$user_db"; then
+    while read -r UID_ UNAME STATUS EMAIL PWSALT PWHASH EXPIRE DEVICES \
+                  FUTUREUSE; do
+    if [ "$UID_" = "$uid" ]; then
+      printf '%s       %s      %s      %s      %s      %s      %i      %s      %s\n' \
+             "$uid" "$(STRING "${uname-$(UNSTRING "$UNAME")}")" \
+             "${status:-${status-${STATUS}}${status+\\}}" \
+             "${email:-${email-${EMAIL}}${email+\\}}" \
+             "${pwsalt:-${PWSALT}}" "${pwhash:-${PWHASH}}" \
+             "${expire:-$((_DATE + USER_ACCOUNTEXPIRE))}" \
+             "$(STRING "${devices-$(UNSTRING "$DEVICES")}")" \
+             "${FUTUREUSE:-\\}"
+    elif [ "$STATUS" = pending -a ! "$EXPIRE" -ge "$_DATE" ]; then
+      # omit expired invitations from output
+      :
+    else
+      printf '%s       %s      %s      %s      %s      %s      %i      %s      %s\n' \
+             "$UID_" "$UNAME" "$STATUS" "$EMAIL" "$PWSALT" "$PWHASH" \
+             "$EXPIRE" "$DEVICES" "$FUTUREUSE"
+    fi
+    done <"$user_db" >"${user_db}.$$"
+    mv -- "${user_db}.$$" "$user_db"
+    RELEASE "$user_db"
+  else
+    return 1
+  fi
+}
+
+new_user(){
+  local user="${1:-$(timeid)}"
+  shift 1
+
+  if LOCK "$user_db"; then
+    if grep -q "^${user}       " "$user_db"; then
+      RELEASE "$user_db"
+      return 1
+    fi
+    printf '%s \\      %s      \\      \\      \\      %i      \\      \\\n' \
+           "$user" "pending" "$(( _DATE + USER_CONFIRMEXPIRE ))" >>"$user_db"
+  else
+    return 1
+  fi
+
+  if [ $# -eq 0 ]; then
+    RELEASE "$user_db"
+    return 0
+  elif update_user "$user" "$@"; then
+    return 0
+  else
+    RELEASE "$user_db"
+    return 1
+  fi
+}
+
+user_idmap(){
+  local uid="$1" ret
+  eval "$LOCAL_USER"
+
+  if [ ! "$USER_IDMAP" ]; then
+    while read_user; do
+      USER_IDMAP="${USER_IDMAP}${USER_ID}      ${USER_NAME}${BR}"
+    done <"$user_db"
+  fi
+  if [ "$uid" -a "$USER_IDMAP" != "${USER_IDMAP##*${uid}       }" ]; then
+    ret="${USER_IDMAP##*${uid} }"; ret="${ret%%${BR}*}";
+    printf '%s\n' "$ret"
+    return 0
+  elif [ "$uid" ]; then
+    return 1
+  else
+    printf '%s' "$USER_IDMAP"
+    return 0
+  fi
+}
+
+user_idof(){
+  local name="$(STRING "$1")" ret
+  [ "$USER_IDMAP" ] || user_idmap >/dev/null
+
+  if [ "${name%\\}" -a "$USER_IDMAP" != "${USER_IDMAP% ${name}${BR}*}" ]; then
+    ret="${USER_IDMAP% ${name}${BR}*}"; ret="${ret##*${BR}}"
+    printf '%s\n' "$ret"
+    return 0
+  else
+    return 1
+  fi
+}
+
+user_checkname(){
+  { [ $# -gt 0 ] && printf %s "$*" || cat; } \
+  | sed -nE '
+    :X; $!{N;bX;}
+    s;[ \t\r\n]+; ;g;
+    s;^ ;;; s; $;;;
+    /@/d;
+    /^[a-zA-Z][a-zA-Z0-9 -~]{2,127}$/!d;
+    p;
+    '
+}
+
+user_checkemail(){
+  { [ $# -gt 0 ] && printf %s "$*" || cat; } \
+  | sed -nE '
+    # W3C recommended email regex
+    # https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)
+    /^[a-zA-Z0-9.!#$%&'\''*+\/=?^_`{|}~-]+@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/p;
+    '
+}
+
+user_nameexist(){
+  local uname="$(STRING "$1")"
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  [ -f "$user_db" -a -r "$user_db" ] \
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+    [ "$EXPIRE" -gt "$_DATE" -a "$UNAME" = "$uname" ] && return 0
+  done <"$user_db"
+  return 1
+}
+
+user_emailexist(){
+  local email="$(STRING "$1")"
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  [ -f "$user_db" -a -r "$user_db" ] \
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+    [ "$EXPIRE" -gt "$_DATE" -a "$EMAIL" = "$email" ] && return 0
+  done <"$user_db"
+  return 1
+}
+
+user_pwhash(){
+  local salt="$1" secret="$2" hash
+  hash="$(printf '%s\n%s\n' "$secret" "$salt" |sha256sum)"
+  printf '%s\n' "${hash%% *}"
+}
+
+user_register_email() {  # TRANSLATION
+  "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
+       From: ${MAILFROM}
+       To: ${email}
+       Subject: Your account registration at ${HTTP_HOST%:*}
+
+       Someone tried to sign up for a user account using this email address.
+
+       You can activate your account using this link:
+
+           ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+
+       This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
+
+       If you did not request an account at ${HTTP_HOST%:*}, then someone else
+       probably entered your email address by accident. In this case you shoud
+       simply ignore this message and we will remove your email address from
+       our database within the next day.
+
+       This is an automatic email. Any direct reply will not be received.
+       Your Account Registration Robot.
+       EOF
+}
+
+user_register(){
+  # reserve account, send registration mail
+  # preliminary uid, expiration, signature
+  local uid="$(timeid)"
+  local uname="$(POST uname |user_checkname)"
+  local email="$(POST email |user_checkemail)"
+  local pwsalt="$(randomid)"
+  local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
+
+  if [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_REGISTRATION_DISABLED"
+  fi
+
+  if   [ "$USER_REQUIREEMAIL" = true ]; then
+    if [ ! "$email" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
+    elif user_emailexist "$email"; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
+    elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
+      debug "Sending Activation Link:" \
+            "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+      user_register_email
+      REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+    fi
+
+  elif [ "$USER_REQUIREEMAIL" != true ]; then
+    if [ ! "$uname" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_INVALID"
+    elif user_nameexist "$uname"; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_UNAME_EXISTS"
+    elif [ ! "$pw" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_EMPTYTOOSHORT"
+    elif [ "$pw" != "$pwconfirm" ]; then
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
+    elif new_user "$uid" uname="$uname" status=active email="$email" password="$pw" expire="$((_DATE + USER_ACCOUNTEXPIRE))"; then
+      SESSION_COOKIE new
+      SESSION_BIND user_id "$uid"
+
+      if [ "$USER_ACCOUNTPAGE" ]; then
+        REDIRECT "${USER_ACCOUNTPAGE}"
+      else
+        REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+      fi
+    else
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+    fi
+  fi
+}
+
+user_invite_email(){  # TRANSLATION
+  "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
+       From: ${MAILFROM}
+       To: ${email}
+       Subject: You have been invited to ${HTTP_HOST%:*}
+
+       ${USER_NAME:-Someone} has offered an invitation to this email address.
+
+       ${message}
+
+       You can create your account using this link:
+
+           ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+
+       This registration link will expire after $((USER_CONFIRMEXPIRE / 3600)) hours.
+
+       If you do not know what this is about, then someone else probably
+       entered your email address by accident. In this case you shoud
+       simply ignore this message and we will remove your email address from
+       our database within the next day.
+
+       This is an automatic email. Any direct reply will not be received.
+       Your Account Registration Robot.
+       EOF
+}
+
+user_invite(){
+  local uid="$(timeid)"
+  local email="$(POST email |user_checkemail)"
+  local message="$(POST message)"
+
+  if [ ! "$email" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_INVALID"
+  elif user_emailexist "$email"; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_EMAIL_EXISTS"
+  elif new_user "$uid" status=pending email="$email" expire="$((_DATE + USER_CONFIRMEXPIRE))"; then
+    debug "Sending Invitation Link:" \
+          "${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+    user_invite_email
+    REDIRECT "${_BASE}${PATH_INFO}#USER_REGISTER_CONFIRM"
+  else
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+  fi
+}
+
+user_confirm(){
+  # enable account
+  eval "$LOCAL_USER"
+  local uid="$(POST uid |checkid || printf invalid)"
+  local signature="$(POST signature)"
+  local uname="$(POST uname |user_checkname)"
+  local pwsalt="$(randomid)"
+  local pw="$(POST pw |grep -m1 -xE '.{6,}' )" pwconfirm="$(POST pwconfirm)"
+
+  read_user "${uid}"
+
+  if [ "$signature" != "$(session_mac "$uid")" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
+  elif [ ! "$uname" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_INVALID"
+  elif user_nameexist "$uname"; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_UNAME_EXISTS"
+  elif [ ! "$pw" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_EMPTYTOOSHORT"
+  elif [ "$pw" != "$pwconfirm" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_PW_MISMATCH"
+  elif [ "$USER_STATUS" != pending -o \! "$USER_EXPIRE" -gt "$_DATE" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}?${QUERY_STRING}#ERROR_LINK_INVALID"
+  elif update_user "$USER_ID" uname="$uname" status=active password="$pw"; then
+    SESSION_COOKIE new
+    SESSION_BIND user_id "$USER_ID"
+    if [ "$USER_ACCOUNTPAGE" ]; then
+      REDIRECT "${USER_ACCOUNTPAGE}"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}?user_register=confirm#USER_REGISTER_CONFIRM"
+    fi
+  else
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_USER_NOLOCK"
+  fi
+}
+
+user_login(){
+  # set cookie
+  # keep logged in - device cookie?
+  # initialize new session!
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local uname="$(POST uname |STRING)" pw="$(POST pw)"
+
+  [ -f "$user_db" -a -r "$user_db" ] \
+  && while read -r UID_        UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE; do
+    if [ "$UNAME" = "$uname" -o "$EMAIL" = "$uname" ]; then
+      if [ "$STATUS" = active -a "$EXPIRE" -gt "$_DATE" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$pw")" ]; then
+        SESSION_COOKIE new
+        SESSION_BIND user_id "$UID_"
+        REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_IN"
+      fi
+    fi
+  done <"$user_db"
+  REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_LOGIN"
+}
+
+user_logout(){
+  # destroy cookie, destroy session
+  # keep device cookie
+  new_session
+  SESSION_COOKIE new
+  SET_COOKIE 0 user_id="" Path="/${_BASE#/}" SameSite=Strict HttpOnly
+  REDIRECT "${_BASE}${PATH_INFO}#USER_LOGGED_OUT"
+}
+
+user_update(){
+  # todo: username update, email update / email confirm
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  # local uname="$(POST uname |STRING)"
+  local uid oldpw pw pwconfirm
+
+        uid="$(POST uid)"
+      oldpw="$(POST oldpw)"
+         pw="$(POST pw |grep -m1 -xE '.{6,}')"
+  pwconfirm="$(POST pwconfirm)"
+
+
+  read -r UID_ UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${uid} " "$user_db")
+       EOF
+
+  if [ "$UID_" = "$USER_ID" -a "$PWHASH" = "$(user_pwhash "$PWSALT" "$oldpw")" ]; then
+    if [ "$pw" -a "$pw" = "$pwconfirm" ]; then
+      update_user "${uid}" password="$pw"
+      REDIRECT "${_BASE}${PATH_INFO}#UPDATE_SUCCESS"
+    else
+      REDIRECT "${_BASE}${PATH_INFO}#ERROR_PW_MISMATCH"
+    fi
+  elif [ "$UID_" = "$USER_ID" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_INVALID_AUTH_PASSWORD"
+  else
+    REDIRECT "${_BASE}${PATH_INFO}#ERROR_NOTLOGGEDIN"
+  fi
+}
+
+user_recover(){
+  # send recover link
+  :
+}
+user_disable(){
+  :
+}
+
+read_user "$(SESSION_VAR user_id)"
+[ "$USER_STATUS" -a "$USER_STATUS" != active ] && eval $UNSET_USER
+
+[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
+  user_register) user_register ;;
+  user_confirm)  user_confirm ;;
+  user_invite)   user_invite ;;
+  user_login)    user_login ;;
+  user_logout)   user_logout ;;
+  user_update)   user_update ;;
+  user_recover)
+    :;;
+  user_disable)
+    :;;
+esac
+
+export USER_ID USER_NAME USER_STATUS USER_EMAIL USER_PWSALT USER_PWHASH \
+       USER_EXPIRE USER_DEVICES USER_FUTUREUSE
+
+
+w_user_update(){
+  if [ ! "$USER_ID" ]; then
+    cat <<-EOF
+       [div #user_update .nouser
+       This page can only be used by registered users
+       ]
+       EOF
+  else
+    cat <<-EOF
+       [form #user_update method=POST
+         [hidden "uid" "$USER_ID"]
+         [p .username Logged in as $USER_NAME]
+         [input type=password name=oldpw placeholder="Current Passphrase"]
+         [input type=password name=pw placeholder="New Passphrase" pattern=".{6,}"]
+         [input type=password name=pwconfirm placeholder="Confirm New Passphrase" pattern=".{6,}"]
+         [submit "action" "user_update" Update Passphrase]
+       ]
+       EOF
+  fi
+}
+
+w_user_register_disabled(){  # TRANSLATION
+  cat <<-EOF
+       [div #user_register .disabled
+       User Registration is disabled.
+       ]
+       EOF
+}
+w_user_register_sendmail(){  # TRANSLATION
+  cat <<-EOF
+       [form #user_register .registeremail method=POST
+         [p We will send an activation mail to your email address.
+           You can continue the signup process when you click on the
+           activation link in this email.]
+         [input type=email name=email placeholder="Email"]
+         [submit "action" "user_register" Sign Up]
+       ]
+       EOF
+}
+w_user_register_direct(){  # TRANSLATION
+  cat <<-EOF
+       [form #user_register .registername method=POST
+          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
+         [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
+         [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
+         [submit "action" "user_register" Sign Up]
+       ]
+       EOF
+}
+
+w_user_register(){
+  if [ "$(GET user_confirm)" ]; then
+    w_user_confirm
+  elif [ "$USER_REGISTRATION" != true -a -s "$user_db" ]; then
+    w_user_register_disabled
+  elif [ "$USER_REQUIREEMAIL" = true ]; then
+    w_user_register_sendmail
+  elif [ "$USER_REQUIREEMAIL" != true ]; then
+    w_user_register_direct
+  fi
+}
+
+w_user_confirm_proceed(){  # TRANSLATION
+  cat <<-EOF
+       [form #user_confirm method=POST
+         [input type=hidden name=uid value="${uid}"]
+         [input type=hidden name=signature value="${signature}"]
+         $([ "$EMAIL" != '\' ] && printf \
+           '[input disabled=disabled value="%s" placeholder="Email"]' "$(UNSTRING "$EMAIL" |HTML)"
+         )
+          [input name=uname placeholder="Choose Username" tooltip="Your username may contain any character but the @ sign. It must be at least 3 characters long, and it must start with a letter." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
+         [input type=password name=pw placeholder="Choose Passphrase" pattern=".{6,}"]
+         [input type=password name=pwconfirm placeholder="Confirm Passphrase" pattern=".{6,}"]
+         [submit "action" "user_confirm" Finish Registration]
+       ]
+       EOF
+}
+w_user_confirm_expired(){  # TRANSLATION
+  cat <<-EOF
+       [div #user_confirm .expired
+         [p This activation link is not valid anymore.]
+       ]
+       EOF
+}
+w_user_confirm_invalid(){  # TRANSLATION
+  cat <<-EOF
+       [div #user_confirm .invalid
+         [p This activation link is invalid. Make sure you copied the whole activation link from your email and be careful not to include any line breaks.]
+       ]
+       EOF
+}
+
+w_user_confirm(){
+  local UID_   UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE
+  local user_confirm="$(GET user_confirm)"
+  local uid="${user_confirm% *}" signature="${user_confirm#* }"
+
+  if [ "$signature" = "$(session_mac "$uid")" ]; then
+    read -r UID_       UNAME   STATUS  EMAIL   PWSALT  PWHASH  EXPIRE  DEVICES FUTUREUSE <<-EOF
+       $(grep "^${uid} " "$user_db")
+       EOF
+    if [ "$STATUS" = pending -a "$EXPIRE" -gt "$_DATE" ]; then
+      w_user_confirm_proceed
+    else
+      w_user_confirm_expired
+    fi
+  else
+    w_user_confirm_invalid
+  fi
+}
+
+w_user_invite_email(){  # TRANSLATION
+  cat <<-EOF
+       [form #user_invite method=POST
+         [input placeholder="Email Recipient" name=email autocomplete=off]
+         [textarea name="message" placeholder="Message to recipient" . ]
+         [submit "action" "user_invite" Send Invitation]
+       ]
+       EOF
+}
+w_user_invite_link(){  # TRANSLATION
+  cat <<-EOF
+       [div #user_invite .link
+          [p An anonymous user account has been set up. Send the following link to the intended user, so they may claim their account. The link will remain valid for $((USER_CONFIRMEXPIRE / 3600)) hours.]
+          [a href="$(HTML "$invlink")" . $(HTML "$invlink")]
+
+          [p [a href="#" . Set up another account]]
+       ]
+       EOF
+}
+w_user_invite_deny(){  # TRANSLATION
+  cat <<-EOF
+       [div #user_invite .notallowed
+         Only registered users may send an invitation to another user.
+       ]
+       EOF
+}
+
+w_user_invite(){
+  local uid invlink
+
+  if [ "$(GET user_confirm)" ]; then
+    w_user_confirm
+  elif [ "$USER_ID" -a "$USER_REQUIREEMAIL" = true ]; then
+    w_user_invite_email
+  elif [ "$USER_ID" ]; then
+    uid="$(timeid)"
+    new_user "$uid" status=pending expire="$((_DATE + USER_CONFIRMEXPIRE))"
+    invlink="${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")"
+    debug "New Invitation Link: $invlink"
+    w_user_invite_link
+  else
+    w_user_invite_deny
+  fi
+}
+
+w_user_login_logon(){  # TRANSLATION
+  cat <<-EOF
+       [form #user_login .login method=POST
+         [input name=uname placeholder="Username or Email"]
+         [input type=password name=pw placeholder="Passphrase"]
+         [submit "action" "user_login" Login]
+       ]
+       EOF
+}
+w_user_login_logoff(){  # TRANSLATION
+  cat <<-EOF
+       [form #user_login .logout method=POST
+         [p Logged in as [span . $(HTML ${USER_NAME})]]
+         [submit "action" "user_logout" Logout]
+       ]
+       EOF
+}
+
+w_user_login(){
+  if [ ! "$USER_ID" ]; then
+    w_user_login_logon
+  elif [ "$USER_ID" ]; then
+    w_user_login_logoff
+  fi
+}
similarity index 60%
rename from actions/filter_card.sh
rename to courses/edit_course.sh
index cc0f9127e404fbbaf52ba752ad97d4545dc70ddc..e8ce0c6914e8331690708c62d5bc6e64301a3e9f 100755 (executable)
@@ -1,6 +1,6 @@
-#!/bin/zsh
+#!/bin/sh
 
-# Copyright 2014, 2017 Paul Hänsch
+# Copyright 2014, 2019, 2020 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-cgi_post
+locktimeout=900
+. "$_EXEC"/session_lock.sh
 
-if [ "${_POST[choice]}" = new_filter ]; then
-  filter="$(
-    for n in filter{0..100}; do
-      # [ -z "${_POST[$n]:+x}" ] && break
-      printf %s "${_POST[$n]}"
-    done \
-    | sed -r 's;\|+;\|;g; s;\^+;\^;g; s;:\|;:;g; :X; s;\^[^:]*:\^;\^;g; /\^[^:]*:\^/bX; s;^\^;;; s;\^[^:]*:$;;;'
-  )"
-  printf 'Location: ?p=cards&order=%s&filter=%s\n\n' "${_POST[order]}" "$filter"
+course="$(GET course |PATH)"
+coursefile="$_DATA/ical/${course##*/}"
+
+if tempfile="$(SLOCK "$coursefile" "$locktimeout")"; then
+  REDIRECT "${_BASE}/courses/?e=${course}"
+elif [ -f "$tempfile" ]; then
+  SET_COOKIE session message="SESSLOCK"
+  REDIRECT "${_BASE}/courses/#${course}"
 else
-  printf 'Location: ?p=cards\n\n'
+  SET_COOKIE session message="EDITLOCK"
+  REDIRECT "${_BASE}/courses/#${course}"
 fi
similarity index 68%
rename from actions/edit_course.sh
rename to courses/export_ical.sh
index 1841e056b00ffc5038b9c4751bf041dfb4edc1d5..3649ed5e0e21e7229f2c747e246e24216c4b285f 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/zsh
 
-# Copyright 2014 Paul Hänsch
+# Copyright 2014,2015,2021 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-course="${_GET[course]}"
+course="$(GET course |PATH)"
+coursefile="$_DATA/ical/${course##*/}"
 
-echo -n "Location: ?p=courses&edit=$course\n\n"
+. $_EXEC/pdiread.sh
+. $_EXEC/cgilite/file.sh
+
+printf 'Content-Disposition: inline; filename="%s.ics"\r\n' "$(pdi_value "$(pdi_load "$coursefile")" SUMMARY)"
+
+FILE "$coursefile" "text/calendar; charset=utf-8"
diff --git a/courses/export_pdf.sh b/courses/export_pdf.sh
new file mode 100755 (executable)
index 0000000..92e10c5
--- /dev/null
@@ -0,0 +1,132 @@
+#!/bin/sh
+
+. "${_EXEC}/pdiread.sh"
+. "$_EXEC/cards/l10n.sh"
+
+coursefile="${_DATA}/ical/$(GET course)"
+
+if [ ! -r "$coursefile" ]; then
+  SET_COOKIE 0 message="Cannot read course file"
+  REDIRECT "${_BASE}/courses/"
+  return 0
+elif ! mkdir -p "$_DATA/export"; then
+  SET_COOKIE 0 message="Cannot create export directory"
+  REDIRECT "${_BASE}/courses/"
+  return 0
+fi
+
+ics="$(pdi_load "$coursefile")"
+htmlfile="${_DATA}/export/$(pdi_value "$ics" SUMMARY |unescape |tr \\n/ __).html"
+pdffile=${htmlfile%.html}.pdf
+
+pdi_date() {
+  local pdt y m d H M S Z
+  [ $# -eq 0 ] && read pdt || pdt="$*"
+
+  case $pdt in
+    *T*Z)
+      Z=UTC; pdt="${pdt%Z}";;
+    TZID=*:*T*)
+      Z="${pdt%%:*}"; Z=${Z#TZID=}; pdt=${pdt#TZID=*:};;
+  esac
+
+  y="${pdt%%????T*}" pdt=${pdt#????}
+  m="${pdt%%??T*}" pdt=${pdt#??}
+  d="${pdt%%T*}" pdt=${pdt#??T}
+  H="${pdt%%????}" pdt=${pdt#??}
+  M="${pdt%%??}" pdt=${pdt#??}
+  S="${pdt}" pdt=''
+
+  case Z in 
+    UTC) date -d "${y}-${m}-${d} ${H}:${M}:${S} UTC" +%s;;
+     '') date -d "${y}-${m}-${d} ${H}:${M}:${S}" +%s;;
+      *) date -d "TZ=\"${Z}\" ${y}-${m}-${d} ${H}:${M}:${S}" +%s;;
+  esac
+}
+
+get_dates() {
+  local dts_date rrule rr_int rr_freq rec today="$(date +%Y%m%d)"
+
+  dts_date="$(pdi_value "$ics" DTSTART || printf %s "$today")"
+  dts_date="${dts_date#TZID=*:}" dts_date="${dts_date%%T*}"
+  rrule="$(pdi_value "$ics" RRULE)"
+  rr_int="${rrule##*INTERVAL=}" rr_int="${rr_int%%;*}"
+  rr_freq="${rrule##*FREQ=}" rr_freq="${rr_freq%%;*}"
+
+  [ "$rr_int" -ge 0 ] || rr_int=1 2>/dev/null
+  case "$rr_freq" in
+    YEARLY)  rec="$rr_int year";;
+    MONTHLY) rec="$rr_int month";;
+    DAILY)   rec="$rr_int day";;
+    WEEKLY)  rec="$rr_int week";;
+    *) rec="$rr_int week";;
+  esac
+
+  while [ "$dts_date" -lt "$today" ]; do dts_date="$(date -d "${dts_date} + ${rec}" +%Y%m%d)"; done
+  for n in 1 2 3 4 5 6 7 8 9 10; do
+    LANG=de_DE.UTF-8 date -d "$dts_date" +"%d. %b."
+    dts_date="$(date -d "${dts_date} + ${rec}" +%Y%m%d)"
+  done
+}
+
+# some table styles need to be inline, because this is how libreoffice works
+style_td='style="border: 1pt solid; padding: 1mm 2mm; vertical-align: top;"'
+
+"$_EXEC/cgilite/html-sh.sed" <<-EOF >"$htmlfile"
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+[html [head
+  [meta http-equiv="content-type" content="text/html; charset=utf-8"]
+  [title]
+  [meta name="generator" content="Confetti"]
+  [meta name="created" content="$(date +%FT%T)"]
+  [meta name="changed" content="$(date +%FT%T)"]
+  [style type="text/css"
+    @page { size: 29.7cm 21cm; margin: 1.5cm; }
+    * { background: inherit; }
+    body { background: transparent; font-family: Liberation Sans, Sans-Serif; }
+
+    th { white-space: pre; }
+    th, td { text-align: left; }
+  ]
+][body lang="de_DE"
+  [table width="100%" style="page-break-after: always;"
+    [col width=10*] [col width=5*] [col width=10*] [col width=15*]
+    [thead
+      [tr [th $style_td . $(l10n N)] [th $style_td . $(l10n BDAY)] [th $style_td . $(l10n TEL)] [th $style_td . $(l10n NOTE)]]
+    ][tbody
+      $(grep -F "${coursefile##*/}     " "$_DATA/mappings/attendance" |while read discard each; do
+        vcf="$(pdi_load "$_DATA/vcard/$each")"
+        tel="$( seq 1 $(pdi_count "$vcf" TEL) |while read n; do
+                  type="$(pdi_attrib "$vcf" TEL $n TYPE)"
+                  [ "$type" ] && type="$(l10n "TYPE=$type"):"
+                  printf '%s %s<br>' "$type" "$(pdi_value "$vcf" TEL $n)"
+                done  )"
+        printf '[tr valign=top [td %s .N . %s] [td %s .BDAY . %s] [td %s .TEL . %s] [td %s .NOTE . %s]]\n' \
+               "$style_td" "$(pdi_value "$vcf" FN   |unescape |HTML)" \
+               "$style_td" "$(pdi_value "$vcf" BDAY |unescape |HTML)" \
+               "$style_td" "$tel" \
+               "$style_td" "$(pdi_value "$vcf" NOTE |unescape |HTML)"
+      done |sort)]
+  ]
+  [table width="100%"
+    [col width=30*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*]
+    [thead
+      [tr [th $style_td ] $(get_dates |xargs -d\\n printf "[th $style_td . %s]")]
+    ][tbody
+    $(grep -F "${coursefile##*/}       " "$_DATA/mappings/attendance" |while read discard each; do
+      vcf="$(pdi_load "$_DATA/vcard/$each")"
+      printf '[tr [td %s .N . %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s] [td %s]]\n' \
+             "$style_td" "$(pdi_value "$vcf" FN |unescape |HTML)" \
+             "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td" "$style_td"
+    done |sort)]
+  ]
+]]
+EOF
+
+export            HOME="$_DATA/"
+export XDG_CONFIG_HOME="$_DATA/xdg_config"
+export  XDG_CACHE_HOME="$_DATA/xdg_cache"
+export   XDG_DATA_HOME="$_DATA/xdg_local"
+
+lowriter --convert-to pdf --outdir "$_DATA/export/" "$htmlfile" >/dev/null
+REDIRECT "$(URL "${_BASE}/export/${pdffile##*/}")"
diff --git a/courses/index.cgi b/courses/index.cgi
new file mode 100755 (executable)
index 0000000..57761ad
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+. $_EXEC/pdiread.sh
+. $_EXEC/courses/l10n.sh
+. $_EXEC/courses/widgets.sh
+. $_EXEC/courses/list.sh
+
+order="$(GET o |grep -m1 -xE 'DOW|TOD')"
+edit="$(GET e |PATH)"
+
+[ "$order" ] || order=DOW
+edit="${edit##*/}"
+
+{ w_sort_courses
+  printf '
+  [form .newcourses action="%s/courses/new_course.sh" method="POST"
+    [button type="submit" %s]
+  ]' "${_BASE}" "$(l10n newcourse)"
+
+  [ "$edit" ] && edit_course "$edit"
+  printf '[div .courselist\n'
+    list_courses
+  printf ']'
+} | yield_page courses #/courses/courses.css
diff --git a/courses/l10n.sh b/courses/l10n.sh
new file mode 100755 (executable)
index 0000000..f98529f
--- /dev/null
@@ -0,0 +1,50 @@
+# Copyright 2014, 2016, 2019 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+l10n(){
+  local word
+  [ $# -eq 0 ] && read -r word || word="$*"
+
+  case $word in
+    newcourse) printf "Neuen Kurs anlegen";;
+    time) printf "Uhrzeit";;
+
+    edit_dtscal) printf "&#x2713;";;
+    edit) printf "Bearbeiten";;
+    ics_export) printf "ICal exportieren";;
+    courselist) printf "Kursliste (PDF)";;
+    
+    course_mail) printf "Mail an Teilnehmende";;
+    
+    sort_order) printf "Sortierung";;
+    order_DOW) printf "Wochentag";;
+    order_TOD) printf "Uhrzeit";;
+    order_apply) printf "Sortieren";;
+    
+    t_every) printf "Alle";;
+    t_eternal) printf "ewig";;
+    t_times) printf "mal";;
+    t_until) printf "Bis";;
+    t_oclock) printf "Uhr";;
+
+    "Mon Tue Wed Thu Fri Sat Sun") printf "Mo Di Mi Do Fr Sa So";;
+    "January February March April May June July August September October November December")
+      printf "Januar Februar März April Mai Juni Juli August September Oktober November Dezember";;
+
+    *) l10n_global "$word";;
+  esac
+}
diff --git a/courses/list.sh b/courses/list.sh
new file mode 100755 (executable)
index 0000000..95857ec
--- /dev/null
@@ -0,0 +1,122 @@
+#!/bin/sh
+
+. "${_EXEC}"/pdiread.sh
+
+SUP_FIELDS="COMMENT"
+MAILTO="${MAILTO:-confetti@confetti}"
+
+edit_course(){
+  local coursefile="$_DATA/ical/$1" 
+  local tempfile course
+
+  . $_EXEC/session_lock.sh
+
+  if ! tempfile="$(CHECK_SLOCK "$coursefile")"; then
+    printf '[div .message %s]' "$(l10n "This course is not set up for editing within this session.")"
+  else
+    course="$(pdi_load "$tempfile")"
+    cat <<-EOF
+       [form .course #${coursefile##*/} action="${_BASE}/courses/update_course.sh" method="POST"
+          [input type="hidden" name="course" value="${coursefile##*/}"]
+         [input type="hidden" name="tid" value="$(transid ${tempfile})"]
+          [div .section .basic . $(
+            edit_item "$course" SUMMARY COMMENT
+          )]
+          [div .section .dtstart . $(
+            edit_item "$course" DTSTART
+          )]
+          [div .section .recur . $(
+            edit_item "$course" RRULE
+          )]
+          [div .section .attendance . $(
+            edit_item "$course" attendance
+          )]
+          [div .control
+            [!-- select .item name=newfield
+              [option disabled="disabled" selected . $(l10n edit_addfieldtext)]
+              $(for f in $SUP_FIELDS; do printf '[option value="%s" . %s]\n' "$f" "$(l10n "$f")"; done)
+            ]
+            [button .item type="submit" name="action" value="addfield" . $(l10n edit_addfield) --]
+            [button .item type="submit" name="action" value="update"   . $(l10n edit_update)]
+            [button .item type="submit" name="action" value="cancel"   . $(l10n edit_cancel)]
+            [div .item .delete label="$(l10n edit_delete)"
+              [input type="checkbox" #delete]
+              [label for="delete" $(l10n edit_delete)]
+              [button type="submit" name="action" value="delete" $(l10n edit_delete)]
+            ]
+          ]
+       ]
+       EOF
+  fi
+}
+
+print_course(){
+  local coursefile="$1"
+  local course="$(pdi_load "$coursefile")"
+
+  cat <<-EOF
+    [div .course #${coursefile##*/}
+      [div .section .basic . $(
+        cal_item "$course" SUMMARY DTSTART RRULE
+      )]
+      [div .section .COMMENT   . $(cal_item "$course" COMMENT)]
+      [div .section .attendance [h3 $(l10n course_attendance) ] [ul . 
+        $(grep -F "${coursefile##*/}   " "$_DATA/mappings/attendance" |while read discard each; do
+          printf '[li [a .item .attendance href="%s/cards/#%s" . %s]]\n' \
+                 "${_BASE}" "$each" \
+                 "$(pdi_value "$(pdi_load "$_DATA/vcard/$each")" FN |unescape |HTML)"
+        done |sort -k7)]
+      ]
+      [div .control
+        [a .button .item href="${_BASE}/courses/edit_course.sh?course=${coursefile##*/}" $(l10n edit)]
+        [a .button .item href="${_BASE}/courses/export_pdf.sh?course=${coursefile##*/}" target="blank" $(l10n courselist)]
+        [a .button .item href="${_BASE}/courses/export_ical.sh?course=${coursefile##*/}" $(l10n ics_export)]
+        [a .button .item href="mailto:${MAILTO}?bcc=$(course_mail "${coursefile##*/}" |HTML)" $(l10n course_mail)]
+      ]
+    ]
+       EOF
+}
+
+course_mail() {
+  course="$1"
+  grep -F "${course}   " "$_DATA/mappings/attendance" |while read junk card; do
+    cat "${_DATA}/vcard/${card}"
+  done \
+  | pdi_load - \
+  | sed -nE 's;^EMAIL(\;[^:]*)*:(.+)\r?$;\2,;p' \
+  | tr -d \\n \
+  | unescape
+}
+
+print_courses(){
+  local calfile cachefile date size name ldate=0 lsize lname
+
+  while read calfile; do
+    cachefile="${_DATA}/cache/${calfile##*/}.cache"
+    if [ -s "$cachefile" -a "$cachefile" -nt "$calfile" ]; then
+      cat "$cachefile"
+    elif [ -s "$calfile" ]; then
+      print_course "$calfile" |tee "$cachefile"
+    fi
+  done
+}
+
+order_courses() {
+  local calfile course
+
+  while read calfile; do
+    icstime="$(pdi_value "$(pdi_load "$calfile")" DTSTART |cal_date)"
+    case $order in
+      DOW) printf '%s  %s\n' "$(date -d "$icstime" "+%u %H:%M:%S")" "$calfile";;
+      TOD) printf '%s  %s\n' "$(date -d "$icstime" "+%H:%M:%S")" "$calfile";;
+    esac
+  done \
+  | sort \
+  | sed -E 's;^.*\t;;g'
+}
+
+list_courses(){
+  printf '%s\n' ${_DATA}/ical/*.ics \
+  | order_courses \
+  | print_courses
+}
similarity index 51%
rename from actions/new_course.sh
rename to courses/new_course.sh
index 4f97d4aadc94d3144135ad6854efeecd1871d1e0..36de913dcf290b93fd802a7e7ba99a9dc31e53d4 100755 (executable)
@@ -1,6 +1,6 @@
-#!/bin/zsh
+#!/bin/sh
 
-# Copyright 2014 Paul Hänsch
+# Copyright 2014, 2021 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-cgi_post
+locktimeout=900
+. "$_EXEC"/session_lock.sh
 
-uid=$(uuidgenerator)
+uid="$(timeid)$(randomid)"  # 32 Octets UID, starting with timestamp
 course="${uid}.ics"
 
-tzid=$(cat /etc/timezone)
-tstamp=$(TZ="$tzid" date +%Y%m%dT%H%M%S)
+tzid="$(cat /etc/timezone)"
+tstamp="$(TZ="$tzid" date +%Y%m%dT%H%M%S)"
 
-tempfile="$_DATA/temp/$course"
+coursefile="$_DATA/ical/$course"
 
-cat >"$tempfile" <<EOF
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:Berlin RAW Confetti
-BEGIN:VEVENT
-UID:$uid
-DTSTAMP:TZID=${tzid}:${tstamp}
-DTSTART:
-DURATION:
-RRULE:
-SUMMARY:
-COMMENT:
-END:VEVENT
-END:VCARD
-EOF
-
-echo -n "Location: ?p=courses&edit=$course\n\n"
+if tempfile="$(SLOCK "$coursefile")"; then
+  cat >"$tempfile" <<-EOF
+       BEGIN:VCALENDAR
+       VERSION:2.0
+       PRODID:Berlin RAW Confetti
+       BEGIN:VEVENT
+       UID:$uid
+       DTSTAMP:TZID=${tzid}:${tstamp}
+       DTSTART:TZID=${tzid}:${tstamp}
+       DURATION:
+       RRULE:
+       SUMMARY:
+       COMMENT:
+       END:VEVENT
+       END:VCARD
+       EOF
+  REDIRECT "${_BASE}/courses/?e=${course}"
+else
+  SET_COOKIE session message="EDITLOCK"
+  REDIRECT "${_BASE}/courses/"
+fi
diff --git a/courses/update_course.sh b/courses/update_course.sh
new file mode 100755 (executable)
index 0000000..aa9632c
--- /dev/null
@@ -0,0 +1,153 @@
+#!/bin/sh
+
+# Copyright 2014, 2015, 2020, 2021 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+. "$_EXEC/pdiread.sh"
+. "$_EXEC/session_lock.sh"
+. "$_EXEC/cgilite/storage.sh"
+
+unset coursefile attfile tempfile
+
+course="$(POST course |PATH)"; course="${course##*/}"
+coursefile="$_DATA/ical/$course"
+attfile="$_DATA/mappings/attendance"
+
+if ! tempfile="$(CHECK_SLOCK "$coursefile")"; then
+  SET_COOKIE 0 message="NO VALID FILE LOCK"
+  REDIRECT "${_BASE}/courses/?e=${course}"
+  exit 0
+elif [ "$(POST tid)" != "$(transid "$tempfile")" ]; then
+  SET_COOKIE 0 message="INVALID TRANSACTION ID"
+  REDIRECT "${_BASE}/courses/?e=${course}"
+  exit 0
+fi
+
+vcf_escape(){
+  for each in "$@"; do
+    printf %s\\n "$each" \
+    | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;'
+  done \
+  | sed -E ':X;$!{N;bX}; s;\n;\;;g'
+}
+
+ics="$(pdi_load "$tempfile")"
+
+tzid=$(cat /etc/timezone)
+
+ics="$(pdi_update_attrib "$ics" DTSTAMP 1 "TZID=${tzid}")"
+ics="$(pdi_update_value  "$ics" DTSTAMP 1 "$(TZ="$tzid" date +%Y%m%dT%H%M%S)")"
+
+dts_year="$(  POST DTS_YEAR    |grep -m1 -xE '[0-9]{4}' || date +%Y)"
+dts_month="$( POST DTS_MONTH   |grep -m1 -xE '0[1-9]|1[012]' || date +%m)"
+dts_day="$(   POST DTS_DAY     |grep -m1 -xE '0[1-9]|[12][0-9]|3[01]' || date +%d)"
+dts_hour="$(  POST DTS_HOUR    |grep -m1 -xE '[0-9]|1[0-9]|2[0-3]' || date +%H)"
+dts_minute="$(POST DTS_MINUTE  |grep -m1 -xE '[0-9]|[1-5][0-9]' || date +%M)"
+[ ${#dts_hour}   -eq 1 ] && dts_minute="0$dts_hour"
+[ ${#dts_minute} -eq 1 ] && dts_minute="0$dts_minute"
+DTSTART="${dts_year}${dts_month}${dts_day}T${dts_hour}${dts_minute}00"
+
+ics="$(pdi_update_attrib "$ics" DTSTART 1 "TZID=${tzid}")"
+ics="$(pdi_update_value  "$ics" DTSTART 1 "$DTSTART")"
+
+rr_int=$(  POST RRULE_INTERVAL |grep -m1 -xE '[0-9]+' || printf 1)
+rr_count=$(POST RRULE_COUNT    |grep -m1 -xE '[0-9]+' || printf 1)
+rr_freq=$( POST RRULE_FREQ     |grep -m1 -xE 'DAILY|WEEKLY|MONTHLY|YEARLY' || printf MONTHLY)
+rr_uy=$(   POST RRULE_UYEAR    |grep -m1 -xE '[0-9]{4}' || date +%Y)
+rr_um=$(   POST RRULE_UMONTH   |grep -m1 -xE '[1-9]|1[012]' || date +%m)
+rr_ud=$(   POST RRULE_UDAY     |grep -m1 -xE '[1-9]|[12][0-9]|3[01]' || date +%d)
+[ ${#rr_um} -eq 1 ] && rr_um="0$rr_um"
+[ ${#rr_ud} -eq 1 ] && rr_ud="0$rr_ud"
+
+case $(POST RRULE_LIMIT) in
+  COUNT)   RRULE="FREQ=$rr_freq;INTERVAL=$rr_int;COUNT=$rr_count";;
+  UNTIL)   RRULE="FREQ=$rr_freq;INTERVAL=$rr_int;UNTIL=${rr_uy}${rr_um}${rr_ud}T000000Z";;
+  ETERN|*) RRULE="FREQ=$rr_freq;INTERVAL=$rr_int";;
+esac
+
+ics="$(pdi_update_value  "$ics" RRULE 1 "$RRULE")"
+
+for field in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*'); do
+  for cnt in $(seq 1 $(POST_COUNT "$field")); do
+    case "$field" in
+      *)
+         ics="$(pdi_update_value "$ics" "$field" "$cnt" "$(vcf_escape "$(POST "$field" "$cnt")")")"
+        ;;
+    esac
+done; done
+
+# delete fields, first mark for deletion using delete_key
+# this way the field enumeration is preserved during the process
+# finally filter marked lines
+delete_key="$(randomid)"
+for delete in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*_delete_[0-9]+'); do
+  f="${delete%%_*}"; c="${delete##*_}";
+  [ "$(POST "$delete")" = "true" ] && ics="$(pdi_update_value "$ics" "$f" "$c" "delete=${delete_key}")"
+done
+ics="$(printf '%s\n' "$ics" |sed -E "/^[^:]+:delete=${delete_key}\$/d")"
+
+case "$(POST action)" in
+  addfield)
+    newfield="$(POST newfield |grep -m 1 -xE '[A-Z][A-Z0-9-]*')"
+    ics="$(pdi_update_value "$ics" "$newfield" $(( $(pdi_count "$ics" "$newfield") + 1 )) '')"
+    printf '%s' "$ics" |grep -vx '' >"$tempfile"
+    REDIRECT "${_BASE}/courses/?e=${course}"
+    ;;
+  addfield\ [A-Z]*)
+    newfield="$(POST action |sed -nE '1s;^addfield ([A-Z][A-Z0-9-]*)$;\1;p')"
+    ics="$(pdi_update_value "$ics" "$newfield" $(( $(pdi_count "$ics" "$newfield") + 1 )) '')"
+    printf '%s' "$ics" |grep -vx '' >"$tempfile"
+    REDIRECT "${_BASE}/courses/?e=${course}"
+    ;;
+  update)
+    if LOCK "$attfile"; then
+      grep -F "${course}       " "$attfile" |while read junk card; do
+        touch "$_DATA/vcard/${card}"
+      done
+      sed -E -i "/^${course}   .+\$/d" "$attfile"
+      seq 1 $(POST_COUNT attendance) |while read n; do
+        printf '%s     %s\n' "$course" "$(POST attendance $n)"
+      done >>"$attfile"
+      grep -F "${course}       " "$attfile" |while read junk card; do
+        touch "$_DATA/vcard/${card}"
+      done
+      RELEASE "$attfile"
+    else
+      SET_COOKIE 0 message="COULD NOT UPDATE COURSE MAPPINGS"
+    fi
+
+    printf '%s' "$ics" |grep -vx '' >"${tempfile}.cp"
+    mv "${tempfile}.cp" "$coursefile"
+    RELEASE_SLOCK "$coursefile"
+    REDIRECT "${_BASE}/courses/#${course}"
+    ;;
+  cancel)
+    RELEASE_SLOCK "$coursefile"
+    [ -f "$coursefile" ] \
+    && REDIRECT "${_BASE}/courses/#${course}" \
+    || REDIRECT "${_BASE}/courses/"
+    ;;
+  delete)
+    rm "$coursefile"
+    RELEASE_SLOCK "$coursefile"
+    REDIRECT "${_BASE}/courses/"
+    ;;
+  *)
+    printf '%s' "$ics" |grep -vx '' >"$tempfile"
+    REDIRECT "${_BASE}/courses/?e=${course}"
+    ;;
+esac
diff --git a/courses/widgets.sh b/courses/widgets.sh
new file mode 100755 (executable)
index 0000000..fee7690
--- /dev/null
@@ -0,0 +1,237 @@
+# Copyright 2014, 2019, 2020 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+check(){
+  [ "$1" = "$2" ] && printf 'checked="checked"'
+}
+
+edit="$(GET e)"
+order="$(GET o |grep -m1 -xE 'DOW|TOD')"
+
+w_sort_courses(){
+  cat <<-EOF
+       [form .sort .search action="?" method="GET"
+          [fieldset .order [legend $(l10n sort_order):]
+           [radio "order" "DOW" $(check $order DOW) $(l10n order_DOW)]
+           [radio "order" "TOD" $(check $order TOD) $(l10n order_TOD)]
+          ]
+         [submit "" "" $(l10n order_apply)]
+       ]
+       EOF
+}
+
+cal_date(){
+  { [ $# -eq 0 ] && cat || printf %s "$*"; } |sed -nE '
+    2q
+    s/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$/\1-\2-\3 \4:\5:\6 UTC/p;t
+    s/^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$/TZ="\1" \2-\3-\4 \5:\6:\7/p;t
+    s/^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$/\1-\2-\3 \4:\5:\6/p;t
+  '
+}
+
+cal_item(){
+  local course="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$course" "$item")"
+
+    case $item in
+      SUMMARY)
+        printf '[h2 . %s]' "$(pdi_value "$course" SUMMARY)"
+        ;;
+      DTSTART)
+        printf '[span .text .DTSTART %s %s ]' \
+               "$(LANG=de_DE.UTF-8 date -d "$(pdi_value "$course" DTSTART |cal_date)" '+%A, %d. %B %Y - %H:%M')" \
+               "$(l10n t_oclock)"
+        ;;
+      RRULE)
+        dts_date="$(pdi_value "$course" DTSTART |cal_date)"
+        rrule=" $(pdi_value "$course" RRULE)"
+        rr_int="${rrule##*INTERVAL=}"; rr_int="${rr_int%%;*}"
+        rr_count="${rrule##*COUNT=}"; rr_count="${rr_count%%;*}"
+        rr_freq="${rrule##*FREQ=}"; rr_freq="${rr_freq%%;*}"
+        rr_until="${rrule##*UNTIL=}"; rr_until="${rr_until%%;*}"
+        rr_until="$(cal_date "${rr_until}")"
+
+        [ "$rr_int" -eq 1 ] \
+        && printf '[span .text .RRULE %s]' "$(l10n "s$rr_freq")" \
+        || printf '[span .text .RRULE %s %s %s]' "$(l10n t_every)" "${rr_int}" "$(l10n $rr_freq)"
+        case "$rrule $rr_freq" in
+          *COUNT*DAILY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) day" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *COUNT*WEEKLY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) week" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *COUNT*MONTHLY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) month" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *COUNT*YEARLY*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$dts_date + $((rr_int * rr_count)) year" "+%A %B %d, %Y - %H:%M")"
+            ;;
+          *UNTIL*)
+            printf '[span .text %s %s]' "$(l10n t_until)" "$(date -d "$rr_until" "+%A %B %d, %Y - %H:%M")"
+            ;;
+        esac
+        ;;
+      attendance);;
+      COMMENT)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[p .item .%s . %s]' "$item" \
+                 "$(pdi_value "$course" "$item" $c |unescape |HTML)"
+        done
+        ;;
+      *)[ $cnt -gt 0 ] && printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[span .item .%s . %s]' "$item" \
+                 "$(pdi_value "$course" "$item" $c |unescape |HTML)"
+        done
+        ;;
+    esac
+  done
+}
+
+edit_item(){
+  local course="$1"
+  local item cnt c
+  shift 1
+
+  for item in $@; do
+    cnt="$(pdi_count "$course" "$item")"
+    [ "$cnt" -lt 1 ] && cnt=1
+
+    case $item in
+      DTSTART)
+        local dtstart="$(pdi_value "$course" DTSTART |cal_date)"
+        local ystart="${dtstart%%-*}"; ystart="${ystart##* }"
+        local mstart="${dtstart#*-}"; mstart="${mstart%%-*}"
+        local dstart="${dtstart##*-}"; dstart="${dstart%% *}"
+        local hhstart="${dtstart##* }"; hhstart="${hhstart%%:*}"
+        local mmstart="${dtstart##* }"; mmstart="${mmstart#*:}"; mmstart="${mmstart%:*}"
+        local m mn cdow d
+
+        cat <<-EOF
+       [h3 . $(l10n DTSTART)]
+       [input type="number" name="DTS_YEAR" value="${ystart}" placeholder="$(l10n YYYY)"]
+       [select name="DTS_MONTH" onchange="this.form.submit();"
+       $(m=1; for mn in $(l10n January February March April May June July August September October November December); do
+         printf '  [option value="%02i" %s . %s]\n' $m "$(selected $m $mstart)" "$mn"
+         m=$((m+1))
+       done)
+       ][submit "DTS" "update" . $(l10n edit_dtscal)]
+       [table .dtscalt
+         [tr $(printf '[th . %s]' $(l10n Mon Tue Wed Thu Fri Sat Sun))]
+         [tr $(
+       local cdow d
+       cdow="$(date -d ${ystart}-${mstart}-1 +%u)"
+       seq 2 $cdow |xargs -n1 printf '[td .padding .%s]'
+       d=1; while [ "$d" -lt 29 ] || [ "$(date -d ${ystart}-${mstart}-${d} +%m)" -eq "$mstart" ]; do
+         [ $cdow -eq 1 -a $d -ne 1 ] && printf ']\n  [tr '
+         printf '[td [input type="radio" name="DTS_DAY" #DTSCAL_%i value="%02i" %s][label for="DTSCAL_%i" %i]]' \
+                $d $d "$(checked $d $dstart)" $d $d
+         d=$((d + 1)); cdow=$(((cdow + 1) % 7))
+       done 2>/dev/null
+       )]
+       ]
+       [label .DTSTIME $(l10n time):]
+       [input type="number" name="DTS_HOUR" value="$hhstart" min="0" max="23"]:[input type="number" name="DTS_MINUTE" value="$mmstart" min="0" max="59"]
+       EOF
+        ;;
+      RRULE)
+        local dtstart="$(pdi_value "$course" DTSTART |cal_date)"
+        local ystart="${dtstart%%-*}"; ystart="${ystart##* }"
+        local mstart="${dtstart#*-}"; mstart="${mstart%%-*}"
+        local dstart="${dtstart##*-}"; dstart="${dstart%% *}"
+
+        local rrule="$(pdi_value "$course" RRULE)"
+        local   rr_int="$(printf %s "$rrule" |sed -nE 's;^(.*\;[        ]*)?INTERVAL=([0-9]+)(\;.*)?$;\2;p')"
+        local rr_count="$(printf %s "$rrule" |sed -nE 's;^(.*\;[        ]*)?COUNT=([0-9]+)(\;.*)?$;\2;p')"
+        local  rr_freq="$(printf %s "$rrule" |sed -nE 's;^(.*\;[        ]*)?FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)(\;.*)?$;\2;p')"
+        local rr_until="$(printf %s "$rrule" |sed -nE 's;^(.*\;[        ]*)?UNTIL=([0-9]{8}T[0-9]{6}Z)(\;.*)?$;\2;p')"
+        local rr_uyear="${rr_until%????T??????Z}"
+        local rr_umonth=${rr_until#????}; rr_umonth="${rr_umonth%??T??????Z}"
+        local   rr_uday=${rr_until#??????}; rr_uday="${rr_uday%T??????Z}"
+        local  rr_limit="ETERN"
+        [ "$rr_count" ] && [ "$rr_count" -ge 0 ] && rr_limit="COUNT"
+        [ "$rr_uyear" ] && [ "$rr_uyear" -ge 0 ] && rr_limit="UNTIL"
+
+       cat <<-EOF
+       [h3 . $(l10n "$item")]
+       [span .item . $(l10n t_every)
+       [input type="number" .RRULE .INTERVAL name="RRULE_INTERVAL" placeholder="#N" value="${rr_int:-1}" min="1"]
+       [select .RRULE .FREQ name="RRULE_FREQ"
+        $(for f in DAILY WEEKLY MONTHLY YEARLY; do
+          printf '  [option value="%s" %s . %s]\n' "$f" "$(selected $f "$rr_freq")" "$(l10n $f)"
+        done)
+       ]]
+       [label .item [input type="radio" name="RRULE_LIMIT" value="ETERN" $(checked "$rr_limit" ETERN)] $(l10n t_eternal)]
+       [label .item
+         [input type="radio" name="RRULE_LIMIT" value="COUNT" $(checked "$rr_limit" COUNT)]
+         [input type="number" .RRULE .COUNT name="RRULE_COUNT" placeholder="#N" value="${rr_count:-1}" min="1"] $(l10n t_times)
+       ]
+       [label .item
+         [input type="radio" name="RRULE_LIMIT" value="UNTIL" $(checked "$rr_limit" UNTIL)] $(l10n t_until)
+         [input type="number" .RRULE .UYEAR name="RRULE_UYEAR" placeholder="$(l10n YYYY)" value="${rr_uyear:-$ystart}" min="$ystart"]
+         [input type="number" .RRULE .UMONTH name="RRULE_UMONTH" placeholder="$(l10n MM)" value="${rr_umonth:-$mstart}" min="1" max="12"]
+         [input type="number" .RRULE .UDAY name="RRULE_UDAY" placeholder="$(l10n DD)" value="${rr_uday:-$dstart}" min="1" max="31"]
+       ]
+       EOF
+        ;;
+      COMMENT)
+        printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '<textarea class="item %s" name="%s">%s</textarea>' \
+            "$item" "$item" "$(pdi_value "$course" "$item" $c |unescape |HTML)"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+      attendance)
+        printf '[h3 %s]' "$(l10n course_attendance)"
+        printf '[div .attendance\n'
+        for vcf in "$_DATA"/vcard/*.vcf; do
+          fn="$(pdi_value "$(pdi_load "$vcf")" FN)"
+          printf '%s/%s\n' "${vcf##*/}" "$fn"
+        done \
+        | sort -t/ -k2 \
+        | while IFS=/ read -r vcf fn; do
+          printf '[span .item [input type="checkbox" id="att%s" name="attendance" value="%s" %s][label for="att%s" . %s]]' \
+                 "$vcf" "$vcf" "$(grep -qxF "${coursefile##*/} $vcf" "$_DATA/mappings/attendance" && printf 'checked="checked"')" "$vcf" "$fn"
+        done
+        printf ']'
+        ;;
+      SUMMARY)
+        printf '[h3 %s]' "$(l10n "$item")"
+        printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \
+               "$item" "$item" "$(pdi_value "$course" "$item" |unescape |HTML)" "$(l10n "$item")"
+        ;;
+      *)
+        printf '[h3 %s]' "$(l10n "$item")"
+        seq 1 $cnt |while read c; do
+          printf '[checkbox "%s_delete_%i" "true" .delete #%s_delete_%i][label for="%s_delete_%i" %s]' \
+            "$item" $c "$item" $c "$item" $c "$(l10n delete)"
+          printf '[input .item .%s name="%s" value="%s" placeholder="%s"]' \
+            "$item" "$item" "$(pdi_value "$course" "$item" $c |unescape |HTML)" "$(l10n "$item")"
+        done
+        printf '[button type="submit" name="action" value="addfield %s" %s ]' "$item" "$(l10n edit_addfield)"
+        ;;
+    esac
+  done
+}
diff --git a/globals.sh b/globals.sh
deleted file mode 100755 (executable)
index 36498e1..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014 - 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-date() {
-  LANG=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 /bin/date $@
-}
-
-PROFILE=$(validate "$PROFILE" 'medical|circus' circus )
-
-PAGE="${_GET[p]:-${PAGE:-cards}}"
-
-uuidgenerator(){
-  head -c16 /dev/urandom |sha1sum - |cut -c1-32
-}
-
-_checked=''
-checked(){
-  if [ "$_checked" = "$1" ]; then
-    printf 'checked="checked"'
-  fi
-}
-
-
-VCF_FIELDS=(PHOTO LOGO FN NICKNAME SOUND GENDER KIND TITLE ROLE ORG MEMBER CATEGORIES ANNIVERSARY BDAY EMAIL TEL IMPP ADR URL LANG NOTE RELATED X-ZACK-JOINDATE X-ZACK-LEAVEDATE X-HEALTH-INSURANCE X-HEALTH-INSURANCE-NOCONTRIB X-CLIENT-REFERRAL)
-
-case "${PROFILE}" in
-medical)
-  data_dirs vcard cache temp mappings prescriptions therapies
-  [ -z "$NAVIGATION" ] && NAVIGATION=(cards)
-;;
-circus)
-  data_dirs vcard ical cache temp mappings
-  [ -z "$NAVIGATION" ] && NAVIGATION=(cards courses)
-;;
-esac
diff --git a/index.cgi b/index.cgi
new file mode 100755 (executable)
index 0000000..71edd85
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+for n in "$@"; do case ${n%%=*} in
+  data) _DATA="${n#data=}";;
+  exec) _EXEC="${n#exec=}";;
+  base) _BASE="${n#base=}";;
+  debug) DEBUG="${n#debug=}";;
+esac; done
+
+[ ! "${_EXEC%/}" ] && _EXEC="$(realpath "${0%/*}")" || _EXEC="${_EXEC%/}"
+[ ! "${_DATA%/}" ] && _DATA="${PWD%/}" || _DATA="${_DATA%/}"
+_BASE="${_BASE%/}"
+[ "$DEBUG" ] && exec 2>>"$DEBUG"
+
+mkdir -p "${_DATA}/cache" "${_DATA}/mappings" "${_DATA}/export" "${_DATA}/lock" "${_DATA}/ical" "${_DATA}/vcard"
+
+debug() {
+  local dbg=/dev/stderr
+  if [ ! "$DEBUG" ]; then
+    [ "$#" -gt 0 ] && : || cat;
+  elif [ "$#" -gt 0 ]; then
+    printf '%s\n' "$@" >>"$dbg"
+  else
+    tee -a "$dbg"
+  fi
+}
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/cgilite/session.sh"
+
+. "$_EXEC/l10n.sh"
+
+_PATH="$(PATH "/${PATH_INFO}")"
+_PATH="${_PATH#${_BASE}}"
+ACTION="$(GET a)"
+
+SESSION_COOKIE
+
+message="$(COOKIE message)"
+[ "$message" ] && SET_COOKIE 0 message=''
+
+checked(){
+  if [ "$1" = "$2" ] || [ "$1" -eq "$2" ]; then
+    printf 'checked="checked"'
+  fi 2>/dev/null
+}
+selected(){
+  if [ "$1" = "$2" ] || [ "$1" -eq "$2" ]; then
+    printf 'selected="selected"'
+  fi 2>/dev/null
+}
+
+yield_page() {
+  local class="$1" style="$2"
+  printf 'Content-Type: text/html; charset=utf-8\r\n\r\n'
+  { printf '
+       [html [head
+          [title Confetti]
+         [meta name="viewport" content="width=device-width"]
+         [link rel="stylesheet" type="text/css" href="%s/cgilite/common.css"]
+         [link rel="stylesheet" type="text/css" href="%s/style.css"]
+    ' "${_BASE}" "${_BASE}"
+    [ -n "$style" ] && printf '
+         [link rel="stylesheet" type="text/css" href="%s"]
+    ' "$style"
+    printf '
+       ] [body #top class="%s"
+    ' "$class"
+    printf '[ul .menu [li [a "%s/cards/" . %s]][li [a "%s/courses/" . %s]][li [a "%s/ledgers/" . %s]]]' \
+           "${_BASE}" "$(l10n cards)" "${_BASE}" "$(l10n courses)" "${_BASE}" "$(l10n ledgers)"
+    [ "$message" ] && printf '[p #message\n%s\n]' "$(l10n "$message")"
+    cat
+    printf '] ]'
+  } \
+  | "${_EXEC}/cgilite/html-sh.sed"
+}
+
+topdir="${_PATH#/}"
+topdir="/${topdir%%/*}"
+
+case ${_PATH} in
+  "/") REDIRECT "${_BASE}/cards/"
+    ;;
+  "/export"/*.pdf) . "$_EXEC/cgilite/file.sh"
+    FILE "${_DATA}/${_PATH}" "application/pdf"
+    ;;
+  "/export"/*) . "$_EXEC/cgilite/file.sh"
+    FILE "${_DATA}/${_PATH}"
+    ;;
+  *)
+    if   [ -d "${_EXEC}/${_PATH}" -a -x "${_EXEC}/${_PATH}/index.cgi" ]; then
+      . "${_EXEC}/${_PATH}/index.cgi"
+    elif [ -f "${_EXEC}/${_PATH}" -a -x "${_EXEC}/${_PATH}" ]; then
+      . "${_EXEC}/${_PATH}"
+    elif [ -f "${_EXEC}/${_PATH}" -a -r "${_EXEC}/${_PATH}" ]; then
+      . "$_EXEC/cgilite/file.sh"
+      FILE "${_EXEC}/${_PATH}"
+    elif [ -d "${_EXEC}/${topdir}" -a -x "${_EXEC}/${topdir}/index.cgi" ]; then
+      . "${_EXEC}/${topdir}/index.cgi"
+    else
+      printf '%s\r\n' 'Status: 404 Not Found' 'Content-Length: 0' ''
+    fi
+    ;;
+esac
diff --git a/l10n.sh b/l10n.sh
new file mode 100755 (executable)
index 0000000..d9e01d0
--- /dev/null
+++ b/l10n.sh
@@ -0,0 +1,179 @@
+# Copyright 2014, 2016, 2019, 2021 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+l10n(){
+  local word
+  [ $# -eq 0 ] && read -r word || word="$*"
+  l10n_global "$word"
+}
+
+l10n_global() {
+  case $1 in
+    # Nav Menu
+    cards) printf %s "Teil&shy;neh&shy;mende";;
+    courses) printf %s "Kurse";;
+    ledgers) printf %s "Bei&shy;trä&shy;ge";;
+
+    # VCF Default
+    PHOTO) printf %s "Foto";;
+    LOGO) printf %s "Logo";;
+    FN) printf %s "Voller Name";;
+    N) printf %s "Name";;
+    n_pre) printf %s "Titel";;
+    n_first) printf %s "Vorname";;
+    n_middle) printf %s "Mittel&shy;namen";;
+    n_last) printf %s "Nachname";;
+    n_post) printf %s "Zusätze";;
+    NICKNAME) printf %s "Spitz&shy;name";;
+    SOUND) printf %s "Aus&shy;sprache";;
+    GENDER) printf %s "Ge&shy;schlecht";;
+    KIND) printf %s "Typ";;
+    TITLE) printf %s "Beruf";;
+    ROLE) printf %s "Position";;
+    ORG) printf %s "Orga&shy;ni&shy;sation";;
+    MEMBER) printf %s "Mitglied";;
+    CATEGORIES) printf %s "Kategorien";;
+    ANNIVERSARY) printf %s "Jubiläum";;
+    BDAY) printf %s "Geburtstag";;
+    EMAIL) printf %s "E-Mail";;
+    TEL) printf %s "Telefon";;
+    teltype) printf %s "Anschluss&shy;typ:";;
+    TYPE=HOME) printf %s "Privat";;
+    TYPE=WORK) printf %s "Geschäft&shy;lich";;
+    TYPE=CELL) printf %s "Mobil";;
+    TYPE=FAX) printf %s "Fax";;
+    IMPP) printf %s "Chat";;
+    ADR) printf %s "Anschrift";;
+    URL) printf %s "Webseite";;
+    LANG) printf %s "Sprache";;
+    NOTE) printf %s "Notiz";;
+    RELATED) printf %s "Kontakte";;
+
+    # ICS Default
+    SUMMARY) printf "Bezeichnung";;
+    COMMENT) printf "Kommentar";;
+    DTSTART) printf "Beginn";;
+    DURATION) printf "Dauer";;
+    RRULE) printf "Regelmäßigkeit";;
+    DAILY) printf "Tage";;
+    WEEKLY) printf "Wochen";;
+    MONTHLY) printf "Monate";;
+    YEARLY) printf "Jahre";;
+    sDAILY) printf "Täglich";;
+    sWEEKLY) printf "Wöchentlich";;
+    sMONTHLY) printf "Monatlich";;
+    sYEARLY) printf "Jährlich";;
+
+    # UI labels
+    year) printf %s "Jahr";;
+    month) printf %s "Monat";;
+    day) printf %s "Tag";;
+    edit) printf %s "Bearbeiten";;
+    edit_categories) printf %s "Kategorien Bearbeiten";;
+    vcf_export) printf %s "Vcard Exportieren";;
+    control) printf %s "Aktionen";;
+    delete) printf %s "entfernen";;
+    edit_update) printf %s "Daten übernehmen";;
+    edit_cancel) printf %s "Abbrechen";;
+    edit_delete) printf %s "Eintrag löschen";;
+    edit_addfieldtext) printf %s "Neues Feld";;
+    edit_addfield) printf %s "+";;
+    edit_deletefield) printf %s "X";;
+
+    filter_label) printf %s "Filter";;
+    filter_item) printf %s "Eingrenzung nach";;
+    filter_placeholder) printf %s "Begriffe zur Eingrenzung eingeben";;
+    filter_type) printf %s "Filter&shy;typ";;
+    filter_order) printf %s "Sortie&shy;rung";;
+    filter_any) printf %s "Alles";;
+    filter_name) printf %s "Name";;
+    filter_firstname) printf %s "Vor&shy;name";;
+    filter_lastname) printf %s "Nach&shy;name";;
+    filter_street) printf %s "Straße";;
+    filter_zip) printf %s "PLZ.";;
+    filter_TEL) printf %s "Tele&shy;fon";;
+    filter_BDAY) printf %s "Geburts&shy;jahr";;
+    filter_bdate) printf %s "Geburts&shy;datum";;
+    filter_course) printf %s "Kurs";;
+    filter_CATEGORIES) printf %s "Kate&shy;go&shy;rien";;
+    filter_more) printf %s "+ mehr Filter";;
+    filter_apply) printf %s "Filtern";;
+    filter_cancel) printf %s "Filter löschen";;
+    export_csv) printf %s "Liste als CSV-Datei";;
+
+    # UI Labels Special
+    course_attendance) printf %s "Kurs&shy;teil&shy;nahme";;
+    vcf_seed_label) printf "Anmeld.    Vorn.   Nachn.  Geb.Tag Geb.Monat       Geb.Jahr        Tel.    Mobil   ()      EMail   ()      Notiz";;
+    '(unnamed course)') printf '(Unbe\302\255nannter Kurs)';;
+  
+    gender_none) printf %s "keine Angabe";;
+    gender_female) printf %s "Weiblich";;
+    gender_male) printf %s "Männlich";;
+    gender_other) printf %s "Sonstiges";;
+  
+    female) printf %s "&#x2640;";;
+    male) printf %s "&#x2642;";;
+    other) printf %s "&#x26A5;";;
+    none) printf %s "&#x26AA;";;
+
+    # Fallback
+    *) printf %s "$word";;
+  esac
+}
+
+l10n_time() {
+  [ $# -eq 0 ] && read -r time || time="$*"
+  printf '%s\n' "$time" |sed -E '
+    s;Monday;Mon\&shy\;tag;g;          s;Mon\.;Mo.;g;
+    s;Tuesday;Diens\&shy\;tag;g;       s;Tue\.;Di.;g;
+    s;Wednesday;Mitt\&shy\;woch;g;     s;Wed\.;Mi.;g;
+    s;Thursday;Don\&shy\;ners\&shy\;tag;g; s;Thu\.;Do.;g;
+    s;Friday;Frei\&shy\;tag;g;         s;Fri\.;Fr.;g;
+    s;Saturday;Sams\&shy\;tag;g;       s;Sat\.;Sa.;g;
+    s;Sunday;Sonn\&shy\;tag;g;         s;Sun\.;So.;g;
+
+    s;January;Ja\&shy\;nu\&shy\;ar;g;          s;Jan\.;Jan.;g;
+    s;February;Fe\&shy\;bru\&shy\;ar;g;                s;Feb\.;Feb.;g;
+    s;March;März;g;                           s;Mar\.;Mär.;g;
+    s;April;April;g;                           s;Apr\.;Apr.;g;
+    s;May;Mai;g;                               s;May\.;Mai.;g;
+    s;June;Juni;g;                             s;Jun\.;Jun.;g;
+    s;July;Juli;g;                             s;Jul\.;Jul.;g;
+    s;August;Au\&shy\;gust;g;                  s;Aug\.;Aug.;g;
+    s;September;Sep\&shy\;tem\&shy\;ber;g;     s;Sep\.;Sep.;g;
+    s;October;Ok\&shy\;to\&shy\;ber;g;         s;Oct\.;Okt.;g;
+    s;November;No\&shy\;vem\&shy\;ber;g;       s;Nov\.;Nov.;g;
+    s;December;De\&shy\;zem\&shy\;ber;g;       s;Dec\.;Dez.;g;
+  '
+}
+
+parse_date() {
+  [ $# -eq 0 ] && read -r date || date="$*"
+
+  case $date in
+    *[0-9].*[0-9].*[0-9])
+      d="${date%%.*}"
+      y="${date##*.}"
+      m="${date%.*}"
+      m="${m#*.}"
+      [ $y -lt 100 ] && y="$((y + 2000))"
+      date -d "$(printf '%04i-%02i-%02i' "$y" "$m" "$d")" +%F
+      ;;
+    *) date -d "$date" +%F
+      ;;
+  esac
+}
diff --git a/ledgers/csv_upload.awk b/ledgers/csv_upload.awk
new file mode 100755 (executable)
index 0000000..04de4d5
--- /dev/null
@@ -0,0 +1,100 @@
+#!/bin/awk -f
+
+function STRING( inp ) {
+  gsub(/\\/, "\\\\", inp);
+  gsub(/\n/, "\\n",  inp);
+  gsub(/\r/, "\\r",  inp);
+  gsub(/\t/, "\\t",  inp);
+  gsub(/\+/, "\\+",  inp);
+  gsub(/ /,  "+",    inp);
+  return inp ? inp : "\\";
+}
+
+function UNSTRING( inp,    out, tmp ) {
+  while ( inp ) {
+         if ( inp ~ /^\\\\/) { out = out "\\"; sub(/^\\\\/, "", inp); }
+    else if ( inp ~ /^\\n/)  { out = out "\n"; sub(/^\\n/,  "", inp); }
+    else if ( inp ~ /^\\r/)  { out = out "\r"; sub(/^\\r/,  "", inp); }
+    else if ( inp ~ /^\\t/)  { out = out "\t"; sub(/^\\t/,  "", inp); }
+    else if ( inp ~ /^\\+/)  { out = out "+";  sub(/^\\+/,  "", inp); }
+    else if ( inp ~ /^\+/)   { out = out " ";  sub(/^\+/,   "", inp); }
+    else if ( inp ~ /^\\/)   { out = out "";   sub(/^\+/,   "", inp); }
+    else { tmp = inp; sub(/[\\+].*$/, "", tmp); out = out tmp; sub(/^[^\\+]*/, "", inp); }
+  }
+  return out;
+}
+
+function isdate( date,    dt, y, m, d ) {
+         if ( match( date, 
+    /^[0-9]{4}-((01|03|05|07|08|10|12)-(0[1-9]|[12][0-9]|3[01])|(04|06|09|11)-(0[1-9]|[12][0-9]|30)|02-(0[1-9]|[12][0-9]))$/ )) {
+    split( date, dt, "-");
+    y = dt[1]; m = dt[2]; d = dt[3];
+
+  } else if ( match( date,
+    /^((0?1|0?3|0?5|0?7|0?8|10|12)\/(0?[1-9]|[12][0-9]|3[01])|(0?4|0?6|0?9|11)\/(0?[1-9]|[12][0-9]|30)|0?2\/(0[1-9]|[12][0-9]))\/([0-9]{2}|[0-9]{4})$/ )) {
+    split( date, dt, "/");
+    m = dt[1]; d = dt[2]; y = dt[3];
+
+  } else if ( match( date,
+    /^((0?[1-9]|[12][0-9]|3[01])[\.\/](0?1|0?3|0?5|0?7|0?8|10|12)|(0?[1-9]|[12][0-9]|30)[\.\/](0?4|0?6|0?9|11)|(0[1-9]|[12][0-9])[\.\/]0?2)[\.\/]([0-9]{2}|[0-9]{4})$/ )) {
+    split( date, dt, /[\.\/]/);
+    d = dt[1]; m = dt[2]; y = dt[3];
+
+  } else return "";
+
+  if ( y < 100 && y >  50 ) y = y + 1900;
+  if (            y <= 50 ) y = y + 2000;
+
+  # leap year
+  if ( m == 2 && d == 29 ) {
+         if ( y % 400 == 0 ) y = y;
+    else if ( y % 100 == 0 ) return "";
+    else if ( y %   4 == 0 ) y = y;
+    else return "";
+  }
+
+  return sprintf("%04i-%02i-%02i", y, m, d);
+}
+
+function cents( val ) {
+  gsub(/\./, "", val); sub(/,/, ".", val);
+  return val * 100;
+}
+
+BEGIN {
+  FS = ";";
+  dtrange_end = dt_from = dt_to = balance_start = balance_end = "";
+  split("", rec);
+  rec[0] = "Date       DateU   IBAN    Name    Subject Amount"
+}
+
+/^([012]?[0-9]|30|31).(0?[1-9]|1[012]).[0-9]{4} - ([012]?[0-9]|30|31).(0?[1-9]|1[012]).[0-9]{4}$/ {
+  dtrange_end = $0; sub(/^.* - /, "", dtrange_end);
+  dt_from = $0; sub(/ - .*$/, "", dt_from); dt_from = isdate(dt_from);
+  dt_to   = $0; sub(/^.* - /, "", dt_to  ); dt_to   = isdate(dt_to  );
+}
+
+/^Letzter Kontostand;;;;[0-9\.,]+;EUR$/ {
+  balance_start = cents($5);
+}
+
+/Kontostand;[^;]+;;;[0-9\.,]+;EUR/ {
+  if ( $2 = dtrange_end ) balance_end = cents($5)
+}
+
+$18 == "EUR" {
+  rec_date = isdate($1); gsub(/-/, " ", rec_date); rec_date = mktime(rec_date " 00 00 00", "UTC");
+  rec[length(rec)] = sprintf("%s       %i      %s      %s      %s      %i",
+                             isdate($1), rec_date, $6 ? $6 : "\\", STRING($4), STRING($5), cents($12));
+}
+
+END {
+  if ( dt_from && dt_to ) {
+    dtu_from = dt_from; gsub(/-/, " ", dtu_from); dtu_from = mktime( dtu_from " 00 00 00", "UTC");
+    dtu_to   = dt_to  ; gsub(/-/, " ", dtu_to  ); dtu_to   = mktime( dtu_to   " 00 00 00", "UTC");
+
+    printf "%i %s      %i      %s      %i      %i\n",
+           dtu_from, dt_from, dtu_to, dt_to, balance_start, balance_end;
+    for ( k = 1; k < length(rec); k++ ) print rec[k];
+  }
+}
diff --git a/ledgers/csv_upload.sh b/ledgers/csv_upload.sh
new file mode 100755 (executable)
index 0000000..4d25b5c
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+# Copyright 2024 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+if [ "${CONTENT_TYPE%%;*}" != "multipart/form-data" ]; then
+  SET_COOKIE 0 message="Not an upload"
+  REDIRECT "${_BASE}/ledgers/"
+fi
+
+. "$_EXEC/multipart.sh"
+multipart_cache
+
+# Validate session id from form to prevent CSRF
+if [ "$(multipart session_id)" != "$SESSION_ID" ]; then
+  rm -- "$multipart_cachefile"
+  SET_COOKIE 0 message="INVALID SESSION ID IN FORM"
+  REDIRECT "${_BASE}/ledgers/"
+fi
+
+mkdir -p "$_DATA/ledgers/"
+CSV="$(multipart "csv" 1 | "$_EXEC/ledgers/csv_upload.awk")"
+rm -- "$multipart_cachefile"
+
+read dtu_start dt_start dtu_end dt_end balance_start balance_end <<-EOF
+       ${CSV%%${BR}*}
+EOF
+
+if [ ! "$dtu_end" -o ! "$dtu_start" ] || [ "$dtu_end" -lt "$dtu_start" ]; then
+  SET_COOKIE 0 message="No valid date range in upload"
+else
+  num=0; while [ ! "$filename" -o -f "$_DATA/ledgers/$filename" ]; do
+    num=$((num + 1)); filename="${dt_start} - ${dt_end} - $(printf '%04i' $num).tbl"
+  done
+  printf '%s\n' "$CSV" >"$_DATA/ledgers/$filename"
+fi
+
+REDIRECT "${_BASE}/ledgers/"
diff --git a/ledgers/iban_assign.awk b/ledgers/iban_assign.awk
new file mode 100755 (executable)
index 0000000..090d950
--- /dev/null
@@ -0,0 +1,91 @@
+#!/bin/awk -f
+
+function STRING( inp ) {
+  gsub(/\\/, "\\\\", inp);
+  gsub(/\n/, "\\n",  inp);
+  gsub(/\r/, "\\r",  inp);
+  gsub(/\t/, "\\t",  inp);
+  gsub(/\+/, "\\+",  inp);
+  gsub(/ /,  "+",    inp);
+  return inp ? inp : "\\";
+}
+
+function UNSTRING( inp,    out, tmp ) {
+  while ( inp ) {
+         if ( inp ~ /^\\\\/) { out = out "\\"; sub(/^\\\\/, "", inp); }
+    else if ( inp ~ /^\\n/)  { out = out "\n"; sub(/^\\n/,  "", inp); }
+    else if ( inp ~ /^\\r/)  { out = out "\r"; sub(/^\\r/,  "", inp); }
+    else if ( inp ~ /^\\t/)  { out = out "\t"; sub(/^\\t/,  "", inp); }
+    else if ( inp ~ /^\\+/)  { out = out "+";  sub(/^\\+/,  "", inp); }
+    else if ( inp ~ /^\+/)   { out = out " ";  sub(/^\+/,   "", inp); }
+    else if ( inp ~ /^\\/)   { out = out "";   sub(/^\+/,   "", inp); }
+    else { tmp = inp; sub(/[\\+].*$/, "", tmp); out = out tmp; sub(/^[^\\+]*/, "", inp); }
+  }
+  return out;
+}
+
+function rx( regex ) {
+  gsub(/[].*+?^${}()|\\[]/, "\\\\&", regex);
+  return regex;
+}
+
+BEGIN {
+  FS = "       "; ledger = 0;
+  fn = n = uid = iban = tmp = "";
+  split("", uid_n); split("", uid_fn); split("", uid_iban);
+  split("", iban_uid); split("", ibans); split("", uids);
+
+  split("", sure); split("", unsure); split("", unknown);
+  split("", unsure_rec)
+}
+
+/^BEGIN;:VCARD$/ { fn = n = id = iban = tmp = ""; }
+
+   /^UID;[^:]*:/ { uid = $0; sub(/^[^;]+;[^:]*:/, "", uid); }
+    /^FN;[^:]*:/ {  fn = $0; sub(/^[^;]+;[^:]*:/, "",  fn); }
+     /^N;[^:]*:/ {   n = $0; sub(/^[^;]+;[^:]*:/, "",   n); sub(/;.*$/, "", n); }
+/^X-IBAN;[^:]*:/ { tmp = $0; sub(/^[^;]+;[^:]*:/, "", tmp); iban = iban ? iban " " tmp : tmp; }
+
+/^END;:VCARD$/ {
+  uid_n[uid] = n; uid_fn[uid] = fn; uid_iban[uid] = iban;
+  split(iban, ibans, / /);
+  for (iban in ibans) iban_uid[iban] = iban_uid[iban] ? iban_uid[iban] " " uid : uid;
+  fn = n = uid = iban = tmp = "";
+}
+
+strftime("%Y-%m-%d", $1, "UTC") == $2 && strftime("%Y-%m-%d", $3, "UTC") == $4 { ledger = 1; }
+
+ledger && strftime("%Y-%m-%d", $2, "UTC") == $1 {
+  if ($3 in iban_uid) {
+    sure[$3] = iban_uid[$3];
+  } else {
+    for (uid in uid_fn) if ( match(UNSTRING($5), rx(uid_fn[uid])) ) {
+      if (! match(unsure[$3], rx(uid))) unsure[$3] = unsure[$3] ? unsure[$3] " " uid : uid;
+      unsure_rec[$3] = $0
+    }
+    for (uid in uid_n) if ( uid_n[uid] && match(UNSTRING($4), rx(uid_n[uid])) ) {
+      if (! match(unsure[$3], rx(uid))) unsure[$3] = unsure[$3] ? unsure[$3] " " uid : uid;
+      unsure_rec[$3] = $0
+    }
+  }
+  if (!($3 in sure) && !($3 in unsure)) unknown[$3] = $0;
+}
+
+END {
+  for (iban in sure) {
+    line = "sure       " iban;
+    split(iban_uid[iban], uids, / /);
+    for (uid in uids) line = line "    " STRING(uid "/" uid_fn[uid]);
+    print line;
+  }
+  for (iban in unsure) {
+    line = "guess      " iban " " STRING(unsure_rec[iban]);
+    split(unsure[iban], uids, / /);
+    for (k in uids) line = line "      " STRING(uids[k] "/" uid_fn[uids[k]]);
+    print line;
+  }
+  for (iban in unknown) {
+    line = "unknown    " iban " " unknown[iban];
+    print line;
+  }
+}
diff --git a/ledgers/index.cgi b/ledgers/index.cgi
new file mode 100755 (executable)
index 0000000..87af8b1
--- /dev/null
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/storage.sh"
+. "$_EXEC/pdiread.sh"
+
+credit() {
+  printf '%03i\n' "$1" \
+  | sed -E 's;[0-9]{2}$;d&;; :0 s;([0-9])([0-9]{3}[dm]);\1m\2;; t0; y;dm;,.;'
+}
+
+{ printf '
+  [form .upload action="%s/ledgers/csv_upload.sh" method="POST" enctype="multipart/form-data"
+    [label for=ledger_upload . %s:]
+    [input #ledger_upload type="file" name="csv" accept=".csv,text/csv"]
+    [input type=hidden name=session_id value="%s"]
+    [button type="submit" %s]
+  ]' \
+  "${_BASE}" "$(l10n "Postbank CSV")" "$SESSION_ID" "$(l10n Upload)"
+  printf '
+  [form .ledgers action="%s/ledgers/delete.sh" method=POST
+    [input type=hidden name=session_id value="%s"]
+    [h3 . %s]
+  ' "${_BASE}" "$SESSION_ID" "$(l10n Ledgers)"
+  for ledger in "$_DATA"/ledgers/????-??-??\ -\ ????-??-??\ -\ ????.tbl; do
+    ledger="${ledger##*/}"
+    [ "$ledger" = "????-??-?? - ????-??-?? - ????.tbl" ] && continue
+    printf '[p .ledger . %s [button type=submit name=delete value="%s" . %s]]' \
+           "$(HTML "${ledger% - ????.tbl}")" "$(HTML "$ledger")" "$(l10n delete)"
+  done
+  printf '  ]'
+  printf '
+  [form .ibanassign action="%s/ledgers/iban_assign.sh" method=POST
+    [input type=hidden name=session_id value="%s"]
+    [h3 . %s]
+  ' "${_BASE}" "$SESSION_ID" "$(l10n "IBAN Assignments")"
+  printf '[datalist id=lattendants .'
+  pdi_load "${_DATA}"/vcard/*.vcf |sed -n '/^FN\;:/!b; s;^FN\;:;;; p;' \
+  | while read name; do
+    printf '[option value="%s"]' "$(HTML "$name")"
+  done
+  printf ']'
+  { pdi_load "${_DATA}"/vcard/*.vcf
+    cat "${_DATA}"/ledgers/????-??-??\ -\ ????-??-??\ -\ ????.tbl
+  } | "${_EXEC}"/ledgers/iban_assign.awk \
+  | while read -r state iban data; do
+    printf '[fieldset .iban .%s [legend . %s ]' \
+           "$state" "$iban"
+      if [ $state = sure ]; then
+      :
+    elif [ $state = guess ]; then
+      record="$(UNSTRING "${data%%     *}")"
+      cards="${data#*  }"
+      principal="${record#*    *       *       }" principal="${principal%%     *}"
+      subject="${record#*      *       *       *       }" subject="${subject%% *}"
+      amount="${record#*       *       *       *       *       }" amount="${amount%%   *}"
+      printf '[p .principal . %s][p .amount %s][p .subject . %s]' \
+             "$(UNSTRING "$principal" |HTML)" "$(credit "$amount")" "$(UNSTRING "$subject" |HTML)"
+      printf '[h4 . %s]' "$(l10n Guesses)"
+      for card in $cards; do
+        uid="${card%%/*}" name="$(UNSTRING "${card#*/}")"
+        printf '[input .card key="cardfn" value="%s" placeholder="%s" list=lattendants]' "$(HTML "${name}")" "$(l10n attendent)"
+      done
+    elif [ $state = unknown ]; then
+      principal="${data#*      *       *       }" principal="${principal%%     *}"
+      subject="${data#*        *       *       *       }" subject="${subject%% *}"
+      amount="${data#* *       *       *       *       }" amount="${amount%%   *}"
+      printf '[p .principal . %s][p .amount %s][p .subject . %s]' \
+             "$(UNSTRING "$principal" |HTML)" "$(credit "$amount")" "$(UNSTRING "$subject" |HTML)"
+    fi
+  printf ']'
+  done
+  printf '  ]'
+} | yield_page ledgers
diff --git a/multipart.sh b/multipart.sh
new file mode 100755 (executable)
index 0000000..02f7dfb
--- /dev/null
@@ -0,0 +1,105 @@
+#!/bin/sh
+
+[ "$include_multipart" ] && return 0
+inlude_multipart="$0"
+
+# Copyright 2022 - 2023 Paul Hänsch
+# 
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+# 
+# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+if [ "${CONTENT_TYPE}" -a ! "${CONTENT_TYPE##multipart/form-data;*}" ]; then
+  multipart_boundary="${CONTENT_TYPE#*; boundary=}"
+  multipart_boundary="${multipart_boundary%%;*}"
+  multipart_boundary="${multipart_boundary%${CR}}"
+fi
+multipart_cachefile="/tmp/multipart.$$"
+
+readbytes(){
+  # read n bytes, like `head -c` but do not consume input
+  local size="$1" block
+
+  for block in 65536 32768 16384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1; do
+    if [ $size -ge $block ]; then
+      dd status=none bs="$block" count="$((size / block))"
+      size="$((size % block))"
+    fi
+  done
+}
+
+multipart_cache() {
+  multipart_cachefile="${1:-${multipart_cachefile}}"  # global
+
+  if [ "${multipart_boundary}" ]; then
+    # readbytes "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}"
+    head -c "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}"
+  else
+    return 1
+  fi
+}
+
+multipart(){
+  local name="$1" count="${2:-1}"
+  local formdata state=begin
+
+  while IFS='' read -r formdata; do case "$formdata" in
+    "--${multipart_boundary}--${CR}")
+      [ $state = data ] && return 0 \
+                        || return 1
+      ;;
+    "--${multipart_boundary}${CR}")
+      [ $state = data ] && return 0 \
+                        || state=header
+      ;;
+    "Content-Disposition: form-data; name=\"${name}\""*"${CR}")
+      [ $state = header -a $count -eq 1 ] && state=dheader
+      [ $state = header -a $count -gt 1 ] && count=$((count - 1))
+      [ $state = data ] && printf "%s\n" "$formdata"
+      ;;
+    "${CR}")
+      if [ $state = dheader ]; then
+        # Do not use `sed -n` (or busybox sed will "convert" NULL to LF)
+        sed "/--${multipart_boundary}\(--\)\?${CR}/{x;q;}" \
+        | head -c-3
+        return 0;
+      fi
+      [ $state = header ] && state=junk
+      ;;
+  esac; done <"${multipart_cachefile}"
+}
+
+multipart_filename(){
+  local name="$1" count="${2:-1}"
+  local formdata state=begin
+
+  while read -r formdata; do case "$formdata" in
+    "--${multipart_boundary}--${CR}")
+      return 1
+      ;;
+    "--${multipart_boundary}${CR}")
+      state=header
+      ;;
+    "Content-Disposition: form-data; name=\"${name}\"; filename=\""*"\""*"${CR}")
+      [ $state = header -a $count -eq 1 ] && break
+      [ $state = header -a $count -gt 1 ] && count=$((count - 1))
+      ;;
+    "${CR}")
+      [ $state = header ] && state=junk
+      ;;
+  esac; done <"${multipart_cachefile}"
+
+  filename="${formdata#*; filename=\"}"
+  filename="${filename%%\"${CR}}"
+  filename="${filename%%\";*}"
+
+  HEX_DECODE % "$filename"
+}
diff --git a/pages/cards.sh b/pages/cards.sh
deleted file mode 100755 (executable)
index a90981c..0000000
+++ /dev/null
@@ -1,265 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014 - 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-BR='
-'
-
-force_items(){
-  for each in "$@"; do
-    [ -z "${values[$each]+x}" ] && values[${each}]=''
-  done
-}
-
-case $PROFILE in
-medical)
-  SUP_FIELDS=(N NICKNAME GENDER BDAY ADR TEL EMAIL X-HEALTH-INSURANCE X-HEALTH-INSURANCE-NOCONTRIB IMPP URL NOTE X-CLIENT-REFERRAL)
-  FORCE_ITEMS=(ADR TEL EMAIL NOTE X-CLIENT-REFERRAL)
-  _GET[order]="${_GET[order]:-lastname}"
-  _GET[filtertype]="${_GET[filtertype]:-name}"
-  profile_medical=x
-;;
-circus)
-  SUP_FIELDS=(N NICKNAME GENDER BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE EMAIL TEL IMPP ADR URL NOTE)
-  FORCE_ITEMS=(BDAY X-ZACK-JOINDATE TEL EMAIL ADR NOTE)
-  _GET[order]="${_GET[order]:-firstname}"
-  _GET[filtertype]="${_GET[filtertype]:-any}"
-  profile_circus=x
-;;
-esac
-
-edit="${_GET[edit]}"
-[ \! -f "vcard/$edit" -a \! -f "temp/$edit" ] && edit=''
-
-listcourses() {
-  ls -1 ${_DATA}/ical/*ics |while read file; do
-    icstime="$(sed -rn 's:^DTSTART\:(TZID=.*\:)?([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?\r$:\2-\3-\4 \5\:\6\:\7:p' "$file")"
-    echo "$(date -d "$icstime" "+%u %H%M%S")\t$file"
-  done |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
-}
-
-list_hi_companies(){
-  sed -rn 's;^X-HEALTH-INSURANCE:([^\;]+)\;.*$;\1;p' ${_DATA}/vcard/*vcf \
-  | sort -u
-}
-
-list_categories() {
-  catfile="${_DATA}/mappings/categories"
-  sort -u "$catfile" \
-  | sed -r '/^[\t ]*$/d'
-}
-
-listcards() {
-  filterex='s;^([^\n]+)\n.*$;\1;p'
-  printf '%s\n' "${_GET[filter]}" |tr '^' '\n' \
-  | sed -r 's;[]\/\(\)\\\^\$\?\.\+\*\;\[\{\}];\\\\&;g' \
-  | while read each; do
-    case $each in
-        name:*) expr='(FN|NICKNAME|N)(\;[^\n]+)*:[^\n]*'"(${each#*:})";;
-      street:*) expr='ADR(\;[^\n]+)*:([^\;]*;){2}[^\;\n]*'"(${each#*:})";;
-         zip:*) expr='ADR(\;[^\n]+)*:([^\;]*;){5}[^\;\n]*'"(${each#*:})";;
-      any:*|:*) expr="[^\n]*"'(\;[^\n]+)*:[^\n]*'"(${each#*:})";;
-           *:*) expr="${each%%:*}"'(\;[^\n]+)*:[^\n]*'"(${each#*:})";;
-             *) expr="(${each})";;
-    esac
-    filterex='/(^|\n)'"${expr}"'/I{'"${filterex}"'}'
-  done
-
-  for file in "${_DATA}/vcard/"*.vcf; do
-    case "${_GET[order]}" in
-      firstname)
-        printf '%s\t%s\n' "$(sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$file")" "$file"
-        ;;
-      lastname)
-        printf '%s\t%s\n' "$(sed -rn 's:^N(;.+)*\:([^;]*;){0} *([^;]*).*$:\3:p' "$file")" "$file"
-        ;;
-      bdate)
-        printf '%s\t%s\n' "$(sed -rn 's:^BDAY(;.+)*\:(.*)$:\2:p' "$file")" "$file"
-        ;;
-      *)  printf 'x\t%s\n' "$file"
-        ;;
-    esac
-  done \
-  | sort -u |sed -r 's;^.*\t;;' \
-  | while read n; do
-    { printf '%s\n' "$n"; cat "$n"; } \
-    | sed -rn ':X;N;$!bX; {'"$filterex"'}'
-  done \
-  | sed -r 's;^(.*/)*;;;'
-}
-
-vcf_parse() {
-  unset key
-  sed -r ':X;N;$!bX; s;\r\n[ \t];;g; s;\r\n;\n;g;' "$1" \
-  | sed -rn '
-    # === turn property names to upper case, strip group names ===
-    h; s;^([^;:]+);;;
-    x; s;^([^;:\.]+\.)?([^;:]+).*$;\2;;
-    y;abcdefghijklmnopqrstuvwxyz;ABCDEFGHIJKLMNOPQRSTUVWXYZ;
-    G; s;\n;;;
-
-    # === strip trailing CR (but keep CRs in property value) ===
-    s;\r$;;;
-
-    # === Normalise various known vendor properties ===
-                s;^X-MS-CARDPICTURE(\;|:);PHOTO\1;;
-                        s;^X-GENDER(\;|:);GENDER\1;;
-                   s;^X-ANNIVERSARY(\;|:);ANNIVERSARY\1;;
-         s;^X-EVOLUTION-ANNIVERSARY(\;|:);ANNIVERSARY\1;;
-    s;^X-KADDRESSBOOK-X-ANNIVERSARY(\;|:);ANNIVERSARY\1;;
-            s;^X-EVOLUTION-BLOG-URL(\;|:);URL\1;;
-                              s;^AGENT(\;|:);RELATED\;VALUE=text\;TYPE=agent\1;;
-                        s;^X-ASSISTANT(\;|:);RELATED\;VALUE=text\;TYPE=assistant\1;;
-              s;^X-EVOLUTION-ASSISTANT(\;|:);RELATED\;VALUE=text\;TYPE=assistant\1;;
-    s;^X-KADDRESSBOOK-X-ASSISTANTSNAME(\;|:);RELATED\;VALUE=text\;TYPE=assistant\1;;
-                          s;^X-MANAGER(\;|:);RELATED\;VALUE=text\;TYPE=manager\1;;
-                s;^X-EVOLUTION-MANAGER(\;|:);RELATED\;VALUE=text\;TYPE=manager\1;;
-      s;^X-KADDRESSBOOK-X-MANAGERSNAME(\;|:);RELATED\;VALUE=text\;TYPE=manager\1;;
-                           s;^X-SPOUSE(\;|:);RELATED\;VALUE=text\;TYPE=spouse\1;;
-                 s;^X-EVOLUTION-SPOUSE(\;|:);RELATED\;VALUE=text\;TYPE=spouse\1;;
-        s;^X-KADDRESSBOOK-X-SPOUSENAME(\;|:);RELATED\;VALUE=text\;TYPE=spouse\1;;
-
-    # === Normalise obsolete vendor IM properties ===
-            s;^X-AIM((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;IMPP\1:aim:;;
-            s;^X-ICQ((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;IMPP\1:aim:;;
-    s;^X-GOOGLE-TALK((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;IMPP\1:xmpp:;;
-         s;^X-JABBER((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;IMPP\1:xmpp:;;
-            s;^X-MSN((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;IMPP\1:msn:;;
-          s;^X-YAHOO((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;IMPP\1:ymsgr:;;
-            s;^X-SIP((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(sip:)?;IMPP\1:sip:;;
-
-    # === Update obsolete LABEL property ===
-    s;^LABEL((\;[A-z0-9-]+|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)$;ADR\1\;LABEL="\5":;;
-
-    /^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)$/{
-      h;
-      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)$;key='\''\1'\'';;
-      H; g;
-
-      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):([^\n]*)\n.*$;\2;;
-      s;'\'';'\'\\\\\'\'';g
-      s;\;([A-Z0-9-]+)(=|=(([^\;,:"]+|"[^"]+")(,[^\;,:"]+|,"[^"]+")*))?;\ntag[\1]='\''\3'\'';g
-      s;^\n+;;; s;\n+$;;;
-      /^.+$/H; g;
-
-      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):([^\n]*)\n.*$;\6;;
-      # :X s;((^|[^\\])(\\\\)*)[;,];\1\n;g; s;((^|[^\\])(\\\\)*)\\([;,]);\4;g; tX;
-      s;'\'';'\'\\\\\'\'';g
-      s;^.*$;value='\''&'\'';
-      H; g;
-
-      s;^[^\n]*[\n ]+;;
-      s;\n+$;;;
-
-      p;
-    }
-    ' \
-  | while read -r line; do
-    declare -A tag
-    case "$line" in
-      value*) eval "$line";;
-      tag*)   eval "$line";;
-      key*)
-        if [ -z "$key" ]; then
-          eval "$line"
-        else
-          printf '%s\n' "$value" |sed -rn '
-            :X
-            s;((^|[^\\])(\\\\)*),;\1\n;g; 
-            tX;
-            s;\\,;,;g;
-            p
-          ' \
-          | while read -r val; do
-            while [ -n "${values[$key$n]+x}" ]; do n=$((${n=-1} + 1)); done
-            if printf '%s\n' "$val" |grep -qE '((^|[^\\])(\\\\)*)\;'; then
-              m=0
-              values[${key}${n}]="${val}"
-              printf '%s\n' "$val" |sed -rn '
-                :X
-                s;((^|[^\\])(\\\\)*)\;;\1\n;g; 
-                tX;
-                s;\\\;;\;;g;
-                p
-              ' \
-              | while read -r v; do
-                values[${key}${n}+${m}]="$(
-                  printf %s\\n "${v}" \
-                  | sed -rn '
-                    :X
-                    s;((^|[^\\])(\\\\)*)\\n;\1\n;g; 
-                    tX;
-                    s;\\\\;\\;g;
-                    p
-                  '
-                )"
-                m=$(($m + 1))
-              done
-            else
-              values[${key}${n}]="$(
-                printf %s\\n "${val}" \
-                | sed -rn '
-                  :X
-                  s;((^|[^\\])(\\\\)*)\\n;\1\n;g; 
-                  tX;
-                  s;\\\;;\;;g;
-                  s;\\\\;\\;g;
-                  p
-                '
-              )"
-            fi
-            for t in ${(k)tag}; do
-              values[${key}${n}_${t}]="${tag[$t]}"
-            done
-          done
-
-          eval "$line"
-          unset n
-          while [ -n "${values[$key$n]+x}" ]; do n=$((${n=-1} + 1)); done
-          unset value
-          unset tag
-        fi
-      ;;
-    esac
-  done
-}
-
-view_card() {  #Parameter: Cardfile
-  id="$1"
-  cardfile="$_DATA/vcard/${id}"
-  cachefile="$_DATA/cache/${id}.cache"
-  if [ "$cachefile" -nt "$cardfile" ]; then
-    cat "$cachefile"
-  else
-    declare -A values
-    vcf_parse "$cardfile"
-    . "$_EXEC/templates/view_card.sh" |tee "$cachefile"
-  fi
-}
-
-edit_card() {  #Parameter: Cardfile
-  id="$1"
-  cardfile="$_DATA/vcard/$id"
-  tempfile="$_DATA/temp/$id"
-  [ -f "$tempfile" ] && cardfile="$tempfile"
-
-  declare -A values
-  vcf_parse "$cardfile"
-  force_items $FORCE_ITEMS
-  . "$_EXEC/templates/edit_card.sh"
-}
diff --git a/pages/categories.sh b/pages/categories.sh
deleted file mode 100755 (executable)
index e875eef..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2015, 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-catfile="${_DATA}/mappings/categories"
-
-listcards() {
-  for file in "${_DATA}/vcard/"*.vcf; do
-    printf '%s\t%s\n' \
-      "$(sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$file")" \
-      "$file"
-  done \
-  | sort \
-  | sed -r 's;^.*\t(.*/)([^/]+)$;\2;'
-}
-
-get_name() {
-  cfile="${_DATA}/vcard/$1"
-  sed -rn 's;^N(\;[^":]+|\;"[^"]+")*:([^\;]*)(\;[^\;]*)(\;[^\;]*)?(\;[^\;]*)?(\;[^\;]*)?\r?$;\5 \3 \4 \2 \6;;
-           tX; b; :X s;[,\; ]+; ;g; p;' "$cfile"
-}
-
-get_categories(){
-  cfile="${_DATA}/vcard/$1"
-  sed -rn 's;^CATEGORIES(\;[^":]+|\;"[^"]+")*:(.+)\r?$;\2;;
-           tX; b; :X
-           s;(^|[^\\]+)((\\\\)+),;\1\2\n;g;
-           s;(^|[^\\]),;\1\n;g; s;(^|[^\\]+)((\\\\)+),;\1\2\n;g;
-           s;(^|[^\\]),;\1\n;g; s;\\,;,;g; p;' "$cfile" \
-  | sort -u
-}
-
-list_categories() {
-  sort -u "$catfile" \
-  | sed -r '/^[\t ]*$/d'
-}
diff --git a/pages/courses.sh b/pages/courses.sh
deleted file mode 100755 (executable)
index 93e8261..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014, 2016, 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-[ -z "${_GET[order]}" ] && _GET[order]=DOW
-
-listcards() {
-  ls -1 ${_DATA}/vcard/*vcf 2>/dev/null |while read file; do
-    fn=$(sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$file")
-    echo "$fn\t$file"
-  done |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
-}
-
-
-listcourses() {
-  case "${_GET[filtertype]}" in
-    *) ls -1 ${_DATA}/ical/*ics 2>/dev/null
-      ;;
-  esac |case "${_GET[order]}" in
-    DOW)
-      while read file; do
-        icstime="$(sed -rn 's:^DTSTART\:(TZID=.*\:)?([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?\r$:\2-\3-\4 \5\:\6\:\7:p' "$file")"
-        echo "$(date -d "$icstime" "+%u %H%M%S")\t$file"
-      done
-      ;;
-    TOD)
-      while read file; do
-        icstime="$(sed -rn 's:^DTSTART\:(TZID=.*\:)?([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?\r$:\2-\3-\4 \5\:\6\:\7:p' "$file")"
-        echo "$(date -d "$icstime" +%H%M%S)\t$file"
-      done
-      ;;
-    *)
-      sed -r 's:^.*$:x\t&:'
-      ;;
-  esac |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
-}
-
-list_attendance() {
-  id="$1"
-  sed -rn 's:'$id'\t(.+)$:\1:p' "$_DATA/mappings/attendance" |while read each; do
-    card="$_DATA/vcard/$each"
-    if [ -r "$card" ]; then
-      aname="$(sed -rn '
-          /^N(;.+)*:/{
-          h;
-          s;^N(\;.+)*:([^\;]*\;){3} *([^\;]*).*$;\3;p; g;
-          s;^N(\;.+)*:([^\;]*\;){1} *([^\;]*).*$;\3;p; g;
-          s;^N(\;.+)*:([^\;]*\;){2} *([^\;]*).*$;\3;p; g;
-          s;^N(\;.+)*:([^\;]*\;){0} *([^\;]*).*$;\3;p; g;
-          s;^N(\;.+)*:([^\;]*\;){4} *([^\;]*).*$;\3;p; g;
-          }' "$card" \
-        | sed -r ':X;N;$!bX; s;([\;\n\r,]| )+; ;g;; s;^ +| +$;;g;'
-      )"
-      fname="$(sed -rn 's;^FN(\;.+)*:(.+)\r?$;\2;g; s;([\;\n,]| )+; ;g;; s;^ +| +$;;g;' "$card")"
-      nname="$(sed -rn 's;^NICKNAME(\;.+)*:(.+)\r?$;\2;g; s;([\;\n,]| )+; ;g;; s;^ +| +$;;g;' "$card")"
-      byear="$(sed -rn 's:^BDAY(\;.+)*\:([0-9]{4})(-[0-9][0-9]){2}.*$:\2:p' "$_DATA/vcard/$each")"
-
-      printf '%s %s (*%04i)\n' "$each" "${aname:-${fname:-${nname}}}" "$byear"
-    fi
-  done
-}
-
-course_mail() {
-  id="$1"
-  coursemail=""
-  sed -rn 's:'$id'\t(.+)$:\1:p' "$_DATA/mappings/attendance" |while read each; do
-    coursemail="$coursemail$(sed -rn 's:^EMAIL(;.+)*\:(.+)\r$:\2,:p' "$_DATA/vcard/$each")"
-  done
-  echo "$coursemail"
-}
-
-ics_parse() {
-  unset key
-  sed -r ':X;N;$!bX; s;\r\n[ \t];;g; s;\r\n;\n;g;' "$1" \
-  | sed -rn '
-    # === turn property names to upper case, strip group names ===
-    h; s;^([^;:]+);;;
-    x; s;^([^;:\.]+\.)?([^;:]+).*$;\2;;
-    y;abcdefghijklmnopqrstuvwxyz;ABCDEFGHIJKLMNOPQRSTUVWXYZ;
-    G; s;\n;;;
-
-    /^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)$/{
-      h;
-      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)$;key='\''\1'\'';;
-      H; g;
-
-      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):([^\n]*)\n.*$;\2;;
-      s;'\'';'\'\\\\\'\'';g
-      s;\;([A-Z0-9-]+)(=|=(([^\;,:"]+|"[^"]+")(,[^\;,:"]+|,"[^"]+")*))?;\ntag[\1]='\''\3'\'';g
-      s;^\n+;;; s;\n+$;;;
-      /^.+$/H; g;
-
-      s;^([A-Z0-9-]+)((\;[A-z0-9-]+=?|\;[A-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):([^\n]*)\n.*$;\6;;
-      # :X s;((^|[^\\])(\\\\)*)[;,];\1\n;g; s;((^|[^\\])(\\\\)*)\\([;,]);\4;g; tX;
-      s;'\'';'\'\\\\\'\'';g
-      s;^.*$;value='\''&'\'';
-      H; g;
-
-      s;^[^\n]*[\n ]+;;
-      s;\n+$;;;
-
-      p;
-    }
-    ' \
-  | while read -r line; do
-    declare -A tag
-    case "$line" in
-      value*) eval "$line";;
-      tag*)   eval "$line";;
-      key*)
-        if [ -z "$key" ]; then
-          eval "$line"
-        else
-          values[$key]="$value"
-          for t in ${(k)tag}; do
-            values[${key}_$t]="$tag[$t]"
-          done
-          eval "$line"
-          if [ -n "$values[$key]" ]; then
-            n=0
-            while [ -n "$values[$key$n]" ]; do n=$(($n + 1)); done
-            key=$key$n
-          fi
-          unset value
-          unset tag
-        fi
-      ;;
-    esac
-  done
-}
-
-view_course() {  #Parameter: Calendarfile
-  id="$1"
-  calendarfile="$_DATA/ical/${id}"
-  cachefile="$_DATA/cache/${id}.cache"
-  unset key
-  if [ "$cachefile" -nt "$calendarfile" ]; then
-    cat "$cachefile"
-  else
-    declare -A values
-    ics_parse "$calendarfile"
-    . ${_EXEC}/templates/view_course.sh |tee "$cachefile"
-  fi
-}
-
-edit_course() {  #Parameter: Calendarfile
-  id="$1"
-  calendarfile="$_DATA/ical/$id"
-  tempfile="$_DATA/temp/$id"
-  [ -f "$tempfile" ] && calendarfile="$tempfile"
-  unset key
-
-  declare -A values
-  ics_parse "$calendarfile"
-  . ${_EXEC}/templates/edit_course.sh
-}
diff --git a/pages/email.sh b/pages/email.sh
deleted file mode 100755 (executable)
index c79876f..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-[ -z "${_GET[order]}" ] && _GET[order]=DOW
-
-listcards() {
-  ls -1 ${_DATA}/vcard/*vcf 2>/dev/null |while read file; do
-    fn=$(sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$file")
-    echo "$fn\t$file"
-  done |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
-}
-
-listcourses() {
-  ls -1 ${_DATA}/ical/*ics |while read file; do
-    icstime="$(sed -rn 's:^DTSTART\:(TZID=.*\:)?([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z?\r$:\2-\3-\4 \5\:\6\:\7:p' "$file")"
-    echo "$(date -d "$icstime" "+%u %H%M%S")\t$file"
-  done |sort |sed -r 's:^.*\t(.*/)([^/]+)$:\2:'
-}
diff --git a/pages/prescriptions.sh b/pages/prescriptions.sh
deleted file mode 100755 (executable)
index fcef09a..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-source "$_EXEC/pages/cards.sh"
-declare -A mpx
-
-BR='
-'
-
-view_card="$_EXEC/templates/view_client.sh"
-
-list_prescriptions(){
-  client="$1"
-  find "$_DATA/prescriptions/" -name "${client%.vcf}.*.mpx" \
-  | while read pfile; do
-    printf '%s\t%s\n' "$(grep '^date' "$pfile")" "${pfile##*/}"
-  done \
-  | sort -r | cut -f2
-}
-
-list_prescription_issuers(){
-  sed -rn 's;^issuer:(.+)$;\1;p' ${_DATA}/prescriptions/*.mpx \
-  | sort -u
-}
-
-edit_prescription(){
-  id="$1"
-  prescfile="$_DATA/prescriptions/$id"
-  tempfile="$_DATA/temp/$id"
-  [ -f "$tempfile" ] || cp "$prescfile" "$tempfile"
-
-  mpx=()
-  cat "$tempfile" |while read -r line; do
-    val="${line#*:}"
-    mpx[${line%%:*}]="${val//\\n/$BR}"
-  done
-
-  . "$_EXEC/templates/edit_prescription.sh"
-}
-
-view_prescription(){
-  id="$1"
-  prescfile="$_DATA/prescriptions/$id"
-
-  mpx=()
-  cat "$prescfile" |while read -r line; do
-    val="${line#*:}"
-    mpx[${line%%:*}]="$(htmlsafe "${val//\\n/$BR}")"
-  done
-
-  . "$_EXEC/templates/view_prescription.sh"
-}
-
-therapy_dates(){
-  tpyfile="$_DATA/therapies/${1%.mpx}.tpy"
-  sed -rn 's;^session[0-9]+_date:(.+)$;\1;p' "$tpyfile"
-}
diff --git a/pages/therapy.sh b/pages/therapy.sh
deleted file mode 100755 (executable)
index 8885515..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-declare -A tpy
-declare -A mpx
-
-BR='
-'
-
-id="$_GET[id]"
-
-tpyfile="$_DATA/therapies/$id"
-mpxfile="$_DATA/prescriptions/${id%.tpy}.mpx"
-client="$_DATA/vcard/${id%%.*}.vcf"
-
-identify "$_EXEC/static/therapy_background.png" \
-| sed -r 's;^.* ([0-9]+x[0-9]+) .*$;\1;' \
-| read bg_dim
-
-card_N="$(sed -nr 's:^(N)(;[^"\:]+|;"[^"]+")*\:(.*)$:\3:gp' "$client")"
-card_FN="$(sed -nr 's:^(FN)(;[^"\:]+|;"[^"]+")*\:(.*)$:\3:gp' "$client")"
-card_NICK="$(sed -nr 's:^(NICKNAME)(;[^"\:]+|;"[^"]+")*\:(.*)$:\3:gp' "$client")"
-
-n=$(printf %s "$card_N" \
-    | sed -rn 's:^([^;]*)(\;[^;]*)(\;[^;]*)?(\;[^;]*)?(\;[^;]*)?$:\4 \2 \3 \1 \5:gp' \
-    | sed -r 's:,: :;s:\;: :g;s: +: :g;s:^ $::;'
-   )
-client_name="${n:-${card_FN:-${card_NICK}}}"
-
-mpx=()
-cat "$mpxfile" |while read -r line; do
-  val="${line#*:}"
-  mpx[${line%%:*}]="$(htmlsafe "${val//\\n/$BR}")"
-done
-
-tpy=()
-cat "$tpyfile" |while read -r line; do
-  val="${line#*:}"
-  tpy[${line%%:*}]="$(htmlsafe "${val//\\n/$BR}")"
-done
diff --git a/pdiread.sh b/pdiread.sh
new file mode 100755 (executable)
index 0000000..e19ceb8
--- /dev/null
@@ -0,0 +1,197 @@
+#!/bin/zsh
+
+# Copyright 2014 - 2018, 2023 Paul Hänsch
+#
+# This file is part of Confetti.
+# 
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
+
+# This is a parsing library for the Personal Data Interchange format (PDI)
+# PDI is the format for encoding VCard (.vcf) and iCalendar (.ics) files
+
+[ -n "$include_pdi" ] && return 0
+include_pdi="$0"
+
+BR='
+'
+unescape(){
+  local in out=''
+  [ $# -gt 0 ] && in="$*" || in="$(cat)"
+  while [ "$in" ]; do case $in in
+    \\\\*) out="${out}\\"; in="${in#\\\\}" ;;
+    \\n*) out="${out}${BR}"; in="${in#\\n}" ;;
+    \\*) in="${in#\\}" ;;
+    *) out="${out}${in%%[\\]*}"; in="${in#"${in%%[\\]*}"}" ;;
+  esac; done
+  printf '%s\n' "$out"
+ }
+
+pdi_load() {
+  # normalise PDI file for processing with pdi_* functions
+  # functions in this library can only be applied to normalised data
+  # Usage example:
+  # data="$(pdi_load file.vcf)"
+
+  sed -srn '
+    # === Read entire file into buffer ===
+    :X $bY; N; bX; :Y s;^.*$;\n&\n;;
+
+    # === Join continuing lines, strip trailing CRs ===
+    s;\r*\n[ \t];;g;
+    s;\r*\n;\n;g;
+
+    # === turn property names to upper case, strip group names ===
+    s;\n([^;:\.\n]+\.)([^;:\n]+);\n\2;g;
+    :upcase
+    s;(\n[^;:]*)a;\1A;g; s;(\n[^;:]*)b;\1B;g; s;(\n[^;:]*)c;\1C;g; s;(\n[^;:]*)d;\1D;g; s;(\n[^;:]*)e;\1E;g;
+    s;(\n[^;:]*)f;\1F;g; s;(\n[^;:]*)g;\1G;g; s;(\n[^;:]*)h;\1H;g; s;(\n[^;:]*)i;\1I;g; s;(\n[^;:]*)j;\1J;g;
+    s;(\n[^;:]*)k;\1K;g; s;(\n[^;:]*)l;\1L;g; s;(\n[^;:]*)m;\1M;g; s;(\n[^;:]*)n;\1N;g; s;(\n[^;:]*)o;\1O;g;
+    s;(\n[^;:]*)p;\1P;g; s;(\n[^;:]*)q;\1Q;g; s;(\n[^;:]*)r;\1R;g; s;(\n[^;:]*)s;\1S;g; s;(\n[^;:]*)t;\1T;g;
+    s;(\n[^;:]*)u;\1U;g; s;(\n[^;:]*)v;\1V;g; s;(\n[^;:]*)w;\1W;g; s;(\n[^;:]*)x;\1X;g; s;(\n[^;:]*)y;\1Y;g;
+    s;(\n[^;:]*)z;\1Z;g;
+    t upcase;
+
+    # === Insert empty attribute fields where no attributes are present ===
+    s;\n([^;:]+):;\n\1\;:;g;
+
+    # === Unscramble aggregated fields ===
+    :disag
+    s;\n([^:\n]+:)(([^\n]*[^\])?(\\\\)*),;\n\1\2\n\1;;
+    t disag;
+
+    # === Insert FN when only N is present ===
+    /\nFN[;:]/!{
+      s,\nN(;[^:]*)?:([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);?\n,&FN;:\5 \3 \4 \2 \6\n,;
+      :despace
+      s,(\nFN;:[^\n]*)  ([^\n]*\n),\1 \2,;
+      s,(\nFN;:) ([^\n]*\n),\1\2,;
+      s,(\nFN;:[^\n]*) (\n),\1\2,;
+      t despace;
+    }
+    /\nFN[;:]/!{ s,\n(N[;:][^\n]*)\n,&F\1\n,; }  # Fallback
+
+    # === Normalise various known vendor properties ===
+                s;\nX-MS-CARDPICTURE(\;|:);\nPHOTO\1;g;
+                        s;\nX-GENDER(\;|:);\nGENDER\1;g;
+                   s;\nX-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+         s;\nX-EVOLUTION-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+    s;\nX-KADDRESSBOOK-X-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+            s;\nX-EVOLUTION-BLOG-URL(\;|:);\nURL\1;g;
+                           s;\nAGENT(\;|:);\nRELATED\;VALUE=text\;TYPE=agent\1;g;
+                     s;\nX-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+           s;\nX-EVOLUTION-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+ s;\nX-KADDRESSBOOK-X-ASSISTANTSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+                       s;\nX-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+             s;\nX-EVOLUTION-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+   s;\nX-KADDRESSBOOK-X-MANAGERSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+                        s;\nX-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+              s;\nX-EVOLUTION-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+     s;\nX-KADDRESSBOOK-X-SPOUSENAME(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+
+    # === Normalise obsolete vendor IM properties ===
+            s;\nX-AIM((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g;
+            s;\nX-ICQ((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g;
+    s;\nX-GOOGLE-TALK((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g;
+         s;\nX-JABBER((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g;
+            s;\nX-MSN((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:msn:;g;
+          s;\nX-YAHOO((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:ymsgr:;g;
+            s;\nX-SIP((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(sip:)?;\nIMPP\1:sip:;g;
+
+    # === Update obsolete LABEL property ===
+    s;\nLABEL((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)\n;\nADR\1\;LABEL="\5":\n;g;
+
+    p;' "$@"
+}
+
+pdi_count(){
+  local card="$1" name="$2" rc='' cnt=0
+  while rc="${card#*${BR}${name};}"; do
+    [ "${rc}" != "${card}" ] || break
+    card="$rc"
+    cnt=$(($cnt + 1))
+  done
+  printf %i\\n $cnt
+}
+
+pdi_attrib(){
+  local card=":$1" name="$2" cnt="${3:-1}" attr="$4"
+  while [ $cnt -gt 0 ]; do
+    [ "${card#*${BR}${name};}" = "$card" ] && return 1
+    card="${card#*${BR}${name};}"
+    cnt=$((cnt - 1))
+  done
+  card="${card%%:*}"
+  if [ "$attr" ]; then
+    case $card in
+      *\;"$attr"=*) card="${card#*;${attr}=}";;
+      "$attr"=*) card="${card#${attr}=}";;
+      "$attr"|*\;"$attr"|"$attr"\;*|*\;"$attr"\;*) return 0;;
+      *) return 1;;
+    esac
+    case $card in
+      \"*\"\;*|\'*\'\;*)
+        card="${card#[\"\']}"; card="${card%%[\"\'];*}"
+        ;;
+      \"*\"|\'*\')
+        card="${card#[\"\']}"; card="${card%%[\"\']}"
+        ;;
+      *\;*) card="${card%%;*}";;
+    esac
+  fi
+  printf %s\\n "${card}"
+}
+
+pdi_value(){
+  local card="${BR}$1" name="$2" cnt="${3:-1}"
+  while [ "$cnt" -gt 0 ]; do
+    [ "${card#*${BR}${name};*:}" = "$card" ] && return 1
+    card="${card#*${BR}${name};*:}"
+    cnt=$((cnt - 1))
+  done
+  printf %s\\n "${card%%${BR}*}"
+}
+
+pdi_update_value(){
+  local card="${BR}$1" name="$2" cnt="$3" val="$4"
+  while [ "$cnt" -gt 0 ]; do
+    if [ "${card#*${BR}${name};*:}" = "${card}" ]; then
+       printf '%s\n%s;:' "${card%${BR}END;:VCARD*}" "${name}"
+       card="${BR}END;:VCARD"
+       break;
+    else
+       printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}"
+       card="${card#*${BR}${name};}"
+       printf '%s:' "${card%%:*}"
+       card="${card#*:}"
+    fi
+    cnt=$((cnt - 1))
+  done
+  printf '%s\n%s\n' "$val" "${card#*${BR}}"
+}
+
+pdi_update_attrib(){
+  local card="${BR}$1" name="$2" cnt="$3" val="$4"
+  while [ "$cnt" -gt 0 ]; do
+    if [ "${card#*${BR}${name};*:}" = "${card}" ]; then
+       printf '%s\n%s;' "${card%${BR}END;:VCARD*}" "${name}"
+       card=":${BR}END;:VCARD"
+       break;
+    else
+       printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}"
+       card="${card#*${BR}${name};}"
+    fi
+    cnt=$((cnt - 1))
+  done
+  printf '%s:%s\n' "$val" "${card#*:}"
+}
diff --git a/session_lock.sh b/session_lock.sh
new file mode 100644 (file)
index 0000000..de1641a
--- /dev/null
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+[ "$include_session_lock" ] && return 0
+include_session_lock="$0"
+
+SLOCK(){
+  local file="$1";
+  local timeout="${2-900}"
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local ovlock="${lockdir%/*}/delete.${lockdir##*/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+  local lockexpire=$(( $(date +%s) - timeout ))
+
+  mkdir -p "${lockdir%/*}"
+
+  if [ -e "$lockdir" ] \
+     && [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \
+     && mkdir "$ovlock"; then
+    [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \
+    && rm -r "$lockdir"
+    rmdir "$ovlock"
+  fi
+
+  printf '%s\n' "$tempfile"
+  if mkdir "$lockdir" 2>&-; then
+    cp "$file" "$tempfile"
+    return 0
+  else
+    return 1
+  fi
+}
+
+CHECK_SLOCK(){
+  local file="$1";
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+
+  printf '%s\n' "$tempfile"
+  if [ -f "$tempfile" ]; then
+    touch "$lockdir"
+    return 0
+  else
+    return 1
+  fi
+}
+
+RELEASE_SLOCK(){
+  local file="$1";
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local ovlock="${lockdir%/*}/delete.${lockdir##*/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+
+  if [ -f "$tempfile" ] && mkdir "$ovlock"; then
+    [ -f "$tempfile" ] && rm -r "$lockdir"
+    rmdir "$ovlock"
+    return 0
+  else
+    return 1
+  fi
+}
diff --git a/shcgi b/shcgi
deleted file mode 160000 (submodule)
index 8d60af4..0000000
--- a/shcgi
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 8d60af41a80ea3760dd51c3bc34eab68d88b16ae
diff --git a/static/cards.css b/static/cards.css
deleted file mode 100644 (file)
index 981a667..0000000
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
-# Copyright 2014 - 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-*/
-
-.filter {
-  display: block;
-  border: solid 1px;
-  border-radius: 4px 4px 0 0;
-  margin: .5em 2em .25em 2em;
-  padding: 0 2ex .5em 2ex;
-  background: #EFF;
-}
-
-.filter h1 {
-  display: block;
-  font-weight: bold;
-  font-size: 1.25em;
-  border-style: none none solid none;
-  border-radius: 4px 4px 0 0;
-  border-width: 1px;
-  margin: 0 -1.625ex .5em -1.625ex;
-  padding: .125em 1ex;
-  background: #EEF;
-}
-.filter input[type="text"] {
-  width: 100%;
-  margin-bottom: .5em;
-}
-.filter fieldset {
-  position: relative;
-  display: block;
-  border: none;
-  padding: 0 0 0 12ex;
-  margin: 0 0 .5em 0;
-}
-.filter fieldset legend {
-  position: absolute;
-  font-size: 1em;
-  font-weight: bold;
-  margin-left: -12ex;
-}
-.filter label {
-  display: inline-block;
-  vertical-align: top;
-  margin: .125em 2ex 0 0;
-  white-space: nowrap;
-}
-.filter button {
-  margin-top: .5em;
-}
-
-.filter fieldset.item { padding-left: 0; }
-.filter fieldset.item legend { display: none; }
-.filter fieldset.item input[type=text],
-.filter fieldset.item fieldset.categories {
-  background-color: #FFF;
-  border: 1px solid #000;
-  margin-top: -1px;
-  padding: .25em .5em;
-}
-.filter fieldset.item input[type=radio] { display: none; }
-.filter fieldset.item input[type=radio] + label {
-  position: relative; z-index: 1;
-  margin: 0; padding: .25em 1em;
-  
-}
-.filter fieldset.item input[type=radio]:checked + label {
-  background-color: #FFF;
-  border: 1px solid #000;
-  border-bottom-color: #FFF;
-}
-.filter fieldset.item input[type=radio][value="CATEGORIES:"]:checked + label + input[type=text],
-.filter fieldset.item input[type=radio][value="CATEGORIES:"] + label + input[type=text] + fieldset.categories {
-  display: none;
-}
-.filter fieldset.item input[type=radio][value="CATEGORIES:"]:checked + label + input[type=text] + fieldset.categories {
-  display: block;
-}
-.filter fieldset.item fieldset.categories a {
-  font-size: .875em;
-  line-height: 1.625em;
-  color: #44C;
-}
-
-.newcard {
-  display: block;
-  border: solid 1px;
-  border-radius: 0 0 4px 4px;
-  margin: .25em 2em 1em 2em;
-  padding: .25em 2ex .25em 2ex;
-  background: #EFF;
-}
-
-.card {
-  display: block;
-  position: relative;
-  border: solid 1px;
-  margin: .25em 2em;
-  padding: 0 20ex 0 0;
-  overflow: auto;
-  background: #FFF;
-  min-height: 8em;
-}
-.card .control {
-  display: inline-block;
-  position: absolute;
-  top: 0; right: 0;
-  text-align: right;
-  background: #EEF;
-  margin: 0;
-  height: 100%;
-  width: 20ex;
-  max-width: 33%;
-  padding: .25em 1ex;
-}
-.card .control .item {
-  display: inline-block;
-  width: 100%;
-  min-height: 2.5em;
-  color: #008;
-  margin: .25em 0 .5em 0;
-  border: 1px solid black;
-  background: #FFF;
-  padding: .25em .75ex 0 .75ex;
-  text-decoration: none;
-  font-size: .75em;
-}
-.card .control .item:hover{ border-color: #888; }
-.card .control input.item { font-size: normal; }
-.card .control button.item { text-align: right;}
-.card .control button.item:hover{ border-color: #888;}
-.card .control .item[name=newfield] { width: 85%;}
-.card .control .item[value=addfield] { width: 15%;}
-
-.card .control #delete + label + button,
-.card .control #delete { display: none; }
-.card .control #delete + label { background-color: #FEE; }
-.card .control #delete:checked + label + button {
-  position: fixed; display: block;
-  left: 40%; top: 30%; width: 20%;
-  font-size: 1.5em;
-  text-align: center;
-  background-color: #FAA;
-  color: #333;
-  border-radius: .25em 0 0 .25em;
-}
-.card .control #delete:checked + label:after {
-  position: fixed; display: block;
-  left: 60%; top: 30%; width: 5%;
-  margin: .25em 0; padding: .5em .75ex 0 .75ex;
-  height: 1.5em;
-  font-size: 2em;
-  content: 'X';
-  text-align: center;
-  color: #AAA; background-color: #000;
-  border-radius: 0 .25em .25em 0;
-}
-
-.card .section {
-  display: inline-block;
-  vertical-align: top;
-  float: left;
-  width: 20ex;
-  min-width: 16.5%;
-  min-height: 6em;
-  margin: .125em 0 .5ex 0;
-  padding: 0 1ex .25em 1ex;
-}
-.card .section:nth-of-type(2n){ background: #EEE;}
-
-.card .section h2, .card .section h3 {
-  font-size: 1em;
-  display: block;
-  font-weight: bold;
-  margin: .25em -.5ex .25em -.5ex;
-  border-style: none none solid none;
-  border-width: 1px;
-}
-.card .section .FN {  /* will override h2 */
-  font-size: 1.2em;
-  margin-top: 0;
-}
-
-.card .section .item {
-  display: block;
-  max-width: 100%;
-  word-wrap: break-word;
-  white-space: pre-line;
-}
-.card .section .item label {
-  font-weight: bold;
-}
-
-.card .section textarea.NOTE {
-  min-height: 6em;
-}
-.card .section textarea.ADR {
-  min-height: 4em;
-}
-
-.card .section .PHOTO {
-  width: 100%;
-  max-height: 10em;
-}
-.card .section .GENDER { display: inline-block; padding-right: .5ex;}
-.card .section .BDAY { display: inline-block;}
-
-.card .section select {
-  display: block;
-  background-color: #FFF;
-  border: 1px solid black;
-}
-
-.card .section.attendance ul { margin: 0; padding: 0; }
-.card .section.attendance ul li {
-  display: block;
-  word-wrap: break-word;
-}
-
-.card .section.prescriptions ul { margin: 0; padding: 0; }
-.card .section.prescriptions ul li {
-  display: block;
-  word-wrap: break-word;
-  margin: 0 .25ex;
-}
-
-.card .section.prescriptions { width: 40ex;}
-
-form.card .section input[type=text],
-form.card .section input:not([type]),
-form.card .section textarea,
-form.card .section select {width: 100%;}
-
-form.card .attendance {
-  min-width: 66%;
-}
-form.card .attendance label {
-  position: relative;
-  display: inline-block;
-  width: 25ex;
-  padding-left: 3ex;
-}
-form.card .attendance label input[type=checkbox] {
-  position: absolute;
-  margin-left: -3ex;
-}
-
-form.card .insurance input[type=radio] {display: none;}
-form.card .insurance input[type=radio] + label {
-  display: inline-block;
-  width: 50%;
-  padding: .25ex 0;
-  text-align: center;
-  border: 1px solid black;
-  
-}
-form.card .insurance input[type=radio]:checked + label {
-  font-weight: bold;
-  background-color: #FFF;
-  border-width: 1px 1px 0 1px;
-}
-form.card .insurance input[type=radio] + label + input + label + select,
-form.card .insurance input[type=radio] + label + select + input {display: none;}
-form.card .insurance input[type=radio]:checked + label + input + label + select,
-form.card .insurance input[type=radio]:checked + label + select + input {
-  display: block;
-  border: 1px solid black;
-  background-color: #FFF;
-  border-width: 0 1px 1px 1px;
-  padding: .25ex 0;
-  margin-top: -1px;
-}
diff --git a/static/categories.css b/static/categories.css
deleted file mode 100644 (file)
index 0a7f41f..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
-# Copyright 2014, 2015, 2018 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-*/
-
-form.categories {
-  display: block;
-  border: solid 1px;
-  border-radius: 4px 4px 0 0;
-  margin: .5em 2em .25em 2em;
-  padding: 0 2ex .5em 2ex;
-  background: #EFF;
-}
-
-form.categories h1 {
-  display: block;
-  font-weight: bold;
-  font-size: 1.25em;
-  border-style: none none solid none;
-  border-radius: 4px 4px 0 0;
-  border-width: 1px;
-  margin: 0 -1.625ex .5em -1.625ex;
-  padding: .125em 1ex;
-  background: #EEF;
-}
-
-form.categories ul { margin: 0; padding: 0; }
-form.categories ul li {
-  display: inline-block;
-  margin: 0 .25ex;
-  padding: .125em .25ex .125em 1ex;
-  border: 1px solid black;
-  background-color: #EEF;
-}
-form.categories ul li button {
-  margin: 0 0 0 1ex;
-  background-color: #FCC;
-  border: 1px solid black;
-  min-width: 3ex;
-  text-align: center;
-}
-form.categories ul li:last-of-type { padding: .125em .25ex .125em .25ex; }
-form.categories ul li:last-of-type button { background-color: #FFF; }
-
-form.namelist {
-  display: block;
-  margin: 1em 2em;
-  padding: 0;
-}
-form.namelist > fieldset {
-  display: block;
-  border: solid 1px;
-  border-radius: 4px 4px 0 0;
-  margin: 0; padding: .25em 2ex;
-  background: #EFF;
-  text-align: right;
-}
-form.namelist > fieldset:last-of-type {
-  border-radius: 0 0 4px 4px;
-}
-ul.namelist { margin: 0; padding: 0;}
-ul.namelist > li {
-  display: block;
-  border: solid 1px;
-  margin: -1px 0;
-  background: #FFF;
-}
-ul.namelist > li:nth-of-type(2n){ background: #EEE;}
-
-ul.namelist h2 {
-  font-size: 1em;
-  display: inline-block;
-  margin: 0;
-  padding: 0 1ex;
-  min-width: 30ex;
-}
-ul.namelist ul { margin: 0; padding: 0; display: inline-block;}
-ul.namelist ul li {
-  display: inline-block;
-  margin: 0 1ex 0 0;
-}
-
-.fakeorder {
-  position: absolute;
-  width: 0; height: 0;
-  left: -100%;
-}
diff --git a/static/common.css b/static/common.css
deleted file mode 100644 (file)
index e466cb7..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-* {
-  font-family: sans-serif;
-  -moz-box-sizing: border-box;
-  -webkit-box-sizing: border-box;
-  box-sizing: border-box;
-  z-index: 0;
-}
-body {
-  background: #EEE;
-  margin: 0;
-  padding: 0;
-  padding-top: 2.5em;
-  position: absolute;
-  min-height: 100%;
-  width: 100%;
-  padding-bottom: 2em;
-}
-.NAVIGATION {
-  position: fixed;
-  top: 0;
-  display: inline-block;
-  border-style: none solid solid solid;
-  border-width: 0 1px 1px 1px;
-  border-radius: 0 0 4px 4px;
-  margin: 0 2em;
-  padding: .5em 1ex;
-  background: #FFF;
-  z-index: 1;
-}
-.NAVIGATION label,
-.NAVIGATION a {
-  color: #008;
-  border: solid 1px #BBF;
-  margin: .5em .5ex .25em .5ex;
-  padding: .2em 3ex .2em 3ex;
-  background: #EFF;
-}
-.NAVIGATION label:hover,
-.NAVIGATION a:hover {
-  border-width: 2px 1px;
-  background: #F3FFFF;
-}
-
-#CONFIGURE        label[for="navigationconfig"] { display: none; }
-#CONFIGURE:target label[for="navigationconfig"] { display: inline; }
-
-.NAVIGATION input#navigationconfig { display: none; }
-.NAVIGATION input + form.config {
-  position: static;
-  display: none;
-}
-
-.NAVIGATION .config a { display: block; }
-.NAVIGATION .config a:hover { border-width: 1px 2px; }
-.NAVIGATION input:checked + form.config { display: block; }
-.NAVIGATION .config input[type=text],
-.NAVIGATION .config button {
-  font-size: .875em;
-  line-height: 1.5em;
-  height: 1.75em;
-  padding: 0 .25em;
-  vertical-align: bottom;
-  border: 1px solid #000;
-  background-color: #FFF;
-}
-.NAVIGATION .config input[type=text] {
-  margin: .25em 0 0 .375em;
-  border-right: none;
-  width: 70%;
-}
-.NAVIGATION .config button {
-  min-width: 2.5em;
-  text-align: center;
-  max-width: 30%;
-}
-.NAVIGATION .config button[value=del] {
-  background: #FCC;
-}
-
-
-.trailbtn { display: none; }
-.trailbtn + .trailbox { display: none; }
-.trailbtn:checked + .trailbox { display: inline-block; }
-.trailbtn:checked + .trailbox + .trailbtn { display: block; }
-.trailbtn:checked + .trailbox + .trailbtn:before {
-   display: block; content: '+';
-   width: 3ex; text-align: center;
-   margin-top: .25em; padding: .25em 0;
-   background-color: #FFF;
-   border-width: 1px; border-style: solid;
-}
-.trailbtn:checked + .trailbox + .trailbtn:checked,
-.trailbtn:checked { display: none; }
-
-#footer {
-  width: 100%;
-  background-color: #FFF;
-  border-top: 1px solid #BBF;
-  margin: 0; padding: .5em 2em;
-  position: absolute; bottom: 0;
-}
diff --git a/static/courses.css b/static/courses.css
deleted file mode 100644 (file)
index a8a8cc3..0000000
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
-# Copyright 2014 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-*/
-
-.sort {
-  display: block;
-  border: solid 1px;
-  border-radius: 4px 4px 0 0 ;
-  margin: .5em 2em .25em 2em;
-  padding: .25em 2ex .25em 2ex;
-  background: #EFF;
-}
-.sort .label {
-  font-weight: bold;
-}
-
-.newcourse {
-  display: block;
-  border: solid 1px;
-  border-radius: 0 0 4px 4px;
-  margin: .25em 2em 1em 2em;
-  padding: .25em 2ex .25em 2ex;
-  background: #EFF;
-}
-
-.course {
-  display: block;
-  position: relative;
-  border: solid 1px;
-  margin: .25em 2em;
-  padding: 0 20ex 0 0;
-  overflow: auto;
-  background: #FFF;
-  min-height: 9em;
-}
-.course .control {
-  display: inline-block;
-  position: absolute;
-  top 0; right: 0;
-  text-align: right;
-  background: #EEF;
-  margin: 0;
-  height: 100%;
-  width: 20ex;
-  max-width: 33%;
-  padding: .25em 1ex;
-}
-.course .control .item {
-  display: inline-block;
-  vertical-align: top;
-  width: 100%;
-  color: #008;
-  margin: .25em 0 0 0;
-  border: 1px solid black;
-  background: #FFF;
-}
-.course .control a.item {
-  border-right: 3px double;
-  padding: .125em 1ex;
-}
-.course .control a.item:hover{
-  border-right: 1px solid;
-}
-
-.course .control .item[name=newfield] { width: 85%;}
-.course .control .item[value=addfield] { width: 15%;}
-
-.course .section {
-  display: inline-block;
-  vertical-align: top;
-  float: left;
-  width: 20ex;
-  min-width: 16.5%;
-  min-height: 6em;
-  margin: .125em 0 .5ex 0;
-  padding: 0 1ex .25em 1ex;
-}
-.course .section:nth-of-type(2n){ background: #EEE;}
-
-.course .section .item {
-  display: block;
-  max-width: 100%;
-  word-wrap: break-word;
-}
-
-.course .section h2, .course .section h3 {
-  font-size: 1em;
-  display: block;
-  font-weight: bold;
-  margin: .25em -.5ex .25em -.5ex;
-  border-style: none none solid none;
-  border-width: 1px;
-}
-.course .section h2 {
-  font-size: 1.2em;
-  margin-top: 0;
-}
-
-.course .attendance {
-  min-width: 66%;
-}
-.course .attendance a {
-  display: inline-block;
-  vertical-align: top;
-  min-width: 13.5ex;
-  width: 19%;
-  color: #008;
-  border-style: solid;
-  border-width: 1px;
-  margin: .25em .125ex .25em .125ex;
-  padding: .125em 1ex .125em 1ex;
-  background: #EFF;
-}
-
-.course .dtstart { width: 37ex; }
-.course .dtstart .DTSYEAR,
-.course .dtstart .DTSMONTH,
-.course .dtstart .DTS{
-  font-size: .875em;
-  margin: .25em .25ex 1em .25ex;
-  display: inline-block;
-  text-align: center;
-  font-weight: bold;
-}
-
-.course .dtstart .DTSYEAR{ width: 11.5ex; }
-.course .dtstart .DTSMONTH{ width: 18.5ex; }
-.course .dtstart .DTS{ width: 5.5ex; }
-
-.course .dtstart .DTSCAL,
-.course .dtstart .DTSCALHEAD {
-  display: inline-block;
-  overflow: hidden;
-  text-align: center;
-  width: 5ex;
-  margin: 0;
-}
-.course .dtstart .DTSCALHEAD {
-  font-weight: bold;
-  border-top: 1px solid;
-  border-bottom: 1px solid;
-}
-
-.course .dtstart input[name=DTSDAY] { display: none; margin: 0; height: 0; width: 0; }
-.course .dtstart input[name=DTSDAY]:checked + label.DTSCAL { font-weight: bold; border: solid 1px; }
-
-.course .dtstart .DTSTIME{
-  display: inline-block;
-  width: 10ex;
-  font-weight: bold;
-}
-
-.course .recur{ width: 37ex; }
-.course .section select,
-.course .section button,
-.course .section input {
-  border: 1px solid;
-  margin: .5em 0;
-}
-
-.course .select_attendance {
-  max-height: 17em;
-  overflow-y: scroll;
-}
-.course .select_attendance label{
-  display: block;
-  position: relative;
-  margin: 0; padding: 0;
-  padding-left: 3ex;
-}
-.course .select_attendance label input{
-  position: absolute;
-  margin: 0; padding: 0;
-  margin-left: -3ex;
-  top: 0;
-}
-
-.course .section input[type="text"] { width: 100%; }
-.course .section textarea.COMMENT { min-height: 6em; width: 100%; overflow: auto; }
-
diff --git a/static/email.css b/static/email.css
deleted file mode 100644 (file)
index e935189..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
-# Copyright 2014 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-*/
-
-.filter {
-  display: block;
-  width: 128ex;
-  border-width: 1px;
-  border-style: solid;
-  border-radius: 4px 4px 0 0;
-  margin: .5em auto .25em auto;
-  padding: .25em 1ex .25em 1ex;
-  background: #EFF;
-}
-
-.filter > .label {
-  display: block;
-  font-weight: bold;
-  font-size: 1.2em;
-  border-style: none none solid none;
-  border-width: 1px;
-  margin: 0em 0ex .5em 0ex;
-  background: #EEF;
-}
-
-.filter .search {
-  margin: 0em 2ex .5em 2ex;
-}
-.filter .search .label {
-  display: inline-block;
-  width: 12ex;
-  font-weight: bold;
-  margin-top: .5em;
-}
-.filter .search input[type="radio"] {
-  margin-top: .5em;
-  margin-left: 1ex;
-}
-.filter .search button {
-  margin-top: .5em;
-}
-
-.filter .search input[type="text"] {
-  width: 124ex;
-}
-
-.newcard {
-  display: block;
-  width: 126ex;
-  border-width: 1px;
-  border-style: solid;
-  border-radius: 0 0 4px 4px;
-  margin: .25em auto 1em auto;
-  padding: .25em 2ex .25em 2ex;
-  background: #EFF;
-}
-
-.cardlist .card {
-  display: block;
-  width: 130ex;
-  border-style: solid;
-  border-width: 1px;
-  margin: .25em auto;
-  padding: 0;
-  overflow: auto;
-  background: #FFF;
-}
-
-.cardlist .card .section {
-  display: inline-block;
-  float:left;
-  width: 20ex;
-  margin: .125em .25ex .5ex .25ex;
-  padding: 0 .2em .2em .2em;
-  background: #EEE;
-}
-.cardlist .card .section a.attendance {
-  display: inline-block;
-  margin-right: 1ex;
-  word-wrap: break-word;
-}
-
-.cardlist .card .attendance {
-  width: 83.5ex;
-}
-.cardlist .card .attendance .check {
-  display: inline-block;
-  width: 27ex;
-}
-
-.cardlist .card .control {
-  float: right;
-  text-align: right;
-  margin-right: 0;
-  background: #EEF;
-}
-.cardlist .card .control .item {
-  color: #008;
-  margin-top: .2em;
-  margin-right: 1ex;
-}
-.cardlist .card .control a.item {
-  min-width: 10ex;
-  border-style: solid double solid solid;
-  border-width: 1px 3px 1px 1px;
-  border-color: #000;
-  padding: .1em 1ex;
-  background: #FFF;
-}
-.cardlist .card .control a.item:hover{
-  border-width: 1px 1px 1px 1px;
-}
-
-.cardlist .card .section .sectitle {
-  display: block;
-  font-weight: bold;
-  margin: .2em .2em .2em .2em;
-}
-
-.cardlist .card .section .item {
-  display: block;
-  max-width: 20ex;
-  word-wrap: break-word;
-}
-
-.cardlist .card .section textarea.NOTE {
-  min-height: 6em;
-}
-.cardlist .card .section textarea.ADR {
-  min-height: 4em;
-}
-
-.cardlist .card .section .PHOTO {
-  width: 20ex;
-  max-heigth: 30ex;
-}
-
-.cardlist .card .section .FN {
-  font-weight: bold;
-  font-size: 1.2em;
-}
-
diff --git a/static/prescriptions.css b/static/prescriptions.css
deleted file mode 100644 (file)
index 6eebe24..0000000
+++ /dev/null
@@ -1,298 +0,0 @@
-/*
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-*/
-
-@import url("?static=cards.css");
-
-body {padding-bottom: 3em; }
-
-.trailbtn { display: none; }
-.trailbtn + .trailbox { display: none; }
-.trailbtn:checked + .trailbox { display: inline-block; }
-.trailbtn:checked + .trailbox + .trailbtn { display: block; }
-.trailbtn:checked + .trailbox + .trailbtn:before {
-   display: block; content: '+';
-   width: 3ex; text-align: center;
-   margin-top: .25em; padding: .25em 0;
-   background-color: #FFF;
-   border-width: 1px; border-style: solid;
-}
-.trailbtn:checked + .trailbox + .trailbtn:checked,
-.trailbtn:checked { display: none; }
-
-.prescription {
-  display: inline-block;
-  width: 96%; max-width: 460px;
-  color: #800;
-  background-color: #DDD;
-  margin: 1em -1% 0 2%; padding: 0;
-  border: 1px solid #888;
-  overflow: hidden;
-  vertical-align: top;
-}
-form.prescription { padding-top: 1ex;}
-
-.newprescription {
-  display: block;
-  margin: 0 2em; padding: .5ex 2ex;
-  background-color: #CFF;
-  border: 1px solid #888;
-  border-radius: 0 0 1ex 1ex;
-}
-
-.prescription * {
-  display: inline-block;
-  font-size: 1em;
-  line-height: 1em;
-  margin: 0; padding: 0;
-}
-.prescription label {
-  padding-left: .5ex;
-  font-size: .75em;
-}
-
-.prescription fieldset {
-  display: inline-block;
-  margin: 0; padding: 1ex;
-  margin-right: -.625ex;
-  border: none;
-  vertical-align: top;
-}
-.prescription fieldset br { display: none;}
-
-.prescription span,
-.prescription input,
-.prescription textarea {
-  height: 1.5em;
-  border: 1px solid #800;
-  padding: .25ex;
-  background-color: #FFF;
-}
-.prescription span {
-  background-color: #EEE;
-  padding: .5ex .25ex;
-  white-space: pre-wrap;
-  font-size: .75em;
-  overflow: hidden;
-}
-
-.prescription input[type=checkbox],
-.prescription input[type=radio] { display: none;}
-
-.prescription label.checkbox,
-.prescription label.radio,
-.prescription input[type=checkbox] + label,
-.prescription input[type=radio] + label { padding-left: 1.25em; font-size: 1em;}
-
-.prescription label.checkbox:before,
-.prescription label.radio:before,
-.prescription input[type=checkbox] + label:before,
-.prescription input[type=radio] + label:before {
-  display: inline-block;
-  position: absolute;
-  margin-left: -1.25em;
-  width: .75em; height: .75em;
-  background-color: #FFF;
-  border: 1px solid #800;
-  content: ' ';
-}
-.prescription label.radio:before,
-.prescription input[type=radio] + label:before { border-radius: .5em;}
-.prescription label.checkbox.checked:before,
-.prescription label.radio.checked:before,
-.prescription input[type=checkbox]:checked + label:before,
-.prescription input[type=radio]:checked + label:before { content: "\2713";}
-
-.prescription a.button,
-.prescription input[type=submit],
-.prescription button {
-  height: 1.5em;
-  color: #FFF;
-  background-color: #800;
-  text-align: center;
-  text-decoration: none;
-  margin: 0; padding: .125em 0 0 0;
-  border: none;
-}
-.prescription a.button {padding: .5ex;}
-
-/* ======== Specific ========== */
-
-.prescription label.presctype,
-.prescription input[name=presctype] + label {
-  font-size: medium;
-  width: 22%;
-  margin: 0; margin-right: -.5ex;
-  vertical-align: top;
-  padding: .25em .5ex .25em 3ex;
-  height: 2.5em;
-  border-top: 1px solid #DDD;
-}
-.prescription label.presctype {
-  text-align: right;
-  font-weight: bold;
-  font-size: .875em;
-  padding-right: 1ex;
-  padding-left: 0;
-}
-
-input[name=presctype][value\$=private]:checked  ~ fieldset,
-  input[name=presctype][value\$=private] + label,
-  .prescription.private { background-color: #CFC;}
-input[name=presctype][value\$=selfpaid]:checked  ~ fieldset,
-  input[name=presctype][value\$=selfpaid]         + label,
-  .prescription.selfpaid { background-color: #FFC;}
-input[name=presctype][value=doctor_compulsory]:checked  ~ fieldset,
-  input[name=presctype][value=doctor_compulsory]  + label,
-  .prescription.doctor.compulsory { background-color: #CFF;}
-input[name=presctype][value=dentist_compulsory]:checked  ~ fieldset,
-  input[name=presctype][value=dentist_compulsory] + label,
-  .prescription.dentist.compulsory { background-color: #FCC;}
-input[name=presctype][value^=altpractition]:checked  ~ fieldset,
-  input[name=presctype][value^=altpractition]     + label,
-  .prescription.altpractition { background-color: #FCF;}
-
-.prescription .baseinfo { width: 60%;}
-
-  .baseinfo label[for=insurance],
-  .baseinfo #insurance { width: 100%; }
-  .baseinfo label[for=name],
-  .baseinfo #name { width: 65%; margin-right: -.875ex;}
-  .baseinfo #name { height: 4em; }
-  .baseinfo label[for=bday],
-  .baseinfo #bday { width: 35%;}
-  .baseinfo #bday { height: 4em; text-align: center; vertical-align: top;}
-  .baseinfo label[for=date],
-  .baseinfo #date { width: 34%; margin-left: 65%;}
-  .baseinfo #date { text-align: right;}
-
-.prescription .misc { width: 40%; }
-
-  .misc h1 {
-    font-size: 1.25em;
-    font-weight: bold;
-    width: 100%;
-  }
-  .misc label[for=addcontrib],
-  .misc label[for=contribconfirm] {width: 100%;}
-  .misc #addcontrib,
-  .misc #contribconfirm {width: 100%; text-align: right;}
-
-.prescription label[for=prescreviewed] {
-  margin-left: 1ex;
-  font-weight: bold;
-  text-decoration: underline;
-  background-color: #FCC;
-}
-.prescription label[for=prescreviewed].checked,
-.prescription :checked + label[for=prescreviewed] {
-  font-weight: normal;
-  text-decoration: none;
-  background-color: transparent;
-}
-
-.prescription .catalogue { width: 100%; }
-
-  .catalogue h2:nth-of-type(1) {
-    font-weight: bold;
-    width: 100%;
-    margin-bottom: .25em;
-  }
-  .catalogue label {
-    display: inline-block;
-    width: 33%;
-    margin-right: -.625ex;
-    margin-top: .25em;
-    vertical-align: top;
-  }
-  .catalogue label[for=presccontinual] { margin-right: 33%;}
-
-.prescription .description { width: 100%; position: relative;}
-  .description * { margin-right: -.625ex; vertical-align: top; }
-  .description label {vertical-align: bottom;}
-
-  .description label[for=quantity] { width: 20%;}
-  .description label[for=remidy] { width: 60%; }
-  .description label[for=quantity_weekly] { width: 20%;}
-  .description #quantity,
-  .description .quantity { width: 20%;  height: 3em; text-align: center;}
-  .description #remidy,
-  .description .remidy { width: 60%; height: 3em;}
-  .description #quantity_weekly,
-  .description .quantity_weekly { width: 20%; height: 3em; text-align: center;}
-
-  .description .indicator_codes {display: inline-block; width: 20%; padding: 0; padding-top: 1.5ex;}
-  .description label[for=indicator],
-  .description label[for=icd10] { display: block; width: 100%;}
-  .description #icd10,
-  .description #indicator {width: 100%; text-align: right;}
-
-  .description .indicator_reading { display: inline-block; width: 78%; padding: 0; padding-top: 1.5ex;  margin-left: 2%;}
-  .description label[for=indicator_reading],
-  .description #indicator_reading { width: 100%; display: block;}
-  .description #indicator_reading { height: 4em;}
-
-.prescription .therapy_dates span { min-width: 8em; margin: 0 .5ex;}
-
-.prescription .issuer { display: inline-block; width: 50%; padding: 0; padding-top: 0; margin-left: 50%;}
-.prescription .issuer label:first-of-type {
-   display: block;
-   position: relative;
-   width: 50%; left: -50%; top: 2.25em;
-   font-size: 1em;
-   text-align: right;
-   padding-right: 1ex;
- }
-.prescription .issuer input[type=radio] + label:before { content: none; }
-.prescription .issuer input[type=radio] { display: none; }
-.prescription .issuer input[type=radio] + label {
-  display: inline-block;
-  width: 50%;
-  padding: .25ex 0; margin: 0;
-  text-align: center;
-  border: 1px solid black;
-}
-.prescription .issuer input[type=radio]:checked + label {
-  font-weight: bold;
-  background-color: #FFF;
-  border-width: 1px;
-  border-bottom: 1px solid #FFF;
-}
-.prescription .issuer input[type=radio] + label + input + label + select,
-.prescription .issuer input[type=radio] + label + select + input { display: none; }
-.prescription .issuer input[type=radio]:checked + label + input + label + select,
-.prescription .issuer input[type=radio]:checked + label + select + input {
-  display: block; width: 100%;
-  border: 1px solid black;
-  background-color: #FFF;
-  border-width: 0 1px 1px 1px;
-  padding: .25ex .5ex;
-  margin-top: -1px;
-}
-.prescription .issuer input[type=radio]:checked + label + input + label + select option { display: block;}
-.prescription span#issuer { width: 100%; height: 3em; padding: 1ex 2ex;}
-
-.prescription .controls { width: 100%; }
-  .controls a.button,
-  .controls button[value=save],
-  .controls button[value=cancel],
-  .controls button[value=delete] { width: 25%;}
-
diff --git a/static/therapy.css b/static/therapy.css
deleted file mode 100644 (file)
index b17497c..0000000
+++ /dev/null
@@ -1,360 +0,0 @@
-.trailbtn:checked + fieldset.trailbox { display: block;}
-
-.trailbtn:checked + .trailbox + .trailbtn {
-  display: block;
-  height: 2.25em; padding: 0 3ex;
-  font-size: 1em; font-weight: normal;
-  color: #000; background-color: #FDD;
-  border: 1px solid #000;
-  border-radius: 4px;
-}
-.trailbtn:checked + .trailbox + .trailbtn[type=submit]:before {content: none;}
-
-
-* { box-sizing: border-box; }
-body {
-  overflow: scroll;
-  position: relative;
-  width: 100%;
-  margin: 0; padding: 0;
-  padding-top: 2em;
-}
-
-form > button[type=submit] {
-  position: fixed; display: block;
-  top: 0; right: 2.5em;
-  height: 2.25em; padding: 0 3ex;
-  font-size: 1em; font-weight: bold;
-  color: #000; background-color: #FDD;
-  border-width: 1px; border-color: #000;
-  border-style: none solid solid solid;
-  border-radius: 0 0 4px 4px;
-  z-index: 3;
-}
-form > button[type=submit]:hover {
-  background-color: #FEE;
-}
-
-input.tab { display: none; }
-input.tab + label.tab { display: block; }
-input.tab + label.tab::before { content: '\25b8 \00a0'; float: left;}
-input.tab:checked + label.tab::before { content: '\25be \00a0'; }
-input.tab + label.tab + div.tab { display: none; }
-input.tab:checked + label.tab + div.tab { display: block; }
-
-input.color { display: none }
-input.color + label{
-  display: inline-block;
-  width: 1em; height: 1em;
-  border: 1px solid black;
-}
-input.color:checked + label{ border-width: 3px;}
-input.color[value="#000"] + label,
-input.color[value="#888"] + label { background-color: #888;}
-input.color[value="#00A"] + label { background-color: #00F;}
-input.color[value="#0A0"] + label { background-color: #0F0;}
-input.color[value="#0AA"] + label { background-color: #0FF;}
-input.color[value="#A00"] + label { background-color: #F00;}
-input.color[value="#A0A"] + label { background-color: #F0F;}
-input.color[value="#AA0"] + label { background-color: #FF0;}
-input.color[value="#FFF"] + label { background-color: #FFF;}
-
-h1, label.tab, div.tab, fieldset.tab,
-div.patient, div.prescription {
-  display: block;
-  width: 96%;
-  margin: 0 2%;
-}
-
-div.prescription span {
-  display: inline-block;
-  width: 50%;
-  margin-right: -.75ex;
-  vertical-align: top;
-}
-div.prescription span label { font-weight: bold; margin-right: 1ex;}
-div.prescription span.prescno,
-div.prescription span.catalogue {
-  width: 33%;
-  font-weight: bold;
-  margin-bottom: .5em;
-  padding: .5ex 1ex;
-}
-
-div.prescription ul {margin-top: 0;}
-
-div.prescription label.checkbox,
-div.prescription label.radio {
-  display: block;
-  padding-left: 1.25em;
-  font-size: 1em;
-  margin: .5em 0;
-}
-
-div.prescription label.checkbox:before,
-div.prescription label.radio:before {
-  display: inline-block;
-  color: #000;
-  background-color: #FFF;
-  height: 1.375em; width: 1.125em;
-  padding: .125em 0 0 .375em;
-  margin: 0 .5em .25em -1.25em;
-  border: 1px solid #000;
-  vertical-align: middle;
-  content: ' ';
-}
-div.prescription label.radio:before { border-radius: .5em;}
-div.prescription label.checkbox.checked:before,
-div.prescription label.radio.checked:before { content: "\2713";}
-
-div.prescription label[for=prescreviewed] {
-  margin-left: 1ex;
-  font-weight: bold;
-  text-decoration: underline;
-  background-color: #FCC;
-}
-div.prescription label[for=prescreviewed].checked {
-  font-weight: normal;
-  text-decoration: none;
-  background-color: transparent;
-}
-
-div.prescription label.tab {width: 96%; border: none; border-bottom: 1px dotted;}
-div.prescription div.tab { width: 96%; background-color: #DDD;}
-
-input.stickynote { display:none; }
-input.stickynote + .stickynote {
-  position: fixed;
-  background-color: #FF8;
-  top: 4em; bottom: 4em;
-  left: -4.5em; width: 5em;
-  padding: 1ex;
-  max-height: 90%;
-  z-index: 2;
-}
-input.stickynote + .stickynote:nth-of-type(2n) {
-  background-color: #8FF;
-  top: 8em;
-}
-
-input.stickynote + .stickynote > * { display: none; }
-input.stickynote + .stickynote > label {
-  position: absolute;
-  top: 0; bottom: 0; right: .5ex;
-  display: block;
-  text-align: right;
-  font-weight: bold;
-}
-input.stickynote + .stickynote:hover {
-  left: -1ex;
-}
-input.stickynote:checked + .stickynote {
-  width: auto; left: 1em; right: 1em;
-}
-input.stickynote:checked + .stickynote > * { display: block; }
-input.stickynote:checked + .stickynote > textarea {
-  display: block;
-  position: absolute;
-  left; 0; right: 0; bottom: 0; top: 0;
-  width: 100%; height: 100%;
-  background-color: #FF8;
-  padding: 2em 1em;
-}
-input.stickynote:checked + .stickynote > button[type="submit"] {
-  display: block;
-  position: absolute;
-  right: .5ex; bottom: .5ex;
-  z-index: 2;
-}
-input.stickynote:checked + .stickynote > label {
-  display: block;
-  position: static;
-  font-size: 0;
-}
-input.stickynote:checked + .stickynote > label:before {
-  position: absolute;
-  font-size: initial;
-  content: "x";
-  top: .5ex; right: .5ex;
-  padding: .125ex .75ex;
-  background-color: #000;
-  color: #FFF;
-  border-radius: 1ex;
-  z-index: 2;
-}
-
-fieldset.penwidth,
-fieldset.color {
-  position: absolute;
-  right: 0; width: 2em;
-  margin: .5em .5em .125em 2%;
-  border: none;
-  padding: 0;
-}
-fieldset.penwidth { bottom: 19em; }
-fieldset.penwidth > input {display: none;}
-fieldset.penwidth > input + label { display: none;}
-fieldset.penwidth > input:checked + label {
-  display: block;
-  width: 2em; height: 2em;
-  background-color: #000;
-  border: 1em solid #FFF;
-  border-radius: 1em;
-}
-fieldset.penwidth > input[value="4"]  + label { border-width: .75em; }
-fieldset.penwidth > input[value="12"] + label { border-width:  .5em; }
-fieldset.penwidth > input[value="36"] + label { border-width: .25em; }
-
-fieldset.color { bottom: 0; }
-fieldset.color > input.color + label {
-  width: 2em; height: 2em;
-}
-.dotmark {
-  max-width: 90%;
-  margin: .5em 1em .125em 2%; padding: 0;
-  text-align: left;
-  border: 1px solid black;
-}
-.dotmark.ov {
-  position: absolute;
-  left: 0; bottom: .25em;
-  z-index: 1;
-}
-
-@media(min-width: 800px){
-  h1, label.tab, div.tab, fieldset.tab,
-  div.patient, div.prescription {
-    width: 38%;
-    margin-right: 0;
-  }
-  input.stickynote:checked + .stickynote { right: 50%; }
-  fieldset.penwidth,
-  fieldset.color { position: fixed; }
-  .dotmark {
-    position: fixed;
-    max-width: 52%;
-    max-height: 98%;
-    right: 2em; bottom: .25em;
-  }
-  .dotmark.ov {
-    position: fixed;
-    right: 2em; left: auto;
-  }
-}
-
-h1 {display: none;}
-
-div.patient, div.prescription { margin-top: 1em; }
-div.prescription {
-  border: 1px solid black;
-  background-color: #EEE;
-  padding: .125em 1.25ex .5em 1.25ex;
-}
-div > h2 { margin: 0; border-bottom: 1px solid black; }
-div:nth-child(n+2) > a:first-of-type {
-  display: block;
-  margin: .125em 0 .5em 0;
-  text-decoration: none;
-}
-
-#report fieldset.tab,
-#report label.tab {
-  font-size: 1.25em;
-  font-weight: bold;
-  padding: .125em 1ex .25em 1ex;
-  color: #FFF;
-  background-color: #333;
-  margin-top: .125em;
-  text-align: right;
-  border: none;
-}
-#report label.heading {
-  background-color: #FFF;
-  margin-top: 1em;
-  border: 2px solid black;
-  border-bottom-width: 1px;
-  color: black;
-}
-#report label.heading > span {
-  text-decoration: underline;
-}
-
-#report fieldset.tab > *,
-#report label > input,
-#report label > span {
-  display: inline-block;
-  text-align: right;
-}
-#report .tab > .no {
-  width:  10%; float: left;
-  border: solid 1px #FFF;
-  background-color: #555;
-  border-radius: 2ex;
-  padding: 0;
-  text-align: center;
-}
-#report label.heading > span.no {
-  background-color: inherit;
-  border: none;
-}
-#report .tab > .date      { width: 30%; }
-#report .tab > .therapist { width: 30%; }
-#report .tab > .signature { width: 20%; }
-#report label.tab > .signature { font-size: .75em; }
-
-#report .signature > input[type=checkbox] {
-  display: inline;
-  font-weight: bold;
-  font-size: 1.25em;
-}
-#report .signature > input[type=checkbox]:before {
-  display: block; width: 1.25em;
-  margin: -.125em 0 0 -.5ex;
-  background-color: #FFF;
-  text-align: center;
-  content: "\00a0 \00a0 \00a0";
-}
-#report .signature > input[type=checkbox]:checked::before {
-  content: "\2713";
-}
-
-#report input.tab + label.tab > input.date,
-#report input.tab + label.tab > input.therapist {
-  display: none;
-}
-#report input.tab:checked + label.tab > input.date,
-#report input.tab:checked + label.tab > input.therapist {
-  display: inline;
-}
-#report input.tab:checked + label.tab > span.date,
-#report input.tab:checked + label.tab > span.therapist {
-  display: none;
-}
-
-#report div.tab {
-  border: 2px solid #333;
-  border-top-width: 1px;
-  margin-top: -1px;
-  padding: .25em .5ex 1em .5ex;
-}
-#report div.tab > fieldset.note {
-  border: none;
-  margin: 0; padding: 0;
-}
-#report div.tab > fieldset.note > textarea {
-  display: block;
-  width: 93%; height: 8em;
-  margin: -8em 0 .5em 2em;
-  font: normal 1em sans-serif;
-}
-div.tab > fieldset.note > input.color + label { margin: 0; display: block; }
-div.tab > fieldset.note > input.color[value="#888"]:checked ~ textarea { background-color: #AAA; }
-div.tab > fieldset.note > input.color[value="#00A"]:checked ~ textarea { background-color: #88F; }
-div.tab > fieldset.note > input.color[value="#0A0"]:checked ~ textarea { background-color: #8F8; }
-div.tab > fieldset.note > input.color[value="#0AA"]:checked ~ textarea { background-color: #8FF; }
-div.tab > fieldset.note > input.color[value="#A00"]:checked ~ textarea { background-color: #F88; }
-div.tab > fieldset.note > input.color[value="#A0A"]:checked ~ textarea { background-color: #F8F; }
-div.tab > fieldset.note > input.color[value="#AA0"]:checked ~ textarea { background-color: #FF8; }
-div.tab > fieldset.note > input.color[value="#FFF"]:checked ~ textarea { background-color: #FFF; }
-
-div.tab > button.delete {float: right; display: inline-block; margin-top: -1em; display: none;}
diff --git a/static/therapy_background.png b/static/therapy_background.png
deleted file mode 100644 (file)
index a0574d5..0000000
Binary files a/static/therapy_background.png and /dev/null differ
diff --git a/static/therapy_background.xcf b/static/therapy_background.xcf
deleted file mode 100644 (file)
index e69c024..0000000
Binary files a/static/therapy_background.xcf and /dev/null differ
diff --git a/static/therapy_draw.js b/static/therapy_draw.js
deleted file mode 100644 (file)
index 8a8e936..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright 2016 Paul Hänsch
-//
-// This file is part of Confetti.
-// 
-// Confetti is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-// 
-// Confetti is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU Affero General Public License for more details.
-// 
-// You should have received a copy of the GNU Affero General Public License
-// along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-body = document.body
-dbg = document.getElementById("jsdebug")
-canvas = document.getElementById("canvas")
-data=document.getElementById("image_serialize")
-
-image = canvas.getContext("2d")
-mouse = 0
-image_serialize=""
-
-// start and current coordinates of a draw
-// serves for tracking, whether path ends close to its beginning
-stx=0, sty=0
-cux=0, cuy=0
-
-function setstroke(w) {
-  image.lineWidth = w
-  data.value += " stroke-width " + image.lineWidth
-}
-function setcol(c) {
-  this.c = c
-  image.strokeStyle = c
-  image.fillStyle =  c
-  data.value += " stroke " + c + "F"
-}
-
-function relX(x){
-  if ( body.clientWidth >= 800 ){
-    return Math.floor(cscaleW * (x - canvas.offsetLeft))
-  } else { 
-    return Math.floor(cscaleW * (x - canvas.offsetLeft + window.pageXOffset))
-  }
-}
-function relY(y){
-  if ( body.clientWidth >= 800 ){
-    return Math.floor(cscaleH * (y - canvas.offsetTop))
-  } else { 
-    return Math.floor(cscaleH * (y - canvas.offsetTop + window.pageYOffset))
-  }
-}
-
-function draw(x, y) {
-  if ( mouse == 1){
-    cux=relX(x), cuy=relY(y)
-
-    image.lineTo( cux, cuy )
-    image.stroke()
-
-    image_serialize += " " + cux + "," + cuy
-  }
-}
-
-function drawstart(x, y) {
-  mouse = 1
-
-  cscaleW = canvas.width / canvas.clientWidth
-  cscaleH = canvas.height / canvas.clientHeight
-
-  stx=relX(x), sty=relY(y)
-
-  setstroke(document.querySelector('input[name="penwidth"]:checked').value);
-  setcol(document.querySelector('input[name="color"]:checked').value);
-
-  image.beginPath()
-  draw(x, y)  // why must this not use relative Coords ???
-
-  image_serialize = " polyline"
-}
-
-function drawstop() {
-  // if path ends close to beginning ( < 50 px); then close path and fill
-  if ( false && mouse == 1 && Math.sqrt( Math.pow(stx - cux, 2) + Math.pow(sty - cuy, 2)) <= 50 && c !== "#FFF" ){
-    image.lineTo( stx, sty )
-    image.stroke()
-
-    image.globalAlpha = .5
-    image.fill()
-    image.globalAlpha = 1
-
-    image_serialize += " " + stx + "," + sty
-    data.value += " fill " + c + "8" + image_serialize
-  } else if (mouse == 1)  {
-    data.value += " fill #0000 " + image_serialize
-  }
-  dbg.innerHTML = " stx: " + stx + " cux: " + cux + " sty: " + sty + " cuy: " + cuy
-  image.closePath()
-  image_serialize = ""
-  mouse = 0
-}
-
-window.addEventListener( 'mouseup',   function()   { drawstop() } )
-canvas.addEventListener( 'mousedown', function(e)  { drawstart(e.clientX, e.clientY) } )
-canvas.addEventListener( 'mousemove', function(e)  {      draw(e.clientX, e.clientY) } )
-
-window.addEventListener( 'touchend',   function()  { drawstop() } )
-canvas.addEventListener( 'touchstart', function(e) { drawstart(e.touches[0].clientX, e.touches[0].clientY) } )
-canvas.addEventListener( 'touchmove',  function(e) { e.preventDefault(); draw(e.touches[0].clientX, e.touches[0].clientY) } )
diff --git a/style.css b/style.css
new file mode 100644 (file)
index 0000000..98b839d
--- /dev/null
+++ b/style.css
@@ -0,0 +1,399 @@
+h1:first-child, h2:first-child, h3:first-child,
+p + h1, p + h2, p + h3 {
+  margin-top: 3pt;
+}
+
+/* ====== COMMON ELEMENTS ======*/
+
+body > ul.menu {
+  display: block;
+  height: 1.75em;
+  margin: 0; -padding: 0 .5em;
+  list-style: none;
+  color: #CCC;
+  background-color: #444;
+  box-shadow: inset 0 -.25em .25em #000;
+  overflow: hidden;
+  z-index: 3;
+}
+
+body > .menu li {
+  display: inline-block;
+}
+body > .menu a {
+  color: inherit;
+  padding: .5em 3em;
+  box-shadow: inset 0 0 .5em #000;
+}
+body.ledgers > .menu a[href$="/ledgers/"],
+body.courses > .menu a[href$="/courses/"],
+body.cards > .menu a[href$="/cards/"] {
+  color: #000;
+  background-color: #FFF;
+  box-shadow: none;
+}
+
+/* =========== FILTER AND SEARCH Headers ========= */
+
+form.upload, form.categories,
+form.search, form.sort, form.filter, form.newcard, form.newcourses {
+  margin-top: 1em; padding: .125em 1em 0 1em;
+  z-index: 1;
+}
+form.filter > h1 { display: none; }
+
+form.filter fieldset { margin-top: .5em; }
+form.filter fieldset.item + fieldset.item legend { display: none; }
+
+form.filter fieldset.item input[value=course] ~ input[type=text],
+form.filter fieldset.item input[value=CATEGORIES] ~ input[type=text] { display: block; }
+form.filter fieldset.item input[value=course]:checked ~ input[type=text],
+form.filter fieldset.item input[value=CATEGORIES]:checked ~ input[type=text] { display: none; }
+form.filter fieldset.item input[value=course]:checked ~ fieldset.courses,
+form.filter fieldset.item input[value=CATEGORIES]:checked ~ fieldset.categories { display: block; }
+
+form.filter fieldset.order legend {
+  float: left; margin-right: 1em;
+}
+
+form.filter fieldset label,
+form.filter fieldset a { white-space: pre;}
+form.filter button[type=submit] {
+  margin-top: .5em; margin-bottom: .5em;
+}
+
+form.filter button[value=export_csv] { margin-left: 1em; }
+
+body.courses form .order { display: inline-block; margin-right: 2em;}
+body.courses form.search.sort fieldset { margin-top: .5em; }
+
+body.cards form.newcard { display: flex; }
+body.cards form.newcard input[name=seed] { flex: 1; }
+
+form.upload label {
+  display: block;
+  font-weight: bold;
+  margin-top: .5em;
+}
+
+
+/* ============ LIST ITEMS, Generic ============= */
+
+body > form,
+div.card,
+div.course {
+  position: relative;
+  width: 98%; width: calc(100% - 2em);
+  margin-left: auto; margin-right: auto;
+  margin-bottom: 1em;
+  box-shadow: .125em .125em .25em;
+  z-index: 1;
+}
+
+/* HACK: put anchor point 10em above card and highlight target element */
+    div:target { box-shadow: none; z-index: 0; }
+    div:target:before {
+      content: '';
+      display: block;
+      margin-top: -10em;
+      height: 10em;
+      visibility: hidden;
+    }
+    div:target:after {
+      content: '';
+      display: block;
+      position: absolute;
+      left: 0; right: 0;
+      top: 10em; bottom: 0;
+      box-shadow: .125em .125em .25em;
+      animation: highlight 4s;
+      z-index: -1;
+    }
+    @keyframes highlight { from { background-color: #FF0; } to { background-color: transparent; } }
+/**/
+
+div .section, form .section {
+  display: block;
+  vertical-align: top;
+  padding: 0 1em;
+  overflow: hidden;
+  word-break: break-word;
+}
+
+div .section :last-child, form .section :last-child {
+  margin-bottom: 1em;
+}
+
+div .section h2, form .section h2,
+div .section h3, form .section h3 { 
+  border-bottom: 1pt solid #EEE;
+}
+div .control, form .control {
+  background-color: #EEE;
+  padding: .25em;
+  text-align: right;
+}
+
+@media(min-width: 60em) {
+  div .section, form .section {
+    display: table-cell;
+    width: calc(100% / 10);
+  }
+  div .section :last-child { margin-bottom: 0; }
+  div .control, form .control {
+    background-color: transparent;
+  }
+  div .section:nth-of-type(2n) {
+    background-color: #EEE;
+  }
+}
+@media(min-width: 80em) {
+  div .control, form .control {
+    display: table-cell;
+    width: calc(100% / 10);
+  }
+  div .control .item, form .control .item {
+    display: block;
+    margin-bottom: .25em;
+  }
+}
+
+div .section .item, form .section .item,
+form .section.attendance > label {
+  display: block;
+  width: 100%;
+}
+
+div .section .item.NOTE {
+  white-space: pre-wrap;
+}
+
+form .section .item {
+  margin-bottom: .25em;
+}
+
+form .section button[value^=addfield] {
+  font-size: .75em;
+  margin-top: .5em; padding: 0 .375em;
+}
+
+/* HACK: "responsive" Delete Button above each field */
+    form input.delete { display: none; }
+    form input.delete + label {
+      float: right;
+      font-size: .75em;
+      line-height: 1;
+      max-width: 1.75em; height: 1.125em; overflow: hidden;
+      color: #FBB; background-color: #444;
+      margin: 0; padding: .125em .5em 0 .5em;
+      border-radius: 4pt 4pt 0 0;
+      transition: max-width .3s;
+    }
+    form input.delete + label:before  { content: '\274c '; margin-right: .5em; }
+    form input.delete + label:hover   { max-width: 10em; }
+    -form input.delete + label:hover:before { content: ''; }
+    -form input.delete + label:hover:after  { content: ' \274c'; }
+    form input.delete:checked + label,
+    form input.delete:checked + label + *,
+    form input.delete:checked + label + .teltype + .TEL {
+      display: none;
+    }
+/**/
+
+
+/* ====== right hand Control Buttons on list items ====== */
+
+form .control {
+  position: relative;
+  padding-left: 11em;
+  padding-top: 1.5em;
+}
+form .control .item {
+  display: inline-block;
+  margin-bottom: .25em;
+  vertical-align: text-bottom;
+}
+
+/* Combined Select/Submit Box */
+    form .control .item.newfield { box-shadow: .125em .125em .25em; }
+    form .control .item.newfield select { margin-right: -1pt; }
+    form .control .item.newfield button { box-shadow: none; }
+/**/
+
+/* HACK: Delete Checkbox before delete Button */
+    form .control .item.delete {
+      position: absolute;
+      bottom: .375em; left: .25em; width: auto;
+      padding-bottom: calc(2.25em + 2pt);
+    }
+    
+    form .control .item.delete input + label + button {
+      display: none;
+      position: absolute;
+      bottom: 0; width: 100%;
+      color: #800;
+      background-color: #FEE;
+      z-index: 1;
+    }
+    form .control .item.delete:after {
+      content: attr(label);
+      display: block;
+      position: absolute;
+      bottom: 0; width: 100%;
+      text-align: center;
+      color: #BAA;
+      padding: .25em 0;
+      border: 1pt solid;
+      box-shadow: .125em .125em .25em;
+    }
+    form .control .item.delete input:checked + label + button { display: block; }
+/**/
+
+@media(min-width: 80em) {
+  form .control { padding: .25em; min-height: 16em; }
+  form .control .item { width: 100%; }
+  form .control .item.newfield select { width: calc(100% - 2.5em); }
+  form .control .item.delete { bottom: .125em; right: .25em; }
+}
+
+
+/* ======= LIST ITEMS, Cards ======= */
+
+form.card .attendance div.attendance,
+form.card .attendance div.categories {
+  max-height: 10em;
+  overflow-y: auto;
+}
+form.card .attendance label {
+  display: inline-block;
+  width: calc(100% - 2em);
+  vertical-align: top;
+  margin-bottom: 0;
+}
+form.card .attendance input { margin-top: .375em; }
+
+
+/* ======= LIST ITEMS, Courses ======= */
+
+form.course .dtstart input[name=DTS_YEAR],
+form.course .dtstart select[name=DTS_MONTH] { width: calc(50% - 1.25em); }
+form.course .dtstart input[name=DTS_YEAR] { margin-right: -.375em; }
+form.course .dtstart table { width: 100%; margin: 1em 0; }
+form.course .dtstart table td { text-align: right; -border: .5pt solid; }
+form.course .dtstart table input[type=radio] { display: none; }
+form.course .dtstart table input[type=radio] + label {
+  display: block;
+  width: 100%;
+  margin: 0; padding: 0 3pt;
+}
+form.course .dtstart table input[type=radio]:checked + label {
+  font-weight: bold;
+  padding: 0 2pt;
+  box-shadow: .125em .125em .25em;
+}
+
+form.course .dtstart label.DTSTIME {
+  display: inline-block;
+  font-weight: bold;
+  margin: 0;
+  width: calc(100% - 7.875em);
+}
+form.course .dtstart input[name=DTS_HOUR],
+form.course .dtstart input[name=DTS_MINUTE] {
+  vertical-align: baseline;
+  width: 3.5em;
+  margin-bottom: 0;
+}
+
+form.course .recur .item { white-space: nowrap; }
+form.course .recur .item > * { margin-bottom: 0; vertical-align: baseline; }
+form.course .recur input[name=RRULE_INTERVAL],
+form.course .recur input[name=RRULE_COUNT],
+form.course .recur input[name=RRULE_UMONTH],
+form.course .recur input[name=RRULE_UDAY] { width: 3.5em; }
+form.course .recur input[name=RRULE_UYEAR] { width: 4.5em; }
+form.course .recur input[name=RRULE_UYEAR],
+form.course .recur input[name=RRULE_UMONTH],
+form.course .recur input[name=RRULE_UDAY] {
+  margin-right: -.375em;
+}
+
+form.course .attendance div.attendance {
+  max-height: 16em;
+  overflow-y: scroll;
+}
+form.course .attendance label {
+  display: inline-block;
+  max-width: calc(100% - 2em);
+  vertical-align: top;
+  margin-bottom: 0;
+}
+form.course .attendance input { margin-top: .375em; }
+
+/* ======== Categories Page ======== */
+
+body.categories form ul { list-style: none; margin: 0; }
+
+form.categories li {
+  display: inline-block;
+  background-color: #EEE;
+  margin-right: .5em; margin-bottom: .5em;
+  padding-left: .5em;
+  box-shadow: .125em .125em .25em;
+}
+form.categories li button[name=remove] {
+  font-size: .75em;
+  width: 2.5em;
+  background-color: #FBB;
+  overflow: hidden;
+  white-space: pre;
+}
+form.categories li button[name=remove]:before {
+  content: '\274C ';
+  margin-right: 3em;
+}
+
+form.categories li:last-child { padding-left: 0 }
+
+body.categories form.namelist ul.namelist > li:nth-of-type(2n + 1) { background-color: #EEE; }
+body.categories form.namelist ul.namelist > li h2,
+body.categories form.namelist ul.namelist > li ul {
+  display: inline-block;
+}
+body.categories form.namelist ul.namelist > li h2 {
+  width: 20%;
+  min-width: 10em;
+}
+body.categories form.namelist ul.namelist > li ul li {
+  display: inline-block;
+}
+
+form.ibanassign,
+form.ledgers {
+  padding: .125em 1em 0 1em;
+}
+
+.ibanassign fieldset.iban.sure { background-color: #DFD; }
+.ibanassign fieldset.iban.guess { background-color: #FFD; }
+.ibanassign fieldset.iban.unknown { background-color: #FDD; }
+
+.ibanassign fieldset.iban {
+  padding: 0 .75em;
+  margin-top: -.5em;
+  box-shadow: .25em .25em .25em #AAA;
+}
+.ibanassign fieldset.iban legend {
+  top: .75em;
+}
+.ibanassign fieldset.iban p.principal {
+  font-size: .875em;
+}
+.ibanassign fieldset.iban p.amount,
+.ibanassign fieldset.iban p.subject {
+  display: inline-block;
+  vertical-align: top;
+}
+.ibanassign fieldset.iban p.amount {
+  font-weight: bold;
+  margin-right: .75em;
+}
diff --git a/templates/cards.html.sh b/templates/cards.html.sh
deleted file mode 100755 (executable)
index bf84eee..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-# Copyright 2014 - 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-setchecked() {
-  printf 'checked="checked"'
-}
-check_type(){
-  [ "${_GET[filter]%%:*}" = "$1" ] && setchecked
-}
-check_order(){
-  [ "${_GET[order]}" = "$1" ] && setchecked
-}
-
-filter_item() {
-cat <<EOF
-  <fieldset class="item">
-    <legend>$(l10n filter_item):</legend>
-    <input type="hidden" name="filter${n}" value="^"/>
-
-    <input  id="any$n" type="radio" name="filter$((n + 1))" value="any:" $([ "$1" = any ] && setchecked)>
-    <label for="any$n" >$(l10n filter_all)</label>
-
-    <input  id="name$n" type="radio" name="filter$((n + 1))" value="name:" $([ "$1" = name ] && setchecked)>
-    <label for="name$n">$(l10n filter_name)</label>
-
-    <!--
-    <input  id="adr$n" type="radio" name="filter$((n + 1))" value="ADR:" $([ "$1" = ADR ] && setchecked)>
-    <label for="adr$n">$(l10n ADR)</label>
-    -->
-
-    <input  id="street$n" type="radio" name="filter$((n + 1))" value="street:" $([ "$1" = street ] && setchecked)>
-    <label for="street$n">$(l10n filter_street)</label>
-
-    <input  id="zip$n" type="radio" name="filter$((n + 1))" value="zip:" $([ "$1" = zip ] && setchecked)>
-    <label for="zip$n">$(l10n filter_zip)</label>
-
-    <input  id="telephone$n" type="radio" name="filter$((n + 1))" value="TEL:" $([ "$1" = TEL ] && setchecked)>
-    <label for="telephone$n">$(l10n filter_phone)</label>
-
-    <input  id="birth$n" type="radio" name="filter$((n + 1))" value="BDAY:" $([ "$1" = BDAY ] && setchecked)>
-    <label for="birth$n">$(l10n filter_birthyear)</label>
-
-    <input  id="cat$n" type="radio" name="filter$((n + 1))" value="CATEGORIES:" $([ "$1" = CATEGORIES ] && setchecked)>
-    <label for="cat$n">$(l10n CATEGORIES)</label>
-    <input type="text" name="filter$((n + 2))" value="$([ "$1" = CATEGORIES ] || attribsafe "$2")" placeholder="$(l10n filter_placeholder)"/>
-    <fieldset class="categories">
-      $(m=3
-        list_categories \
-        | while read cat; do
-          printf '<label><input type="checkbox" name="filter%i" value="|%s" %s/>%s</label>' \
-                 "$((n + m))" "$(attribsafe "$cat")" "$(printf %s "$cat" |grep -qEx "$2" && setchecked)" "$(htmlsafe "$cat")"
-          m=$((m + 1))
-        done
-      )
-      <a href="?p=categories">$(l10n edit_categories)</a>
-    </fieldset>
-
-    <!--
-    ${profile_circus:+
-    <input  id="course" type="radio" name="filtertype" value="course" $(check_type course)>
-    <label for="course">$(l10n filter_course)</label>
-    }
-    -->
-  </fieldset>
-EOF
-}
-
-cat <<EOF
-<form class="filter" action="?action=filter_card" method="POST">
-  <h1>$(l10n filter_label)</h1>
-  <input type="hidden" name="page" value="cards"/>
-
-  $(
-  n=0; m="$(list_categories |wc -l)"
-  printf '%s\n' "${_GET[filter]}" |tr '^' '\n' \
-  | while read filter; do
-    [ -n "$filter" ] && filter_item "${filter%%:*}" "${filter#*:}" "$n"
-    n=$((n + 3 + m))
-  done
-  filter_item any '' "$n"
-  )
-
-  <fieldset class="order">
-    <legend>$(l10n filter_order):</legend>
-    <label><input type="radio" name="order" value="firstname" $(check_order firstname)>$(l10n filter_firstname)</label>
-    <label><input type="radio" name="order" value="lastname"  $(check_order lastname) >$(l10n filter_lastname)</label>
-    <label><input type="radio" name="order" value="bdate"     $(check_order bdate)    >$(l10n filter_bdate)</label>
-  </fieldset>
-  <button type="submit" name="choice" value="new_filter">$(l10n filter_apply)</button>
-  <button type="submit" name="choice" value="del_filter">$(l10n filter_cancel)</button>
-</form>
-
-<form class="newcard" action="?action=new_card" method="POST">
-  <button type="submit">$(l10n newcard)</button>
-</form>
-
-${edit:+$(edit_card "$edit")}
-EOF
-
-listcards |grep ${edit:+-v} "$edit" \
-| while read card; do
-  cat <<-ENDCARD
-       <div id="${card}" class="card">
-         $(view_card "$card")<!--
-         --><div class="control">
-           <a class="item" href="?action=edit_card&card=${card}">$(l10n edit)</a>
-           <a class="item" href="?action=export_vcard&card=${card}">$(l10n vcf_export)</a>
-            ${profile_medical:+
-           <a class="item" href="?action=new_prescription&client=${card}">$(l10n new_prescription)</a>
-           }
-         </div>
-       </div>
-       ENDCARD
-done
-
-# vi:set filetype=html:
diff --git a/templates/categories.html.sh b/templates/categories.html.sh
deleted file mode 100755 (executable)
index 75223bb..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-# Copyright 2015, 2017, 2018 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-cat_listing(){
-  list_categories | while read cat; do
-    printf '<li>%s<button type="submit" name="remove" value="%s">%s</button></li>\n' \
-           "$(htmlsafe "$cat")" "$(attribsafe "$cat")" "$(l10n cat_remove)"
-  done
-}
-
-list_catsel(){
-  card="$1"
-  cats="$(get_categories "$card")"
-
-  list_categories |while read cat; do
-    printf '<li><label><input %s type="checkbox" name="%s" value="%s">%s</label></li>' \
-          "$(printf %s "$cats" |grep -qF "$cat" && printf 'checked="checked"')" \
-           "$(attribsafe "$card")" "$(attribsafe "$cat")" "$(htmlsafe "$cat")" 
-  done
-}
-
-display_catsel(){
-  card="$1"
-  printf '<li><h2>%s</h2><ul>' "$(htmlsafe "$(get_name "$card")")"
-  list_catsel "$card"
-  printf '</ul></li>'
-}
-
-cat <<EOF
-<form class="categories" action="?action=edit_categories" method="POST">
-  <input type="submit" class="fakeorder" name="add" value="add" />
-  <h1>$(l10n categories_label)</h1>
-  <input type="hidden" name="page" value="categories"/>
-  <ul>
-    $(cat_listing)
-    <li>
-      <input type="text" name="newcat" placeholder="$(l10n cat_newlabel)">
-      <button type="submit" name="add" value="add">$(l10n cat_add)</button>
-    </li>
-  </ul>
-</form>
-
-<form class="namelist" action="?action=update_categories" method="POST">
-  <fieldset>
-    <button type="submit" name="submit" value="submit">$(l10n cat_update)</button>
-  </fieldset>
-  <ul class="namelist">
-EOF
-listcards \
-| while read card; do
-  display_catsel "$card"
-done
-cat <<EOF
-  </ul>
-  <fieldset>
-    <button type="submit" name="submit" value="submit">$(l10n cat_update)</button>
-  </fieldset>
-</form>
-EOF
-
diff --git a/templates/course_print.sh b/templates/course_print.sh
deleted file mode 100755 (executable)
index 72ab8b8..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright 2014 - 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-. ${_EXEC}/templates/text_cards.sh
-
-echo -E '
-\documentclass[landscape,10pt]{article}
-\usepackage[utf8x]{inputenc}
-\usepackage{ngerman}
-\usepackage{eurosym}
-\usepackage[landscape,margin=0.25in]{geometry}
-\usepackage{longtable}
-
-\begin{document}
-
-\section*{Teilnehmende}
-\begin{longtable}{|p{60mm}|l|p{50mm}|p{80mm}|}
-\hline
-  \textbf{'"$(l10n N)"'} &
-  \textbf{'"$(l10n BDAY)"'} &
-  \textbf{'"$(l10n TEL)"'} &
-  \textbf{'"$(l10n NOTE)"'} \\
-\hline
-\hline
-\endhead
-'"$(
-list_attendance "$course" |sort -k 2 |while read line; do
-  cardfile="$(echo "$line" |cut -d\  -f1)"
-  list_attendee "$cardfile" |sed -r 's:$:\\\\[3ex] \\hline:'
-done
-)"'
-\end{longtable}
-
-\newpage
-
-\section*{Termine}
-\begin{longtable}{|p{60mm}|c|c|c|c|c|c|c|c|c|c|}
-\hline
- '"$(get_dates)"' \\
-\hline
-\hline
-\endhead
-'"$(
-tex_clean "$(list_attendance "$course")" |sort -k 2 | debug |sed -r 's:^[0-9a-z\.]+ (.+) \(\*[0-9]{4}\)$:\1:;s:$: \& \& \& \& \& \& \& \& \& \& \\\\[3ex] \\hline:'
-)"'
-\end{longtable}
-
-\end{document}
-'
diff --git a/templates/courses.html.sh b/templates/courses.html.sh
deleted file mode 100755 (executable)
index 99e3381..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright 2014 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-edit="${_GET[edit]}"
-
-check_order(){
-  [ "${_GET[order]}" = "$1" ] && echo 'checked="checked"'
-}
-
-cat <<EOF
-<div class="sort">
-  <form class="search" action="?" method="GET">
-    <input type="hidden" name="page" value="courses"/>
-    <input type="hidden" name="edit" value="$edit"/>
-    <span class="label">$(l10n sort_order):</span>
-    <input type="radio" name="order" value="DOW" $(check_order DOW)>$(l10n order_DOW)</input>
-    <input type="radio" name="order" value="TOD" $(check_order TOD)>$(l10n order_TOD)</input>
-    <br>
-    <button type="submit" name="" value="">$(l10n order_apply)</button>
-  </form>
-</div>
-
-<div class="newcourse">
-  <form action="?action=new_course" method="POST">
-    <button type="submit">$(l10n newcourse)</button>
-  </form>
-</div>
-
-<div class="courselist">
-$(
-[ -f "ical/$edit" -o -f "temp/$edit" ] && edit_course "$edit"
-if [ "$?" = 0 ]; then
-  listcourses |grep -v "$edit"
-else
-  listcourses
-fi |while read card; do
-  view_course "$card"
-done
-)
-</div>
-EOF
-
-# vi:set filetype=html:
diff --git a/templates/edit_card.sh b/templates/edit_card.sh
deleted file mode 100755 (executable)
index bc71db2..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-# Copyright 2014 - 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-setchecked() {
-  printf 'checked="checked"'
-}
-setselected() {
-  printf 'selected="selected"'
-}
-check_gen() {
-  [ "$values[GENDER]" = "$1" ] && setselected
-}
-check_a() {
-  egrep -q "^${1}.${id}$" "$_DATA/mappings/attendance" && setchecked
-}
-check_c() {
-  for n in CATEGORIES CATEGORIES{0..100}; do
-    if [ -n "${values[$n]+x}" ]; then
-      [ "${values[$n]}" = "$1" ] && setchecked && break
-    else
-      break
-    fi
-  done
-}
-
-teltype() {
-  cat <<-EOF
-       <select class="item phonetype" name="phonetype">
-         <option value="" disabled="disabled" $([ -z "$1" ] && setselected )>$(l10n phone_typeselect)</option>
-         <option value=""></option>
-         <option value="HOME" $([ "$1" = HOME ] && setselected)>$(l10n phone_home)</option>
-         <option value="CELL" $([ "$1" = CELL ] && setselected)>$(l10n phone_cell)</option>
-         <option value="WORK" $([ "$1" = WORK ] && setselected)>$(l10n phone_work)</option>
-         <option value="FAX"  $([ "$1" = FAX  ] && setselected)>$(l10n phone_fax )</option>
-       </select>
-       EOF
-}
-
-list_items(){
-  item="$1"
-  placeholder="$2"
-  [ -n "${values[$item]+x}" ] && printf '<h3>%s</h3>\n' "$(l10n $item)"
-  for n in "$item" "$item"{0..100}; do
-    if [ -n "${values[$n]+x}" ]; then
-      case "$item" in
-        (ADR|NOTE)
-          printf '<textarea class="item %s" name="%s">%s</textarea>\n' \
-            "$item" "$item" "$(htmlsafe ${values[$n]})"
-          ;;
-        (TEL)
-          teltype "${values[${n}_TYPE]}"
-          printf '<input class="item %s" name="%s" value="%s" />\n' \
-            "$item" "$item" "$(attribsafe ${values[$n]})"
-          ;;
-        (*)
-          printf '<input class="item %s" name="%s" value="%s" placeholder="%s"/>\n' \
-            "$item" "$item" "$(attribsafe ${values[$n]})" "$placeholder"
-          ;;
-      esac
-    else
-      break
-    fi
-  done
-}
-
-list_section(){
-  printf '<div class="section %s">' "$1"
-  shift 1
-  for each in $@; do
-    list_items "$each"
-  done
-  printf '</div>'
-}
-
-hi_company="${values[X-HEALTH-INSURANCE+0]}"
- hi_number="${values[X-HEALTH-INSURANCE+1]}"
- hi_status="${values[X-HEALTH-INSURANCE+2]}"
-
-[ -z "$values[UID]" ] && values[UID]="$(uuidgenerator)"
-
-cat <<END_HTML
-<form id="$id" class="card" action="?action=update_card" method="POST">
-  <input type="hidden" name="card" value="${id}"/>
-  <div class="section basic">
-    <h3>$(l10n N)</h3>
-    <input class="item N" name="3N" placeholder="$(l10n n_pre)" value="$values[N+3]" />
-    <input class="item N" name="1N" placeholder="$(l10n n_first)" value="$values[N+1]" />
-    <input class="item N" name="2N" placeholder="$(l10n n_middle)" value="$values[N+2]" />
-    <input class="item N" name="0N" placeholder="$(l10n n_last)" value="$values[N+0]" />
-    <input class="item N" name="4N" placeholder="$(l10n n_post)" value="$values[N+4]" />
-
-    <select class="item GENDER" name="GENDER">
-      <option value="" disabled="disabled">$(l10n GENDER)</option>
-      <option value="female" $(check_gen female)>$(l10n gender_female)</option>
-      <option value="male" $(check_gen male)>$(l10n gender_male)</option>
-      <option value="other" $(check_gen other)>$(l10n gender_other)</option>
-      <option value="none" $(check_gen none)>$(l10n gender_none)</option>
-    </select>
-END_HTML
-
-    for n in NICKNAME NICKNAME{0..100}; do
-      [ -z "${values[$n]+x}" ] && break \
-      || printf '
-          <input class="item NICKNAME" name="NICKNAME" value="%s" placeholder="%s"/>
-      ' "$(attribsafe ${values[$n]})" "$(l10n NICKNAME)"
-    done
-
-    list_items BDAY YYYY-MM-DD
-    list_items X-ZACK-JOINDATE YYYY-MM-DD
-    list_items X-ZACK-LEAVEDATE YYYY-MM-DD
-
-    [ -n "$values[SOUND]" ] && printf '
-      <audio controls="controls" class="item SOUND">
-        <source type="audio/ogg" src="data:audio/ogg;base64,%s" />
-      </audio>' "$values[SOUND]"
-
-    [ -n "$values[PHOTO]" ] && printf '
-      <img class="item PHOTO" src="data:image/%s;base64,%s" />
-      ' "${values[PHOTO_TYPE]}" "${values[PHOTO]}"
-
-    [ -n "$values[LOGO]" ] && printf '
-      <img class="item PHOTO" src="data:image/%s;base64,%s" />
-      ' "${values[LOGO_TYPE]}" "${values[LOGO]}"
-
-printf '</div>'
-
-if [ "$PROFILE" = circus ]; then
-  list_section phone TEL
-  list_section message EMAIL IMPP URL
-  list_section address ADR
-  list_section note NOTE X-CLIENT-REFERRAL
-  cat <<-END_HTML
-       <div class="section attendance">
-         <h3>$(l10n course_attendance)</h3>
-         $(listcourses |while read each; do
-           cname="$(sed -rn 's:^SUMMARY\:(.*)$:\1:p' "$_DATA/ical/$each")"
-           printf '<label><input type="checkbox" name="attendance" value="%s" %s/>%s</label>' \
-             "$(attribsafe "$each")" "$(check_a "$each")" "$(htmlsafe "$cname")"
-         done)
-          <h3>$(l10n CATEGORIES)</h3>
-          $(list_categories |while read each; do
-            printf '<label><input type="checkbox" name="CATEGORIES" value="%s" %s/>%s</label>' \
-              "$(attribsafe "$each")" "$(check_c "$each")" "$(htmlsafe "$each")"
-          done)
-       </div>
-       END_HTML
-elif [ "$PROFILE" = medical ]; then
-  list_section address ADR
-  list_section phone TEL EMAIL IMPP URL
-  cat <<-END_HTML
-       <div class="section insurance">
-         <h3>$(l10n X-HEALTH-INSURANCE)</h3>
-         <input type="radio" name="hi_select" value="list" id="hi_select_list" checked /><label for="hi_select_list">$(l10n hi_from_list)</label
-         ><input type="radio" name="hi_select" value="other" id="hi_other"><label for="hi_other">$(l10n hi_other)</label>
-         <select class="item" name="hi_company">
-           <option value="" disabled="disabled" ${hi_company:-selected}>$(l10n hi_company)</option>
-           $(list_hi_companies |while read -r f; do
-             printf '<option value="%s" %s>%s</option>
-             '  "$(attribsafe "$f")" "$([ "$f" = "$hi_company" ] && setselected )" "$(htmlsafe "$f")"
-           done)
-         </select>
-         <input type="text" name="hi_other" value="$hi_company" placeholder="$(l10n hi_company)" />
-         <input name="hi_number" value="$hi_number" placeholder="$(l10n hi_number)" />
-         <input name="hi_status" value="$hi_status" placeholder="$(l10n hi_status)" />
-       </div>
-       END_HTML
-  list_section note NOTE X-CLIENT-REFERRAL
-fi
-
-
-cat <<END_HTML
-  <div class="control">
-    <select class="item" name="newfield">
-      <option value="" disabled="disabled" selected="selected">$(l10n edit_addfieldtext)</option>
-      $(for f in $SUP_FIELDS; do printf '<option value="%s">%s</option>\n' "$f" "$(l10n "$f")"; done)
-    </select
-    ><button class="item" type="submit" name="action" value="addfield">$(l10n edit_addfield)</button>
-    <button class="item" type="submit" name="action" value="update">$(l10n edit_update)</button>
-    <input type="checkbox" id="delete"><label class="item" for="delete">$(l10n edit_delete)</label>
-      <button class="item" type="submit" name="action" value="delete">$(l10n edit_delete)</button>
-    <button class="item" type="submit" name="action" value="cancel">$(l10n edit_cancel)</button>
-  </div>
-  <input type="hidden" name="UID" value="$values[UID]"/>
-</form>
-END_HTML
diff --git a/templates/edit_course.sh b/templates/edit_course.sh
deleted file mode 100755 (executable)
index 0e1cd73..0000000
+++ /dev/null
@@ -1,176 +0,0 @@
-# Copyright 2014 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-SUP_FIELDS=(SUMMARY COMMENT)
-[ -z "$values[UID]" ] && values[UID]="$(uuidgenerator)"
-
-dtstart="$values[DTSTART]"
-[ -z "$dtstart" ] && dtstart=$(date +%Y%m%dT%H%M%S)
-
-echo "$dtstart" |case "$dtstart" in
-  *Z)
-    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$:\1-\2-\3 \4\:\5\:\6 UTC:p'
-    ;;
-  TZID*)
-    sed -rn 's:^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:TZ="\1" \2-\3-\4 \5\:\6\:\7:p'
-    ;;
-  *)
-    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:\1-\2-\3 \4\:\5\:\6:p'
-    ;;
-esac |read dts_date
-
-dts_year=$(date -d "$dts_date" +%Y)
-dts_month=$(date -d "$dts_date" +%m)
-dts_dom=$(date -d "$dts_date" +%d)
-dts_dow=$(date -d "$dts_date" +%u)
-dts_weekday=$(date -d "$dts_date" +%A)
-dts_hour=$(date -d "$dts_date" +%H)
-dts_min=$(date -d "$dts_date" +%M)
-dts_sec=$(date -d "$dts_date" +%S)
-
-rrule="$values[RRULE]"
-rr_int="$(echo $rrule |sed -rn 's:^.*INTERVAL=([0-9]+)(;.*)?$:\1:p')"
-rr_count="$(echo $rrule |sed -rn 's:^.*COUNT=([0-9]+)(;.*)?$:\1:p')"
-rr_freq="$(echo $rrule |sed -rn 's:^.*FREQ=(YEARLY|MONTHLY|WEEKLY|DAILY)(;.*)?$:\1:p')"
-rr_uyear="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\1:p')"
-rr_umonth="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\2:p')"
-rr_uday="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\3:p')"
-if [ -n "$rr_count" ]; then
-  rr_limit=COUNT
-elif [ -n "$rr_uyear" ]; then
-  rr_limit=UNTIL
-else
-  rr_limit=ETERN
-fi
-
-echo '<div id="'${id}'" class="course">'
-echo '<form action="?action=update_course" method="POST">'
-echo '<input type="hidden" name="course" value="'${id}'"/>'
-echo '  <div class="section basic">'
-echo '    <h2>'$(l10n SUMMARY)'</h2>'
-echo '    <input type="text" class="item SUMMARY" name="SUMMARY" value="'$values[SUMMARY]'" />'
-echo '    <h3>'$(l10n COMMENT)'</h3>'
-for n in COMMENT COMMENT{0..10}; do [ -n "$values[$n]" ] &&\
-  echo '    <textarea class="item COMMENT" name="COMMENT">'$values[$n]'</textarea>'
-done
-
-echo '  </div><div class="section dtstart">'
-echo '    <h3>'$(l10n DTSTART)'</h3>'
-echo '    <select class="DTSYEAR" name="DTSYEAR" onchange="this.form.submit();">'
-for y in {$(($dts_year - 50))..$(($dts_year + 50))}; do
-  echo '    <option value="'$y'" '$([ "$y" -eq "$dts_year" ] && echo 'selected="selected"')'>'$y'</option>'
-done
-echo '    </select>'
-echo '    <select class="DTSMONTH" name="DTSMONTH" onchange="this.form.submit();">'
-for m in {01..12}; do
-  echo '    <option value="'$m'" '$([ "$m" -eq "$dts_month" ] && echo 'selected="selected"')'>'$(date -d ${dts_year}-${m}-1 +%B)'</option>'
-done
-echo '    </select>'
-echo '    <button class="DTS" type="submit" name="DTS" value="update">'$(l10n edit_dtscal)'</button><br/>'
-for wd in mon tue wed thu fri sat sun; do
-  echo -n '<span class="DTSCALHEAD">'$(date -d $wd +%a)'</span>'
-done
-fdom=$(date -d ${dts_year}-${dts_month}-1 +%u)
-while [ "$fdom" -gt 1 ]; do
-  echo -n '<span class="DTSCAL"></span>'
-  fdom=$(($fdom - 1))
-done
-cnt=$(date -d ${dts_year}-${dts_month}-1 +%s)
-while [ "$(date -d @$cnt +%m)" = "$dts_month" ]; do
-  dn=$(date -d @$cnt +%d)
-  echo -n '<input type="radio" class="DTSCAL" name="DTSDAY" value="'$dn'" id="DTSCAL_'$dn'" '$([ $dn = $dts_dom ] && echo checked)'><label for="DTSCAL_'$dn'" class="DTSCAL">'$dn'</label>'
-  cnt=$(($cnt + 86400))
-done
-echo '    <br />'
-echo '    <span class="DTSTIME">'$(l10n time)':</span>'
-echo '    <select class="DTSTIME" name="DTSHOUR">'
-for h in {00..23}; do
-  echo '    <option value="'$h'" '$([ "$h" -eq "$dts_hour" ] && echo 'selected="selected"')'>'$h'</option>'
-done
-echo '    </select>:<select class="DTSTIME" name="DTSMINUTE">'
-for m in 00 05 10 15 20 25 30 35 40 45 50 55; do
-  echo '    <option value="'$m'" '$([ "$m" -eq "$dts_min" ] && echo 'selected="selected"')'>'$m'</option>'
-done
-echo '    </select>'
-
-echo '  </div><div class="section recur">'
-echo '    <h3>'$(l10n RRULE)'</h3>'
-echo '    <span class="item">'
-echo '    '$(l10n t_every)''
-echo '    <select class="RRULE INTERVAL" name="RRULE_INTERVAL">'
-for i in {1..365}; do
-  echo '    <option value="'$i'" '$([ "$i" = "$rr_int" ] && echo 'selected="selected"')'>'$i'</option>'
-done
-echo '    </select>'
-echo '    <select class="RRULE FREQ" name="RRULE_FREQ">'
-for f in DAILY WEEKLY MONTHLY YEARLY; do
-  echo '    <option value="'$f'" '$([ "$f" = "$rr_freq" ] && echo 'selected="selected"')'>'$(l10n $f)'</option>'
-done
-echo '    </select></span>'
-echo '    <span class="item">'
-echo '    <input type="radio" name="RRULE_LIMIT" value="ETERN" '$([ "$rr_limit" = ETERN ] && echo 'checked="checked"')'>'$(l10n t_eternal)'</input><br/>'
-echo '    </span><span class="item">'
-echo '    <input type="radio" name="RRULE_LIMIT" value="COUNT" '$([ "$rr_limit" = COUNT ] && echo 'checked="checked"')'></input>'
-echo '    <select class="RRULE COUNT" name="RRULE_COUNT">'
-for t in {1..365}; do
-  echo '    <option value="'$t'" '$([ "$t" = "$rr_count" ] && echo 'selected="selected"')'>'$t'</option>'
-done
-echo '    </select>'$(l10n t_times)'</span>'
-echo '    <span class="item">'
-echo '    <input type="radio" name="RRULE_LIMIT" value="UNTIL" '$([ "$rr_limit" = UNTIL ] && echo 'checked="checked"')'>'$(l10n t_until)'</input>'
-echo '    <select class="RRULE UYEAR" name="RRULE_UYEAR">'
-for uy in {${dts_year}..$(($dts_year + 50))}; do
-  echo '    <option value="'$uy'" '$([ "$uy" = "$rr_uyear" ] && echo 'selected="selected"')'>'$uy'</option>'
-done
-echo '    </select>'
-echo '    <select class="RRULE UMONTH" name="RRULE_UMONTH">'
-for um in {01..12}; do
-  echo '    <option value="'$um'" '$([ "$um" = "$rr_umonth" ] && echo 'selected="selected"')'>'$(date -d 2000-${um}-1 +%B)'</option>'
-done
-echo '    </select>'
-echo '    <select class="RRULE UDAY" name="RRULE_UDAY">'
-for ud in {01..31}; do
-  echo '    <option value="'$ud'" '$([ "$ud" = "$rr_uday" ] && echo 'selected="selected"')'>'$ud'</option>'
-done
-echo '    </select></span>'
-
-echo '  </div><div class="section select_attendance">'
-echo '    <h3>'$(l10n course_attendance)'</h3>'
-listcards |while read card; do
-  cfile="$_DATA/vcard/$card"
-  aname=$(sed -rn 's:^N(;[^"\:]+|;"[^"]+")*\:([^;]*)(\;[^;]*)(\;[^;]*)?(\;[^;]*)?(\;[^;]*)?$:\5 \3 \4 \2 \6:p' "$cfile" \
-          |sed -r 's:,: :;s:\;: :g;s: +: :g' \
-          |tr -d '\r'
-         )
-  selected=$(sed -rn "/^$id\t$card\$/{s;^.*\$;checked=\"checked\";p;q}" "${_DATA}/mappings/attendance")
-  echo '    <label><input type="checkbox" class="item" name="attendance" value="'$card'" '$selected'>'$aname'</label>'
-done
-
-echo '  </div><div class="control">'
-echo '    <select class="item" name="newfield">'
-echo '      <option value="" disabled="disabled" selected="selected">'$(l10n edit_addfieldtext)'</option>'
-            for f in $SUP_FIELDS; do echo "<option value=\"$f\">$(l10n $f)</option>"; done
-echo '    </select'
-echo '    ><button class="item" type="submit" name="action" value="addfield">'$(l10n edit_addfield)'</button>'
-echo '    <button class="item" type="submit" name="action" value="update">'$(l10n edit_update)'</button>'
-echo '    <button class="item" type="submit" name="action" value="delete">'$(l10n edit_delete)'</button>'
-echo '    <button class="item" type="submit" name="action" value="cancel">'$(l10n edit_cancel)'</button>'
-echo '  </div>'
-echo '<input type="hidden" name="UID" value="'$values["UID"]'"/>'
-echo '</form>'
-echo '</div>'
-
diff --git a/templates/edit_prescription.sh b/templates/edit_prescription.sh
deleted file mode 100755 (executable)
index b7a9af4..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-check(){ [ "$1" = "$2" ] && printf checked}
-[ -z $mpx[presctype] ] && mpx[presctype]=doctor_compulsory
-
-cat <<END_HTML
-<form class="prescription" action="?action=update_prescription" method="POST">
-<input type="hidden" name="prescription" value="${mpx[prescription]}">
-
-<label class=presctype>$(l10n doctor):</label>
-<input type=radio name=presctype id=doctor_selfpaid value=doctor_selfpaid $(check "$mpx[presctype]" doctor_selfpaid)>
-<label for=doctor_selfpaid>$(l10n selfpaid)</label>
-<input type=radio name=presctype id=doctor_private value=doctor_private $(check "$mpx[presctype]" doctor_private)>
-<label for=doctor_private>$(l10n private)</label>
-<input type=radio name=presctype id=doctor_compulsory value=doctor_compulsory $(check "$mpx[presctype]" doctor_compulsory)>
-<label for=doctor_compulsory>$(l10n compulsory)</label>
-<br>
-
-<label class=presctype>$(l10n dentist):</label>
-<input type=radio name=presctype id=dentist_selfpaid value=dentist_selfpaid $(check "$mpx[presctype]" dentist_selfpaid)>
-<label for=dentist_selfpaid>$(l10n selfpaid)</label>
-<input type=radio name=presctype id=dentist_private value=dentist_private $(check "$mpx[presctype]" dentist_private)>
-<label for=dentist_private>$(l10n private)</label>
-<input type=radio name=presctype id=dentist_compulsory value=dentist_compulsory $(check "$mpx[presctype]" dentist_compulsory)>
-<label for=dentist_compulsory>$(l10n compulsory)</label>
-<br>
-
-<label class=presctype>$(l10n noprescription):</label>
-<input type=radio name=presctype id=noprescription_selfpaid value=noprescription_selfpaid $(check "$mpx[presctype]" noprescription_selfpaid)>
-<label for=noprescription_selfpaid>$(l10n selfpaid)</label>
-<br>
-
-<label class=presctype>$(l10n altpractition):</label>
-<input type=radio name=presctype id=altpractition_selfpaid value=altpractition_selfpaid $(check "$mpx[presctype]" altpractition_selfpaid)>
-<label for=altpractition_selfpaid>$(l10n selfpaid)</label>
-<input type=radio name=presctype id=altpractition_private value=altpractition_private $(check "$mpx[presctype]" altpractition_private)>
-<label for=altpractition_private>$(l10n private)</label>
-<br>
-
-<fieldset class="baseinfo">
-<label for="insurance">$(l10n insurance)</label>
-<input id="insurance" name="insurance" value="${mpx[insurance]}" placeholder="$(l10n insurance)">
-<br>
-<label for="name">$(l10n name)</label>
-<label for="bday">$(l10n bday)</label>
-<br>
-<textarea id="name" name="name" placeholder="$(l10n name)">${mpx[name]}</textarea>
-<input id="bday" name="bday" value="${mpx[bday]}" placeholder="$(l10n bday)">
-
-<br>
-<label for="date">$(l10n date)</label>
-<input id="date" name="date" value="${mpx[date]}" placeholder="$(l10n date)">
-</fieldset>
-
-<fieldset class="misc">
-<h1 id="${mpx[prescription]}">$(l10n therapy_prescription)</h1>
-
-<label for="addcontrib">$(l10n addcontrib)</label>
-<input id="addcontrib" name="addcontrib" value="${mpx[addcontrib]}" placeholder="$(l10n addcontrib)">
-<label for="contribconfirm">$(l10n contribconfirm)</label>
-<input id="contribconfirm" name="contribconfirm" value="${mpx[contribconfirm]}" placeholder="$(l10n contribconfirm)">
-<input type="checkbox" id="contribreceipt" name="contribreceipt" value="true" ${mpx[contribreceipt]:+checked}>
-<label for="contribreceipt">$(l10n contribreceipt)</label>
-</fieldset>
-
-<input type="checkbox" id="prescreviewed" name="prescreviewed" value="true" ${mpx[prescreviewed]:+checked}>
-<label for="prescreviewed">$(l10n prescreviewed)</label>
-
-<fieldset class="catalogue">
-<h2>$(l10n prescription_by_catalogue)</h2>
-<input type="radio" id="prescfirst" name="prescno" value="first" $(check "$mpx[prescno]" first)>
-<label for="prescfirst">$(l10n prescfirst)</label>
-<br>
-<input type="radio" id="prescfollow1" name="prescno" value="follow1" $(check "$mpx[prescno]" follow1)>
-<label for="prescfollow1">$(l10n prescfollow1)</label>
-<br>
-<input type="radio" id="prescfollow2" name="prescno" value="follow2" $(check "$mpx[prescno]" follow2)>
-<label for="prescfollow2">$(l10n prescfollow2)</label>
-<br>
-<input type="radio" id="prescother" name="prescno" value="other" $(check "$mpx[prescno]" other)>
-<label for="prescother">$(l10n prescother)</label>
-<br>
-<input type="radio" id="presccontinual" name="prescno" value="continual" $(check "$mpx[prescno]" continual)>
-<label for="presccontinual">$(l10n presccontinual)</label>
-
-<br>
-<input type="checkbox" id="grouptherapy" name="grouptherapy" value="true" ${mpx[grouptherapy]:+checked}>
-<label for="grouptherapy">$(l10n grouptherapy)</label>
-<br>
-<input type="checkbox" id="housecall" name="housecall" value="true" ${mpx[housecall]:+checked}>
-<label for="housecall">$(l10n housecall)</label>
-<br>
-<input type="checkbox" id="report" name="report" value="true" ${mpx[report]:+checked}>
-<label for="report">$(l10n report)</label>
-</fieldset>
-
-<fieldset class="description">
-<label for="quantity">$(l10n quantity)</label>
-<label for="remidy">$(l10n remidy)</label>
-<label for="quantity_weekly">$(l10n quantity_weekly)</label>
-<p>
-<input id="quantity" name="quantity" value="${mpx[quantity]}" placeholder="$(l10n quantity)">
-<textarea id="remidy" name="remidy" placeholder="$(l10n remidy)">${mpx[remidy]}</textarea>
-<input id="quantity_weekly" name="quantity_weekly" value="${mpx[quantity_weekly]}" placeholder="$(l10n quantity_weekly)">
-</p>
-
-$( for n in {0..10}; do
-  if [ "$n" -eq 0 -o -n "${mpx[quantity$n]}" -o -n "${mpx[remidy$n]}" -o -n "${mpx[quantity_weekly$n]}" ]; then
-    printf '<input class="trailbtn" type="checkbox" checked="checked" />'
-  else
-    printf '<input class="trailbtn" type="checkbox" />'
-  fi
-  printf '<p class="trailbox">
-    <input class="quantity" name="quantity" placeholder="%s" value="%s">
-    <textarea class="remidy" name="remidy" placeholder="%s">%s</textarea>
-    <input class="quantity_weekly" name="quantity_weekly" placeholder="%s" value="%s">
-    </p>
-  ' "$(l10n quantity)" "${mpx[quantity$n]}" \
-    "$(l10n remidy)" "${mpx[remidy$n]}" \
-    "$(l10n quantity_weekly)" "${mpx[quantity_weekly$n]}"
-done )
-
-<br>
-<p class="indicator_codes">
-<label for="indicator">$(l10n indicator)</label>
-<input id="indicator" name="indicator" value="${mpx[indicator]}" placeholder="$(l10n indicator)">
-<br>
-<label for="icd10">$(l10n icd10)</label>
-<input id="icd10" name="icd10" value="${mpx[icd10]}" placeholder="$(l10n icd10)">
-</p>
-<br>
-<p class="indicator_reading">
-<label for="indicator_reading">$(l10n indicator_reading)</label>
-<textarea id="indicator_reading" name="indicator_reading" placeholder="$(l10n indicator_reading)">${mpx[indicator_reading]}</textarea>
-</p>
-<br>
-<p class="issuer">
-  <label>$(l10n issuer)</label>
-  <input type="radio" name="issuer_select" value="list" id="issuer_select_list" checked /><label for="issuer_select_list">$(l10n issuer_from_list)</label><!--
-  --><input type="radio" name="issuer_select" value="other" id="issuer_other"><label for="issuer_other">$(l10n issuer_other)</label>
-  <select class="item" name="issuer">
-    <option value="" disabled="disabled" $([ -z "${mpx[issuer]}" ] && printf 'selected' )>$(l10n issuer)...</option>
-    $(list_prescription_issuers |while read f; do
-      [ "$f" = "$mpx[issuer]" ] \
-      && printf '<option value="%s" selected>%s</option>' "$f" "$f" \
-      || printf '<option value="%s">%s</option>' "$f" "$f"
-    done)
-  </select>
-  <input type="text" name="issuer_other" value="" placeholder="$(l10n issuer)..." />
-</p>
-</fieldset>
-
-<fieldset class="controls">
-<button type="submit" name="action" value="save">$(l10n save)</button>
-<button type="submit" name="action" value="cancel">$(l10n cancel)</button>
-<button type="submit" name="action" value="delete">$(l10n delete)</button>
-</fieldset>
-
-</form>
-END_HTML
diff --git a/templates/email.html.sh b/templates/email.html.sh
deleted file mode 100755 (executable)
index 7f93893..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright 2014 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-cat <<EOF
-<div class="select">
-  <span class="label">$(l10n filter_label)</span>
-  <form class="search" action="?action=select_mailing" method="POST">
-
-    <div class="selectlist">
-      <span class="label">$(l10n select_courses):</span>
-      <select name="courses" multiple="multiple">
-       $(listcourses |while read course; do
-         name="$(sed -rn 's:^SUMMARY(;.+)*\:(.*)\r$:\2:p' "$_DATA/ical/$course")"
-         echo '<option value="'$course'">'$name'</option>'
-       done)
-      </select>'
-    </div>
-
-    <div class="selectlist">
-      <span class="label">$(l10n select_attendees):</span>
-      <select name="courses" multiple="multiple">
-       $(listcards |while read card; do
-         n_last="$(  sed -rn 's:^N(;.+)*\:([^;]*;){0} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
-         n_first="$( sed -rn 's:^N(;.+)*\:([^;]*;){1} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
-         n_middle="$(sed -rn 's:^N(;.+)*\:([^;]*;){2} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
-         n_pre="$(   sed -rn 's:^N(;.+)*\:([^;]*;){3} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
-         n_post="$(  sed -rn 's:^N(;.+)*\:([^;]*;){4} *([^;]*).*$:\3:p' "$_DATA/vcard/$card")"
-         name="${n_pre} ${n_first} ${n_middle} ${n_last} ${n_post}"
-         echo '<option value="'$card'">'$name'</option>'
-       done)
-      </select>'
-    </div>
-
-    <button type="submit" name="choice" value="new_selection">$(l10n selection_apply)</button>
-    <button type="submit" name="choice" value="del_selection">$(l10n selection_clear)</button>
-  </form>
-</div>
-
-<div class="maillist">
-</div>
-EOF
-
-# vi:set filetype=html:
diff --git a/templates/frame.html.sh b/templates/frame.html.sh
deleted file mode 100755 (executable)
index b8128a3..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/bin/zsh
-
-# Copyright 2014, 2015, 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-. "${_EXEC}/templates/text_frame.sh"
-[ -x "${_EXEC}/templates/text_${PAGE}.sh" ] && . "${_EXEC}/templates/text_${PAGE}.sh"
-
-bmfile="${_DATA}/mappings/bookmarks"
-
-cat <<EOF
-<!Doctype HTML>
-
-<html>
-  <head>
-    <title>$(l10n p_${PAGE})</title>
-    <meta name="viewport" content="width=device-width">
-    <link rel="stylesheet" href="?static=common.css">
-    <link rel="stylesheet" href="?static=${PAGE}.css">
-  </head>
-  <body class="$PAGE" id="CONFIGURE">
-    <div class="NAVIGATION">
-      $(sed -rn '/^[^ ]+\t[^ ]+$/p' "$bmfile" \
-        | while read page name; do
-        printf '<a href="%s">%s</a>' "${page}" "${name}"
-      done)
-      <label for="navigationconfig">$(l10n nc_edit)</label>
-      <input  id="navigationconfig" class="config" type="checkbox" />
-      <form class="config" method="POST" action="?action=update_bookmarks">
-        $(for page in "${_EXEC}/pages/"*.sh; do
-          page="${page##*/}"
-          page="${page%.sh}"
-          printf '<a href="?p=%s">%s</a>\n' "${page}" "$(l10n "p_${page}")"
-        done |grep -E '^<a href="\?p='"${page_filter}" )
-        $(if grep -qF "$REQUEST_URI" "$bmfile"; then
-          printf '<input type="hidden" name="bm_url" value="%s" />
-                  <input type="text" name="bm_name" value="%s" readonly="readonly" /
-                  ><button type="submit" name="submit" value="del">%s</button>' \
-                  "$(attribsafe ${REQUEST_URI})" \
-                  "$(attribsafe $(grep -m1 -F "$REQUEST_URI    " "$bmfile" |sed -r 's;.*\t;;'))" \
-                  "$(l10n bm_del)"
-        else
-          printf '<input type="hidden" name="bm_url" value="%s" />
-                  <input type="text" name="bm_name" placeholder="%s" /
-                  ><button type="submit" name="submit" value="add">%s</button>' \
-                  "$(attribsafe ${REQUEST_URI})" "$(l10n Bookmark)" "$(l10n bm_add)"
-        fi)
-      </form>
-    </div>
-EOF
-
-debug BODY; [ -x "${BODY}" ] && . "${BODY}" || printf %s 'Error'
-
-cat <<EOF
-  <div id="footer">
-    <a href="#CONFIGURE">$(l10n configure)</a>
-  </div>
-</body></html>'
-EOF
-
-# vi:set filetype=html:
diff --git a/templates/prescriptions.html.sh b/templates/prescriptions.html.sh
deleted file mode 100755 (executable)
index c4ef19a..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-client="${_GET[client]}"
-edit="${_GET[edit]}"
-[ -n "$edit" ] && client="${edit%.*.mpx}.vcf"
-
-cat <<EOF
-
-<div id="${card}" class="card">$(view_card "$client")</div>
-
-<!--h1>$(l10n prescriptions_current)</h1-->
-
-<div class="newprescription">
-  <form action="?action=new_prescription" method="POST">
-    <input type="hidden" name="client" value="$client">
-    <button type="submit">$(l10n newprescription)</button>
-  </form>
-</div>
-EOF
-
-list_prescriptions "$client" |grep -q "$edit" || edit_prescription "$edit"
-
-list_prescriptions "$client" \
-|while read pre; do 
-  [ "$pre" = "$edit" ] \
-  && edit_prescription "$pre" \
-  || view_prescription "$pre"
-done
-
-#<!--h1>$(l10n prescriptions_past)</h1-->
-
-# vi:set filetype=html:
diff --git a/templates/text_cards.sh b/templates/text_cards.sh
deleted file mode 100755 (executable)
index aaa6832..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-# Copyright 2014, 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-item_name[PHOTO]="Foto"
-item_name[LOGO]="Logo"
-item_name[FN]="Voller Name"
-item_name[N]="Name"
-item_name[n_pre]="Titel"
-item_name[n_first]="Vorname"
-item_name[n_middle]="Mittelnamen"
-item_name[n_last]="Nachname"
-item_name[n_post]="Zusätze"
-item_name[NICKNAME]="Spitzname"
-item_name[SOUND]="Aussprache"
-item_name[GENDER]="Geschlecht"
-item_name[KIND]="Typ"
-item_name[TITLE]="Beruf"
-item_name[ROLE]="Position"
-item_name[ORG]="Organisation"
-item_name[MEMBER]="Mitglied"
-item_name[CATEGORIES]="Kategorien"
-item_name[ANNIVERSARY]="Jubiläum"
-item_name[BDAY]="Geburtstag"
-item_name[EMAIL]="E-Mail"
-item_name[TEL]="Telefon"
-item_name[phone_typeselect]="Typ"
-item_name[phone_home]="Privat"
-item_name[phone_cell]="Mobil"
-item_name[phone_work]="Büro"
-item_name[phone_fax]="Fax"
-item_name[tHOME]="Privat:"
-item_name[tWORK]="Büro:"
-item_name[tCELL]="Mobil:"
-item_name[tFAX]="Fax:"
-item_name[tVOICE]=""
-item_name[IMPP]="Chat"
-item_name[ADR]="Anschrift"
-item_name[URL]="Webseite"
-item_name[LANG]="Sprache"
-item_name[NOTE]="Notiz"
-
-item_name[RELATED]="Kontakte"
-
-item_name[BEGIN]=""
-item_name[CALADRURI]=""
-item_name[CALURI]=""
-item_name[CLASS]=""
-item_name[CLIENTPIDMAP]=""
-item_name[END]=""
-item_name[FBURL]=""
-item_name[GEO]=""
-item_name[MAILER]=""
-item_name[NAME]=""
-item_name[PRODID]=""
-item_name[PROFILE]=""
-item_name[REV]=""
-item_name[SORT-STRING]=""
-item_name[SOURCE]=""
-item_name[TZ]=""
-item_name[UID]=""
-item_name[VERSION]=""
-item_name[XML]=""
-
-item_name[X-HEALTH-INSURANCE]="Kran&shy;ken&shy;ver&shy;sich&shy;er&shy;ung"
-item_name[hi_from_list]="Aus Liste"
-item_name[hi_other]="Andere"
-item_name[hi_company]="Ver&shy;sich&shy;er&shy;ungs&shy;ge&shy;sell&shy;schaft"
-item_name[hi_number]="Ver&shy;sich&shy;er&shy;ten&shy;num&shy;mer"
-item_name[hi_status]="Ver&shy;sich&shy;er&shy;ten&shy;sta&shy;tus"
-item_name[X-HEALTH-INSURANCE-NOCONTRIB]="Zu&shy;zahl&shy;ungs&shy;be&shy;frei&shy;ung"
-item_name[X-CLIENT-REFERRAL]="Empfehl&shy;ung durch"
-item_name[prescriptions]="Verordnungen"
-item_name[new_prescription]="Neue Verordnung"
-item_name[no_icd]="Kein ICD Code"
-
-item_name[X-ZACK-JOINDATE]="An&shy;mel&shy;de&shy;da&shy;tum"
-item_name[X-ZACK-LEAVEDATE]="Ab&shy;mel&shy;de&shy;da&shy;tum"
-item_name[label_join]="Anm."
-item_name[label_leave]="Abm."
-
-item_name[edit]="Bearbeiten"
-item_name[edit_categories]="Kategorien Bearbeiten"
-item_name[vcf_export]="Vcard Exportieren"
-item_name[control]="Aktionen"
-item_name[edit_update]="Daten übernehmen"
-item_name[edit_cancel]="Abbrechen"
-item_name[edit_delete]="Eintrag löschen"
-item_name[edit_addfieldtext]="Neues Feld"
-item_name[edit_addfield]="+"
-item_name[edit_deletefield]="X"
-item_name[filter_label]="Filter"
-item_name[filter_placeholder]="Begriffe zur Eingrenzung eingeben"
-item_name[filter_type]="Filtertyp"
-item_name[filter_order]="Sortierung"
-item_name[filter_all]="Alles"
-item_name[filter_name]="Name"
-item_name[filter_firstname]="Vorname"
-item_name[filter_lastname]="Nachname"
-item_name[filter_street]="Straße"
-item_name[filter_zip]="PLZ."
-item_name[filter_phone]="Telefon"
-item_name[filter_birthyear]="Geburtsjahr"
-item_name[filter_bdate]="Geburtsdatum"
-item_name[filter_course]="Kurs"
-item_name[filter_apply]="Filtern"
-item_name[filter_cancel]="Filter löschen"
-item_name[newcard]="Neuen Eintrag anlegen"
-item_name[course_attendance]="Kursteilnahme"
-
-item_name[gender_none]="keine Angabe"
-item_name[gender_female]="Weiblich"
-item_name[gender_male]="Männlich"
-item_name[gender_other]="Sonstiges"
-
-item_name[female]="&#x2640;"
-item_name[male]="&#x2642;"
-item_name[other]="&#x26A5;"
-item_name[none]="&#x26AA;"
-
diff --git a/templates/text_categories.sh b/templates/text_categories.sh
deleted file mode 100755 (executable)
index 00c61ce..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright 2014, 2015 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-item_name[cat_remove]="&#45;"
-item_name[cat_add]="+"
-item_name[cat_newlabel]="neue Kategorie"
-item_name[cat_update]="Zuweisungen übernehmen"
-item_name[categories_label]="Kategorien"
diff --git a/templates/text_courses.sh b/templates/text_courses.sh
deleted file mode 100755 (executable)
index b86240a..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright 2014, 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-declare -A item_name
-
-item_name[SUMMARY]="Bezeichnung"
-item_name[COMMENT]="Kommentar"
-item_name[DTSTART]="Beginn"
-item_name[DURATION]="Dauer"
-item_name[RRULE]="Regelmäßigkeit"
-
-item_name[DAILY]="Tage"
-item_name[WEEKLY]="Wochen"
-item_name[MONTHLY]="Monate"
-item_name[YEARLY]="Jahre"
-
-item_name[sDAILY]="Täglich"
-item_name[sWEEKLY]="Wöchentlich"
-item_name[sMONTHLY]="Monatlich"
-item_name[sYEARLY]="Jährlich"
-
-item_name[newcourse]="Neuen Eintrag anlegen"
-item_name[time]="Uhrzeit"
-item_name[edit_update]="Daten übernehmen"
-item_name[edit_cancel]="Abbrechen"
-item_name[edit_delete]="Eintrag löschen"
-item_name[edit_addfieldtext]="Neues Feld"
-item_name[edit_addfield]="+"
-item_name[edit_dtscal]="&#x2713;"
-item_name[edit]="Bearbeiten"
-item_name[ics_export]="ICal exportieren"
-item_name[courselist]="Kursliste (PDF)"
-
-item_name[course_attendance]="Teilnahme"
-item_name[course_mail]="Mail an Teilnehmende"
-
-item_name[sort_order]="Sortierung"
-item_name[order_DOW]="Wochentag"
-item_name[order_TOD]="Uhrzeit"
-item_name[order_apply]="Sortieren"
-
-item_name[t_every]="Alle"
-item_name[t_eternal]="ewig"
-item_name[t_times]="mal"
-item_name[t_until]="Bis"
-item_name[t_oclock]="Uhr"
diff --git a/templates/text_frame.sh b/templates/text_frame.sh
deleted file mode 100755 (executable)
index 75e1511..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-# Copyright 2014 - 2016, 2018 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-item_name[p_cards]="Personen"
-item_name[p_courses]="Kurse"
-item_name[p_email]="Email"
-item_name[p_error]="Fehler"
-item_name[p_categories]="Kategorien"
-item_name[p_prescriptions]="Verschreibungen"
-item_name[p_therapy]="Therapie"
-item_name[nc_edit]="&#x2699;"
-#item_name[nc_edit]="&#9776;"
-item_name[bm_add]="+"
-item_name[bm_del]="-"
-item_name[configure]="Anpassen"
diff --git a/templates/text_prescriptions.sh b/templates/text_prescriptions.sh
deleted file mode 100755 (executable)
index 8934bc7..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-. $_EXEC/templates/text_cards.sh
-
-item_name[therapy_prescription]="Heil&shy;mit&shy;tel&shy;ver&shy;ord&shy;nung"
-item_name[prescriptions_current]="Aktuelle Verordnungen"
-item_name[prescriptions_past]="Frühere Verordnungen"
-item_name[newprescription]="Neue Verordnung"
-item_name[date]="Datum"
-item_name[name]="Name d. Versicherten"
-item_name[bday]="geb. am"
-item_name[addcontrib]="Zuzahlung"
-item_name[contribconfirm]="Zuzahlung erfolgt am..."
-item_name[contribreceipt]="Quit&shy;tung heraus&shy;ge&shy;ge&shy;ben"
-item_name[prescreviewed]="Verordnung geprüft"
-item_name[prescreview]="Verordnung prüfen!"
-item_name[quantity]="Ver&shy;ord&shy;nungs&shy;men&shy;ge"
-item_name[remidy]="Heil&shy;mit&shy;tel nach Maß&shy;ga&shy;be des Ka&shy;ta&shy;lo&shy;ges"
-item_name[prescfirst]="Erst&shy;ver&shy;ord&shy;nung"
-item_name[prescfollow1]="1. Folge-&shy;VO"
-item_name[prescfollow2]="2. Folge-&shy;VO"
-item_name[prescother]="VO außer&shy;halb des Re&shy;gel&shy;falls"
-item_name[presccontinual]="Lang&shy;frist&shy;ver&shy;ord&shy;nung"
-item_name[grouptherapy]="Grup&shy;pen&shy;the&shy;ra&shy;pie"
-item_name[housecall]="Haus&shy;be&shy;such"
-item_name[report]="The&shy;ra&shy;pie&shy;be&shy;richt"
-item_name[indicator]="In&shy;di&shy;ka&shy;tions&shy;schlüssel"
-item_name[icd10]="ICD-10-Code"
-item_name[indicator_reading]="Befund Beschreibung"
-item_name[insurance]="Krankenkasse bzw. Kostenträger"
-item_name[prescription_by_catalogue]="Verordnung nach Maßgabe des Kataloges (Regelfall)"
-item_name[therapy_start]="Be&shy;hand&shy;lungs&shy;be&shy;ginn spä&shy;test. am"
-item_name[quantity_weekly]="An&shy;zahl pro Wo&shy;che"
-item_name[save]="Speichern"
-item_name[cancel]="Abbrechen"
-item_name[delete]="Löschen"
-item_name[therapy]="Zur Therapie"
-
-item_name[doctor]="Arzt"
-item_name[dentist]="Zahn&shy;arzt"
-item_name[altpractition]="Heil&shy;prak&shy;tiker"
-item_name[noprescription]="Ohne Ver&shy;ord&shy;nung"
-item_name[selfpaid]="Selbst&shy;zah&shy;lend"
-item_name[private]="Pri&shy;vat"
-item_name[compulsory]="Ge&shy;setz&shy;lich"
-
-item_name[therapy_dates]="Be&shy;hand&shy;lungs&shy;ter&shy;mi&shy;ne"
-
-item_name[issuer]="Ausgestellt durch"
-item_name[issuer_from_list]="Aus Liste"
-item_name[issuer_other]="Andere"
diff --git a/templates/text_therapy.sh b/templates/text_therapy.sh
deleted file mode 100755 (executable)
index a2735b2..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-. $_EXEC/templates/text_prescriptions.sh
-
-item_name[prescriptionlist]="Zur Verordnungsliste"
-item_name[delete_session]="Therapiesitzung entfernen"
-item_name[therapist]="Therapeut"
-item_name[number]="Nr."
-item_name[signature]="Un&shy;ter&shy;schrift"
-item_name[weekly]="p. Woche"
-item_name[notes]="Notizen"
-item_name[trailsave]="Speichern für weitere Felder"
diff --git a/templates/therapy.html.sh b/templates/therapy.html.sh
deleted file mode 100755 (executable)
index 1821c24..0000000
+++ /dev/null
@@ -1,227 +0,0 @@
-# Copyright 2016, 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-t_session_note(){
-  session_n="$1"
-  note_n="$2"
-  
-  color=session${session_n}_note${note_n}_color
-  unset c0 c1 c2 c3 c4 c5 c6 c7
-  
-  if [ "$note_n" -eq 1 -o -n "${tpy[session${session_n}_note${note_n}]}" ]; then
-    printf '<input class="trailbtn" type="checkbox" checked="checked">'
-  else
-    printf '<input class="trailbtn" type="checkbox">'
-  fi
-
-  _checked="$(validate "$tpy[$color]" '#(888|00A|0A0|0AA|A00|A0A|AA0)' '#FFF')"
-       cat <<-EOF
-         <fieldset class="note trailbox">
-           <input class=color type=radio name="$color" value="#888" id=${color}_000 $(checked \#888)><label for=${color}_000></label>
-           <input class=color type=radio name="$color" value="#00A" id=${color}_001 $(checked \#00A)><label for=${color}_001></label>
-           <input class=color type=radio name="$color" value="#0A0" id=${color}_010 $(checked \#0A0)><label for=${color}_010></label>
-           <input class=color type=radio name="$color" value="#0AA" id=${color}_011 $(checked \#0AA)><label for=${color}_011></label>
-           <input class=color type=radio name="$color" value="#A00" id=${color}_100 $(checked \#A00)><label for=${color}_100></label>
-           <input class=color type=radio name="$color" value="#A0A" id=${color}_101 $(checked \#A0A)><label for=${color}_101></label>
-           <input class=color type=radio name="$color" value="#AA0" id=${color}_110 $(checked \#AA0)><label for=${color}_110></label>
-           <input class=color type=radio name="$color" value="#FFF" id=${color}_111 $(checked \#FFF)><label for=${color}_111></label>
-           <textarea name="session${session_n}_note${note_n}">${tpy[session${session_n}_note${note_n}]}</textarea>
-         </fieldset>
-       EOF
-}
-
-t_session(){
-  session_n="$1"
-
-  sid=session${session_n}
-
-  if [ "$tpy[${sid}_sigset]" = pos ]; then
-    sigcheck=checked
-  else
-    unset sigcheck
-  fi
-
-       cat <<-EOF
-         <input type=hidden name=$sid value=exists>
-         <input class="tab" type="checkbox" id=${sid}_open name="${sid}_open" value="checked" ${tpy[${sid}_open]}>
-         <label class="tab" for="${sid}_open">
-           <span class=no>${session_n}.</span><!--
-        --><span class=date>${tpy[${sid}_date]}</span><!--
-        --><input class="date" name="${sid}_date" value="${tpy[${sid}_date]}" placeholder="$(l10n date)"><!--
-        --><span class=therapist>${tpy[${sid}_therapist]}</span><!--
-        --><input class="therapist" name="${sid}_therapist" value="${tpy[${sid}_therapist]}" placeholder="$(l10n therapist)"><!--
-        --><span class=signature><input type="checkbox" name="${sid}_sigset" value="pos" $sigcheck></span>
-         </label>
-         <div class=tab>
-           <img class="dotmark ov" src="/therapies/${id%.tpy}_${sid}.png" alt="">
-           $(n=1;
-              while [ -n "${tpy[session${session_n}_note${n}]+x}" ]; do
-                if [ -n "${tpy[session${session_n}_note${n}]}" ]; then
-                  x=$n
-                fi
-                n=$(($n + 1))
-              done
-              for n in $(seq 1 $((${x:-0} + 3)) ); do t_session_note $session_n $n; done
-             )
-           <button class="trailbtn" type="submit">$(l10n trailsave)</button>
-           <button class=delete type=submit name=delete_session value=${session_n}>$(l10n delete_session)</button>
-         </div>
-       EOF
-}
-
-therapy_sessions(){
-  n=1; while [ -n "${tpy[session${n}]}" ]; do
-    t_session $n
-    n=$(($n+1))
-  done
-
-  sid=session$n
-
-       cat <<-EOF
-         <fieldset class="tab">
-           <button class=no type=submit name=new_session value=$sid>+</button><!--
-        --><input class=date name=${sid}_date value="" placeholder="$(l10n date)" /><!--
-        --><input class=therapist name=${sid}_therapist value="" placeholder="$(l10n therapist)" /><!--
-        --><span class=signature><span></span>
-           <input type=hidden name="${sid}_note1" value="">
-         </fieldset>
-       EOF
-}
-
-cat <<EOF
-<h1>$(l10n therapy)</h1>
-
-<div class="patient">
-  <h2>$client_name</h2>
-  <a href="?p=prescriptions&amp;client=${id%%.*}.vcf">&lt; $(l10n prescriptionlist)</a>
-</div>
-
-<div class="prescription">
-  <h2>$(l10n therapy_prescription)</h2>
-  <span class="insurance">${mpx[insurance]}</span>
-  <span class="date"><label>$(l10n date):</label>${mpx[date]}</span>
-
-  <label class="checkbox ${mpx[prescreviewed]:+checked}" for="prescreviewed">
-    $([ -n "${mpx[prescreviewed]}" ] && printf %s "$(l10n prescreviewed)" \
-                                     || printf %s "$(l10n prescreview)" )
-  </label>
-
-       ${mpx[prescno]:+<span class="prescno">$(l10n presc${mpx[prescno]})</span>}
-  ${mpx[grouptherapy]:+<span class="catalogue">$(l10n grouptherapy)</span>}
-     ${mpx[housecall]:+<span class="catalogue">$(l10n housecall)</span>}
-        ${mpx[report]:+<span class="catalogue">$(l10n report)</span>}
-
-  <ul>$(for n in '' {0..10}; do
-    [ -n "${mpx[remidy$n]}" ] && \
-    printf '<li>%s %s %s</li>' "${mpx[quantity$n]}" \
-                               "${mpx[remidy$n]}" \
-                               "${mpx[quantity_weekly$n]:+($mpx[quantity_weekly$n] $(l10n weekly))}"
-  done)</ul>
-
-  ${mpx[indicator]:+<span class="indicator"><label>$(l10n indicator):</label>${mpx[indicator]}</span>}
-      ${mpx[icd10]:+<span class="icd10">    <label>$(l10n icd10):</label>${mpx[icd10]}</span>}
-
-  ${mpx[addcontrib]:+
-  <label class='checkbox ${mpx[contribconfirm]:+checked}' for="addcontrib">
-    $([ -n "${mpx[contribconfirm]}" ] && printf %s "$(l10n contribconfirm)" ${mpx[contribconfirm]} \
-                                      || printf %s "$(l10n addcontrib)" )
-  </label>
-  }
-
-  <input class="tab" type="checkbox" id="indicator_reading">
-  <label class="tab" for="indicator_reading">$(l10n indicator_reading)</label>
-  <div class="tab">${mpx[indicator_reading]}</div>
-</div>
-
-<form id="report" method="POST" action="?action=update_therapy">
-  <input type="hidden" name="id" value="${id}">
-
-  <input class="stickynote" type="checkbox" name="c_stickynote" id="show_stickynote">
-  <fieldset class="stickynote">
-    <label for="show_stickynote">$(l10n notes)</label>
-    <h2>$(l10n notes)</h2>
-    <textarea name="stickynote">${tpy[stickynote]}</textarea>
-    <button type="submit">$(l10n save)</button>
-  </fieldset>
-
-  <input class="stickynote" type="checkbox" name="c_timesheet" id="show_timesheet">
-  <fieldset class="stickynote">
-    <label for="show_timesheet">$(l10n timesheet)</label>
-    <h2>$(l10n timesheet)</h2>
-    <table><thead>
-      <tr><th>$(l10n time_goal)</th><th>$(l10n time_actual)</th><th>$(l10n time_difference)</th></tr>
-    </thead><tbody>
-      $(for n in '' {0..10}; do
-        printf '<tr><td><input type="number" name="tsgoal" value="%s"/></td>
-                    <td><input type="number" name="tsactual" value="%s"/></td>
-                    <td>%s</td>
-                </tr>\n' \
-                "$mpx[tsgoal${n}]" "$mpx[tsactual${n}]" "$((${mpx[tsgoal${n}]:-0} - ${mpx[tsactual${n}]:-0}))"
-      done)
-    </tbody></table>
-    <button type="submit">$(l10n save)</button>
-  </fieldset>
-
-  <!--input class="tab" type="checkbox" id="lookout">
-  <label class="tab" for="lookout">$(l10n indicator_reading)</label>
-  <div class="tab"><textarea name="lookout">${mpx[lookout]}</textarea -->
-
-  <label class="tab heading">
-    <span class=no>$(l10n number)</span><!--
- --><span class=date>$(l10n date)</span><!--
- --><span class=therapist>$(l10n therapist)</span><!--
- --><span class=signature>$(l10n signature)</span>
-  </label>
-EOF
-
-therapy_sessions
-
-_checked="$(validate "$tpy[penwidth]" '(4|12|36)' '4')"
-cat <<EOF
-  <fieldset class="penwidth">
-    <input type="radio" name="penwidth" value="4"  id="pw1" $(checked  4)><label for="pw2"></label>
-    <input type="radio" name="penwidth" value="12" id="pw2" $(checked 12)><label for="pw3"></label>
-    <input type="radio" name="penwidth" value="36" id="pw3" $(checked 36)><label for="pw1"></label>
-  </fieldset>
-EOF
-
-_checked="$(validate "$tpy[color]" '#(0[0A]{2}|A00|A0A|AA0|FFF)' '#000')"
-cat <<EOF
-  <fieldset class="color">
-    <input class="color" type="radio" name="color" value="#000" id="c000" $(checked \#000)><label for="c000"></label>
-    <input class="color" type="radio" name="color" value="#00A" id="c001" $(checked \#00A)><label for="c001"></label>
-    <input class="color" type="radio" name="color" value="#0A0" id="c010" $(checked \#0A0)><label for="c010"></label>
-    <input class="color" type="radio" name="color" value="#0AA" id="c011" $(checked \#0AA)><label for="c011"></label>
-    <input class="color" type="radio" name="color" value="#A00" id="c100" $(checked \#A00)><label for="c100"></label>
-    <input class="color" type="radio" name="color" value="#A0A" id="c101" $(checked \#A0A)><label for="c101"></label>
-    <input class="color" type="radio" name="color" value="#AA0" id="c110" $(checked \#AA0)><label for="c110"></label>
-    <input class="color" type="radio" name="color" value="#FFF" id="c111" $(checked \#FFF)><label for="c111"></label>
-  </fieldset>
-  <img class="dotmark bg" src="?static=therapy_background.png" alt="WARNING: Background Image not available!">
-  <canvas id="canvas" class="dotmark ov" width="${bg_dim%x*}" height="${bg_dim#*x}"></canvas>
-
-  <input type=hidden id=image_serialize name=imagedata value="">
-
-  <button type="submit">$(l10n save)</button>
-</form>
-
-<span id="jsdebug" style="display: none; position: fixed; right:0; bottom:0">Debug</span>
-
-<script type="text/javascript" src="?static=therapy_draw.js"></script>
-EOF
-
-# vi:set filetype=html:
diff --git a/templates/view_card.sh b/templates/view_card.sh
deleted file mode 100755 (executable)
index 13dbab7..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-# Copyright 2014 - 2017 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-list_items(){
-  item="$1"
-  [ -n "${values[$item]+x}" ] && \
-    printf '<h3>%s</h3>\n' "$(l10n $item)"
-  for n in "$item" "$item"{0..100}; do
-    if [ -z "${values[$n]+x}" ]; then
-      break
-    else case "$item" in
-      EMAIL)
-        printf '<a class="item EMAIL" href="mailto:%s">%s</a>\n' \
-          "$(attribsafe "${values[$n]}")" "$(htmlsafe "${values[$n]}")"
-        ;;
-      *)
-        printf '<span class="item %s">%s</span>\n' \
-          "$item" "$(htmlsafe ${values[$n]})"
-        ;;
-    esac; fi
-  done
-}
-
-list_section(){
-  printf '<div class="section %s">' "$1"
-  shift 1
-  for each in $@; do
-    list_items "$each"
-  done
-  printf '</div>'
-}
-
-n=$(printf %s "$values[N+3] $values[N+1] $values[N+2] $values[N+0] $values[N+4]" \
-    | sed -r ':X;$!{N;bX}; s;^[\n ]+;;; s;[\n ]+$;;; s;[\r\t\n ]+; ;g;'
-   )
-fullname="${n:-${values[FN]:-${values[NICKNAME]}}}"
-
-hi_company="${values[X-HEALTH-INSURANCE+0]}"
- hi_number="${values[X-HEALTH-INSURANCE+1]}"
- hi_status="${values[X-HEALTH-INSURANCE+2]}"
-
-printf '<div class="section basic">
-    <h2 class="item FN">%s</h2>
-' "$fullname"
-[ -n "$values[GENDER]" ] && printf '
-  <span class="item GENDER">%s</span>
-  ' "$(l10n "$values[GENDER]")"
-
-for n in NICKNAME NICKNAME{0..100}; do
-  [ -z "${values[$n]+x}" ] && break \
-  || printf '
-      <span class="item NICKNAME">aka. %s</span>
-  ' "$(htmlsafe ${values[$n]})"
-done
-
-[ -n "$values[BDAY]" ] && printf '
-  <span class="item BDAY"><b>*:</b> %s</span>
-  ' "$(htmlsafe "$values[BDAY]")"
-[ -n "$values[X-ZACK-JOINDATE]" ] && printf '
-  <span class="item X-ZACK-JOINDATE"><b>%s:</b> %s</span>
-  ' "$(l10n label_join)" "$(htmlsafe "$values[X-ZACK-JOINDATE]")"
-[ -n "$values[X-ZACK-LEAVEDATE]" ] && printf '
-  <span class="item X-ZACK-LEAVEDATE"><b>%s:</b >%s</span>
-  ' "$(l10n label_leave)" "$(htmlsafe "$values[X-ZACK-LEAVEDATE]")"
-
-[ -n "$values[SOUND]" ] && printf '
-  <audio controls="controls" class="item SOUND">
-    <source type="audio/ogg" src="data:audio/ogg;base64,%s" />
-  </audio>' "$values[SOUND]"
-
-[ -n "$values[PHOTO]" ] && printf '
-  <img class="item PHOTO" src="data:image/%s;base64,%s" />
-  ' "${values[PHOTO_TYPE]}" "${values[PHOTO]}"
-
-[ -n "$values[LOGO]" ] && printf '
-  <img class="item PHOTO" src="data:image/%s;base64,%s" />
-  ' "${values[LOGO_TYPE]}" "${values[LOGO]}"
-
-if [ "$PROFILE" = circus ]; then
-  printf '</div>'
-
-  list_section phone TEL
-  list_section message EMAIL IMPP URL
-  list_section address ADR
-  list_section note NOTE
-
-  printf '<div class="section attendance"><h3>%s</h3><ul>' "$(l10n course_attendance)"
-  sed -rn 's:(.*)\t'"$id"'$:\1:p' "$_DATA/mappings/attendance" |while read each; do
-    cname="$(sed -rn 's:^SUMMARY\:(.*)$:\1:p' "$_DATA/ical/$each")"
-    printf '   <li><a class="item attendance" href="?p=courses#%s">%s</a></li>' "$each" "$(htmlsafe $cname)"
-  done
-  printf '</ul>'
-  list_items CATEGORIES
-  printf '</div>'
-
-elif [ "$PROFILE" = medical ]; then
-  list_items ADR
-  list_items URL
-  printf '</div>'
-
-  list_section phone TEL EMAIL IMPP
-
-  printf '<div class="section insurance"><h3>%s</h3>' "$(l10n X-HEALTH-INSURANCE)"
-  [ -n "$hi_company" ] && printf '<span class="item hi_comapany">%s</span>' \
-    "$(htmlsafe "$hi_company")"
-  [ -n "$hi_number" ] && printf '<span class="item hi_number"><label>%s:</label> %s</span>' \
-    "$(l10n hi_number)" "$(htmlsafe "$hi_number")"
-  [ -n "$hi_status" ] && printf '<span class="item hi_status"><label>%s:</label> %s</span>' \
-    "$(l10n hi_status)" "$(htmlsafe "$hi_status")"
-  printf '</div>'
-
-  list_section note NOTE X-CLIENT-REFERRAL
-
-  printf '<div class="section prescriptions"><h3>%s</h3><ul>' "$(l10n prescriptions)"
-  declare -A mpx
-  find "$_DATA/prescriptions/" -name "${id%.vcf}.*.mpx" \
-  | while read pfile; do
-    mpx=(); cat "$pfile" |while read -r line; do
-      val="${line#*:}"
-      mpx[${line%%:*}]="$(htmlsafe "${val//\\n/$BR}")"
-    done
-    printf '<li><a href="?p=prescriptions&amp;client=%s#%s" >%s: %s - %s</a></li>' \
-      "${id}" "${pfile##*/}" "${mpx[date]}" "${mpx[indicator]}" \
-      "$([ -n "${mpx[remidy]}" ] && printf '%s %s' "${mpx[quantity]}" "${mpx[remidy]}" 
-         for n in {0..10}; do
-           [ -n "${mpx[remidy${n}]}" ] && printf ', %s %s' "${mpx[quantity${n}]}" "${mpx[remidy${n}]}" 
-         done
-       )"
-  done |sort -r
-  printf '</ul></div>'
-fi
diff --git a/templates/view_course.sh b/templates/view_course.sh
deleted file mode 100755 (executable)
index 23088f1..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-# Copyright 2014 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-dtstart="$values[DTSTART]"
-[ -z "$dtstart" ] && dtstart=$(date +%Y%m%dT%H%M%S)
-
-echo "$dtstart" |case "$dtstart" in
-  *Z)
-    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z$:\1-\2-\3 \4\:\5\:\6 UTC:p'
-    ;;
-  TZID*)
-    sed -rn 's:^TZID=(.+)\:([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:TZ="\1" \2-\3-\4 \5\:\6\:\7:p'
-    ;;
-  *)
-    sed -rn 's:^([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})$:\1-\2-\3 \4\:\5\:\6:p'
-    ;;
-esac |read dts_date
-
-dts_year=$(date -d "$dts_date" +%Y)
-dts_month=$(date -d "$dts_date" +%m)
-dts_dom=$(date -d "$dts_date" +%d)
-dts_dow=$(date -d "$dts_date" +%u)
-dts_weekday=$(date -d "$dts_date" +%A)
-dts_hour=$(date -d "$dts_date" +%H)
-dts_min=$(date -d "$dts_date" +%M)
-dts_sec=$(date -d "$dts_date" +%S)
-
-rrule="$values[RRULE]"
-rr_int="$(echo $rrule |sed -rn 's:^.*INTERVAL=([0-9]+)(;.*)?$:\1:p')"
-rr_count="$(echo $rrule |sed -rn 's:^.*COUNT=([0-9]+)(;.*)?$:\1:p')"
-rr_freq="$(echo $rrule |sed -rn 's:^.*FREQ=(YEARLY|MONTHLY|WEEKLY|DAILY)(;.*)?$:\1:p')"
-rr_uyear="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\1:p')"
-rr_umonth="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\2:p')"
-rr_uday="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T[0-9]{6}Z(;.*)?$:\3:p')"
-rr_udate="$(echo $rrule |sed -rn 's:^.*UNTIL=([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2})Z(;.*)?$:\1-\2-\3 \4\:\5\:\6:p')"
-if [ -n "$rr_count" ]; then
-  rr_limit=COUNT
-elif [ -n "$rr_uyear" ]; then
-  rr_limit=UNTIL
-else
-  rr_limit=ETERN
-fi
-
-coursemail=""
-
-echo '<div id="'${id}'" class="course">'
-echo '<form action="?action=update_course" method="POST">'
-echo '<input type="hidden" name="course" value="'${id}'"/>'
-echo '  <div class="section basic">'
-echo '    <h2>'$values[SUMMARY]'</h2>'
-echo '    <span class="text">'$(date -d "$dts_date" "+%A %B %d, %Y - %H:%M") $(l10n t_oclock)'</span>'
-[ "$rr_int" -eq 1 ] \
-&& echo '<span class="text">'$(l10n s$rr_freq)'</span>' \
-|| echo '<span class="text">'$(l10n t_every) ${rr_int} $(l10n $rr_freq)'</span>'
-case "$rr_limit" in
-  COUNT)
-    m1=$(echo $rr_freq |sed -r "s:DAILY:day:g;s:WEEKLY:week:g;s:MONTHLY:month:g;s:YEARLY:year:g;")
-    m2=$(($rr_int * $rr_count))
-    echo '<span class="text">'$(l10n t_until) $(date -d "$dts_date + $m2 $m1" "+%A %B %d, %Y - %H:%M")'</span>'
-    ;;
-  UNTIL)
-    echo '<span class="text">'$(l10n t_until) $(date -d "$rr_udate" "+%A %B %d, %Y - %H:%M")'</span>'
-    ;;
-esac
-echo '  </div>'
-
-echo '  <div class="section COMMENT">'
-echo '    <h3>'$(l10n COMMENT)'</h3>'
-for n in COMMENT COMMENT{0..10}; do [ -n "$values[$n]" ] &&\
-  echo '    <p class="item COMMENT">'$(echo "$values[$n]" |sed -r "s:$:<br/>:g" )'</p>'
-done
-echo '  </div>'
-
-echo '  <div class="section attendance">'
-echo '    <h3>'$(l10n course_attendance)'</h3>'
-list_attendance "$id" |sort -k 2 |sed -r 's:^([^ ]+) (.*)$:<a class="attendance" href="?p=cards#\1">\2</a>:'
-echo '  </div>'
-
-echo '  <div class="control">'
-echo '    <a class="item" href="?action=edit_course&course='${id}'">'$(l10n edit)'</a>'
-echo '    <a class="item" href="?action=generate_courselist&course='${id}'" target="_blank">'$(l10n courselist)'</a>'
-echo '    <a class="item" href="?action=export_ical&course='${id}'">'$(l10n ics_export)'</a>'
-echo '    <a class="item" href="mailto:zack@vuesch.org?bcc='$(course_mail "$id")'">'$(l10n course_mail)'</a>'
-echo '  </div>'
-echo '<input type="hidden" name="UID" value="'$values["UID"]'"/>'
-echo '</form>'
-echo '</div>'
-
diff --git a/templates/view_prescription.sh b/templates/view_prescription.sh
deleted file mode 100755 (executable)
index ced7241..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-# Copyright 2016 Paul Hänsch
-#
-# This file is part of Confetti.
-# 
-# Confetti is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-# 
-# Confetti is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-# 
-# You should have received a copy of the GNU Affero General Public License
-# along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
-
-check(){ [ "$1" = "$2" ] && printf checked}
-
-cat <<END_HTML
-<div class="prescription ${mpx[presctype]%_*} ${mpx[presctype]#*_}">
-<fieldset class="baseinfo">
-<label for="insurance">$(l10n insurance)</label>
-<span id="insurance">${mpx[insurance]}</span>
-<br>
-<label for="name">$(l10n name)</label>
-<label for="bday">$(l10n bday)</label>
-<span id="name">${mpx[name]}</span>
-<span id="bday">${mpx[bday]}</span>
-
-<br>
-<label for="date">$(l10n date)</label>
-<span id="date">${mpx[date]}</span>
-</fieldset>
-
-<fieldset class="misc">
-<h1 id="${mpx[prescription]}">$(l10n therapy_prescription)</h1>
-<br>
-<label for="addcontrib">$(l10n addcontrib)</label>
-<span id="addcontrib">${mpx[addcontrib]}</span>
-<label for="contribconfirm">$(l10n contribconfirm)</label>
-<span id="contribconfirm">${mpx[contribconfirm]}</span>
-<label class="checkbox ${mpx[contribreceipt]:+checked}" for="contribreceipt">$(l10n contribreceipt)</label>
-</fieldset>
-
-<label class="checkbox ${mpx[prescreviewed]:+checked}" for="prescreviewed">
-  $([ -n "${mpx[prescreviewed]}" ] && printf %s "$(l10n prescreviewed)" \
-                                   || printf %s "$(l10n prescreview)" )
-</label>
-
-<fieldset class="catalogue">
-<h2>$(l10n prescription_by_catalogue)</h2>
-<label class="radio $(check "$mpx[prescno]" first)" for="prescfirst">$(l10n prescfirst)</label>
-<br>
-<label class="radio $(check "$mpx[prescno]" follow1)" for="prescfollow1">$(l10n prescfollow1)</label>
-<br>
-<label class="radio $(check "$mpx[prescno]" follow2)" for="prescfollow2">$(l10n prescfollow2)</label>
-<br>
-<label class="radio $(check "$mpx[prescno]" other)" for="prescother">$(l10n prescother)</label>
-<br>
-<label class="radio $(check "$mpx[prescno]" continual)" for="presccontinual">$(l10n presccontinual)</label>
-
-<br>
-<label class="checkbox ${mpx[grouptherapy]:+checked}" for="grouptherapy">$(l10n grouptherapy)</label>
-<br>
-<label class="checkbox ${mpx[housecall]:+checked}" for="housecall">$(l10n housecall)</label>
-<br>
-<label class="checkbox ${mpx[report]:+checked}" for="report">$(l10n report)</label>
-</fieldset>
-
-<fieldset class="description">
-  <label for="quantity">$(l10n quantity)</label>
-  <label for="remidy">$(l10n remidy)</label>
-  <label for="quantity_weekly">$(l10n quantity_weekly)</label>
-  <span id="quantity">${mpx[quantity]}</span>
-  <span id="remidy">${mpx[remidy]}</span>
-  <span id="quantity_weekly">${mpx[quantity_weekly]}</span>
-$( for n in {0..10}; do
-  [ -n "${mpx[quantity$n]}" -o -n "${mpx[remidy$n]}" -o -n "${mpx[quantity_weekly$n]}" ] \
-  && printf '
-    <span id="quantity">%s</span>
-    <span id="remidy">%s</span>
-    <span id="quantity_weekly">%s</span>
-    ' "${mpx[quantity$n]}" "${mpx[remidy$n]}" "${mpx[quantity_weekly$n]}"
-done )
-<br>
-<p class="indicator_codes">
-  <label for="indicator">$(l10n indicator)</label>
-  <span id="indicator">${mpx[indicator]}</span>
-  <br>
-  <label for="icd10">$(l10n icd10)</label>
-  <span id="icd10">${mpx[icd10]}</span>
-</p>
-<br>
-<p class="indicator_reading">
-  <label for="indicator_reading">$(l10n indicator_reading)</label>
-  <span id="indicator_reading">${mpx[indicator_reading]}</span>
-</p>
-</fieldset>
-
-<p class="therapy_dates">
-  <label>$(l10n therapy_dates):</label>
-  $(therapy_dates "$id" \
-    | while read date; do
-      printf '<span>%s</span>' "$date"
-    done
-  )
-</p>
-
-<p class="issuer">
-  <label>$(l10n issuer)</label>
-  <span id="issuer">$mpx[issuer]</span>
-</p>
-
-<fieldset class="controls">
-<a class="button" href="?p=prescriptions&amp;edit=${mpx[prescription]}#${mpx[prescription]}">$(l10n edit)</a>
-<a class="button" href="?p=therapy&amp;id=${mpx[prescription]%.mpx}.tpy">$(l10n therapy)</a>
-</fieldset>
-
-</div>
-END_HTML