add Teckids utility suite, by request from Natureshadow
authormirabilos <t.glaser@tarent.de>
Thu, 23 Nov 2017 19:42:26 +0000 (20:42 +0100)
committermirabilos <t.glaser@tarent.de>
Thu, 23 Nov 2017 19:42:26 +0000 (20:42 +0100)
43 files changed:
mksh/teckids/addgroup [new file with mode: 0644]
mksh/teckids/addperson [new file with mode: 0644]
mksh/teckids/aliaseltern [new file with mode: 0644]
mksh/teckids/astat [new file with mode: 0644]
mksh/teckids/gebtag [new file with mode: 0644]
mksh/teckids/getphoto [new file with mode: 0644]
mksh/teckids/getpost [new file with mode: 0644]
mksh/teckids/groupadd [new file with mode: 0644]
mksh/teckids/groupdel [new file with mode: 0644]
mksh/teckids/intpasswd [new file with mode: 0644]
mksh/teckids/kalics [new file with mode: 0644]
mksh/teckids/kaltag [new file with mode: 0644]
mksh/teckids/karte [new file with mode: 0644]
mksh/teckids/lizenz [new file with mode: 0644]
mksh/teckids/mailadd [new file with mode: 0644]
mksh/teckids/mk/assockit.ksh [new symlink]
mksh/teckids/mk/assoldap.ksh [new symlink]
mksh/teckids/mk/base64.ksh [new file with mode: 0644]
mksh/teckids/mk/common [new file with mode: 0644]
mksh/teckids/mkliste [new file with mode: 0644]
mksh/teckids/mkrechnung [new file with mode: 0644]
mksh/teckids/nominatim [new file with mode: 0644]
mksh/teckids/projadd [new file with mode: 0644]
mksh/teckids/projall [new file with mode: 0644]
mksh/teckids/projintpasswd [new file with mode: 0644]
mksh/teckids/projkarte [new file with mode: 0644]
mksh/teckids/projrand [new file with mode: 0644]
mksh/teckids/rplanung [new file with mode: 0644]
mksh/teckids/schulsql2ldif [new file with mode: 0644]
mksh/teckids/setphoto [new file with mode: 0644]
mksh/teckids/showphoto [new file with mode: 0644]
mksh/teckids/shuffle [new file with mode: 0644]
mksh/teckids/ssh_wrapper [new file with mode: 0755]
mksh/teckids/sshctl [new file with mode: 0644]
mksh/teckids/statupd [new file with mode: 0644]
mksh/teckids/subprjck [new file with mode: 0644]
mksh/teckids/subprjmk [new file with mode: 0644]
mksh/teckids/tbl2kdmn [new file with mode: 0644]
mksh/teckids/teckids [new file with mode: 0755]
mksh/teckids/updatekid [new file with mode: 0644]
mksh/teckids/viplanung [new file with mode: 0644]
mksh/teckids/whoami [new file with mode: 0644]
mksh/teckids/whois [new file with mode: 0644]

