#!/bin/bash
#Scan for shares and make Rox apps for mount points. Public domain.
# merged with 01micko's update for slacko 20121028 (0.5)
# 201711 step: update AppRun only, versioned 1.0.0:
#   Support multiple servers with assorted shares/passwords; all dialogs yad; gettexted.
#   Three phases: scan loop; edit/save/load dialog; create RoxApps loop.
# 1.0.1 step: hide/show passwords.
# 2.0.0 step: +privacy, +usability, +gettext, +dev tools, bug fixing. Depend yad patched with github PR 26.
VERSION=2.0.0
handle_fatal() # {{{1
{
  trap - ERR
  local fifo=/tmp/smb-browser-fatal frame=0
  [ -e "$fifo" ] && rm -f "$fifo"; mkfifo "$fifo"
  yad \
    --title="Unexpected Error - SMB Browser" \
    --text="<b>Call Stack</b> - close app windows and quit\r" \
    --on-top --center --image=gtk-stop --window-icon=gtk-stop \
    --width=600 --height=400 --timeout=0 \
    --button=gtk-quit --buttons-layout=center --borders=4 \
    --text-info < "$fifo" &
  sleep 0.5
  while caller $((frame++)); do :; done > "$fifo" 2>&1
  wait
  rm "$fifo"
  exit 1
}
trap handle_fatal ERR # make your code error free!

init_app1() # {{{1 @EXPORT@ see EXP''ORT further down
{
  set +f
  PARAMS="$@"
  APPDIR=`dirname "$0"` #find where we are, not $HOME.
  cd "$APPDIR"
  APPDIR="$PWD"
}
init_app1 "$@" || exit 1 # {{{1}}}
export TEXTDOMAIN=fatdog OUTPUT_CHARSET=UTF-8
. gettext.sh
. ./scripts/i18n_table.sh && i18n_table || exit 1
[ $EUID -ne 0 ] && exec gtksu "$i18n_GtksuTitle" "$0" "$@"
APPTITLE=$i18n_AppTitle APPWINSIZE=$i18n_WinSize
YAD_TITLE=$APPTITLE
init_app2() # {{{1 @EXPORT@
{
  export YAD_OPTIONS="$YAD_OPTIONS --title='$YAD_TITLE' --window-icon='$APPDIR/.DirIcon' --buttons-layout=center --borders=4"
}
init_app2
. yad-lib.sh # {{{1}}} In: $YAD_TITLE
export YAD_GEOMETRY YAD_GEOMETRY_POPUP
HIDEPASSWORDS=${HIDEPASSWORDS:-1}
SEPARATOR=$'\b' # must not be one of the default $IFS characters
# set =. to hide resource files or $MOUNTNAME icon of the Mount RoxApps
MOUNTNAME=mnt-point RES=resources HIDE_RES=. HIDE_MOUNTNAME=
# sec=ntlm removed for 3.9 onwards
# scripts/part2 automatically converts gid=<name> to its numeric gid equivalent.
DEFAULT_MOUNT_OPTIONS="noserverino,file_mode=0775,dir_mode=0775,gid=users"

SMBCLIENT=smbclient
# $SMBCLIENT error 'not support EXTENDED_SECURITY.*spnego = yes.*ntlmv2 auth = yes'
# is harmless but if you want to control it set
# SMBCLIENT="smbclient --option=clientusespnego=no"
# (SMB1 client without extended security)

