#!/bin/bash

# Author:     darren chamberlain <dlc@sevenroot.org>
# Co-Author:  Paul Bournival <paulb-ns@cajun.nu>
# Co-Author:  Mikhail Novosyolov <mikhailnov@dumalogiya.ru>
#

# bashlib is used by sourcing it at the beginning of scripts that
# needs its functionality (by using the . or source commands).
#
# The library targets bash: it relies on bash-only features such as
# ${!var} indirection, printf -v, ${!PREFIX@} listings, ${var//pat/rep}
# substitution and read -d. All hot paths are fork-free (builtins only):
# param()/cookie() used to spawn env|grep|sed|cut on every call, and the
# URL decoding used to fork once per %XX escape.

#
# Set version number
# Must be an integer because bash cannot compare float numbers
#
VERSION="4"

# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Initialization stuff begins here. These things run immediately, and
# do the parameter/cookie parsing.
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

# Characters removed by safe_param(), as one glob character class.
# The class is built by concatenation because a literal ' cannot
# appear inside a '...' string: '[$`<>"%;)(&+' + "'" + ']'.
SAFE_STRIP='[$`<>"%;)(&+'"'"']'

# capture stdin for POST methods. POST requests don't always come in
# with a newline attached, so read everything up to EOF (the empty -d
# delimiter means NUL, which never occurs in CGI input). A plain
# $(cat) would fork; read is a builtin.
STDIN=
IFS= read -r -d '' STDIN || true
if [ -n "${STDIN}" ]; then
  QUERY_STRING="${STDIN}&${QUERY_STRING}"
fi

