#! /bin/bash
#
# Johannes Meixner <jsmeix@suse.de>, 2004, 2005, 2006, 2007, 2008

#set -x

export PATH="/sbin:/usr/sbin:/usr/bin:/bin"
export LC_ALL="POSIX"
export LANG="POSIX"
umask 022

# When the package "sane" is installed, the HAL_GLOBAL_FILE is created
# which lists all known USB scanners.
# Up to openSUSE 11.0 it is /etc/hal/fdi/policy/10osvendor/70-scanner.fdi
# since openSUSE 11.1 it is /usr/share/hal/fdi/information/20thirdparty/70-scanner.fdi
# (see https://bugzilla.novell.com/show_bug.cgi?id=438867):
HAL_GLOBAL_FILE="/dev/null"
if test -r /usr/share/hal/fdi/information/20thirdparty/70-scanner.fdi
then HAL_GLOBAL_FILE="/usr/share/hal/fdi/information/20thirdparty/70-scanner.fdi"
else if test -r /etc/hal/fdi/policy/10osvendor/70-scanner.fdi
     then HAL_GLOBAL_FILE="/etc/hal/fdi/policy/10osvendor/70-scanner.fdi"
     fi
fi
# The HAL_LOCAL_FILE is written by this script for those scanners
# which are not listed in HAL_GLOBAL_FILE.
# Up to openSUSE 11.0 it was /etc/hal/fdi/policy/10osvendor/80-scanner.fdi
# since openSUSE 11.1 it is /etc/hal/fdi/information/20thirdparty/80-scanner.fdi
# (see https://bugzilla.novell.com/show_bug.cgi?id=438867).
# Because /etc/hal/fdi/information/20thirdparty/80-scanner.fdi
# should also work for openSUSE 11.0 it is used here in any case.
# The directory /etc/hal/fdi/information/20thirdparty/ is not provided by the hal RPM
# so that it is created with the usual default permissions "drwxr-xr-x  root root"
# as the directories in /etc/hal/fdi/ which are already provided by the hal RPM:
HAL_LOCAL_FILE="/dev/null"
if test -d /etc/hal/fdi/information/20thirdparty/
then HAL_LOCAL_FILE="/etc/hal/fdi/information/20thirdparty/80-scanner.fdi"
else if mkdir -m 755 -p /etc/hal/fdi/information/20thirdparty
     then HAL_LOCAL_FILE="/etc/hal/fdi/information/20thirdparty/80-scanner.fdi"
     fi
fi