TMPD=$(mktemp -d -p "${TMPDIR:-/tmp}" "smb-browser_XXXXXX") && chmod 700 "$TMPD" || exit 1
export TMPD
handler() # {{{1
{
  trap - HUP INT QUIT TERM ABRT 0 ERR
  close_preview_dialog  >/dev/null 2>&1
  close_progress_dialog >/dev/null 2>&1
  case $TMPD in
    ""|/|.|..""|./*|../*) : ;;
    *)
      [ -e "$TMPD"/tmp-mount ] && mountpoint -q "$TMPD"/tmp-mount && umount -n "$TMPD"/tmp-mount
      rm -rf "$TMPD"* ;;
  esac
}
trap handler HUP INT QUIT TERM ABRT 0

add_resources() # $1-path-to-appdir; In/Out {{{1
# In: $HIDE_RES $RES
# Out: $ICON_MOUNTED $ICON_UNMOUNTED $ICON_SHOW $ICON_HIDE
{
  local p AP=${1%%/} resd
  ICON_MOUNTED=mounted.svg ICON_UNMOUNTED=drive48.png ICON_SHOW=show.svg ICON_HIDE=hide.svg
  resd="$AP/$HIDE_RES$RES" && mkdir "$resd" &&
  (cd "$APPDIR"/icons && cp $ICON_UNMOUNTED smb_overlay.png $ICON_SHOW $ICON_HIDE CREDITS "$resd") &&
  # Create moveable $ICON_MOUNTED: image data is embedded in the file.
  # For a smaller but non-moveable icon replace lines image1 and image2 below with:
  # <image xlink:href="$resd/$ICON_UNMOUNTED" width="48" height="48" id="image1"/>
  # <image xlink:href="$resd/smb_overlay.png" width="48" height="48" id="image2"/>
  cat > "$resd/$ICON_MOUNTED" << EOF &&
<svg width="48" height="48" id="svg1" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs id="defs1">
<linearGradient id="LG1">
   <stop style="stop-color:#00ff00;stop-opacity:1;" id="stop1"/>
   <stop style="stop-color:#333333;stop-opacity:1;" offset="1" id="stop2"/>
</linearGradient>
<radialGradient xlink:href="#LG1" id="RG1" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"/>
</defs>
<image xlink:href="data:image/png;base64,$(base64 "$resd/$ICON_UNMOUNTED")" width="48" height="48" id="image1"/>
<circle cx="40" cy="8" r="8" style="fill:url(#RG1);" id="ellipse1"/>
<image xlink:href="data:image/png;base64,$(base64 "$resd/smb_overlay.png")" width="48" height="48" id="image2"/>
</svg>
EOF
  mountpoint -q "$AP/$HIDE_MOUNTNAME$MOUNTNAME" 2>/dev/null &&
  ln -sfT ${resd#$AP/}/$ICON_MOUNTED "$AP"/.DirIcon ||
  ln -sfT ${resd#$AP/}/$ICON_UNMOUNTED "$AP"/.DirIcon
}

get_subnets() # {{{1 @EXPORT@
# IP4 only for now
{
  ifconfig |
  awk '/inet[ ]/ && !/127\.0\.0\.1/ { split($2, A, /[:.]/); print A[1]"."A[2]"."A[3] }'
}

scan_subnets() #$1-list-of-subnets; Out {{{1
# Out: "$TMPD"/net-*
{
  local net pids
  # "$TMPD"/subnets lists subnets
  # Scan subnets in parallel.
  rm -f "$TMPD"/net-*
  for net in $1; do
    stdbuf -oL mpscan $TIMEOUT -p $PORT $net.1 - $net.254 >"$TMPD"/net-$net 2>/dev/null &
    pids+=" $!"
  done
  trap ": got USR1; kill $pids" USR1
  wait $pids
  cat "$TMPD"/net-* 2>/dev/null | cut -d " " -f 2
}

get_server_name() # $1-IP {{{1
{
  # Couldn't reliably get server name with smbclient.
  nmblookup -A $1 | awk '/<20>/ { sub(/^[ \t]+/, "", $1); print $1; exit }'
}

get_share_names() # In/Out {{{1
# In: $Username $Password $SMBCLIENT $IP $NAME
# Out: $NSHARES $NADMIN_SHARES (number of all shares = NSHARES + NADMIN_SHARES)
{
  NSHARES=0 NADMIN_SHARES=0
  local ret auth="$TMPD"/$RANDOM out="$TMPD"/$RANDOM err="$TMPD"/$RANDOM count="$TMPD"/$RANDOM
  # also 'domain = <value>' if needed
  printf 'username = %s\npassword = %s\n' "$Username" "$Password" > "$auth"
  $SMBCLIENT -A "$auth" -g -L $IP 1>"$out" 2>"$err" || # try with credentials
  $SMBCLIENT -U % -g -L $IP 1>>"$out" 2>>"$err" # if that failed try open access
  awk -F\| -v COUNT="$count" '
/Disk/ {
    # Filter automatic Windows administrative shares https://en.wikipedia.org/wiki/Administrative_share.
  if($2 ~ /[A-Za-z][$]$/) {
    ++nadmin_shares
  } else {
    ++nshares
    print $2
  }
}
END {
  print 0+nshares, 0+nadmin_shares > COUNT
}' "$out" && ret=$? || ret=$?
  if [ -s "$err" ]; then
    grep -H --label "$Name $IP" . < "$err"
    ! [ -s "$count" ] && echo 0 0 > "$count"
  fi >&2
  read NSHARES NADMIN_SHARES < "$count"
  rm -f "$auth" "$out" "$err" "$count"
}

is_firewalled() # {{{1 @EXPORT@
{
  service eztables status | grep -qm1 running
}

set_Username_Password() # In/Out {{{1 @EXPORT@ only part2 only uses this
# In/Out: $Username, $Password
{
  local hidepassword=1 button_showhide IFS=$'\b' ret=2 focus notice
  [ -n "$Username" ] && focus="--focus-field=2"
  [ "$hidepassword" ] &&
    button_showhide="$i18n_p2_Show!$ICON_SHOW!$i18n_p2_Show_ttip" ||
    button_showhide="$i18n_p2_Hide!$ICON_HIDE!$i18n_p2_Hide_ttip"
  is_firewalled && notice="   <b><span bgcolor=\"yellow\">  $i18n_p2_Firewalled  </span></b>"
  while [ 2 = $ret ]; do
    set -- $(yad \
      --on-top --center --text=$(printf "$i18n_p2_Enter_credentials" "$Share" "$Name") \
      --form --separator="$IFS" $focus \
      $notice \
      --field="$i18n_p2_User_name": "$Username" \
      --field="$i18n_p2_Password":${hidepassword:+H} "$Password" \
      --button=gtk-ok:0 --button=gtk-cancel:1 --button="$button_showhide:2" \
      --button="gtk-help:defaultbrowser /usr/share/doc/faqs/smb-browser.html" \
      ; echo "${IFS}$?")
    ret=${@: -1}
    if [ 2 = $ret ]; then [ "$hidepassword" ] && unset hidepassword || hidepassword=1 ; fi
    if [ 0 = $ret -o 2 = $ret ]; then
      Username=$1 Password=$2
    else
      break
    fi
  done
  return $ret
}

prompt_to_set_Username_Password_for() # $1='SHARE'|'NAME' [$2-preamble] [$3-invalid] ; In/Out {{{1
# In: $SHARE $NAME $SEPARATOR $HIDEPASSWORDS
# Out: $Username $Password (unchanged on Cancel) $HIDEPASSWORDS
{
  local key=$1 preamble=$2 invalid=$3 button_share button_showhide IFS ret msg
  case $key in
    NAME ) printf -v msg "$i18n_NAME" "$preamble" "$NAME" ;;
    SHARE) printf -v msg "$i18n_SHARE" "$preamble" "$SHARE" "$NAME"
      button_share=TRUE ;;
  esac
  [ "$HIDEPASSWORDS" ] &&
    button_showhide="$i18n_p2_Show!$APPDIR/icons/show.svg!$i18n_p2_Show_ttip" ||
    button_showhide="$i18n_p2_Hide!$APPDIR/icons/hide.svg!$i18n_p2_Hide_ttip"
  yad_lib_set_YAD_GEOMETRY '' "$YAD_TITLE"
  IFS=$SEPARATOR
  set -- $(yad \
    ${YAD_GEOMETRY_POPUP// /$IFS} --on-top \
    --text="$invalid${invalid:+\r}$msg\r${i18n_credentials_help//\*/  $'\u26AB'}" \
    --form --separator="$IFS" \
    --field="$i18n_p2_User_name": "$Username" \
    --field="$i18n_p2_Password":${HIDEPASSWORDS:+H} "$Password" \
    --button=gtk-ok:0 \
    --button="$button_showhide:4" \
    "${button_share:+--button=$i18n_Skip!gtk-go-forward!$i18n_Skip_ttip $i18n_Go_to_next_share_for_server:1}" \
    --button="$i18n_Jump!gtk-media-forward!$i18n_Jump_ttip:2" \
    --button=gtk-quit:3 \
    --button="gtk-help:defaultbrowser /usr/share/doc/faqs/smb-browser.html" \
    ;echo "${IFS}$?") #yad exit status
  ret=${@: -1} #yad exit status 0(OK) 1(Skip) 2(Jump) 3(Quit)
  # Note: By design 1(Skip) and 2(Jump) do not store Username/Password.
  case $ret in
    0 ) Username="$1" Password="$2" ;;
    4 ) Username="$1" Password="$2" ret=4
      [ "$HIDEPASSWORDS" ] && unset HIDEPASSWORDS || HIDEPASSWORDS=1 ;;
    70|252 ) ret=1 ;; #map timeout and ESC key => skip to the next share
  esac
  return $ret
}

