session aware file locking
authorPaul Hänsch <paul@plutz.net>
Sat, 3 Aug 2019 15:11:47 +0000 (17:11 +0200)
committerPaul Hänsch <paul@plutz.net>
Sat, 3 Aug 2019 15:11:47 +0000 (17:11 +0200)
cards/edit_card.sh
cards/list.sh
cards/update_card.sh
session_lock.sh [new file with mode: 0644]

index 6b62395..5a05194 100755 (executable)
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-mkdir -p "${_DATA}/lock"
+locktimeout=900
+. "$_EXEC"/session_lock.sh
 
 card="$(GET card)"
 
 card="$(GET card)"
-lock="$_DATA/lock/${card}/"
 cardfile="$_DATA/vcard/$card"
 cardfile="$_DATA/vcard/$card"
-tempfile="$lock/temp.vcf"
-
 filter="$(REF f)"
 order="$(REF o)"
 
 filter="$(REF f)"
 order="$(REF o)"
 
-if mkdir "$lock" 2>&-; then
-  cp "$cardfile" "$tempfile"
+if tempfile="$(SLOCK "$cardfile" "$locktimeout")"; then
   REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
   REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+elif [ -f "$tempfile" ]; then
+  SET_COOKIE session message="SESSLOCK"
+  REDIRECT "/cards/?o=${order}&f=${filter}#${card}"
 else
   SET_COOKIE session message="EDITLOCK"
   REDIRECT "/cards/?o=${order}&f=${filter}#${card}"
 else
   SET_COOKIE session message="EDITLOCK"
   REDIRECT "/cards/?o=${order}&f=${filter}#${card}"