# Create temporary file names:
MY_NAME=${0##*/}
TMP_DATA=$(mktemp -u /tmp/$MY_NAME.XXXXXX)
TMP_DATA_RAW=$(mktemp -u /tmp/$MY_NAME.XXXXXX)

# Get the raw data (i.e. the SANE devices for the active scanners).
# Either it is specified as command line parameter $1
# (this is how YaST calls it to avoid duplicate time-consuming "scanimage" calls
# because it knows the SANE devices from its determine_active_scanners call
# note that YaST calls it with all SANE devices just like "scanimage -f '%d;'")
# or (if $1 is empty) then run "scanimage" to determine the SANE devices:
if [ -n "$1" ]
then echo "$1" >$TMP_DATA_RAW
else MAXIMUM_WAIT="60"
     if [ -x $( type -p scanimage || echo qqq ) ]
     then scanimage -f '%d;' >$TMP_DATA_RAW &
          scanimagePID=$!
          for i in $( seq $MAXIMUM_WAIT )
          do ps $scanimagePID &>/dev/null || break
             sleep 1
          done
          if ps $scanimagePID &>/dev/null
          then kill -9 $scanimagePID &>/dev/null
          fi
     else echo "Cannot execute /usr/bin/scanimage" 1>&2
          exit 2
     fi
fi
# Split it into seperated lines, remove the SANE backend name if it is a usual SANE backend,
# substitute the 'hpaio:/usb/' backend name with ':hpaio:/usb/' to keep this backend name
# because it is needed later to match the SANE device from the 'hp-makeuri -s' command,
# and remove duplicates which happen when different backends are active for the same scanner:
tr ';' '\n' <$TMP_DATA_RAW | sed -e 's/^hpaio:\/usb\//:hpaio:\/usb\//' | cut -d ':' -s -f 2- | sort -u >$TMP_DATA
# TMP_DATA should contain lines with plain device infos for scanners like
#   libusb:001:002
#   libusb:004:005
#   hpaio:/usb/HP_LaserJet_1220?serial=00XXXXXXXXXX
#   /dev/sg0
#   /dev/sg2
# and random useless stuff e.g. when the net backend is used on localhost
# or for network scanners via the 'hpaio:/net/...' backend like
#   localhost:hpaio:/usb/HP_LaserJet_1220?serial=00XXXXXXXXXX
#   localhost:plustek:libusb:001:005
#   /net/Officejet_7200_series?ip=10.10.100.100
# A detailed syntax check is not done here because it happens later,
# see "Test for USB and SCSI scanners and ignore anything else" below.

# For each 'hpaio:/usb/<model-string>' line output a matching 'libusb:<bus>:<device>' line:
# List all HP USB devices, extract the bus and device number,
# use 'hp-makeuri -s' to make the SANE device (ignore error messages on stderr
# which happen for plain HP printers like 'error: Device does not support scan.'
# or may happen for whatever else USB devices like 'error: Device not found'),
# and if the SANE device from 'hp-makeuri -s' is found in TMP_DATA,
# append a matching 'libusb:<bus>:<device>' line to TMP_DATA.
# Duplicate 'libusb:<bus>:<device>' lines could happen regardless that
# the BusDevice values are unique (because the lsusb output is unique)
# because another backend might have already reported a HP USB scanner.
# Skip this section if hp-makeuri cannot be executed for whatever reason.
if [ -x $( type -p hp-makeuri || echo qqq ) ]
then for BusDevice in $( lsusb -d 03f0: | cut -d ' ' -f 2,4 | tr ' :' ': ' )
     do for URI in $( hp-makeuri -s $BusDevice 2>/dev/null )
        do if grep -q "^$URI\$" $TMP_DATA
           then grep -q "^libusb:$BusDevice\$" $TMP_DATA || echo libusb:$BusDevice >>$TMP_DATA
           fi
        done
     done
else echo "Cannot execute /usr/bin/hp-makeuri" 1>&2
fi

# Write header to HAL_LOCAL_FILE
# and exit silently if this fails for whatever reason
# (e.g. no hal package installed):
[ "$HAL_LOCAL_FILE" = "/dev/null" ] && exit 0
cat /dev/null >$HAL_LOCAL_FILE || exit 0
echo '<?xml version="1.0" encoding="ISO-8859-1"?>' >>$HAL_LOCAL_FILE
echo '<deviceinfo version="0.2">' >>$HAL_LOCAL_FILE
echo '  <device>' >>$HAL_LOCAL_FILE
echo '' >>$HAL_LOCAL_FILE
echo '<!-- To grant access to USB scanners create entries like:' >>$HAL_LOCAL_FILE
echo '    <match key="info.subsystem" string="usb">' >>$HAL_LOCAL_FILE
echo '      <match key="usb.vendor_id" int="0x1a2b">' >>$HAL_LOCAL_FILE
echo '        <match key="usb.product_id" int="0x3c4d">' >>$HAL_LOCAL_FILE
echo '          <append key="info.capabilities" type="strlist">scanner</append>' >>$HAL_LOCAL_FILE
echo '        </match>' >>$HAL_LOCAL_FILE
echo '      </match>' >>$HAL_LOCAL_FILE
echo '    </match>' >>$HAL_LOCAL_FILE
echo '"1a2b" and "3c4d" stand for the USB device IDs' >>$HAL_LOCAL_FILE
echo 'as displayed by the command /usr/sbin/lsusb' >>$HAL_LOCAL_FILE
echo 'Up to openSUSE 11.0 replace "usb" with "usb_device" i.e. use' >>$HAL_LOCAL_FILE
echo 'string="usb_device", usb_device.vendor_id, usb_device.product_id' >>$HAL_LOCAL_FILE
echo 'Since openSUSE 11.1 "usb_device" is replaced by "usb"' >>$HAL_LOCAL_FILE
echo 'see https://bugzilla.novell.com/show_bug.cgi?id=438867 -->' >>$HAL_LOCAL_FILE
echo '' >>$HAL_LOCAL_FILE

# Setting extglob is required to remove an arbitrary number
# of leading zeroes from a varaiable via ${varaiable##*(0)}
shopt -s extglob

# Get the HAL UDIs:
KNOWN_HAL_SCANNER_UDIs=""
if [ -x $( type -p hal-find-by-capability || echo qqq ) ]
then KNOWN_HAL_SCANNER_UDIs="$( hal-find-by-capability --capability scanner )"
fi
UNKNOWN_TO_HAL=""

# Test and set scanner access permissions:
exec <$TMP_DATA
while read LINE
do # Test for USB and SCSI scanners and ignore anything else:
   if echo $LINE | grep -q '^libusb:[0-9][0-9]*:[0-9][0-9]*$'
   then # It is a USB scanner:
        # Determine the USB vendor and product IDs:
        BUS_DEVICE=$( echo $LINE | cut -d ':' -s -f 2- )
        VENDOR_PRODUCT=$( lsusb -s $BUS_DEVICE | tr '[:upper:]' '[:lower:]' | grep -o 'id [0-9a-f]*:[0-9a-f]*' | cut -b 4- )
        VENDOR=$( echo $VENDOR_PRODUCT | cut -d ':' -s -f 1 )
        PRODUCT=$( echo $VENDOR_PRODUCT | cut -d ':' -s -f 2 )
        # Skip scanners where VENDOR or PRODUCT contains only zeroes:
        echo $VENDOR | grep -q [1-9a-f] || continue
        echo $PRODUCT | grep -q [1-9a-f] || continue
        # Skip "SCSI via USB" scanners:
        # There is a small number of USB scanners which are not talked to via libusb.
        # They talk an unusual protocol which is a derivative of usb storage.
        # User space should treat them as SCSI scanners.
        # Furthermore, granting access via libusb here could oops the kernel.
        # Therefore USB user access for those scanners must not be enabled.
        # See the Novell/Suse Bugzilla bug
        # https://bugzilla.novell.com/show_bug.cgi?id=341565
        [ "04ce:0300" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:0094" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:0099" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:009a" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:00a0" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:00a3" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:80a3" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:80ac" = "$VENDOR_PRODUCT" ] && continue
        [ "05da:00b6" = "$VENDOR_PRODUCT" ] && continue
        # Scanners which are not skipped above must have an entry in a HAL fdi file.
        # If the "scanner" capability is already known to HAL, everything is already o.k
        # regardless if it is listed in the global or local HAL file or elsewhere.
        # If there are several scanners with the same USB IDs connected,
        # it is assumed that when HAL knows one, it knows all of them.
        # If KNOWN_HAL_SCANNER_UDIs is emptly e.g. because HAL does not run or is not installed
        # it is perfectly right to collect the models in UNKNOWN_TO_HAL
        # so that YaST can show an appropriate message to the user.
        HAL_UDI_VENDOR_PRODUCT=${VENDOR##*(0)}_${PRODUCT##*(0)}
        echo "$KNOWN_HAL_SCANNER_UDIs" | grep -q -i "/usb_device_$HAL_UDI_VENDOR_PRODUCT" && continue
        # The "scanner" capability is not yet known to HAL:
        UNKNOWN_TO_HAL="$UNKNOWN_TO_HAL USB-ID(hex)=$VENDOR_PRODUCT"
        # Skip scanners which are already listed in the global HAL file:
        # Up to openSUSE 11.0 the keys were usb_device.vendor_id and usb_device.product_id in HAL_GLOBAL_FILE
        # since openSUSE 11.1 the keys are usb.vendor_id and usb.product_id in HAL_GLOBAL_FILE:
        grep -i -A 1 "usb.vendor_id\" int=\"0x$VENDOR\"" $HAL_GLOBAL_FILE | grep -q -i "usb.product_id\" int=\"0x$PRODUCT\"" && continue
        grep -i -A 1 "usb_device.vendor_id\" int=\"0x$VENDOR\"" $HAL_GLOBAL_FILE | grep -q -i "usb_device.product_id\" int=\"0x$PRODUCT\"" && continue
        # Write an entry to the local HAL file:
        echo '    <match key="info.subsystem" string="usb">' >>$HAL_LOCAL_FILE
        echo "      <match key=\"usb.vendor_id\" int=\"0x$VENDOR\">" >>$HAL_LOCAL_FILE
        echo "        <match key=\"usb.product_id\" int=\"0x$PRODUCT\">" >>$HAL_LOCAL_FILE
        echo '          <append key="info.capabilities" type="strlist">scanner</append>' >>$HAL_LOCAL_FILE
        echo '        </match>' >>$HAL_LOCAL_FILE
        echo '      </match>' >>$HAL_LOCAL_FILE
        echo '    </match>' >>$HAL_LOCAL_FILE
        echo '' >>$HAL_LOCAL_FILE
   fi
   if echo $LINE | grep -q '^/dev/sg[0-9][0-9]*$'
   then # It is a SCSI scanner:
        # If the "scanner" capability is already known to HAL, everything is already o.k
        # regardless if it is listed in the global or local HAL file or elsewhere.
        # If KNOWN_HAL_SCANNER_UDIs is emptly e.g. because HAL does not run or is not installed
        # it is perfectly right to collect the models in UNKNOWN_TO_HAL
        # so that YaST can show an appropriate message to the user.
        for u in $( echo "$KNOWN_HAL_SCANNER_UDIs" )
        do hal-get-property --udi "$u" --key linux.device_file
        done | grep -q "$LINE" && continue
        # The "scanner" capability is not yet known to HAL:
        UNKNOWN_TO_HAL="$UNKNOWN_TO_HAL SCSI-device=$LINE"
        # Skip scanners which show up as SCSI type "scanner" because those
        # match to the generic SCSI entry in the global HAL file.
        LSSCSI_LINE=$( lsscsi -g | grep $LINE )
        echo $LSSCSI_LINE | tr -s ' ' | cut -d ' ' -f2 | grep -q -i 'scanner' && continue
        # There are two know manufacturers where SCSI scanners do not show up as "scanner":
        # "HP" and "EPSON": Some (all?) of their SCSI scanners show up as as "processor"
        # (because those devices do not support the SCSI scanner protocol
        # but only some kind of generic SCSI protocol):
        # Test for the particular manufacturer:
        if echo $LSSCSI_LINE | tr -s ' ' | cut -d ' ' -f3 | grep -q -i 'HP'
        then # Write a HP entry to the local HAL file:
             echo '    <match key="info.category" string="scsi_generic">' >>$HAL_LOCAL_FILE
             echo '      <match key="@info.parent:scsi.type" string="processor">' >>$HAL_LOCAL_FILE
             echo '        <match key="@info.parent:scsi.vendor" string="HP">' >>$HAL_LOCAL_FILE
             echo '          <append key="info.capabilities" type="strlist">scanner</append>' >>$HAL_LOCAL_FILE
             echo '        </match>' >>$HAL_LOCAL_FILE
             echo '      </match>' >>$HAL_LOCAL_FILE
             echo '    </match>' >>$HAL_LOCAL_FILE
             echo '' >>$HAL_LOCAL_FILE
        fi
        if echo $LSSCSI_LINE | tr -s ' ' | cut -d ' ' -f3 | grep -q -i 'EPSON'
        then # Write an EPSON entry to the local HAL file:
             echo '    <match key="info.category" string="scsi_generic">' >>$HAL_LOCAL_FILE
             echo '      <match key="@info.parent:scsi.type" string="processor">' >>$HAL_LOCAL_FILE
             echo '        <match key="@info.parent:scsi.vendor" string="EPSON">' >>$HAL_LOCAL_FILE
             echo '          <append key="info.capabilities" type="strlist">scanner</append>' >>$HAL_LOCAL_FILE
             echo '        </match>' >>$HAL_LOCAL_FILE
             echo '      </match>' >>$HAL_LOCAL_FILE
             echo '    </match>' >>$HAL_LOCAL_FILE
             echo '' >>$HAL_LOCAL_FILE
        fi
   fi
done

# Write footer to HAL_LOCAL_FILE:
echo '  </device>' >>$HAL_LOCAL_FILE
echo '</deviceinfo>' >>$HAL_LOCAL_FILE

# Remove the temporary files:
rm $TMP_DATA $TMP_DATA_RAW

# If the scanner is not yet known to HAL, a re-plug of the USB scanner might help
# otherwise a reboot of the computer should be done to restart the whole udev/HAL machinery
# and if even a reboot doesn't help, the saned+net workaround should be used.
# The exit code 10 indicates this case so that YaST can show an appropriate message to the user.
if [ -n "$UNKNOWN_TO_HAL" ]
then echo "The following models are currently not known to HAL:" 1>&2
     for m in $UNKNOWN_TO_HAL
     do echo "  $m" 1>&2
     done
     echo "" 1>&2
     echo "To access a scanner as normal user," 1>&2
     echo "udev and HAL are needed to grant" 1>&2
     echo "appropriate access permissions automatically." 1>&2
     echo "(Up to openSUSE 11.0 hal-resmgr is also needed.)" 1>&2
     echo "Therefore the scanner model must be known to HAL." 1>&2
     echo "If the scanner is not known to HAL, a re-plug" 1>&2
     echo "of a USB scanner should help." 1>&2
     echo "Otherwise a reboot should be done to restart" 1>&2
     echo "the whole udev and HAL machinery." 1>&2
     echo "Check if the scanner is listed in the 'lshal' output." 1>&2
     echo "If a SCSI scanner which was switched on during boot" 1>&2
     echo "is not listed in the 'lshal' output, the usual reason" 1>&2
     echo "is that the kernel module for the scanner's SCSI host" 1>&2
     echo "adapter does not notify HAL about the scanner device." 1>&2
     echo "If even a reboot does not help, you could access" 1>&2
     echo "the scanner via the 'saned' as a workaround." 1>&2
     echo "For this workaround choose 'scanning via network'" 1>&2
     echo "and select the 'local host configuration'." 1>&2
     exit 10
fi

exit 0