declare -A PASSWORD_STORE #{{{1}}}

try_mount() # [--leave-unmounted] [--no_retry] ; In/Out {{{1
# In: $IP $SHARE $NAME $Username $Password $IOCHARSET $AKEY $DEFAULT_MOUNT_OPTIONS
# Out: $NetworkResource[$AKEY] $PASSWORD_STORE $Username $Password $MountOptions[$AKEY]
{
  local then_unmount no_retry i=0 retval=0 opt_ip
  [[ $* =~ "--leave-unmounted" ]] && leave_unmounted=$((++i))
  [[ $* =~ "--no-retry" ]] && no_retry=$((++i))
  ##shift $i
  MountOptions[$AKEY]="$DEFAULT_MOUNT_OPTIONS$IOCHARSET"
  [ "$SV" = "on" ] && MountOptions[$AKEY]+=",vers=1.0"
  # Experimentally adding ip=$IP enhances ability to connect to open shares.
  # Note that scripts/part2 mounts //$IP/$SHARE.
  [ "guest" = "$Username" ] && opt_ip="ip=$IP"
  try_mount_cmd_and_set_NetworkResource "$opt_ip" 2> "$TMPD"/error
  retval=$?
  rm -f "$TMPD"/error
  [ -n "$leave_unmounted" ] && umount -n "$TMPD"/tmp-mount 2>/dev/null
  if [ 0 = $retval ]; then #store password for possible re-use
    PASSWORD_STORE[$Username]="$Password"
  elif [ -z "$no_retry" ]; then #try stored passwords
    local usave="$Username" psave="$Password"
    for Username in "${!PASSWORD_STORE[@]}"; do
      Password="${PASSWORD_STORE[$Username]}"
      try_mount --no-retry "$@"
      retval=$?
      if [ 0 = $retval ]; then
        return 0
      else
        # This assignment "takes" at the last iteration.
        Username="$usave" Password="$psave"
      fi
    done
  fi
  [ 0 = $retval -a guest = "$Username" ] && MountOptions[$AKEY]="ip=$IP,${MountOptions[$AKEY]}"
  return $retval
}

try_mount_cmd_and_set_NetworkResource() # $1-options In/Out {{{1
# In: $NAME $SHARE $IP $Username $Password $MountOptions $AKEY
# Out: $NetworkResource[$AKEY]
{
  local options=$1 ret UNC
  for UNC in "//$NAME/$SHARE" "//$IP/$SHARE"; do
    USER=$Username PASSWD=$Password mount.cifs "$UNC" "$TMPD"/tmp-mount -o "$options,${MountOptions[$AKEY]}"
    ret=$?
    [ 0 = $ret ] && break
  done
  NetworkResource[$AKEY]="$UNC"
  return $ret
}