diff --git a/mksh/teckids/addgroup b/mksh/teckids/addgroup
new file mode 100644 (file)
index 0000000..3bffab0
--- /dev/null
@@ -0,0 +1,62 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that creates a group
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+read -r "cn?Gruppenname: "
+read -r "description?Beschreibung: "
+print
+
+mksh "$ROOT/util/whois"
+print
+read "r?Diese Person wird Besitzer der Gruppe. Korrekt? [j/n] "
+[[ $r = j ]] || exit 1
+
+owner_dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+
+asso_setldap_sasl groups -- -b ou=Groups,dc=teckids,dc=org \
+    "(objectClass=posixGroup)" gidNumber
+
+asso_loadk groups
+for group_dn in "${asso_y[@]}"; do
+       asso_getv groups "$group_dn" gidNumber 0
+       print
+done | sort -n | tail -1 |&
+read -p gidNumber; (( gidNumber++ ))
+
+dn="cn=$cn,ou=Groups,dc=teckids,dc=org"
+
+ldapmodify <<EOF
+dn: $dn
+changetype: add
+objectClass: groupOfNames
+objectClass: posixGroup
+objectClass: teckidsProject
+cn: $cn
+gidNumber: $gidNumber
+description: $description
+owner: $owner_dn
+member: $owner_dn
+EOF
diff --git a/mksh/teckids/addperson b/mksh/teckids/addperson
new file mode 100644 (file)
index 0000000..df4dc07
--- /dev/null
@@ -0,0 +1,106 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015, 2017
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that adds a person
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if (( eltern_subfilter )); then
+       [[ -s $TECKIDS_CACHE_DIR/last_whois ]] || \
+           die "Missing last “teckids whois” information"
+fi
+
+print "givenName|Vorname
+sn|Nachname
+dateOfBirth|Geburtsdatum (TT.MM.JJJJ)
+mail|E-Mail
+homePostalAddress|Anschrift (privat)
+homePhone|Telefon (privat)
+mobile|Handy
+o|Schule
+l|Schulort
+ou|Klasse" |&
+
+asso_setasso fields
+
+while IFS="|" read -rp f q; do
+       # special casing
+       t=
+       if (( eltern_subfilter )) && [[ $f = homePostalAddress ]]; then
+               t=$(teckids whois -s homePostalAddress)
+       fi
+       [[ -n $t ]] && print -r "Standardwert (einfach Enter zum Übernehmen): $t"
+       read -r "v?$q: "
+       v=${v:-"$t"}
+       [[ -n $v ]] && asso_sets "$v" fields "$f" v
+done
+
+givenName=$(asso_getv fields givenName v)
+sn=$(asso_getv fields sn v)
+
+if [[ -z $givenName || -z $sn ]]; then
+       print -u2 "Vorname und Nachname müssen angegeben werden!"
+       exit 1
+fi
+
+if (( kids_only )); then
+       (( eltern_subfilter )) && die "Conflicting options -E and -K for this script"
+       dn="cn=$givenName $sn,ou=Kids,ou=People,dc=teckids,dc=org"
+elif (( eltern_subfilter )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+       [[ -n $dn ]] || die "Missing last “teckids whois” information"
+       tdn="ou=Eltern,$dn"
+       dn="cn=$givenName $sn,ou=Eltern,$dn"
+else
+       dn="cn=$givenName $sn,ou=People,dc=teckids,dc=org"
+fi
+
+asso_sets "$givenName $sn" fields cn v
+
+ldif=
+(( eltern_subfilter )) && ldif+="dn: $tdn
+changetype: add
+objectClass: organizationalUnit
+ou: Eltern
+
+"
+
+ldif+="dn: $dn
+changetype: add
+objectClass: inetOrgPerson
+objectClass: teckidsPerson
+"
+
+(( eltern_subfilter )) && ldif+="objectClass: teckidsParent
+"
+
+asso_loadk fields
+for f in "${asso_y[@]}"; do
+       ldif+="$f: $(asso_getv fields "$f" v)
+"
+done
+
+ldapmodify -c <<EOF
+$ldif
+EOF
+
+mksh "$(dirname "$0")/whois" dn "$dn"
diff --git a/mksh/teckids/aliaseltern b/mksh/teckids/aliaseltern
new file mode 100644 (file)
index 0000000..17cd4d3
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2015
+#      Dominik George <dominik.george@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that aliases Eltern records from a marked
+# user to the current user.
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+[[ -s $TECKIDS_CACHE_DIR/marked_whois ]] || \
+    die "Missing “teckids setmark” information"
+[[ -s $TECKIDS_CACHE_DIR/last_whois ]] || \
+    die "Missing last “teckids whois” information"
+
+from=$(cat "$TECKIDS_CACHE_DIR"/marked_whois)
+to=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+
+ldif="dn: ou=Eltern,$to
+changetype: add
+objectClass: organizationalUnit
+ou: Eltern
+
+"
+
+asso_setldap_sasl users -- -b "ou=Eltern,$from" "(objectClass=inetOrgPerson)"
+teckids_loadk_users
+for user_dn in "${asso_y[@]}"; do
+       cn=$(asso_getv users "$user_dn" cn 0)
+       ldif+="dn: cn=$cn,ou=Eltern,$to
+changetype: add
+aliasedObjectName: $user_dn
+cn: $cn
+objectClass: alias
+objectClass: extensibleObject
+objectClass: top
+
+"
+done
+
+ldapmodify -c <<EOF
+$ldif
+EOF
+
+exit 0
diff --git a/mksh/teckids/astat b/mksh/teckids/astat
new file mode 100644 (file)
index 0000000..71b1a32
--- /dev/null
@@ -0,0 +1,278 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2016
+#      Thorsten “mirabilos” Glaser <thorsten.glaser@teckids.org>
+# Copyright © 2014, 2015
+#      Thorsten “mirabilos” Glaser <tg@mirbsd.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+#-
+# Age statistic analysis
+
+[[ -n $TECKIDS_COMMON ]] && . "$TECKIDS_COMMON"
+export LC_ALL=C.UTF-8
+nl='
+'
+mydir=$(realpath "$0"/..)
+
+# call this with desired scale and bc formula
+function bcfixup {
+       REPLY=$(bcfixup2 "$@")
+       [[ $REPLY = .* ]] && REPLY=0$REPLY
+}
+function bcfixup2 {
+       local bcf=$2 dscale=$1 cs cl epsilon=0. i=$1
+
+       while (( i-- > 0 )); do
+               epsilon+=0
+       done
+       epsilon+=5
+
+       bc <<-EOF
+               scale = 20 + ($dscale)
+               x = ($bcf)
+               if (x > 0) x += $epsilon
+               if (x < 0) x -= $epsilon
+               scale = ($dscale)
+               print (x / 1)
+       EOF
+}
+
+function x_select {
+       local fo=
+       while read -Ar line; do
+               if [[ -z $fo ]]; then
+                       if [[ ${line[0]} != ℹ ]]; then
+                               print -ru2 "E: invalid first column"
+                               exit 1
+                       fi
+                       set -A fo -- 0
+                       for x in "$@"; do
+                               i=${#line[@]}
+                               while (( i-- )); do
+                                       if [[ ${line[i]} = "$x" ]]; then
+                                               fo+=($i)
+                                               break
+                                       fi
+                               done
+                       done
+               fi
+
+               set -A oline
+               for x in "${fo[@]}"; do
+                       oline+=("${line[x]}")
+               done
+               print -r -- "${oline[@]}"
+       done | column -t
+}
+
+Ja=1
+Nein=0
+
+set -A ages
+amin=999
+amax=0
+asum=0
+asqs=0
+acnt=0
+atot=0
+
+input=$(cat; print x)
+
+set -A revs
+[[ -n $veranst_dt ]] && set -A revs -- $(HOME=/nonexistent/really-nonexistent \
+    git log --date=iso --pretty=tformat:%H,%cd \
+    --author-date-order --reverse -- planung.txt | sed 's/ .*$//')
+if (( ${#revs[*]} )); then
+       set -A dws
+       set -A vst -- $veranst_dt
+       set -A vst -- $(mjd_implode 0 0 0 ${vst[2]} $((vst[1] - 1)) $((vst[0] - 1900)))
+       last=
+       for rev in "${revs[@]}" -; do
+               print -nu2 I: Updating statistics... $((++revnum))/${#revs[*]}'\r'
+               if [[ $rev = - ]]; then
+                       set -A ent -- $(timet2mjd $(date +%s))
+                       cat planung.txt |&
+               else
+                       ent=${rev#*,}
+                       rev=${rev%,*}
+                       set -A ent -- ${ent//-/ }
+                       set -A ent -- $(mjd_implode 0 0 0 ${ent[2]} $((ent[1] - 1)) $((ent[0] - 1900)))
+                       git show "$rev":./planung.txt |&
+               fi
+               ent=$(((vst[0] - ent[0]) / 7))
+               s=0
+               while IFS= read -pr line; do
+                       case $s:$line {
+                       (0:+==*)
+                               s=1
+                               ;;
+                       (1:*Message-ID*Geschlecht*Klasse*Alter*)
+                               tbl=$line
+                               IFS= read -pr line
+                               tbl+=$nl$line
+                               s=2
+                               ;;
+                       (1*)
+                               s=0
+                               ;;
+                       (2:'+'*)
+                               tbl+=$nl$line
+                               IFS= read -pr line
+                               IFS= read -pr line
+                               s=3
+                               ;;
+                       (2*)
+                               tbl+=$nl$line
+                               ;;
+                       }
+               done
+               if [[ $s != 3 ]]; then
+                       print -u2 E: Parse error in revision $rev, s=$s
+                       exit 1
+               fi
+               this=$(print -r -- "$tbl" | mksh "$mydir/tbl2kdmn" | \
+                   x_select Message-ID | while read x msgid; do
+                       print -r -- "$msgid"
+               done | sort)
+               comm -3 /dev/fd/4 /dev/fd/5 4<<<"$last" 5<<<"$this" |&
+               while IFS= read -pr line; do
+                       if [[ $line = ' '* ]]; then
+                               (( dws[ent]++ ))
+                       else
+                               (( dws[ent]-- ))
+                       fi
+               done
+               last=$this
+       done
+       set -A ent -- ${!dws[*]}
+       ent=${ent[${#ent[*]}-1]}
+       (( i = ent ))
+       while (( i-- > 0 )); do
+               (( dws[i] += dws[i + 1] ))
+       done
+       s=
+       t=
+       set -A lengths
+       (( i = ent + 1 ))
+       while (( i-- > 0 )); do
+               (( n = i > dws[i] ? i : dws[i] ))
+               lengths[i]=${#n}
+               typeset -R${#n} pad=$i
+               s+=\ $pad
+               typeset -R${#n} pad=${dws[i]:-0}
+               t+=\ $pad
+       done
+       s=$t$'\n'$s
+       l=1
+       while (( l )); do
+               (( i = ent + 1 ))
+               l=0 t=
+               while (( i-- > 0 )); do
+                       n=${dws[i]}
+                       (( l |= n ))
+                       case $((n)) {
+                       (0) x=\ ;;
+                       (1) x=▁ n=0 ;;
+                       (2) x=▂ n=0 ;;
+                       (3) x=▃ n=0 ;;
+                       (4) x=▄ n=0 ;;
+                       (5) x=▅ n=0 ;;
+                       (6) x=▆ n=0 ;;
+                       (7) x=▇ n=0 ;;
+                       (*) x=█; let n-=8 ;;
+                       }
+                       dws[i]=$n
+                       (( n = lengths[i] + 1 ))
+                       while (( n-- )); do
+                               t+=$x
+                       done
+               done
+               s=$t$'\n'$s
+       done
+       print -r -- "$s Wochen vor Veranstaltungsbeginn"
+fi
+
+print -nu2 'I: Updating statistics... (age)       \r'
+print -nr -- "${input%x}" | x_select Alter | {
+       read
+       while read x age; do
+               (( atot++ ))
+               [[ $age = . ]] && continue
+               (( ages[$age]++ ))
+               (( amin = (amin < age) ? amin : age ))
+               (( amax = (amax > age) ? amax : age ))
+               (( asum += age ))
+               (( asqs += age * age ))
+               (( acnt++ ))
+       done
+
+       (( amin == 999 )) && print 'Keine Alter bekannt.'
+       s=
+       t=
+       set -A lengths
+       (( i = amin - 1 ))
+       while (( ++i <= amax )); do
+               (( n = i > ages[i] ? i : ages[i] ))
+               lengths[i]=${#n}
+               typeset -R${#n} pad=$i
+               s+=\ $pad
+               typeset -R${#n} pad=${ages[i]:-0}
+               t+=\ $pad
+       done
+       s=$t$'\n'$s
+       l=1
+       while (( l )); do
+               (( i = amin - 1 ))
+               l=0 t=
+               while (( ++i <= amax )); do
+                       n=${ages[i]}
+                       (( l |= n ))
+                       case $((n)) {
+                       (0) x=\ ;;
+                       (1) x=▁ n=0 ;;
+                       (2) x=▂ n=0 ;;
+                       (3) x=▃ n=0 ;;
+                       (4) x=▄ n=0 ;;
+                       (5) x=▅ n=0 ;;
+                       (6) x=▆ n=0 ;;
+                       (7) x=▇ n=0 ;;
+                       (*) x=█; let n-=8 ;;
+                       }
+                       ages[i]=$n
+                       (( n = lengths[i] + 1 ))
+                       while (( n-- )); do
+                               t+=$x
+                       done
+               done
+               s=$t$'\n'$s
+       done
+       print -r -- "$s Jahre (Alter der Teilnehmer)"
+       if (( amin == 999 )); then
+               amin=0
+               aavg=0
+               asdv=0
+       else
+               aavg=${|bcfixup 2 "$asum/$acnt";}
+               asdv=${|bcfixup 2 "sqrt($asqs / $acnt - ($asum / $acnt) * ($asum / $acnt))";}
+       fi
+       asamples=$((acnt))
+       (( acnt == atot )) || asamples+=" (of $((atot)))"
+       print
+       print "Minimum:         $((amin))               Maximum:        $((amax))"
+       print "Average:         $aavg           Samples:        $asamples"
+       print "Standard deviation:      $asdv"
+}
diff --git a/mksh/teckids/gebtag b/mksh/teckids/gebtag
new file mode 100644 (file)
index 0000000..6318853
--- /dev/null
@@ -0,0 +1,82 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2015
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+cmd_options='y\ 1fullyear\ 10\ 1Ausgabe für ein ganzes Jahr'
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+. "$(dirname "$0")/rplanung"
+
+if [[ -z $ldap_base ]]; then
+       print_v "Lade LDAP-Gruppe ..."
+       asso_setldap_sasl groups -- -b ou=Groups,dc=teckids,dc=org \
+           '(&(objectClass=groupOfNames)(cn='$project_cn'))'
+
+       asso_loadk groups
+       pdn=${asso_y[0]}
+fi
+
+base=${ldap_base:-ou=People,dc=teckids,dc=org}
+filter="(&(objectClass=inetOrgPerson)(memberOf=$pdn))"
+[[ -n $pdn ]] || filter='(objectClass=inetOrgPerson)'
+filter=${ldap_filter:-$filter}
+
+set -A dtoday -- $(date +'%d %m %Y')
+
+set -A jtoday -- $(mjd_implode 0 0 0 ${dtoday[0]} $((dtoday[1] - 1)) \
+    $((dtoday[2] - 1900)) 0 0 0 0 0)
+
+if (( fullyear )); then
+       jbeg=$jtoday
+       set -A tm -- $(mjd_explode $jtoday 0)
+       (( tm[tm_year]++ ))
+       set -A jend -- $(mjd_implode "${tm[@]}")
+else
+       (( jbeg = jtoday - 6 ))
+       (( jend = jtoday + 6 ))
+fi
+
+typeset -i10 -Z2 Td Tm
+typeset -i10 -Z4 Ty Uy
+
+(( j = jbeg - 1 ))
+while (( ++j <= jend )); do
+       set -A tm -- $(mjd_explode $j 0)
+       (( Td = tm[tm_mday] ))
+       (( Tm = tm[tm_mon] + 1 ))
+       (( Ty = tm[tm_year] + 1900 ))
+       print_v "Teste $Td.$Tm.$Ty <(&${filter}(dateOfBirth=*-$Tm-$Td))>"
+
+       asso_setldap_sasl users -- -b "$base" \
+           "(&${filter}(dateOfBirth=*-$Tm-$Td))" cn dateOfBirth
+       teckids_loadk_users
+       for user_dn in "${asso_y[@]}"; do
+               dob=$(asso_getv users "$user_dn" dateOfBirth 0)
+               cn=$(asso_getv users "$user_dn" cn 0)
+               set -A Ud -- ${dob//-/ }
+               (( Uy = Ud[0] ))
+               (( age = Ty - Uy ))
+               v=; (( verbose )) && v=" <$user_dn>"
+               print -r -- "${Uy}\ 1${mirtime_months[tm[tm_mon]]} $Td    $cn" \
+                   "($Uy AD; ${age}yo)$v"
+       done | LC_ALL=de_DE.UTF-8 sort | sed 's/^[^\ 1]*\ 1//'
+done
diff --git a/mksh/teckids/getphoto b/mksh/teckids/getphoto
new file mode 100644 (file)
index 0000000..1eab0a5
--- /dev/null
@@ -0,0 +1,43 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that displays the JPEG photo of a user
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if (( $# == 0 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+else
+       dn=$1
+fi
+
+asso_setldap_sasl users -- -b "$dn" -s base
+
+teckids_loadk_users
+for user_dn in "${asso_y[@]}"; do
+       jpegPhoto=$(asso_getv users "$user_dn" jpegPhoto 0)
+       [[ -z "$jpegPhoto" ]] && continue
+       Lb64decode "$jpegPhoto"
+done
+
+exit 0
diff --git a/mksh/teckids/getpost b/mksh/teckids/getpost
new file mode 100644 (file)
index 0000000..66973c8
--- /dev/null
@@ -0,0 +1,47 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that prints all known postal addresses
+# of the currently whois’d user
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+value=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+value=${value#*,ou=Eltern,}
+
+asso_setldap_sasl users -- -b "$value" -s base
+kids_only=0
+eltern_subfilter=2
+teckids_loadk_users
+
+for user_dn in "${asso_y[@]}"; do
+       givenName=$(asso_getv users "$user_dn" givenName 0)
+       sn=$(asso_getv users "$user_dn" sn 0)
+       homePostalAddress=$(asso_getv users "$user_dn" homePostalAddress 0)
+
+       print -r -- "$givenName $sn"
+       while [[ $homePostalAddress = *', '* ]]; do
+               print -r -- "${homePostalAddress%%, *}"
+               homePostalAddress=${homePostalAddress#*, }
+       done
+       [[ -n $homePostalAddress ]] && print -r -- "$homePostalAddress"
+       print
+done
diff --git a/mksh/teckids/groupadd b/mksh/teckids/groupadd
new file mode 100644 (file)
index 0000000..d8ed10d
--- /dev/null
@@ -0,0 +1,57 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that adds a DN to a group
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if (( $# == 1 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+       cn=$1
+elif (( $# == 2 )); then
+       dn=$1
+       cn=$2
+else
+       print -u2 "Der Gruppenname muss angegeben werden!"
+       exit 100
+fi
+
+asso_setldap_sasl groups -- -b ou=Groups,dc=teckids,dc=org \
+    "(cn=$cn)"
+
+asso_loadk groups
+gdn=${asso_y[0]}
+
+if [[ -z $gdn ]]; then
+       print -u2 "Gruppe existiert nicht!"
+       exit 101
+fi
+
+
+ldapmodify <<EOF
+dn: $gdn
+changetype: modify
+add: member
+member: $dn
+-
+EOF
+
+exit 0
diff --git a/mksh/teckids/groupdel b/mksh/teckids/groupdel
new file mode 100644 (file)
index 0000000..3e0a15b
--- /dev/null
@@ -0,0 +1,57 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that adds a DN to a group
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if (( $# == 1 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+       cn=$1
+elif (( $# == 2 )); then
+       dn=$1
+       cn=$2
+else
+       print -u2 "Der Gruppenname muss angegeben werden!"
+       exit 100
+fi
+
+asso_setldap_sasl groups -- -b ou=Groups,dc=teckids,dc=org \
+    "(cn=$cn)"
+
+asso_loadk groups
+gdn=${asso_y[0]}
+
+if [[ -z $gdn ]]; then
+       print -u2 "Gruppe existiert nicht!"
+       exit 101
+fi
+
+
+ldapmodify <<EOF
+dn: $gdn
+changetype: modify
+delete: member
+member: $dn
+-
+EOF
+
+exit 0
diff --git a/mksh/teckids/intpasswd b/mksh/teckids/intpasswd
new file mode 100644 (file)
index 0000000..679619b
--- /dev/null
@@ -0,0 +1,130 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2017
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+cmd_options='U:\ 1username\ 1\ 1Vorgegebener Benutzername'
+cmd_options+=$'\n''P:\ 1password\ 1\ 1Vorgegebenes Passwort'
+cmd_options+=$'\n''W\ 1askpass\ 10\ 1Nach Passwort fragen'
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+highest_uid=$(terracmdn getent passwd | sort -t: -nk3 | tail -n -2 |&
+       IFS=: read -pA
+       echo ${REPLY[2]}
+    )
+
+if (( $# == 0 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+else
+       dn=$1
+fi
+
+asso_setldap_sasl users -- -b "$dn" -s base givenName sn
+
+function machmalklein {
+       local -l l=$1
+
+       l=${l//ä/ae}
+       l=${l//ö/oe}
+       l=${l//ü/ue}
+       l=${l//ß/ss}
+       l=${l//Ä/ae}
+       l=${l//Ö/oe}
+       l=${l//Ü/ue}
+       l=${l//ẞ/ss}
+       l=${l//[!0-9a-zA-Z]}
+
+       REPLY=$l
+}
+
+function username_used {
+       local ln=$1
+       terracmdn getent passwd $ln
+       return $?
+}
+
+gn=$(asso_getv users "$dn" givenName 0)
+sn=$(asso_getv users "$dn" sn 0)
+
+gn_klein=${|machmalklein "$gn";}
+sn_klein=${|machmalklein "$sn";}
+
+if [[ -z $username ]]; then
+       lg=${#gn}
+       i=0
+       found=0
+       while (( ++i <= lg )); do
+               ln=${gn_klein::i}$sn_klein
+               if ! username_used "$ln"; then
+                       # wish I had goto here…
+                       found=1
+                       break
+               fi
+       done
+       if (( !found )); then
+               i=0
+               ln=${gn_klein::1}$sn_klein
+               while username_used "$ln$i"; do
+                       let i++
+               done
+               ln+=$i
+       fi
+else
+       ln=$username
+fi
+
+if [[ -z $password ]]; then
+       pw=$(pwgen -B 8 1)
+else
+       pw=$password
+fi
+
+ldapmodify <<EOF
+dn: $dn
+changetype: modify
+add: objectClass
+objectClass: posixAccount
+objectClass: shadowAccount
+-
+add: uid
+uid: $ln
+-
+add: uidNumber
+uidNumber: $((++highest_uid))
+-
+add: gidNumber
+gidNumber: 100
+-
+add: homeDirectory
+homeDirectory: /home/$ln
+-
+add: loginShell
+loginShell: /bin/bash
+-
+add: userPassword
+userPassword: $pw
+-
+EOF
+rv=$?
+(( rv )) && exit $rv
+
+(( !askpass )) || mksh "$(dirname "$0")/chpasswd" "$dn"
diff --git a/mksh/teckids/kalics b/mksh/teckids/kalics
new file mode 100644 (file)
index 0000000..7a1a3ed
--- /dev/null
@@ -0,0 +1,191 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2016
+#      Thorsten “mirabilos” Glaser <t.glaser@tarent.de>
+# Copyright © 2017
+#      Thorsten Glaser <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+cd "$ROOT"/intern
+if ! mtime=$(git log -1 --format=%cd --date=unix -- jahresplan.txt) || \
+    [[ $mtime != [1-9]+([0-9]) ]]; then
+       print -ru2 E: cannot stat teckids/intern/jahresplan.txt
+       exit 1
+fi
+
+function phpescape {
+       local s
+
+       s="$*"
+       s=${s//\\/\\\\}
+       s=${s//"'"/\\\'}
+       REPLY=\'$s\'
+}
+
+while read datum typ label; do
+       case x$datum {
+       (x|x\#*)
+               continue
+               ;;
+       (xPlaner:)
+               continue
+               ;;
+       (xZeitraum:)
+               continue
+               ;;
+       (x2[0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])
+               ;;
+       (*)
+               print -ru2 "E: unbekannte Zeile:        $datum  $typ    $label"
+               exit 1
+               ;;
+       }
+       tf=false
+       tt=false
+       case $typ {
+       (Veranstaltung)
+               m='geplante Veranstaltung'
+               ;;
+       (Workday-normal)
+               m='großer Workday'
+               ;;
+       (Workday-klein)
+               m='kleiner Workday'
+               ;;
+       (noch-ungeplant)
+               m='ungeplante oder externe Veranstaltung'
+               ;;
+       (Gruppentreffen)
+               m='Gruppentreffen'
+               if [[ -z $label ]]; then
+                       print -ru2 "E: Gruppentreffen $datum ohne Label!"
+                       exit 1
+               fi
+               ;;
+       (Onlinemeeting)
+               m='Onlinemeeting'
+               if [[ -n $label ]]; then
+                       print -ru2 "E: Onlinemeeting $datum mit Label: $label"
+                       exit 1
+               fi
+               tf=17:00
+               tt=18:00
+               ;;
+       (*)
+               print -ru2 "E: unbekannter Typ: $datum  $typ    $label"
+               exit 1
+               ;;
+       }
+       case $label {
+       (VS)
+               label='Vorstandssitzung'
+               ;;
+       (PM)
+               label='Treffen der pädagogischen Leitung'
+               ;;
+       (VS/PM)
+               label='Vorstandssitzung und/oder Päda-Meeting'
+               ;;
+       (MGV)
+               label='Mitgliederversammlung'
+               ;;
+       (CLT)
+               label='Chemnitzer Linux-Tage'
+               ;;
+       }
+       [[ -n $label ]] && m+=": $label"
+       if [[ $tf = false ]]; then
+               if [[ $tt != false ]]; then
+                       print -ru2 E: internal error: tt not false
+                       exit 1
+               fi
+       else
+               tf=\'$tf\'
+               tt=\'$tt\'
+       fi
+       print -r -- "   array('$datum', $tf, $tt, ${|phpescape "$m";}),"
+done <jahresplan.txt | \
+    (print '<?p''hp\n$mtime='$mtime';\n$events = array('; cat; cat <<\EOF
+);
+
+ini_set('include_path', '/usr/share/php/sabre21');
+
+function __autoload($class_name) {
+       $fn = strtr($class_name, "\\", '/') . '.php';
+       //echo "D: autoload($class_name) -> '$fn'\n";
+       include($fn);
+}
+
+use Sabre\VObject;
+
+$tzid = 'Europe/Berlin';
+$tz = new \DateTimeZone($tzid);
+$gmt = new \DateTimeZone('UTC');
+$xnow = new \DateTime('@' . $mtime);
+$xnow->setTimeZone($gmt);
+
+$vcalendar = new VObject\Component\VCalendar();
+
+$vtimezone = $vcalendar->add('VTIMEZONE', array(
+       'TZID' => $tzid,
+       'X-LIC-LOCATION' => $tzid,
+    ));
+/* simple EU rule, for years from 2002 onwards */
+$vtimezone->add($vcalendar->createComponent('STANDARD', array(
+       'DTSTART' => '20011028T030000',
+       'TZOFFSETFROM' => '+0200',
+       'TZOFFSETTO' => '+0100',
+       'TZNAME' => 'CET',
+       'RRULE' => 'FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
+    )));
+$vtimezone->add($vcalendar->createComponent('DAYLIGHT', array(
+       'DTSTART' => '20020331T020000',
+       'TZOFFSETFROM' => '+0100',
+       'TZOFFSETTO' => '+0200',
+       'TZNAME' => 'CEST',
+       'RRULE' => 'FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
+    )));
+
+foreach ($events as $e) {
+       $vevent = $vcalendar->add('VEVENT', array(
+               'UID' => 'teckids-jahresplan:' . $e[0] . ':' .
+                   base64_encode($e[3]),
+               'DTSTAMP' => $xnow,
+               'SUMMARY' => $e[3],
+           ));
+       if ($e[1] === false) {
+               $beg = new \DateTime($e[0], $tz);
+               $beg->setTimeZone($tz);
+               $vevent->add('DTSTART', $beg, array('VALUE' => 'DATE'));
+               /* a whole-day event taking one day by default */
+       } else {
+               $beg = new \DateTime($e[0] . 'T' . $e[1], $tz);
+               $end = new \DateTime($e[0] . 'T' . $e[2], $tz);
+               $beg->setTimeZone($tz);
+               $end->setTimeZone($tz);
+               $vevent->add('DTSTART', $beg);
+               $vevent->add('DTEND', $end);
+       }
+}
+
+echo $vcalendar->serialize();
+exit(0);
+EOF
+) | php
diff --git a/mksh/teckids/kaltag b/mksh/teckids/kaltag
new file mode 100644 (file)
index 0000000..c33dd41
--- /dev/null
@@ -0,0 +1,98 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2017
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+set -A mon -- x Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
+
+while read datum typ label; do
+       case x$datum {
+       (x|x\#*)
+               continue
+               ;;
+       (xPlaner:)
+               continue
+               ;;
+       (xZeitraum:)
+               continue
+               ;;
+       (x2[0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9])
+               ;;
+       (*)
+               print -ru2 "E: unbekannte Zeile:        $datum  $typ    $label"
+               exit 1
+               ;;
+       }
+       case $typ {
+       (Veranstaltung)
+               m='geplante Veranstaltung'
+               ;;
+       (Workday-normal)
+               m='großer Workday'
+               ;;
+       (Workday-klein)
+               m='kleiner Workday'
+               ;;
+       (noch-ungeplant)
+               m='ungeplante oder externe Veranstaltung'
+               ;;
+       (Gruppentreffen)
+               m='Gruppentreffen'
+               if [[ -z $label ]]; then
+                       print -ru2 "E: Gruppentreffen $datum ohne Label!"
+                       exit 1
+               fi
+               ;;
+       (Onlinemeeting)
+               m='Onlinemeeting'
+               if [[ -n $label ]]; then
+                       print -ru2 "E: Onlinemeeting $datum mit Label: $label"
+                       exit 1
+               fi
+               ;;
+       (*)
+               print -ru2 "E: unbekannter Typ: $datum  $typ    $label"
+               exit 1
+               ;;
+       }
+       case $label {
+       (VS)
+               label='Vorstandssitzung'
+               ;;
+       (PM)
+               label='Treffen der pädagogischen Leitung'
+               ;;
+       (VS/PM)
+               label='Vorstandssitzung und/oder Päda-Meeting'
+               ;;
+       (MGV)
+               label='Mitgliederversammlung'
+               ;;
+       (CLT)
+               label='Chemnitzer Linux-Tage'
+               ;;
+       }
+       [[ -n $label ]] && m+=": $label"
+       m+=" (${datum%%-*})"
+       datum=${datum#*-}
+       m="${mon[10#${datum%-*}]} ${datum##*-}  $m"
+       print -r -- "$m"
+done <"$ROOT"/intern/jahresplan.txt
diff --git a/mksh/teckids/karte b/mksh/teckids/karte
new file mode 100644 (file)
index 0000000..188bca0
--- /dev/null
@@ -0,0 +1,144 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+teckids_sourcing_wrapper=1
+offline=1
+. "$(dirname "$0")/teckids"
+
+cd "$ROOT"
+
+exec >.tmp/karte.htm
+cat <<'EOF'
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta name="copyright" content="see mirkarte.js" />
+ <title>Teckids Karte (Beta)</title>
+ <link rel="stylesheet" type="text/css" href="../karte/leaflet/leaflet.css" />
+ <style type="text/css"><!--/*--><![CDATA[/*><!--*/
+  #nomap {
+       padding:12px;
+       margin:12px;
+       border:1px solid black;
+       width:24em;
+  }
+  #map {
+       height:100%;
+       width:100%;
+       position:relative;
+  }
+  #map_coors {
+       position:fixed;
+       right:0; bottom:16px;
+       padding:6px;
+       font:12px monospace, sans-serif;
+       text-align:right;
+       z-index:3;
+  }
+  #map_coors span {
+       background:rgba(255, 255, 255, 0.33);
+  }
+  #map_wrapper {
+       position:absolute;
+       top:0; left:0;
+       bottom:0; right:0;
+  }
+  .myzoomcontrol-text {
+       font:bold 14px 'Lucida Console', Monaco, monospace;
+       text-align:center;
+       vertical-align:middle;
+  }
+  .nowrap {
+       white-space:nowrap;
+  }
+ /*]]>*/--></style>
+ <script type="text/javascript" src="../karte/leaflet/leaflet-src.js"></script>
+ <script type="text/javascript" src="../karte/prototype/prototype.js"></script>
+ <script type="text/javascript"><!--//--><![CDATA[//><!--
+ mirkarte_default_loc = [50.72205, 7.06162, 12];
+ function mirkarte_hookfn(map) {
+       var markers = [
+EOF
+
+while IFS= read -r line; do
+       coors=${line%% *}
+       print -r -- "           [[$coors], $(json_escape "${line#* }")],"
+done
+
+cat <<'EOF'
+               [[666,0], ""]
+       ];
+       var i = 0;
+       var xn = -1000, xe = -1000, xs = 1000, xw = 1000;
+       var mseen = {};
+       while (markers[i][0][0] != 666) {
+               var mtext = markers[i][0][0] + ',' + markers[i][0][1];
+               if (mtext in mseen) {
+                       mseen[mtext].setContent(mseen[mtext].getContent() +
+                           '<hr />' + markers[i][1]);
+                       i++;
+                       continue;
+               }
+               if (markers[i][0][0] > xn)
+                       xn = markers[i][0][0];
+               if (markers[i][0][0] < xs)
+                       xs = markers[i][0][0];
+               if (markers[i][0][1] > xe)
+                       xe = markers[i][0][1];
+               if (markers[i][0][1] < xw)
+                       xw = markers[i][0][1];
+               var marker = L.marker(markers[i][0], {
+                       "draggable": false
+                   }).addTo(map);
+               marker.bindPopup(mseen[mtext] = new L.Popup({
+                   }, marker).setContent(markers[i][1]));
+               i++;
+       }
+       if (xn != -1000 && xe != -1000 && xs != 1000 && xw != 1000) {
+               nextpos = [[[xs, xw], [xn, xe]], {
+                       "padding": [48, 48],
+                       "maxZoom": 14
+                   }];
+               jumptonextpos();
+       }
+ }
+ //--><!]]></script>
+ <script type="text/javascript" src="../karte/mirkarte.js"></script>
+</head><body>
+<div id="map_wrapper">
+ <div id="map">
+  <p id="nomap">
+   This is an interactive map application called “MirKarte”.
+   Unfortunately, it is implemented completely client-side
+   in JavaScript – so, you have to enable that, and use a
+   GUI webbrowser supported by Leaflet and Prototype.
+  </p>
+ </div>
+ <div id="map_coors">
+  <span id="map_coors_ns"></span><br />
+  <span id="map_coors_we"></span>
+ </div>
+</div>
+</body></html>
+EOF
+exec >&2
+
+"${BROWSER:-sensible-browser}" "file://$(realpath .tmp/karte.htm)"
diff --git a/mksh/teckids/lizenz b/mksh/teckids/lizenz
new file mode 100644 (file)
index 0000000..9b8f07c
--- /dev/null
@@ -0,0 +1,68 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+comm='#'
+case $1 {
+(py*3)
+       shebang='/usr/bin/env python3'
+       mode=py
+       ;;
+(py*)
+       shebang='/usr/bin/env python'
+       mode=py
+       ;;
+(sh)
+       shebang='/bin/sh'
+       mode=sh
+       ;;
+(tex|latex)
+       shebang=
+       mode=tex
+       comm=%
+       ;;
+(*)
+       shebang='/bin/mksh'
+       mode=sh
+       ;;
+}
+
+cat <<EOF
+${shebang:+#!$shebang
+}${comm} -*- mode: $mode -*-
+${comm}-
+${comm} Copyright © $(date +%Y)
+${comm}        $(git config --get user.name) <$(git config --get user.email)>
+${comm}
+${comm} Provided that these terms and disclaimer and all copyright notices
+${comm} are retained or reproduced in an accompanying document, permission
+${comm} is granted to deal in this work without restriction, including un‐
+${comm} limited rights to use, publicly perform, distribute, sell, modify,
+${comm} merge, give away, or sublicence.
+${comm}
+${comm} This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+${comm} the utmost extent permitted by applicable law, neither express nor
+${comm} implied; without malicious intent or gross negligence. In no event
+${comm} may a licensor, author or contributor be held liable for indirect,
+${comm} direct, other damage, loss, or other issues arising in any way out
+${comm} of dealing in the work, even if advised of the possibility of such
+${comm} damage or existence of a defect, except proven that it results out
+${comm} of said person’s immediate fault when using the work as intended.
+${comm}-
+EOF
diff --git a/mksh/teckids/mailadd b/mksh/teckids/mailadd
new file mode 100644 (file)
index 0000000..ba3490b
--- /dev/null
@@ -0,0 +1,48 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015
+#      Dominik George <dominik.george@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that adds a mailLocalAddress to the whois'd entry
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if (( $# < 2 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+       m=$1
+else
+       dn=$1
+       m=$2
+fi
+
+ldapmodify -c <<EOF
+dn: $dn
+changetype: modify
+add: objectClass
+objectClass: inetLocalMailRecipient
+-
+
+dn: $dn
+changetype: modify
+add: mailLocalAddress
+mailLocalAddress: $m
+-
+EOF
+
+exit 0
diff --git a/mksh/teckids/mk/assockit.ksh b/mksh/teckids/mk/assockit.ksh
new file mode 120000 (symlink)
index 0000000..828f194
--- /dev/null
@@ -0,0 +1 @@
+../../assockit.ksh
\ No newline at end of file
diff --git a/mksh/teckids/mk/assoldap.ksh b/mksh/teckids/mk/assoldap.ksh
new file mode 120000 (symlink)
index 0000000..5b2da68
--- /dev/null
@@ -0,0 +1 @@
+../../assoldap.ksh
\ No newline at end of file
diff --git a/mksh/teckids/mk/base64.ksh b/mksh/teckids/mk/base64.ksh
new file mode 100644 (file)
index 0000000..eec4c4c
--- /dev/null
@@ -0,0 +1,89 @@
+# -*- mode: sh -*-
+
+# From MirOS: src/bin/mksh/dot.mkshrc,v 1.68 2011/11/25 23:58:04 tg Exp $
+#-
+# Copyright (c) 2002, 2003, 2004, 2006, 2007, 2008, 2009, 2010,
+#              2011, 2014
+#      Thorsten Glaser <tg@mirbsd.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un-
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided "AS IS" and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person's immediate fault when using the work as intended.
+#-
+# RFC compliant base64 encoder and decoder
+
+if [[ -z $TOP ]]; then
+       TOP=$(realpath .)
+       while [[ ! -e $TOP/gnu.mk ]]; do
+               TOP=$(realpath "$TOP"/..)
+       done
+       export TOP=$(realpath "$TOP"/www)
+fi
+
+if whence -p b64decode >/dev/null 2>&1; then
+       function Lb64decode {
+               local s="$*"
+#              [[ -n $s ]] || { s=$(cat;print x); s=${s%x}; }
+
+               print -nr -- "$s" | b64decode -r
+       }
+elif whence -p base64 >/dev/null 2>&1; then
+       function Lb64decode {
+               local s="$*"
+               print -nr -- "$s" | base64 -d -i
+       }
+else
+       # technically, we could use the pure-shell code,
+       # but that would make some LDAP things very slow
+       print -u2 'E: Need b64decode (MirBSD) or base64 (GNU coreutils)!'
+       exit 1
+fi
+
+set -A Lb64encode_code -- A B C D E F G H I J K L M N O P Q R S T U V W X Y Z \
+    a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9 + /
+# NUL safe base64 encoder, needs mksh R40
+function Lb64encode {
+       [[ -o utf8-mode ]]; local u=$?
+       set +U
+       local c s t
+       if (( $# )); then
+               read -raN-1 s <<<"$*"
+               unset s[${#s[*]}-1]
+       else
+               read -raN-1 s
+       fi
+       local -i i=0 n=${#s[*]} j v
+
+       while (( i < n )); do
+               (( v = s[i++] << 16 ))
+               (( j = i < n ? s[i++] : 0 ))
+               (( v |= j << 8 ))
+               (( j = i < n ? s[i++] : 0 ))
+               (( v |= j ))
+               t+=${Lb64encode_code[v >> 18]}${Lb64encode_code[v >> 12 & 63]}
+               c=${Lb64encode_code[v >> 6 & 63]}
+               if (( i <= n )); then
+                       t+=$c${Lb64encode_code[v & 63]}
+               elif (( i == n + 1 )); then
+                       t+=$c=
+               else
+                       t+===
+               fi
+               if (( ${#t} == 76 || i >= n )); then
+                       print $t
+                       t=
+               fi
+       done
+       (( u )) || set -U
+}
diff --git a/mksh/teckids/mk/common b/mksh/teckids/mk/common
new file mode 100644 (file)
index 0000000..3068565
--- /dev/null
@@ -0,0 +1,343 @@
+# -*- mode: sh -*-
+
+rcsid_common='$MirOS: www/mk/common,v 1.7 2014/01/24 18:44:39 tg Exp $'
+#-
+# Copyright © 2007, 2008, 2012, 2013, 2014, 2015
+#      Thorsten “mirabilos” Glaser <tg@mirbsd.org>
+# Copyright © 2015
+#      Dominik George <dominik.george@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# unbreak bugs due to running BSD code in a GNU environment, early.
+export LC_ALL=C
+
+# RFC 2396 and some optional characters _plus_ apostrophe
+# -> escapes all shell meta-characters as well
+function uri_escape {
+       if (( $# )); then
+               print -nr -- "$@"
+       else
+               cat
+       fi | sed -e '
+           s.%.%25.g
+           s.;.%3B.g
+           s./.%2F.g
+           s.?.%3F.g
+           s.:.%3A.g
+           s.@.%40.g
+           s.&.%26.g
+           s.=.%3D.g
+           s.+.%2B.g
+           s.\$.%24.g
+           s.,.%2C.g
+           s.  .%09.g
+           s. .%20.g
+           s.<.%3C.g
+           s.>.%3E.g
+           s.#.%23.g
+           s.".%22.g
+           s.{.%7B.g
+           s.}.%7D.g
+           s.|.%7C.g
+           s.\\.%5C.g
+           s.\^.%5E.g
+           s.\[.%5B.g
+           s.\].%5D.g
+           s.`.%60.g
+           s.'\''.%27.g
+       '
+}
+
+# escape XHTML characters (three mandatory XML ones plus double quotes,
+# the latter in an XML safe fashion numerically though)
+function xhtml_escape {
+       if (( $# )); then
+               print -nr -- "$@"
+       else
+               cat
+       fi | sed \
+           -e 's\ 1&\ 1\&amp;\ 1g' \
+           -e 's\ 1<\ 1\&lt;\ 1g' \
+           -e 's\ 1>\ 1\&gt;\ 1g' \
+           -e 's\ 1"\ 1\&#34;\ 1g'
+}
+
+# escape string into JSON string (with surrounding quotes)
+function json_escape {
+       [[ -o utf8-mode ]]; local u=$?
+       set -U
+       local o=\" s
+       if (( $# )); then
+               read -raN-1 s <<<"$*"
+               unset s[${#s[*]}-1]
+       else
+               read -raN-1 s
+       fi
+       local -i i=0 n=${#s[*]} wc
+       local -Uui16 -Z7 x
+       local -i1 ch
+
+       while (( i < n )); do
+               (( ch = x = wc = s[i++] ))
+               case $wc {
+               (8) o+=\\b ;;
+               (9) o+=\\t ;;
+               (10) o+=\\n ;;
+               (12) o+=\\f ;;
+               (13) o+=\\r ;;
+               (34) o+=\\\" ;;
+               (92) o+=\\\\ ;;
+               (*)
+                       if (( wc < 0x20 || wc > 0xFFFD || \
+                           (wc >= 0xD800 && wc <= 0xDFFF) || \
+                           (wc > 0x7E && wc < 0xA0) )); then
+                               o+=\\u${x#16#}
+                       else
+                               o+=${ch#1#}
+                       fi
+                       ;;
+               }
+       done
+       (( u )) && set +U
+       print -nr -- "$o\""
+}
+
+# escape ampersands for sed (pipe only: post-processing)
+function sed_escape {
+       sed -e 's\ 1&\ 1\\\&\ 1g'
+}
+
+# magic from MirOS: src/kern/c/mirtime.c,v 1.3 2011/11/20 23:40:10 tg Exp $
+
+# struct tm members and (POSIX) time functions
+typeset -ir tm_sec=0           # seconds [0-59]
+typeset -ir tm_min=1           # minutes [0-59]
+typeset -ir tm_hour=2          # hours [0-23]
+typeset -ir tm_mday=3          # day of month [1-31]
+typeset -ir tm_mon=4           # month of year - 1 [0-11]
+typeset -ir tm_year=5          # year - 1900
+typeset -ir tm_wday=6          # day of week [0 = sunday]      input:ignored
+typeset -ir tm_yday=7          # day of year [0-365]           input:ignored
+typeset -ir tm_isdst=8         # summer time act.? [0/1] (0)   input:ignored
+typeset -ir tm_gmtoff=9                # seconds offset from UTC (0)
+typeset -ir tm_zone=10         # abbrev. of timezone ("UTC")   input:ignored
+
+set -A mirtime_months -- Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
+set -A mirtime_wdays -- Sun Mon Tue Wed Thu Fri Sat
+readonly mirtime_months[*] mirtime_wdays[*]
+
+# $ timet2mjd posix_timet
+# ⇒ mjd sec
+function timet2mjd {
+       local -i10 mjd=$1 sec
+
+       (( sec = mjd % 86400 ))
+       (( mjd = (mjd / 86400) + 40587 ))
+       while (( sec < 0 )); do
+               (( --mjd ))
+               (( sec += 86400 ))
+       done
+
+       print -- $mjd $sec
+}
+
+# $ mjd2timet mjd sec
+# ⇒ posix_timet
+function mjd2timet {
+       local -i10 t=$1 sec=$2
+
+       (( t = (t - 40587) * 86400 + sec ))
+       print -- $t
+}
+
+# $ mjd_explode mjd sec
+# ⇒ tm_sec tm_min tm_hour tm_mday tm_mon tm_year \
+#   tm_wday tm_yday "0" "0" "UTC"
+function mjd_explode {
+       local tm
+       set -A tm
+       local -i10 sec=$2 day yday mon year=$1
+
+       while (( sec < 0 )); do
+               (( --year ))
+               (( sec += 86400 ))
+       done
+       while (( sec >= 86400 )); do
+               (( ++year ))
+               (( sec -= 86400 ))
+       done
+
+       (( day = year % 146097 + 678881 ))
+       (( year = 4 * ((year / 146097) + (day / 146097)) ))
+       (( day %= 146097 ))
+       (( tm[tm_wday] = (day + 3) % 7 ))
+       if (( day == 146096 )); then
+               (( year += 3 ))
+               (( day = 36524 ))
+       else
+               (( year += day / 36524 ))
+               (( day %= 36524 ))
+       fi
+       (( year = 4 * ((year * 25) + (day / 1461)) ))
+       (( day %= 1461 ))
+       (( yday = (day < 306) ? 1 : 0 ))
+       if (( day == 1460 )); then
+               (( year += 3 ))
+               (( day = 365 ))
+       else
+               (( year += day / 365 ))
+               (( day %= 365 ))
+       fi
+       (( yday += day ))
+       (( day *= 10 ))
+       (( mon = (day + 5) / 306 ))
+       (( day = ((day + 5) % 306) / 10 ))
+       if (( mon >= 10 )); then
+               (( mon -= 10 ))
+               (( yday -= 306 ))
+               (( ++year ))
+       else
+               (( mon += 2 ))
+               (( yday += 59 ))
+       fi
+       (( tm[tm_sec] = sec % 60 ))
+       (( sec /= 60 ))
+       (( tm[tm_min] = sec % 60 ))
+       (( tm[tm_hour] = sec / 60 ))
+       (( tm[tm_mday] = day + 1 ))
+       (( tm[tm_mon] = mon ))
+       (( tm[tm_year] = (year < 1 ? year - 1 : year) - 1900 ))
+       (( tm[tm_yday] = yday ))
+       (( tm[tm_isdst] = 0 ))
+       (( tm[tm_gmtoff] = 0 ))
+       tm[tm_zone]=UTC
+
+       print -r -- "${tm[@]}"
+}
+
+# $ mjd_implode tm_sec tm_min tm_hour tm_mday tm_mon tm_year \
+#   ignored ignored ignored tm_gmtoff [ignored]
+# ⇒ mjd sec
+function mjd_implode {
+       local tm
+       set -A tm -- "$@"
+       local -i10 day x y sec
+
+       (( sec = tm[tm_sec] + 60 * tm[tm_min] + 3600 * tm[tm_hour] - \
+           tm[tm_gmtoff] ))
+       (( (day = tm[tm_year] + 1900) < 0 )) && (( ++day ))
+       (( y = day % 400 ))
+       (( day = (day / 400) * 146097 - 678882 + tm[tm_mday] ))
+       while (( sec < 0 )); do
+               (( --day ))
+               (( sec += 86400 ))
+       done
+       while (( sec >= 86400 )); do
+               (( ++day ))
+               (( sec -= 86400 ))
+       done
+       (( x = tm[tm_mon] ))
+       while (( x < 0 )); do
+               (( --y ))
+               (( x += 12 ))
+       done
+       (( y += x / 12 ))
+       (( x %= 12 ))
+       if (( x < 2 )); then
+               (( x += 10 ))
+               (( --y ))
+       else
+               (( x -= 2 ))
+       fi
+       (( day += (306 * x + 5) / 10 ))
+       while (( y < 0 )); do
+               (( day -= 146097 ))
+               (( y += 400 ))
+       done
+       (( day += 146097 * (y / 400) ))
+       (( y %= 400 ))
+       (( day += 365 * (y % 4) ))
+       (( y /= 4 ))
+       (( day += 1461 * (y % 25) + 36524 * (y / 25) ))
+
+       print -- $day $sec
+}
+
+# convenience function to check (German or ISO) date input (no time-of-day)
+# input is $2, MJD is written to $$1, normalised date (if $3 is set) to $$3
+# in dmy form and if $4 is also set to $$4 in ymd form
+function dtchk {
+       local tm mjd x saveIFS r=${2//[  \r]}
+       set -A tm
+       set -A mjd
+       set -A x
+
+       errstr="'$r' not in DD.MM.YYYY or YYYY-MM-DD format"
+       saveIFS=$IFS
+       if [[ $r = [12][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9] ]]; then
+               IFS=-
+               set -A x -- $r
+               set -A x -- ${x[2]} ${x[1]} ${x[0]}
+       elif [[ $r = +([0-9]).+([0-9]).+([0-9]) ]]; then
+               IFS=.
+               set -A x -- $r
+       else
+               return 1
+       fi
+       IFS=$saveIFS
+       if (( x[2] > 0 && x[2] < 30 )); then
+               # accept year w/o leading 20xx
+               (( x[2] += 2000 ))
+       elif (( x[2] >= 30 && x[2] <= 99 )); then
+               # accept year w/o leading 19xx
+               (( x[2] += 1900 ))
+       fi
+       (( x[2] > 1000 )) || return 1
+       set -A tm -- 0 0 0 $((x[0])) $((x[1] - 1)) $((x[2] - 1900)) \
+           - - - 0 -
+       set -A mjd -- $(mjd_implode "${tm[@]}")
+       set -A x -- $(mjd_explode "${mjd[0]}" 0)
+       local -i10 -Z2 rd rm
+       local -i10 -Z4 ry
+       (( rd = x[tm_mday] ))
+       (( rm = x[tm_mon] + 1 ))
+       (( ry = x[tm_year] + 1900 ))
+       errstr="invalid date $r, normalises to $rd.$rm.$ry ${x[tm_hour]}:${x[tm_min]}:${x[tm_sec]}"
+       [[ ${tm[tm_hour]} = ${x[tm_hour]} ]] || return 1
+       [[ ${tm[tm_min]} = ${x[tm_min]} ]] || return 1
+       [[ ${tm[tm_sec]} = ${x[tm_sec]} ]] || return 1
+       errstr="bogus date $r, normalises to $rd.$rm.$ry"
+       [[ ${tm[tm_mday]##*(0)} = ${x[tm_mday]##*(0)} ]] && tm[tm_mday]=${x[tm_mday]}   # accept day w/o leading zeroes
+       [[ ${tm[tm_mday]} = ${x[tm_mday]} ]] || return 1
+       [[ ${tm[tm_mon]##*(0)} = ${x[tm_mon]##*(0)} ]] && tm[tm_mon]=${x[tm_mon]}       # accept month w/o leading zeroes
+       [[ ${tm[tm_mon]} = ${x[tm_mon]} ]] || return 1
+       [[ ${tm[tm_year]} = ${x[tm_year]} ]] || return 1
+       errstr=
+       nameref r=$1
+       r=${mjd[0]}
+       if [[ -n $3 ]]; then
+               nameref rr=$3
+               rr="$rd.$rm.$ry"
+               if [[ -n $4 ]]; then
+                       nameref rr=$4
+                       rr="$ry-$rm-$rd"
+               fi
+       fi
+       return 0
+}
+
+common_read=1
diff --git a/mksh/teckids/mkliste b/mksh/teckids/mkliste
new file mode 100644 (file)
index 0000000..ea71835
--- /dev/null
@@ -0,0 +1,358 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015, 2016
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that inserts LDAP data into a given LaTeX document
+# Used to produce lists of LDAP data
+
+cmd_options='c\ 1mangle_description\ 11\ 1Beschreibungs-Text bereinigen'
+cmd_options+=$'\n''=D:\ 1defines\ 1\ 1Extra-Platzhalter und Werte angeben'
+cmd_options+=$'\n''I\ 1insecure\ 10\ 1insecure mode: --shell-escape in LaTeX'
+cmd_options+=$'\n''M:\ 1override_month\ 1\ 1Monat angeben (statt aktueller)'
+cmd_options+=$'\n''m\ 1mangle_address\ 10\ 1Adressen aus homePostalAddress umbrechen'
+cmd_options+=$'\n''N\ 1notemplates\ 10\ 1Templates nicht vorher neubauen'
+cmd_options+=$'\n''s:\ 1sortfield\ 1sn\ 1Feld, nach dem die Liste sortiert werden soll'
+cmd_options+=$'\n''U:\ 1mapbaseurl\ 1http://staticmap.openstreetmap.de/staticmap.php?center=51.15,10.45&zoom=7&size=1200x1600&maptype=osmarender&markers=\ 1Basis-URL für statische Karte'
+cmd_options+=$'\n''Y:\ 1override_year\ 1\ 1Jahr angeben (statt aktuelles)'
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+[[ -n $1 ]] || usage
+
+insec=; (( insecure )) && insec=--shell-escape
+
+t_f=$(realpath "$1")
+deltmpl=
+
+if [[ $t_f = *.tex.in && -s $t_f && -f $t_f ]]; then
+       t_f=${t_f%.in}
+       if ! env "${defines[@]}" ROOT="$ROOT" TK_TMPL_MON="$override_month" \
+           TK_TMPL_YEAR="$override_year" "$MKSH" "$t_f.in" >"$t_f~"; then
+               rm -f "$t_f" "$t_f~"
+               die error running template "$t_f.in"
+       fi
+       mv -f "$t_f~" "$t_f"
+       deltmpl=$t_f
+fi
+
+t_b=${t_f##*/}
+t_p=${t_b%.*}
+
+for f in "$t_f" "docs/$t_b" "docs/$t_p.tex" "$ROOT/templates/$t_p.tex" "$ROOT/templates/template_$t_p.tex"; do
+       t_f=$f
+       [[ -e $t_f && -f $t_f ]] && break
+done
+
+if [[ ! -e $t_f || ! -f $t_f || ! -s $t_f ]]; then
+       print -ru2 "E: template $t_f not found"
+       exit 1
+fi
+
+print_v "Lese planung.txt ..."
+. "$(dirname "$0")/rplanung"
+
+asso_setasso dn2msgid
+
+asso_loadk planung
+print_v "Lade Personendaten zu ${#asso_y[*]} Anmelde-IDs ..."
+n=0; for msgid in "${asso_y[@]}"; do
+       print_v " Lade Eintrag Nr. $((++n)) ..."
+
+       asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+           "(anmMessageId=<$msgid*)"
+
+       asso_loadk users
+       asso_sets "$msgid" dn2msgid "${asso_y[0]}"
+done
+
+d=$(mktemp -d "$TOP/../.tmp/XXXXXXXXXX")
+set -A tmNow -- $(date +'%s %d.%m.%Y')
+dtNow=${tmNow[1]}
+set -A tmNow -- $(mjd_explode $(timet2mjd ${tmNow[0]}))
+
+if [[ -z $ldap_base ]]; then
+       print_v "Lade LDAP-Gruppe ..."
+       asso_setldap_sasl groups -- -b ou=Groups,dc=teckids,dc=org \
+           '(&(objectClass=groupOfNames)(cn='$project_cn'))'
+
+       asso_loadk groups
+       for group_dn in "${asso_y[@]}"; do
+               pdescription=$(asso_getv groups "$group_dn" description 0)
+               pjpegPhoto=$(asso_getv groups "$group_dn" jpegPhoto 0)
+               pimg=$TOP/pics/people
+               if [[ -n $pjpegPhoto ]]; then
+                       pimg=$project_cn
+                       Lb64decode "$pjpegPhoto" >"$d"/"$pimg.jpg"
+               fi
+               pdn=$group_dn
+               break
+       done
+else
+       pdescription=
+       pimg=unset-if-ldap_base-not-set
+fi
+
+base=${ldap_base:-ou=People,dc=teckids,dc=org}
+filter=${ldap_filter:-(&(objectClass=inetOrgPerson)(memberOf=$pdn))}
+
+print_v "Lade Daten der Gruppenmitglieder... (base $base) (filter $filter)"
+asso_setldap_sasl users -- -b "$base" "$filter"
+# load user data into "${asso_y[@]}"
+teckids_loadk_users
+count=${#asso_y[*]}
+
+[[ -e artwork ]] && ln -sf "$(realpath artwork)" "$d/artwork"
+[[ -e img ]] && ln -sf "$(realpath img)" "$d/img"
+
+print_v "Lade Template aus $d ..."
+# cannot use \n as field separator because it’s IFS_WS
+tr '\n' '\ 1' <"$t_f" |& IFS='\ 1' read -N -1 -Apr tpl
+# analyse template
+tplen=${#tpl[*]}
+hasbeg=0
+hasend=0
+hasmap=0
+i=-1
+while (( ++i < tplen )); do
+       if [[ ${tpl[i]} = '%% --repeat--' ]]; then
+               (( hasbeg || hasend )) && die "repeat begin found after other repeat instructions"
+               hasbeg=1
+               ibeg=$i
+               continue
+       elif [[ ${tpl[i]} = '%% --taeper--' ]]; then
+               (( hasend )) && die "repeat end found after other repeat end instruction"
+               (( hasbeg )) || die "repeat end found with no repeat begin instruction"
+               hasend=1
+               iend=$i
+               continue
+       fi
+       [[ ${tpl[i]} = *'--date--'* ]] && \
+           tpl[i]=${tpl[i]//--date--/$dtNow}
+       [[ ${tpl[i]} = *'--pdescription--'* ]] && \
+           tpl[i]=${tpl[i]//--pdescription--/$pdescription}
+       [[ ${tpl[i]} = *'--pimg--'* ]] && \
+           tpl[i]=${tpl[i]//--pimg--/$pimg}
+       if [[ ${tpl[i]} = *'--staticmap--'* ]]; then
+               hasmap=1
+               tpl[i]=${tpl[i]//--staticmap--/staticmap}
+       fi
+       [[ ${tpl[i]} = *'--count--'* ]] && \
+           tpl[i]=${tpl[i]//--count--/$count}
+done
+(( hasbeg ^ hasend )) && die "repeat begin found with no repeat end instruction"
+# now hasbeg==hasend is either 0 (multifile mode) or 1 (block-repeat mode)
+
+function substitute {
+       local f v s=$1
+
+       # handle arbitrary extra tags
+       for f in "${defines[@]}"; do
+               if [[ $f = *=* ]]; then
+                       v=${f#*=}
+                       f=${f%%=*}
+               else
+                       v=1
+               fi
+               s=${s//--"$f"--/$v}
+       done
+
+       # Remove unmatched tags
+       print -nr -- "${s//--+([!-])--}"
+}
+
+print_v "Erzeuge Dokument aus Template in $d ..."
+if (( hasbeg )); then
+       mapurl=$mapbaseurl
+       # open *.tex output file
+       exec >"$d/$t_p.tex"
+       # emit begin block
+       i=-1
+       while (( ++i < ibeg )); do
+               substitute "${tpl[i]}$nl"
+       done
+else
+       typeset -Uui16 -Z11 n=0
+       ibeg=-1
+       iend=$tplen
+       rm -rf "$t_p"
+       mkdir "$t_p"
+fi
+# load repeating part of template
+content=
+i=$ibeg
+while (( ++i < iend )); do
+       content+=${tpl[i]}$nl
+done
+
+# make templates first
+(( notemplates )) || (make templates || (cd "$ROOT/templates" && make all)) >&2
+
+# iterate over all entries
+for user_dn in "${asso_y[@]}"; do
+       print -r -- "$(asso_getv users "$user_dn" $sortfield 0)\ 1$(asso_getv \
+           users "$user_dn" uid 0)\ 1$user_dn"
+done | LC_ALL=de_DE.UTF-8 sort |&
+while IFS='\ 1' read -pr sn uid user_dn; do
+       if (( !hasbeg )); then
+               mapurl=$mapbaseurl
+               # open output file
+               [[ -n $uid ]] || uid=x${n#16#}
+               exec >"$d/$uid.tex"
+               let ++n
+       fi
+       contentr=$content
+       # modify content according to LDAP information
+       if [[ $contentr = *'--img--'* ]]; then
+               # CPU-intensive operation; only run when needed ☺
+               jpegPhoto=$(asso_getv users "$user_dn" jpegPhoto 0)
+               img=../../www/pics/people
+               if [[ -n $jpegPhoto ]]; then
+                       img=$RANDOM
+                       while [[ -e $d/$img.jpg ]]; do
+                               img=$RANDOM
+                       done
+                       Lb64decode "$jpegPhoto" >"$d/$img.jpg"
+               fi
+               contentr=${contentr//--img--/$img}
+       fi
+
+       if [[ $contentr = *'--Eltern_'* ]]; then
+               set -A eltern_fields
+               for eltern_field in $(print -r -- "$contentr" | grep -o -- '--Eltern_.*--'); do
+                       eltern_field=${eltern_field#--Eltern_}
+                       eltern_field=${eltern_field%--}
+                       eltern_fields+=($eltern_field)
+               done
+               asso_setldap_sasl parents -- -b "ou=Eltern,$user_dn" '(objectClass=inetOrgPerson)' "${eltern_fields[@]}" cn
+               asso_loadk parents
+               for eltern_field in "${eltern_fields[@]}"; do
+                       res=
+                       for parent_dn in "${asso_y[@]}"; do
+                               cn=$(asso_getv parents "$parent_dn" cn 0)
+                               field_value=$(asso_getv parents "$parent_dn" "$eltern_field" 0)
+                               [[ -n $field_value ]] && res+="$cn: $field_value, "
+                       done
+                       res=${res%, }
+                       res=${res//, /'\par{}'}
+                       contentr=${contentr//--Eltern_${eltern_field}--/$res}
+               done
+       fi
+
+       for f in "${fields[@]}"; do
+               msgid=$(asso_getv dn2msgid "$user_dn")
+               v=$(asso_getv planung "$msgid" "$f")
+               contentr=${contentr//--"$f"--/$v}
+       done
+
+       if asso_isset users "$user_dn" employeeNumber; then
+               v='\CheckedBox'
+       else
+               v='\Square'
+       fi
+       v="{\\Huge$v}"
+       contentr=${contentr//--isMember--/$v}
+
+       asso_loadk users "$user_dn"
+       xlat= xlon=
+       for f in "${asso_y[@]}"; do
+               v=$(asso_getv users "$user_dn" "$f" 0)
+
+               case $f {
+               (homePostalAddress)
+                       [[ $mangle_address = 1 ]] && v=${v/, /\\newline\{\}}
+                       ;;
+               (description)
+                       [[ $mangle_description = 1 ]] && v=${v//'<'*([!>])'>'/}
+                       ;;
+               (dateOfBirth)
+                       age=
+                       if [[ -n $v ]] && dtchk dtJ "$v" dtv; then
+                               set -A tmGeb -- $(mjd_explode "$dtJ" 0)
+                               (( age = tmNow[tm_year] - tmGeb[tm_year] - \
+                                   ((tmNow[tm_yday] < tmGeb[tm_yday]) ? 1 : 0) ))
+                       fi
+                       contentr=${contentr//--age--/$age}
+                       ;;
+               (teckidsLatitude)
+                       xlat=$v
+                       ;;
+               (teckidsLongitude)
+                       xlon=$v
+                       ;;
+               (userPassword)
+                       pp='***'
+                       [[ -n $v && $v != '{'* ]] && pp=$v
+                       contentr=${contentr//--plainPassword--/$pp}
+                       ;;
+               }
+               v=${v//&/\\&}
+               contentr=${contentr//--"$f"--/$v}
+       done
+
+       (( hasmap )) && \
+           [[ $xlat = ?(-)+([0-9])?(.+([0-9])) ]] && \
+           [[ $xlon = ?(-)+([0-9])?(.+([0-9])) ]] && \
+           mapurl+=$xlat,$xlon,lightblue\|
+
+       substitute "$contentr"
+       print_v " Eintrag Nr. $((++n)) erzeugt."
+       if (( !hasbeg )); then
+               # close output file
+               exec >&2
+               if (( hasmap )); then
+                       wget -O "$d/staticmap.png" "$mapurl" || \
+                           die "could not download static map $mapurl"
+               fi
+               # generate output PDF and copy out
+               (cd "$d" && mksh "$ROOT/.pdflatex_twice" "$uid.pdf" $insec) || \
+                   die "make \"$d/$uid.pdf\" died with errorlevel $?"
+               [[ -s $d/$uid.pdf ]] || die "zero-length output"
+               cp -f "$d/$uid.pdf" "$t_p/"
+       fi
+done
+if (( hasbeg )); then
+       # emit end block
+       i=$iend
+       while (( ++i < tplen )); do
+               substitute "${tpl[i]}$nl"
+       done
+       # close output file
+       exec >&2
+       if (( hasmap )); then
+               wget -O "$d/staticmap.png" "$mapurl" || \
+                   die "could not download static map $mapurl"
+       fi
+       # generate output PDF and copy out
+       (cd "$d" && mksh "$ROOT/.pdflatex_twice" "${t_p}.pdf" $insec) || \
+           die "make \"$d/${t_p}.pdf\" died with errorlevel $?"
+       [[ -s $d/${t_p}.pdf ]] || die "zero-length output"
+       cp -f "$d/${t_p}.pdf" ./
+fi
+
+if (( extradebug )); then
+       print -ru2 "I: temporary directory was ${d@Q}"
+       print -ru2 "N: please clean it up yourself!"
+       exit 0
+fi
+
+[[ -n $deltmpl ]] && rm -f "$deltmpl"
+rm -rf "$d"
+exit 0
diff --git a/mksh/teckids/mkrechnung b/mksh/teckids/mkrechnung
new file mode 100644 (file)
index 0000000..1438689
--- /dev/null
@@ -0,0 +1,291 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2016, 2017
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015, 2016, 2017
+#      Thorsten Glaser <thorsten.glaser@teckids.org>
+# Copyright © 2016
+#      Niklas Bildhauer <niklas.bildhauer@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+function split_addr {
+       local a=$addr s=$1
+
+       while [[ $a = *', '* ]]; do
+               print -r -- "${a%%, *}$s"
+               a=${a#*, }
+       done
+       [[ -n $a ]] && print -r -- "$a"
+}
+
+cd "$ROOT/finance/rechnungen"
+
+
+rg=$(<rgnr.txt)
+if [[ $rg != +([0-9]) ]]; then
+       print -ru2 "E: rgnr '$rg' ungültig!"
+       exit 1
+fi
+# Führende Nullen nicht oktal interpretieren. POSIX will das… ☹
+rg=$((10#$rg))
+
+# 1. Volle Anschrift des Empfängers (kann auch unvolständig sein) einlesen
+
+reqanschrift=1
+[[ -s $TECKIDS_CACHE_DIR/last_whois ]] || reqanschrift=2
+#"${VISUAL:-${EDITOR:-vi}}"
+dopersonenkonto=1
+while (( reqanschrift )); do
+       case $reqanschrift {
+       (4)
+               split_addr "" >"$ROOT"/.tmp/mkrechnung
+               "${VISUAL:-${EDITOR:-vi}}" "$ROOT"/.tmp/mkrechnung
+               addr=
+               while IFS= read -r line; do
+                       addr+=", $line"
+               done <"$ROOT"/.tmp/mkrechnung
+               rm -f "$ROOT"/.tmp/mkrechnung
+               addr=${addr#, }
+               reqanschrift=3
+               ;;
+       (3)
+               print -u2 Bitte bestätigen, 0 für Neueingabe
+               select x in "$addr" "(im Texteditor bearbeiten)"; do
+                       if [[ $REPLY = 0 ]]; then
+                               reqanschrift=2
+                               break
+                       fi
+                       if [[ $REPLY = 1 ]]; then
+                               reqanschrift=0
+                               break
+                       fi
+                       if [[ $REPLY = 2 ]]; then
+                               reqanschrift=4
+                               break
+                       fi
+               done
+               ;;
+       (2)
+               dopersonenkonto=0
+               print -u2 "Rechnungsempfänger eingeben, Ende mit leerer Zeile"
+               print -u2
+
+               addr=
+               while IFS= read -r line && [[ -n $line ]]; do
+                       addr+=", $line"
+               done
+               addr=${addr#, }
+               reqanschrift=3
+               ;;
+       (1)
+               $MKSH "$ROOT"/util/getpost |&
+               set -A askanschrift
+               naskanschrift=0
+               addr=
+               while IFS= read -pr line; do
+                       if [[ -n $line ]]; then
+                               addr+=", $line"
+                       else
+                               askanschrift[naskanschrift++]=${addr#, }
+                               addr=
+                       fi
+               done
+               print -u2 Bitte Rechnungsempfänger-Basisadresse auswählen, 0 für Neueingabe
+               select addr in "${askanschrift[@]}"; do
+                       if [[ $REPLY = 0 ]]; then
+                               reqanschrift=2
+                               break
+                       fi
+                       if [[ -n $addr ]]; then
+                               reqanschrift=3
+                               break
+                       fi
+               done
+               ;;
+       }
+done
+
+addr=$(split_addr "\\\\")
+
+# Personenkontonummer finden
+if (( dopersonenkonto )); then
+       asso_setldap_sasl users -- -b "$(cat "$TECKIDS_CACHE_DIR"/last_whois)" -s base
+       asso_loadk users "$(cat "$TECKIDS_CACHE_DIR"/last_whois)"
+       employeeNumber=$(asso_getv users "$(cat "$TECKIDS_CACHE_DIR"/last_whois)" employeeNumber 0)
+       cashAccount=$(asso_getv users "$(cat "$TECKIDS_CACHE_DIR"/last_whois)" teckidsCashAccount 0)
+fi
+kontostand=$("$ROOT/util/not_teckidscmd/gnc_balance" ${employeeNumber:-$cashAccount})
+[[ -z $kontostand ]] && kontostand=0.00
+
+# 2. Beliebig viele Artikel aus artikel.lst auswählen
+# 3. Artikelpreise summieren
+
+sepa=
+while [[ $sepa != @(j|y|n) ]]; do
+       print -n 'SEPA-Lastschriftmandat? '
+       read sepa
+done
+
+art=
+sum_0=000
+sum_7=000
+sum_19=000
+while cat artikel.lst; IFS= read -r \
+    artnr?"Artikelnummer (0 für Sonderposten, leer für Ende): "; do
+       [[ -n $artnr ]] || break
+       if [[ $artnr != +([0-9]) ]]; then
+               print -u2 "E: Artikelnummern sind Zahlen!"
+               sleep 2
+               continue
+       fi
+       if [[ $artnr = +(0) ]]; then
+               line=
+               while [[ -z $line ]]; do
+                       IFS= read -r line?"Artikelbezeichnung Sonderposten: "
+               done
+               dasfeld=
+               while [[ $dasfeld != ?(-)+([0-9]).[0-9][0-9] ]]; do
+                       IFS= read -r dasfeld?"Preis in Euro (xxxx.xx): "
+               done
+               ssatz=
+               while [[ $ssatz != 0 && $ssatz != 7 && $ssatz != 19 ]]; do
+                       IFS= read -r ssatz?"USt-Satz in % (0, 7 oder 19): "
+               done
+               bereich=
+               while [[ $bereich != IB && $bereich != ZB && $bereich != GB ]]; do
+                       IFS= read -r bereich?"Bereich (IB/ZB/GB): "
+               done
+               line="          ${line//[&      ]/ }    $dasfeld        $ssatz  $bereich"
+       else
+               line=$(grep "^$artnr    " artikel.lst)
+       fi
+       if [[ -z $line ]]; then
+               print -u2 "E: Unbekannter Artikel $artnr!"
+               sleep 2
+               continue
+       fi
+
+       sIFS=$IFS; IFS=$'\t'
+       set -A felder -- $line
+       IFS=$sIFS
+
+       brutto=$(echo "scale=2; ${felder[2]} * (100 + ${felder[3]}) / 100" | bc -q)
+
+       art="$art${art:+\\\\\\hline$nl}${felder[0]} & ${felder[1]} & ${felder[3]}~\\% & ${felder[4]} & ${felder[2]}~€ & ${brutto}~€"
+
+       case ${felder[3]} in
+       0) (( sum_0 += ${felder[2]/.} )) ;;
+       7) (( sum_7 += ${felder[2]/.} )) ;;
+       19) (( sum_19 += ${felder[2]/.} )) ;;
+       esac
+done
+
+st_0=000
+st_7=000 ; [[ $sum_7 != 000 ]] && st_7=$(echo "scale=2; $sum_7 / 100 * 0.07" | bc -q) ; st_7=${st_7/.}
+st_19=000 ; [[ $sum_19 != 000 ]] && st_19=$(echo "scale=2; $sum_19 / 100 * 0.19" | bc -q) ; st_19=${st_19/.}
+
+rblk=
+
+print "Kontostand: $kontostand"
+kontostand=${kontostand/.}
+if (( kontostand < 0 )); then
+       verr=
+       while [[ $verr != @(j|y|n) ]]; do
+               print -n 'Verrechnen? '
+               read verr
+       done
+
+       if [[ $verr != n ]]; then
+               rblk+='\textbf{Das Personenkonto weist einen Betrag von '${kontostand::${#kontostand}-2}.${kontostand: -2}'~€ auf. '
+               (( rsum = sum + kontostand ))
+               if (( rsum <= 0 )); then
+                       rblk+='Es ist keine Zahlung notwendig!}'
+               else
+                       rblk+='Bitte zahlen Sie nur den Restbetrag von '${sum::${#sum}-2}.${sum: -2}'~€.}
+
+'
+               fi
+       fi
+fi
+
+# 4. template-rechnung.tex kopieren und folgendes ersetzen:
+#      --Anschrift-- durch  Zeile 1\\
+#                           Zeile 2\\
+#                           ...
+#                           Zeile 4
+#      --RgNr--      durch  Zahl in rgnr.txt
+#      --Pos--       durch  Artikelnummer & Bezeichnung & Preis\\\hline
+#                           Artikelnummer # Bezeichnung # Preis\\\hline
+#                           ...
+#      --Summe--     durch  Summe der Preise
+
+# Name aus erster Zeile von $addr, sanitised
+name=${addr%%*(\\)$'\n'*}
+name=${name//@([/ ])/-}
+
+if [[ -z $rsum ]] || (( rsum > 0 )); then
+       if [[ $sepa = n ]]; then
+               rblk+='Bitte leisten Sie die Zahlung bis spätestens \AdvanceDate[7]\today{}
+       bei einem Vorstandsmitglied oder per Überweisung auf folgendes Konto:
+
+       \begin{tabular}{ l l }
+        Kontoinhaber:   & Teckids e.V.\\
+        IBAN:           & DE31 3705 0198 1933 0485 46\\
+        BIC:            & COLSDE33XXX\\
+        Kreditinstitut: & Sparkasse KölnBonn
+       \end{tabular}'
+       else
+               sman=TECKIDS$(date +'%Y%m%d')RG$rg
+               sgid="DE70ZZZ00001497650"
+               rblk+='Der Betrag wird frühestens am \AdvanceDate[5]\today{} mit der Mandatsreferenz \textbf{'"$sman"'} und der
+       Gläubiger-ID \textbf{'$sgid'} entsprechend dem erteilten SEPA-Lastschriftmandat eingezogen.'
+       fi
+fi
+
+file=$(<template-rechnung.tex)
+file=${file//--Konto--/"${employeeNumber:-$cashAccount}"}
+file=${file//--RBLK--/"$rblk"}
+file=${file//--Anschrift--/"$addr"}
+file=${file//--RgNr--/"$rg"}
+file=${file//--Pos--/"$art"}
+sum=$(( sum_0 + sum_7 + sum_19 ))
+sumtext=${sum::${#sum}-2}.${sum: -2}   # LP: #1453827
+file=${file//--Summe--/$sumtext}
+sum=$(( sum + st_7 + st_19 ))
+sumtext=${sum::${#sum}-2}.${sum: -2}   # LP: #1453827
+file=${file//--BruttoSumme--/$sumtext}
+file=${file//--St7--/${st_7::${#st_7}-2}.${st_7: -2}}
+file=${file//--St19--/${st_19::${#st_19}-2}.${st_19: -2}}
+
+print -r -- "$file" >"rechnung_${rg}_$name.tex"
+
+# 5. rgnr.txt erhöhen
+print -- $((rg + 1)) >rgnr.txt
+
+set -e
+make "rechnung_${rg}_$name.pdf"
+if [[ -n $DISPLAY ]]; then
+       for x in xdg-open mupdf okular atril; do
+               if whence -p $x >/dev/null; then
+                       $x "rechnung_${rg}_$name.pdf"
+                       exit 0
+               fi
+       done
+fi
diff --git a/mksh/teckids/nominatim b/mksh/teckids/nominatim
new file mode 100644 (file)
index 0000000..5508804
--- /dev/null
@@ -0,0 +1,133 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2016
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that pulls location data of a person from nominatim
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+function parse_address {
+       local _a="$*"
+
+       # Check what format the address is in
+       if [[ $_a = +([!,])" "+([0-9])?([a-zA-Z])", "[0-9][0-9][0-9][0-9][0-9]" "+([!,]) ]]; then
+               # Deutsche Anschrift in der Form „Musterstr. 17b, 12345 Musterstadt“
+               str_nr=${_a%%, *}
+               nr=${str_nr##* }
+               str=${str_nr% *}
+               str=${str/%str./straße}
+               str=${str/%Str./Straße}
+               plz_ort=${_a#*, }
+               plz=${plz_ort%% *}
+               ort=${plz_ort#* }
+               land=Germany
+       elif [[ $_a = +([!,])" "+([0-9])?([a-zA-Z])", "[0-9][0-9][0-9][0-9]" "+([!,])", Switzerland" ]]; then
+               # Schweizer Anschrift in der Form „Musterstr. 17b, 12345 Musterstadt, Switzerland“
+               str_nr=${_a%%, *}
+               nr=${str_nr##* }
+               str=${str_nr% *}
+               str=${str/%str./straße}
+               str=${str/%Str./Straße}
+               plz_ort=${_a#*, }
+               plz_ort=${plz_ort%%, *}
+               plz=${plz_ort%% *}
+               ort=${plz_ort#* }
+               land=${_a##*, }
+       elif [[ $_a = +([0-9])" "+([!,])", "+([!,])", "[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]" "[A-Z0-9][A-Z0-9][A-Z0-9]", Great Britain" ]]; then
+               # Britische Anschrift in der Form „32 Whatever Road, Cambridge, CB24 8XL, Great Britain“
+               str_nr=${_a%%, *}
+               nr=${str_nr%% *}
+               str=${str_nr#* }
+               ort=${_a#*, }
+               ort=${ort%%, *}
+               plz=${_a#*, }
+               plz=${plz#*, }
+               plz=${plz%%, *}
+               land=${_a##*, }
+       elif [[ $_a = +([!0-9])", "*([!0-9])", "*([!0-9]) ]]; then
+               # Nur Ortsname mit Bundesland und ggf. Land
+               ort=${_a%%, *}
+               bl=${_a#*, }
+               bl=${bl%, *}
+               land=${_a##*, }
+               : ${land:=Germany}
+       else
+               return 1
+       fi
+
+       if [[ -n $str ]]; then
+               print -r -- "street=$(uri_escape "$nr $str")&city=$(uri_escape "$ort")&postalcode=$(uri_escape "$plz")&country=$(uri_escape "$land")"
+       elif [[ -n $bl ]]; then
+               print -r -- "city=$(uri_escape "$ort")&state=$(uri_escape "$bl")&country=$(uri_escape "$land")"
+       else
+               print -r -- "city=$(uri_escape "$ort")&country=$(uri_escape "$land")"
+       fi
+       return 0
+}
+
+function addr2latlon {
+       if (( $# == 1 )); then
+               q=$(parse_address "$1")
+       else
+               q=$(parse_address "$1, $2, $3")
+       fi
+
+       if [[ $? = 0 ]]; then
+               set -- $(wget --timeout=1 -t 3 -O- -U "Teckids addr2latlon converter" -q \
+                   "http://nominatim.openstreetmap.org/search?format=xml&limit=1&$q)" | \
+                   xmlstarlet pyx - | sort | sed -n '/^Al[ao][tn] /s///p')
+               sleep 1
+               [[ -n $1$2 ]] && print -r -- "$1,$2"
+       fi
+}
+
+(( export_only )) && return
+
+if (( $# == 0 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+elif (( $# == 1 )); then
+       dn=$1
+fi
+
+asso_setldap_sasl users -- -b "$dn" -s base \
+       homePostalAddress l st c cn
+
+teckids_loadk_users
+for user_dn in "${asso_y[@]}"; do
+       homePostalAddress=$(asso_getv users "$user_dn" homePostalAddress 0)
+       l=$(asso_getv users "$user_dn" l 0)
+       st=$(asso_getv users "$user_dn" st 0)
+       c=$(asso_getv users "$user_dn" c 0)
+       cn=$(asso_getv users "$user_dn" cn 0)
+
+       r=
+       if [[ -n $homePostalAddress ]]; then
+               r=$(addr2latlon "$homePostalAddress")
+       fi
+       if [[ -z $r && -n $l ]]; then
+               r=$(addr2latlon "$l" "$st" "$c")
+       fi
+
+       print -r -- "$r"
+done
+
+exit 0
diff --git a/mksh/teckids/projadd b/mksh/teckids/projadd
new file mode 100644 (file)
index 0000000..4557bff
--- /dev/null
@@ -0,0 +1,135 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2017
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that adds a DN to the current project
+
+cmd_options='r\ 1rnd_msgid\ 10\ 1Zufällige Message-ID erzeugen'
+cmd_options+=$'\n''V\ 1run_viplanung\ 11\ 1viplanung am Ende aufrufen (Standard)'
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if (( $# == 0 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+else
+       if [[ $1 = "<"*">" || $1 = "Ticket#"* ]]; then
+               dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+               msgid="$1"
+       else
+               dn=$1
+               msgid="$2"
+       fi
+fi
+
+if (( rnd_msgid )); then
+       msgid="<$(date +%Y%m%d%H%M%S).generated@fakeid.teckids.org>"
+fi
+
+if [[ -z $msgid || -z $dn ]]; then
+       print -u2 "Either DN or Message-ID are missing!"
+       exit 105
+fi
+
+ldapmodify -c <<EOF
+dn: cn=$project_cn,ou=Projekte,ou=Groups,dc=teckids,dc=org
+changetype: modify
+add: member
+member: $dn
+-
+
+dn: $dn
+changetype: modify
+add: anmMessageId
+anmMessageId: $msgid
+-
+EOF
+
+x=planung.txt; until test -e $x; do x=../$x; done; ptxt=$x
+
+mkdir -p "$TOP/../.tmp"
+d=$(mktemp -d "$TOP/../.tmp/XXXXXXXXXX")
+
+s=0
+l=0
+cat "$ptxt" |&
+while IFS= read -rp line; do
+       (( l++ ))
+
+       case $s in
+       0)
+               if [[ $line = "| Message-ID "* ]]; then
+                       liner=${line#\| }
+                       liner=${liner% \|}
+                       sIFS=$IFS; IFS="|"; set -A fields -- $liner; IFS=$sIFS
+
+                       i=-1
+                       while (( ++i < ${#fields[@]} )); do
+                               fields[i]=${fields[i]##*( )}
+                               fields[i]=${fields[i]%%*( )}
+                       done
+
+                       # Read values for new entry
+                       set -A values
+
+                       if [[ $msgid = "<"* ]]; then
+                               smsgid=${msgid#<}
+                               smsgid=${smsgid%%.*}
+                       elif [[ $msgid = "Ticket#"* ]]; then
+                               smsgid=${msgid#Ticket#}
+                       fi
+                       values[0]=$smsgid
+
+                       i=0
+                       while (( ++i < ${#fields[@]} )); do
+                               read -r "values[i]?${fields[i]}: "
+                       done
+
+                       print -r -- "| $liner |"
+
+                       s=1
+               else
+                       print -r -- "$line"
+               fi
+               ;;
+       1)
+               if [[ $line = "+-"* ]]; then
+                       ln=$l
+
+                       print -n "|"
+
+                       i=-1
+                       while (( ++i < ${#values[@]} )); do
+                               print -rn -- " ${values[i]} |"
+                       done
+                       print
+
+                       print -r -- "$line"
+                       s=0
+               else
+                       print -r -- "$line"
+               fi
+               ;;
+       esac
+done >"$d/planung.tmp"
+
+mv "$d/planung.tmp" "$ptxt"
+(( run_viplanung )) && mksh "$ROOT/util/viplanung" #+$ln
diff --git a/mksh/teckids/projall b/mksh/teckids/projall
new file mode 100644 (file)
index 0000000..dd5d831
--- /dev/null
@@ -0,0 +1,57 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2016
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+cmd_options='s:\ 1single\ 10\ 1Nur einen einzelnen Feldnamen pro Ergebnis ausgeben'
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+asso_setldap_sasl projects -- -b ou=Groups,dc=teckids,dc=org \
+    "(cn=$project_cn)"
+
+asso_loadk projects
+project_dn=${asso_y[0]}
+
+if [[ $single != 0 ]]; then
+       asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+           "(&(objectClass=inetOrgPerson)(memberOf=$project_dn))" \
+          $single
+else
+       asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+           "(&(objectClass=inetOrgPerson)(memberOf=$project_dn))" \
+          givenName sn uid mail
+fi
+
+teckids_loadk_users
+for user_dn in "${asso_y[@]}"; do
+       if [[ $single != 0 ]]; then
+               v=$(asso_getv users "$user_dn" $single 0)
+               print -r -- "$v"
+       else
+               gn=$(asso_getv users "$user_dn" givenName 0)
+               sn=$(asso_getv users "$user_dn" sn 0)
+               uu=$(asso_getv users "$user_dn" uid 0)
+               mail=$(asso_getv users "$user_dn" mail 0)
+               print -r -- "$gn $sn|<$mail>"
+       fi
+
+done | sort | column -s'|' -t
diff --git a/mksh/teckids/projintpasswd b/mksh/teckids/projintpasswd
new file mode 100644 (file)
index 0000000..30ca797
--- /dev/null
@@ -0,0 +1,127 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that creates password sheets
+# for all people in a project that do not have an account
+# yet
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+mkdir -p passwd
+
+asso_setldap_sasl projects -- -b ou=Groups,dc=teckids,dc=org \
+    "(cn=$project_cn)"
+
+asso_loadk projects
+project_dn=${asso_y[0]}
+
+asso_setldap_sasl uidpeople -- -b ou=People,dc=teckids,dc=org \
+    objectClass=posixAccount uid
+asso_loadk uidpeople
+for dn in "${asso_y[@]}"; do
+       asso_setnull uids "$(asso_getv uidpeople "$dn" uid 0)"
+done
+
+highest_uid=$(terracmdn getent passwd | sort -t: -nk3 | tail -n -2 |&
+       IFS=: read -pA
+       echo ${REPLY[2]}
+    )
+
+asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+    "(&(objectClass=inetOrgPerson)(!(objectClass=posixAccount))(memberOf=$project_dn))" \
+    givenName sn jpegPhoto
+
+function machmalklein {
+       local -l l=$1
+
+       l=${l//ä/ae}
+       l=${l//ö/oe}
+       l=${l//ü/ue}
+       l=${l//ß/ss}
+       l=${l//Ä/ae}
+       l=${l//Ö/oe}
+       l=${l//Ü/ue}
+       l=${l//ẞ/ss}
+       l=${l//[!0-9a-zA-Z]}
+
+       REPLY=$l
+}
+
+teckids_loadk_users
+for user_dn in "${asso_y[@]}"; do
+       gn=$(asso_getv users "$user_dn" givenName 0)
+       sn=$(asso_getv users "$user_dn" sn 0)
+       jpegPhoto=$(asso_getv users "$user_dn" jpegPhoto 0)
+
+       gn_klein=${|machmalklein "$gn";}
+       sn_klein=${|machmalklein "$sn";}
+
+       lg=${#gn}
+       i=0
+       found=0
+       while (( ++i <= lg )); do
+               ln=${gn_klein::i}$sn_klein
+               if ! asso_isset uids "$ln"; then
+                       # wish I had goto here…
+                       found=1
+                       break
+               fi
+       done
+       if (( !found )); then
+               i=0
+               ln=${gn_klein::1}$sn_klein
+               while asso_isset uids "$ln$i"; do
+                       let i++
+               done
+               ln+=$i
+       fi
+       asso_setnull uids "$ln"
+
+       pw=$(pwgen -B 8 1)
+
+ldapmodify <<EOF
+dn: $user_dn
+changetype: modify
+add: objectClass
+objectClass: posixAccount
+objectClass: shadowAccount
+-
+add: uid
+uid: $ln
+-
+add: uidNumber
+uidNumber: $((++highest_uid))
+-
+add: gidNumber
+gidNumber: 100
+-
+add: homeDirectory
+homeDirectory: /home/$ln
+-
+add: loginShell
+loginShell: /bin/bash
+-
+add: userPassword
+userPassword: $pw
+-
+EOF
+done
diff --git a/mksh/teckids/projkarte b/mksh/teckids/projkarte
new file mode 100644 (file)
index 0000000..2774790
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+asso_setldap_sasl projects -- -b ou=Groups,dc=teckids,dc=org \
+    "(cn=$project_cn)"
+
+asso_loadk projects
+project_dn=${asso_y[0]}
+
+asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+    "(&(objectClass=inetOrgPerson)(memberOf=$project_dn))" \
+    givenName sn homePostalAddress o l teckidsLatitude teckidsLongitude jpegPhoto
+
+rm -rf "$ROOT"/.tmp/karte-pics
+mkdir -p "$ROOT"/.tmp/karte-pics
+
+teckids_loadk_users
+for user_dn in "${asso_y[@]}"; do
+       gn=$(xhtml_escape "$(asso_getv users "$user_dn" givenName 0)")
+       sn=$(xhtml_escape "$(asso_getv users "$user_dn" sn 0)")
+       hp=$(xhtml_escape "$(asso_getv users "$user_dn" homePostalAddress 0)")
+       o=$(xhtml_escape "$(asso_getv users "$user_dn" o 0)")
+       l=$(xhtml_escape "$(asso_getv users "$user_dn" l 0)")
+       lat=$(asso_getv users "$user_dn" teckidsLatitude 0)
+       lon=$(asso_getv users "$user_dn" teckidsLongitude 0)
+
+       jpegPhoto=$(asso_getv users "$user_dn" jpegPhoto 0)
+       img=../www/pics/people
+       if [[ -n $jpegPhoto ]]; then
+               asso_isset users "$user_dn"
+               img=karte-pics/${asso_k#16#}
+               Lb64decode "$jpegPhoto" >"$ROOT/.tmp/$img.jpg"
+       fi
+
+       [[ -z $lat || -z $lon ]] && continue
+
+       a=${hp:-$l}
+
+       print -r -- "$lat,$lon <table><tr><td><img src="$img.jpg" style="height:150px" alt="$gn $sn" /></td><td><div><b>$gn $sn</b><br />$a</div><p>$o, $l</p></td></tr></table>"
+done | $MKSH "$ROOT"/util/karte
diff --git a/mksh/teckids/projrand b/mksh/teckids/projrand
new file mode 100644 (file)
index 0000000..6d99b27
--- /dev/null
@@ -0,0 +1,153 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# arc4random(3) in Pure mksh™
+set -A seedbuf -- $(dd if=/dev/urandom bs=257 count=1 2>&- | \
+    hexdump -ve '1/1 "0x%02X "')
+set -A rs_S
+typeset -i rs_S rs_i=-1 rs_j=0 n
+while (( ++rs_i < 256 )); do
+       (( rs_S[rs_i] = rs_i ))
+done
+rs_i=-1
+while (( ++rs_i < 256 )); do
+       (( n = rs_S[rs_i] ))
+       (( rs_j = (rs_j + n + seedbuf[rs_i]) & 0xFF ))
+       (( rs_S[rs_i] = rs_S[rs_j] ))
+       (( rs_S[rs_j] = n ))
+done
+rs_i=0
+rs_j=0
+typeset -i rs_out
+function arcfour_byte {
+       typeset -i si sj
+
+       (( rs_i = (rs_i + 1) & 0xFF ))
+       (( si = rs_S[rs_i] ))
+       (( rs_j = (rs_j + si) & 0xFF ))
+       (( sj = rs_S[rs_j] ))
+       (( rs_S[rs_i] = sj ))
+       (( rs_S[rs_j] = si ))
+       (( rs_out = rs_S[(si + sj) & 0xFF] ))
+}
+(( n = 256 * 12 + seedbuf[256] + (RANDOM & 0xFF) ))
+while (( n-- )); do
+       arcfour_byte
+done
+(( n = rs_out ))
+while (( n-- )); do
+       arcfour_byte
+done
+
+typeset -Uui16 -Z11 arc4random_rv
+function arc4random {
+       # apply uncertainty
+       arcfour_byte
+       (( rs_out & 1 )) && arcfour_byte
+       # read four octets into result dword
+       arcfour_byte
+       (( arc4random_rv = rs_out ))
+       arcfour_byte
+       (( arc4random_rv |= rs_out << 8 ))
+       arcfour_byte
+       (( arc4random_rv |= rs_out << 16 ))
+       arcfour_byte
+       (( arc4random_rv |= rs_out << 24 ))
+}
+
+# arc4random_uniform(3) in Pure mksh™
+function arc4random_uniform {
+       # Derived from code written by Damien Miller <djm@openbsd.org>
+       # published under the ISC licence, with simplifications by
+       # Jinmei Tatuya. Written in mksh by Thorsten Glaser.
+       #-
+       # Calculate a uniformly distributed random number less than
+       # upper_bound avoiding “modulo bias”.
+       # Uniformity is achieved by generating new random numbers
+       # until the one returned is outside the range
+       # [0, 2^32 % upper_bound[. This guarantees the selected
+       # random number will be inside the range
+       # [2^32 % upper_bound, 2^32[ which maps back to
+       # [0, upper_bound[ after reduction modulo upper_bound.
+       #-
+       typeset -Ui upper_bound=$1 min
+
+       if (( upper_bound < 2 )); then
+               arc4random_rv=0
+               return
+       fi
+
+       # calculate (2^32 % upper_bound) avoiding 64-bit math
+       # if upper_bound > 2^31: 2^32 - upper_bound (only one
+       # “value area”); otherwise (x <= 2^31) use the fact
+       # that ((2^32 - x) % x) == (2^32 % x)
+       ((# min = upper_bound > 0x80000000 ? 1 + ~upper_bound :
+           (0xFFFFFFFF - upper_bound + 1) % upper_bound ))
+
+       # This could theoretically loop forever but each retry has
+       # p > 0.5 (worst case, usually far better) of selecting a
+       # number inside the range we need, so it should rarely need
+       # to re-roll (at all).
+       while :; do
+               arc4random
+               ((# arc4random_rv >= min )) && break
+       done
+
+       ((# arc4random_rv %= upper_bound ))
+}
+
+
+set -U
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+asso_setldap_sasl projects -- -b ou=Groups,dc=teckids,dc=org \
+    "(cn=$project_cn)"
+
+asso_loadk projects
+project_dn=${asso_y[0]}
+
+asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+    "(&(objectClass=inetOrgPerson)(memberOf=$project_dn))" \
+    givenName sn uid
+
+teckids_loadk_users
+set -A names -- "${asso_y[@]}"
+typeset -R20 uu
+trap 'echo; exit 0' INT
+while (( ${#names[@]} )); do
+       print -n 'Enter drücken für Lotterie (^C to exit):'
+       read
+       print -n '\033[1A\r\033[K'
+
+       arc4random_uniform ${#names[@]}
+       user_dn=${names[arc4random_rv]}
+       unset names[arc4random_rv]
+       set -A names -- "${names[@]}"
+
+       gn=$(asso_getv users "$user_dn" givenName 0)
+       sn=$(asso_getv users "$user_dn" sn 0)
+       uu=$(asso_getv users "$user_dn" uid 0)
+
+       print -r -- "$uu | $gn $sn"
+done
+print Keine Namen mehr da.
diff --git a/mksh/teckids/rplanung b/mksh/teckids/rplanung
new file mode 100644 (file)
index 0000000..44f4735
--- /dev/null
@@ -0,0 +1,86 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that gets a field from planung.txt by key and field name
+
+if [[ $(basename "$0") = rplanung ]]; then
+       teckids_sourcing_wrapper=1
+       offline=1
+       . "$(dirname "$0")/teckids"
+elif [[ -z $TECKIDS_WRAPPER_LOADED ]]; then
+       print -ru2 E: may only be sourced from teckids scripts
+       exit 1
+fi
+
+asso_setasso planung
+
+[[ -e $ptxt ]] || return
+
+s=0
+while IFS= read -r line; do
+       case $s in
+       0)
+               if [[ $line = "| Message-ID "* ]]; then
+                       line=${line#\| }
+                       line=${line% \|}
+                       sIFS=$IFS; IFS="|"; set -A fields -- $line; IFS=$sIFS
+
+                       i=-1
+                       while (( ++i < ${#fields[@]} )); do
+                               fields[i]=${fields[i]##*( )}
+                               fields[i]=${fields[i]%%*( )}
+                       done
+
+                       s=1
+               fi
+               ;;
+       1)
+               s=2
+               ;;
+       2)
+               if [[ $line == "+-"* ]]; then
+                       break
+               else
+                       line=${line#\| }
+                       line=${line% \|}
+                       sIFS=$IFS; IFS="|"; set -A linea -- $line; IFS=$sIFS
+
+                       i=-1
+                       while (( ++i < ${#linea[@]} )); do
+                               linea[i]=${linea[i]##*( )}
+                               linea[i]=${linea[i]%%*( )}
+                       done
+
+                       key=${linea[0]}
+
+                       i=-1
+                       while (( ++i < ${#fields[@]} )); do
+                               asso_sets "${linea[i]}" planung "$key" "${fields[i]}"
+                       done
+               fi
+               ;;
+       esac
+done <"$ptxt"
+
+if [[ $(basename "$0") = rplanung ]]; then
+       asso_getv planung "$1" "$2"
+       exit 0
+fi
diff --git a/mksh/teckids/schulsql2ldif b/mksh/teckids/schulsql2ldif
new file mode 100644 (file)
index 0000000..fea87ec
--- /dev/null
@@ -0,0 +1,155 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+#-
+# Required additional Debian packages:
+# - sqlite3
+
+# Teckids utility subcommand that converts a school SQL to LDIF
+
+cmd_options='b:\ 1bundesland\ 1\ 1Bundesland (LDIF st)'
+cmd_options+=$'\n''c:\ 1land\ 1\ 1Staat (LDIF c oder co)'
+cmd_options+=$'\n''t:\ 1typ\ 1\ 1Schultyp (LDIF businessCategory)'
+cmd_options+=$'\n''f:\ 1infile\ 1\ 1SQLite3-Eingabedatenbank'
+cmd_options+=$'\n''o:\ 1outfile\ 1\ 1LDIF-Ausgabedateiname'
+
+teckids_sourcing_wrapper=1
+offline=1
+. "$(dirname "$0")/teckids"
+
+[[ -n $bundesland ]] || usage
+[[ -n $land ]] || usage
+[[ -n $typ ]] || usage
+[[ -n $infile ]] || usage
+[[ -s $infile ]] || usage
+[[ -n $outfile ]] || usage
+[[ -s $outfile ]] && usage
+
+est=${bundesland//,/\\,}
+ec=${land//,/\\,}
+bst=$(Lb64encode "$bundesland" | tr -d \\n)
+bc=$(Lb64encode "$land" | tr -d \\n)
+bbc=$(Lb64encode "$typ" | tr -d \\n)
+
+# mapping between LHS and objectClass
+asso_setasso classes
+asso_sets organizationalUnit classes ou
+asso_sets country classes c
+asso_sets locality classes st
+asso_sets locality classes l
+asso_sets organization classes o
+
+# assume these LDAP keys exist, or were already generated
+asso_setasso dn
+asso_setnull dn dc=org
+asso_setnull dn dc=teckids,dc=org
+
+function makedn {
+       local x s sep
+
+       for x in "$@"; do
+               s+=$sep$x
+               sep=,
+       done
+
+       REPLY=$s
+}
+
+function makeparent {
+       shift
+
+       (( $# > 1 )) && makeparent "$@"
+
+       local adn
+       adn=${|makedn "$@";}
+
+       asso_isset dn "$adn" && return 0
+
+       local cls=${1%%=*}
+       cls=$(asso_getv classes "$cls")
+       [[ -n $cls ]] || die "Cannot resolve '$1' to objectClass"
+
+       print -r "dn:: $(Lb64encode "$adn" | tr -d \\n)"
+       print -r "objectClass: $cls"
+       print -r "${1%%=*}:: $(Lb64encode "${1#*=}" | tr -d \\n)"
+       print
+       asso_setnull dn "$adn"
+}
+
+(print '.mode line\nSELECT * FROM schuldaten;' | sqlite3 "$infile"; print) |&
+ninp=0
+nent=0
+while IFS= read -pr line; do
+       if [[ -z $line ]]; then
+               let ++ninp
+               if [[ -z $xbez || -z xstr || -z $xplz || -z $xort ]]; then
+                       print -ru2 "N: xbez='$xbez'"
+                       print -ru2 "N: xort='$xort'"
+                       print -ru2 "N: xstr='$xstr'"
+                       print -ru2 "N: xplz='$xplz'"
+                       print -ru2 "N: xeml='$xeml'"
+                       print -ru2 "N: xsnr='$xsnr'"
+                       print -ru2 "W: record #$ninp missing mandatory fields, ignoring"
+                       xbez= xort= xstr= xplz= xeml= xsnr=
+                       continue
+               fi
+               set -A xdn -- "o=${xbez//,/\\,}" "l=${xort//,/\\,}" "st=$est" \
+                   "c=$ec" ou=Schulen ou=Contacts dc=teckids dc=org
+               rdn=${|makedn "${xdn[@]}";}
+               makeparent "${xdn[@]}"
+
+               print -r "dn:: $(Lb64encode "$rdn" | tr -d \\n)"
+               print -r "objectClass: organization"
+               print -r "objectClass: teckidsSchule"
+               print -r "o:: $(Lb64encode "$xbez" | tr -d \\n)"
+               print -r "businessCategory:: $bbc"
+               print -r "c:: $bc"
+               print -r "st:: $bst"
+               print -r "street:: $(Lb64encode "$xstr" | tr -d \\n)"
+               print -r "postalCode:: $(Lb64encode "$xplz" | tr -d \\n)"
+               print -r "l:: $(Lb64encode "$xort" | tr -d \\n)"
+               [[ -n $xeml ]] && print -r "mail:: $(Lb64encode "$xeml" | tr -d \\n)"
+               [[ -n $xsnr ]] && print -r "schulnummer:: $(Lb64encode "$xsnr" | tr -d \\n)"
+               print
+               let ++nent
+
+               xbez= xort= xstr= xplz= xeml= xsnr=
+               continue
+       fi
+
+       [[ $line = *=* ]] || die "line from SQL unexpected format: '$line'"
+
+       line=${line##*([         ])}    # ltrim$
+       fn=${line%%=*}                  # left$
+       val=${line#*=}                  # mid$
+       fn=${fn%%*([     ])}            # rtrim$
+       val=${val##*([   ])}            # ltrim$
+       val=${val%%*([   ])}            # rtrim$
+
+       case $fn {
+       (bezeichnung)   xbez=$val ;;
+       (ort)           xort=$val ;;
+       (strasse)       xstr=$val ;;
+       (plz)           xplz=$val ;;
+       (email)         xeml=$val ;;
+       (snr)           xsnr=$val ;;
+       }
+done >"$outfile"
+print -u2 "I: $nent entries created from $ninp input records"
+exit 0
diff --git a/mksh/teckids/setphoto b/mksh/teckids/setphoto
new file mode 100644 (file)
index 0000000..5e5d12e
--- /dev/null
@@ -0,0 +1,82 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2016
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that updates the jpegPhoto for a person
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if (( $# == 1 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+       f=$1
+elif (( $# == 2 )); then
+       dn=$1
+       f=$2
+else
+       print -u2 "Der Dateiname muss angegeben werden!"
+       exit 100
+fi
+
+f=$(qualify_pathname "$f") || die 101 "Der Pfad zur Datei existiert nicht!"
+
+if ! [[ -e $f ]]; then
+       print -u2 "Datei existiert nicht!"
+       exit 101
+fi
+
+# Sanitise jpeg photo
+mkdir -p "$TOP/../.tmp"
+d=$(mktemp -d "$TOP/../.tmp/XXXXXXXXXX")
+
+# Fill arrary with image properties - width, height, bytes, format, quality
+set -A imgprops -- $(identify -format "%w %h %b %m %Q" "$f")
+
+# Normalise and force to JPEG
+convert "$f" "$d/out.jpg"
+
+# Check width
+if (( ${imgprops[0]} > 400 )); then
+       mogrify -scale 400x "$d/out.jpg"
+fi
+
+# Check compression
+if (( ${imgprops[4]} > 75 )); then
+       mogrify -quality 75 "$d/out.jpg"
+fi
+
+# yes, both need to be here
+jpegoptim -s --all-normal "$d/out.jpg"
+jpegoptim -s --all-progressive "$d/out.jpg"
+
+b=$(Lb64encode <"$d/out.jpg" | sed 's/^/ /')
+
+ldapmodify <<EOF
+dn: $dn
+changetype: modify
+replace: jpegPhoto
+jpegPhoto::$b
+-
+EOF
+
+rm -rf "$d"
+
+exit 0
diff --git a/mksh/teckids/showphoto b/mksh/teckids/showphoto
new file mode 100644 (file)
index 0000000..62b085f
--- /dev/null
@@ -0,0 +1,63 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015, 2016
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that displays the JPEG photo of a user
+
+cmd_options='p:\ 1prog\ 1\ 1Anzeigeprogramm'
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+# consider making a teckids-utils config file (not in git) instead
+for x in "$prog" xdg-open run-mailcap xloadimage konqueror false; do
+       [[ $x = false ]] && die Could not find an image viewer
+       [[ -z $x ]] || if whence -p $x >/dev/null; then
+               viewer=$x
+               break
+       fi
+done
+
+if (( $# == 0 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+else
+       dn=$1
+fi
+
+asso_setldap_sasl users -- -b "$dn" -s base
+
+mkdir -p "$TOP/../.tmp"
+d=$(mktemp -d "$TOP/../.tmp/XXXXXXXXXX")
+
+teckids_loadk_users
+found=0
+for user_dn in "${asso_y[@]}"; do
+       jpegPhoto=$(asso_getv users "$user_dn" jpegPhoto 0)
+       [[ -z "$jpegPhoto" ]] && continue
+       Lb64decode "$jpegPhoto" >"$d/out.jpg"
+       $viewer "$d/out.jpg"
+       found=1
+done
+(( found )) || (( quiet )) || print -ru2 W: no jpegPhoto found
+
+(sleep 3; rm -rf "$d") &
+
+exit 0
diff --git a/mksh/teckids/shuffle b/mksh/teckids/shuffle
new file mode 100644 (file)
index 0000000..44f50c7
--- /dev/null
@@ -0,0 +1,129 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2017
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# arc4random(3) in Pure mksh™
+set -A seedbuf -- $(dd if=/dev/urandom bs=257 count=1 2>&- | \
+    hexdump -ve '1/1 "0x%02X "')
+set -A rs_S
+typeset -i rs_S rs_i=-1 rs_j=0 n
+while (( ++rs_i < 256 )); do
+       (( rs_S[rs_i] = rs_i ))
+done
+rs_i=-1
+while (( ++rs_i < 256 )); do
+       (( n = rs_S[rs_i] ))
+       (( rs_j = (rs_j + n + seedbuf[rs_i]) & 0xFF ))
+       (( rs_S[rs_i] = rs_S[rs_j] ))
+       (( rs_S[rs_j] = n ))
+done
+rs_i=0
+rs_j=0
+typeset -i rs_out
+function arcfour_byte {
+       typeset -i si sj
+
+       (( rs_i = (rs_i + 1) & 0xFF ))
+       (( si = rs_S[rs_i] ))
+       (( rs_j = (rs_j + si) & 0xFF ))
+       (( sj = rs_S[rs_j] ))
+       (( rs_S[rs_i] = sj ))
+       (( rs_S[rs_j] = si ))
+       (( rs_out = rs_S[(si + sj) & 0xFF] ))
+}
+(( n = 256 * 12 + seedbuf[256] + (RANDOM & 0xFF) ))
+while (( n-- )); do
+       arcfour_byte
+done
+(( n = rs_out ))
+while (( n-- )); do
+       arcfour_byte
+done
+
+typeset -Uui16 -Z11 arc4random_rv
+function arc4random {
+       # apply uncertainty
+       arcfour_byte
+       (( rs_out & 1 )) && arcfour_byte
+       # read four octets into result dword
+       arcfour_byte
+       (( arc4random_rv = rs_out ))
+       arcfour_byte
+       (( arc4random_rv |= rs_out << 8 ))
+       arcfour_byte
+       (( arc4random_rv |= rs_out << 16 ))
+       arcfour_byte
+       (( arc4random_rv |= rs_out << 24 ))
+}
+
+# arc4random_uniform(3) in Pure mksh™
+function arc4random_uniform {
+       # Derived from code written by Damien Miller <djm@openbsd.org>
+       # published under the ISC licence, with simplifications by
+       # Jinmei Tatuya. Written in mksh by Thorsten Glaser.
+       #-
+       # Calculate a uniformly distributed random number less than
+       # upper_bound avoiding “modulo bias”.
+       # Uniformity is achieved by generating new random numbers
+       # until the one returned is outside the range
+       # [0, 2^32 % upper_bound[. This guarantees the selected
+       # random number will be inside the range
+       # [2^32 % upper_bound, 2^32[ which maps back to
+       # [0, upper_bound[ after reduction modulo upper_bound.
+       #-
+       typeset -Ui upper_bound=$1 min
+
+       if (( upper_bound < 2 )); then
+               arc4random_rv=0
+               return
+       fi
+
+       # calculate (2^32 % upper_bound) avoiding 64-bit math
+       # if upper_bound > 2^31: 2^32 - upper_bound (only one
+       # “value area”); otherwise (x <= 2^31) use the fact
+       # that ((2^32 - x) % x) == (2^32 % x)
+       ((# min = upper_bound > 0x80000000 ? 1 + ~upper_bound :
+           (0xFFFFFFFF - upper_bound + 1) % upper_bound ))
+
+       # This could theoretically loop forever but each retry has
+       # p > 0.5 (worst case, usually far better) of selecting a
+       # number inside the range we need, so it should rarely need
+       # to re-roll (at all).
+       while :; do
+               arc4random
+               ((# arc4random_rv >= min )) && break
+       done
+
+       ((# arc4random_rv %= upper_bound ))
+}
+
+
+set -A names
+nnames=0
+while IFS= read -r; do
+       names[nnames++]=$REPLY
+done
+while (( ${#names[@]} )); do
+       arc4random_uniform ${#names[@]}
+       user_dn=${names[arc4random_rv]}
+       unset names[arc4random_rv]
+       set -A names -- "${names[@]}"
+
+       print -r -- "$user_dn"
+done
diff --git a/mksh/teckids/ssh_wrapper b/mksh/teckids/ssh_wrapper
new file mode 100755 (executable)
index 0000000..b4af6bc
--- /dev/null
@@ -0,0 +1,35 @@
+#!/bin/mksh
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+me=$(realpath "$0")
+
+if [[ $GIT_SSH != "$me" ]]; then
+       print "E: do not call me directly!" >/dev/tty
+       exit 255
+fi
+
+if ! ssh -S "$TECKIDS_SSHCP/mux" -O check "$TECKIDS_MANAGEMENT_HOST" 2>/dev/null; then
+       print "W: teckids ssh muxmaster not running!" >/dev/tty
+       ssh -S "$TECKIDS_SSHCP/mux" -fNM "$TECKIDS_MANAGEMENT_HOST"
+fi
+
+exec ssh -S "$TECKIDS_SSHCP/mux" "$@"
diff --git a/mksh/teckids/sshctl b/mksh/teckids/sshctl
new file mode 100644 (file)
index 0000000..3eb9a49
--- /dev/null
@@ -0,0 +1,63 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand to control the mode of operation for
+# the SSH muxmaster. Arguments are:
+# - auto: (default) start muxmaster automatically; tear down if
+#              no PID using it is running any more
+# – manual: avoid muxmaster teardown; trigger starting it now
+# – stop: force stopping the muxmaster; return to “auto” state
+
+teckids_sourcing_wrapper=1
+offline=1
+. "$(dirname "$0")/teckids"
+
+mkdir -p -m0700 "$TECKIDS_SSHCP"
+cd "$TECKIDS_SSHCP" || die cannot cd TECKIDS_SSHCP
+
+rv=1
+case x$1 {
+(xauto)
+       rm -f 1
+       rv=0
+       ;;
+(xmanual)
+       rm -f 1
+       :>1
+       unset offline
+       mksh "$(dirname "$0")/ssh" true
+       rv=$?
+       ;;
+(xstop)
+       ssh -S "$TECKIDS_SSHCP/mux" -O check $TECKIDS_MANAGEMENT_HOST \
+           >/dev/null 2>&1 && >/dev/null 2>&1 \
+           ssh -S "$TECKIDS_SSHCP/mux" -O exit $TECKIDS_MANAGEMENT_HOST
+       rm -f *
+       rv=0
+       ;;
+(x)
+       rv=0
+       ;&
+(*)
+       print -u2 "Usage: teckids sshctl { auto | manual | stop }"
+       ;;
+}
+
+exit $rv
diff --git a/mksh/teckids/statupd b/mksh/teckids/statupd
new file mode 100644 (file)
index 0000000..639023c
--- /dev/null
@@ -0,0 +1,323 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015, 2016
+#      Thorsten “mirabilos” Glaser <thorsten.glaser@teckids.org>
+# Copyright © 2014, 2015
+#      Dominik George <dominik.george@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+#-
+# Column types:
+# • i “info” (e.g. ℹ, Message-ID, Land)
+#     informative, statline empty
+# • g “gender” (exactly Geschlecht)
+#     gender stats (m/w), recording distribution, statline "%dm, %dw"
+# • c “count/m/w” (default type)
+#     gender stats (ja/nein), counting !nein, statline "%dm, %dw"
+# • C “count/tot” (e.g. Bezahlt, Fahrt)
+#     stats (ja/nein), counting amount of !nein, statline "%d"
+# • S “sum/tot” (unused type)
+#     numeric, summed per column, totalled, statline "%d"
+# • eis “sum/div3” (exactly Eis)
+#       exactly like S but summed after integer division by 3
+# • s “sum/avg” (e.g. Smiley, Fehl)
+#     numeric, summed per column, averaged over #lines, statline "%.2f"
+# • a “average” (e.g. Alter, Klasse)
+#     numeric, summed per column, averaged over #entries, statline "%.2f"
+
+# call this with desired scale and bc formula
+function bcfixup {
+       REPLY=$(bcfixup2 "$@")
+       [[ $REPLY = .* ]] && REPLY=0$REPLY
+}
+function bcfixup2 {
+       local bcf=$2 dscale=$1 cs cl epsilon=0. i=$1
+
+       while (( i-- > 0 )); do
+               epsilon+=0
+       done
+       epsilon+=5
+
+       bc <<-EOF
+               scale = 20 + ($dscale)
+               x = ($bcf)
+               if (x > 0) x += $epsilon
+               if (x < 0) x -= $epsilon
+               scale = ($dscale)
+               print (x / 1)
+       EOF
+}
+
+if [[ ! -s planung.txt ]]; then
+       print -u2 E: planung.txt not found or empty
+       exit 1
+fi
+if [[ $(basename "$0") = rplanung ]]; then
+       mydir=$(dirname "$(realpath "$0")")
+elif [[ $TECKIDS_RUNNING_COMMAND = */statupd ]]; then
+       mydir=${TECKIDS_RUNNING_COMMAND%/statupd}
+elif [[ -z $TECKIDS_WRAPPER_LOADED ]]; then
+       print -ru2 E: may only be sourced from teckids scripts
+       exit 1
+else
+       mydir=$ROOT/util
+fi
+if [[ ! -s $mydir/statupd ]]; then
+       print -u2 E: wrong dir
+       exit 255
+fi
+if [[ ! -s $mydir/tbl2kdmn || ! -s $mydir/astat || \
+    ! -s $mydir/../www/mk/common ]]; then
+       print -u2 E: helper scripts missing
+       exit 255
+fi
+print -nu2 'I: Updating statistics (pre)...\r'
+
+nl=$'\n'
+pre=
+tbl=
+s=0
+post=
+while IFS= read -r line; do
+       case $s:$line {
+       (0:+==*)
+               pre+=$line$nl
+               s=1
+               ;;
+       (0*)
+               pre+=$line$nl
+               ;;
+       (1:*Message-ID*Geschlecht*Klasse*Alter*)
+               tbl=$line
+               IFS= read -r line
+               tbl+=$nl$line
+               s=2
+               ;;
+       (1*)
+               pre+=$line$nl
+               s=0
+               ;;
+       (2:'+'*)
+               tbl+=$nl$line
+               IFS= read -r line
+               IFS= read -r line
+               aftertbl=$line
+               s=3
+               ;;
+       (2*)
+               tbl+=$nl$line
+               ;;
+       (3*)
+               post+=$line$nl
+               ;;
+       }
+done <planung.txt
+if [[ $s != 3 ]]; then
+       print -u2 E: Parse error, s=$s
+       exit 1
+fi
+
+unset veranst_dt
+[[ -s vars ]] && . ./vars
+[[ -n $veranst_dt ]] && export veranst_dt
+export TECKIDS_COMMON=$mydir/../www/mk/common
+
+post=${post/"${nl}Statistik:${nl}"*"Standard deviation:"*([     -~])"$nl"/﷐}
+[[ $post = *'﷐'* ]] || post=﷐$post
+
+kdmn=$(print -r -- "$tbl" | mksh "$mydir/tbl2kdmn")
+astat=$(print -r -- "$kdmn" | mksh "$mydir/astat" planung.txt | sed 's/[        ]*$//')
+print -nu2 'I: Updating statistics (post)...                        \r'
+post=${post/'﷐'/"${nl}Statistik:${nl}${astat}$nl"}
+
+print -r -- "$kdmn" |&
+set -A headline
+IFS= read -pr line
+set -A l -- $line
+n=-1
+for x in "${l[@]}"; do
+       case $x {
+       (ℹ|Message-ID|Land)
+               headline[++n]=i
+               ;;
+       (Geschlecht)
+               headline[++n]=g
+               ;;
+       (Klasse|Alter|Bw*)
+               headline[++n]=a
+               ;;
+       (Fahrt*|Info|Bezahlt|Veg.)
+               headline[++n]=C
+               ;;
+       (Sonne|Ausrufe|Smiley|Frowney|Fehl|☀|\!|☺|☹)
+               headline[++n]=s
+               ;;
+       (Eis)
+               headline[++n]=eis
+               ;;
+       (*)
+               headline[++n]=c
+               ;;
+       }
+done
+set -A fieldsums
+set -A fieldentries
+set -A cntm
+set -A cntw
+while IFS= read -pr line; do
+       (( fieldsums[0]++ ))
+       set -A l -- $line
+       n=-1
+       g=
+       for x in "${l[@]}"; do
+               case ${headline[++n]} {
+               (i)
+                       ;;
+               (a)
+                       [[ $x = . ]] && continue
+                       (( fieldentries[n]++ ))
+                       ;&
+               (s|S)
+                       [[ $x = ?(.) ]] && x=0
+                       if [[ $x != +([0-9]) ]]; then
+                               print -ru2 "E: not numeric: $line"
+                               exit 1
+                       fi
+                       (( fieldsums[n] += $x ))
+                       ;;
+               (g)
+                       g=$x
+                       ;&
+               (c|C)
+                       if [[ $g != @(m|w) ]]; then
+                               print -ru2 "E: gender unknown: $line"
+                               exit 1
+                       fi
+                       case $x {
+                       (ja|Ja|JA|$g)
+                               eval 'y=${cnt'$g'[n]}'
+                               (( y++ ))
+                               eval 'cnt'$g'[n]=$y'
+                               ;;
+                       (nein|Nein|NEIN|N/A)
+                               ;;
+                       (*)
+                               print -ru2 "W: '$x' counted as ja: $line"
+                               eval 'y=${cnt'$g'[n]}'
+                               (( y++ ))
+                               eval 'cnt'$g'[n]=$y'
+                               ;;
+                       }
+                       ;;
+               (eis)
+                       [[ $x = ?(.) ]] && x=0
+                       if [[ $x != +([0-9]) ]]; then
+                               print -ru2 "E: not numeric: $line"
+                               exit 1
+                       fi
+                       (( fieldsums[n] += $x / 3 ))
+                       ;;
+               }
+       done
+done
+
+typeset -R2 xm xw
+if [[ $aftertbl != '+='*([+=])'=+' ]]; then
+       print -u2 "E: aftertbl '$aftertbl' not in expected format"
+       exit 1
+fi
+widcal=${aftertbl#?}
+function nextw {
+       nxtw=0
+       while [[ $widcal = =* ]]; do
+               let ++nxtw
+               widcal=${widcal#?}
+       done
+       widcal=${widcal#?}
+}
+
+line='|'; nextw; while (( nxtw-- )); do line+=' '; done; line+='|'
+unset headline[0]
+unset headline[1]
+n=1
+for x in "${headline[@]}"; do
+       let ++n
+       fld=' '
+       case $x {
+       (i)
+               nextw; while (( nxtw-- )); do line+=' '; done; line+='|'
+               ;;
+       (C)
+               (( cw = cntm[n] + cntw[n] ))
+               if (( cw < 100 )); then
+                       (( xw = cw ))
+                       fld+="${xw}"
+               else
+                       fld+="$((cw))"
+               fi
+               fld=${fld##*( )}
+               flen=${%fld}
+               (( flen = (flen == -1) ? (${#fld} + 1) : (flen + 1) ))
+               nextw; while (( nxtw-- > flen )); do line+=' '; done
+               line+="$fld |"
+               ;;
+       (g|c)
+               (( cm = cntm[n] ))
+               (( cw = cntw[n] ))
+               if (( cm < 100 )); then
+                       (( xm = cm ))
+                       fld+="${xm}m, "
+               else
+                       fld+="$((cm))m, "
+               fi
+               if (( cw < 100 )); then
+                       (( xw = cw ))
+                       fld+="${xw}w"
+               else
+                       fld+="$((cw))w"
+               fi
+               fld=${fld##*( )}
+               flen=${%fld}
+               (( flen = (flen == -1) ? (${#fld} + 1) : (flen + 1) ))
+               nextw; while (( nxtw-- > flen )); do line+=' '; done
+               line+="$fld |"
+               ;;
+       (s)
+               (( fieldentries[n] = fieldsums[0] ))
+               ;&
+       (a)
+               (( fieldentries[n] )) && \
+                   fld+=${|bcfixup 2 "${fieldsums[n]}/${fieldentries[n]}";}
+               fld=${fld##*( )}
+               flen=${%fld}
+               (( flen = (flen == -1) ? (${#fld} + 1) : (flen + 1) ))
+               nextw; while (( nxtw-- > flen )); do line+=' '; done
+               line+="$fld |"
+               ;;
+       (S|eis)
+               fld+=$((fieldsums[n]))
+               fld=${fld##*( )}
+               flen=${%fld}
+               (( flen = (flen == -1) ? (${#fld} + 1) : (flen + 1) ))
+               nextw; while (( nxtw-- > flen )); do line+=' '; done
+               line+="$fld |"
+               ;;
+       }
+done
+
+print -nr -- "$pre$tbl$nl$line$nl$aftertbl$nl$post" >planung.txt~~
+print -nu2 'I: Updating statistics... done  \r'
+mv planung.txt~~ planung.txt
diff --git a/mksh/teckids/subprjck b/mksh/teckids/subprjck
new file mode 100644 (file)
index 0000000..79da83c
--- /dev/null
@@ -0,0 +1,73 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2016
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility to check subproject membership
+#XXX TODO: rewrite to use DNs and LDAP ipv getent; Nik says we
+#XXX can only use getent for dealing in accounts, and not every
+#XXX project member needs to have a POSIX account, especially
+#XXX not at first…
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+set -o noglob
+
+[[ -n $ptxt ]] || die vielleicht im falschen Verzeichnis?
+[[ -n $project_cn ]] || die vielleicht im falschen Verzeichnis?
+
+function getmembers {
+       local saveIFS=$IFS tmp
+       IFS=:
+       set -A tmp -- $(terracmdn getent group "$1")
+       [[ ${tmp[1]} = '*' ]] || die "Kann Gruppe $1 nicht holen: ${tmp[*]}"
+       IFS=,
+       set -sA tmp -- ${tmp[3]}
+       IFS=$saveIFS
+       print -r -- ${tmp[*]}
+}
+
+warns=0
+
+asso_setnull whitelist jtobisch
+asso_setnull whitelist nbildhauer
+asso_setnull whitelist nik
+asso_setnull whitelist tglaser
+
+set -A proj -- $(getmembers "$project_cn")
+for x in "${proj[@]}"; do
+       asso_setnull proj "$x"
+done
+
+for subp in "${subprojects[@]}"; do
+       for x in $(getmembers "$subp"); do
+               asso_setnull subp "$x"
+               asso_isset proj "$x" && continue
+               echo "W: $x not in parent but in subproject $subp"
+               asso_isset whitelist "$x" || warns=1
+       done
+done
+
+for x in "${proj[@]}"; do
+       asso_isset subp "$x" && continue
+       echo "W: $x in parent but in no subproject"
+       asso_isset whitelist "$x" || warns=1
+done
+
+exit $warns
diff --git a/mksh/teckids/subprjmk b/mksh/teckids/subprjmk
new file mode 100644 (file)
index 0000000..9e3aa9f
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2016
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility to run mkliste per subproject
+
+xpdf=$1; shift
+set -A parms -- "$@"
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+set -o noglob
+
+[[ -n $ptxt ]] || die vielleicht im falschen Verzeichnis?
+[[ -n $project_cn ]] || die vielleicht im falschen Verzeichnis?
+
+dashn=
+rm -f subprjmk.?.pdf
+for subp in "${subprojects[@]}"; do
+       asso_setldap_sasl groups -- -b ou=Groups,dc=teckids,dc=org \
+           '(&(objectClass=groupOfNames)(cn='"$subp"'))'
+       asso_loadk groups
+       pdn=${asso_y[0]}
+       pcn=$(asso_getv groups "$pdn" cn 0)
+       pdesc=$(asso_getv groups "$pdn" description 0)
+       pdesc=${pdesc:-${pcn:-$pdn}}
+       filter='(&(objectClass=inetOrgPerson)(memberOf='"$pdn"'))'
+
+       print -ru2 -- "N: running mkliste for $subp…"
+       teckids mkliste -F "$filter" -D pextra="$pdesc" -D subprjcn="$pcn" \
+           $dashn "${parms[@]}" || die mkliste died
+       dashn=-N
+       if [[ -e subprjmk.0.pdf ]]; then
+               mksh "$ROOT/print/ps2pdfmir" -o subprjmk.1.pdf \
+                   subprjmk.0.pdf "$xpdf" || die ps2pdf died
+               mv -f subprjmk.1.pdf subprjmk.0.pdf
+       else
+               mv -f "$xpdf" subprjmk.0.pdf
+       fi
+done
+mv -f subprjmk.0.pdf "$xpdf"
+rm -f subprjmk.?.pdf
+ls -l -- "$xpdf"
diff --git a/mksh/teckids/tbl2kdmn b/mksh/teckids/tbl2kdmn
new file mode 100644 (file)
index 0000000..915563f
--- /dev/null
@@ -0,0 +1,39 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Thorsten Glaser <tg@mirbsd.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+#-
+# Convert a Niktextfile format table to Kidsman format
+
+f1=ℹ
+while IFS= read -r line; do
+       [[ $line = '|'*'|' ]] || continue
+       set -A fields -- "$f1"
+       f1=.
+       line=${line#'|'}
+       while [[ -n $line ]]; do
+               field=${line%%'|'*}
+               line=${line#*'|'}
+               field=${field##*([       ])}
+               field=${field%%*([       ])}
+               [[ -n $field ]] || field=.
+               field=${field//[         ]/ }
+               fields+=("$field")
+       done
+       print -r -- "${fields[@]}"
+done | column -t
diff --git a/mksh/teckids/teckids b/mksh/teckids/teckids
new file mode 100755 (executable)
index 0000000..af8b253
--- /dev/null
@@ -0,0 +1,458 @@
+#!/bin/mksh
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015, 2017
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015, 2016
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility wrapper script
+# Designed to be symlinked into $PATH (e.g. ~/bin)
+# Calls subcommands residing in util/ of the repo
+
+[[ -n $TECKIDS_WRAPPER_LOADED ]] && return 0
+TECKIDS_WRAPPER_LOADED=1
+
+export LC_ALL=C.UTF-8
+unset LANGUAGE
+ulimit -c 0
+set -U
+nl=$'\n'
+TECKIDS_CWD=$PWD
+
+function die {
+       local rv=1
+
+       # arith syntax check
+       if [[ $1 = [1-9]*([0-9]) ]]; then
+               rv=$1
+               # bounds check
+               if (( rv > 0 && rv <= 255 )); then
+                       # ok use it
+                       shift
+               else
+                       # nope
+                       rv=1
+               fi
+       fi
+       print -ru2 -- E: "$@"
+       exit $rv
+}
+
+qualify_pathname() (
+       cd "$TECKIDS_CWD"
+       realpath "$1"
+)
+
+[[ ${HOSTNAME:=$(hostname -s 2>/dev/null)} = *([  ]|localhost) ]] && \
+    HOSTNAME=$(hostname 2>/dev/null)
+: "${HOSTNAME:=nil}"
+[[ ${HOSTNAME%%.*} = luna ]] && die nicht auf luna arbeiten!
+
+for cmd in "$MKSH" "$(whence -p mksh)" /bin/mksh false; do
+       [[ $cmd = false ]] && die cannot find mksh
+       [[ -n $cmd ]] || continue
+       [[ -s $cmd ]] || continue
+       [[ -x $cmd ]] || continue
+       MKSH=$cmd
+       break
+done
+
+TECKIDS_CACHE_DIR=${XDG_CACHE_HOME:-~/.cache}/teckids
+[[ -d $TECKIDS_CACHE_DIR ]] || mkdir -p "$TECKIDS_CACHE_DIR" || die cannot mkdir TECKIDS_CACHE_DIR
+
+TECKIDS_CONFIG_DIR=${XDG_CONFIG_HOME:-~/.config}/teckids
+[[ -d $TECKIDS_CONFIG_DIR ]] || mkdir -p "$TECKIDS_CONFIG_DIR" || die cannot mkdir TECKIDS_CONFIG_DIR
+
+TECKIDS_MANAGEMENT_HOST=ticdesk.teckids.org
+
+export ROOT=./$(git rev-parse --show-cdup 2>&1)
+if [[ -z $ROOT || ! -d $ROOT || ! -e $ROOT/util/teckids ]]; then
+       ROOT=$(cat "$TECKIDS_CONFIG_DIR/root" 2>/dev/null)
+       if [[ -z $ROOT || ! -d $ROOT || ! -e $ROOT/util/teckids ]]; then
+               # Try to find root from script $0
+               ROOT=$(realpath "$0"/../..)
+               if [[ -z $ROOT || ! -d $ROOT || ! -e $ROOT/util/teckids ]]; then
+                       print -u2 "N: Run setup command to get the repository."
+                       die "Not inside teckids repository and no cached root found."
+               fi
+       fi
+
+       cd "$ROOT/util" || die "cannot cd ROOT/util ($ROOT/util)"
+fi
+ROOT=$(realpath "$ROOT")
+print -r -- "$ROOT" >"$TECKIDS_CONFIG_DIR/root"
+TOP=$(realpath "$ROOT"/www)
+
+# do not change the following line, I hardcoded this elsewhere too!
+TECKIDS_TMP_DIR=$ROOT/.tmp
+[[ -d $TECKIDS_TMP_DIR ]] || mkdir -p "$TECKIDS_TMP_DIR" || die cannot mkdir TECKIDS_TMP_DIR
+
+if [[ $1 = / ]]; then
+       cd "$ROOT"/util
+       ls
+       exit 0
+fi
+
+PATH=$TOP/mk:$PATH . assoldap.ksh
+if (( !common_read )); then
+       . "$TOP"/mk/common
+       export LC_ALL=C.UTF-8
+fi
+typeset -f Lb64decode >/dev/null || . "$TOP"/mk/base64.ksh
+#set -o xtrace +o inherit-xtrace
+
+cmd=
+if ! (( teckids_sourcing_wrapper )); then
+       cmd=$1; shift
+
+       case $cmd {
+       (.) ;;
+       (add|clone|commit|diff|log|pull|push|rm|status) ;;
+       (git|ci|st|up) ;;
+       (*)
+               export TECKIDS_RUNNING_COMMAND=$(realpath "$ROOT/util/$cmd")
+               [[ -e $ROOT/util/$cmd ]] && exec $MKSH "$ROOT/util/$cmd" "$@"
+
+               print -u2 -- "teckids: $cmd: not found"
+               exit 127
+               ;;
+       }
+fi
+
+if [[ -z $cmd ]]; then
+       # Option parsing
+       cmd_options=${cmd_options:+$cmd_options$'\n'}'B:\ 1ldap_base\ 1\ 1LDAP-Suchbasis (bei Tools, die es unterstützen)'
+       cmd_options+=$'\n''E\ 1eltern_subfilter\ 10\ 1Alle Operationen auf Eltern von Findlingen beschränken'
+       cmd_options+=$'\n''F:\ 1ldap_filter\ 1\ 1LDAP-Filter (bei Tools, die es unterstützen)'
+       cmd_options+=$'\n''h\ 1do_usage\ 10\ 1Benutzungshinweise anzeigen'
+       cmd_options+=$'\n''K\ 1kids_only\ 10\ 1Alle Operationen auf Kinder beschränken'
+       cmd_options+=$'\n''q\ 1quiet\ 10\ 1Nichts (oder so gut wie nichts) ausgeben'
+       cmd_options+=$'\n''v\ 1verbose\ 10\ 1Fortschritt der Ausführung ausgeben'
+       cmd_options+=$'\n''X\ 1extradebug\ 10\ 1Extra-Debugging (skriptabhängig)'
+       cmd_options+=$'\n''x\ 1xtrace\ 10\ 1Unter „set -x“ laufen lassen (-xx für mehr)'
+
+       print -r -- "$cmd_options" |&
+       while IFS="\ 1" read -rp o v d e; do
+               [[ $o != =* ]]; a=$?
+               o=${o#=}
+               asso_isset _opts "${o::1}" && die "E: Option $o mehrdeutig!"
+               opts+=$o
+               asso_sets "$o" _opts "${o::1}" o
+               asso_sets "$v" _opts "${o::1}" v
+               asso_sets "$d" _opts "${o::1}" d
+               asso_sets "$e" _opts "${o::1}" e
+               asso_seti "$a" _opts "${o::1}" a
+
+               [[ -n $v ]] || die "E: Option $o ohne Variablennamen!"
+               eval "$v=\$d"
+               [[ $a = 0 ]] || eval set -A "$v"
+       done
+
+       usage() {
+               print -r "Optionen dieses Kommandos (${0##*/}): $cmd_arguments"
+               print
+
+               print -r -- "$cmd_options" |&
+               while IFS="\ 1" read -rp o e; do
+                       o=${o#=}
+                       o=${o::1}
+                       e=$(asso_getv _opts "$o" e)
+                       d=$(asso_getv _opts "$o" d)
+
+                       print -r -- "  -$o    $e${d:+ (Standardwert: ${d@Q})}"
+               done
+
+               exit ${1:-1}
+       }
+
+       while getopts ":$opts" ch; do
+               [[ $ch = '?' ]] && die "Unknown option: $OPTARG"
+               [[ $ch = ':' ]] && die "Option $OPTARG missing required argument"
+               o=${ch#'+'}
+               nameref v=$(asso_getv _opts "$o" v)
+               if [[ $(asso_getv _opts "$o" a) = 1 ]]; then
+                       a=${#v[*]}
+                       nameref v="$(asso_getv _opts "$o" v)[$a]"
+               fi
+               o=$(asso_getv _opts "$o" o)
+
+               if [[ $o = *: ]]; then
+                       # ignore + in ch
+                       v=$OPTARG
+               elif [[ $ch = '+'* ]]; then
+                       v=0
+               else
+                       let v++
+               fi
+       done
+       shift $((OPTIND - 1))
+
+       typeset +n v
+
+       (( do_usage )) && usage 0
+       (( quiet )) && exec >/dev/null
+fi
+
+TECKIDS_SSHCP=$TECKIDS_TMP_DIR/sshmux
+# contract: $TECKIDS_SSHCP is a directory
+function teckids_ssh_checkpids {
+       local _wd=$PWD _i _notfound=1
+
+       cd "$TECKIDS_SSHCP" || die TECKIDS_SSHCP does not exist
+       for _i in *; do
+               if [[ $_i = mux ]]; then
+                       : skip
+               elif [[ $_i = 1 ]]; then
+                       _notfound=0
+               elif [[ $_i != +([0-9]) ]]; then
+                       # junk
+                       rm -f "$_i"
+               elif kill -0 "$_i" >/dev/null 2>&1; then
+                       _notfound=0
+               else
+                       rm -f "$_i"
+               fi
+       done
+       if (( _notfound )); then
+               ssh -S "$TECKIDS_SSHCP/mux" -O check $TECKIDS_MANAGEMENT_HOST \
+                   >/dev/null 2>&1 && >/dev/null 2>&1 \
+                   ssh -S "$TECKIDS_SSHCP/mux" -O exit $TECKIDS_MANAGEMENT_HOST
+               rm -f *
+       fi
+       cd "$_wd"
+       return $_notfound
+}
+
+function extra_cleanup {
+       :
+}
+
+if [[ ${HOSTNAME%%.*} != ${TECKIDS_MANAGEMENT_HOST%%.*} && $offline != 1 ]]; then
+       mkdir -p -m0700 "$TECKIDS_SSHCP"
+       :>"$TECKIDS_SSHCP/$$"
+       teckids_ssh_checkpids
+
+       if ! ssh -S "$TECKIDS_SSHCP/mux" -O check $TECKIDS_MANAGEMENT_HOST 2>/dev/null; then
+               (( verbose )) && print -ru2 "I: opening teckids ssh muxmaster"
+               ssh -S "$TECKIDS_SSHCP/mux" -L 15432:/var/run/postgresql/.s.PGSQL.5432 -fNM $TECKIDS_MANAGEMENT_HOST
+       fi
+
+       function terracmdn {
+               local _cmd _x
+
+               for _x in "$@"; do
+                       _cmd+=" ${_x@Q}"
+               done
+               ssh -n -S "$TECKIDS_SSHCP/mux" "$TECKIDS_MANAGEMENT_HOST" "$_cmd"
+       }
+
+       function terracmds {
+               local _cmd _x
+
+               for _x in "$@"; do
+                       _cmd+=" ${_x@Q}"
+               done
+               ssh -S "$TECKIDS_SSHCP/mux" "$TECKIDS_MANAGEMENT_HOST" "$_cmd"
+       }
+
+       function terracmdi {
+               local _cmd _x
+
+               for _x in "$@"; do
+                       _cmd+=" ${_x@Q}"
+               done
+               ssh -t -S "$TECKIDS_SSHCP/mux" "$TECKIDS_MANAGEMENT_HOST" "$_cmd"
+       }
+
+       function ldapsearch {
+               terracmdn ldapsearch -a always "$@"
+       }
+
+       function ldapmodify {
+               terracmds ldapmodify "$@"
+       }
+
+       function cleanup {
+               extra_cleanup
+               mkdir -p -m0700 "$TECKIDS_SSHCP"
+               rm -f "$TECKIDS_SSHCP/$$"
+               teckids_ssh_checkpids
+       }
+
+       function teckids_git {
+               GIT_SSH=$(realpath "$ROOT/util/ssh_wrapper") \
+                   TECKIDS_SSHCP=$TECKIDS_SSHCP \
+                   TECKIDS_MANAGEMENT_HOST=$TECKIDS_MANAGEMENT_HOST \
+                   git "$@"
+       }
+else
+       function terracmdn {
+               </dev/null terracmds "$@"
+       }
+
+       function terracmds {
+               "$@"
+       }
+
+       function terracmdi {
+               "$@"
+       }
+
+       function cleanup {
+               extra_cleanup
+       }
+
+       function teckids_git {
+               git "$@"
+       }
+fi
+
+function print_v {
+       (( verbose )) && print -u2 -r -- "$*"
+}
+
+function find_up {
+       local _x
+
+       _x=./$1
+       while :; do
+               [[ -e $_x ]] && break
+
+               if [[ -e ${_x%/*}/gnu.mk ]]; then
+                       return 1
+               fi
+
+               _x=../$_x
+       done
+
+       print -r -- "$_x"
+}
+
+function find_projfiles {
+       local _x
+
+       ptxt=
+
+       _x=$(find_up vars) && . ./"$_x"
+       _x=$(find_up planung.txt) && ptxt=$_x
+}
+
+function teckids_loadk_users {
+       local args dn n=0
+       set -A upath -- "$@"
+       # default associative array path
+       (( $# )) || upath[0]=users
+
+       # load list of DNs to asso_y shell array
+       asso_loadk "${upath[@]}"
+
+       if (( kids_only )); then
+               # postprocess for filter
+               set -A args
+               for dn in "${asso_y[@]}"; do
+                       [[ $dn = *',ou=Kids,'* ]] || continue
+                       [[ $dn = *',ou=Eltern,'* ]] && continue
+                       args[n++]=$dn
+               done
+               # load result
+               set -A asso_y -- "${args[@]}"
+       fi
+
+       if (( eltern_subfilter )); then
+               # postprocess for filter
+               if (( eltern_subfilter < 2 )); then
+                       asso_setnull "${upath[@]}"
+                       asso_setasso "${upath[@]}"
+               fi
+               for dn in "${asso_y[@]}"; do
+                       asso_setldap_sasl "${upath[@]}" -+ \
+                           -b "ou=Eltern,$dn" -s sub '(objectClass=inetOrgPerson)'
+               done
+               # load result
+               asso_loadk "${upath[@]}"
+       fi
+}
+
+function ask_password_once {
+       set -o noglob
+       stty -echo
+       read REPLY?"$1 "
+       stty echo
+       set +o noglob
+       echo
+}
+
+function cleanup_and_exit {
+       cleanup "$@"
+       exit "$1"
+}
+
+case $cmd {
+(.)
+       print -u2 'I: success; run "cleanup" to finish'
+       return 0
+       ;;
+(git)
+       teckids_git "$@"
+       cleanup_and_exit $?
+       ;;
+(clone)
+       if [[ -n $1 ]]; then
+               teckids_git clone "$@"
+               cleanup_and_exit $?
+       else
+               teckids_git clone edugit.org:Teckids/verein.git
+               cleanup_and_exit $?
+       fi
+       ;;
+(add|commit|diff|log|pull|push|rm|status)
+       teckids_git "$cmd" "$@"
+       cleanup_and_exit $?
+       ;;
+(ci)
+       teckids_git commit "$@"
+       cleanup_and_exit $?
+       ;;
+(st)
+       teckids_git status "$@"
+       cleanup_and_exit $?
+       ;;
+(up)
+       teckids_git pull --rebase "$@"
+       cleanup_and_exit $?
+       ;;
+(?*)
+       print -u2 "E: should not happen, cmd='$cmd'"
+       ;;
+(*)
+       # command directly called, e.g. “mksh util/whoami”
+       [[ -n $TECKIDS_RUNNING_COMMAND ]] || \
+           export TECKIDS_RUNNING_COMMAND=$(realpath "$0")
+       ;;
+}
+
+find_projfiles
+
+trap 'cleanup_and_exit $?' 0
+trap 'trap - 0; cleanup_and_exit 1' 1 2 3 13 15
+
+if (( xtrace )); then
+       set -o xtrace +o inherit-xtrace
+       (( xtrace > 1 )) && set -o inherit-xtrace
+fi
diff --git a/mksh/teckids/updatekid b/mksh/teckids/updatekid
new file mode 100644 (file)
index 0000000..42604ea
--- /dev/null
@@ -0,0 +1,52 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that updates attributes of a kid
+
+cmd_arguments='[dn] field value [field value …]'
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+(( $# )) || usage
+
+if (( $# % 2 == 0 )); then
+       dn=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+else
+       dn=$1
+       shift
+fi
+
+f=
+while (( $# > 0 )); do
+       f="${f}replace: $1
+$1: $2
+-
+"
+       shift; shift
+done
+
+ldapmodify <<EOF
+dn: $dn
+changetype: modify
+${f}
+EOF
+
+exit 0
diff --git a/mksh/teckids/viplanung b/mksh/teckids/viplanung
new file mode 100644 (file)
index 0000000..96e21b0
--- /dev/null
@@ -0,0 +1,430 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015, 2017
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that copies planung.txt, deanonymises it and
+# pulls it up for editing
+
+# teckids shell stuff
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+set -U
+
+# teckids framework should have found planung.txt, bail out if not
+[[ -e $ptxt ]] || exit 1
+
+# Get a temporary directory
+mkdir -p "$TOP/../.tmp"
+d=$(mktemp -d "$TOP/../.tmp/XXXXXXXXXX")
+
+# Array for field lengths in order to rearrange ASCII table
+set -A lengths
+
+# Start reading planung.txt, iterating over lines in a state machine
+s=0
+while IFS= read -r line; do
+       case $s in
+       0) # Nothing read yet
+               # Keep looking for table head line, marked by a Message-ID field guaranteed to be there
+               if [[ $line = "| Message-ID "* ]]; then
+                       # Add columns Vorname, Nachname, UID, Schule to headline
+                       line=${line/\| Message-ID+( )\|/\| Message-ID \| Vorname \| Nachname \| UID \| Schule \|}
+                       liner=${line#\| }
+                       liner=${liner% \|}
+
+                       # Read remaining fields into array „fields“
+                       sIFS=$IFS; IFS="|"; set -A fields -- $liner; IFS=$sIFS
+
+                       # Iterate over field names and calculate lengths, initial values
+                       i=-1
+                       while (( ++i < ${#fields[@]} )); do
+                               fields[i]=${fields[i]##*( )}
+                               fields[i]=${fields[i]%%*( )}
+                               lengths[i]=${%fields[i]}
+                       done
+
+                       # Output resulting new headline
+                       print -r -- "| $liner |"
+
+                       # Continue to next state
+                       s=1
+               else
+                       # Print raw line if headline not yet found
+                       print -r -- "$line"
+               fi
+               ;;
+       1) # Line seperator expected
+               print -r -- "$line"
+               s=2
+               ;;
+       2) # Content line expected
+               if [[ $line = "+-"* ]]; then
+                       # Content finished, found terminating seperator line
+                       print -r -- "$line"
+                       s=3
+               else
+                       # Strip leading and trailing col seperator
+                       liner=${line#\| }
+                       liner=${liner% \|}
+
+                       # Read all fields into array „linea“
+                       sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+                       # Iterate over all fields and strip leading and trailing whitespace
+                       i=-1
+                       while (( ++i < ${#linea[@]} )); do
+                               linea[i]=${linea[i]##*( )}
+                               linea[i]=${linea[i]%%*( )}
+                       done
+
+                       # Load fields from LDAP, associated with Message-ID (array field 0)
+                       asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+                           "(|(anmMessageId=<${linea[0]}*)(anmMessageId=Ticket#${linea[0]}*))" givenName sn o l uid
+
+                       # Load various fields from LDAP result
+                       asso_loadk users
+                       dn=${asso_y[0]}
+                       gn=$(asso_getv users "$dn" givenName 0)
+                       sn=$(asso_getv users "$dn" sn 0)
+                       uid=$(asso_getv users "$dn" uid 0)
+                       o=$(asso_getv users "$dn" o 0)
+                       l=$(asso_getv users "$dn" l 0)
+
+                       # Replace loaded fields into beginning of original line
+                       liner=${line/#\|*( )"${linea[0]}"*( )/\| ${linea[0]} \| $gn \| $sn \| $uid \| $o, $l }
+                       liner=${liner#\| }
+                       liner=${liner% \|}
+
+                       # Read fields of resulting new line
+                       sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+                       # Iterate over fields and collect lengths
+                       i=-1
+                       while (( ++i < ${#linea[@]} )); do
+                               linea[i]=${linea[i]##*( )}
+                               linea[i]=${linea[i]%%*( )}
+                               # Use length if longer than last known value
+                               (( ${%linea[i]} > ${lengths[i]} )) && lengths[i]=${%linea[i]}
+                       done
+
+                       # Print resulting new line
+                       print -r -- "| $liner |"
+               fi
+               ;;
+       3) # Stats line expected after second row seperator
+               # Remove leading and trailing | from line
+               liner=${line#\|}
+               liner=${liner% \|}
+               # Generate line with empty values for LDAP fields
+               liner="${liner%%\|*}|  |  |  |  | ${liner#*\|}"
+
+               # Read fields of resulting new line
+               sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+               # Iterate over fields and collect lengths
+               i=-1
+               while (( ++i < ${#linea[@]} )); do
+                       linea[i]=${linea[i]##*( )}
+                       linea[i]=${linea[i]%%*( )}
+                       # Use length if longer than last known value
+                       (( ${%linea[i]} > ${lengths[i]} )) && lengths[i]=${%linea[i]}
+               done
+
+               print -r -- "| $liner |"
+               s=0
+               ;;
+       esac
+done <"$ptxt" >"$d/planung.tmp"
+
+# Function to rearrange a table with known lengths
+function fixtable {
+       # State machine
+       s=0
+       oline=
+       first=1
+       # Iterate over lines
+       while IFS= read -r line; do
+               case $s in
+               0) # Look for headline, marked by Message-ID field
+                       if [[ $line = "| Message-ID "* ]]; then
+                               # Generate new top seperator with known lengths
+                               print -n "+"
+                               for l in "${lengths[@]}"; do
+                                       perl -e 'print "=" x '$(( l + 2 ))
+                                       print -n "+"
+                               done
+                               print
+
+                               # Continue to next state
+                               s=1
+                       else
+                               # FIXME What is „first“?
+                               (( first )) || print -r -- "$oline"
+                       fi
+                       ;;
+               1) # Top seperator written, caring for headline
+                       # Strip leading and trailing |
+                       liner=${oline#\| }
+                       liner=${liner% \|}
+
+                       # Read all fields into array „linea“
+                       sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+                       # Iterate over all fields
+                       i=-1
+                       while (( ++i < ${#linea[@]} )); do
+                               # Strip leading and trailing space from field
+                               linea[i]=${linea[i]##*( )}
+                               linea[i]=${linea[i]%%*( )}
+                               # Calculate necessary whitespace to fill field to known length
+                               p=$(( lengths[i] - ${%linea[i]} + 1 ))
+
+                               # Print field
+                               print -rn "| ${linea[i]}"
+                               # Print necessary whitespace
+                               perl -e 'print " " x '$p
+                       done
+                       print "|"
+
+                       # Continue to next state
+                       s=2
+                       ;;
+               2) # Headline written, write next seperator
+                       print -n "+"
+                       for l in "${lengths[@]}"; do
+                               perl -e 'print "=" x '$(( l + 2 ))
+                               print -n "+"
+                       done
+                       print
+
+                       s=3
+                       ;;
+               3) # Headline and seperator written, expecting content lines now
+                       # Strip leading and trailing |
+                       liner=${oline#\| }
+                       liner=${liner% \|}
+
+                       # Read all fields into array „linea“
+                       sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+                       # Iterate over fields
+                       i=-1
+                       while (( ++i < ${#linea[@]} )); do
+                               # Strip leading and trailing whitespace from field
+                               linea[i]=${linea[i]##*( )}
+                               linea[i]=${linea[i]%%*( )}
+                               # Calculate whitespace necessary to fill field
+                               p=$(( lengths[i] - ${%linea[i]} ))
+
+                               # Switch for justification of column, either numeric (right) or string (left)
+                               if [[ ${linea[i]} = @(0|[1-9]*([0-9])) ]]; then
+                                       print -rn "| "
+                                       perl -e 'print " " x '$p
+                                       print -rn "${linea[i]} "
+                               else
+                                       print -rn "| ${linea[i]}"
+                                       perl -e 'print " " x '$(( p + 1 ))
+                               fi
+                       done
+                       print "|"
+
+                       # Continue to next state if line was a seperator
+                       [[ $line = "+-"* ]] && s=4
+                       ;;
+               4) # End of content lines, seperator found, generate new seperator with known lengths
+                       print -n "+"
+                       for l in "${lengths[@]}"; do
+                               perl -e 'print "-" x '$(( l + 2 ))
+                               print -n "+"
+                       done
+                       print
+
+                       s=5
+                       ;;
+               5) # Stats line expected, print with known lengths
+                       # Strip leading and trailing |
+                       liner=${oline#\| }
+                       liner=${liner% \|}
+
+                       # Read all fields into array „linea“
+                       sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+                       # Iterate over all fields
+                       i=-1
+                       while (( ++i < ${#linea[@]} )); do
+                               # Strip leading and trailing whitespace from field
+                               linea[i]=${linea[i]##*( )}
+                               linea[i]=${linea[i]%%*( )}
+                               # Calculate whitespace necessary to fill field
+                               p=$(( lengths[i] - ${%linea[i]} + 1 ))
+
+                               # Print new field with necessary whitespace
+                               print -rn "| ${linea[i]}"
+                               perl -e 'print " " x '$p
+                       done
+                       print "|"
+
+                       s=6
+                       ;;
+               6) # Stats line written, genereate last seperator
+                       print -n "+"
+                       for l in "${lengths[@]}"; do
+                               perl -e 'print "=" x '$(( l + 2 ))
+                               print -n "+"
+                       done
+                       print
+
+                       # GO into state 0 again to consume and not touch rest of file
+                       s=0
+                       ;;
+               esac
+
+               oline=$line
+               first=0
+       done
+
+       # Print last read line again
+       print -r -- "$oline"
+}
+
+# Output table after having parsed all lengths
+fixtable <"$d/planung.tmp" >"$d/planung.ed"
+
+# Take checksum, spawn interactive editor, take checksum again
+ck1=$(md5sum "$d/planung.ed")
+"${VISUAL:-${EDITOR:-vi}}" $1 "$d/planung.ed"
+ck2=$(md5sum "$d/planung.ed")
+
+# Only update original file if changes were made
+if [[ $ck1 != "$ck2" ]]; then
+       # New array for lengths
+       set -A lengths
+
+       # Iterate over all lines wit hstate machine
+       s=0
+       while IFS= read -r line; do
+               case $s in
+               0) # Look for headline, marked by Message-ID field
+                       if [[ $line = "| Message-ID "* ]]; then
+                               # Remove dynamic fields added from LDAP, replace by single Message-ID field
+                               line=${line/#\|+([!\|])\|+([!\|])\|+([!\|])\|+([!\|])\|+([!\|])/| Message-ID }
+
+                               # Strip leading and trailing | from line
+                               liner=${line#\| }
+                               liner=${liner% \|}
+
+                               # Read all fields into array „fields“
+                               sIFS=$IFS; IFS="|"; set -A fields -- $liner; IFS=$sIFS
+
+                               # Iterate over all fields and calculate lengths
+                               i=-1
+                               while (( ++i < ${#fields[@]} )); do
+                                       fields[i]=${fields[i]##*( )}
+                                       fields[i]=${fields[i]%%*( )}
+                                       lengths[i]=${%fields[i]}
+                               done
+
+                               # Print line again
+                               print -r -- "| $liner |"
+
+                               s=1
+                       else
+                               # Print raw line if headline not yet found
+                               print -r -- "$line"
+                       fi
+                       ;;
+               1) # Headline found, print seperator
+                       print -r -- "$line"
+                       s=2
+                       ;;
+               2) # Seperator printed, expecting content lines
+                       if [[ $line = "+-"* ]]; then
+                               # Next seperator found, continue to next state
+                               print -r -- "$line"
+                               s=3
+                       else
+                               # Strip leading | from line
+                               liner=${line#\|}
+                               # Get beginning of line, i.e. strip everything after last pipe (annotations)
+                               beg=${liner%%\|*}
+                               # Remove fields loaded from LDAP
+                               line=${line/#\|+([!\|])\|+([!\|])\|+([!\|])\|+([!\|])\|+([!\|])/|$beg }
+
+                               # Strip leading and trailing | from line
+                               liner=${line#\| }
+                               liner=${liner% \|}
+
+                               # Read all fields into array „linea“
+                               sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+                               # Iterate over all fields, take new lengths if bigger than previous value
+                               i=-1
+                               while (( ++i < ${#linea[@]} )); do
+                                       linea[i]=${linea[i]##*( )}
+                                       linea[i]=${linea[i]%%*( )}
+                                       (( ${%linea[i]} > ${lengths[i]} )) && lengths[i]=${%linea[i]}
+                               done
+
+                               # Print line again
+                               print -r -- "| $liner |"
+                       fi
+                       ;;
+               3) # End of content lines, seperator found and printed, care about stats line
+                       # Same logic as for content lines
+                       liner=${line#\|}
+                       beg=${liner%%\|*}
+                       line=${line/#|+([!\|])\|+([!\|])\|+([!\|])\|+([!\|])\|+([!\|])/$beg }
+
+                       # Strip leading and trailing | from line
+                       liner=${line#\| }
+                       liner=${liner% \|}
+
+                       # Read all fields into array „linea“
+                       sIFS=$IFS; IFS="|"; set -A linea -- $liner; IFS=$sIFS
+
+                       # Iterate over all fields, take new lengths if bigger than previous value
+                       i=-1
+                       while (( ++i < ${#linea[@]} )); do
+                               linea[i]=${linea[i]##*( )}
+                               linea[i]=${linea[i]%%*( )}
+                               (( ${%linea[i]} > ${lengths[i]} )) && lengths[i]=${%linea[i]}
+                       done
+
+                       print -r -- "| $liner |"
+
+                       # Go into state 0 again to consume and not touch rest of file
+                       s=0
+                       ;;
+               esac
+       done <"$d/planung.ed" >"$d/planung.tmp"
+
+       # Call fixtable to fix lengths in generated table
+       fixtable <"$d/planung.tmp" >"$d/planung.out"
+
+       # Move generated file over to original file and update stats
+       cp "$d/planung.out" "$ptxt"
+       . "$ROOT/util/statupd"
+else
+       print "planung.txt not changed." >&2
+fi
+
+# Remove temporary directory
+rm -rf "$d"
diff --git a/mksh/teckids/whoami b/mksh/teckids/whoami
new file mode 100644 (file)
index 0000000..1aa9e89
--- /dev/null
@@ -0,0 +1,24 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2016
+#      Dominik George <dominik.george@teckids.org>
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that wraps whois for myself by authenticating to management host
+
+. "$(dirname "$0")/whois"
diff --git a/mksh/teckids/whois b/mksh/teckids/whois
new file mode 100644 (file)
index 0000000..08131f5
--- /dev/null
@@ -0,0 +1,210 @@
+# -*- mode: sh -*-
+#-
+# Copyright © 2014, 2015, 2016, 2017
+#      Dominik George <dominik.george@teckids.org>
+# Copyright © 2014, 2015, 2016
+#      mirabilos <thorsten.glaser@teckids.org>
+#
+# Provided that these terms and disclaimer and all copyright notices
+# are retained or reproduced in an accompanying document, permission
+# is granted to deal in this work without restriction, including un‐
+# limited rights to use, publicly perform, distribute, sell, modify,
+# merge, give away, or sublicence.
+#
+# This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
+# the utmost extent permitted by applicable law, neither express nor
+# implied; without malicious intent or gross negligence. In no event
+# may a licensor, author or contributor be held liable for indirect,
+# direct, other damage, loss, or other issues arising in any way out
+# of dealing in the work, even if advised of the possibility of such
+# damage or existence of a defect, except proven that it results out
+# of said person’s immediate fault when using the work as intended.
+
+# Teckids utility subcommand that prints a summary about a user
+
+cmd_options='r\ 1raw\ 10\ 1Rohe Ausgabe erzeugen (LDAP-Feldnamen)'
+cmd_options+=$'\n''s:\ 1single\ 10\ 1Nur einzelne Felder pro Ergebnis ausgeben'
+cmd_options+=$'\n''t:\ 1single_sep\ 1,\ 1Dies als Trennzeichen für CSV benutzen'
+
+teckids_sourcing_wrapper=1
+. "$(dirname "$0")/teckids"
+
+if [[ $TECKIDS_RUNNING_COMMAND = */whoami || $0 = ?(*/)whoami ]]; then
+       (( $# == 0 )) || die „whoami“ nimmt keine Argumente, nur Optionen!
+       field=uid
+       if [[ $HOSTNAME = ${TECKIDS_MANAGEMENT_HOST%%.*} ]]; then
+               value=$(whoami)
+       else
+               value=$(mksh "$(dirname "$0")/ssh" whoami)
+       fi
+elif (( $# == 0 )); then
+       field=dn
+       value=$(cat "$TECKIDS_CACHE_DIR"/last_whois)
+       if (( kids_only )) && [[ $value = *,ou=Eltern,* ]]; then
+               value=${value#*,ou=Eltern,}
+       fi
+elif (( $# == 1 )); then
+       case $1 in
+               [a-z]+([a-z0-9])) field=uid ;;
+               *@*) field=mail ;;
+               +([0-9])) field=employeeNumber ;;
+               *) field=cn ;;
+       esac
+       value=$1
+       [[ $field = employeeNumber ]] && (( value >= 20000 )) && field=teckidsCashAccount
+elif (( $# == 2 )); then
+       field=$1
+       value=$2
+fi
+
+if [[ $field = dn ]]; then
+       asso_setldap_sasl users -- -b "$value" -s base
+else
+       asso_setldap_sasl users -- -b ou=People,dc=teckids,dc=org \
+           "($field=$value)"
+fi
+
+teckids_loadk_users
+
+if (( raw )); then
+       for user_dn in "${asso_y[@]}"; do
+               asso_loadk users "$user_dn"
+               for field in "${asso_y[@]}"; do
+                       n=$(asso_getv users "$user_dn" "$field" count)
+                       i=-1
+                       while (( ++i < n )); do
+                               print -r -- "$field: $(asso_getv users "$user_dn" "$field" $i)"
+                       done
+               done
+               print -- ----
+               print -r -- "$user_dn" >"$TECKIDS_CACHE_DIR"/last_whois
+       done | if [[ -t 1 ]]; then
+               # stdout is a tty
+               less +-S
+       else
+               cat
+       fi
+       exit 0
+fi
+
+if [[ $single != 0 ]]; then
+       sIFS=$IFS; IFS=,; set -A single_fields -- $single; IFS=$IFS
+       for user_dn in "${asso_y[@]}"; do
+               _first=1
+               for f in "${single_fields[@]}"; do
+                       v=$(asso_getv users "$user_dn" "$f" 0)
+                       (( _first )) || print -rn -- "$single_sep"
+                       print -rn -- "$v"
+                       _first=0
+               done
+               print
+       done
+       exit 0
+fi
+
+for user_dn in "${asso_y[@]}"; do
+       givenName=$(asso_getv users "$user_dn" givenName 0)
+       sn=$(asso_getv users "$user_dn" sn 0)
+       employeeNumber=$(asso_getv users "$user_dn" employeeNumber 0)
+       dateOfBirth=$(asso_getv users "$user_dn" dateOfBirth 0)
+       mail=$(asso_getv users "$user_dn" mail 0)
+       homePostalAddress=$(asso_getv users "$user_dn" homePostalAddress 0)
+       mobile=$(asso_getv users "$user_dn" mobile 0)
+       uid=$(asso_getv users "$user_dn" uid 0)
+       uidNumber=$(asso_getv users "$user_dn" uidNumber 0)
+       homeDirectory=$(asso_getv users "$user_dn" homeDirectory 0)
+       loginShell=$(asso_getv users "$user_dn" loginShell 0)
+       o=$(asso_getv users "$user_dn" o 0)
+       ou=$(asso_getv users "$user_dn" ou 0)
+       l=$(asso_getv users "$user_dn" l 0)
+       st=$(asso_getv users "$user_dn" st 0)
+       c=$(asso_getv users "$user_dn" c 0)
+       cashAccount=$(asso_getv users "$user_dn" teckidsCashAccount 0)
+       dues=$(asso_getv users "$user_dn" teckidsMemberDues 0)
+       memberSince=$(asso_getv users "$user_dn" teckidsMemberSince 0)
+       iban=$(asso_getv users "$user_dn" iBAN 0)
+       bic=$(asso_getv users "$user_dn" bIC 0)
+
+       : ${c:=Deutschland}
+       [[ -n $st ]] && c="$st, $c"
+
+       if dtchk dtJ "$dateOfBirth" dtv; then
+               set -A tmGeb -- $(mjd_explode "$dtJ" 0)
+               set -A tmNow -- $(mjd_explode $(timet2mjd $(date +%s)))
+               (( age = tmNow[tm_year] - tmGeb[tm_year] - ((tmNow[tm_yday] < tmGeb[tm_yday]) ? 1 : 0) ))
+       else
+               unset age
+       fi
+
+       print "Personendaten"
+       print "============="
+       print
+       print -r -- "Nachname:|$sn|Vorname:|$givenName"
+       print -r -- "Geburtstag:|${dateOfBirth:- }|Alter:|$age"
+       print
+
+       print "Kontakt"
+       print "======="
+       print
+       print -r -- "E-Mail:|$mail"
+       print -r -- "Handy:|${mobile:- }|Anschrift:|$homePostalAddress"
+       print
+
+       print "Schule"
+       print "======"
+       print
+       print -r -- "Schule:|${o:- }|Schulort:|$l"
+       print -r -- "Klasse:|${ou:- }|Land:|$c"
+       print
+
+       if [[ -n $employeeNumber ]]; then
+               print "Mitglied"
+               print "========"
+               print
+               print -r -- "Mitglieds-Nr.:|${employeeNumber:- }|Eintritt:|$memberSince"
+               print -r -- "Beitrag:|$dues €"
+               print
+       fi
+
+       if [[ -n $cashAccount$employeeNumber ]]; then
+               print "Buchhaltung"
+               print "==========="
+               print
+               print -r -- "Konto:|${employeeNumber:-$cashAccount}|Kontostand:|$("$ROOT/util/not_teckidscmd/gnc_balance" ${employeeNumber:-$cashAccount}) €"
+               print -r -- "IBAN:|${iban:- }|BIC:|$bic"
+               print
+       fi
+
+       if [[ -n $uid ]]; then
+               print "Account"
+               print "======="
+               print
+               print -r -- "Benutzer:|${uid:- }|UID:|$uidNumber"
+               print -r -- "Home:|${homeDirectory:- }|Shell:|$loginShell"
+               print
+       fi
+
+       for a in Gremien Teams Themen Privileges Projekte; do
+               print "$a"
+               print "${a//?/=}"
+               print
+
+               asso_setldap_sasl groups -- -b ou=$a,ou=Groups,dc=teckids,dc=org \
+                   -s one "(member=$user_dn)"
+
+               asso_loadk groups
+               for group_dn in "${asso_y[@]}"; do
+                       cn=$(asso_getv groups "$group_dn" cn 0)
+                       description=$(asso_getv groups "$group_dn" description 0)
+
+                       print -r -- " |$cn| |$description"
+               done
+
+               print
+       done
+
+       print -r -- "$user_dn" >"$TECKIDS_CACHE_DIR"/last_whois
+done | column -s"|" -t -e
+
+[[ -n "${asso_y[*]}" ]] || exit 1
+exit 0