index a7d39e2..f598cd2 100755 (executable)
@@ -146,65 +146,73 @@ edit_item(){
 
 edit_card(){
   local cardfile="$_DATA/vcard/$1" 
 
 edit_card(){
   local cardfile="$_DATA/vcard/$1" 
-  local card="$(pdi_load "$cardfile")"
+  local tempfile card
 
 
-  cat <<-EOF
-  [form .card #${cardfile##*/} action="/cards/update_card.sh" method="POST"
-    [div .section .basic $(
-      edit_item "$card" N GENDER
-      [ "$(pdi_count "$card" NICKNAME)" -gt 0 ] \
-      && edit_item "$card" NICKNAME
-      edit_item "$card" X-ZACK-JOINDATE
-      [ "$(pdi_count "$card" X-ZACK-LEAVEDATE)" -gt 0 ] \
-      && edit_item "$card" X-ZACK-LEAVEDATE
-      card_item "$card" SOUND PHOTO LOGO
-    )]
-    [div .section .phone   $(edit_item "$card" TEL)]
-    [div .section .message $(
-      edit_item "$card" EMAIL
-      [ $(pdi_count "$card" IMPP) -gt 0 ] && edit_item "$card" IMPP
-      [ $(pdi_count "$card" URL ) -gt 0 ] && edit_item "$card" URL
-    )]
-    [div .section .address $(edit_item "$card" ADR)]
-    [div .section .note    $(edit_item "$card" NOTE)]
-    [div .section .attendance
-      [h3 $(l10n course_attendance) ] $(
-      for course in "$_DATA"/ical/*.ics; do
-        printf '[label [input type="checkbox" name="attendance" value="%s" %s] %s]' \
-               "${course##*/}" \
-               "$(grep -qF "${course##*/}      ${cardfile##*/}" "$_DATA/mappings/attendance" \
-                  && printf 'checked="checked"'
-               )" \
-               "$(pdi_value "$(pdi_load "$course")" SUMMARY |unescape |HTML)"
-      done)
-      [h3 $(l10n CATEGORIES) ] $(
-      grep -xE '[^ ]+' "$_DATA"/mappings/categories |while read -r cat; do
-        printf '[label [input type="checkbox" name="attendance" value="%s" %s] %s]' \
-               "$(HTML "$cat")" \
-               "$(seq 1 $(pdi_count "$card" CATEGORIES) |while read c; do
-                 pdi_value "$card" CATEGORIES $c |grep -qxF "$cat" \
-                 && printf 'checked="checked"' && break
-               done)" \
-               "$(HTML "$cat")"
-      done)
-    ]
-    [div .control
-      [select .item name="newfield"
-        [option value="" disabled="disabled" selected="selected" $(l10n edit_addfieldtext)]
-        $(for f in ; do
-          printf '[option value="%s" %s] ' "$f" "$(l10n "$f")"
-        done)
-      ]
-      [button .item type="submit" name="action" value="addfield" $(l10n edit_addfield)]
-      [button .item type="submit" name="action" value="update"   $(l10n edit_update)]
-      [input type="checkbox" #delete] [label .item for="delete" $(l10n edit_delete)]
-        [button .item type="submit" name="action" value="delete"]
-      [button .item type="submit" name="action" value="cancel"   $(l10n edit_cancel)]
-    ]
-    [input type="hidden" name="UID" value="$(pdi_value "$card" UID |HTML)"]
-    [input type="hidden" name="card" value="${cardfile##*/}"]
-  ]
+  . $_EXEC/session_lock.sh
+
+  if ! tempfile="$(CHECK_SLOCK "$cardfile")"; then
+    printf '[div .message %s]' "$(l10n "This card is not set up for editing within this session.")"
+  else
+    card="$(pdi_load "$tempfile")"
+    cat <<-EOF
+       [form .card #${cardfile##*/} action="/cards/update_card.sh" method="POST"
+         [input type="hidden" name="tid" value="$(transid ${tempfile})"]
+         [div .section .basic $(
+           edit_item "$card" N GENDER
+           [ "$(pdi_count "$card" NICKNAME)" -gt 0 ] \
+           && edit_item "$card" NICKNAME
+           edit_item "$card" X-ZACK-JOINDATE
+           [ "$(pdi_count "$card" X-ZACK-LEAVEDATE)" -gt 0 ] \
+           && edit_item "$card" X-ZACK-LEAVEDATE
+           card_item "$card" SOUND PHOTO LOGO
+         )]
+         [div .section .phone   $(edit_item "$card" TEL)]
+         [div .section .message $(
+           edit_item "$card" EMAIL
+           [ $(pdi_count "$card" IMPP) -gt 0 ] && edit_item "$card" IMPP
+           [ $(pdi_count "$card" URL ) -gt 0 ] && edit_item "$card" URL
+         )]
+         [div .section .address $(edit_item "$card" ADR)]
+         [div .section .note    $(edit_item "$card" NOTE)]
+         [div .section .attendance
+           [h3 $(l10n course_attendance) ] $(
+           for course in "$_DATA"/ical/*.ics; do
+             printf '[label [input type="checkbox" name="attendance" value="%s" %s] %s]' \
+                    "${course##*/}" \
+                    "$(grep -qF "${course##*/} ${cardfile##*/}" "$_DATA/mappings/attendance" \
+                       && printf 'checked="checked"'
+                    )" \
+                    "$(pdi_value "$(pdi_load "$course")" SUMMARY |unescape |HTML)"
+           done)
+           [h3 $(l10n CATEGORIES) ] $(
+           grep -xE '[^ ]+' "$_DATA"/mappings/categories |while read -r cat; do
+             printf '[label [input type="checkbox" name="attendance" value="%s" %s] %s]' \
+                    "$(HTML "$cat")" \
+                    "$(seq 1 $(pdi_count "$card" CATEGORIES) |while read c; do
+                      pdi_value "$card" CATEGORIES $c |grep -qxF "$cat" \
+                      && printf 'checked="checked"' && break
+                    done)" \
+                    "$(HTML "$cat")"
+           done)
+         ]
+         [div .control
+           [select .item name="newfield"
+             [option value="" disabled="disabled" selected="selected" $(l10n edit_addfieldtext)]
+             $(for f in ; do
+               printf '[option value="%s" %s] ' "$f" "$(l10n "$f")"
+             done)
+           ]
+           [button .item type="submit" name="action" value="addfield" $(l10n edit_addfield)]
+           [button .item type="submit" name="action" value="update"   $(l10n edit_update)]
+           [input type="checkbox" #delete] [label .item for="delete" $(l10n edit_delete)]
+             [button .item type="submit" name="action" value="delete"]
+           [button .item type="submit" name="action" value="cancel"   $(l10n edit_cancel)]
+         ]
+         [input type="hidden" name="UID" value="$(pdi_value "$card" UID |HTML)"]
+         [input type="hidden" name="card" value="${cardfile##*/}"]
+       ]
        EOF
        EOF
+  fi
 }
 
 print_card(){
 }
 
 print_card(){
@@ -233,7 +241,7 @@ print_card(){
         $(card_item "$card" CATEGORIES)
       ]
       [div .control
         $(card_item "$card" CATEGORIES)
       ]
       [div .control
-        [a .item href="/cards/?e=${cardfile##*/}" $(l10n edit)]
+        [a .item href="/cards/edit_card.sh?card=${cardfile##*/}" $(l10n edit)]
         [a .item href="/cards/?x=${cardfile##*/}" $(l10n vcf_export)]
       ]
     ]
         [a .item href="/cards/?x=${cardfile##*/}" $(l10n vcf_export)]
       ]
     ]
index a1143c3..ee9ac3e 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/zsh
 
 #!/bin/zsh
 