printf_VARS() # $1-index $2-field-separator; In {{{1
# In: $VARS arrays.
{
# Note the special handling of $1=0, which allows printing a data-frame
# "record" of VARS, for $1>0, and a list of variables named $VARS, for $1==0
# (recall that bash treats 'VarName[0]' as 'VarName').
  local k=$1 fmt="${2:-$SEPARATOR}%s"
  # When index is zero the caller prints an index value, otherwise we do.
  [ 0 != $k ] && printf "%d" $k
  printf "$fmt" \
    "${Attach[$k]}" \
    "${Error[$k]}" \
    "${NetworkResource[$k]}" \
    "${MountOptions[$k]}" \
    "${ServerName[$k]}" \
    "${FolderName[$k]}" \
    "${ServerIP[$k]}" \
    "${UserName[$k]}" \
    "${UserPassword[$k]}" \
    ;
  printf "\n" # record sep
}
# ---------------------------------------------------------------------
# Reminder: Sync $VARS order with printf_VARS() and i18n_VARS !!! {{{1
# ---------------------------------------------------------------------
# Don't change \t separator.
VARS=$'Attach\tError\tNetworkResource\tMountOptions\tServerName\tFolderName\tServerIP\tUserName\tUserPassword'
declare -a $VARS # vectors of the "mount data" matrix, table columns.
# ---------------------------------------------------------------------

declare -A YADCOLUMNS #{{{1}}}

set_YADCOLUMNS() # In/Out {{{1
# In: $i18n_VARS
# OUT: $YADCOLUMNS
{
  local IFS=$'\t' name
  set -- $i18n_VARS
  unset IFS YADCOLUMNS
  YADCOLUMNS=( "--column=$1:CHK" ) # Attach
  shift
  for name; do
    YADCOLUMNS+=( "--column=$name" )
  done
}

table_VARS() # $1-field-separator ; In/Out {{{1
# stdout: table of VARS
# In: $AKEY, all data-frame arrays.
{
  local k sep="$1"
  k=1; while [ $k -le ${AKEY:-0} ]; do
    printf_VARS $((k++)) "$sep"
  done
}

tr_SEPARATOR_to_x() # $1-x(\t|\n) $2-infile $3-outfile {{{1
# In: $SEPARATOR
# Background: the data flows in this script:
# a. yad --list --separator $SEPARATOR > records
# b. Map(records, tr_separator_to_\n) | yad --list
# c. Map(records, tr_separator_to_\t) > savefile.tsv
# d. savefile.tsv | tr \t $SEPARATOR > records
{
  case $1 in
    $'\n') # $2: IRS=$'\n' IFS=$SEPARATOR; $3: ORS='' OFS=$'\n'
      local -a A
      while IFS="$SEPARATOR" read -r -a A; do
        printf "%s\n" "${A[@]}"
      done < "$2" > "$3" ;;
    $'\t') # $2: IRS=$'\n' IFS=$SEPARATOR; $3: ORS=$'\n' OFS=$'\t'
      tr "$SEPARATOR" "$1" < "$2" > "$3" ;;
    *) return 1 ;;
  esac
}

# Document/work around yad bugs {{{1
# ignore: yad version 0.40.3: --ellipsize=<position> always ellipsizes at the start of a string.
# work-around: yad version < 0.40.3: --confirm-overwrite is buggy.
awk -v V="`yad --version`" 'BEGIN{split(V,a,/[., ]/);exit((a[1]>0||a[2]>40||a[2]==40&&a[3]>=30)?0:1)}' &&
  YAD_CONFIRM_OVERWRITE=--confirm-overwrite || unset YAD_CONFIRM_OVERWRITE

