#!/bin/bash

# TODO: the english version wasn't tested in practice yet.

# Note on patching:
# It isn't recommended to edit files from a downloaded module or Drupal core,
# but you don't have a choice for .htaccess etc. The patching system in this
# script was created to automatize the process.
# When the script extracts an archive, it searches the patches/ directory for
# *executable* files named packagename-* or all-* and issues commands to run
# them with a single argument -- the path to the directory from the archive.
# The executable can be anything, but if you want to use a real patch made
# with 'diff', prepend the patch with this:
# #!/bin/sh
# exec patch -d "$1" -p0 <"$0"
# 'patch' ignores any leading garbage, so it won't mind. -d chdirs to the
# package's directory. (When using -p1, you may want to use -d "$1/..".)
# Scripts named all-* run for every package. The argument can be used to
# discern the package's name.

usage () {
  echo "$0 - friendly Drupal site upgrader

Usage:
export DBUSER=user DBNAME=database
  optional - give correct database connection info to mysqldump
$0 <package> <package> ...
  find out the commands needed to upgrade the packages. for each command,
  ask whether to run it. this ensures maximum transparency.
  a <package> is either a filename or it's in packages/<package>-*.
  with no arguments, use everything in packages/. if there's nothing, run
  $0 --download first.
$0 <package> <package> ... <&-
  when stdin is closed, don't ask to run any command.
$0 <package> <package> ... >file.sh
  when stdout isn't a tty, don't ask to run any command, and don't
  colorize the output.
$0 --download
  use Drupal to find out what can be upgraded, and download it to packages/"
  exit 1
}

note () {
  echo "$blue$1$none"
}

cmd () {
  echo "$red$1$none"
  if [ "$havein" ] && [ "$ttyout" ]; then
    if [ "$ttyin" ]; then
      read -p "${white}Run? (y/n) [$default]$none " answer
      [ -z "$answer" ] && answer=$default
    else
      # don't show a prompt when input is from a pipe (e.g. from yes)
      read answer
    fi
    case "$answer" in
      y*|Y*)
        default=y
        if ! eval "$1"; then
          echo "${red}Command ended with error status!$none
When you resolve the error and fix any damage, run this again and answer
all commands before this one with 'n' to skip them."
          exit 1
        fi
        ;;
      *)
        default=n
        ;;
    esac
  fi
}

# override mysqldump with our own function
mysqldump () {
  if [ "$*" == "-u user -p database" ]; then
    # that's probably not what the user wanted
    echo "# database credentials are in sites/$SITEID/settings.php." >&2
    read -p "Database: " DBNAME
    read -p "User: " DBUSER
    command mysqldump -u $DBUSER -p $DBNAME
  else
    command mysqldump "$@"
  fi
}

# we don't have any options except --download
if [ "$#" == 1 ] && [ "$1" == --download ]; then
  download=yes
else
  download=
  for arg; do if [[ $arg == -* ]]; then usage; fi; done
fi

# the last used answer for cmd
default=n

# are we connected to a tty?
[ -e /dev/stdin ] && havein=yes || havein=
[ -e /dev/stdin ] && [ -t 0 ] && ttyin=yes || ttyin=
[ -t 1 ] && ttyout=yes || ttyout=
blue=${ttyout:+$'\e[1;36m'}
red=${ttyout:+$'\e[1;31m'}
white=${ttyout:+$'\e[37m'}
none=${ttyout:+$'\e[0m'}

