From: Paul Hänsch Date: Wed, 27 Jul 2022 12:01:29 +0000 (+0200) Subject: Merge commit '4f6c45f6bef36f8033bb9f93cfdcff050d3f0b02' X-Git-Url: https://git.plutz.net/?a=commitdiff_plain;h=ba2f64a4c4bd30046d357c51e222674af64b740e;hp=4f6c45f6bef36f8033bb9f93cfdcff050d3f0b02;p=webpoll Merge commit '4f6c45f6bef36f8033bb9f93cfdcff050d3f0b02' --- diff --git a/.gitignore b/.gitignore index 5c9950a..5f74930 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -cgilite serverkey -users.db +[0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=][0-9a-zA-Z:=] +comments/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24781a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: _subtrees + +_subtrees: _cgilite + +cgilite: + git subtree add --squash -P $@ https://git.plutz.net/git/$@ master + +_cgilite: cgilite + git subtree pull --squash -P $< https://git.plutz.net/git/$< master diff --git a/cgilite/.gitignore b/cgilite/.gitignore new file mode 100644 index 0000000..5c9950a --- /dev/null +++ b/cgilite/.gitignore @@ -0,0 +1,3 @@ +cgilite +serverkey +users.db diff --git a/cgilite.sh b/cgilite/cgilite.sh similarity index 100% rename from cgilite.sh rename to cgilite/cgilite.sh diff --git a/common.css b/cgilite/common.css similarity index 100% rename from common.css rename to cgilite/common.css diff --git a/file.sh b/cgilite/file.sh similarity index 100% rename from file.sh rename to cgilite/file.sh diff --git a/html-sh.sed b/cgilite/html-sh.sed similarity index 100% rename from html-sh.sed rename to cgilite/html-sh.sed diff --git a/logging.sh b/cgilite/logging.sh similarity index 100% rename from logging.sh rename to cgilite/logging.sh diff --git a/markdown.awk b/cgilite/markdown.awk similarity index 100% rename from markdown.awk rename to cgilite/markdown.awk diff --git a/session.sh b/cgilite/session.sh similarity index 100% rename from session.sh rename to cgilite/session.sh diff --git a/storage.sh b/cgilite/storage.sh similarity index 100% rename from storage.sh rename to cgilite/storage.sh diff --git a/users.sh b/cgilite/users.sh similarity index 100% rename from users.sh rename to cgilite/users.sh diff --git a/comments.sh b/comments.sh new file mode 100755 index 0000000..90fdbb9 --- /dev/null +++ b/comments.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +. "${_EXEC}/cgilite/session.sh" +. "${_EXEC}/cgilite/storage.sh" + +comments_file="${_DATA}/comments/${PATH_INFO}.db" + +comments_postcomment() { + local cuid="$1" username="$2" text="$3" + local db="$comments_file" + + [ ! "$cuid" -o ! "$username" -o ! "$text" ] \ + && REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_INVALID" + + mkdir -p "${comments_file%/*}" || REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_NOCREAT" + if LOCK "$db"; then + if grep -qE "^${cuid} " "$db"; then + RELEASE "$db" + REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_EXISTS" + else + printf "%s %s %s %s %s\n" \ + "$cuid" "$(STRING "$username")" "$SESSION_ID" "$_DATE" "$(STRING "$text")" \ + >>"$db" + RELEASE "$db" + REDIRECT "${_BASE}${PATH_INFO}#comment_${cuid}" + fi + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_NOLOCK" + fi +} +comments_updatecomment() { + local cuid="$1" updatekey="$2" username="$3" text="$4" + local db="$comments_file" + local ousername sid time otext + + [ ! "$cuid" -o ! "$username" -o ! "$text" ] \ + && REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_INVALID" + + mkdir -p "${comments_file%/*}" || REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_NOCREAT" + if LOCK "$db"; then + read -r cuid ousername sid time otext <<-EOF + $(grep -E "^${cuid} " "$db") + EOF + if [ "$sid" = "$SESSION_ID" -a "$(session_mac "${ousername}|${time}|${otext}")" = "$updatekey" ]; then + sed -Ei "/^${cuid} /d" "$db" + printf "%s %s %s %s %s\n" \ + "$cuid" "$(STRING "$username")" "$SESSION_ID" "${time%,*},$_DATE" "$(STRING "$text")" \ + >>"$db" + RELEASE "$db" + REDIRECT "${_BASE}${PATH_INFO}#comment_${cuid}" + else + RELEASE "$db" + REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_DIVERGE" + fi + else + REDIRECT "${_BASE}${PATH_INFO}#ERROR_COMMENT_NOLOCK" + fi + +} + +[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in + postcomment) comments_postcomment \ + "$(POST cuid |checkid)" \ + "$(POST username |grep -m1 -oE '[^[:space:]].*[^[:space:]]')" \ + "$(POST text)";; + updatecomment) comments_updatecomment \ + "$(POST cuid |checkid)" "$(POST updatekey)" \ + "$(POST username |grep -m1 -oE '[^[:space:]].*[^[:space:]]')" \ + "$(POST text)";; + cancelcommentpost) REDIRECT "${_BASE}${PATH_INFO}#comments";; + cancelcommentedit) REDIRECT "${_BASE}${PATH_INFO}#comment_$(POST cuid |checkid)";; +esac + +w_comments() { + local db="$comments_file" + local edit="$(GET editcomment |checkid)" + local cuid username sid time text + + printf '[section #comments' + [ -f "$db" ] && grep -qE "^${edit} [^ ]+ ${SESSION_ID}" "$db" \ + || cat <<-EOF + [h2 Comments] + [input type=checkbox #comments_toggle_new][label for="comments_toggle_new" Write a Comment] + [form method=POST + [hidden "cuid" "$(timeid)"] + [input name=username placeholder="Your Name" autocomplete=off] + [textarea name=text placeholder="Your Text"] + [label .legend You can use Markdown formatting for the comment text. Comments can be edited up to a short time after they have been posted.] + [submit "action" "cancelcommentpost" Cancel][submit "action" "postcomment" . Post Comment] + ] + EOF + + [ -f "$db" ] && sort -r "$db" \ + | while read -r cuid username sid time text; do + if [ "$edit" = "$cuid" -a "$sid" = "$SESSION_ID" ]; then + printf ' + [form .comment .edit #comment_%s method=POST + [hidden "cuid" "%s"][hidden "updatekey" "%s"] + [input type=text name=username placeholder="Your Name" value="%s" autocomplete=off] + [textarea name=text placeholder="Your Text" . %s] + [submit "action" "cancelcommentedit" Cancel][submit "action" "updatecomment" . Update Comment] + ]' "$cuid" "$cuid" "$(session_mac "${username}|${time}|${text}")" \ + "$(UNSTRING "$username" |HTML)" "$(UNSTRING "$text" |HTML)" + elif [ "$username" -a "$text" ]; then + printf '[div .comment #comment_%s [h3 . %s:]' "$cuid" "$(UNSTRING "$username" |HTML)" + printf '[span .time [label posted] %s]' "$(date -d "@${time%,*}")" + [ "${time}" != "${time%,*}" ] \ + && printf '[span .update [label updated] %s]' "$(date -d "@${time#*,}")" + [ "$edit" = "$cuid" ] \ + && printf '[span .error You cannot edit this comment]' + [ "$sid" = "$SESSION_ID" ] \ + && printf '[a .edit href="?editcomment=%s#comment_%s" edit]' "$cuid" "$cuid" + printf '[div . %s]]' "$(UNSTRING "$text" |markdown)" + else + printf '[div .comment .deleted #comment_%s [h3 (deleted)]]' "$cuid" + fi + done + printf ']' +} diff --git a/home.sh b/home.sh new file mode 100755 index 0000000..b8f8192 --- /dev/null +++ b/home.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +if [ "$REQUEST_METHOD" = POST ]; then + case $(POST start) in + date) + id="$(randomid)" + admin="$(randomid)" + file="$_DATA/$id" + # touch "$file" + DBM "$file" add adminkey "$admin" + REDIRECT "$_BASE/$id/$admin" + ;; + options) + id="$(randomid)" + touch "$_DATA/$id" + REDIRECT "$_BASE/$id/newoptions" + ;; + *) REDIRECT "$_BASE/";; + esac +else + [ "$PATH_INFO" != / ] && printf 'Status: 404 Not Found\r\n' + yield_page "Start a Poll" "home" <<-EOF + [main [form method=post + [submit "start" "date" Start a new poll] + $(if [ "$bookmarks" ]; then + printf '[h2 Recent Polls][ul .recent' + for page in $bookmarks; do + page="${page%/*}" + [ -f "$_DATA/$(checkid "$page")" ] \ + && printf '[li [a href="./%s" . %s]]' "$page" "$(pagename "$page" |HTML)" + done + printf ']' + fi) + ]] + EOF +fi diff --git a/index.cgi b/index.cgi new file mode 100755 index 0000000..060d7dc --- /dev/null +++ b/index.cgi @@ -0,0 +1,64 @@ +#!/bin/sh + +. "${_EXEC:-${0%/*}}"/cgilite/cgilite.sh +. "$_EXEC"/cgilite/session.sh +. "$_EXEC"/cgilite/file.sh +. "$_EXEC"/cgilite/storage.sh +. "$_EXEC"/widgets.sh + +# PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")" + +# bookmarks_cookie="$(COOKIE bookmarks |grep -xE '[0-9a-zA-Z:=]{16}(/[0-9a-zA-Z:=]{16})?( [0-9a-zA-Z:=]{16}(/[0-9a-zA-Z:=]{16})?)*')" +bookmarks='' +for bm in $(COOKIE bookmarks); do + [ -f "${_DATA}/$(checkid "${bm%/*}")" ] && bookmarks="${bookmarks}${bookmarks:+ }${bm}" +done +if [ "$bookmarks" ]; then + SET_COOKIE +$((182 * 86400)) bookmarks="${bm}" Path="${_BASE}/" +fi + +yield_page(){ + title="${1:-Webpoll}" page="$2" + printf '%s\r\n' 'Content-Type: text/html; charset=utf-8' \ + "Content-Security-Policy: script-src 'none'" \ + '' + { printf '[html + [head + [meta name="viewport" content="width=device-width"] + [link rel="stylesheet" type="text/css" href="%s/common.css"] + [link rel="stylesheet" type="text/css" href="%s/widgets.css"] + [link rel="stylesheet" type="text/css" href="%s/webpoll.css"] + [title %s] + ] [body class="%s" + ' "$_BASE" "$_BASE" "$_BASE" "$(HTML "$title")" "$page" + cat + printf '] ]' + } |"$_EXEC/cgilite/html-sh.sed" -u +} + +pagename() { + local id="$1" + local file="$_DATA/$id" + if [ -f "$file" ]; then + DBM "$file" get title || printf 'Unnamed Page' + else + return 1; + fi +} + + page_home() { . "$_EXEC"/home.sh; } +page_newdate() { . "$_EXEC"/newdate.sh; } + page_poll() { . "$_EXEC"/poll.sh; } + +case ${PATH_INFO} in + /favicon.ico) printf '%s\r\n' 'Content-Length: 0' '';; + /common.css) FILE "$_EXEC/cgilite/common.css";; + /widgets.css|/webpoll.css) FILE "${_EXEC}/${PATH_INFO}";; + /) page_home;; + /[0-9a-zA-Z:=]???????????????/[0-9a-zA-Z:=]???????????????) page_newdate;; + /*/newoptions);; + /[0-9a-zA-Z:=]???????????????) page_poll;; + /*) page_home;; +esac + +exit 0 diff --git a/languages.sh b/languages.sh new file mode 100644 index 0000000..c3649f4 --- /dev/null +++ b/languages.sh @@ -0,0 +1,12 @@ +[ -n "$include_languages" ] && return 0 +include_languages="$0" + +language="$(HEADER Accept-Language)"; language="${language%%[;,._-]*}" +case "$language" in + de) export LC_TIME=de_DE.UTF-8 + table_date="%A
%_d. %B %Y" + ;; + *) export LC_TIME=C + table_date="%A
%B %_d, %Y" + ;; +esac diff --git a/newdate.sh b/newdate.sh new file mode 100755 index 0000000..83e27b6 --- /dev/null +++ b/newdate.sh @@ -0,0 +1,187 @@ +#!/bin/sh + +id="${PATH_INFO%/*}"; id="${id#/}" +admin=${PATH_INFO##*/} +file="$_DATA/$id" + +if [ "$(DBM "$file" get adminkey)" != "$admin" ]; then + page_home; + return 0 + # REDIRECT "$_BASE/#ERRO_INVALIDKEY" +elif expr match "$bookmarks" ".*${id}/${admin}.*" >/dev/null; then + : +elif expr match "$bookmarks" ".*${id}.*" >/dev/null; then + bookmarks="$(printf %s "$bookmarks" |tr \ \\n |grep -vxF "${id}")" + bookmarks="$(printf %s "$bookmarks" |tr \\n \ )" + SET_COOKIE +$((182 * 86400)) bookmarks="${bookmarks}${bookmarks:+ }${id}/${admin}" Path="${_BASE}/" +elif [ "$bookmarks" ]; then + SET_COOKIE +$((182 * 86400)) bookmarks="${bookmarks} ${id}/${admin}" Path="${_BASE}/" +fi + +fs_timeofday() { + local todall="$(DBM "$file" get todall)" time c=0 + cat <<-EOF + [fieldset .timeofday + [label .todstart Start Time (optional): + ] + [label .todend End Time (optional): + ] + $(for time in ${todall:--}; do + c=$((c + 1)) + printf ' + + + [submit "todremove" "%i" -] + ' "${time%-*}" "${time#*-}" "${c}" + done) + [submit "addtime" "global" + Add Time Option] + [checkbox "none" "none" .splittimes disabled=disabled] [submit "splittimes" "yes" Separate Time Options per Day] + ] + EOF +} + +fs_splittimes() { + local days day times time c + days="$(DBM "$file" get dates)" + cat <<-EOF + [fieldset .splittimes + [checkbox "none" "none" .splittimes checked disabled=disabled] [submit "splittimes" "no" Separate Time Options per Day] + $([ ! "$days" ] && printf '[p You have not selected any days yet.]\n') + $(for day in $days; do + date -d $day +"[h2 . %A - %B %_d, %Y]" + times=$(DBM "$file" get "tod_$day") + for time in ${times:--}; do + c=$((c + 1)) + printf ' + + + [submit "todremove_%s" "%i" -] + ' "$day" "${time%-*}" "$day" "${time#*-}" "$day" "${c}" + done + printf '[submit "addtime" "%s" + Add Time Option]' "$day" + done) + ] + EOF +} + +if [ "$REQUEST_METHOD" = POST ]; then + month="$(POST month |grep -m 1 -xE '[0-9]{4}-(0[1-9]|1[012])')" + todremove="$(POST todremove |grep -m 1 -xE '[0-9]+')" + splittimes="$(POST splittimes |grep -m 1 -xE 'yes|no')" + addtime="$(POST addtime)" + + if [ "$splittimes" = yes ]; then + DBM "$file" set splittimes "$splittimes" + splittimes="no" # receive remainder of todall form + elif [ "$splittimes" = no ]; then + DBM "$file" set splittimes "$splittimes" + splittimes="yes" # receive remainder of splittimes form + else + splittimes="$(DBM "$file" get splittimes || printf no)" + fi + + DBM "$file" set title "$(POST title)" + DBM "$file" set description "$(POST description)" + # Store common time options "todall" + [ "$splittimes" = no ] && DBM "$file" set todall "$( + for todcount in $(seq 1 $(POST_COUNT todstart)); do + [ "$todremove" -eq "$todcount" ] 2>&- && continue; + todstart="$(POST todstart "$todcount")" + todend="$(POST todend "$todcount")" + [ "${todstart%:??}" -lt "${todend%:??}" -o "${todstart%:??}" -eq "${todend%:??}" -a "${todstart#*:}" -lt "${todend#*:}" ] \ + 2>&- \ + && { printf '%02i:%02i-%02i:%02i\n' "${todstart%:??}" "${todstart#*:}" "${todend%:??}" "${todend#*:}"; }\ + || { [ "${todstart%:??}" -ge 0 -a "${todstart#*:}" -ge 0 ] 2>&- && printf '%02i:%02i-\n' "${todstart%:??}" "${todstart#*:}"; } + done |grep -xE '^([01][0-9]|2[0-3]):([0-5][0-9])-(([01][0-9]|2[0-3]):([0-5][0-9]))?$' |sort -u + )" + [ "$addtime" = global ] && DBM "$file" append todall "${BR}-" + + # Store per-date time options "tod_YYYY-mm-dd" + [ "$splittimes" = yes ] && for date in $(DBM "$file" get dates); do + todremove="$(POST todremove_$date |grep -m 1 -xE '[0-9]+')" + DBM "$file" set "tod_$date" "$( + for todcount in $(seq 1 $(POST_COUNT "todstart_${date}")); do + [ "$todremove" -eq "$todcount" ] 2>&- && continue; + todstart="$(POST "todstart_${date}" "$todcount")" + todend="$(POST "todend_${date}" "$todcount")" + [ "${todstart%:??}" -lt "${todend%:??}" -o "${todstart%:??}" -eq "${todend%:??}" -a "${todstart#*:}" -lt "${todend#*:}" ] \ + 2>&- \ + && { printf '%02i:%02i-%02i:%02i\n' "${todstart%:??}" "${todstart#*:}" "${todend%:??}" "${todend#*:}"; }\ + || { [ "${todstart%:??}" -ge 0 -a "${todstart#*:}" -ge 0 ] 2>&- && printf '%02i:%02i-\n' $(dec ${todstart%:??} ${todstart#*:}); } + done |grep -xE '^([01][0-9]|2[0-3]):([0-5][0-9])-(([01][0-9]|2[0-3]):([0-5][0-9]))?$' |sort -u + )" + [ "$addtime" = "$date" ] && DBM "$file" append "tod_${date}" "${BR}-" + done + + DBM "$file" set dates "$( + for date in $(seq 0 $(POST_COUNT date)); do + [ "$date" -eq 0 ] \ + && POST date_add \ + || POST date "$date" + printf \\n + done \ + | grep -vxF "$(POST date_remove)" \ + | grep -xE '^[0-9]{4}-((01|03|05|07|08|10|12)-([012][0-9]|3[01])|(04|06|09|11)-([012][0-9]|30)|02-[012][0-9])$' \ + | sort -u + )" + + if [ "$(POST delete)" = delete ]; then + if [ "$(POST delconfirm)" -o ! "$(DBM "$file" get participants)" ]; then + rm -- "$file" + REDIRECT "$_BASE/" + else + REDIRECT "$_BASE$PATH_INFO${month:+?month=}${month}#ERROR_NEEDCONFIRM" + fi + elif [ "$(POST post)" = post ]; then + REDIRECT "$_BASE${PATH_INFO%/*}" + elif [ "$(POST bookmark)" -a ! "$bookmarks" ]; then + SET_COOKIE +$((182 * 86400)) bookmarks="${id}/${admin}" Path="${_BASE}/" + REDIRECT "$_BASE$PATH_INFO${month:+?month=}${month}" + else + REDIRECT "$_BASE$PATH_INFO${month:+?month=}${month}" + fi +else + month="$(GET month |grep -m1 -xE '[0-9]{4}-(0[1-9]|1[012])' || date +%Y-%m)" + Y="${month%-*}"; m="${month#*-}"; Y=${Y#0}; m=${m#0}; + [ "$m" = 1 ] && prev=$(printf '%04i-%02i' $((Y - 1)) 12) || prev=$(printf '%04i-%02i' $Y $((m - 1))) + [ "$m" = 12 ] && next=$(printf '%04i-%02i' $((Y + 1)) 01) || next=$(printf '%04i-%02i' $Y $((m + 1))) + dates="$(DBM "$file" get dates)" + days="$(printf %s "$dates" |sed -E "/^${month}-/!d; s;^.*-([0-9]{2})$;\1;g")" + additional="$(printf %s "$dates" |sed -E "/^${month}-/d;")" + splittimes="$(DBM "$file" get splittimes || printf no)" + + yield_page "$(pagename "$id")" "newdate" <<-EOF + $(dlist_timeofday) + [main [form method=post + [input name=title value="$(DBM "$file" get title |HTML)" placeholder="Title" autocomplete=off] + [textarea name=description placeholder="Description" . $(DBM "$file" get description |HTML)] + $(if [ "$bookmarks" ]; then + printf '[section .bookmark This admin page is accessible via the link [a href="%s" . %s]. You must copy this link and keep it safe, so you can modify this poll later! The poll has also been bookmarked and will be listed on the front page.]' \ + "$(URL "//$(HEADER Host)/${_BASE}/${id}/${admin}")" \ + "$(HTML "${HTTPS:+https:}${HTTPS:-http:}//$(HEADER Host)$(PATH "/${_BASE}/${id}/${admin}")")" + else + printf '[section .bookmark This admin page is accessible via the link [a href="%s" . %s]. You must copy this link and keep it safe, so you can modify this poll later! You can also set a Cookie to bookmark all polls you visit, including this admin page. Bookmarked polls will be listed on the frontpage.
[submit "bookmark" "add" Set Cookie]]' \ + "$(URL "//$(HEADER Host)/${_BASE}/${id}/${admin}")" \ + "$(HTML "${HTTPS:+https:}${HTTPS:-http:}//$(HEADER Host)$(PATH "/${_BASE}/${id}/${admin}")")" + fi) + [fieldset .date + $(printf '[hidden "date" "%s"]' $additional) + [submit "month" "$prev" Previous Month] + $([ "$splittimes" = yes ] && w_month submit date "$month" $days || w_month multiple date "$month" $days) + [submit "month" "$next" Next Month] + [hidden "month" "$month"] + ] + $([ "$splittimes" = "yes" ] && fs_splittimes || fs_timeofday ) + $(if [ "$(DBM "$file" get participants)" ]; then + printf '[checkbox "delconfirm" "confirm" id="delconfirm"][label for=delconfirm Delete Poll] + [submit "delete" "delete" Delete Poll] [submit "post" "post" Poll page]' + else + printf '[submit "delete" "delete" Cancel] [submit "post" "post" Post Event]' + fi) + ]] + EOF +fi diff --git a/poll.sh b/poll.sh new file mode 100644 index 0000000..240a155 --- /dev/null +++ b/poll.sh @@ -0,0 +1,251 @@ +#!/bin/sh + +. "${_EXEC}/comments.sh" +. "${_EXEC}/languages.sh" + +id="$(checkid "${PATH_INFO#/}")" +file="${_DATA}/${id}" + +#cancel if poll is invalid +if [ ! "$id" -o ! -f "$file" ]; then + page_home; + return 0 +fi + +if expr match "$bookmarks" ".*${id}.*" >/dev/null; then + : +elif [ "$bookmarks" ]; then + SET_COOKIE +$((182 * 86400)) bookmarks="${bookmarks} ${id}" Path="${_BASE}/" +fi + +admin="$(expr match "$bookmarks" ".*$id/\([a-zA-Z0-9:=]\{16\}\).*")" +admin="$(DBM "$file" get adminkey |grep -xF "$admin")" + +tkey() { + # convert time stamps for use in POST keys + local str="$1" out + while [ "$str" ]; do + case $str in + :*) out="${out}.";; + *) out="${out}${str%"${str#?}"}";; + esac + str="${str#?}" + done + printf %s "$out" +} + +timelist() { + local dates todall splittimes + local date tod todsplit + + if [ "$splittimes" = no -a "$dates" -a "$todall" ]; then + for date in $dates; do for tod in $todall; do + printf %s\\n "${date}_${tod%-}" + done ;done + + elif [ "$splittimes" = no -a "$dates" ]; then + for date in $dates; do + printf %s\\n "${date}" + done + + elif [ "$splittimes" = no -a "$todall" ]; then + for tod in $todall; do + printf %s\\n "${tod%-}" + done + + elif [ "$splittimes" = yes ]; then + for date in $dates; do + todsplit="$(DBM "$file" get "tod_$date")" + [ "$todsplit" ] \ + && for tod in $todsplit; do printf %s\\n "${date}_${tod%-}"; done \ + || printf %s\\n "${date}" + done + + else + return 1 + + fi +} + +table_poll() { + local splittimes="$(DBM "$file" get splittimes || printf no)" + local dates="$(DBM "$file" get dates)" + local todall="$(DBM "$file" get todall)" + local timelist="$(timelist)" + local edit="$(GET edit)" + local time date span name yes no maybe yc nc mc + + table_date="${table_date+"%A
%B %_d, %Y"}" + + [ "$timelist" ] || return 1 + + printf '[table .poll [thead\n' + # date header + if [ "$dates" ]; then + printf '[tr .dates [th]' + for date in $dates; do + span=0; for time in $timelist; do case $time in + ${date}*) span=$((span + 1));; + esac; done + date -d "$date" +"[th colspan=\"${span}\" . ${table_date}]"; + done + printf '[th]]\n' + fi + + # tod header + if [ "$splittimes" = yes -o "$todall" ]; then + printf '[tr .tod [th]' + for time in $timelist; do + case $time in + *-*-*_*:*) time="${time#*_}";; + *-*-*) time="";; + *:*);; # time="${time}" + esac + printf '[th . %s]' "${time}" + done + printf '[th]]\n' + fi + + printf '][tbody\n' + + # Vote displays + { DBM "$file" get participants; printf \\n; } |while read -r name; do + [ "$name" = "$edit" ] && continue + maybe="$(DBM "$file" get "reply_maybe_${name}")" + yes="$(DBM "$file" get "reply_yes_${name}")" + no="$(DBM "$file" get "reply_no_${name}")" + + printf '[tr [th .name . %s]' "$(HTML "$name")" + for time in $timelist; do + printf %s "$yes" |grep -qwF "$time" && printf '[td .yes Yes]' && continue + printf %s "$no" |grep -qwF "$time" && printf '[td .no No]' && continue + printf %s "$maybe" |grep -qwF "$time" && printf '[td .maybe Maybe]' && continue + printf '[td .missing . ?]' + done + printf '[td .edit [a href="?edit=%s" Edit]]]\n' "$(URL "$name")" + done + + if [ "$edit" ]; then + maybe="$(DBM "$file" get "reply_maybe_${edit}")" + yes="$(DBM "$file" get "reply_yes_${edit}")" + no="$(DBM "$file" get "reply_no_${edit}")" + + printf '[tr .new [th .name [submit "delete" "%s" -] %s]' "$(HTML "$edit")" "$(HTML "$edit")" + for time in $timelist; do + ktime="$(tkey "$time")" + printf '[td [radio "%s" "yes" #yes_%s %s][label for="yes_%s" Yes] + [radio "%s" "no" #no_%s %s][label for="no_%s" No] + [radio "%s" "maybe" #maybe_%s %s][label for="maybe_%s" Maybe] + ]' "${ktime}" "${time}" "$(checked "$time" $yes)" "${time}" \ + "${ktime}" "${time}" "$(checked "$time" $no)" "${time}" \ + "${ktime}" "${time}" "$(checked "$time" $maybe)" "${time}" + done + printf '[td [submit "update" "%s" Update]]]\n' "$(HTML "$edit")" + else + + # Vote counts + printf '[tr .votecount [td]' + for time in $timelist; do + yc=0 nc=0 mc=0 + { DBM "$file" get participants; printf \\n\\n; } |while read -r name; do + [ ! "$name" ] && printf '[td %i (%i)]' "$yc" "$((yc + mc))" && break; + yes="$(DBM "$file" get "reply_yes_${name}")" + no="$(DBM "$file" get "reply_no_${name}")" + maybe="$(DBM "$file" get "reply_maybe_${name}")" + + printf %s "$yes" |grep -qwF "$time" && yc=$((yc + 1)) && continue + printf %s "$maybe" |grep -qwF "$time" && mc=$((mc + 1)) && continue + done + done + printf '[td]]\n' + + # Submit line + printf '[tr .new [td [input name="name" value="" placeholder="Your Name" autocomplete=off]]' + for time in $timelist; do + time="$(tkey "$time")" + printf '[td [radio "%s" "yes" #yes_%s][label for="yes_%s" Yes] + [radio "%s" "no" #no_%s][label for="no_%s" No] + [radio "%s" "maybe" #maybe_%s][label for="maybe_%s" Maybe] + ]' "${time}" "${time}" "${time}" \ + "${time}" "${time}" "${time}" \ + "${time}" "${time}" "${time}" + done + printf '[td [submit "new" "new" Submit]]]\n' + fi + + printf ']]' +} + +if [ "$REQUEST_METHOD" = POST ]; then + local update="$(POST update)" delete="$(POST delete)" + local name="$(POST name |grep -m 1 -xE '.*[^ ].*')" + local splittimes="$(DBM "$file" get splittimes || printf no)" + local dates="$(DBM "$file" get dates)" + local todall="$(DBM "$file" get todall)" + local timelist="$(timelist)" + local time yes no maybe reply + + if [ "$(POST new)" = new -o "$update" ]; then + [ "$update" ] && name="$update" + + if [ ! "$name" ]; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_NONAME" + elif [ ! "$update" ] && DBM "$file" get participants |grep -qxF "$name"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_NAMEEXISTS" + elif [ "$update" ] && ! DBM "$file" get participants |grep -qxF "$name"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_NAMENONEXIST" + fi + if [ ! "$update" ]; then + DBM "$file" append participants "${BR}${name}" || DBM "$file" insert participants "${name}" \ + || REDIRECT "${_BASE}${PATH_INFO}#ERROR_DBACCESS" + fi + + for time in $timelist; do reply="$(POST "$(tkey "$time")")"; case $reply in + yes) yes="${yes}${yes:+ }${time}";; + no) no="${no}${no:+ }${time}";; + maybe) maybe="${maybe}${maybe:+ }${time}";; + esac; done + DBM "$file" set "reply_yes_${name}" "$yes" + DBM "$file" set "reply_no_${name}" "$no" + DBM "$file" set "reply_maybe_${name}" "$maybe" + + elif [ "$delete" ]; then + if ! DBM "$file" get participants |grep -qxF "$delete"; then + REDIRECT "${_BASE}${PATH_INFO}#ERROR_NAMENONEXIST" + fi + DBM "$file" set participants "$(DBM "$file" get participants |grep -vxF "$delete")" + DBM "$file" delete "reply_yes_${delete}" + DBM "$file" delete "reply_no_${delete}" + DBM "$file" delete "reply_maybe_${delete}" + + elif [ "$(POST bookmark)" -a ! "$bookmarks" ]; then + SET_COOKIE +$((182 * 86400)) bookmarks="${id}" Path="${_BASE}/" + + fi + REDIRECT "${_BASE}${PATH_INFO}" + +else + pagename="$(pagename "$id")" + + yield_page "$pagename" poll <<-EOF + [main + [section .description + [h1 .title $(HTML "$pagename")] + $(DBM "$file" get description |markdown) + ] + [form method=POST + $(table_poll || printf '[p Poll parameters are invalid]') + + $(if [ "$admin" ]; then + printf '[section .bookmark You have bookmarked the admin page of this poll: [a href="./%s" modify poll]]' "$(URL ${id}/${admin})" + elif [ "$bookmarks" ]; then + printf '[section .bookmark This poll has been bookmarked and is accessible via a link on the front page.]' + else + printf '[section .bookmark Polls are accessible only via their URL. You can set a Cookie to bookmark all polls you visit. Bookmarked polls will be listed on the frontpage.
[submit "bookmark" "add" Set Cookie]]' + fi) + ] + + $(w_comments) + ] + EOF +fi diff --git a/webpoll.css b/webpoll.css new file mode 100644 index 0000000..479d198 --- /dev/null +++ b/webpoll.css @@ -0,0 +1,248 @@ +body { + background-size: 4pt 4pt; + background-image: /* #6AF #6FF */ + linear-gradient( 0deg, transparent 25%, rgba(102,170,255,.5) 25% 50%, transparent 50% 75%, rgba(102,255,255,.5) 75%), + linear-gradient(90deg, transparent 25%, rgba(102,170,255,.5) 25% 50%, transparent 50% 75%, rgba(102,255,255,.5) 75%); +} + +body > main { + background-color: rgba(255,255,255,.75) ; + padding: 1em; margin: 1em auto 1em auto; + border-radius: .5ex; + box-shadow: #000 .125em .125em 1em; +} + +body.home main { + position: fixed; + left: 50%; top: 50%; + transform: translate(-50%, -50%); +} + +body.poll main { + text-align: center; + max-width: 95%; +} +body section { + max-width: 50em; + padding: 1em; + margin: 1em auto; + background-color: rgba(255,255,255,.5); + word-wrap: break-word; +} +body.poll .description { + margin-top: 0; + text-align: left; +} +body.poll .description .title { + margin-top: 0; + text-align: center; +} +body.poll table { + display: block; + max-width 100%; + overflow-x: auto; + + background-color: rgba(255,255,255,.5); + border-collapse: collapse; + margin: auto; + box-shadow: #000 .25em .25em .5em; + border-radius: 2pt; +} + +body.poll table thead tr.dates th { + padding: .25em; +} +body.poll table thead tr.tod th { + border-width: .5pt; + border-style: none solid none solid; + padding: .25em; +} +body.poll table tbody tr td { + text-align: center; + border: .5pt solid; + padding: 0 .25em; + background-clip: padding-box; +} +body.poll table tbody tr td:first-child, +body.poll table tbody tr td:last-child, +body.poll table thead tr th:first-child, +body.poll table thead tr th:last-child { border: none; } + +body.poll table tbody tr th.name { padding: .25em .5em; text-align: right; } +body.poll table tbody tr td.yes { background-color: #AFA; } +body.poll table tbody tr td.no { background-color: #FAA; } +body.poll table tbody tr td.maybe { background-color: #FFA; } +body.poll table tbody tr td.edit { text-align: left; font-size: .875em; } + +body.poll table tbody tr.votecount td { padding: .375em .25em;} +body.poll table tbody tr th.name button[name=delete] { font-size: .625em; line-height: 1.5em; background-color: #FAA; } + +body.poll table tbody tr.new td:first-child { text-align: right; } +body.poll table tbody tr.new td input[name=name] { min-width: 100%; width: 8em; } + +body.poll table td input[type=radio] { display: none; } +body.poll table td input[type=radio] + label { + font-size: .875em; + text-decoration: underline; + color: #066; + padding: .25em; + margin: 0; +} +body.poll table td input[type=radio]:checked + label { + font-weight: bold; +} +body.poll table td input[type=radio][value=yes]:checked + label { + background-color: #AFA; + margin: 0 -1.5pt; +} +body.poll table td input[type=radio][value=no]:checked + label { + background-color: #FAA; + margin: 0 -.75pt; +} +body.poll table td input[type=radio][value=maybe]:checked + label { + background-color: #FFA; + margin: 0 -1.75pt; +} + +body.newdate main { + text-align: center; + max-width: 100%; +} +body.newdate form fieldset.date, +body.newdate form fieldset.timeofday, +body.newdate form fieldset.splittimes { + display: inline-block; + vertical-align: top; + margin: .5em 0 1em 0; +} + +body.newdate main { width: 26em; } +body.newdate form input[name=title], +body.newdate form textarea[name=description] { + width: 100%; +} +body.newdate form fieldset.date, +body.newdate form fieldset.timeofday, +body.newdate form fieldset.splittimes { + width: 100%; +} + +@media(min-width: 50em) { + body.newdate main { width: 50em; } + body.newdate form input[name=title], + body.newdate form textarea[name=description] { + width: 100%; + } + body.newdate form fieldset.date, + body.newdate form fieldset.timeofday, + body.newdate form fieldset.splittimes { + width: 49.5%; width: calc(50% - .375ex); + } + body.newdate form fieldset.date { padding-right: .75em; } + body.newdate form fieldset.timeofday, + body.newdate form fieldset.splittimes { padding-left: .75em; } + body.poll table { display: table;} +} + +body.newdate form input[name=title], +body.newdate form textarea[name=description] { + display: block; + margin-bottom: .75em; +} +body.newdate form textarea[name=description] { + height: 8em; +} + +body.newdate form .date button[name=month] { + display: inline-block; + position: absolute; + top: 0; + height: 2.375em; + width: 2em; width: calc(50% - 9em); + padding: 0; + color: transparent; + background-color: transparent; + border: none; + overflow: hidden; + z-index: 1; +} +body.newdate form .date button[name=month]:before { + display: block; + content: '<'; + font-size: 1.75em; + font-weight: bold; + color: #666; + margin-top: .25em; +} +body.newdate form .date table + button[name=month] { right: .75em; } +body.newdate form .date table + button[name=month]:before { + content: '>'; +} + +body.newdate form .date table.calendar { + background-color: #FFF; + font-size: 17pt; + -vertical-align: middle; +} + +body.newdate form .timeofday label.todstart, +body.newdate form .timeofday label.todend { + display: inline-block; + margin: 0; + font-weight: bold; + text-align: left; + font-size: .75em; + width: 49%; width: calc(50% - 2.5pt); +} +body.newdate form fieldset > input[name^=todstart], +body.newdate form fieldset > input[name^=todend] { + display: inline-block; + margin: 0; + width: 49%; width: calc(50% - 2.5pt); + text-align: right; +} +body.newdate form fieldset > input[name^=todend] { + width: 39%; width: calc( 50% - 4.375ex); +} + +body.newdate form fieldset button[name^=addtime] { + width: 100%; +} + +body.newdate form fieldset input.splittimes { display: none;} +body.newdate form fieldset input.splittimes + * { left: 12pt; } +body.newdate form fieldset input.splittimes + *:before { + position: absolute; + width: 16pt; height: 16pt; + left: -24pt; + content: ''; + text-align: center; + font-weight: bold; + font-size: 1.75em; + border: 1pt solid; + border-radius: .25ex; + background-color: #FFF; +} +body.newdate form fieldset input.splittimes:checked + *:before { content: '\2713'; background-color: #6AF;} +body.newdate form .timeofday button[name=splittimes] { margin-top: 1.5em;} + +body.newdate form .splittimes p { margin-top: 2em; padding: .5em; background: rgba(255,255,255,.5); } + +body.newdate form #delconfirm { + margin-left: -6em; +} +body.newdate form #delconfirm + label:after { + content: '\0A'; + white-space: pre; +} +body.newdate form #delconfirm + label + button { + pointer-events: none; + color: #AAA; + border-color: #AAA; +} +body.newdate form #delconfirm:checked + label + button { + pointer-events: auto; + color: inherit; + border-color: inherit; + background-color: #FDD; +} diff --git a/widgets.css b/widgets.css new file mode 100644 index 0000000..a93e2a3 --- /dev/null +++ b/widgets.css @@ -0,0 +1,129 @@ +table.calendar { + display: inline-block; + border-collapse: collapse; + vertical-align: top; +} +table.calendar td { + border: .5pt solid; +} +table.calendar th { + font-weight: normal; +} + +table.calendar thead tr.monthname { + border-style: solid; + border-width: .5pt .5pt .5pt .5pt; + line-height: 1.5em; + font-size: 1.125em; +} +table.calendar thead tr.weekday { + line-height: 1.5em; +} +table.calendar thead tr.weekday th:first-of-type { + border-style: solid; + border-width: 0pt 0pt 0pt .5pt; +} +table.calendar thead tr.weekday th:last-child { + border-right: .5pt solid; +} +table.calendar tbody tr th { + border-left: .5pt solid #000; +} +table.calendar tbody tr:last-child th { + border-bottom: .5pt solid #000; +} + +table.calendar tbody tr th.weekno { + width: 2em; + padding: 0 .25em; + text-align: right; + font-weight: normal; + color: #888; +} + +table.calendar input[type=radio], +table.calendar input[type=checkbox] { + display: none; +} +table.calendar td label, +table.calendar td button { + display: inline-block; + width: 2em; + margin: 0; padding: .25em; + text-align: right; + line-height: 1em; + box-shadow: none; + border-radius: 0; + border: none; +} +table.calendar td input:checked + label, +table.calendar td label[checked], +table.calendar td button[name$=_remove] { + font-weight: bold; + line-height: .75em; + border: .125em solid; +} + +#comments input#comments_toggle_new, +#comments input#comments_toggle_new + label + form { + display: none; + text-align: left; +} +#comments input#comments_toggle_new:checked + label + form { + display: block; +} +#comments input#comments_toggle_new + label { + display: block; + width: 100%; margin: 0; + padding: .25em .75em; + background-color: #FFF; + text-align: left; + border: .5pt solid; + border-radius: 2pt; +} +#comments input#comments_toggle_new:checked + label { + display: none; +} + +#comments form label.legend { + display: block; +} + +#comments input[name=username], +#comments textarea { + width: 100%; +} +#comments textarea { + min-height: 7em; + margin: .5em 0; +} + +#comments .comment { + text-align: left; + margin: .5em 0; + padding: 0 .5em .25em .5em; + -padding-left: .5em; + -border-left: 3pt solid #CCC; + box-shadow: #000 .25em .25em .5em; +} + +#comments .comment > h3 { + padding-top: .5em; + margin-bottom: 0; +} + +#comments .comment > span { + display: inline-block; + min-width: 50%; + font-size: .875em; + color: #666; +} +#comments .comment > a.edit { + display: block; + min-width: 50%; + font-size: .875em; +} +#comments .comment > div { + border-top: .5pt solid; +} + diff --git a/widgets.sh b/widgets.sh new file mode 100755 index 0000000..8410a0c --- /dev/null +++ b/widgets.sh @@ -0,0 +1,128 @@ +#!/bin/sh + +[ -n "$include_widgets" ] && return 0 +include_widgets="$0" + +export MD_HTML="false" +if [ "$(which awk)" ]; then + markdown() { awk -f "$_EXEC/cgilite/markdown.awk"; } +else + markdown() { busybox awk -f "$_EXEC/cgilite/markdown.awk"; } +fi + +dec(){ + local n + for n in "$@"; do + while [ "${n}" != "${n#0}" ]; do n="${n#0}"; done + printf %i\\n "$n" + done +} + +checked(){ + local check="$1"; shift 1; + for comp in "$@"; do + if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then + printf 'checked="checked"' + break; + fi 2>/dev/null + done +} +selected(){ + local check="$1"; shift 1; + for comp in "$@"; do + if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then + printf 'selected="selected"' + break; + fi 2>/dev/null + done +} + +w_month() { + # Arguments: + # 1. (optional) select, multiple, submit, none - default: select + # 2. (optional) Name of form field - default: "date" + # 3. (optional) Month to display in format: YYYY-MM - default: current month + # 4. (optional, multiple) Days to preselect in format: DD - default: none + + local type="${1:-select}" input="${2:-date}" month="$3" + shift 3; local selected="$*" + local dow dom days n=1 Y m d V w B + if [ $month ]; then + read Y m d V w B<<-EOF + $(date -d "${month}-01" +"%_Y %_m %_d %_V %w %B") + EOF + else + read Y m d V w <<-EOF + $(date +"%Y %m %d %V %w") + EOF + month="$Y-$m" + V="$((V - d / 7))" + [ $V -lt 1 ] && V=$((V + 53)) + fi + + case $m in + [13578]|10|12) + days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31";; + [469]|11) + days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30";; + 2) if [ $(( Y / 400 )) = 0 ]; then + days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29"; + elif [ $(( Y / 100 )) = 0 ]; then + days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28"; + elif [ $(( Y / 4 )) = 0 ]; then + days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29"; + else + days="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28"; + fi;; + esac + + printf '[table .calendar month=%s [thead + [tr .monthname [th colspan=8 . %s]] + [tr .weekday [th][th . %s][th . %s][th . %s][th . %s][th . %s][th . %s][th . %s]] + ][tbody + ' "$month" "$B $Y" Mo Tu We Th Fr Sa Su + for dom in $days; do + dow=$(( ( w - d + 35 + dom ) % 7)) + [ $dow = 1 -o $dom = 1 ] && printf '[tr [th .weekno . %i]' $V + [ $dom = 1 ] && while [ $n -lt $(( ($dow + 6) % 7 + 1)) ]; do printf '[td ]'; n=$((n + 1)); done + date="$(printf "%04i-%02i-%02i" $Y $m $dom)" + case $type in + none) + printf '[td [label %s . %i]]' \ + "$(checked $dom $selected)" "$dom" + ;; + multiple) + printf '[td [input type=checkbox id="%s_%s" name="%s" value="%s" %s][label for="%s_%s" . %i]]' \ + "$input" "$date" "$input" "$date" "$(checked $dom $selected)" "$input" "$date" "$dom" + ;; + submit) + [ "$(checked $dom $selected)" ] \ + && printf '[td [submit "%s_remove" "%s" . %i][hidden "%s" "%s"]]' "$input" "$date" "$dom" "$input" "$date" \ + || printf '[td [submit "%s_add" "%s" . %i]]' "$input" "$date" "$dom" + ;; + select|*) + printf '[td [input type=radio id="%s_%s" name="%s" value="%s" %s][label for="%s_%s" . %i]]' \ + "$input" "$date" "$input" "$date" "$(checked $dom $selected)" "$input" "$date" "$dom" + ;; + esac + if [ $dow = 0 ]; then + printf ']\n' + V=$((V + 1)) + [ $m = 1 -a $V -ge 53 ] && V=1 + fi + done + if [ $dow -gt 0 ]; then + while [ $dow -le 6 ]; do printf '[td ]'; dow=$((dow + 1)) ; done + printf ']\n' + fi + printf ']]' +} + +dlist_timeofday() { + local step="${1:-15}" id="${2:-dlist_timeofday}" + printf '[datalist id="%s"\n' $id + for h in $(seq 0 23); do for m in $(seq 0 "$step" 59); do + printf '[option value="%i:%02i"]\n' $h $m + done; done + printf ']\n' +}