dialog_VARS() # $@-yad-options-override ; In/Out {{{1
# stdin: table of VARS
# In: $AKEY, $SEPARATOR, $YADCOLUMNS, $APPDIR, all data-frame arrays.
# Out: "$TMPD"/vars, $SAVEFILE
{
  local k ret warn1 count=0
  k=1; while [ $k -le ${AKEY:-0} ]; do [ ${Attach[$k]} = FALSE ] && : $((++count)); : $((++k)); done
  if [ $count -gt 0 ]; then
    warn1="$(printf "<span font='sans 28' color='red'>\u26a0</span> $(ngettext "$i18n_unchecked_row" "$i18n_unchecked_rows" $count)" $count) $i18n_A_share_could_not_be_mounted <i>$i18n_Double_click_customize_entry</i>\r"
  fi
  set_YADCOLUMNS
  yad ${YAD_GEOMETRY:- --width=${APPWINSIZE%x*} --height=${APPWINSIZE#*x}} --no-escape --editable --list --scroll \
    --print-all --separator="$SEPARATOR" --ellipsize=MIDDLE \
    --text="<b><big>$i18n_Click_to_accept_default</big>\n$i18n_Dclick_to_edit</b>\n$warn1" \
    --column='':NUM --hide-column=1 "${YADCOLUMNS[@]}" \
    --button=gtk-ok --button=gtk-cancel --button=gtk-save:2 --button="$i18n_Load!gtk-open":4 \
    --button="$i18n_Stats!gtk-info!$i18n_Stats_ttip:bash -c discovery_stats" \
    --button="gtk-help:defaultbrowser /usr/share/doc/faqs/smb-browser.html" \
    "$@" > "$TMPD"/vars
  ret=$?
  if [ 2 = $ret ]; then
    yad ${YAD_GEOMETRY:- --center --width=${APPWINSIZE%x*} --height=${APPWINSIZE#*x}} --file --save $YAD_CONFIRM_OVERWRITE \
      --button=gtk-go-back:1 --button=gtk-ok:0 \
      --filename="${APPDIR%/*}/shares.tsv" \
      --text="$i18n_Save_result\n" > "$TMPD"/savefile
    [ 0 != $? ] && return 21 # save cancelled
    read SAVEFILE < "$TMPD"/savefile
    if [ "$SAVEFILE" ]; then
      # to tab-separated file .tsv
      tr_SEPARATOR_to_x $'\t' "$TMPD"/vars "$SAVEFILE"
      ret=$?
      [ 0 = $ret ] && return 20 # ok
      yad ${YAD_GEOMETRY_POPUP:- --center} --text="$(printf "$i18n_File_save_error" $ret)" \
        --button=gtk-go-back
      return 23
    fi
    ret=23
  elif [ 4 = $ret ]; then
    yad ${YAD_GEOMETRY:- --center --width=${APPWINSIZE%x*} --height=${APPWINSIZE#*x}} --file --filename="${APPDIR%/*}/shares.tsv" \
      --button=gtk-go-back:1 --button=gtk-ok:0 \
      --text="$i18n_Load_result\n" > "$TMPD"/savefile
    [ 0 != $? ] && return 41 # load cancelled
    read SAVEFILE < "$TMPD"/savefile
    if [ "$SAVEFILE" ]; then
      # from tab-separated file .tsv
      tr $'\t' "$SEPARATOR" < "$SAVEFILE" > "$TMPD"/vars
      ret=$?
      [ 0 = $ret ] && return 40 # ok
      yad ${YAD_GEOMETRY_POPUP:- --center} --text="$(printf "$i18n_File_load_error" $ret)" \
        --button=gtk-go-back
      return 43
    fi
    ret=43
  fi
  return $ret
}

options_dialog() # In/Out {{{1
# In: $APPDIR/options
# Out: PT SV CS SN TO SUBNETSALT TIMEOUT
{
  local c subnets
  # Since yad uses TRUE/FALSE we need to convert values from on/off.
  if [ -e "$APPDIR"/options ] &&
    # Temporarily map on/off => TRUE/FALSE for yad's use.
    sed -e 's/on/TRUE/' -e 's/off/FALSE/' "$APPDIR"/options > "$TMPD"/options
  then
    . "$TMPD"/options
  fi
  PT=${PT:-FALSE} SV=${SV:-TRUE} CS=${CS:-FALSE} SN=${SN:-FALSE} TO=${TO:-FALSE}
  [ -z "$TIMEOUT" ] && TIMEOUT="-t 5"
  [ TRUE = "$SN" ] && subnets="$SUBNETSALT"
  [ -z "$subnets" ] && subnets="$(get_subnets)"
  [ -z "$subnets" ] && subnets="<b>$i18n_network_configuration_error</b>"
  YAD_OPTIONS+=" --center --on-top"
  set -- $(yad --text="<b>$i18n_Options</b>" --text-align=center \
    --form --separator=" " \
    --field="<b>$i18n_Shares:</b>":LBL \
    --field="$i18n_Connect_SMB1":CHK \
    --field="$i18n_Use_utf8":CHK \
    --field="<b>$i18n_Scan:</b>":LBL \
    --field="$i18n_Scan_445":CHK \
    --field="$(printf "$i18n_Scan_another_subnet" "$subnets")":CHK \
    --field="$(printf "$i18n_Port_scan_timeout" "${TIMEOUT##* }")":CHK \
    lbl:1 "$SV" "$CS" lbl:4 "$PT" "$SN" "$TO"
  )
  [ "" = "$*" ] && exit # Cancel or ESC
  SV=$1 CS=$2 PT=$3 SN=$4 TO=$5
  while [ TRUE = $SN ]; do
    subnets="$(get_subnets)"
    c=$(yad --form --separator= \
      --field="$i18n_Subnet" "$SUBNETSALT" \
      --text="$(printf "$i18n_Enter_subnet" $subnets)\n" \
    )
    if [ -n "$c" ]; then
      ! [[ $c =~ [0-9]{1,3}"."[0-9]{1,3}"."[0-9]{1,3} ]] && continue
      SUBNETSALT="$c"
    fi
    break
  done
  if [ TRUE = $TO ]; then
    c=$(yad --form --separator= \
      --field="$i18n_New_port_scan_timeout":NUM ${TIMEOUT##* } \
    )
    [ "$c" = 0 -o -z "$c" ] && c=5
    TIMEOUT="-t $c"
  fi
  printf '%s\n' "PT=$PT" "SV=$SV" "CS=$CS" "SN=$SN" "TO=$TO" \
    "SUBNETSALT=\"$SUBNETSALT\"" "TIMEOUT=\"$TIMEOUT\"" \
    > "$TMPD"/options &&
    # map yad's TRUE/FALSE to options file's on/off
    sed -e 's/TRUE/on/' -e 's/FALSE/off/' "$TMPD"/options > "$APPDIR"/options
}

open_progress_dialog() # $1-message {{{1
{
  yad_lib_set_YAD_GEOMETRY '' "$YAD_TITLE" 96:1
  coproc DLG_PROGRESS {
    yad --text="$1" --text-align=center \
      --undecorated $YAD_GEOMETRY_POPUP --on-top \
      --progress --pulsate --auto-close \
      --button=gtk-cancel:1 ||
    kill -USR1 $$ # trapped by scan_subnets
  }
}

close_progress_dialog() # {{{1
{
  [ -z "$DLG_PROGRESS_PID" ] && return
  eval "exec ${DLG_PROGRESS[1]}>&-"
  kill $DLG_PROGRESS_PID; wait $DLG_PROGRESS_PID
  true
}

open_preview_dialog() # $1-message [$2@-yad-options] {{{1
# Live preview the list of scanned shares.
{
  local message=$1; shift
  coproc DLG_PREVIEW {
    dialog_VARS "$@" --no-buttons --text="<b><big>$message</big></b>" >/dev/null
  }
  sleep 0.5 # keep for callers of yad_lib_set_YAD_GEOMETRY
}

close_preview_dialog() # {{{1
{
  [ -z "$DLG_PREVIEW_PID" ] && return
  # stash YAD_GEOMETRY{,_POPUP} for other dialogs
  yad_lib_set_YAD_GEOMETRY '' "$YAD_TITLE"
  eval "exec ${DLG_PREVIEW[1]}>&-"
  local yad=" $(ps -ho ppid,ppid,pid | grep -m1 " $DLG_PREVIEW_PID ")"
  [ "$yad" ] && kill ${yad##* } # yad = pid whose ppid is DLG_PREVIEW_PID
  kill $DLG_PREVIEW_PID; wait $DLG_PREVIEW_PID
  true
}

exit_no_subnets() # {{{1 @EXPORT@
{
  yad --center --on-top --button=gtk-ok --timeout=8 --text="$i18n_export_You_are_not_connected" || true
  exit
}

discovery_stats() # [--tsv] [$1-button] ; In {{{1
# In: "$TMPD"/{servers,server-ips,nshares,shares-<ip>}
{
  local opt_tsv
  [ "$1" = --tsv ] && opt_tsv=$'\t' && shift
  local -a IP; readarray -t IP < "$TMPD"/server-ips
  local button=$1 nserver=${#IP[@]} opt_list paths_shares_by_ip
  printf -v paths_shares_by_ip " $TMPD/shares-%s" "${IP[@]}"
  paste -s -d '|' $paths_shares_by_ip > "$TMPD"/shares-by-ip
  paste -d "${opt_tsv:-$'\n'}" "$TMPD"/{servers,server-ips,nshares,shares-by-ip} > "$TMPD"/stats
  [ "$opt_tsv" ] && return
  yad ${YAD_GEOMETRY_POPUP:- --center} --image=gtk-info --text="<b>$i18n_Discovery_Stats</b>\r$i18n_Discovery_note\r" \
    --text-align=center --button="${button:-gtk-ok}" \
    --column="$i18n_Server" --column="$i18n_IP" --column="$i18n_SharesNum" \
    --column=tt:HD --tooltip-column=-4 --list --no-selection \
    < "$TMPD"/stats || true
}
export -f discovery_stats
export i18n_Discovery_Stats i18n_Discovery_note i18n_Server i18n_IP i18n_SharesNum

# Main {{{1

MNTDIR="${APPDIR%/*}"
# APPDIR start-rox {{{2
[[ $PARAMS == *start-rox* ]] && ( sleep 1; defaultrox "$MNTDIR" ) &
# APPDIR options {{{2
[ -e "$APPDIR"/options ] && . "$APPDIR"/options
if [[ $PARAMS == *options* ]]; then options_dialog; exit $?; fi
[ -z "$TIMEOUT" ] && TIMEOUT="-t 5"
[ "$PT" = on ] && PORT=445 || PORT=139
[ "$CS" = on ] && IOCHARSET=",iocharset=utf8" || IOCHARSET=""
[ "$SN" = on ] && SUBNETS="$SUBNETSALT" || SUBNETS="$(get_subnets)"

[ "$SUBNETS" = "" ] && exit_no_subnets

mkdir -p "$TMPD"/tmp-mount

#scan for servers, hacked mpscan to time out faster. {{{2
SCANSPEED=1
is_firewalled && notice="   <b><span bgcolor=\"yellow\">  $i18n_p2_Firewalled  </span></b>" || notice=
# coprocess DLG_PREVIEW        ; coprocess DLG_PROGRESS
open_preview_dialog "$i18n_Scanning_Network" 2>/dev/null
open_progress_dialog "$(a=($SUBNETS); ngettext "$i18n_Scanning_Network" "$i18n_Scanning_Networks" ${#a[*]})$notice" 2>/dev/null
echo "#$SUBNETS" >&${DLG_PROGRESS[1]} # start pulsating progress
scan_subnets "$SUBNETS" > "$TMPD"/server-ips
close_progress_dialog

#scan servers for shares {{{2
# "$TMPD"/server₋ips lists servers in $IP order.
if [ -s "$TMPD"/server-ips ]; then
  SEQ=0
  close_preview_dialog
  open_preview_dialog "$i18n_Gathering_Authentications" --no-escape
  while read IP; do
    # Note that Samba on linux with user level security rejects Username="guest" and causes problems with smbclient-3.6.12
    NAME=$(get_server_name $IP); [ -z "$NAME" ] && NAME=$IP
    echo "${NAME#$IP}" >> "$TMPD"/servers  # "$TMPD"/servers lists servers by $NAME, "" if NAME unknown
    : > "$TMPD"/shares-$IP # for discovery_stats
    # Test for open access server (anonymous login)
    $SMBCLIENT -g -N -L $IP > "$TMPD"/error 2>&1 || true
    if grep -qFm1 failed "$TMPD"/error; then
      # Not open access; request credentials then.
      Username=guest Password=
      ret=4; while [ $ret = 4 ]; do # while show/hide passwords
        prompt_to_set_Username_Password_for NAME "<span color='red'>$(< "$TMPD"/error)</span>\n\n" && ret=$? || ret=$?
      done
      case $ret in
        1|2 ) echo 0 >> "$TMPD"/nshares; continue ;; #to the next server
        3 ) exit ;;
      esac
    fi
    get_share_names > "$TMPD"/shares # "$TMPD"/shares lists verbatim non-admin $SHAREs per server $IP a.k.a $NAME
    echo $NSHARES >> "$TMPD"/nshares # "$TMPD"/nshares counts server's $SHAREs
    cp "$TMPD"/shares{,-$IP} # for discovery_stats
    ! [ -s  "$TMPD"/shares ] && continue  # no visible shared folders found for this server

    # SEQ is a sequence index
    # AKEY is the key (index in this case) for bash arrays
    # NAME is the server name
    # SHARE is the verbatim share name for mount.cifs
    # ROXAPP is the RoxApp folder name for SHARE

    while read SHARE; do
      # Test SHARE for valid mount, and aggregate from VARS + print the AKEYth row to the main window DLG_PREVIEW.

      AKEY=$((++SEQ))
      ROXAPP="${SHARE//[\!@#$%^&*()]/}"
      unset invalid

      [ "$Username" = "" ] && Username="guest"
      ServerName[$AKEY]=$NAME ServerIP[$AKEY]=$IP FolderName[$AKEY]=$ROXAPP Error[$AKEY]=0
      while : "SHARE isn't mounted"; do
        UserName[$AKEY]="$Username" UserPassword[$AKEY]="$Password"
        try_mount --leave-unmounted && Error[$AKEY]=$? || Error[$AKEY]=$?
        if [ 0 = ${Error[$AKEY]} ]; then # we have valid credentials
          Attach[$AKEY]=TRUE  # don't change "TRUE"
          break 1 #to the next share for the current server
        else
          Attach[$AKEY]=FALSE # don't change "FALSE"
          ret=4; while [ $ret = 4 ]; do # while show/hide passwords
            prompt_to_set_Username_Password_for SHARE '' "$invalid" && ret=$? || ret=$?
            invalid="<span color=\"red\">$i18n_invalid_credentials</span>"
          done
          case $ret in
            0) if ! [ "$Password" ]; then
                # guest access or ask me later for my password
                Attach[$AKEY]=TRUE
                UserName[$AKEY]="$Username" UserPassword[$AKEY]="$Password"
                break 1
              fi ;;
            1) break 1 ;; #to the next share for the current server
            2) break 2 ;; #to the next server
            3) exit ;;
          esac
        fi
      done #trying to mount the current SHARE
      # Append input form data to the preview dialog, which could be
      # closed without negatively impacting this loop.
      [ "$DLG_PREVIEW_PID" ] && printf_VARS $AKEY "\n" >&${DLG_PREVIEW[1]}
    done < "$TMPD"/shares
  done < "$TMPD"/server-ips
  close_preview_dialog
fi

# Edit/Save/Load "mount data" records. {{{2
# This block can be commented out and still RoxApps will be created.
table_VARS $'\n' > "$TMPD"/table-vars
while [ -s "$TMPD"/table-vars ]; do
  unset update show_final_dialog
  dialog_VARS < "$TMPD"/table-vars && ret=$? || ret=$?
  case $ret in # 2x(Save) 4x(Load) x: 0(success) 1(cancel) 3(error)
    0) : OK;   show_final_dialog=1; break ;;
   2?) : Save; update=1 ;;
    3) : Back; update=1 ;;
   43) : Load error ;;
   4?) : Load ok/cancel; update=1 ;;
    *) : Cancel/unexpected; exit $ret ;;
  esac
  [ $update ] && tr_SEPARATOR_to_x $'\n' "$TMPD"/vars "$TMPD"/table-vars