-# Copyright 2014, 2016 Paul Hänsch
+# Copyright 2014, 2016, 2019 Paul Hänsch
 #
 # This file is part of Confetti.
 # 
 #
 # This file is part of Confetti.
 # 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
 # You should have received a copy of the GNU Affero General Public License
 # along with Confetti.  If not, see <http://www.gnu.org/licenses/>. 
 
-cgi_refdata
+. "$_EXEC/pdiread.sh"
+. "$_EXEC/session_lock.sh"
 
 
-filter="&filter=${_REF[filter]}"
-filtertype="&filtertype=${_REF[filtertype]}"
-order="&order=${_REF[order]}"
+filter="$(REF f)"
+order="$(REF o)"
 
 
-card="${_POST[card]}"
-tempfile="$_DATA/temp/$card"
+card="$(POST card)"
 cardfile="$_DATA/vcard/$card"
 attfile="$_DATA/mappings/attendance"
 
 cardfile="$_DATA/vcard/$card"
 attfile="$_DATA/mappings/attendance"
 
+if ! tempfile=$(CHECK_SLOCK "$cardfile"); then
+  SET_COOKIE 0 message="NO VALID FILE LOCK"
+  REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+  exit 0
+elif [ "$(POST tid)" != "$(transid "$tempfile")" ]; then
+  SET_COOKIE 0 message="INVALID TRANSACTION ID"
+  REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
+  exit 0
+fi
+
 vcf_escape(){
   for each in "$@"; do
     printf %s\\n "$each" \
 vcf_escape(){
   for each in "$@"; do
     printf %s\\n "$each" \
-    | sed -r ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;'
+    | sed -E ':X;$!{N;bX}; s;\r\n;\n;g; s;([;,\\]);\\\1;g; s;\n;\\n;g;'
   done \
   done \
-  | sed -r ':X;$!{N;bX}; s;\n;\;;g'
+  | sed -E ':X;$!{N;bX}; s;\n;\;;g'
 }
 
 }
 
-[ "${_POST[hi_select]}" = "list" ] || _POST[hi_company]="${_POST[hi_other]}"
-[ -n "${_POST[hi_company]}${_POST[hi_number]}${_POST[hi_status]}" ] \
-&& _POST[X-HEALTH-INSURANCE]="$(vcf_escape "${_POST[hi_company]}" "${_POST[hi_number]}" "${_POST[hi_status]}")"
+[ "${_POST[hi_select]}" = "list" ] || _POST[hi_company]="${_POST[hi_other]}"
+[ -n "${_POST[hi_company]}${_POST[hi_number]}${_POST[hi_status]}" ] \
+&& _POST[X-HEALTH-INSURANCE]="$(vcf_escape "${_POST[hi_company]}" "${_POST[hi_number]}" "${_POST[hi_status]}")"
 
 
-sed -r 's;$;\r;' >"$tempfile" <<EOF
-BEGIN:VCARD
-VERSION:4.0
-N:$(vcf_escape "${_POST[0N]}" "${_POST[1N]}" "${_POST[2N]}" "${_POST[3N]}" "${_POST[4N]}")
-UID:${_POST[UID]}
-$(
-  for field in $VCF_FIELDS; do for key in $field $field{0..100}; do
-    [ -z "${_POST[$key]+x}" ] && break
-    [ -z "${_POST[$key]}" ] && continue
-    case "$key" in
-      (TEL[0-9]*)
-        printf '%s;TYPE=%s:%s\r\n' "${field}" "${_POST[phonetype${key#TEL}]}" "$(vcf_escape "${_POST[$key]}")"
-        ;;
-      (X-HEALTH-INSURANCE)
-        printf '%s:%s\r\n' "${field}" "${_POST[$key]}"
-        ;;
+vcf="$(pdi_load "$cardfile")"
+
+vcf="$(pdi_update_value "$vcf" N 1 "$(vcf_escape "$(POST 1N)" "$(POST 2N)" "$(POST 3N)" "$(POST 4N)" "$(POST 5N)")")"
+
+for field in $(POST_KEYS |grep -xE '[A-Z][A-Z0-9-]*'); do
+  for cnt in $(seq 1 $(POST_COUNT "$field")); do
+    case "$field" in
+      # (TEL)
+      #   printf '%s;TYPE=%s:%s\r\n' "${field}" "${_POST[phonetype${key#TEL}]}" "$(vcf_escape "$(POST "$field" "$cnt")")"
+      #   ;;
       (*)
       (*)
-        printf '%s:%s\r\n' "${field}" "$(vcf_escape "${_POST[$key]}")"
+         vcf="$(pdi_update_value "$vcf" "$field" "$cnt" "$(POST "$field" "$cnt")")"
         ;;
     esac
         ;;
     esac
-  done; done
-  [ "${_POST[action]}" = addfield ] && printf '%s:\r\n' "${_POST[newfield]}"
-)
-END:VCARD
-EOF
+done; done
+[ "$(POST action)" = addfield ] && vcf="$(vcf_update_field "$vcf" "$(POST newfield)" $(($(pdi_count $(POST newfield)) + 1)) '')"
+
+printf '%s' "$vcf" >"$tempfile"
 
 
-case "${_POST[action]}" in
+case "$(POST action)" in
   addfield)
   addfield)