# Handle GET and POST requests... (the QUERY_STRING will be set)
if [ -n "${QUERY_STRING}" ]; then
  # name=value params, separated by either '&' or ';'
  case ${QUERY_STRING} in
  *=*)
    # '&' and ';' become spaces, so the unquoted expansion below splits
    # QUERY_STRING into one word per parameter:
    #   "a=1&b=2;c=3" -> "a=1 b=2 c=3" -> words "a=1" "b=2" "c=3"
    for Q in ${QUERY_STRING//[;&]/ } ; do
      #
      # Clear our local variables
      #
      unset name
      value=

      #
      # Decode the name of the key so that it can never break the eval
      # below: turn %XX into \xHH for printf, decode '+' as a space,
      # then strip '.', '-', '$' and '`'.
      #
      # cut at the first '=':     "user%2Ename=admin" -> "user%2Ename"
      name=${Q%%=*}
      # every % becomes \x:       "user%2Ename"       -> "user\x2Ename"
      name=${name//%/\\x}
      # '+' is an encoded space:  "a+b+c"             -> "a b c"
      name=${name//+/ }
      # delete '.' and '-' (literal ones only; encoded %2E/%2D survive
      # here and decode back below): "user.name-x"    -> "usernamex"
      name=${name//[.-]/}
      # decode \xHH hex:          "user\x2Enamex"     -> "user.namex"
      printf -v name '%b' "${name}"
      # delete every '$'
      name=${name//$/}
      # delete every '`'
      name=${name//\`/}

      #
      # Decode the value: turn %XX into \xHH and let a single printf
      # do the whole job. printf reads at most two hex digits per
      # \xHH, so no separators between escapes are needed.
      #
      # cut at the first '=':     "q=hello%21" -> "hello%21"
      value=${Q#*=}
      # decode %XX:               "hello%21"   -> "hello!"
      printf -v value '%b' "${value//%/\\x}"

      eval "export FORM_${name}='${value}'"
    done
    ;;
  *) # keywords: foo.cgi?a+b+c
    # "alpha+beta+gamma" -> "alpha beta gamma"
    eval "export KEYWORDS='${QUERY_STRING//+/ }'"
    ;;
  esac
fi

#
# this section works identically to the query string parsing code,
# with the (obvious) exception that variables are stuck into the
# environment with the prefix COOKIE_ rather than FORM_. This is to
# help distinguish them from the other variables that get set
# automatically.
#
if [ -n "${HTTP_COOKIE}" ]; then
  for Q in ${HTTP_COOKIE}; do
    #
    # Clear our local variables
    #
    name=
    value=

    #
    # drop one trailing ';': "session=abc123;" -> "session=abc123"
    #
    Q=${Q%;}

    #
    # Decode the name of the key; see the parameter section above.
    #
    # cut at the first '=':    "session=abc123" -> "session"
    name=${Q%%=*}
    # every % becomes \x:      "a%73b"          -> "a\x73b"
    name=${name//%/\\x}
    # '+' is an encoded space: "a+b"            -> "a b"
    name=${name//+/ }
    # delete '.' and '-':      "user.name"      -> "username"
    name=${name//[.-]/}
    # decode \xHH hex:         "a\x73b"         -> "asb"
    printf -v name '%b' "${name}"

    #
    # Decode the cookie value; see the parameter section above.
    #
    # cut at the first '=':    "q=a%20b" -> "a%20b"
    value=${Q#*=}
    # decode %XX:              "a%20b"   -> "a b"
    printf -v value '%b' "${value//%/\\x}"

    #
    # Export COOKIE_${name} into the environment
    #
    eval "export COOKIE_${name}='${value}'"
  done
fi

# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# functions and all that groovy stuff
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
#
# Shameless plug, advertises verion.
version() {
  echo "bashlib, version ${VERSION}"
}

version_html() {
  echo -n "<a href=\"http://sevenroot.org/dlc/2000/12/bashlib\">bashlib</a>,"
  echo "version ${VERSION}"
}

#
# Internal helper: store the raw stored value of parameter $1 in REPLY.
# Anything that is not a valid variable name yields an empty value.
#
_param_value() {
  # "foo" and "FORM_foo" both give "FORM_foo"
  local name="FORM_${1#FORM_}"
  case ${name} in
    # empty name or not a valid variable name -> empty value
    FORM_|FORM_*[!A-Za-z0-9_]*) REPLY= ;;
    # ${!name} is the value of the variable whose name is in $name
    *) REPLY="${!name}" ;;
  esac
}

#
# Parameter function.
# * When called with no arguments, returns a list of parameters that
#   were passed in.
# * When called with one argument, returns the value of that parameter
#   (if any)
# * When called with more than one argument, assumes that the first is a
#   paramter name and the rest are values to be assigned to a paramter of
#   that name.
#
param() {
  local name value
  if [ $# -eq 1 ]; then
    _param_value "$1"
    # '+' is an encoded space: "a+b" -> "a b"
    value="${REPLY//+/ }"
  elif [ $# -gt 1 ]; then
    name=$1
    shift
    eval "export 'FORM_${name}=$*'"
    return
  else
    # ${!FORM_@} expands to every existing variable named FORM_*
    for name in ${!FORM_@}; do
      # "FORM_foo" -> "foo"
      echo "${name#FORM_}"
    done
    return
  fi
  echo "${value}"
  unset name
  unset value
}

# shell invocation and X-site scripting prevention
safe_param() {
  local value
  if [ $# -eq 1 ]; then
    _param_value "$1"
    # '+' is an encoded space: "a+b" -> "a b"
    value="${REPLY//+/ }"
    # delete the SAFE_STRIP characters: 'a$b"c' -> 'abc'
    echo "${value//${SAFE_STRIP}/}"
  else
    param "$@"
  fi
}

# cookie function. Same explanation as param
cookie() {
  local name value
  if [ $# -eq 1 ]; then
    # "foo" and "COOKIE_foo" both give "COOKIE_foo"
    name="COOKIE_${1#COOKIE_}"
    case ${name} in
      # empty name or not a valid variable name -> empty value
      COOKIE_|COOKIE_*[!A-Za-z0-9_]*) value= ;;
      # ${!name} is the value of the variable whose name is in $name
      *) value="${!name}" ;;
    esac
  elif [ $# -gt 1 ]; then
    name=$1
    shift
    eval "export 'COOKIE_${name}=$*'"
    return
  else
    # ${!COOKIE_@} expands to every existing variable named COOKIE_*
    for name in ${!COOKIE_@}; do
      # "COOKIE_foo" -> "foo"
      echo "${name#COOKIE_}"
    done
    return
  fi
  echo "${value}"
  unset name
  unset value
}

# keywords returns a list of keywords. This is only set when the script is
# called with an ISINDEX form (these are pretty rare nowadays).
keywords() {
  echo "${KEYWORDS}"
}

set_cookie() {
  local name=$1
  shift
  local value=$*
  # "" -> "; a=1" -> "; a=1; b=2"
  bashlib_cookies="${bashlib_cookies}; ${name}=${value}"

  # drop the leading ';' only (the space after it stays): "; a=1" -> " a=1"
  bashlib_cookies=${bashlib_cookies#;}

  cookie "$name" "$value"
}

#
# send_redirect takes a URI and redirects the browser to that uri, exiting
# the script along the way.
#
send_redirect() {
  local uri
  if [ $# -eq 1 ]; then
    uri=$1
  else
    uri="http://${SERVER_NAME}/${SCRIPT_NAME}"
  fi
  echo "Location: ${uri}"
  echo ""
}
