]> git.plutz.net Git - shellwiki/commitdiff
Merge commit 'ef47a42e878d7668aadeb1e14d9cb031303e92a3'
authorPaul Hänsch <paul@plutz.net>
Thu, 18 May 2023 12:05:58 +0000 (14:05 +0200)
committerPaul Hänsch <paul@plutz.net>
Thu, 18 May 2023 12:05:58 +0000 (14:05 +0200)
52 files changed:
Makefile [new file with mode: 0644]
acl.sh [new file with mode: 0755]
cgilite/.gitignore [moved from .gitignore with 100% similarity]
cgilite/cgilite.sh [moved from cgilite.sh with 100% similarity]
cgilite/common.css [moved from common.css with 100% similarity]
cgilite/file.sh [moved from file.sh with 100% similarity]
cgilite/html-sh.sed [moved from html-sh.sed with 100% similarity]
cgilite/logging.sh [moved from logging.sh with 100% similarity]
cgilite/markdown.awk [moved from markdown.awk with 100% similarity]
cgilite/session.sh [moved from session.sh with 100% similarity]
cgilite/storage.sh [moved from storage.sh with 100% similarity]
cgilite/users.sh [moved from users.sh with 100% similarity]
handlers/10_css.sh [new file with mode: 0755]
handlers/10_page.sh [new file with mode: 0755]
handlers/20_account.sh [new file with mode: 0755]
handlers/20_attachment.sh [new file with mode: 0755]
handlers/20_edit_attachment.sh [new file with mode: 0755]
handlers/20_revision.sh [new file with mode: 0755]
handlers/30_edit.sh [new file with mode: 0755]
handlers/30_move_rename_delete.sh [new file with mode: 0755]
handlers/30_newpage.sh [new file with mode: 0755]
handlers/90_brackets.sh [new file with mode: 0755]
index.cgi [new file with mode: 0755]
macros/attachments [new file with mode: 0755]
macros/errormessage [new file with mode: 0755]
macros/include [new file with mode: 0755]
macros/newpage [new file with mode: 0755]
macros/pagelist [new file with mode: 0755]
macros/reflink [new file with mode: 0755]
macros/revisions [new file with mode: 0755]
macros/toc [new file with mode: 0755]
macros/wikiform [new file with mode: 0755]
md_macros.awk [new file with mode: 0755]
multipart.sh [new file with mode: 0755]
pages/#attachments/favicon.ico [new file with mode: 0644]
pages/#page.md [new file with mode: 0644]
pages/[wiki]/#page.md [new file with mode: 0644]
pages/[wiki]/400/#page.md [new file with mode: 0644]
pages/[wiki]/403/#page.md [new file with mode: 0644]
pages/[wiki]/404/#page.md [new file with mode: 0644]
pages/[wiki]/409/#page.md [new file with mode: 0644]
pages/[wiki]/500/#page.md [new file with mode: 0644]
pages/[wiki]/footer/#page.md [new file with mode: 0644]
pages/[wiki]/header/#page.md [new file with mode: 0644]
pages/[wiki]/invite/#page.md [new file with mode: 0644]
pages/[wiki]/login/#page.md [new file with mode: 0644]
pages/[wiki]/register/#page.md [new file with mode: 0644]
pages/[wiki]/settings/#page.md [new file with mode: 0644]
session_lock.sh [new file with mode: 0755]
themes/default.css [new file with mode: 0644]
themes/default.sh [new file with mode: 0755]
tools.sh [new file with mode: 0755]

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/acl.sh b/acl.sh
new file mode 100755 (executable)
index 0000000..0ba6663
--- /dev/null
+++ b/acl.sh
@@ -0,0 +1,122 @@
+#!/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:-${PATH_INFO}}"
+  # Get directory part of PATH_INFO
+  local path="${path%/*}/./"
+  local pagefile head acl
+
+  if [ "$acl_cachepath" = "$path" ]; then
+    printf '%s\n' "$ACL_OVERRIDE" "$acl_collection" "$ACL_DEFAULT"
+    return 0
+  else
+    acl_cachepath="$path"
+    acl_collection=''
+  fi
+
+  printf '%s\n' "$ACL_OVERRIDE"
+
+  while :; do
+    [ "$path" = / ] && break
+    path="${path%/*/}/"
+
+    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}"
+    acl_collection="${acl_collection}${acl}${BR}"
+  done
+
+  printf '%s\n' "$ACL_DEFAULT"
+}
+
+acl_read(){
+  local page="${1:-${PATH_INFO}}"
+  local acl
+
+  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_collect "$page")
+       EOF
+  return 1
+}
+
+acl_write(){
+  local page="${1:-${PATH_INFO}}"
+  local acl
+
+  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_collect "$page")
+       EOF
+  return 1
+}
similarity index 100%
rename from .gitignore
rename to cgilite/.gitignore
similarity index 100%
rename from cgilite.sh
rename to cgilite/cgilite.sh
similarity index 100%
rename from common.css
rename to cgilite/common.css
similarity index 100%
rename from file.sh
rename to cgilite/file.sh
similarity index 100%
rename from html-sh.sed
rename to cgilite/html-sh.sed
similarity index 100%
rename from logging.sh
rename to cgilite/logging.sh
similarity index 100%
rename from markdown.awk
rename to cgilite/markdown.awk
similarity index 100%
rename from session.sh
rename to cgilite/session.sh
similarity index 100%
rename from storage.sh
rename to cgilite/storage.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 (executable)
index 0000000..2efc07b
--- /dev/null
@@ -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_page.sh b/handlers/10_page.sh
new file mode 100755 (executable)
index 0000000..87af685
--- /dev/null
@@ -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.${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/20_account.sh b/handlers/20_account.sh
new file mode 100755 (executable)
index 0000000..c4982fd
--- /dev/null
@@ -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/20_attachment.sh b/handlers/20_attachment.sh
new file mode 100755 (executable)
index 0000000..1e07190
--- /dev/null
@@ -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/20_edit_attachment.sh b/handlers/20_edit_attachment.sh
new file mode 100755 (executable)
index 0000000..97a4b9d
--- /dev/null
@@ -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
+       <h1 class="rename partial">Some files could not be renamed</h1>
+       <h2 class="rename success">Successfully renamed:</h2>
+       <ul class="rename success">
+       $(printf %s "$success" |while read html; do
+         printf '<li><span class=from>%s</span> -&gt; <span class=to>%s</span></li>' \
+                "${html%%/*}" "${html##*/}"
+       done)
+       </ul>
+       <h2 class="rename fail">Errors:</h2>
+       <ul class="rename fail">
+       $(printf %s "$fail" |while read html; do
+         printf '<li><span class=from>%s</span> -&gt; <span class=to>%s</span></li>' \
+                "${html%%/*}" "${html##*/}"
+       done)
+       </ul>
+       <a class="button rename fail" href="[attachment]">OK</a>
+       EOF
+    exit 0
+
+  elif [ "$fail" ]; then
+    printf "%s\r\n" "Status: 500 Internal Server Error"
+    theme_page - "Attachment rename" <<-EOF
+       <h1 class="rename fail">Files could not be renamed</h1>
+       <ul class="rename fail">
+       $(printf %s "$fail" |while read html; do
+         printf '<li><span class=from>%s</span> -&gt; <span class=to>%s</span></li>' \
+                "${html%%/*}" "${html##*/}"
+       done)
+       </ul>
+       <a class="button rename fail" href="[attachment]">OK</a>
+       EOF
+    exit 0
+
+  elif [ "$success" ]; then
+    printf 'Refresh: %i\r\n' 4
+    theme_page - "Attachment rename" <<-EOF
+       <h1 class="rename success">Files were renamed</h1>
+       <ul class="rename success">
+       $(printf %s "$success" |while read html; do
+         printf '<li><span class=from>%s</span> -&gt; <span class=to>%s</span></li>' \
+                "${html%%/*}" "${html##*/}"
+       done)
+       </ul>
+       <a class="button rename success" href="[attachment]">OK</a>
+       EOF
+    exit 0
+
+  else
+    REDIRECT "${_BASE}${PATH_INFO}"
+
+  fi
+fi
+
+return 1
diff --git a/handlers/20_revision.sh b/handlers/20_revision.sh
new file mode 100755 (executable)
index 0000000..08350c7
--- /dev/null
@@ -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 '<article class="revision">'; md; printf '</article>'; } \
+      | theme_page - "${page##*/}"
+    )
+    fi
+    return 0
+    ;;
+esac
+
+return 1
diff --git a/handlers/30_edit.sh b/handlers/30_edit.sh
new file mode 100755 (executable)
index 0000000..6d4e1ea
--- /dev/null
@@ -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 [ "$(which git)" ]; 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/30_move_rename_delete.sh b/handlers/30_move_rename_delete.sh
new file mode 100755 (executable)
index 0000000..be84ca8
--- /dev/null
@@ -0,0 +1,161 @@
+#!/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
+       <article>
+         <p class=error>
+         <h1>Immutable Page</h1>
+         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.
+         </p>
+       </article>
+       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
+       <form method=POST id=movepage>
+         <input type=hidden name=session_id value="$SESSION_KEY">
+         <h1>Move Page</h1>
+         <p class="pageid">$(HTML "${page}")</p>
+         <input name="newlocation" value="$(HTML "$location")" placeholder="New Location">
+         <ul>
+           <li>A page with the same name must not already exist at the new location.</li>
+           <li>You must have permission to create new pages at this location.</li>
+           <li>All subpages will become available under the new path name.</li>
+           <li>Subpages will become unavailable under their current name.</li>
+         </ul>
+         <button type=submit name=action value=move>Move</button>
+         <button type=submit name=action value=cancel>Cancel</button>
+       </form>
+       EOF
+      return 0
+    ;;
+  */\[rename\])
+      name="${page%/}" name="${name##*/}"
+      theme_page - <<-EOF
+       <form method=POST id=renamepage>
+         <input type=hidden name=session_id value="$SESSION_KEY">
+         <h1>Rename Page</h1>
+         <p class="pageid">$(HTML "${page}")</p>
+         <input name="newname" value="$(HTML "$name")" placeholder="New Name">
+         <ul>
+           <li>A page with the new name must not already exist.</li>
+           <li>You must have permission to create new pages at this location.</li>
+           <li>All subpages will become available under the new path name.</li>
+           <li>Subpages will become unavailable under their current name.</li>
+         </ul>
+         <button type=submit name=action value=rename>Rename</button>
+         <button type=submit name=action value=cancel>Cancel</button>
+       </form>
+       EOF
+      return 0
+    ;;
+  */\[delete\])
+      theme_page - <<-EOF
+       <form method=POST id=deletepage>
+         <input type=hidden name=session_id value="$SESSION_KEY">
+         <h1>Delete Page</h1>
+         <p class="pageid">$(HTML "${page}")</p>
+         <p>This page and its attachments will be deleted</p>
+         <ul>
+           <li>Past revisions of the page text (including the current one) will remain accessible and can be restored.</li>
+           <li>Attachments will be deleted completely, and cannot be restored.</li>
+           <li>Subpages will not be affected and can still be accessed normally.</li>
+         </ul>
+         <button type=submit name=action value=delete>Delete</button>
+         <button type=submit name=action value=cancel>Cancel</button>
+       </form>
+       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
+  else
+    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}"
+  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
+  else
+    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}"
+  fi
+elif [ "$action" = delete ]; then
+  oldname="${PATH_INFO%\[*\]}"
+  if ! acl_write "$oldname"; then
+    printf 'Refresh: %i\r\n' 4
+    theme_error 403
+    return 0
+  else
+    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 ./
+  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/30_newpage.sh b/handlers/30_newpage.sh
new file mode 100755 (executable)
index 0000000..f3cbde7
--- /dev/null
@@ -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 (executable)
index 0000000..1f11cf4
--- /dev/null
@@ -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 (executable)
index 0000000..db4d749
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+. "${_EXEC:-${0%/*}}/cgilite/cgilite.sh"
+. "${_EXEC}/cgilite/session.sh"
+. "${_EXEC}/cgilite/users.sh"
+. "${_EXEC}/tools.sh"
+. "${_EXEC}/acl.sh"
+
+REV_ATTACHMENTS=${REV_ATTACHMENTS:-false}
+WIKI_THEME="${WIKI_THEME:-default}"
+. "${_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 [ "$(which git)" -a ! -f "$_DATA/.gitignore" ]; then
+  cat >"$_DATA/.gitignore" <<-EOF
+       users.db
+       serverkey
+       $([ "$REV_ATTACHMENTS" = true ] || printf %s "**/#attachments/")
+       **/#cache/
+       **/#page.lock
+       **/#page.*.cache
+       **/#page.*.cache.*
+       EOF
+  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/macros/attachments b/macros/attachments
new file mode 100755 (executable)
index 0000000..bcd6722
--- /dev/null
@@ -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 '<ul class="macro attachment list">'
+
+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 '<li><a class=name href="[attachment]/%s">%s</a>
+          <span class=size>%s</span><span class=date>%s</span></li>' \
+    "$(HTML "${file##*/}")" "$(HTML "${file##*/}")" \
+    "$(size_human "$size")" "$(date -d @"$date" +"%F %T")"
+done
+
+printf %s\\n '</ul>'
diff --git a/macros/errormessage b/macros/errormessage
new file mode 100755 (executable)
index 0000000..c95688a
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+
+if [ "$1" -o "$ERROR_MSG" ]; then
+  printf '<p .error>%s</p>' "$(HTML "${1:-${ERROR_MSG}}")"
+fi
diff --git a/macros/include b/macros/include
new file mode 100755 (executable)
index 0000000..5254a26
--- /dev/null
@@ -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 '<div class="macro include">
+               <a class="include link" href="%s">%s</a>
+               <article class="include" id="include_%s">' \
+            "${hglob}" "${hglob}" "${hglob}" \
+  || printf '<div class="macro include">
+               <article class="include" id="include_%s">' \
+            "${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 '</article></div>'
+done
diff --git a/macros/newpage b/macros/newpage
new file mode 100755 (executable)
index 0000000..2502b00
--- /dev/null
@@ -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
+       <form class="macro newpage" method=POST action="[newpage]">
+         <input type=hidden name=pattern value="$(HTML "$pattern")">
+         <input type=hidden name=template value="$(HTML "$template")">
+         $([ ! "${pattern##*%%s*}" ] \
+           && printf '<input name=page placeholder="page name">'
+         )<button type=submit name=action value=newpage>$(HTML "$label")</button>
+       </form>
+       EOF
+fi
diff --git a/macros/pagelist b/macros/pagelist
new file mode 100755 (executable)
index 0000000..54d4a69
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/acl.sh"
+. "$_EXEC/tools.sh"
+
+dir="${1:-./}"
+depth="${2:-1}"
+
+printf '<ul class="pagelist">\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 '<li><a href="%s">%s</a></li>' "$(HTML "$page")" "$(HTML "$page")"
+  done
+printf '</ul>\n'
diff --git a/macros/reflink b/macros/reflink
new file mode 100755 (executable)
index 0000000..1b38c15
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+
+title="$(HTML "$1")"
+ref="$(HEADER Referer)"
+
+printf '<a class=reflink href="%s">%s</a>' "${ref:-./}" "${title:-Return}"
diff --git a/macros/revisions b/macros/revisions
new file mode 100755 (executable)
index 0000000..9a5730f
--- /dev/null
@@ -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 [ ! "$(which git)" ]; then
+  printf '<div class="macro revisions error">GIT is not available to handle revisioning.</div>'
+fi
+
+printf '<ul class="macro revisions">\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 '<li><span class="hash"><a href="%s">%s</a></span><span class="date">%s</span><span class="user">%s</span></li>\n' \
+      "$(HTML "${page%/}/[revision]/$hash")" "$(HTML "$hash")" "$(HTML "$date")" "$(HTML "$user")"
+  done
+printf '</ul>\n'
diff --git a/macros/toc b/macros/toc
new file mode 100755 (executable)
index 0000000..f3a6358
--- /dev/null
@@ -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<ul class="macro toc">
+  s;^<(h[1-6]) id="([^"]*)">(([^<]|<[^aA]|<[aA][^ ])+)(<a class="anchor" href="[^>]*"></a>)?</h[1-6]>$;<li class="toc \1"><a href="#\2">\3</a></li>;p
+  $i</ul>
+'
diff --git a/macros/wikiform b/macros/wikiform
new file mode 100755 (executable)
index 0000000..ecb003b
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+action="$1"
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/cgilite/users.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 (executable)
index 0000000..88e662f
--- /dev/null
@@ -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 (executable)
index 0000000..09e3ca8
--- /dev/null
@@ -0,0 +1,90 @@
+#!/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
+        sed -n "/--${multipart_boundary}\(--\)\?${CR}/q; p;" \
+        | head -c-2
+        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 (file)
index 0000000..e69de29
diff --git a/pages/#page.md b/pages/#page.md
new file mode 100644 (file)
index 0000000..4b72bae
--- /dev/null
@@ -0,0 +1,5 @@
+%acl   Known:read,write
+       All:read,write
+
+It Works!
+=========
diff --git a/pages/[wiki]/#page.md b/pages/[wiki]/#page.md
new file mode 100644 (file)
index 0000000..c0a7a04
--- /dev/null
@@ -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.
+
+<<pagelist *>>
diff --git a/pages/[wiki]/400/#page.md b/pages/[wiki]/400/#page.md
new file mode 100644 (file)
index 0000000..94ba677
--- /dev/null
@@ -0,0 +1,9 @@
+%nocache
+
+400
+===
+
+**Bad Request**  
+<<errormessage>>
+
+<<reflink>>
diff --git a/pages/[wiki]/403/#page.md b/pages/[wiki]/403/#page.md
new file mode 100644 (file)
index 0000000..d15309b
--- /dev/null
@@ -0,0 +1,8 @@
+%nocache
+
+403
+===
+
+**Forbidden**
+
+<<reflink>>
diff --git a/pages/[wiki]/404/#page.md b/pages/[wiki]/404/#page.md
new file mode 100644 (file)
index 0000000..f54bad5
--- /dev/null
@@ -0,0 +1,8 @@
+%nocache
+
+404
+===
+
+**page not found**
+
+<<reflink>>
diff --git a/pages/[wiki]/409/#page.md b/pages/[wiki]/409/#page.md
new file mode 100644 (file)
index 0000000..0727e7c
--- /dev/null
@@ -0,0 +1,9 @@
+%nocache
+
+409
+===
+
+**Conflict**  
+<<errormessage>>
+
+<<reflink>>
diff --git a/pages/[wiki]/500/#page.md b/pages/[wiki]/500/#page.md
new file mode 100644 (file)
index 0000000..0d56e0a
--- /dev/null
@@ -0,0 +1,9 @@
+%nocache
+
+500
+===
+
+**Internal Server Error**  
+<<errormessage>>
+
+<<reflink>>
diff --git a/pages/[wiki]/footer/#page.md b/pages/[wiki]/footer/#page.md
new file mode 100644 (file)
index 0000000..205e8f8
--- /dev/null
@@ -0,0 +1,6 @@
+::: { .menu }
+ * <<wikiform login>>
+ * [Register](./[register])
+ * [Invite](./[invite])
+:::
+Edit the Footer [here](/[wiki]/footer/[edit])
diff --git a/pages/[wiki]/header/#page.md b/pages/[wiki]/header/#page.md
new file mode 100644 (file)
index 0000000..8624c9b
--- /dev/null
@@ -0,0 +1,11 @@
+[Shellwiki](/)
+--------------
+
+::: { .menu }
+ * [Help](/[wiki]/)
+ * [Edit Style](/[wiki]/)
+:::
+
+Edit the Header [here](/[wiki]/header/[edit])
+
+----
diff --git a/pages/[wiki]/invite/#page.md b/pages/[wiki]/invite/#page.md
new file mode 100644 (file)
index 0000000..114b208
--- /dev/null
@@ -0,0 +1,7 @@
+%nocache
+
+Set up an account
+-----------------
+<<wikiform invite>>
+
+[Return](./)
diff --git a/pages/[wiki]/login/#page.md b/pages/[wiki]/login/#page.md
new file mode 100644 (file)
index 0000000..ac715ae
--- /dev/null
@@ -0,0 +1,8 @@
+%nocache
+
+Login
+-----
+<<wikiform login>>  
+[Account registration]([register] "Sign up for a new user account")
+
+[Return](./)
diff --git a/pages/[wiki]/register/#page.md b/pages/[wiki]/register/#page.md
new file mode 100644 (file)
index 0000000..b88149c
--- /dev/null
@@ -0,0 +1,7 @@
+%nocache
+
+Set up an account
+-----------------
+<<wikiform register>>
+
+[Return](./)
diff --git a/pages/[wiki]/settings/#page.md b/pages/[wiki]/settings/#page.md
new file mode 100644 (file)
index 0000000..9dd280f
--- /dev/null
@@ -0,0 +1,7 @@
+%nocache
+
+Change Your Password
+-----------------
+<<wikiform settings>>
+
+[Return](./)
diff --git a/session_lock.sh b/session_lock.sh
new file mode 100755 (executable)
index 0000000..21c9808
--- /dev/null
@@ -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 (file)
index 0000000..52179a6
--- /dev/null
@@ -0,0 +1,206 @@
+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: 70vh;
+  font-family: monospace;
+  font-size: inherit;
+}
+
+/* === 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; }
+
diff --git a/themes/default.sh b/themes/default.sh
new file mode 100755 (executable)
index 0000000..7c31003
--- /dev/null
@@ -0,0 +1,179 @@
+#!/bin/sh
+
+. "$_EXEC/tools.sh"
+
+theme_head(){
+  local IFS="$BR"
+  printf '
+  <meta name="viewport" content="width=device-width">
+  '
+  for css in "$_BASE/%5B.%5D/cgilite/common.css" "$_BASE/%5B.%5D/themes/default.css" $PAGE_CSS; do
+    printf  '<link rel="stylesheet" type="text/css" href="%s">' \
+            "$(HTML "${css##*//}")"
+  done
+}
+
+theme_header(){
+  printf '<header>%s</header>' "$(wiki '[wiki]/header/')"
+}
+
+theme_footer(){
+  printf '<footer>%s</footer>' "$(wiki '[wiki]/footer/')"
+}
+
+theme_pagemenu(){
+  local page="$1"
+
+  if acl_write "$page"; then
+    printf  '
+    <ul class="pagemenu">
+      <li><a href="./%%5Bview%%5D">View</a></li>
+      <li><a href="./%%5Bedit%%5D">Edit</a></li>
+      <li><a href="./%%5Battachment%%5D">Attachments</a></li>
+      <li><a href="./%%5Brevision%%5D">Revisions</a></li>
+      <li><a href="./%%5Brename%%5D">Rename</a></li>
+      <li><a href="./%%5Bmove%%5D">Move</a></li>
+      <li><a href="./%%5Bdelete%%5D">Delete</a></li>
+    </ul>'
+  fi
+}
+
+theme_page(){
+  local page="$1" title="$2"
+  title="$(HTML "${title:-"${page}"}")"
+
+  # Important! Web Server response including newline
+  printf "%s\r\n" "Content-Type: text/html; charset=utf-8" ""
+
+  cat <<-EOF
+       <!DOCTYPE HTML>
+       <html><head>
+         $(theme_head)
+         <title>${title}</title>
+       </head><body id="$(HTML "${PATH_INFO}")">
+         $(theme_header)
+         <main>
+           $(theme_pagemenu)
+           $(if [ "$page" = '-' ]; then
+               cat
+             else
+               printf '<article>'
+               wiki "$page"
+               printf '</article>'
+           fi)
+         </main>
+         $(theme_footer)
+       </body></html>
+       EOF
+}
+
+theme_editor(){
+  local page="$1" template="$2" title
+  title="${page%/}"; title="${title##*/}"
+
+  [ "$template" ] && acl_read "$template" || template="$page"
+
+  theme_page - "Editor: $title" <<-EOF
+       <form method=POST action="$(HTML "${_BASE}${page%/}/[edit]")">
+         <input type=hidden name=session_key value="${SESSION_KEY}"/>
+         <textarea name=pagetext>$({ wiki_text "$page" \
+                                  || wiki_text "$template"; } |HTML)</textarea>
+         <button type=submit name=action value=update>Update</button>
+         <button type=submit name=action value=cancel>Cancel</button>
+       </form>
+       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
+       <form class=upload method=POST enctype="multipart/form-data">
+         <h2>Upload</h2>
+         <input type=hidden name=session_id value="$SESSION_ID">
+         <input type=file name=file multiple>
+         <button type=submit name=action value=upload>Upload</button>
+       </form>
+       
+       <form method=POST>
+         <h2>Attachments</h2>
+         <input type=hidden name=session_key value="$SESSION_KEY">
+          <input checked type=radio class=tab name=attachaction value=view id=tview><label for=tview>View</label>
+          <input         type=radio class=tab name=attachaction value=del  id=tdel ><label for=tdel >Delete</label>
+          <input         type=radio class=tab name=attachaction value=move id=tmove><label for=tmove>Move</label>
+          <input         type=radio class=tab name=attachaction value=ren  id=tren ><label for=tren >Rename</label>
+          <div class="tab">
+           <ul class="attachment list">
+       $(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 '<li><input id="select_%s" type=checkbox name=select value="%s">
+                   <label class=name for="select_%s">%s</label>
+                   <input class=name name="rename_%s" value="" placeholder="%s" />
+                   <a class=name href="[attachment]/%s">%s</a>
+                   <span class=size>%s</span>
+                   <span class=date>%s</span>
+                 </li>' \
+                "$hfile" "$hfile" "$hfile" "$hfile" \
+                "$(slopecode "${file##*/}")" "$hfile" "$hfile" "$hfile" \
+                "$(size_human "$size")" "$(date -d @"$date" +"%F %T")"
+       done)
+           </ul>
+           <button type=submit name=action value=delete>Delete</button>
+           <label for=moveto>Move To:</label>
+           <input id="moveto" name="moveto" value="$(HTML "$page")" placeholder="page name">
+           <button type=submit name=action value=move  >Move</button>
+           <button type=submit name=action value=rename>Rename</button>
+         </div>
+       </form>
+       EOF
+  else
+    theme_page - "Attachments: $title" <<-EOF
+       <ul class="attachment list">
+       $(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 '<li><a class=name href="%s">%s</a>
+                 <span class=size>%s</span><span class=date>%s</span></li>' \
+                "$hfile" "$hfile" "$(size_human "$size")" "$(date -d @"$date" +"%F %T")"
+       done)
+       </ul>
+       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 (executable)
index 0000000..c5545fb
--- /dev/null
+++ b/tools.sh
@@ -0,0 +1,106 @@
+#!/bin/sh
+
+[ "$include_tools" ] && return 0
+include_tools="$0"
+
+if [ "$(which awk)" ]; then
+  md() { awk -f "$_EXEC/md_macros.awk" -f "$_EXEC/cgilite/markdown.awk"; }
+elif [ "$(which busybox)" ]; then
+  md() { busybox awk -f "$_EXEC/md_macros.awk" -f "$_EXEC/cgilite/markdown.awk"; }
+else
+  md() { cat; }
+fi
+
+mdfile(){
+  local page="$(PATH "$1")"
+
+  if   [ -f "$_DATA/pages/$page/#page.md" ]; then
+    printf %s\\n "$_DATA/pages/$page/#page.md"
+  elif [ -f "$_EXEC/pages/$page/#page.md" ]; then
+    printf %s\\n "$_EXEC/pages/$page/#page.md"
+  else
+    return 1
+  fi
+}
+
+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
+}
+
+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
+}