From: Paul Hänsch
Date: Mon, 19 Jun 2023 13:32:26 +0000 (+0200)
Subject: Merge commit 'c15fd72ed0c779b872360a2546ff60767772d8ee'
X-Git-Url: https://git.plutz.net/?a=commitdiff_plain;h=d13adbdd1e9e258af215d20405b7de6737755fc2;hp=c15fd72ed0c779b872360a2546ff60767772d8ee;p=shellwiki
Merge commit 'c15fd72ed0c779b872360a2546ff60767772d8ee'
---
diff --git a/Makefile b/Makefile
new file mode 100644
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/acl.sh b/acl.sh
new file mode 100755
index 0000000..1017c6e
--- /dev/null
+++ b/acl.sh
@@ -0,0 +1,125 @@
+#!/bin/sh
+
+[ "$include_acl" ] && return 0
+include_acl="$0"
+
+# ACL_OVERRIDE="${ACL_OVERRIDE:-Admin:read,write}"
+ACL_DEFAULT="${ACL_DEFAULT:-Known:read,write${BR}All:read}"
+
+acl_cachepath=''
+acl_collection=''
+
+acl_collect(){
+ local path="$1"
+ # Get directory part of PATH_INFO
+ local path="${path%/*}/./"
+ local pagefile head acl
+
+ printf '%s\n' "$ACL_OVERRIDE"
+
+ while :; do
+ [ "$path" = / ] && break
+ path="${path%/*/}/"
+
+ # Do not use `mdfile` function here because of specialties
+ # in translation handler (`handlers/10_translations.sh`)
+ if [ -f "$_DATA/pages/$path/#page.md" ]; then
+ pagefile="$_DATA/pages/$path/#page.md"
+ elif [ -f "$_EXEC/pages/$path/#page.md" ]; then
+ pagefile="$_EXEC/pages/$path/#page.md"
+ else
+ continue
+ fi
+
+ acl="$(sed -En '
+ s;\r$;;;
+ /^%acl([\t ]+.*)?$/bACL;
+ 20q;
+ b;
+
+ :ACL
+ s;(%(acl)?)?[\t ]*;;
+ p; n; s;\r$;;;
+ /^(%[ \t]+|%acl[ \t]+|[ \t]+)[^ \t\r]+$/bACL;
+ /^(%[ \t]*|%acl[ \t]*)$/bACL;
+ ' <"$pagefile")"
+
+ printf %s\\n "${acl}"
+ done
+
+ printf '%s\n' "$ACL_DEFAULT"
+}
+
+acl_read(){
+ local page="${1:-${PATH_INFO}}"
+ local acl
+
+ if [ "$acl_cachepath" != "$page" ]; then
+ acl_cachepath="$page"
+ acl_collection="$(acl_collect "$page")"
+ fi
+
+ while read -r acl; do
+ case ${acl##*:} in
+ read|*,read,*|read,*|*,read)
+ acl="${acl%%:*}:read";;
+ *) acl="${acl%%:*}:";;
+ esac
+ [ "$USER_NAME" ] && case $acl in
+ "Known:read") return 0;;
+ "Known:") return 1;;
+ "+Known:read") return 0;;
+ "-Known:read") return 1;;
+ "@${USER_NAME}:read") return 0;;
+ "@${USER_NAME}:") return 1;;
+ "+@{$USER_NAME}:read") return 0;;
+ "-@{$USER_NAME}:read") return 1;;
+ esac
+ case $acl in
+ "All:read") return 0;;
+ "All:") return 1;;
+ "+All:read") return 0;;
+ "-All:read") return 1;;
+ esac
+ done <<-EOF
+ ${acl_collection}
+ EOF
+ return 1
+}
+
+acl_write(){
+ local page="${1:-${PATH_INFO}}"
+ local acl
+
+ if [ "$acl_cachepath" != "$page" ]; then
+ acl_cachepath="$page"
+ acl_collection="$(acl_collect "$page")"
+ fi
+
+ while read -r acl; do
+ case ${acl##*:} in
+ write|*,write,*|write,*|*,write)
+ acl="${acl%%:*}:write";;
+ *) acl="${acl%%:*}:";;
+ esac
+ [ "$USER_NAME" ] && case ${acl} in
+ "Known:write") return 0;;
+ "Known:") return 1;;
+ "+Known:write") return 0;;
+ "-Known:write") return 1;;
+ "@${USER_NAME}:write") return 0;;
+ "@${USER_NAME}:") return 1;;
+ "+@{$USER_NAME}:write") return 0;;
+ "-@{$USER_NAME}:write") return 1;;
+ esac
+ case $acl in
+ "All:write") return 0;;
+ "All:") return 1;;
+ "+All:write") return 0;;
+ "-All:write") return 1;;
+ esac
+ done <<-EOF
+ ${acl_collection}
+ EOF
+ return 1
+}
diff --git a/.gitignore b/cgilite/.gitignore
similarity index 100%
rename from .gitignore
rename to cgilite/.gitignore
diff --git a/cgilite.sh b/cgilite/cgilite.sh
similarity index 100%
rename from cgilite.sh
rename to cgilite/cgilite.sh
diff --git a/common.css b/cgilite/common.css
similarity index 100%
rename from common.css
rename to cgilite/common.css
diff --git a/file.sh b/cgilite/file.sh
similarity index 100%
rename from file.sh
rename to cgilite/file.sh
diff --git a/html-sh.sed b/cgilite/html-sh.sed
similarity index 100%
rename from html-sh.sed
rename to cgilite/html-sh.sed
diff --git a/logging.sh b/cgilite/logging.sh
similarity index 100%
rename from logging.sh
rename to cgilite/logging.sh
diff --git a/markdown.awk b/cgilite/markdown.awk
similarity index 100%
rename from markdown.awk
rename to cgilite/markdown.awk
diff --git a/session.sh b/cgilite/session.sh
similarity index 100%
rename from session.sh
rename to cgilite/session.sh
diff --git a/storage.sh b/cgilite/storage.sh
similarity index 100%
rename from storage.sh
rename to cgilite/storage.sh
diff --git a/users.sh b/cgilite/users.sh
similarity index 100%
rename from users.sh
rename to cgilite/users.sh
diff --git a/handlers/10_css.sh b/handlers/10_css.sh
new file mode 100755
index 0000000..2efc07b
--- /dev/null
+++ b/handlers/10_css.sh
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+css(){
+ local path="${1:-${PATH_INFO}}"
+ local pagefile css=''
+ # Get directory part of PATH_INFO
+ path="${path%/*}/./"
+
+ while :; do
+ [ "$path" = / ] && break
+ path="${path%/*/}/"
+
+ if ! acl_read "$path"; then
+ continue
+ elif [ -f "$_DATA/pages/$path/#page.md" ]; then
+ pagefile="$_DATA/pages/$path/#page.md"
+ elif [ -f "$_EXEC/pages/$path/#page.md" ]; then
+ pagefile="$_EXEC/pages/$path/#page.md"
+ else
+ continue
+ fi
+
+ css="$(sed -En '
+ s;\r$;;;
+ /^%css([\t ]+.*)?$/bCSS;
+ 20q;
+ b;
+
+ :CSS
+ s;(%(css)?)?[\t ]*;;
+ p; n; s;\r$;;;
+ /^(%[ \t]+|%css[ \t]+|[ \t]+)[^ \t\r]+$/bCSS;
+ /^(%[ \t]*|%css[ \t]*)$/bCSS;
+ ' <"$pagefile")${BR}${css}"
+ done
+
+ printf %s\\n "${css}"
+}
+
+PAGE_CSS="$(css "${PATH_INFO}")"
+
+return 1
diff --git a/handlers/10_translations.sh b/handlers/10_translations.sh
new file mode 100755
index 0000000..4b34640
--- /dev/null
+++ b/handlers/10_translations.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+
+# Set LANGUAGE_DEFAULT to enable Plugin
+[ ! "$LANGUAGE_DEFAULT" ] && return 1
+
+export LANGUAGE_DEFAULT="${LANGUAGE_DEFAULT:-en}"
+export HTTP_REFERER="${HTTP_REFERER:-$(HEADER Referer)}"
+export LANGUAGE ERROR_MSG
+
+case ${HTTP_REFERER} in
+ */:*/*):;;
+ */:*)
+ LANGUAGE_REFERRED="${HTTP_REFERER##*/:}"
+ ;;
+esac
+
+LANGUAGE="${LANGUAGE_REFERRED:-${LANGUAGE_DEFAULT}}"
+
+case ${PATH_INFO} in
+ */:?*/\[attachment\]/?*)
+ LANGUAGE="${PATH_INFO#*/:}"
+ LANGUAGE="${LANGUAGE%%/*}"
+ PATH_INFO="${PATH_INFO%%:?*/*}${PATH_INFO#*/:?*/}"
+ ;;
+ */:?*/\[attachment\])
+ LANGUAGE="${PATH_INFO#*/:}"
+ LANGUAGE="${LANGUAGE%%/*}"
+ PATH_INFO="${PATH_INFO%:?*/\[attachment\]}[attachment]"
+ ;;
+ */:?*/\[*\])
+ :;; # Default handler, considered valid in most cases
+ */:?*/?*)
+ :;; # Default attachment handler
+ */:?*/) # Faulty URL build
+ REDIRECT "${_BASE}${PATH_INFO%/}"
+ ;;
+# */:"${LANGUAGE_DEFAULT}")
+# REDIRECT "${_BASE}${PATH_INFO%:*}"
+# ;;
+ */:?*)
+ LANGUAGE="${PATH_INFO##*/:}"
+ PATH_INFO="${PATH_INFO%:*}"
+
+ [ "$LANGUAGE" != "$LANGUAGE_DEFAULT" ] \
+ && case "$(mdfile "${PATH_INFO}")" in
+ *"/:$LANGUAGE/#page.md")
+ :;;
+ '')
+ :;;
+ *)ERROR_MSG="TRANSLATION NOT FOUND"
+ ;;
+ esac
+ ;;
+ /|*/*/) # Keep Language from Referer
+ if [ "$LANGUAGE_REFERRED" -a "$LANGUAGE_REFERRED" != "$LANGUAGE_DEFAULT" ]; then
+ REDIRECT "${_BASE}${PATH_INFO}:${LANGUAGE_REFERRED}"
+ fi
+ ;;
+ */\[*\]) # Keep Language from Referer
+ if [ "$LANGUAGE_REFERRED" -a "$LANGUAGE_REFERRED" != "$LANGUAGE_DEFAULT" ]; then
+ REDIRECT "${_BASE}${PATH_INFO%\[*\]}:${LANGUAGE_REFERRED}/[${PATH_INFO##*/\[}"
+ fi
+ ;;
+esac
+
+[ -r "${_EXEC}/l10n/${LANGUAGE}.sh" ] && . "${_EXEC}/l10n/${LANGUAGE}.sh"
+
+return 1
diff --git a/handlers/20_title.sh b/handlers/20_title.sh
new file mode 100755
index 0000000..a0079d2
--- /dev/null
+++ b/handlers/20_title.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+if acl_read ${PATH_INFO}; then
+ mdfile="$(mdfile "${PATH_INFO%/*}")"
+else
+ PAGE_TITLE="${SITE_TITLE}"
+ return 1
+fi
+
+if [ "$mdfile" ]; then
+ PAGE_TITLE="$(
+ sed -nE '
+ s;^%title[ \t]+([[:graph:]][[:print:]]+)\r?$;\1;p; tQ;
+ b; :Q q;
+ ' "$mdfile"
+ )"
+ [ ! "${PAGE_TITLE}" ] && PAGE_TITLE="$(
+ MD_MACROS="" md <"$mdfile" \
+ | sed -nE '
+ s;^.*]*>(.*>)?([^<]+)(<.*)? .*$;\2;p; tQ;
+ s;^.*]*>(.*>)?([^<]+)(<.*)? .*$;\2;p; tQ;
+ b; :Q q;
+ '
+ )"
+else
+ PAGE_TITLE="${SITE_TITLE}"
+ return 1
+fi
+
+case $PATH_INFO in
+ *"/[attachment]")
+ PAGE_TITLE="${PAGE_TITLE} (Attachments)"
+ ;;
+ *"/[revision]")
+ PAGE_TITLE="${PAGE_TITLE} (Revisions)"
+ ;;
+esac
+
+[ "$PAGE_TITLE" ] \
+&& PAGE_TITLE="${PAGE_TITLE}${SITE_TITLE:+ - ${SITE_TITLE}}" \
+|| PAGE_TITLE="${SITE_TITLE}"
+
+return 1
diff --git a/handlers/30_page.sh b/handlers/30_page.sh
new file mode 100755
index 0000000..5c3da0c
--- /dev/null
+++ b/handlers/30_page.sh
@@ -0,0 +1,70 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/file.sh"
+
+CACHE_AGE=${CACHE_AGE:-300}
+export MD_MACROS="$_EXEC/macros"
+export MD_HTML="${MD_HTML:-false}"
+
+wiki() {
+ # Print content of a wiki page
+ # Get page from data or underlay dir, handle caching
+ local page="$(PATH "$1")" mdfile cache cachetime
+
+ cache="$_DATA/pages/$page/#page:${LANGUAGE}.${USER_ID}.cache"
+
+ mdfile="$(mdfile "$page")" || return 4
+ acl_read "$page" || return 3
+
+ cachetime="$(stat -c %Y -- "$mdfile" "$cache" 2>/dev/null)"
+
+ if [ "${cachetime#*${BR}}" -gt "${cachetime%${BR}*}" \
+ -a "${cachetime#*${BR}}" -gt "$((_DATE - CACHE_AGE))" ]; then
+ cat "${cache}"
+ else
+ mkdir -p -- "$_DATA/pages/$page/"
+ # Macros expect to find page directory as working dir
+ ( cd -- "$_DATA/pages/$page/";
+ md <"$mdfile" \
+ | tee -- "${cache}.$$"
+ )
+ grep -q '^%nocache' "$mdfile" \
+ && rm -- "${cache}.$$" \
+ || mv -- "${cache}.$$" "${cache}"
+ fi
+}
+
+case "${PATH_INFO}" in
+ /"[.]"/*)
+ # usually some file related to theme
+ # let file server handle errors
+ FILE "${_EXEC}/${PATH_INFO#/\[.\]}"
+ return 0
+ ;;
+ *${BR}*)
+ export ERROR_MSG='Page names containing newline character are not allowed'
+ theme_error 400
+ return 0
+ ;;
+ */\#*)
+ export ERROR_MSG='Page names starting with "#" are not allowed'
+ theme_error 400
+ return 0
+ ;;
+ */\[*\]/*|*/\[*\])
+ # looks like some kind of handler
+ return 1
+ ;;
+ */)
+ if ! mdfile "$PATH_INFO" >&-; then
+ theme_error 404
+ elif ! acl_read "$PATH_INFO"; then
+ theme_error 403
+ else
+ theme_page "${PATH_INFO}"
+ fi
+ return 0
+ ;;
+esac
+
+return 1
diff --git a/handlers/40_account.sh b/handlers/40_account.sh
new file mode 100755
index 0000000..c4982fd
--- /dev/null
+++ b/handlers/40_account.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+case "${PATH_INFO}" in
+ */"[login]")
+ acl_read "/wiki/login/" \
+ && theme_page "/[wiki]/login/" \
+ || theme_error 403
+ return 0
+ ;;
+ */"[register]")
+ acl_read "/wiki/register/" \
+ && theme_page "/[wiki]/register/" \
+ || theme_error 403
+ return 0
+ ;;
+ */"[invite]")
+ acl_read "/wiki/invite/" \
+ && theme_page "/[wiki]/invite/" \
+ || theme_error 403
+ return 0
+ ;;
+ */"[settings]")
+ acl_read "/wiki/settings/" \
+ && theme_page "/[wiki]/settings/" \
+ || theme_error 403
+ return 0
+ ;;
+esac
+
+return 1
diff --git a/handlers/40_attachment.sh b/handlers/40_attachment.sh
new file mode 100755
index 0000000..1e07190
--- /dev/null
+++ b/handlers/40_attachment.sh
@@ -0,0 +1,135 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/file.sh"
+
+# REV_ATTACHMENTS="${REV_ATTACHMENTS:-false}"
+
+attachment_convert(){
+ local attpath="$1"
+ local cachepath="${attpath%/#attachments/*}/#cache/${attpath#*/#attachments/}"
+ local res junk
+
+ case $attpath in
+ *.webm|*.mp4|*.mkv|*.avi)
+ cachepath="${cachepath}.webm"
+ ;;
+ esac
+
+ if [ -s "$cachepath" ]; then
+ printf %s "$cachepath"
+ return 0
+ elif [ -f "$cachepath" ]; then
+ printf %s "$attpath"
+ return 0
+ elif ! mkdir -p -- "${cachepath%/*}" && touch "$cachepath"; then
+ printf %s "$attpath"
+ return 0
+ fi
+
+ case $attpath in
+ *.jpg|*.jpeg|*.png)
+ read junk junk res junk <<-EOF
+ $(identify "$attpath")
+ EOF
+ if [ "${res%x*}" -gt 2048 ]; then
+ convert "$attpath" -resize 1920x-2 -quality 85 "$cachepath"
+ else
+ convert "$attpath" -quality 85 "$cachepath"
+ fi
+ printf %s "$cachepath"
+ return 0
+ ;;
+ *.webm|*.mp4|*.mkv|*.avi)
+ res=$(ffprobe -show_entries stream=width "$attpath" 2>&-)
+ res="${res#*width=}" res="${res%%${BR}*}"
+ if [ "$res" -gt 1280 ]; then
+ ( exec >&- 2>&1;
+ ffmpeg -y -nostdin -i "$attpath" \
+ -c:v libvpx -vf scale=1280:-2 -crf 28 -b:v 0 \
+ -c:a libvorbis -q:a 6 \
+ "${cachepath%.*}.tmp.webm" \
+ && mv -- "${cachepatch%.*}.tmp.webm" "${cachepath}" \
+ & ) &
+
+ else
+ ( exec >&- 2>&1;
+ ffmpeg -y -nostdin -i "$attpath" \
+ -c:v libvpx -crf 28 -b:v 0 \
+ -c:a libvorbis -q:a 6 \
+ "${cachepath%.*}.tmp.webm" \
+ && mv -- "${cachepatch%.*}.tmp.webm" "${cachepath}" \
+ & ) &
+ fi
+ printf %s "$attpath"
+ return 0
+ ;;
+ *) printf "$attpath";;
+ esac
+}
+
+case ${PATH_INFO} in
+ */\[attachment\]/)
+ # no trailing slash
+ REDIRECT "${_BASE}${PATH_INFO%/}"
+ ;;
+ */*/)
+ # attached files never end on /
+ return 1
+ ;;
+ */\[attachment\])
+ # show attachment page
+ page="${PATH_INFO%\[attachment\]}"
+
+ if [ ! -d "$_DATA/pages${page}" -a ! -d "$_DATA/pages${page}" ]; then
+ # base page does not exist
+ return 1
+ elif [ "${CONTENT_TYPE%%;*}" = "multipart/form-data" ]; then
+ # pass uploads to next handler
+ return 1
+ elif [ "$(POST action)" ]; then
+ # pass edits to next handler
+ return 1
+ elif ! acl_read "${page}"; then
+ theme_error 403
+ return 0
+ else
+ theme_attachments "${page}"
+ return 0
+ fi
+ ;;
+
+ */\[attachment\]/*)
+ attpath="${PATH_INFO%/\[attachment\]/*}/#attachments/${PATH_INFO##*/}"
+
+ if [ ! -f "$_DATA/pages/$attpath" -a ! -f "$_EXEC/pages/$attpath" ]; then
+ return 1
+ elif ! acl_read "${PATH_INFO%/\[attachment\]/*}"; then
+ theme_error 403
+ return 0
+ elif [ -f "$_DATA/pages/$attpath" ]; then
+ FILE "$_DATA/pages/$attpath"
+ return 0
+ elif [ -f "$_EXEC/pages/$attpath" ]; then
+ FILE "$_EXEC/pages/$attpath"
+ return 0
+ fi
+ ;;
+ */*)
+ attpath="${PATH_INFO%/*}/#attachments/${PATH_INFO##*/}"
+
+ if [ ! -f "$_DATA/pages/$attpath" -a ! -f "$_EXEC/pages/$attpath" ]; then
+ return 1
+ elif ! acl_read "${PATH_INFO%/*}/"; then
+ theme_error 403
+ return 0
+ elif [ -f "$_DATA/pages/$attpath" ]; then
+ FILE "$(attachment_convert "$_DATA/pages/$attpath")"
+ return 0
+ elif [ -f "$_EXEC/pages/$attpath" ]; then
+ FILE "$(attachment_convert "$_EXEC/pages/$attpath")"
+ return 0
+ fi
+ ;;
+esac
+
+return 1
diff --git a/handlers/40_edit_attachment.sh b/handlers/40_edit_attachment.sh
new file mode 100755
index 0000000..97a4b9d
--- /dev/null
+++ b/handlers/40_edit_attachment.sh
@@ -0,0 +1,220 @@
+#!/bin/sh
+
+REV_ATTACHMENTS="${REV_ATTACHMENTS:-false}"
+
+if [ "${PATH_INFO##*/\[attachment\]}" ]; then
+ # Skip any action not happening on attachment page
+ return 1
+fi
+
+page="${PATH_INFO%\[attachment\]}"
+action="$(POST action)"
+
+tsid="$(POST session_key)"; tsid="${tsid%% *}"
+
+
+if ! acl_write "${PATH_INFO%\[attachment\]}"; then
+ # Deny access to write protected pages
+ printf 'Refresh: %i\r\n' 4
+ theme_error 403
+ [ "${CONTENT_TYPE%%;*}" = "multipart/form-data" ] \
+ && head -c $((CONTENT_LENGTH)) >/dev/null
+ return 0
+
+elif [ "${CONTENT_TYPE%%;*}" = "multipart/form-data" ]; then
+ . "$_EXEC/multipart.sh"
+ multipart_cache
+
+ # Use positional parameters for filename collection
+ # The positional array is the only array available
+ # in plain posix shells, see the documentation for
+ # your shells "set" builtin for a hint to this
+ # obscure use mode
+ set --
+
+ # Validate session id from form to prevent CSRF
+ # Only validate if username is present, because no username means
+ # anonymous uploads are allowed via acl and cgilite/session.sh does not
+ # validate anonymous sessions from a multipart/formdata
+ if [ "$USER_NAME" -a "$(multipart session_id)" != "$SESSION_ID" ]; then
+ rm -- "$multipart_cachefile"
+ printf 'Refresh: %i\r\n' 4
+ theme_error 403
+ return 0
+ fi
+
+ mkdir -p "$_DATA/pages${page}#attachments/"
+ n=1; while filename=$(multipart_filename "file" "$n"); do
+ filename="$(printf %s "$filename" |tr /\\0 __)"
+ set -- "$@" "pages${page}#attachments/$filename"
+ multipart "file" "$n" >"$_DATA/pages${page}#attachments/$filename"
+ n=$((n + 1))
+ done
+ rm -- "$multipart_cachefile"
+ if [ "$REV_ATTACHMENTS" = true ]; then
+ git -C "$_DATA" add -- "$@"
+ git -C "$_DATA" commit -qm "Attachments to # $page # uploaded by @ $USER_NAME @" -- "$@"
+ fi
+ REDIRECT "${_BASE}${PATH_INFO}"
+
+elif [ "$SESSION_ID" != "$tsid" ]; then
+ # Match session key from POST-Data to prevent CSRF:
+ # For authenticated users the POST session_key must match
+ # the session key used for authentication (usually from a
+ # cookie). This should ensure that POST requests were not
+ # triggered by malicious 3rd party sites freeriding on an
+ # existing user authentication.
+ # For pages that are writable by anonymous users, this is
+ # not reliable.
+
+ printf 'Refresh: %i\r\n' 4
+ theme_error 403
+ return 0
+fi
+
+if [ "$action" = delete -o "$action" = move ]; then
+ set --
+ n="$(POST_COUNT select)"; while [ $n -gt 0 ]; do
+ select="$(POST select $n |PATH)"
+ set -- "$@" "pages${page}#attachments/${select##*/}"
+ n=$((n - 1))
+ done
+fi
+
+if [ "$action" = delete ]; then
+ if [ "$REV_ATTACHMENTS" = true ]; then
+ git -C "$_DATA" rm -- "$@"
+ git -C "$_DATA" commit -qm \
+ "Attachment to # $page # deleted by @ $USER_NAME @" -- "$@"
+ else
+ ( cd "$_DATA" && rm -- "$@"; )
+ fi
+ REDIRECT "${_BASE}${PATH_INFO}"
+
+elif [ "$action" = move ]; then
+ moveto="$(POST moveto |PATH)"
+
+ if ! acl_write "$moveto"; then
+ printf 'Refresh: %i\r\n' 4
+ theme_error 403
+ return 0
+
+ elif [ ! -d "${_DATA}/pages${moveto}" ]; then
+ printf 'Refresh: %i\r\n' 4
+ theme_error 404
+ return 0
+
+ elif [ "$REV_ATTACHMENTS" = true ]; then
+ mkdir -p -- "${_DATA}/pages${moveto}/#attachments"
+ git -C "$_DATA" mv -f -- "$@" "pages${moveto}/#attachments/"
+
+ cnt=$#; while [ $cnt -gt 0 ]; do
+ set -- "$@" "$1" "pages/${moveto}/#attachments/${1##*/}"
+ cnt=$((cnt - 1)); shift 1
+ done
+
+ git -C "$_DATA" commit -qm \
+ "Attachment moved from # $page # to # $moveto # by @ $USER_NAME @" -- "$@"
+ else
+ mkdir -p -- "${_DATA}/pages${moveto}/#attachments"
+ ( cd "$_DATA" && mv -- "$@" "pages${moveto}/#attachments/"; )
+ fi
+ REDIRECT "${_BASE}${PATH_INFO}"
+
+elif [ "$action" = rename ]; then
+ fail='' success=''
+ set --
+
+ for file in "${_DATA}/pages${page}#attachments"/*; do
+ rename="$(POST rename_"$(slopecode "${file##*/}" |sed 's;=;%3D;g')")"
+
+ if [ "$REV_ATTACHMENTS" = true -a \
+ -f "${file}" -a \
+ "$rename" -a \
+ "${rename%/*}" = "${rename}" -a \
+ ! -e "${_DATA}/pages${page}#attachments/${rename}" ] \
+ && git -C "$_DATA" mv -- "pages${page}#attachments/${file##*/}" "pages${page}#attachments/${rename}"; then
+ success="${success}$(HTML "${file##*/}/${rename}")${BR}"
+ set -- "$@" "pages${page}#attachments/${file##*/}" "pages${page}#attachments/${rename}"
+
+ elif [ "$REV_ATTACHMENTS" = true -a "${rename}" ]; then
+ fail="${fail}$(HTML "${file##*/}/${rename}")${BR}"
+
+ elif [ -f "${file}" -a \
+ "$rename" -a \
+ "${rename%/*}" = "${rename}" -a \
+ ! -e "${_DATA}/pages${page}#attachments/${rename}" ] \
+ && mv -- "${file}" "${_DATA}/pages${page}#attachments/${rename}"; then
+ success="${success}$(HTML "${file##*/}/${rename}")${BR}"
+
+ elif [ "${rename}" ]; then
+ fail="${fail}$(HTML "${file##*/}/${rename}")${BR}"
+
+ fi
+ done
+
+ if [ "$REV_ATTACHMENTS" = true -a $# -gt 2 ]; then
+ git -C "$_DATA" commit -qm \
+ "Attachment files renamed by @ $USER_NAME @" -- "$@"
+ elif [ "$REV_ATTACHMENTS" = true -a $# -eq 2 ]; then
+ git -C "$_DATA" commit -qm \
+ "Attachment file renamed by @ $USER_NAME @" -- "$@"
+ fi
+
+ if [ "$success" -a "$fail" ]; then
+ printf "%s\r\n" "Status: 500 Internal Server Error"
+ theme_page - "Attachment rename" <<-EOF
+ Some files could not be renamed
+ Successfully renamed:
+
+ $(printf %s "$success" |while read html; do
+ printf '%s -> %s ' \
+ "${html%%/*}" "${html##*/}"
+ done)
+
+ Errors:
+
+ $(printf %s "$fail" |while read html; do
+ printf '%s -> %s ' \
+ "${html%%/*}" "${html##*/}"
+ done)
+
+ OK
+ EOF
+ exit 0
+
+ elif [ "$fail" ]; then
+ printf "%s\r\n" "Status: 500 Internal Server Error"
+ theme_page - "Attachment rename" <<-EOF
+ Files could not be renamed
+
+ $(printf %s "$fail" |while read html; do
+ printf '%s -> %s ' \
+ "${html%%/*}" "${html##*/}"
+ done)
+
+ OK
+ EOF
+ exit 0
+
+ elif [ "$success" ]; then
+ printf 'Refresh: %i\r\n' 4
+ theme_page - "Attachment rename" <<-EOF
+ Files were renamed
+
+ $(printf %s "$success" |while read html; do
+ printf '%s -> %s ' \
+ "${html%%/*}" "${html##*/}"
+ done)
+
+ OK
+ EOF
+ exit 0
+
+ else
+ REDIRECT "${_BASE}${PATH_INFO}"
+
+ fi
+fi
+
+return 1
diff --git a/handlers/40_revision.sh b/handlers/40_revision.sh
new file mode 100755
index 0000000..08350c7
--- /dev/null
+++ b/handlers/40_revision.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+case "${PATH_INFO}" in
+ */\[revision\]/)
+ REDIRECT "${_BASE}${PATH_INFO%/}"
+ ;;
+ */\[revision\])
+ if ! acl_read "${PATH_INFO%\[revision\]}"; then
+ theme_error 403
+ else
+ theme_revisions "${PATH_INFO%\[revision\]}"
+ fi
+ return 0
+ ;;
+ */\[revision\]/\[*\]|*/\[revision\]/*/*)
+ REDIRECT "${_BASE}${PATH_INFO%%\[revision\]/*}${PATH_INFO##*/\[revision\]/}"
+ ;;
+ */\[revision\]/*)
+ page="${PATH_INFO%\[revision\]/*}"
+ rev="${PATH_INFO##*/}"
+ if ! acl_read "${page}"; then
+ theme_error 403
+ else
+ ( export PATH_INFO="${page}"
+ cd "${_DATA}/pages${page}" || cd "${_DATA}/pages/"
+ git -C "${_DATA}" show "${rev}:pages${PATH_INFO}#page.md" \
+ | { printf ''; md; printf ' '; } \
+ | theme_page - "${page##*/}"
+ )
+ fi
+ return 0
+ ;;
+esac
+
+return 1
diff --git a/handlers/60_edit.sh b/handlers/60_edit.sh
new file mode 100755
index 0000000..9d1a404
--- /dev/null
+++ b/handlers/60_edit.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+
+. "${_EXEC}/session_lock.sh"
+
+wiki_text() {
+ # Print source text of a wiki page
+ # Get page from data or underlay dir
+ local page="$(PATH "$1")"
+
+ if [ -f "$_DATA/pages/$page/#page.md" ]; then
+ cat -- "$_DATA/pages/$page/#page.md"
+ elif [ -f "$_EXEC/pages/$page/#page.md" ]; then
+ cat -- "$_EXEC/pages/$page/#page.md"
+ else
+ return 1
+ fi
+}
+
+case $PATH_INFO in
+ */\[edit\]) : ;;
+ *) return 1 ;;
+esac
+
+edit_page="${PATH_INFO%\[edit\]}"
+edit_file="$_DATA/pages/$edit_page/#page.md"
+[ "$REQUEST_METHOD" = POST ] && edit_action="$(POST action)"
+
+if ! acl_write "$edit_page"; then
+ theme_error 403
+ return 0
+
+elif [ "$edit_action" = cancel ]; then
+ S_RELEASE "$edit_file"
+ REDIRECT "${_BASE}${PATH_INFO%\[edit\]}"
+
+elif [ "$edit_action" = update ]; then
+ if mkdir -p -- "${edit_file%/#page.md}" \
+ && S_LOCK "$edit_file"; then
+ POST pagetext >"$edit_file"
+ S_RELEASE "$edit_file"
+ else
+ export ERRMSG="ERR_NOLOCK"
+ REDIRECT "${_BASE}${PATH_INFO%\[edit\]}/[edit]"
+ fi
+
+ if [ "$REV_PAGES" = true ]; then
+ git -C "$_DATA" add \
+ -- "pages/$edit_page/#page.md"
+ git -C "$_DATA" commit -qm \
+ "Page # ${edit_page} # updated by user @ ${USER_NAME} @" \
+ -- "pages/$edit_page/#page.md"
+ fi 1>&2
+
+ REDIRECT "${_BASE}${edit_page}"
+
+elif mkdir -p -- "${edit_file%/#page.md}" \
+ && S_LOCK "$edit_file"; then
+ theme_editor "$edit_page"
+ return 0
+
+else
+ printf 'Refresh: %i; url=%s\r\n' 4 ../
+ export ERROR_MSG="Unable to lock page for editing"
+ theme_error 409
+ return 0
+
+fi
+
+return 1
diff --git a/handlers/60_move_rename_delete.sh b/handlers/60_move_rename_delete.sh
new file mode 100755
index 0000000..bbe994e
--- /dev/null
+++ b/handlers/60_move_rename_delete.sh
@@ -0,0 +1,172 @@
+#!/bin/sh
+
+case "${PATH_INFO}" in
+ */\[move\]|*/\[rename\]|*/\[delete\])
+ page="${PATH_INFO%\[*\]}"
+ if [ ! -d "$_DATA/pages/${page}" -a ! -d "$_EXEC/pages/${page}" ]; then
+ theme_error 404
+ return 0
+ elif ! acl_write "$page"; then
+ printf 'Refresh: %i, url=%s\r\n' 4 ./
+ theme_error 403
+ return 0
+ elif [ -d "$_EXEC/pages/${page}/" ]; then
+ theme_page - <<-EOF
+
+
+
Immutable Page
+ This is a core page of the wiki system. Its name and position cannot be changed.
+ You may however update this page and you can use ACLs to hide it from various listings.
+
+
+ EOF
+ return 0
+ fi
+ ;;
+ *) return 1;;
+esac
+
+if [ "$REQUEST_METHOD" = POST ]; then
+ action="$(POST action)"
+ newname="$(POST newname |grep -m1 -xE '[^#/]*')"
+ newlocation="$(POST newlocation |grep -m1 -xE '/[^#]*')"
+else case "${PATH_INFO}" in
+ */\[move\])
+ location="${page%/}" location="${location%/*}/"
+ theme_page - <<-EOF
+
+ EOF
+ return 0
+ ;;
+ */\[rename\])
+ name="${page%/}" name="${name##*/}"
+ theme_page - <<-EOF
+
+ EOF
+ return 0
+ ;;
+ */\[delete\])
+ theme_page - <<-EOF
+
+ EOF
+ return 0
+ ;;
+ esac
+fi
+
+if [ "$action" = rename -a "$newname" ]; then
+ oldname="${PATH_INFO%\[*\]}"
+ newname="${oldname%/*/}$(PATH "${newname}/")"
+
+ if [ -d "$_DATA/pages/$newname" ]; then
+ printf 'Refresh: %i\r\n' 4
+ export ERRORMSG="A location of that name already exists."
+ theme_error 400
+ return 0
+ elif ! acl_write "$oldname" || ! acl_write "$newname"; then
+ printf 'Refresh: %i\r\n' 4
+ theme_error 403
+ return 0
+ elif [ "$REV_PAGES" = true ]; then
+ git -C "$_DATA" mv "pages/$oldname" "pages/$newname"
+ git -C "$_DATA" commit -m 'Page # '"$oldname"' # renamed to # '"$newname"' # by user @ '"$USER_NAME"' @' \
+ -- "pages/$oldname" "pages/$newname"
+ REDIRECT "$_BASE${newname}"
+ else
+ mv -- "$_DATA/pages/$oldname" "$_DATA/pages/$newname"
+ REDIRECT "$_BASE${newname}"
+ fi
+elif [ "$action" = move -a "$newlocation" ]; then
+ oldname="${PATH_INFO%\[*\]}"
+ newlocation="$(PATH "$newlocation")"
+ newname="${oldname%/}"
+ newname="${newlocation%/}/${newname##*/}/"
+
+ if [ -d "$_DATA/pages/$newname" ]; then
+ printf 'Refresh: %i\r\n' 4
+ export ERRORMSG="A page of that name already exists at the given location."
+ theme_error 400
+ return 0
+ elif [ ! -d "$_DATA/pages/$newlocation" ]; then
+ printf 'Refresh: %i\r\n' 4
+ export ERRORMSG="The given location does not exist."
+ theme_error 400
+ return 0
+ elif ! acl_write "$oldname" || ! acl_write "$newname"; then
+ printf 'Refresh: %i\r\n' 4
+ theme_error 403
+ return 0
+ elif [ "$REV_PAGES" = true ]; then
+ git -C "$_DATA" mv "pages/${oldname}" "pages/${newname}"
+ git -C "$_DATA" commit -m 'Page # '"$oldname"' # moved to # '"$newname"' # by user @ '"$USER_NAME"' @' \
+ -- "pages/${oldname}" "pages/${newname}"
+ REDIRECT "$_BASE${newname}"
+ else
+ mv -- "$_DATA/pages/$oldname" "$_DATA/pages/$newname"
+ REDIRECT "$_BASE${newname}"
+ fi
+elif [ "$action" = delete ]; then
+ oldname="${PATH_INFO%\[*\]}"
+ if ! acl_write "$oldname"; then
+ printf 'Refresh: %i\r\n' 4
+ theme_error 403
+ return 0
+ elif [ "$REV_PAGES" = true ]; then
+ git -C "$_DATA" rm "pages/${oldname}/#page.md"
+ git -C "$_DATA" commit -m 'Page # '"$oldname"' # deleted by user @ '"$USER_NAME"' @' \
+ -- "pages/${oldname}/#page.md"
+ rm -r -- "$_DATA/pages/${oldname}"/\#*
+ rmdir -- "$_DATA/pages/${oldname}/" || true
+ REDIRECT ./
+ else
+ rm -- "$_DATA/pages/${oldname}/#page.md"
+ rm -r -- "$_DATA/pages/${oldname}"/\#*
+ rmdir -- "$_DATA/pages/${oldname}/" || true
+ REDIRECT ./
+ fi
+elif [ "$action" = cancel ]; then
+ REDIRECT ./
+elif [ "$action" ]; then
+ printf 'Refresh: %i\r\n' 4
+ export ERRORMSG="Missing parameters."
+ theme_error 400
+ return 0
+fi
diff --git a/handlers/60_newpage.sh b/handlers/60_newpage.sh
new file mode 100755
index 0000000..f3cbde7
--- /dev/null
+++ b/handlers/60_newpage.sh
@@ -0,0 +1,61 @@
+#!/bin/sh
+
+. "$_EXEC/session_lock.sh"
+
+case $PATH_INFO in
+ */\[newpage\]):;;
+ *) return 1;;
+esac
+
+if [ "$(POST action)" != newpage ]; then
+ printf 'Refresh: %i; url=%s\r\n' 4 ./
+ export ERROR_MSG="Formdata invalid"
+ theme_error 400
+ return 0
+fi
+
+pattern="$(POST pattern)"
+template="$(POST template)"
+page="$(POST page)"
+
+if [ "$page" ]; then
+ pattern="$(date +"$pattern")"
+ page="$(printf -- "$pattern" "$page")"
+else
+ printf 'Refresh: %i; url=%s\r\n' 4 ./
+ export ERROR_MSG="Page name required"
+ theme_error 400
+ return 0
+fi
+
+page="$(page_abs "$page")"
+[ "$template" ] \
+&& template="$(page_abs "$template")" \
+|| template="$page"
+
+if [ -f "$_DATA/pages/$page/#page.md" -o \
+ -f "$_EXEC/pages/$page/#page.md" ]; then
+ printf 'Refresh: %i; url=%s\r\n' 4 ./
+ export ERROR_MSG="Page exists already"
+ theme_error 409
+ return 0
+
+elif ! acl_write "$page"; then
+ printf 'Refresh: %i; url=%s\r\n' 4 ./
+ export ERROR_MSG="You don't have permission to write to this page"
+ theme_error 403
+ return 0
+
+elif mkdir -p -- "$_DATA/pages/${page}" \
+ && S_LOCK "$_DATA/pages/$page/#page.md"; then
+ theme_editor "$page" "$template"
+ return 0
+
+else
+ printf 'Refresh: %i; url=%s\r\n' 4 ./
+ export ERROR_MSG="Unable to lock page for editing"
+ theme_error 409
+ return 0
+fi
+
+return 1
diff --git a/handlers/90_brackets.sh b/handlers/90_brackets.sh
new file mode 100755
index 0000000..ce49b0e
--- /dev/null
+++ b/handlers/90_brackets.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+# special case for odd pages that are not handled otherwise
+# usually those pages look like handlers (i.e. containing brackets)
+# but are not
+# attachment and edit (and really all) handlers should take precedence
+
+case "${PATH_INFO}" in
+ */\[view\])
+ # explicit view handler for linking
+ REDIRECT "${_BASE}${PATH_INFO%\[view\]}"
+ ;;
+ */)
+ if ! mdfile "${PATH_INFO}" >&-; then
+ theme_error 404
+ elif ! acl_read "${PATH_INFO}"; then
+ theme_error 403
+ else
+ theme_page "${PATH_INFO}"
+ fi
+ return 0
+ ;;
+ *)
+ if [ -d "$_DATA/pages${PATH_INFO}/" -o -d "$_EXEC/pages${PATH_INFO}/" ]; then
+ REDIRECT "${_BASE}${PATH_INFO}/"
+ fi
+ ;;
+esac
+
+return 1
diff --git a/index.cgi b/index.cgi
new file mode 100755
index 0000000..61a8bb0
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+. "${_EXEC:-${0%/*}}/cgilite/cgilite.sh"
+. "${_EXEC}/cgilite/session.sh"
+. "${_EXEC}/cgilite/users.sh"
+. "${_EXEC}/tools.sh"
+. "${_EXEC}/acl.sh"
+
+REV_PAGES=${REV_PAGES:-true}
+REV_ATTACHMENTS=${REV_ATTACHMENTS:-false}
+WIKI_THEME="${WIKI_THEME:-default}"
+
+which git >/dev/null || REV_PAGES=false
+[ "$REV_PAGES" != true ] && REV_ATTACHMENTS=false
+
+. "${_EXEC}/themes/${WIKI_THEME}.sh"
+
+# Renew session cookie, only if cookie already set
+[ "$(COOKIE session)" ] && SESSION_COOKIE
+
+wiki_text() {
+ # Print source text of a wiki page
+ # Get page from data or underlay dir
+ local page="$(PATH "$1")" mdfile
+
+ mdfile="$(mdfile "$page")" || return 4
+ acl_read "$page" || return 3
+ cat -- "$mdfile"
+}
+
+if [ "$REV_PAGES" = true -a ! -f "$_DATA/.gitignore" ]; then
+ cat >"$_DATA/.gitignore" <<-EOF
+ users.db
+ serverkey
+ **/#cache/
+ **/#page.lock
+ **/#page.*.cache
+ **/#page.*.cache.*
+ **/#page:*.*.cache
+ **/#page:*.*.cache.*
+ EOF
+ [ "$REV_ATTACHMENTS" != true ] \
+ && printf '**/#attachments/\n' >>"$_DATA/.gitignore"
+ git init "$_DATA"
+ git -C "$_DATA" add .gitignore
+ git -C "$_DATA" commit -m 'initialization' -- .gitignore
+fi 1>&2
+
+for handler in "$_EXEC"/handlers/*; do
+ . "$handler" && break
+done
+if [ $? != 0 ]; then
+ export ERROR_MSG="The presented URL schema cannot be handled"
+ theme_error 400
+fi
diff --git a/l10n/de.sh b/l10n/de.sh
new file mode 100644
index 0000000..d2783ac
--- /dev/null
+++ b/l10n/de.sh
@@ -0,0 +1,152 @@
+#!/bin/sh
+
+user_register_email() { # TRANSLATION
+ "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
+ From: ${MAILFROM}
+ To: ${email}
+ Subject: Ihre Benutzeranmeldung für ${HTTP_HOST%:*}
+
+ Jemand hat versucht ein Benutzerkonto mit dieser Email-Adresse zu erstellen.
+
+ Sie können Ihr Benutzerkonto aktivieren, indem Sie auf diesen Link klicken:
+
+ ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+
+ Der Registrierungslink wird nach $((USER_CONFIRMEXPIRE / 3600)) Stunden ungültig.
+
+ Falls Sie kein Konto bei ${HTTP_HOST%:*} beantragt haben, hat wahrscheinlich
+ jemand anderes versehentlich Ihre Emain-Adresse dort eingegeben. In diesem Fall
+ ignorieren Sie bitte diese Email und wir löschen Ihre Email-Adresse in den
+ nächsten Tagen aus unserer Datenbank.
+
+ Dies ist eine automatische Email. Eine direkte Antwort wird nicht empfangen.
+ --
+ Automat zur Kontenregistrierung.
+ EOF
+}
+user_invite_email(){ # TRANSLATION
+ "$SENDMAIL" -t -f "$MAILFROM" <<-EOF
+ From: ${MAILFROM}
+ To: ${email}
+ Subject: Sie wurden zu ${HTTP_HOST%:*} eingeladen
+
+ ${USER_NAME:-Jemand} hat eine Einladung an diese Email-Adresse ausgesprochen.
+
+ ${message}
+
+ Sie können Ihr Benutzerkonto aktivieren, indem Sie auf diesen Link klicken:
+
+ ${SCHEMA}://${HTTP_HOST}${_BASE}${PATH_INFO}?user_confirm=${uid}+$(session_mac "$uid")
+
+ Der Registrierungslink wird nach $((USER_CONFIRMEXPIRE / 3600)) Stunden ungültig.
+
+ Falls Sie nicht wissen worum es hier geht, hat wahrscheinlich jemand anderes
+ versehentlich Ihre Emain-Adresse dort eingegeben. In diesem Fall ignorieren
+ Sie bitte diese Email und wir löschen Ihre Email-Adresse in den nächsten
+ Tagen aus unserer Datenbank.
+
+ Dies ist eine automatische Email. Eine direkte Antwort wird nicht empfangen.
+ --
+ Automat zur Kontenregistrierung.
+ EOF
+}
+w_user_register_disabled(){ # TRANSLATION
+ cat <<-EOF
+ [div #user_register .disabled
+ Die Registrierung von Benutzerkonten ist deaktiviert.
+ ]
+ EOF
+}
+w_user_register_sendmail(){ # TRANSLATION
+ cat <<-EOF
+ [form #user_register .registeremail method=POST
+ [p Wir schicken eine Aktivierungsmail an Ihre Email-Adresse.
+ Sie können mit der Registierung fortfahren, sobald Sie den
+ Aktivierungslink in dieser Email anklicken.]
+ [input type=email name=email placeholder="Email Adresse"]
+ [submit "action" "user_register" Registrieren]
+ ]
+ EOF
+}
+w_user_register_direct(){ # TRANSLATION
+ cat <<-EOF
+ [form #user_register .registername method=POST
+ [input name=uname placeholder="Benutzername wählen" tooltip="Ihr Benutzername darf jedes Zeichen, auÃer dem @-Zeichen enthalten. Er muss mindestens drei Zeichen lang sein und mit einem Buchstaben anfangen." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
+ [input type=password name=pw placeholder="Passwort wählen" pattern=".{6,}"]
+ [input type=password name=pwconfirm placeholder="Passwort bestätigen" pattern=".{6,}"]
+ [submit "action" "user_register" Registrieren]
+ ]
+ EOF
+}
+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 Adresse"]' "$(UNSTRING "$EMAIL" |HTML)"
+ )
+ [input name=uname placeholder="Benutzername wählen" tooltip="Ihr Benutzername darf jedes Zeichen, auÃer dem @-Zeichen enthalten. Er muss mindestens drei Zeichen lang sein und mit einem Buchstaben anfangen." pattern="^\[\\\\p{L}\]\[\\\\p{L}0-9 -~\]{2,127}$" autocomplete=off]
+ [input type=password name=pw placeholder="Passwort wählen" pattern=".{6,}"]
+ [input type=password name=pwconfirm placeholder="Passwort bestätigen" pattern=".{6,}"]
+ [submit "action" "user_confirm" Registrierung AbschlieÃen]
+ ]
+ EOF
+}
+w_user_confirm_expired(){ # TRANSLATION
+ cat <<-EOF
+ [div #user_confirm .expired
+ [p Diser Aktivierungslink ist nicht mehr gültig.]
+ ]
+ EOF
+}
+w_user_confirm_invalid(){ # TRANSLATION
+ cat <<-EOF
+ [div #user_confirm .invalid
+ [p Dieser Aktivierungslink ist ungültig. Stellen Sie sicher, dass Sie den gesamten Aktivierungslink aus Ihrer Email kopiert haben und achten Sie darauf, keine Zeilenumbrüche mit zu kopieren.]
+ ]
+ EOF
+}
+w_user_invite_email(){ # TRANSLATION
+ cat <<-EOF
+ [form #user_invite method=POST
+ [input placeholder="Email-Empfänger" name=email autocomplete=off]
+ [textarea name="message" placeholder="Nachricht an Empfänger" . ]
+ [submit "action" "user_invite" Einladung Senden]
+ ]
+ EOF
+}
+w_user_invite_link(){ # TRANSLATION
+ cat <<-EOF
+ [div #user_invite .link
+ [p Ein anonymes Benutzerkonto wurde angelegt. Schicken Sie den folgenden Link an den vorgesehene Person, so dass sie ihr Benutzerkonto annehmen kann. Der Link ist für $((USER_CONFIRMEXPIRE / 3600)) Stunden gültig.]
+ [a href="$(HTML "$invlink")" . $(HTML "$invlink")]
+
+ [p [a href="#" . Ein weiteres Konto anlegen]]
+ ]
+ EOF
+}
+w_user_invite_deny(){ # TRANSLATION
+ cat <<-EOF
+ [div #user_invite .notallowed
+ Nur angemeldete Benutzer können einen Einladungslink an Andere versenden.
+ ]
+ EOF
+}
+w_user_login_logon(){ # TRANSLATION
+ cat <<-EOF
+ [form #user_login .login method=POST
+ [input name=uname placeholder="Benutzername oder Email-Adresse" autocomplete=off]
+ [input type=password name=pw placeholder="Passwort"]
+ [submit "action" "user_login" Einloggen]
+ ]
+ 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" Ausloggen]
+ ]
+ EOF
+}
diff --git a/macros/attachments b/macros/attachments
new file mode 100755
index 0000000..bcd6722
--- /dev/null
+++ b/macros/attachments
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/acl.sh"
+. "$_EXEC/tools.sh"
+
+page="$1"
+
+if [ "${page#/}" = "$page" ]; then
+ page="$(PATH "${PATH_INFO}/$page")"
+fi
+
+acl_read "$page" || exit 0
+
+printf %s\\n ''
+
+for file in "$_EXEC/pages/$page/#attachments"/* "$_DATA/pages/$page/#attachments"/*; do
+ [ "$file" = "$_EXEC/pages/$page/#attachments/${file##*/}" \
+ -a -f "$_DATA/pages/$page/#attachments/${file##*/}" ] && continue
+ stat="$(stat -c '%s %Y' -- "$file" 2>&-)" || continue
+ size="${stat% *}" date="${stat#* }"
+
+ printf '%s
+ %s %s ' \
+ "$(HTML "${file##*/}")" "$(HTML "${file##*/}")" \
+ "$(size_human "$size")" "$(date -d @"$date" +"%F %T")"
+done
+
+printf %s\\n ' '
diff --git a/macros/errormessage b/macros/errormessage
new file mode 100755
index 0000000..ff1e06e
--- /dev/null
+++ b/macros/errormessage
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+
+if [ "$1" -o "$ERROR_MSG" ]; then
+ printf '%s
' "$(HTML "${1:-${ERROR_MSG}}")"
+fi
diff --git a/macros/gallery b/macros/gallery
new file mode 100755
index 0000000..3a05ee8
--- /dev/null
+++ b/macros/gallery
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/acl.sh"
+. "$_EXEC/tools.sh"
+
+[ $# = 0 ] && set -- "*"
+
+printf ''
+for glob in "$@"; do
+ page="${glob%/*}"
+ [ "$page" = "$glob" ] && page=./
+ [ ! "$page" ] && page=/
+
+ acl_read "$(page_abs "${page}")" \
+ && attachment_glob "$glob"
+done \
+| grep -xiE '.*\.jpe?g|.*\.png|.*\.gif' \
+| while read attachment; do
+ case $attachment in
+ */*)
+ printf '
' \
+ "${attachment%/*}/[attachment]/${attachment##*/}" "${attachment}"
+ ;;
+ *)
+ printf '
' \
+ "[attachment]/${attachment}" "${attachment}"
+ ;;
+ esac
+done
+printf '
'
diff --git a/macros/include b/macros/include
new file mode 100755
index 0000000..5254a26
--- /dev/null
+++ b/macros/include
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/acl.sh"
+. "$_EXEC/tools.sh"
+
+from='1'; to='$'; rev=''; items='$'; link='true'
+
+while [ $# -gt 0 ]; do case $1 in
+ --from) from="$2"; shift 2;;
+ from=*) from="${1#*=}"; shift 1;;
+ --to) to="$2"; shift 2;;
+ to=*) to="${1#*=}"; shift 1;;
+ --items) items="$2"; shift 2;;
+ items=*) items="${1#*=}"; shift 1;;
+ --rev|--reverse) rev="-r"; shift 1;;
+ --nolink) link=""; shift 1;;
+ *) page="$1"; shift 1;;
+esac; done
+
+if ! printf %s\\n "$from" |grep -qEx '[0-9]+|/([^/\\]|\\/|\\.)*/'; then
+ debug 'Include macro invalid argument: "from"'
+ exit 1
+fi
+if ! printf %s\\n "$to" |grep -qEx '\$|[0-9]+|/([^/\\]|\\/|\\.)*/'; then
+ debug 'Include macro Invalid argument: "to"'
+ exit 1
+fi
+if ! printf %s\\n "$items" |grep -qEx '\$|[0-9]+'; then
+ debug 'Include macro Invalid argument: "items"'
+ exit 1
+fi
+
+page_glob "$page" \
+| sort $rev \
+| sed "${items}q" \
+| while read glob; do
+ page="$(page_abs "$glob")"
+ acl_read "$page" || continue
+ mdfile="$(mdfile "$page")" || continue
+ hglob="$(HTML "$glob")"
+ refpfx="$(printf %s\\n "$hglob" |sed 's;[\;&\;];\\&;g')"
+ [ "$link" ] \
+ && printf '
+
%s
+
' \
+ "${hglob}" "${hglob}" "${hglob}" \
+ || printf '
+
' \
+ "${hglob}"
+ ( # PATH_INFO may be used by macros in the included page
+ export PATH_INFO="$page"
+ cd -- "${mdfile%/*}/"
+ sed -n "${from},${to}p" <"$mdfile" \
+ | md \
+ | grep -vx ''
+ ) | sed -E '
+ s;(<[^>]+ )(href|src)="([^"]+://[^"]*|[mM][aA][iI][lL][tT][oO]:[^"]*)"([^>]*>);\1\2="/#safe/\3"\4;g
+ s;(<[^>]+ )(href|src)="([^#/"][^"]*)"([^>]*>);\1\2="'"${refpfx}"'\3"\4;g
+ s;(<[^>]+ )(href|src)="/#safe/([^"]*)"([^>]*>);\1\2="\3"\4;g
+ '
+ printf ' '
+done
diff --git a/macros/newpage b/macros/newpage
new file mode 100755
index 0000000..2502b00
--- /dev/null
+++ b/macros/newpage
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/acl.sh"
+. "$_EXEC/tools.sh"
+
+pattern=./%%s
+template=''
+label='New Page'
+
+while [ $# -gt 0 ]; do case $1 in
+ template=*) template="${1#*=}"; shift 1;;
+ --template) template="$2"; shift 2;;
+ label=*) label="${1#*=}"; shift 1;;
+ --label) label="$2"; shift 2;;
+ *) pattern="$1"; shift 1;;
+esac; done
+
+if acl_write "$(page_abs "$pattern")"; then
+ cat <<-EOF
+
+ EOF
+fi
diff --git a/macros/pagelist b/macros/pagelist
new file mode 100755
index 0000000..54d4a69
--- /dev/null
+++ b/macros/pagelist
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/acl.sh"
+. "$_EXEC/tools.sh"
+
+dir="${1:-./}"
+depth="${2:-1}"
+
+printf '\n'
+ page_glob "$dir" \
+ | while read -r glob; do
+ printf %s\\n "$glob"
+ page_children "$glob" "$((depth - 1))"
+ done \
+ | while read -r page; do
+ pagedir="$(page_abs "$page")"
+ [ -f "$_DATA/pages/${pagedir}/#page.md" -o \
+ -f "$_EXEC/pages/${pagedir}/#page.md" ] \
+ && acl_read "$pagedir" \
+ && printf '%s ' "$(HTML "$page")" "$(HTML "$page")"
+ done
+printf ' \n'
diff --git a/macros/reflink b/macros/reflink
new file mode 100755
index 0000000..1b38c15
--- /dev/null
+++ b/macros/reflink
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+
+title="$(HTML "$1")"
+ref="$(HEADER Referer)"
+
+printf '%s ' "${ref:-./}" "${title:-Return}"
diff --git a/macros/revisions b/macros/revisions
new file mode 100755
index 0000000..4271149
--- /dev/null
+++ b/macros/revisions
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/tools.sh"
+. "$_EXEC/acl.sh"
+
+page="$1"
+page_abs="$(page_abs "$page")"
+
+if ! acl_read "$page_abs"; then
+ return 0
+elif [ "$REV_PAGES" != true ]; then
+ printf 'GIT is not available to handle revisioning.
'
+fi
+
+printf '\n'
+ IFS=" "
+ { git -C "$_DATA" log --date=format:"%a, %x %H:%M" \
+ --pretty=format:"%h %cd %s" \
+ -- "pages${page_abs}#page.md"
+ printf '\n'
+ } | while read -r hash date message; do
+ user="${message% @*}"; user="${user##*@ }"
+ printf '%s %s %s \n' \
+ "$(HTML "${page%/}/[revision]/$hash")" "$(HTML "$hash")" "$(HTML "$date")" "$(HTML "$user")"
+ done
+printf ' \n'
diff --git a/macros/toc b/macros/toc
new file mode 100755
index 0000000..f3a6358
--- /dev/null
+++ b/macros/toc
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+
+unset MD_MACROS
+
+if [ "$(which awk)" ]; then
+ md() { awk -f "$_EXEC/cgilite/markdown.awk"; }
+elif [ "$(which busybox)" ]; then
+ md() { busybox awk -f "$_EXEC/cgilite/markdown.awk"; }
+else
+ md() { cat; }
+fi
+
+md |sed -nE '
+ 1i
+ s;^<(h[1-6]) id="([^"]*)">(([^<]|<[^aA]|<[aA][^ ])+)( )?$;\3 ;p
+ $i
+'
diff --git a/macros/wikiform b/macros/wikiform
new file mode 100755
index 0000000..9a736da
--- /dev/null
+++ b/macros/wikiform
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+action="$1"
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/cgilite/users.sh"
+
+[ "${LANGUAGE}" -a -r "${_EXEC}/l10n/${LANGUAGE}.sh" ] && . "${_EXEC}/l10n/${LANGUAGE}.sh"
+
+case $action in
+ login)
+ w_user_login |"$_EXEC/cgilite/html-sh.sed"
+ ;;
+ register)
+ w_user_register |"$_EXEC/cgilite/html-sh.sed"
+ ;;
+ invite)
+ w_user_invite |"$_EXEC/cgilite/html-sh.sed"
+ ;;
+ settings)
+ w_user_update |"$_EXEC/cgilite/html-sh.sed"
+ ;;
+esac
diff --git a/md_macros.awk b/md_macros.awk
new file mode 100755
index 0000000..88e662f
--- /dev/null
+++ b/md_macros.awk
@@ -0,0 +1,69 @@
+#!/bin/awk -f
+#!/opt/busybox/awk -f
+
+function sh_escape(arg){
+ return "'" gensub(/'/, "'\"'\"'", "g", arg) "'";
+}
+
+function argsplit(line, args, LOCAL, c, n, ctx) {
+ ctx="space"; n=0;
+
+ while ( length(line) > 0 ) {
+ c = substr(line, 1, 1);
+ line = substr(line, 2);
+ if (ctx == "space" )
+ if (c ~ /[ \t]/) ctx = "space";
+ else if (c ~ /\\/) { n++; ctx = "escbare"; }
+ else if (c ~ /"/) { n++; ctx = "dquot"; }
+ else if (c ~ /'/) { n++; ctx = "squot"; }
+ else { n++; args[n] = c; ctx = "bare"; }
+ else if (ctx == "bare")
+ if (c ~ /[ \t]/) ctx = "space";
+ else if (c ~ /\\/) ctx = "escbare";
+ else if (c ~ /"/) ctx = "dquot";
+ else if (c ~ /'/) ctx = "squot";
+ else args[n] = args[n] c;
+ else if (ctx == "dquot")
+ if (c ~ /"/) ctx = "bare";
+ else if (c ~ /\\/) ctx = "escdquot";
+ else args[n] = args[n] c;
+ else if (ctx == "squot")
+ if (c ~ /'/) ctx = "bare";
+ else args[n] = args[n] c;
+ else if (ctx == "escbare") {
+ args[n] = args[n] c;
+ ctx = "bare";
+ }
+ else if (ctx == "escdquot") {
+ args[n] = args[n] c;
+ ctx = "dquot";
+ }
+ }
+}
+
+function macro(call, LOCAL, line, args) {
+ argsplit(call, args);
+ call="";
+
+ for (n = 1; n in args; n++) call = call sh_escape(args[n]) " ";
+
+ if (args[1] in MACROS) {
+ oldRS=RS; oldORS=ORS;
+ RS=""; ORS=""; line="";
+ "printf '%s' " sh_escape(file) " | " sh_escape(ENVIRON["MD_MACROS"]) "/" call | getline line;
+ RS=oldRS; ORS=oldORS;
+ return line;
+ } else {
+ return HTML("<<" call ">>");
+ }
+}
+
+BEGIN {
+ if (ENVIRON["MD_MACROS"]) {
+ AllowMacros = "true";
+ "cd " sh_escape(ENVIRON["MD_MACROS"]) "; printf '%s/' *" |getline macro_list;
+ split(macro_list, MACROS, "/");
+ for (n in MACROS) { MACROS[MACROS[n]] = ""; delete MACROS[n]; }
+ delete MACROS[""];
+ }
+}
diff --git a/multipart.sh b/multipart.sh
new file mode 100755
index 0000000..7c0d5dd
--- /dev/null
+++ b/multipart.sh
@@ -0,0 +1,91 @@
+#!/bin/sh
+
+[ "$include_multipart" ] && return 0
+inlude_multipart="$0"
+
+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/#attachments/favicon.ico b/pages/#attachments/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/pages/#page.md b/pages/#page.md
new file mode 100644
index 0000000..4b72bae
--- /dev/null
+++ b/pages/#page.md
@@ -0,0 +1,5 @@
+%acl Known:read,write
+ All:read,write
+
+It Works!
+=========
diff --git a/pages/:de/#page.md b/pages/:de/#page.md
new file mode 100644
index 0000000..9e00514
--- /dev/null
+++ b/pages/:de/#page.md
@@ -0,0 +1,5 @@
+%acl Known:read,write
+ All:read,write
+
+Es Funktioniert!
+================
diff --git a/pages/[wiki]/#page.md b/pages/[wiki]/#page.md
new file mode 100644
index 0000000..dfed0bc
--- /dev/null
+++ b/pages/[wiki]/#page.md
@@ -0,0 +1,12 @@
+%acl Known:read,write
+ All:read
+
+TECHNICAL pages
+===============
+The pages in this section are used to display various wiki functions. You can update them to apply your own theme to the wiki.
+
+I.e. you can change Error pages, the wikis header and footer sections etc.
+
+*Write* access should be controlled by overriding this page. However most pages in this section should remain *readable* to All.
+
+<>
diff --git a/pages/[wiki]/400/#page.md b/pages/[wiki]/400/#page.md
new file mode 100644
index 0000000..94ba677
--- /dev/null
+++ b/pages/[wiki]/400/#page.md
@@ -0,0 +1,9 @@
+%nocache
+
+400
+===
+
+**Bad Request**
+<>
+
+<>
diff --git a/pages/[wiki]/400/:de/#page.md b/pages/[wiki]/400/:de/#page.md
new file mode 100644
index 0000000..23f2af5
--- /dev/null
+++ b/pages/[wiki]/400/:de/#page.md
@@ -0,0 +1,9 @@
+%nocache
+
+400
+===
+
+**Fehlerhafte Anfrage**
+<>
+
+<>
diff --git a/pages/[wiki]/403/#page.md b/pages/[wiki]/403/#page.md
new file mode 100644
index 0000000..d15309b
--- /dev/null
+++ b/pages/[wiki]/403/#page.md
@@ -0,0 +1,8 @@
+%nocache
+
+403
+===
+
+**Forbidden**
+
+<>
diff --git a/pages/[wiki]/403/:de/#page.md b/pages/[wiki]/403/:de/#page.md
new file mode 100644
index 0000000..1e1a9e4
--- /dev/null
+++ b/pages/[wiki]/403/:de/#page.md
@@ -0,0 +1,8 @@
+%nocache
+
+403
+===
+
+**Zugriff verweigert**
+
+<>
diff --git a/pages/[wiki]/404/#page.md b/pages/[wiki]/404/#page.md
new file mode 100644
index 0000000..f54bad5
--- /dev/null
+++ b/pages/[wiki]/404/#page.md
@@ -0,0 +1,8 @@
+%nocache
+
+404
+===
+
+**page not found**
+
+<>
diff --git a/pages/[wiki]/404/:de/#page.md b/pages/[wiki]/404/:de/#page.md
new file mode 100644
index 0000000..17ed504
--- /dev/null
+++ b/pages/[wiki]/404/:de/#page.md
@@ -0,0 +1,8 @@
+%nocache
+
+404
+===
+
+**Seite nicht gefunden**
+
+<>
diff --git a/pages/[wiki]/409/#page.md b/pages/[wiki]/409/#page.md
new file mode 100644
index 0000000..0727e7c
--- /dev/null
+++ b/pages/[wiki]/409/#page.md
@@ -0,0 +1,9 @@
+%nocache
+
+409
+===
+
+**Conflict**
+<>
+
+<>
diff --git a/pages/[wiki]/409/:de/#page.md b/pages/[wiki]/409/:de/#page.md
new file mode 100644
index 0000000..13c5091
--- /dev/null
+++ b/pages/[wiki]/409/:de/#page.md
@@ -0,0 +1,9 @@
+%nocache
+
+409
+===
+
+**Konflikt**
+<>
+
+<>
diff --git a/pages/[wiki]/500/#page.md b/pages/[wiki]/500/#page.md
new file mode 100644
index 0000000..0d56e0a
--- /dev/null
+++ b/pages/[wiki]/500/#page.md
@@ -0,0 +1,9 @@
+%nocache
+
+500
+===
+
+**Internal Server Error**
+<>
+
+<>
diff --git a/pages/[wiki]/500/:de/#page.md b/pages/[wiki]/500/:de/#page.md
new file mode 100644
index 0000000..3dfcf7f
--- /dev/null
+++ b/pages/[wiki]/500/:de/#page.md
@@ -0,0 +1,9 @@
+%nocache
+
+500
+===
+
+**Interner Serverfehler**
+<>
+
+<>
diff --git a/pages/[wiki]/:de/#page.md b/pages/[wiki]/:de/#page.md
new file mode 100644
index 0000000..7e5a6f8
--- /dev/null
+++ b/pages/[wiki]/:de/#page.md
@@ -0,0 +1,14 @@
+%acl Known:read,write
+ All:read
+
+TECHNISCHE Seiten
+=================
+Die Seiten in dieser Sektion werden genutzt um verschiedene Wiki-Funktionen anzuzeigen. Sie können sie anpassen
+um dem Wiki Ihren eigenen Stil zu geben.
+
+Z.B. können Sie hier Fehlerseiten, die Kopfzeile und die FuÃzeile ändern.
+
+Der *Schreib*zugriff sollte angepasst werden, indem Sie diese Seite editieren. Jedoch sollten die meisten Seiten in
+dieser Sektion für alle *Lesbar* bleiben.
+
+<>
diff --git a/pages/[wiki]/editorhelp/#page.md b/pages/[wiki]/editorhelp/#page.md
new file mode 100644
index 0000000..930fda3
--- /dev/null
+++ b/pages/[wiki]/editorhelp/#page.md
@@ -0,0 +1,31 @@
+### Formatting:
+
+\*\***strong**\*\* \**emphasized*\* `~~`~~strikethrough~~`~~` \``verbatim`\`
+
+a backslash `\` prevents \*\*accidental formatting\*\*: \\\* \\\`
+
+### Links:
+
+Simple Weblink (use angle brackets): < >
+
+Simple Email Link: < >
+
+Weblink with Text: \[Wikipedia article\](https://en.wikipedia.org/wiki/Markdown) - [Wikipedia article](https://en.wikipedia.org/wiki/Markdown)
+
+Other pages on the same site:
+[Start page](/): `[Start page](/)`, [Help](/[wiki]/editorhelp/): `[Help](/[wiki]/editorhelp/)`
+
+
+### Lists:
+
++----------------------------------+---------------------+
+| [space] [dash] [space] [text] | - bullet |
+| - bullet | - list |
+| - list | - indented point |
+| - indented point | |
++------------------------------------------+---------------------+
+| [space] [number] [dot] [space] [text] | 1. ordered |
+| 1. ordered | 2. list |
+| 2. list | 1. indented point|
+| 1. indented point | |
++------------------------------------------+---------------------+
diff --git a/pages/[wiki]/editorhelp/:de/#page.md b/pages/[wiki]/editorhelp/:de/#page.md
new file mode 100644
index 0000000..52a7f4b
--- /dev/null
+++ b/pages/[wiki]/editorhelp/:de/#page.md
@@ -0,0 +1,31 @@
+### Formatierung:
+
+\*\***kräftig**\*\* \**hervorgehoben*\* `~~`~~durchgestrichen~~`~~` \``wörtlich`\`
+
+Ein Backslash `\` verhindert \*\*versehentliche Textformatierung\*\*: \\\* \\\`
+
+### Links:
+
+Einfacher Weblink (spitze Klammern): < >
+
+Einfacher Email Link: < >
+
+Weblink mit Text: \[Wikipediaartikel\](https://de.wikipedia.org/wiki/Markdown) - [Wikipediaartikel](https://de.wikipedia.org/wiki/Markdown)
+
+Andere Seiten auf der selben Domain:
+[Startseite](/): `[Startseite](/)`, [Hilfe](/[wiki]/editorhelp/): `[Hilfe](/[wiki]/editorhelp/)`
+
+
+### Listen:
+
++--------------------------------------------------------+--------------------------+
+| [Leerzeichen] [Bindestrich] [Leerzeichen] [Text] | - Stichpunkt |
+| - Stichpunkt | - Liste |
+| - Liste | - eingerückter Punkt |
+| - eingerücker Punkt | |
++--------------------------------------------------------+--------------------------+
+| [Leerzeichen] [Ziffer] [Punkt] [Leerzeichen] [Text] | 1. nummerierte |
+| 1. nummerierte | 2. Liste |
+| 2. Liste | 1. eingerückter Punkt |
+| 1. eingerückter Punkt | |
++--------------------------------------------------------+--------------------------+
diff --git a/pages/[wiki]/footer/#page.md b/pages/[wiki]/footer/#page.md
new file mode 100644
index 0000000..205e8f8
--- /dev/null
+++ b/pages/[wiki]/footer/#page.md
@@ -0,0 +1,6 @@
+::: { .menu }
+ * <>
+ * [Register](./[register])
+ * [Invite](./[invite])
+:::
+Edit the Footer [here](/[wiki]/footer/[edit])
diff --git a/pages/[wiki]/footer/:de/#page.md b/pages/[wiki]/footer/:de/#page.md
new file mode 100644
index 0000000..20b579d
--- /dev/null
+++ b/pages/[wiki]/footer/:de/#page.md
@@ -0,0 +1,6 @@
+::: { .menu }
+ * <>
+ * [Registrieren](./[register])
+ * [Einladen](./[invite])
+:::
+Der Seitenfuà kann [hier](/[wiki]/footer/[edit]) bearbeitet werden
diff --git a/pages/[wiki]/header/#page.md b/pages/[wiki]/header/#page.md
new file mode 100644
index 0000000..8624c9b
--- /dev/null
+++ b/pages/[wiki]/header/#page.md
@@ -0,0 +1,11 @@
+[Shellwiki](/)
+--------------
+
+::: { .menu }
+ * [Help](/[wiki]/)
+ * [Edit Style](/[wiki]/)
+:::
+
+Edit the Header [here](/[wiki]/header/[edit])
+
+----
diff --git a/pages/[wiki]/header/:de/#page.md b/pages/[wiki]/header/:de/#page.md
new file mode 100644
index 0000000..306f95b
--- /dev/null
+++ b/pages/[wiki]/header/:de/#page.md
@@ -0,0 +1,11 @@
+[Shellwiki](/)
+--------------
+
+::: { .menu }
+ * [Hilfe](/[wiki]/)
+ * [Stil anpassen](/[wiki]/)
+:::
+
+Der Seitenkopf kann [hier](/[wiki]/header/[edit]) bearbeitet werden
+
+----
diff --git a/pages/[wiki]/invite/#page.md b/pages/[wiki]/invite/#page.md
new file mode 100644
index 0000000..114b208
--- /dev/null
+++ b/pages/[wiki]/invite/#page.md
@@ -0,0 +1,7 @@
+%nocache
+
+Set up an account
+-----------------
+<>
+
+[Return](./)
diff --git a/pages/[wiki]/invite/:de/#page.md b/pages/[wiki]/invite/:de/#page.md
new file mode 100644
index 0000000..1386381
--- /dev/null
+++ b/pages/[wiki]/invite/:de/#page.md
@@ -0,0 +1,7 @@
+%nocache
+
+Neues Benutzerkonto
+-------------------
+<>
+
+[Zurück](./)
diff --git a/pages/[wiki]/login/#page.md b/pages/[wiki]/login/#page.md
new file mode 100644
index 0000000..ac715ae
--- /dev/null
+++ b/pages/[wiki]/login/#page.md
@@ -0,0 +1,8 @@
+%nocache
+
+Login
+-----
+<>
+[Account registration]([register] "Sign up for a new user account")
+
+[Return](./)
diff --git a/pages/[wiki]/login/:de/#page.md b/pages/[wiki]/login/:de/#page.md
new file mode 100644
index 0000000..9a110eb
--- /dev/null
+++ b/pages/[wiki]/login/:de/#page.md
@@ -0,0 +1,8 @@
+%nocache
+
+Login
+-----
+<>
+[Neu registrieren]([register] "Ein neues Benutzerkonto erstellen")
+
+[Zurück](./)
diff --git a/pages/[wiki]/register/#page.md b/pages/[wiki]/register/#page.md
new file mode 100644
index 0000000..b88149c
--- /dev/null
+++ b/pages/[wiki]/register/#page.md
@@ -0,0 +1,7 @@
+%nocache
+
+Set up an account
+-----------------
+<>
+
+[Return](./)
diff --git a/pages/[wiki]/register/:de/#page.md b/pages/[wiki]/register/:de/#page.md
new file mode 100644
index 0000000..c46109b
--- /dev/null
+++ b/pages/[wiki]/register/:de/#page.md
@@ -0,0 +1,7 @@
+%nocache
+
+Neues Benutzerkonto
+-------------------
+<>
+
+[Zurück](./)
diff --git a/pages/[wiki]/settings/#page.md b/pages/[wiki]/settings/#page.md
new file mode 100644
index 0000000..b03e0e2
--- /dev/null
+++ b/pages/[wiki]/settings/#page.md
@@ -0,0 +1,7 @@
+%nocache
+
+Change Your Passphrase
+----------------------
+<>
+
+[Return](./)
diff --git a/pages/[wiki]/settings/:de/#page.md b/pages/[wiki]/settings/:de/#page.md
new file mode 100644
index 0000000..280f012
--- /dev/null
+++ b/pages/[wiki]/settings/:de/#page.md
@@ -0,0 +1,7 @@
+%nocache
+
+Passwort ändern
+---------------
+<>
+
+[Zurück](./)
diff --git a/parsers/50_markdown.sh b/parsers/50_markdown.sh
new file mode 100755
index 0000000..94a0b06
--- /dev/null
+++ b/parsers/50_markdown.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+if which awk >/dev/null; then
+ awk -f "$_EXEC/md_macros.awk" -f "$_EXEC/cgilite/markdown.awk"
+ # | sed -E 's;(<[^>]+ )href="((/[^"/]+|[^"/]+[^:/]|)/([^"/]+/)*)"([^>]*>);\1href="\2:'"${LANGUAGE}"'"\5;g'
+elif which busybox >/dev/null; then
+ busybox awk -f "$_EXEC/md_macros.awk" -f "$_EXEC/cgilite/markdown.awk"
+ # | sed -E 's;(<[^>]+ )href="((/[^"/]+|[^"/]+[^:/]|)/([^"/]+/)*)"([^>]*>);\1href="\2:'"${LANGUAGE}"'"\5;g'
+else
+ cat
+fi
diff --git a/parsers/60_translation_links.sh b/parsers/60_translation_links.sh
new file mode 100755
index 0000000..0020719
--- /dev/null
+++ b/parsers/60_translation_links.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+if [ "$LANGUAGE" != "$LANGUAGE_DEFAULT" ]; then
+ sed -E 's;(<[^>]+ )href="((/[^"/]+|[^"/]+[^:/]|)/([^"/]+/)*)"([^>]*>);\1href="\2:'"${LANGUAGE}"'"\5;g'
+else
+ cat
+fi
diff --git a/session_lock.sh b/session_lock.sh
new file mode 100755
index 0000000..21c9808
--- /dev/null
+++ b/session_lock.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+[ "$include_sessionlock" ] && return 0
+include_sessionlock="$0"
+
+. "$_EXEC/cgilite/storage.sh"
+. "$_EXEC/cgilite/session.sh"
+
+LOCK_TIMEOUT="${LOCK_TIMEOUT:-1200}"
+
+S_LOCK(){
+ local file="$1" timeout="${2:-$LOCK_TIMEOUT}"
+ local date sid
+
+ printf "%i %s\n" "$_DATE" "$SESSION_ID" >>"${file}.lock"
+
+ if ! read date sid <"${file}.lock"; then
+ debug "Unable to access lock: ${file}.lock"
+
+ elif [ $((date + timeout)) -lt $_DATE ]; then
+ # Override stale lock
+ if LOCK "${file}.lock" 1; then
+ debug "Overriding stale lock: ${file}.lock"
+ printf "%i %s\n" "$_DATE" "$SESSION_ID" >"${file}.lock"
+ RELEASE "${file}.lock"
+ return 0
+ else
+ return 1
+ fi
+
+ elif [ "$sid" = "$SESSION_ID" -a "$date" -ne "$_DATE" ]; then
+ # Refresh aged lock
+ printf "%i %s\n" "$_DATE" "$SESSION_ID" >"${file}.lock"
+ return 0
+
+ elif [ "$sid" = "$SESSION_ID" ]; then
+ # Simple success
+ return 0
+
+ else
+ return 1
+ fi
+}
+
+S_RELEASE(){
+ local file="$1" timeout="${2:-$LOCK_TIMEOUT}"
+ local date sid
+
+ if ! read date sid <"${file}.lock"; then
+ # File was not locked
+ return 0
+
+ elif [ "$sid" = "$SESSION_ID" -a $((date + timeout)) -lt $_DATE ]; then
+ # if lock is stale, protect against stale override before release
+ if LOCK "${file}.lock" 1; then
+ rm -- "${file}.lock"
+ RELEASE "${file}.lock"
+ return 0
+ else
+ return 1
+ fi
+
+ elif [ "$sid" = "$SESSION_ID" ]; then
+ # Simple success
+ rm -- "${file}.lock"
+ return 0
+
+ else
+ return 1
+ fi
+}
diff --git a/themes/default.css b/themes/default.css
new file mode 100644
index 0000000..e23c182
--- /dev/null
+++ b/themes/default.css
@@ -0,0 +1,247 @@
+html { min-height: 100%; }
+
+body {
+ position: absolute;
+ width: 100%;
+ min-height: 100%;
+ padding-bottom: 6em;
+ background-color: #EEE;
+ font-size: 12pt;
+}
+
+header, footer {
+ background-color: #FFF;
+ box-shadow: 0 0 .75em;
+ width: 100%;
+ z-index: 1;
+}
+
+footer {
+ padding-top: .5em;
+ position: absolute;
+ bottom: 0;
+}
+
+header > :last-child,
+main > :last-child {
+ margin-bottom: 0;
+}
+
+header h1,
+header h2,
+header .menu,
+footer .menu {
+ display: inline-block;
+}
+
+header .menu,
+footer .menu { list-style: none; }
+
+header .menu > ul > li,
+header .menu > ol > li,
+footer .menu > ul > li,
+footer .menu > ol > li {
+ display: inline-block;
+ margin-right: .5em;
+ vertical-align: top;
+}
+
+header .menu.dropdown li > ul,
+header .menu.dropdown li > ol {
+ display: none;
+ position: absolute;
+ background-color: #FFF;
+ margin: 0;
+ padding: .25em .5em;
+ padding-left: 1.5em;
+ box-shadow: 0 0 .75em;
+}
+header .menu.dropdown li:hover > ul,
+header .menu.dropdown li:hover > ol {
+ display: table;
+}
+
+main .pagemenu {
+ list-style: none;
+ background-color: #666;
+ margin: 0;
+ box-shadow: 0 0 .5em;
+ padding: .25em 2em;
+}
+main .pagemenu li {
+ display: inline-block;
+ margin-right: 1em;
+}
+main .pagemenu li a { color: #FFF; }
+
+main article,
+main > form#renamepage, main > form#movepage,
+main > form#deletepage,
+[id$="/[attachment]"] main > form {
+ margin: 1em;
+ padding: .125em 1em 1em 1em;
+ box-shadow: .25em .25em .75em;
+ background-color: #FFF;
+}
+
+[id$="/[revision]"] main .revisions,
+[id$="/[attachment]"] main > .attachment.list {
+ margin: 1em;
+ padding: 1em 2em;
+}
+[id$="/[revision]"] main .revisions:before,
+[id$="/[attachment]"] main > .attachment.list:before {
+ content: '';
+ position: absolute;
+ top: 0; bottom: 0; left: 0; right: 0;
+ background-color: #FFF;
+ box-shadow: .25em .25em .75em;
+}
+
+main code {
+ padding: .125em .25em;
+ background-color: #CCC;
+}
+main pre {
+ padding: .5em .5em;
+ background-color: #CCC;
+ max-width: 100%;
+ overflow-x: auto;
+}
+
+/* === Editor === */
+
+body[id$="/[edit]"] textarea,
+body[id$="/[newpage]"] textarea {
+ width: 100%;
+ min-height: 20em; min-height: 50vh;
+ font-family: monospace;
+ font-size: inherit;
+}
+
+[id$="/[edit]"] main {
+ padding-top: 2.5em;
+}
+[id$="/[edit]"] input[type=radio].tab ~ div.tab.syntax,
+[id$="/[edit]"] input[type=radio].tab ~ div.tab.help {
+ background-color: #FFF;
+ min-height: 20em; min-height: 50vh;
+}
+[id$="/[edit]"] input[type=radio].tab#edtr:checked ~ form.tab.editor { display: block; }
+[id$="/[edit]"] input[type=radio].tab#help:checked ~ div.tab.syntax { display: block; }
+[id$="/[edit]"] input[type=radio].tab#himg:checked ~ div.tab.help,
+[id$="/[edit]"] input[type=radio].tab#hdoc:checked ~ div.tab.help {
+ display: block;
+}
+[id$="/[edit]"] input[type=radio].tab ~ div.tab.help .himg,
+[id$="/[edit]"] input[type=radio].tab ~ div.tab.help .hdoc {
+ display: none;
+}
+[id$="/[edit]"] input[type=radio].tab ~ div.tab.help {
+ padding-top: 1em;
+ padding-left: 7em;
+}
+[id$="/[edit]"] input[type=radio].tab ~ div.tab.help .himg img {
+ float: left;
+ max-height: 4em;
+ margin-left: -6em;
+}
+[id$="/[edit]"] input[type=radio].tab#himg:checked ~ div.tab.help .himg { display: block; }
+[id$="/[edit]"] input[type=radio].tab#hdoc:checked ~ div.tab.help .hdoc { display: block; }
+
+
+/* === Attachments === */
+
+.attachment.list button[name=delete] {
+ font-size: .75em;
+ line-height: 1.25em;
+ margin-right: 1.25em;
+}
+.attachment.list .size,
+.attachment.list .date {
+ font-size: .875em;
+ top: -.25em;
+}
+
+.attachment.list .name:after {
+ white-space: pre-line;
+ content: "\0a";
+}
+.attachment.list .size {
+ margin-right: 1em;
+}
+
+.revisions li { margin: 1em 0; }
+.revisions li span.hash,
+.revisions li span.date {
+ margin-right: 1em;
+}
+
+[id$="/[attachment]"] input[type=radio].tab ~ div.tab {
+ display: block;
+ padding-top: 1em;
+}
+[id$="/[attachment]"] input[type=radio].tab ~ div.tab ul.attachment.list {
+ list-style: none;
+ margin-left: 0;
+}
+
+.tab ul li input[name=select],
+.tab ul li label.name,
+.tab ul li a.name,
+.tab ul li input.name {
+ display: none;
+}
+
+[id$="/[attachment]"] input[type=radio].tab#tview:checked ~ div.tab ul li a.name,
+[id$="/[attachment]"] input[type=radio].tab#tdel:checked ~ div.tab ul li input[name=select],
+[id$="/[attachment]"] input[type=radio].tab#tdel:checked ~ div.tab ul li label.name,
+[id$="/[attachment]"] input[type=radio].tab#tmove:checked ~ div.tab ul li input[name=select],
+[id$="/[attachment]"] input[type=radio].tab#tmove:checked ~ div.tab ul li label.name {
+ display: inline;
+}
+[id$="/[attachment]"] input[type=radio].tab#tren:checked ~ .tab ul li input.name {
+ display: block;
+}
+
+[id$="/[attachment]"] label[for=moveto], [id$="/[attachment]"] input#moveto,
+[id$="/[attachment]"] button[name=action] { display: none; }
+
+[id$="/[attachment]"] .upload button[name=action] { display: inline-block; }
+[id$="/[attachment]"] input[type=radio].tab#tdel:checked ~ div.tab button[name=action][value=delete],
+[id$="/[attachment]"] input[type=radio].tab#tmove:checked ~ div.tab label[for=moveto],
+[id$="/[attachment]"] input[type=radio].tab#tmove:checked ~ div.tab input#moveto,
+[id$="/[attachment]"] input[type=radio].tab#tmove:checked ~ div.tab button[name=action][value=move],
+[id$="/[attachment]"] input[type=radio].tab#tren:checked ~ div.tab button[name=action][value=rename] {
+ display: inline;
+}
+
+
+/* === Macros === */
+
+.macro.toc {
+ display: inline-block;
+ list-style-position: inside;
+ margin-left: 0;
+ background-color: #DDD;
+ background-color: rgba(0, 0, 0, .125);
+ padding: .75em 1em;
+ border: 1pt solid;
+ border-radius: 2pt;
+}
+.macro.toc li.h2 { margin-left: 1.25em; }
+.macro.toc li.h3 { margin-left: 2.5em; }
+.macro.toc li.h4 { margin-left: 3.75em; }
+.macro.toc li.h5 { margin-left: 5em; }
+.macro.toc li.h6 { margin-left: 6.25em; }
+
+.macro.gallery {
+ text-align: center;
+ margin: 2em 0;
+ padding: .5em .125em;
+ background-color: #444;
+}
+.macro.gallery img {
+ max-height: 9em;
+ margin: 0 .25em;
+}
diff --git a/themes/default.sh b/themes/default.sh
new file mode 100755
index 0000000..d82764b
--- /dev/null
+++ b/themes/default.sh
@@ -0,0 +1,203 @@
+#!/bin/sh
+
+. "$_EXEC/tools.sh"
+
+theme_head(){
+ local IFS="$BR"
+ printf '
+
+ '
+ for css in "$_BASE/%5B.%5D/cgilite/common.css" "$_BASE/%5B.%5D/themes/default.css" $PAGE_CSS; do
+ printf ' ' \
+ "$(HTML "${css##*//}")"
+ done
+}
+
+theme_header(){
+ printf '' "$(wiki '[wiki]/header/')"
+}
+
+theme_footer(){
+ printf '' "$(wiki '[wiki]/footer/')"
+}
+
+theme_pagemenu(){
+ local page="$1"
+
+ if acl_write "$page"; then
+ printf '
+ '
+ fi
+}
+
+theme_page(){
+ local page="$1" title="$2"
+ title="$(HTML "${title:-"${PAGE_TITLE:-"${page}"}"}")"
+
+ # Important! Web Server response including newline
+ printf "%s\r\n" "Content-Type: text/html; charset=utf-8" ""
+
+ cat <<-EOF
+
+
+ $(theme_head)
+ ${title}
+
+ $(theme_header)
+
+ $(theme_pagemenu)
+ $(if [ "$page" = '-' ]; then
+ cat
+ else
+ printf ''
+ wiki "$page"
+ printf ' '
+ fi)
+
+ $(theme_footer)
+
+ EOF
+}
+
+theme_editor(){
+ local page="$1" template="$2" title file att
+ title="$(HTML "${PAGE_TITLE:-"${page}"}")"
+
+ [ "$template" ] && acl_read "$template" || template="$page"
+
+ theme_page - "Editor: $title" <<-EOF
+ Editor
+ Syntax
+ Images
+ Documents
+
+ $(wiki "/[wiki]/editorhelp/")
+
+ $(for file in "$_EXEC/pages/${page%/:$LANGUAGE/}/#attachments"/* "$_DATA/pages/${page%/:$LANGUAGE/}/#attachments"/*; do
+ [ "$file" = "$_EXEC/pages/${page%/:$LANGUAGE/}/#attachments/${file##*/}" \
+ -a -f "$_DATA/pages/${page%/:$LANGUAGE/}/#attachments/${file##*/}" ] && continue
+ case ${file##*/} in
+ \*) continue;;
+ *.[pP][nN][gG]|*.[jJ][pP][gG]|*.[jJ][pP][eE][gG]|*.[gG][iI][fF])
+ [ "$page" != "${page%/:$LANGUAGE/}" ] && p=../ || p=''
+ att="$(HTML "${file##*/}")"
+ printf '
![](%s)
[%s]([attachment]/%s) [![%s](%s)]([attachment]/%s) ' \
+ "$p" "$att" "$att" "$att" "$att" "$att" "$att" "$att"
+ ;;
+ *)
+ att="$(HTML "${file##*/}")"
+ printf '
[%s](%s)
' "$att" "$att"
+ ;;
+ esac
+ done)
+
+ EOF
+}
+
+theme_revisions(){
+ local page="$1" title
+ title="${page%/}"; title="${title##*/}"
+
+ "$_EXEC/macros/revisions" "$page" \
+ | theme_page - "Revisions: $title"
+}
+
+theme_attachments(){
+ local page="$1" title
+ title="${page%/}"; title="${title##*/}"
+
+ if acl_write "$page"; then
+ theme_page - "Attachments: $title" <<-EOF
+
+ Upload
+
+
+ Upload
+
+
+
+ Attachments
+
+ View
+ Delete
+ Move
+ Rename
+
+
+
Delete
+
Move To:
+
+
Move
+
Rename
+
+
+ EOF
+ else
+ theme_page - "Attachments: $title" <<-EOF
+
+ $(for file in "$_EXEC/pages/$page/#attachments"/* "$_DATA/pages/$page/#attachments"/*; do
+ [ "$file" = "$_EXEC/pages/$page/#attachments/${file##*/}" \
+ -a -f "$_DATA/pages/$page/#attachments/${file##*/}" ] && continue
+ stat="$(stat -c '%s %Y' -- "$file" 2>&-)" || continue
+ size="${stat% *}" date="${stat#* }"
+ hfile="$(HTML "${file##*/}")"
+
+ printf '%s
+ %s %s ' \
+ "$hfile" "$hfile" "$(size_human "$size")" "$(date -d @"$date" +"%F %T")"
+ done)
+
+ EOF
+ fi
+}
+
+theme_error(){
+ local errno="$1"
+
+ case $errno in
+ 400) printf "%s\r\n" "Status: 400 Bad Request";;
+ 403) printf "%s\r\n" "Status: 403 Forbidden";;
+ 404) printf "%s\r\n" "Status: 404 Not Found";;
+ 409) printf "%s\r\n" "Status: 409 Conflict";;
+ 500) printf "%s\r\n" "Status: 500 Internal Server Error";;
+ esac
+
+ if mdfile "/[wiki]/$errno/" >&-; then
+ theme_page "/[wiki]/$errno/"
+ else
+ printf "Content-Length: 0\r\n\r\n"
+ fi
+}
diff --git a/tools.sh b/tools.sh
new file mode 100755
index 0000000..6388e3f
--- /dev/null
+++ b/tools.sh
@@ -0,0 +1,153 @@
+#!/bin/sh
+
+[ "$include_tools" ] && return 0
+include_tools="$0"
+
+md(){
+ local parser
+
+ if [ "$#" = 0 ]; then
+ md "${_EXEC}"/parsers/*
+ elif [ "$#" = 1 ]; then
+ "$1"
+ else
+ parser="$1"
+ shift 1
+ "$parser" |md "$@"
+ fi
+}
+
+mdfile(){
+ # Check if page exists, if possible fall
+ # back to default page from installation
+ local page="$(PATH "$1")"
+ page="${page%/}"
+
+ # Regular processing, keep in sync with tools.sh
+ if [ -f "$_DATA/pages/$page/:$LANGUAGE/#page.md" ]; then
+ printf %s\\n "$_DATA/pages/$page/:$LANGUAGE/#page.md"
+ elif [ -f "$_DATA/pages/$page/#page.md" ]; then
+ printf %s\\n "$_DATA/pages/$page/#page.md"
+ elif [ -f "$_EXEC/pages/$page/:$LANGUAGE/#page.md" ]; then
+ printf %s\\n "$_EXEC/pages/$page/:$LANGUAGE/#page.md"
+ elif [ -f "$_EXEC/pages/$page/#page.md" ]; then
+ printf %s\\n "$_EXEC/pages/$page/#page.md"
+ else
+ return 1
+ fi 2>&-
+ # ^^ suppress error messages produced
+ # by printf when stdout was closed
+
+ return 0
+}
+
+size_human(){
+ local size="$1"
+
+ if [ $size -gt $((1024 * 1024 * 1024)) ]; then
+ size=$((size / 1024 / 1024 / 1024 * 10 + size / 1024 / 1024 % 1024 / 100))
+ printf "%i.%i GB" "$((size / 10))" "$((size % 10))"
+
+ elif [ $size -gt $((1024 * 1024)) ]; then
+ size=$((size / 1024 / 1024 * 10 + size / 1024 % 1024 / 100))
+ printf "%i.%i MB" "$((size / 10))" "$((size % 10))"
+
+ elif [ $size -gt $((1024)) ]; then
+ size=$((size / 1024 * 10 + size % 1024 / 100))
+ printf "%i.%i KB" "$((size / 10))" "$((size % 10))"
+
+ else
+ printf "%i B" "$size"
+ fi
+}
+
+attachment_glob(){
+ local pattern="${1%/}" IFS=''
+ local glob page pagedir
+
+ page="${pattern%/*}"
+ [ "$page" = "$pattern" ] && page=.
+ [ ! "$page" ] && page=/
+ pattern="${pattern##*/}"
+ [ ! "$pattern" ] && pattern="*"
+
+ case $page in
+ /*)
+ for glob in "$_DATA/pages/$page/#attachments"/$pattern; do printf '%s\n' "${glob#"$_DATA/pages"}"; done
+ for glob in "$_EXEC/pages/$page/#attachments"/$pattern; do printf '%s\n' "${glob#"$_EXEC/pages"}"; done
+ ;;
+ *)
+ for glob in "$_DATA/pages/$PATH_INFO/$page/#attachments"/$pattern; do printf '%s\n' "${glob#"$_DATA/pages/$PATH_INFO/"}"; done
+ for glob in "$_EXEC/pages/$PATH_INFO/$page/#attachments"/$pattern; do printf '%s\n' "${glob#"$_EXEC/pages/$PATH_INFO/"}"; done
+ ;;
+ esac \
+ | sort -u \
+ | while read -r glob; do
+ [ -e "$glob" ] || continue
+ pagedir="$(page_abs "${glob%%/#attachments/*}/")"
+ [ -d "$_DATA/pages/$pagedir" -o -d "$_EXEC/pages/$pagedir" ] \
+ && printf '%s\n' "${glob%%/#attachments/*}/${glob#*/#attachments/}"
+ done
+}
+
+page_glob(){
+ local pattern="${1%/}/" IFS=''
+ local glob page pagedir
+
+ case $pattern in
+ /*)
+ for glob in "$_DATA/pages"$pattern; do printf '%s\n' "${glob#"$_DATA/pages"}"; done
+ for glob in "$_EXEC/pages"$pattern; do printf '%s\n' "${glob#"$_EXEC/pages"}"; done
+ ;;
+ *)
+ for glob in "$_DATA/pages/$PATH_INFO"/$pattern; do printf '%s\n' "${glob#"$_DATA/pages/$PATH_INFO/"}"; done
+ for glob in "$_EXEC/pages/$PATH_INFO"/$pattern; do printf '%s\n' "${glob#"$_EXEC/pages/$PATH_INFO/"}"; done
+ ;;
+ esac \
+ | sort -u \
+ | while read -r page; do
+ case $page in
+ \#*|*/\#*) continue;;
+ *) pagedir="$(page_abs "$page")";;
+ esac
+ [ -d "$_DATA/pages/$pagedir" -o -d "$_EXEC/pages/$pagedir" ] \
+ && printf '%s\n' "$page"
+ done
+}
+
+page_children(){
+ local page="${1:-${PATH_INFO}}" depth="${2:-1}"
+ local glob pagedir IFS=''
+ page="${page%/}/"
+
+ [ "$depth" -eq 0 ] && return 0
+
+ case $page in
+ /*)
+ for glob in "$_DATA/pages/${page}"*/; do printf '%s\n' "${glob#"$_DATA/pages/"}"; done
+ for glob in "$_EXEC/pages/${page}"*/; do printf '%s\n' "${glob#"$_EXEC/pages/"}"; done
+ ;;
+ *)
+ for glob in "$_DATA/pages/$PATH_INFO/${page}"*/; do printf '%s\n' "${glob#"$_DATA/pages/$PATH_INFO/"}"; done
+ for glob in "$_EXEC/pages/$PATH_INFO/${page}"*/; do printf '%s\n' "${glob#"$_EXEC/pages/$PATH_INFO/"}"; done
+ ;;
+ esac \
+ | sort -u \
+ | while read -r page; do
+ case $page in
+ \#*|*/\#*) continue;;
+ *) pagedir="$(page_abs "$page")";;
+ esac
+ if [ -d "$_DATA/pages/$pagedir" -o -d "$_EXEC/pages/$pagedir" ]; then
+ printf %s\\n "$page"
+ page_children "$page" "$((depth - 1))"
+ fi
+ done
+}
+
+page_abs(){
+ case $1 in
+ /*) PATH "${1%/}/";;
+ *) PATH "${PATH_INFO%/*}/${1%/}/";;
+ esac
+}