#!/bin/bash
#Scan for shares and make Rox apps for mount points. Public domain.
# merged with 01micko's update for slacko 20121028 (0.5)
# step's 201711 update of AppRun only:
#   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.
VERSION=1.0.1
PARAMS="$@"
APPDIR=`dirname "$0"` #find where we are, not $HOME.
cd "$APPDIR"
APPDIR="$PWD"
export TEXTDOMAIN=fatdog
export OUTPUT_CHARSET=UTF-8
#. gettext.sh

APPTITLE="$(gettext "SMB Browser")"
[ $(id -u) -ne 0 ] && exec gtksu "$(gettext "Scan for SMB Shares")" "$0" "$@"
HIDEPASSWORDS=${HIDEPASSWORDS:-1}
SEPARATOR=$'\b' # don't choose default $IFS characters
TMP="/tmp/sambabrowser-"
rm -rf ${TMP}*

handler() {
  trap - HUP INT QUIT TERM ABRT 0
  close_progress_dialog >/dev/null 2>&1
  rm -fr "${TMP:----}"*
}
trap handler HUP INT QUIT TERM ABRT 0

build_icons() { # $1-path-to-appdir
	cp  $APPDIR/icons/*  "$1" &&
	cat > "$1/mounted.svg" << 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="$1/drive48.png" width="48" height="48" id="image1"/>
<circle cx="40" cy="8" r="8" style="fill:url(#RG1);" id="ellipse1"/>
<image xlink:href="$1/smb_overlay.png" width="48" height="48" id="image2"/>
</svg>
EOF
}

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

scan_subnets() #$1-list-of-subnets ; Out
# Out: ${TMP}subnets
{
  printf '%s\n' $1 > ${TMP}subnets
  # ${TMP}subnets lists subnets
  # Perform parallel scans.
  declare -a words=( $1 )
  < ${TMP}subnets xargs -P${#words[@]} -I{} mpscan $TIMEOUT -p $PORT {}.1 - {}.254 |
  cut -d " " -f 2
}

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

get_share_names() # In
# In: $Username $Password 
{
  smbclient -U $Username%$Password -g -L $IP |
  awk -F\| '/Disk/ { print $2 }'
}

# TRANSLATORS: If possible use the same accelerator key for toggle button S_how/_Hide
declare -A MSG
MSG=( \
[NAME]="$(gettext "%sThe server named \"%s\" seems to need a password. Do you want to enter one?")" \
[SHARE]="$(gettext "%sThe share named \"%s\" for server \"%s\" seems to need a password. Do you want to enter one?")" \
[0]="slot" \
[1]="$(gettext "_User name")" \
[2]="$(gettext "_Password")" \
[3]="$(gettext "_Next Server")" \
[4]="$(gettext "Skip all remaining shares for this server, and go to the next server.")" \
[5]="$(gettext "_Skip Share")" \
[6]="$(gettext "Skip this share, and go to the next share for this server.")" \
[7]="$(gettext "S_how")" \
[8]="$(gettext "Show passwords.")" \
[9]="$(gettext "_Hide")" \
[10]="$(gettext "Hide passwords.")" \
)

prompt_to_set_Username_Password_for() # $1='SHARE'|'NAME' [$2-preamble] ; In/Out
# In: $SHARE $NAME $MSG $SEPARATOR $HIDEPASSWORDS
# Out: $Username $Password (unchanged on Cancel) $MSG[0] $HIDEPASSWORDS
{
  local key=$1 preamble=$2 button_share button_showhide choice IFS ret
  case $key in 
    NAME ) printf -v MSG[0] "${MSG[$key]}" "$preamble" "$NAME" ;;
    SHARE) printf -v MSG[0] "${MSG[$key]}" "$preamble" "$SHARE" "$NAME"
      button_share=TRUE ;;
  esac
  [ "$HIDEPASSWORDS" ] &&
    button_showhide="${MSG[7]}!$APPDIR/icons/show.svg!${MSG[8]}" ||
    button_showhide="${MSG[9]}!$APPDIR/icons/hide.svg!${MSG[10]}"
  choice="$(yad \
    --center --on-top --text="${MSG[0]}" \
    --form --separator="$SEPARATOR" \
    --field="${MSG[1]}": "$Username" \
    --field="${MSG[2]}":${HIDEPASSWORDS:+H} "$Password" \
    --buttons-layout=center \
    --button=gtk-ok:0 \
      --button="$button_showhide:4" \
      "${button_share:+--button=${MSG[5]}!gtk-no!${MSG[6]}:1}" \
      --button="${MSG[3]}!gtk-no!${MSG[4]}:2" \
      --button=gtk-quit:3 \
      ;echo "${SEPARATOR}$?")" #yad exit status
  IFS="$SEPARATOR"
  set -- $choice
  ret=${@: -1} #yad exit status 0(OK) 1(Share) 2(Server) 3(Quit)
  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

try_mount() # [--leave-unmounted] [--no_retry] ; In/Out
# In: $UNC $Username $Password $IOCHARSET $AKEY
# Out: $PASSWORD_STORE $Username $Password $MountOptions[$AKEY]
{
  local then_unmount no_retry cmd i=0 retval=0 opt_ip
  [[ $* =~ "--leave-unmounted" ]] && leave_unmounted=$((++i))
  [[ $* =~ "--no-retry" ]] && no_retry=$((++i))
  ##shift $i
  #sec=ntlm removed for 3.9 onwards
  MountOptions[$AKEY]="noserverino$IOCHARSET"
  [ "$SV" = "on" ] && MountOptions[$AKEY]+=",vers=1.0"
  # Experimentally adding ip=$IP enhances ability to connect to open shares.
  [ "guest" = "$Username" ] && opt_ip="ip=$IP,"
  cmd="mount.cifs \"$UNC\" /tmp/sambabrowser-tmp-mount -o \"username=$Username,password=$Password,${opt_ip}${MountOptions[$AKEY]}\""
  echo "$cmd" > /tmp/trymount
  . /tmp/trymount
  retval=$?
  rm /tmp/trymount
  [ -n "$leave_unmounted" ] && umount -n /tmp/sambabrowser-tmp-mount
  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
}

# ---------------------------------------------------------------------
# $VARS order matters in printf_VARS() - remember to keep coordinated!
# ---------------------------------------------------------------------
# Don't change \t separator.
VARS="MountQ\tNetworkResource\tMountOptions\tServerName\tUserName\tUserPassword\tFolderName\tServerIP"
# Yad column titles:
# i18n: Logically we perform VARSi18n="$(gettext "$VARS")" but translation tools need literals.
VARSi18n="$(gettext "MountQ\tNetworkResource\tMountOptions\tServerName\tUserName\tUserPassword\tFolderName\tServerIP")"
VARS="${VARS//\\t/$'\t'}" VARSi18n="${VARSi18n//\\t/$'\t'}"
declare -a $VARS # all combined they make a "data frame" record of VARS
# ---------------------------------------------------------------------

printf_VARS() # $1-index $2-field-separator; In
# 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" \
    "${MountQ[$k]}" \
    "${NetworkResource[$k]}" \
    "${MountOptions[$k]}" \
    "${ServerName[$k]}" \
    "${UserName[$k]}" \
    "${UserPassword[$k]}" \
    "${FolderName[$k]}" \
    "${ServerIP[$k]}" \
    ;
  printf "\n" # record sep
}

declare -A YADCOLUMNS

set_YADCOLUMNS() # In/Out.
# In: $VARSi18n
# OUT: $YADCOLUMNS
{
  local IFS=$'\t' name
  set -- $VARSi18n
  unset IFS YADCOLUMNS
  YADCOLUMNS=( "--column=$1:CHK" ) # MountQ
  shift
  for name; do
    YADCOLUMNS=( "${YADCOLUMNS[@]}" "--column=$name" )
  done
}

edit_VARS() # $1-field-separator ; In/Out
# In: $AKEY, $SEPARATOR, $YADCOLUMNS, all data-frame arrays.
# Out: ${TMP}vars, $SAVEFILE
{
  [ 0 = ${AKEY:-0} ] && return
  local k ret sep="$1" warn1 count=0
  k=1; while [ $k -le $AKEY ]; do [ ${MountQ[$k]} = FALSE ] && : $((++count)); : $((++k)); done
  if [ $count -gt 0 ]; then
    warn1="$(printf "<span font='sans 28' color='red'>\u26a0</span> $(ngettext "%d row isn't checked." "%d rows aren't checked." $count)" $count) $(gettext "A share could not be mounted therefore the share folder won't be created. You can check a row to force folder creation but the folder could be empty when you open it.")\n"
  fi
  set_YADCOLUMNS
  k=1; while [ $k -le $AKEY ]; do
    printf_VARS $k "$sep"
    : $((++k))
  done | # TODO encrypt ??
  yad --geometry 700x400+0+0 --editable --list --hide-column=1 --scroll \
    --print-all --separator="$SEPARATOR" \
    --text "<b><big>$(gettext 'Click OK to accept the defaults.')</big>\n$(gettext 'Double-click a cell to edit. Right-click for more options.')</b>\n$warn1" \
    --column='':NUM "${YADCOLUMNS[@]}" \
    --button=gtk-ok --button=gtk-save:2 --button="$(gettext "_Load")!gtk-open":4 \
    > ${TMP}vars
  ret=$? 
  if [ 2 = $ret ]; then
    SAVEFILE="$(yad --center --file --save --confirm-overwrite \
      --filename="$APPDIR/shares.tsv" \
      --text "$(gettext "Save scan results to file.")\n")"
    if [ "$SAVEFILE" ]; then
      # to tab-separated file .tsv
      tr "$SEPARATOR" $'\t' < ${TMP}vars > "$SAVEFILE"
      ret=$?
      [ 0 != $ret ] && yad --center --text "$(gettext "Error saving results.")"
    fi
  elif [ 4 = $ret ]; then
    SAVEFILE="$(yad --center --file filename="$APPDIR/shares.tsv" \
      --text "$(gettext "Load scan results from file.")\n")"
    if [ "$SAVEFILE" ]; then
      # from tab-separated file .tsv
      tr $'\t' "$SEPARATOR" < "$SAVEFILE" > ${TMP}vars
      ret=$?
      [ 0 != $ret ] && yad --center --text "$(gettext "Error loading results.")"
    fi
  fi
  return $ret
}

options_dialog() # In/Out
# In: $APPDIR/options
# Out: PT SV CS SN TO SUBNETSALT TIMEOUT
{
  local choice c subnets timeout
  # Since yad uses TRUE/FALSE we need to convert values from on/off.
  if [ -e "$APPDIR"/options ] &&
    sed -e 's/on/TRUE/' -e 's/off/FALSE/' "$APPDIR"/options > ${TMP}options
  then
      . ${TMP}options
  fi
  [ -z "$TIMEOUT" ] && TIMEOUT="-t 5"
  [ TRUE = "$SN" ] && subnets="$SUBNETSALT"
  [ -z "$subnets" ] && subnets="$(get_subnets)"
  [ -z "$subnets" ] && subnets="*$(gettext "network configuration error")*"
  timeout=${TIMEOUT##* }
  YAD_OPTIONS+=" --center --on-top"
  choice="$(yad --text="$(gettext "Options"):" \
    --form --separator=" " \
    --field="$(gettext "Scan on port 445 instead of port 139.")":CHK $PT \
    --field="$(gettext "Connect with SMB version 1 by default.")":CHK $SV \
    --field="$(gettext "Use the utf-8 character set.")":CHK $CS \
    --field="$(gettext "Specify a different subnet to scan,\ncurrent value:") ${subnets:-}":CHK $SN \
    --field="$(gettext "Specify the timeout for the port scan,\ncurrent value:") $timeout":CHK FALSE \
  )"
  [ -z "$choice" ] && exit # Cancel or ESC
  set -- $choice
  PT=$1 SV=$2 CS=$3 SN=$4 TO=$5
  while [ TRUE = $SN ]; do
    subnets="$(get_subnets)"
    c=$(yad --form --separator= \
      --field="$(gettext "Subnet:")" \
      --text="$(gettext "Enter a subnet to scan, just the first 3 octets.\nFor example the local subnet is:") $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="$(gettext "New timeout for the port scan:")":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=FALSE" \
    "SUBNETSALT=\"$SUBNETSALT\"" "TIMEOUT=\"$TIMEOUT\"" \
    > ${TMP}options &&
    sed -e 's/TRUE/on/' -e 's/FALSE/off/' ${TMP}options > "$APPDIR"/options
}

open_progress_dialog() # [$1-message]
{
  local message="${1:-$(gettext "Scanning Network...")}"
  yad --text "$message" --center --on-top --splash \
    --progress --pulsate --auto-close \
    --button="gtk-cancel:kill $$" # invokes trap handler
}

close_progress_dialog() {
  # assumes coproc open_progress_dialog
  [ -n "$COPROC_PID" ] && kill $COPROC_PID
}

export YAD_OPTIONS="$YAD_OPTIONS --title='$APPTITLE' --window-icon='$APPDIR/.DirIcon' --buttons-layout=center --borders=4"

#APPDIR options
[ -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)"
if [ "$SUBNETS" = "" ] ; then
  yad --center --on-top --button=gtk-ok --timeout=8 --text="$(gettext "It doesn't seem like you are connected to your network.\nYou need to be on a network to share files.")"
  exit
fi

[ ! -d  /tmp/sambabrowser-tmp-mount ] && mkdir -p /tmp/sambabrowser-tmp-mount
MNTDIR="${APPDIR%/*}"

#scan for servers, hacked mpscan to time out faster.
SCANSPEED=1
coproc open_progress_dialog
echo '#' >&${COPROC[1]} # start pulsating progress
scan_subnets "$SUBNETS" > ${TMP}server-ips
# ${TMP}server₋ips lists servers in $IP order

#scan servers for shares
SEQ=0
if ! [ -s ${TMP}server-ips ]; then
  close_progress_dialog
  yad --center --on-top --button=gtk-ok --timeout=8 --text="$(gettext "No shares found on: ")$SUBNETS"
else
while read IP; do
  unset NAME Username Password
  smbclient -g -N -L $IP | grep -F failed > ${TMP}error 2>&1
  if [ -s ${TMP}error ]; then
  # Note that Samba on linux with user level security rejects Username="guest" and causes problems with smbclient-3.6.12 

    NAME="$(get_server_name)"; [ -z "$NAME" ] && NAME=$IP
    ret=4; while [ $ret = 4 ]; do # while show/hide passwords
      prompt_to_set_Username_Password_for NAME "<span color='red'>$(< ${TMP}error)</span>\n\n"
      ret=$?
    done
    case $ret in
      1|2 ) continue ;; #to the next server
      3 ) exit ;;
    esac
  fi
#[ "$Username" = "" ] && Username="guest" # added by 01micko but not working for jamesbond
  SHARENAMES="$(get_share_names)"
  [ "$SHARENAMES" = "" ] && continue  #no visible shared folders
  # Filter automatic Windows administrative shares https://en.wikipedia.org/wiki/Administrative_share.
  echo "$SHARENAMES" | grep -v '[A-Za-z][$]$' > ${TMP}shares
  # ${TMP}shares lists verbatim $SHARENAMES per server $NAME.

  # 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
  # UNC is the network resource, see mount.cifs(8)

  while read SHARE; do
    # Test share for valid mount and create a "data frame record".

    SEQ=$((SEQ +1))
    AKEY=$SEQ
    NAME="$(get_server_name)"; [ -z "$NAME" ] && NAME=$IP
    UNC="//$NAME/$SHARE" # alternative //$IP/$SHARE
    ROXAPP="${SHARE//[\!@#$%^&*()]/}"

    [ "$Username" = "" ] && Username="guest"                     
    NetworkResource[$AKEY]="$UNC" ServerName[$AKEY]="$NAME" ServerIP[$AKEY]=$IP FolderName["$AKEY"]="$ROXAPP"
    while : "SHARE isn't mounted"; do
      UserName[$AKEY]="$Username" UserPassword[$AKEY]="$Password"
      if try_mount --leave-unmounted; then #we have valid credentials
        MountQ[$AKEY]=TRUE  # don't change "TRUE"
        break 1 #to the next share for the current server
      else
        MountQ[$AKEY]=FALSE # don't change "FALSE"
        ret=4; while [ $ret = 4 ]; do # while show/hide passwords
        prompt_to_set_Username_Password_for SHARE
        ret=$?
        done
        case $ret in
          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
  done < ${TMP}shares
done < ${TMP}server-ips
close_progress_dialog
fi

# Edit/Save/Load "data frame" records.
# edit_VARS can be commented out below and still RoxApps will be created.
edit_VARS "\n" ; ret=$?; [ 0 != $ret ] && exit $ret

# Since the user could have tweaked some values in the yad dialog,
# now we iterate over yad output records.
unset $VARS # clear arrays
#DEBUG while IFS="$SEPARATOR" read -r k $VARS; do printf "%d" $k; printf_VARS 0; done >&2 < "${TMP}vars"; exit

# Create RoxApps.
[ -s ${TMP}vars ] && while IFS="$SEPARATOR" read -r k $VARS; do
  # This AppRun doesn't create a RoxApp if the test mount didn't succeed.
  # (Depart from legacy behavior).
  ! [ TRUE = $MountQ ] && continue

# Set legacy interface variables for scripts/part* ---------------------
  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"
  IP="$ServerIP"
  # scripts/part* also use: MountOptions
  MountOptions=${MountOptions#ip=*,} # but are unaware of option ip=
# ----------------------------------------------------------------------

# Legacy.
#mount shares for each server.
SHAREAPPDIR="${SHARE2// /_}" #get rid of spaces,replace with underscore $APPDIR only!
SHAREAPPDIR=${SHAREAPPDIR//\'/} #get rid of apostrophes if there
mkdir -p $MNTDIR/$NAME"--""$SHAREAPPDIR"/mnt-point || continue
SHAREAPPDIR="$MNTDIR/$NAME--$SHAREAPPDIR" # shorthand

build_icons "$SHAREAPPDIR" &&
ln -sfT drive48.png "$SHAREAPPDIR"/.DirIcon &&
{
cat "$APPDIR"/scripts/part1
echo "Name=$NAME
Share=\"$SHARE\"
MountOptions=\"$MountOptions\"
Username=\"$Username\""
if [ "$Password" = "guest" -o -z "$Password" ]; then
  echo "Password=\"$Password\""
else #encrypt
  echo "Password=\"$(printf "$Password"|base64)\""
fi
echo "IOCHARSET=$IOCHARSET
PORT=$PORT"
[ "$SN" = "on" ] && echo "SUBNETSALT=$SUBNETSALT"
cat $APPDIR/scripts/part2
} > "$SHAREAPPDIR"/AppRun &&
cp $APPDIR/scripts/AppInfo.xml "$SHAREAPPDIR"/ &&
chmod -R 755 "$SHAREAPPDIR"

done < ${TMP}vars