# as multiple sites can run from a single Drupal installation, find out
# which one to upgrade.
[ ! -d sites/all ] && echo "Run this from Drupal's directory!" && exit 1
if [ ! -n "$SITEID" ]; then
  # if there's exactly one directory in sites/ (besides all/), choose it.
  tmp=( `find sites/ -mindepth 1 -maxdepth 1 ! -name all` )
  [ ${#tmp[@]} == 1 ] && SITEID=${tmp[0]##*/}
fi
if [ ! -n "$SITEID" ]; then
  echo "Which site in sites/ do you want to upgrade?" >&2
  ls -1 -I all sites/ >&2
  echo "Choose one and do: export SITEID=sitename" >&2
  exit
fi

UPGRADES=upgrade-$SITEID-`date +%m%d`

if [ "$download" ]; then
  set -e
  echo "${blue}Downloading package updates...$none" >&2
  # we want to do this without user intervention, so we can't rely on being
  # the admin. we'll use PHP CLI, and cleverly set HTTP_HOST and SCRIPT_NAME
  # to make sure sites/$SITEID will be used. (see also conf_path() sources.)
  code='
    // $_SERVER["HTTP_HOST"] is unset
    $_SERVER["SCRIPT_NAME"]     = "'"${SITEID//./\/}"'/";
    $_SERVER["SERVER_SOFTWARE"] = "PHP CLI";
    $_SERVER["REQUEST_METHOD"]  = "GET";

    require_once "includes/bootstrap.inc";
    drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

    function get_availability_data() {
      module_load_include("inc", "update", "update.report");
      if($available = update_get_available(TRUE)) {
        $data = update_calculate_project_data($available);
        foreach($data as $project) {
          if($project["status"] == UPDATE_NOT_SECURE ||
              $project["status"] == UPDATE_REVOKED ||
              $project["status"] == UPDATE_NOT_SUPPORTED ||
              $project["status"] == UPDATE_NOT_CURRENT) {
            if (isset($project["recommended"])) {
              print $project["releases"][$project["recommended"]]["download_link"]."\n";
            }
          }
        }
      }
    }

    get_availability_data();'
  if ! urls=`php -r "$code"`; then
    mkdir -p packages
    echo "Error checking for available updates."
    echo "Download them into packages/ by hand."
    exit
  fi
  if [ -n "$urls" ]; then
    mkdir -p packages
    cd packages
    for url in $urls; do
      echo "$url" >&2
      curl -# -O "$url" >&2
    done
  fi
  exit
fi

# list of archive files to install.
targets=()
if [ $# == 0 ]; then
  targets=( packages/* )
  if [ ! -f "${targets[0]}" ]; then
    # there's nothing in packages/, run --download
    $BASH $0 --download || exit
    targets=( packages/* )
    if [ ! -f "${targets[0]}" ]; then
      echo "No package to upgrade, ending."
      exit
    fi
  fi
else
  for arg; do
    # convert package name to file name in packages/
    if [[ $arg != */* ]]; then
      tmp=( packages/$package-[0-9]* )
      if [ "${#tmp[@]}" == 1 ] && [ -f "${tmp[0]}" ]; then
        arg=${tmp[0]}
      else
        echo "Can't find package $arg in packages/."
        exit
      fi
    fi
    targets+=( $arg )
  done
fi

note "
# please read UPGRADE.txt. (it's only two pages long.) this script can
# automate a lot of tasks, but the file has a lot of useful information,
# particularly for major upgrades or if something goes wrong."

if [ -e "$UPGRADES" ]; then
  note "
# it may be useful to remove the temporary directory and start from scratch."
  cmd "rm -rf $UPGRADES"
fi

note "
# create a temporary directory for extracting archives.
# when installing a package, remove the old version and replace it with the
# new one immediately (without extraction overhead). this makes sure the files
# are missing for just a little while. this isn't a perfect deployment method,
# but it's good enough for our purposes.
# use chmod 700, because it'll contain the complete backup later."
cmd "mkdir $UPGRADES"
cmd "chmod 700 $UPGRADES"

dirs=()
for archivefile in "${targets[@]}"; do
  note ""
  archivebasename=${archivefile##*/}
  package=${archivebasename%%-[0-9].x-*}
  dir=$package
  # an archive named cck-6.x-1.0.beta2.tar.gz should contain cck/.
  if [[ $archivebasename == drupal-[0-9]* ]]; then
    # a core archive named drupal-6.8.tar.gz should contain drupal-6.13/.
    dir=${archivebasename%%.[^0-9]*}
    package=drupal
  fi
  if [[ $archivebasename == drupal-7.* ]]; then
    note "# What, Drupal 7 is out? Don't you want to leave the upgrade for later (remove
# it from packages/)? If not, good luck and remember, RTFM. (Read drupal.org
# and Drupal 7's UPGRADE.txt.)"
  fi
  cmd "tar xf $archivefile -C $UPGRADES --no-same-owner"
  for file in ./patches/{all,$package}-*[^~]; do
    [ -x "$file" ] && cmd "$file $UPGRADES/$dir"
  done
  dirs+=($dir)
done

note "
# backup everything.
# restore: mysql -u user -p database <$UPGRADES/backup.sql"
cmd "mysqldump -u ${DBUSER:-user} -p ${DBNAME:-database} >$UPGRADES/backup.sql"
note "# restore: remove everything except $UPGRADES and unpack this.
# (unpack it as root, to keep file ownership intact.)
# (we don't compress anything, because compressing files/ may be slow.)"
cmd "tar cf $UPGRADES/backup.tar \`ls -A -I $UPGRADES -I packages\`"

note "
# we're ready to install.
# ATTENTION: it's recommended to place the site in Off-line mode. You'll find
# it in the Administration section under Site maintenance."

for dir in ${dirs[@]}; do
  note ""
  if [[ $dir == drupal-* ]]; then
    note "# installing core.
# we don't have to remove .php files, mv will overwrite them anyway. but
# remove all directories, to get rid of removed files (mv would keep them).
# sites/ contains everything that's not part of core - don't touch it."
    cmd "rm -rf includes misc modules profiles scripts themes &&
  mv $UPGRADES/$dir/{.htaccess,[^.s]*,scripts} ."
  else
    # is it a module? is it a theme? where is it installed? let's find out.
    founddest=()
    for search in sites/{all,$SITEID}/{modules,themes}/$dir; do
      [ -e "$search" ] && founddest+=($search)
    done
    # choose the default...
    if [ "${#founddest[@]}" == 0 ]; then
      note "# ATTENTION! something's wrong! the previous location of $dir couldn't be found.
# this command will place it in sites/all/modules/$dir/, but it's suspicious.
# if it wasn't installed there before, why put it there now? (if you placed it
# in packages/ specifically so that it gets installed, not updated, it's OK.)"
      founddest=(sites/all/modules/$dir)
    fi
    if [ "${#founddest[@]}" != 1 ]; then
      note "# ATTENTION! something's wrong! the previous location of $dir couldn't be found.
# $dir is already present in these locations:"
      for f in "${founddest[@]}"; do note "# $f"; done
      note "# this command chooses the first location, but that's just a guess."
    fi
    cmd "rm -rf ${founddest[0]} && mv $UPGRADES/$dir ${founddest[0]}"
  fi
done

note "
# now, visit http://example.com/update.php (with your site's address).
# you must be logged in as user #1. if that's not possible, temporarily
# edit sites/$SITEID/settings.php and set \$update_free_access to TRUE.

# return your site on-line and make sure everything works. then do this:"
cmd "rm -rf $UPGRADES"
cmd "rm -rf packages"
note ""


