]> git.plutz.net Git - shellwiki/commitdiff
Merge commit 'a0e80b53ee3e393907040ce06b0d7fb82d8e0eec'
authorPaul Hänsch <paul@plutz.net>
Tue, 10 May 2022 22:24:28 +0000 (00:24 +0200)
committerPaul Hänsch <paul@plutz.net>
Tue, 10 May 2022 22:24:28 +0000 (00:24 +0200)
32 files changed:
Makefile [new file with mode: 0644]
acl.sh [new file with mode: 0755]
attachment.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]
index.cgi [new file with mode: 0755]
macros/attachments [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: 0644]
page_edit.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]/403/#page.md [new file with mode: 0644]
pages/[wiki]/404/#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]/login/#page.md [new file with mode: 0644]
pages/[wiki]/register/#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: 0644]

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..74fc8dc
--- /dev/null
+++ b/acl.sh
@@ -0,0 +1,114 @@
+#!/bin/sh
+
+# ACL_OVERRIDE="${ACL_OVERRIDE:-Admin:read,write}"
+ACL_DEFAULT="${ACL_DEFAULT:-All:read${BR}Known:read,write}"
+
+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
+
+    n=20; while read -r head acl; do
+      if [ "$head" = "%acl" ]; then
+        acl_collection="${acl%${CR}}${BR}"
+        printf "%s\n" "${acl%${CR}}"
+        n=$((n+1))
+      fi
+
+      n="$((n - 1))"
+      [ "$n" -eq 0 ] && break
+    done <"$pagefile"
+  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
+}
diff --git a/attachment.sh b/attachment.sh
new file mode 100755 (executable)
index 0000000..43e70c5
--- /dev/null
@@ -0,0 +1,116 @@
+#!/bin/sh
+
+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
+    ;;
+  esac
+}
+
+if [ "${PATH_INFO%/\[attachment\]/}"  != "${PATH_INFO}" ]; then
+  . "$_EXEC/multipart.sh"
+
+  if [ "${CONTENT_TYPE%%;*}" = "multipart/form-data" ] && acl_write "${PATH_INFO%\[attachment\]/}"; then
+    multipart_cache
+    mkdir -p "$_DATA/pages/${PATH_INFO%/\[attachment\]/}/#attachments/"
+    n=1; while filename=$(multipart_filename "file" "$n"); do
+      filename="$(printf %s "$filename" |tr /\\0 __)"
+      multipart "file" "$n" >"$_DATA/pages/${PATH_INFO%/\[attachment\]/}/#attachments/$filename"
+      n=$((n + 1))
+    done
+    rm -- "$multipart_cachefile"
+    REDIRECT "${_BASE}${PATH_INFO}"
+  elif [ "${CONTENT_TYPE%%;*}" = "multipart/form-data" ]; then
+    theme_403
+  elif acl_read "${PATH_INFO%\[attachment\]/}"; then
+    theme_attachments "${PATH_INFO%\[attachment\]/}"
+  else
+    theme_404
+  fi
+
+elif [ "${PATH_INFO%/\[attachment\]/*}" != "${PATH_INFO}" ]; then
+  attpath="${PATH_INFO%/\[attachment\]/*}/#attachments/${PATH_INFO##*/}"
+
+  if ! acl_read "${PATH_INFO%/\[attachment\]/*}"; then
+    theme_403
+  elif [ -f "$_DATA/pages/$attpath" ]; then
+    FILE "$_DATA/pages/$attpath"
+  elif [ -f "$_EXEC/pages/$attpath" ]; then
+    FILE "$_EXEC/pages/$attpath"
+  else
+    theme_404
+  fi
+  exit 0;
+  
+elif [ "${PATH_INFO%/}" = "${PATH_INFO}" ]; then
+  attpath="${PATH_INFO%/*}/#attachments/${PATH_INFO##*/}"
+
+  if ! acl_read "${PATH_INFO%/*}/"; then
+    theme_403
+  elif [ -f "$_DATA/pages/$attpath" ]; then
+    FILE "$(attachment_convert "$_DATA/pages/$attpath")"
+  elif [ -f "$_EXEC/pages/$attpath" ]; then
+    FILE "$(attachment_convert "$_EXEC/pages/$attpath")"
+  elif [ -d "$_DATA/pages/${PATH_INFO}" -o -d "$_EXEC/pages/${PATH_INFO}" ]; then
+    REDIRECT "${_BASE}${PATH_INFO}/"
+  else
+    theme_404
+  fi
+  exit 0
+
+fi
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/index.cgi b/index.cgi
new file mode 100755 (executable)
index 0000000..90f7d77
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+. "${_EXEC:-${0%/*}}/cgilite/cgilite.sh"
+. "${_EXEC}/cgilite/session.sh"
+. "${_EXEC}/cgilite/file.sh"
+. "${_EXEC}/cgilite/users.sh"
+. "${_EXEC}/acl.sh"
+
+. "${_EXEC}/themes/default.sh"
+
+CACHE_AGE=${CACHE_AGE:-1800}
+export MD_MACROS="$_EXEC/macros"
+export MD_HTML="${MD_HTML:-false}"
+
+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
+}
+
+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"
+}
+
+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/";
+      sed -E '1,20{ /^%[a-z]+/d; }' "$mdfile" \
+      | md |tee -- "${cache}.$$"
+    )
+    grep -q '^%nocache' "$mdfile" \
+    && rm -- "${cache}.$$" \
+    || mv -- "${cache}.$$" "${cache}"
+  fi
+}
+
+case "${PATH_INFO}" in
+  /"[.]"/*)
+    FILE "${_EXEC}/${PATH_INFO#/\[.\]}"
+    ;;
+  */\[*\]/) :;;
+  */)
+    theme_page "${PATH_INFO}"
+    exit 0
+    ;;
+  */"[login]")
+    theme_login
+    exit 0
+    ;;
+  */"[register]")
+    theme_register
+    exit 0
+    ;;
+  */"#"*)
+    :  # TODO: Invalid page name
+    ;;
+esac
+
+. "$_EXEC/page_edit.sh"
+. "$_EXEC/attachment.sh"
diff --git a/macros/attachments b/macros/attachments
new file mode 100755 (executable)
index 0000000..0a49075
--- /dev/null
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+. "$_EXEC/cgilite/cgilite.sh"
+. "$_EXEC/tools.sh"
+
+page="$1"
+
+if [ "${page#/}" = "$page" ]; then
+  page="$(PATH "${PATH_INFO}/$page")"
+fi
+
+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><span class=name>%s</span>
+          <span class=size>%s</span><span class=date>%s</span></li>' \
+    "$(HTML "${file##*/}")" "$(size_human "$size")" "$(date -d @"$date" +"%F %T")"
+done
+
+printf %s\\n '</ul>'
diff --git a/macros/toc b/macros/toc
new file mode 100755 (executable)
index 0000000..308fced
--- /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="([^"]*)">([^<]+)</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..13ff113
--- /dev/null
@@ -0,0 +1,15 @@
+#!/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"
+    ;;
+esac
diff --git a/md_macros.awk b/md_macros.awk
new file mode 100755 (executable)
index 0000000..4b85a0e
--- /dev/null
@@ -0,0 +1,67 @@
+#!/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) {
+    RS=""; ORS=""; line="";
+    "printf '%s' " sh_escape(file) " | " sh_escape(ENVIRON["MD_MACROS"]) "/" call | getline line;
+    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 100644 (file)
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/page_edit.sh b/page_edit.sh
new file mode 100755 (executable)
index 0000000..b5beeaf
--- /dev/null
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+. "${_EXEC}/themes/default.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
+}
+
+edit_page="${PATH_INFO%\[edit\]}"
+edit_file="$_DATA/pages/$edit_page/#page.md"
+[ "$REQUEST_METHOD" = POST ] && edit_action="$(POST action)"
+
+if [ "$edit_page" = "$PATH_INFO" ]; then
+  unset edit_page edit_action edit_file
+  # END EDIT SCRIPT, continue in index.cgi
+
+elif [ "$edit_action" = update ]; then
+  mkdir -p -- "${edit_file%/#page.md}"
+
+  if S_LOCK "$edit_file"; then
+    POST pagetext >"$edit_file"
+    S_RELEASE "$edit_file"
+    REDIRECT "${_BASE}${PATH_INFO%\[edit\]}"
+  else
+    export ERRMSG="ERR_NOLOCK"
+    REDIRECT "${_BASE}${PATH_INFO%\[edit\]}/[edit]"
+  fi
+
+elif [ "$edit_action" = cancel ]; then
+  S_RELEASE "$edit_file"
+  REDIRECT "${_BASE}${PATH_INFO%\[edit\]}"
+
+elif mkdir -p -- "${edit_file%/#page.md}" && S_LOCK "$edit_file"; then
+  # Display editor page
+  SESSION_COOKIE
+  theme_editor "$edit_page"
+
+else
+  export ERRMSG="ERR_NOLOCK"
+  theme_page "$edit_page"
+
+fi
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..4dd6aeb
--- /dev/null
@@ -0,0 +1,2 @@
+It Works!
+=========
diff --git a/pages/[wiki]/403/#page.md b/pages/[wiki]/403/#page.md
new file mode 100644 (file)
index 0000000..66ea5e2
--- /dev/null
@@ -0,0 +1,4 @@
+403
+===
+
+**Forbidden**
diff --git a/pages/[wiki]/404/#page.md b/pages/[wiki]/404/#page.md
new file mode 100644 (file)
index 0000000..7596bdd
--- /dev/null
@@ -0,0 +1,4 @@
+404
+===
+
+**page not found**
diff --git a/pages/[wiki]/footer/#page.md b/pages/[wiki]/footer/#page.md
new file mode 100644 (file)
index 0000000..da2b20f
--- /dev/null
@@ -0,0 +1,3 @@
+----
+Shellwiki  
+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..7d50e32
--- /dev/null
@@ -0,0 +1,10 @@
+# Shellwiki
+
+::: { .menu }
+ * [Login]([login])
+ * [Register]([register])
+:::
+
+Edit the Header [here](/[wiki]/header/[edit])
+
+----
diff --git a/pages/[wiki]/login/#page.md b/pages/[wiki]/login/#page.md
new file mode 100644 (file)
index 0000000..2e4f544
--- /dev/null
@@ -0,0 +1,4 @@
+Login
+-----
+<<wikiform login>>  
+[Account registration]([register] "Sign up for a new user account")
diff --git a/pages/[wiki]/register/#page.md b/pages/[wiki]/register/#page.md
new file mode 100644 (file)
index 0000000..d6ba4e4
--- /dev/null
@@ -0,0 +1,3 @@
+Set up an account
+-----------------
+<<wikiform register>>
diff --git a/session_lock.sh b/session_lock.sh
new file mode 100755 (executable)
index 0000000..f55c9cd
--- /dev/null
@@ -0,0 +1,68 @@
+#!/bin/sh
+
+. "$_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..c8db718
--- /dev/null
@@ -0,0 +1,120 @@
+html { min-height: 100%; }
+
+body {
+  position: absolute;
+  width: 100%;
+  min-height: 100%;
+  padding-bottom: 6em;
+  background-color: #EEE;
+}
+
+header, footer {
+  background-color: #FFF;
+  box-shadow: 0 0 .75em;
+  width: 100%;
+  z-index: 1;
+}
+
+footer {
+  position: absolute;
+  bottom: 0;
+}
+
+header :last-child,
+main :last-child {
+  margin-bottom: 0;
+}
+
+header h2,
+header .menu {
+  display: inline-block;
+}
+
+header .menu { list-style: none; }
+
+header .menu li {
+  display: inline-block;
+  margin-right: .5em;
+}
+
+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,
+[id$="/[attachment]/"] main form.upload {
+  margin: 1em;
+  padding: .125em 1em 1em 1em;
+  box-shadow: .25em .25em .75em;
+  background-color: #FFF;
+}
+
+[id$="/[attachment]/"] main .attachment.list {
+  margin: 1em;
+  padding: 1em 2em;
+}
+[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;
+}
+
+
+/* === Editor === */
+
+body.editor textarea {
+  width: 100%;
+  min-height: 20em;
+}
+
+/* === 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;
+}
+
+
+/* === 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..7edd4b9
--- /dev/null
@@ -0,0 +1,192 @@
+#!/bin/sh
+
+. "$_EXEC/tools.sh"
+
+theme_head(){
+  printf '
+  <meta name="viewport" content="width=device-width"/>
+  <link rel="stylesheet" type="text/css" href="%s/[.]/cgilite/common.css">
+  <link rel="stylesheet" type="text/css" href="%s/[.]/themes/default.css">
+  ' "$_BASE"
+}
+
+theme_header(){
+  printf '<header>%s</header>' "$(wiki '[wiki]/header/')"
+}
+
+theme_footer(){
+  printf '<footer>%s</footer>' "$(wiki '[wiki]/footer/' ||echo No footer)"
+}
+
+theme_page(){
+  local page="$1" title
+  title="${page%/}"; title="${title##*/}"
+
+  if [ ! "$(mdfile "$page")" ]; then
+    theme_404
+    return 0
+  elif ! acl_read "$page"; then
+    theme_403
+    return 0
+  fi
+
+  # 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>$(HTML "${title}")</title>
+       </head><body id="$(HTML "$page")">
+         $(theme_header)
+         <main>
+           $(acl_write "$page" && printf %s \
+             '<ul class="pagemenu">
+                <li><a href="[edit]">Edit</a></li>
+                <li><a href="[attachment]/">Attachments</a></li>
+              </ul>'
+           )
+           <article>
+             $(wiki "$page" || printf 'Error while loading page <br> function "wiki" of index.sh returned with an error.')
+           </article>
+         </main>
+         $(theme_footer)
+       </body></html>
+       EOF
+}
+
+theme_editor(){
+  local page="$1" title
+  title="${page%/}"; title="${title##*/}"
+
+  if [ ! "$(mdfile "$page")" ]; then
+    theme_404
+    return 0
+  elif ! acl_write "$page"; then
+    theme_403
+    return 0
+  fi
+
+  # 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>$(HTML "${title}")</title>
+       </head><body id="$(HTML "$page")" class="editor">
+         $(theme_header)
+         <main><form method=POST>
+           <input type=hidden name=session_key value="${SESSION_KEY}"/>
+            <textarea name=pagetext>$(wiki_text "$page" |HTML)</textarea>
+            <button type=submit name=action value=update>Update</button>
+            <button type=submit name=action value=cancel>Cancel</button>
+          </form></main>
+         $(theme_footer)
+       </body></html>
+       EOF
+}
+
+theme_attachments(){
+  local page="$1" title
+  title="${page%/}"; title="${title##*/}"
+
+  if [ ! "$(mdfile "$page")" ]; then
+    theme_404
+    return 0
+  elif ! acl_read "$page"; then
+    theme_403
+    return 0
+  fi
+
+  # Important! Web Server response including newline
+  printf "%s\r\n" "Content-Type: text/html; charset=utf-8" ""
+
+  if acl_write "$page"; then
+    cat <<-EOF
+       <!DOCTYPE HTML>
+       <html><head>
+         $(theme_head)
+         <title>Attachments $(HTML "${title}")</title>
+       </head><body id="$(HTML "$page")[attachment]/">
+         $(theme_header)
+         <main>
+           <form class=upload method=POST enctype="multipart/form-data">
+             <input type=file name=file multiple>
+             <button type=submit name=action value=upload>Upload</button>
+           </form>
+
+            <form method=POST><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#* }"
+           
+             printf '<li><button type=submit name=delete value="%s">Delete</button><a class=name href="%s">%s</a>
+                     <span class=size>%s</span><span class=date>%s</span></li>' \
+               "$(HTML "${file##*/}")" "$(HTML "${file##*/}")" "$(HTML "${file##*/}")" \
+               "$(size_human "$size")" "$(date -d @"$date" +"%F %T")"
+           done)
+            </ul></form>
+         </main>
+         $(theme_footer)
+       </body></html>
+       EOF
+  else
+    cat <<-EOF
+       <!DOCTYPE HTML>
+       <html><head>
+         $(theme_head)
+         <title>Attachments $(HTML "${title}")</title>
+       </head><body id="$(HTML "$page")[attachment]/">
+         $(theme_header)
+         <main>
+            <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#* }"
+           
+             printf '<li><a class=name href="%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)
+            </ul>
+         </main>
+         $(theme_footer)
+       </body></html>
+       EOF
+  fi
+}
+
+theme_login(){
+  theme_page '/[wiki]/login/'
+}
+
+theme_register(){
+  theme_page '/[wiki]/register/'
+}
+
+theme_403(){
+  printf "%s\r\n" "Status: 403 Forbidden"
+
+  if [ "$(mdfile '/[wiki]/403/')" ]; then
+    theme_page '/[wiki]/403/'
+  else
+    printf "Content-Length: 0\r\n\r\n"
+  fi
+}
+
+theme_404(){
+  printf "%s\r\n" "Status: 404 Not Found"
+
+  if [ "$(mdfile '/[wiki]/404/')" ]; then
+    theme_page '/[wiki]/404/'
+  else
+    printf "Content-Length: 0\r\n\r\n"
+  fi
+}
diff --git a/tools.sh b/tools.sh
new file mode 100644 (file)
index 0000000..591e9bc
--- /dev/null
+++ b/tools.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+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
+}