done

# Create the Mount RoxApps {{{2
# . The created AppRun and AppInfo.xml import some variables/functions from this script.
# . AppRun's shebang must be /bin/bash - like this script's shebang.
# . UI messages are translated into the system language active when this script runs.
# . The Mount RoxApp is self-contained and can be moved elsewhere.
# . The loop reads the records that dialog_VARS wrote to "$TMPD"/vars, rather than
#   reading $VARS directly, because the user can change yad rows via "right-click edit".
# . The loop abandons creating the current RoxApp if an error occurs; see the ERR trap.

unset $VARS # clear arrays
#DEBUG while IFS="$SEPARATOR" read -r k $VARS; do printf "%d" $k; printf_VARS 0; done >&2 < ""$TMPD"/vars"; exit

: > "$TMPD"/error
trap 'echo "$NetworkResource" >> "$TMPD"/error; continue' ERR
declare -i count_started=0 count_created=0
if [ -s "$TMPD"/vars ]; then
  while IFS="$SEPARATOR" read -r k $VARS; do
    ! [ TRUE = $Attach ] && continue # SHARE could not be mounted/is not checked
    : $((++count_started))
    # ----- Mount variables for scripts/part2 -----{{{
    SHARE="${NetworkResource##*/}" # strip off server name (or server IP address)
    SHARE2="$FolderName" # which was assigned $ROXAPP
    NAME="$ServerName"   # normally a hostname; an IP if smblookup didn't return a name
    Username="$UserName"
    Password="$UserPassword"
    ClearText=''
    IP="$ServerIP"
    # scripts/part2 should never add mount options of its own. Mount options
    # can be added in $DEFAULT_MOUNT_OPTIONS, and the user can edit yad list
    # cells for specific shares.
    MountOptions=$MountOptions # guest includes ip=<IP addr>
    # ---------------------------------------------}}}

    SHAREAPPDIR="${SHARE2// /_}" #get rid of spaces,replace with underscore $APPDIR only!
    SHAREAPPDIR=${SHAREAPPDIR//\'/} #get rid of apostrophes if there
    mkdir -p "$MNTDIR/$NAME--$SHAREAPPDIR/$HIDE_MOUNTNAME$MOUNTNAME"
    SHAREAPPDIR="$MNTDIR/$NAME--$SHAREAPPDIR" # shorthand
    add_resources "$SHAREAPPDIR"
    #-------------------------------------------------------
    exec 3>&1 1>"$SHAREAPPDIR"/AppRun # save+redirect stdout
    #-------------------------------------------------------{{{
    # AppRun imports functions and variables from this script.
    echo '#!/bin/bash'
    # Import marked functions.
    imports=$(sed -n -e 's/^\([a-zA-Z_0-9]\+\).*@EXP''ORT@.*$/\1/p' "$APPDIR"/AppRun)
    for x in $imports; do printf \#; type $x; done
    cat "$APPDIR"/scripts/part1
    [ "$Username" = guest -o -z "$Password" ] && passwd=$Password || passwd=$(printf %s "$Password"|base64)
    [ "$SN" = on ] && salt=$SUBNETSALT || unset salt
    echo "
. ./.config
MOUNTNAME='$HIDE_MOUNTNAME$MOUNTNAME'
ICON_MOUNTED='$HIDE_RES$RES/$ICON_MOUNTED'
ICON_UNMOUNTED='$HIDE_RES$RES/$ICON_UNMOUNTED'
ICON_SHOW='$HIDE_RES$RES/$ICON_SHOW'
ICON_HIDE='$HIDE_RES$RES/$ICON_HIDE'
YAD_TITLE='$YAD_TITLE'"
    set | grep '^i18n_\(p2\|export\)_'
    echo
    cat $APPDIR/scripts/part2
    #-------------------------------------------------------}}}
    exec 1>"$SHAREAPPDIR"/.config
    #-------------------------------------------------------{{{
    echo "
#begin share
Server='$NAME'
Share='$SHARE'
MountOptions='$MountOptions'
Username='$Username'
Password='$passwd'
ClearText='$ClearText'
#end share

#begin scan
PORT='$PORT'
SUBNETSALT='$salt'
TIMEOUT='$TIMEOUT'
#end scan"
    #-------------------------------------------------------}}}
    exec 1>&3 # restore stdout
    #-------------------------------------------------------
    printf -v x "$i18n_UMOUNT" "${SHAREAPPDIR#$MNTDIR/}" &&
    sed -e "s|@SUMMARY@|$i18n_SUMMARY|" -e "s|@UMOUNT@|$x|" -e "s|@EDITCONF@|$i18n_EDITCONF|" \
      $APPDIR/scripts/AppInfo.xml.tpl > "$SHAREAPPDIR"/AppInfo.xml &&
    chmod uga+x "$SHAREAPPDIR"/AppRun
    : TODO chmod -R 755 "$SHAREAPPDIR"
    echo 'exec "$(dirname "$0")"/AppRun' > "$SHAREAPPDIR/$i18n_mount_it"
    chmod +x "$SHAREAPPDIR/$i18n_mount_it"
    echo 'exec "$(dirname "$0")"/AppRun unmount' > "$SHAREAPPDIR/$i18n_unmount_it"
    chmod +x "$SHAREAPPDIR/$i18n_unmount_it"
    : $((++count_created))
  done < "$TMPD"/vars
fi
trap - ERR

# Show final feedback {{{2
trap handle_fatal ERR
if ! [ -s "$TMPD"/server-ips ]; then
  yad ${YAD_GEOMETRY_POPUP:- --center} --image=gtk-info --text="$i18n_found_no_servers" --button=gtk-quit || true
elif [ 0 = $(awk '{n+=$0}END{print 0+n}' "$TMPD"/nshares) ]; then
  yad --center --image=gtk-info --text="$i18n_found_no_shares" \
    --button="$i18n_Stats!gtk-info!$i18n_Stats_ttip:bash -c discovery_stats" \
    --button=gtk-quit || true
elif [ $count_started -gt 0 -o "$show_final_dialog" ]; then
  # Display (warning) dialog with save results button.
  keepfile="$HOME/${MOUNTNAME#${HIDE_MOUNTNAME}}-$(date +%F-%H-%M).tsv"
  save_results() {
    tr_SEPARATOR_to_x $'\t' "$TMPD"/vars "$keepfile" &&
    if [ -s "$TMPD"/error ]; then cp "$TMPD"/error "${keepfile%.tsv}"-errors.txt; fi &&
    discovery_stats --tsv &&
    cp "$TMPD"/stats "${keepfile%.tsv}"-stats.tsv &&
    kill -USR1 $YAD_PID ||
    yad --center --text="$(printf "$i18n_File_save_error" $?)" --button=gtk-ok || true
  }
  export -f save_results tr_SEPARATOR_to_x
  export SEPARATOR i18n_File_save_error keepfile

  printf -v ask_save_file "$i18n_final_ask_save_file" "$keepfile"
  printf -v disk_icon_created "$(ngettext "$i18n_disk_icon_created" "$i18n_disk_icons_created" $count_created)" $count_created
  [ $count_created -gt 0 ] && usage=" $i18n_final_usage" || unset usage
  count_incomplete=$((count_started - count_created))
  printf -v warning "$(ngettext "$i18n_disk_icon_incomplete" "$i18n_disk_icons_incomplete" $count_incomplete)" $count_incomplete
  if [ $count_incomplete = 0 ]; then
    button=gtk-ok warning=
  else
    button=gtk-dialog-warning warning="\r\r<span color='red'>$warning</span>"
  fi
  yad --center \
    --image="$button" \
    --text="$disk_icon_created$usage$warning\r\r$ask_save_file\r" \
    --button="gtk-save:bash -c save_results" \
    --button="$i18n_Stats!gtk-info!$i18n_Stats_ttip:bash -c discovery_stats" \
    --button=gtk-quit || true
fi