-    redirect "?p=cards${filter}${filtertype}${order}&edit=$card"
+    REDIRECT "/cards/?o=${order}&f=${filter}&e=${card}"
     ;;
   update)
     ;;
   update)
-    attendance=()
-    for att in attendance attendance{0..100}; do
-      [ -n "${_POST[$att]}" ] && attendance+=("${_POST[$att]}")
-    done
-    sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
-      touch "$_DATA/ical/$course"
-    done
-    sed -i -r '/^(.+)\t'$card'$/d' "$attfile"
-    for each in $attendance; do
-      echo "$each\t$card"
-    done >>"$attfile"
-    sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
-      touch "$_DATA/ical/$course"
-    done
+    attendance=()
+    for att in attendance attendance{0..100}; do
+      [ -n "${_POST[$att]}" ] && attendance+=("${_POST[$att]}")
+    done
+    sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
+      touch "$_DATA/ical/$course"
+    done
+    sed -i -r '/^(.+)\t'$card'$/d' "$attfile"
+    for each in $attendance; do
+      echo "$each\t$card"
+    done >>"$attfile"
+    sed -rn 's:^(.+)'$card'$:\1:p' "$attfile" |while read course; do
+      touch "$_DATA/ical/$course"
+    done
 
 
-    mv "$tempfile" "$cardfile"
-    redirect "?p=cards${filter}${filtertype}${order}#$card"
+    cp "$tempfile" "$cardfile"
+    RELEASE_SLOCK "$cardfile"
+    REDIRECT "/cards/?o=${order}&f=${filter}#${card}"
     ;;
   cancel)
     ;;
   cancel)
-    rm "$tempfile"
+    RELEASE_SLOCK "$cardfile"
     [ -f "$cardfile" ] \
     [ -f "$cardfile" ] \
-    && redirect "?p=cards${filter}${filtertype}${order}#$card" \
-    || redirect "?p=cards${filter}${filtertype}${order}"
+    && REDIRECT "/cards/?o=${order}&f=${filter}#${card}" \
+    || REDIRECT "/cards/?o=${order}&f=${filter}"
     ;;
   delete)
     ;;
   delete)
-    rm "$tempfile" "$cardfile"
-    redirect "?p=cards${filter}${filtertype}${order}"
+    rm "$cardfile"
+    RELEASE_SLOCK "$cardfile"
+    REDIRECT "/cards/?o=${order}&f=${filter}"
     ;;
 esac
     ;;
 esac
diff --git a/session_lock.sh b/session_lock.sh
new file mode 100644 (file)
index 0000000..17af0e6
--- /dev/null
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+[ "$include_session_lock" ] && return 0
+include_session_lock="$0"
+
+SLOCK(){
+  local file="$1";
+  local timeout="${2-900}"
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local ovlock="${lockdir%/*}/delete.${lockdir##*/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+  local lockexpire=$(( $(date +%s) - timeout ))
+
+  mkdir -p "$_DATA/lock/${file%/*}"
+
+  if [ -e "$lockdir" ] \
+     && [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \
+     && mkdir "$ovlock"; then
+    [ "$(stat -c %Y "$lockdir")" -lt "$lockexpire" ] \
+    && rm -r "$lockdir"
+    rmdir "$ovlock"
+  fi
+
+  printf '%s\n' "$tempfile"
+  if mkdir "$lockdir" 2>&-; then
+    cp "$file" "$tempfile"
+    return 0
+  else
+    return 1
+  fi
+}
+
+CHECK_SLOCK(){
+  local file="$1";
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+
+  printf '%s\n' "$tempfile"
+  if [ -f "$tempfile" ]; then
+    touch "$lockdir"
+    return 0
+  else
+    return 1
+  fi
+}
+
+RELEASE_SLOCK(){
+  local file="$1";
+  local lockdir="$_DATA/lock/${file#$_DATA}"; lockdir="${lockdir%/}"
+  local ovlock="${lockdir%/*}/delete.${lockdir##*/}"
+  local tempfile="$lockdir/${SESSION_ID}"
+
+  if [ -f "$tempfile" ] && mkdir "$ovlock"; then
+    [ -f "$tempfile" ] && rm -r "$lockdir"
+    rmdir "$ovlock"
+    return 0
+  else
+    return 1
+  fi
+}