#!/bin/bash

# Tento skript automaticky upgradne instalaciu Drupalu a zmenezuje zalohy a
# vsetko okolo. Je zdokumentovany, nech ho chapu aj ini, ako ja.
# Skript je dost stabilny, ale neriesim pripady ako ze spustim dvakrat naraz,
# alebo ze tam je filename obsahujuci '\n' (lebo to by zblblo find).

# Aj ak upgrade na niecom spadne, malo by ist splnit dane instrukcie a pustit
# ho znovu presne odkial prestal. Ale ak fakt chces obnovit povodny stav,
# obnov databazu cez: mysql -u uzivatel -p databaza <mojazaloha.sql
# a potom subory. Tie su spolu v tar.gz, proste stare vymaz a rozbal to.
# Ale pozor! Subory v sites/cokolvek-okrem-all vlastni apache. Odporuca sa
# pomocou tar --exclude dane adresare nechat na pokoji.

# Velmi neriesim specialne pripady... typu ze ked ten skript spustim dvakrat
# naraz, alebo ked zadane cesty obsahuju \n (medzery by snad mali byt OK).
# (Mimo ineho by mi to pokazilo vysledky z find, ktory ich oddeluje \n.) Ale
# zato sa snazim, aby vzdy skoncil/spadol v konzistentnom stave (alebo aspon
# povedal, co kde opravit).

# Skriptu ide na vstup dat ruru z yes "". To je program, co stale vypisuje \n.
# Vtedy skript skusi zvolit najlepsiu moznost. Ale ten skript je celkovo
# dost automaticky, ked uz sa na nieco pyta, lepsie je si to fakt precitat.

# Stiahnute baliky dokazem patchovat. V komunite Drupalu sa to neodporuca, ale
# napr. pri .htaccess to treba. Kazda zmena suborov Drupalu (ked nevyrabas
# novy modul/temu) musi byt v nejakom patchi, inak sa pri upgrade strati.
# Napr. stiahnem cck-6.x-2.2.tar.gz a rozbalim ho do staged/cck-6.x-2.2/.
# (Preto samotne subory su v staged/cck-6.x-2.2/cck/.) Skript teraz vojde do
# staged/cck-6.x-2.2/ a spusti vsetky skripty PATCH-* z hlavneho adresara.
# Kedze program patch ignoruje nezmysly na zaciatku suboru, mozem dat bashovu
# hlavicku aj ten patch do jedneho suboru. Hlavicka vyzera napriklad takto:
# #!/bin/bash
# [[ $(basename `pwd`) != cck-* ]] && exit 0    # ine baliky ma nezaujimaju
# patch -p0 <"$0"    # $0 je prave vykonavany skript. -p0 vid man patch
# exit
# Potom idu samotne data, co vygeneroval prikaz "diff -ru cck.orig/ cck/".
# Patchovanie jadra je zlozitejsie, tam to je staged/drupal-6.8/drupal-6.8/.
# Meno vnutorneho drupal-6.8/ sa meni (nie ako cck/), vlez donho cez "cd *".
# (Vnutri drupal-6.8.tar.gz je len ten jeden adresar, preto * bude len on.)

# Co pri major upgradoch (major verzia jadra, napr. Drupal 7)? Odporuca sa
# odlozit to, kym nebudu portovane vsetky moduly. Ak robis viacero major
# upgradov za sebou, dosiahni celkom funkcnu stranku na kazdej medziverzii.
# 1. Kedze je to dlhodobejsi podnik, sprav kopiu databazy a kopiu suborov.
#    V novom sites/default/settings.php nastav parametre novej databazy.
#    Pocas upgradu pracuj na tej kopii, a ked bude hotova, premenuj ju spat.
#    Na stranke nech medzitym nikto nepracuje, lebo o svoje zmeny pride.
# 2. Disablni vsetky moduly a temy, co nie su "Core".
# 3. Kto vie, co bude Drupal hlasit v "Update status"? Radsej rucne do suboru
#    update-default/available napis URLky kazdeho stahovaneho baliku pre
#    novu verziu Drupalu. (Ale posledne slovo ma dokumentacia na drupal.org.)
# 4. Rozhodni o kazdom PATCH-*, ci ho este treba a ci bude stale fungovat.
# 5. Spustie ten skript ako normalne.
# 6. Spusti update.php, to updatne databazu jadra. Potom znovu enabluj
#    vsetky moduly a temy, a potom spusti update.php znovu.
# 7. Ked vsetko funguje, zrus staru databazu a premenuj tu novu naspat.


set -e
shopt -s dotglob nullglob
IFS=$'\n'

die () { echo "$@"; exit 1; }
message () { sed 's/^ \+//g' | fmt -78; }
red () { tput setf 4; tput bold; }
yellow () { tput setf 6; tput bold; }
white () { tput setf 7; }
op () { tput op; tput sgr0; }
header () { echo $'\n'"`yellow`*** $1`op`"; }
beginhelp () { echo "
BTW, ked ides upgradovat, uisti sa, ze si `yellow`prihlaseny ako user #1`op`,
nech ti ide update.php. Tiez mozno `yellow`zapni maintenance mode`op`, aj ked
to este moze pockat."; }
randomhex () { head -c128 /dev/urandom | sha1sum | head -c40; }
stale () {
  echo "`red`Od minuleho spustenia zostal stary $1.`op`"
  local answer; read -p "Vymazat? (Y/n) " answer; answer=${answer:0:1}
  [[ $answer != n && $answer != N ]] && rm -rf $file && return 0
  return 1
}

[[ $1 == -* ]] && die "`red`Help je na vrchu zdrojoveho kodu.`op`
Usage: `white`$0 funkcia argument1 argument2 ...`op`
(Ked nedas ziadnu funkciu, pouzije sa: `white`$0 main`op`)
`beginhelp`"

[[ -f xmlrpc.php ]] || die "`red`Skript pustaj z adresara s Drupalom!`op`"
[[ -w xmlrpc.php ]] || die "`red`Skript sem musi moct zapisovat!`op`"

# bsdtar nielen ze dokaze rozbalovat aj tar aj zip, a ma *vstavany* gzip
# aj bzip2, ale hlavne ma zaruku, ze zly archiv obsahujuci absolutne cesty
# alebo /../ v ceste neprepise subory mimo tohto adresara. (Vid option -P.)
bsdtar --help &>/dev/null || die "`red`Neviem najst program bsdtar!`op`"

if [[ ! $SITEID ]]; then   # autodetekcia, ak je len jedna moznost
  tmp=( `find sites/ -mindepth 1 -maxdepth 1 \! -name all` )
  (( ${#tmp[@]} == 1 )) && SITEID=${tmp[0]} && SITEID=${SITEID##*/}
  unset tmp
fi
[[ $SITEID && ! $STATE ]] && STATE=update-$SITEID

echo "
URL (adresa stranky)      = `white`${URL:-(?)}`op`
SITEID (adresar v sites/) = `white`${SITEID:-(?)}`op`
STATE (na docasne subory) = `white`${STATE:-update-(?)}`op`
"

[[ $URL ]] || die \
  "Nastav `white`export URL=http://example.com/adresa/stranky/`op`
`beginhelp`"
[[ $SITEID ]] || die \
  "Nastav `white`export SITEID=...`op` na meno adresara v sites/, co obsahuje
data o tejto konkretnej stranke. Z tejto instalacie Drupalu bezi viacero, tak
nezabudni `yellow`nakoniec spustit update.php z kazdej`op`.
`beginhelp`"

mkdir -p $STATE
chmod 700 $STATE

# Pomocou specialneho PHP skriptu sa spoji s Drupalom, nacita data z jeho
# stranky "Available updates" a do $STATE/available vypluje zoznam URLiek.
available_updates () {
  [[ -f "$STATE/available" ]] && return 0
  header "Hladam dostupne updaty"
  local file; for file in avail-*.php; do   # vycistim stare avail-*.php
    grep -q "get_availability_data" $file && stale $file
  done
  # Spravim maly PHP skript, ktory mi potom zisti, ktore updaty su dostupne,
  # podobne ako na admin/reports/updates, ale preparsovatelne v bashi a bez
  # nutnosti loginu. To by bola potencialna bezpecnostna diera, takze ten
  # skript bude fungovat, iba ked mu dam cez ?pw=... spravne heslo.
  secret=`randomhex`
  secret_hashed=`echo -n $secret | sha1sum | head -c40`
  # Potom v tom PHP checkneme, ci sha1($_GET['pw']) == $secret_hashed.
  # Teraz vyrobim samotny PHP subor (tiez nahodne meno) a zapisem tam program.
  php_file=./avail-`randomhex | head -c6`.php
  trap "echo 'Cistim po sebe $php_file'; rm $php_file; exit 1" SIGINT SIGTERM
  echo >$php_file '<?php

    if(sha1($_GET["pw"]) != "'$secret_hashed'")
      die("Access denied.");

    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();'
  curl -s ${URL}${php_file#./}'?pw='$secret -o $STATE/available
  rm $php_file; trap - SIGINT SIGTERM
  cat $STATE/available
}

# download $url: stiahne (resp. dostahuje) dany tar.gz balik do packages/
# bez zadanej URL stiahne vsetky adresy, co su v subore available.
download () {
  if [[ ! $1 ]]; then
    header "Stahujem nove baliky"
    local file; for file in $(< $STATE/available); do download $file; done
    return
  fi
  local oldpwd="`pwd`"
  mkdir -p $STATE/packages; cd $STATE/packages
  # curl vrati chybu, ked je subor uz cely stiahnuty. Odhadom je stiahnuty
  # prave vtedy, ked sa da v poriadku rozbalit.
  if ! tar tf "${1##*/}" &>/dev/null; then
    echo "$1"
    curl -# -C - -O "$1"
  fi
  cd "$oldpwd"
}

# patch $package: na balik v staged/$package aplikuje vsetky PATCH-* skripty
patch () {
  [[ ! -d $STATE/staged/$1 ]] &&
    echo "$STATE/staged/$1 neexistuje!" && return 1
  local oldpwd=`pwd`
  cd $STATE/staged/$1
  local file; for file in $oldpwd/PATCH-*; do
    [[ "$file" != "$oldpwd/PATCH-*" ]] && sh $file
  done
  cd "$oldpwd"
  touch $STATE/staged/.ready-$1
}

# prepare $file: rozbali a patchne to do staged/${file-basename-bez-koncovky}
# bez zadaneho suboru to spravi so vsetkymi archivmi v packages/
prepare () {
  if [[ ! $1 ]]; then
    header "Rozbalujem baliky"
    local file; for file in $STATE/packages/*; do prepare $file; done
    return
  fi
  [[ ! -f $1 ]] && echo "$1 neexistuje!" && return 1
  # Vymyslim pekny nazov pre adresar, kam obsah archivu rozbalim.
  # Archiv mozno obsahuje vela top-level poloziek (aka "tarbomb"), preto to
  # nerozbalujem priamo do staged/.
  local id=${1##*/}
  local i; for i in .tgz .tar.gz .tar.bz2 .zip; do id=${id%$i}; done
  local staged="$STATE/staged/$id"
  echo "`yellow`Rozbalujem ${1##*/}...`op` "
  # .ready chrani pred situaciou, ze bsdtar spadol uprostred
  [[ -f $STATE/staged/.ready-$id ]] && echo "Uz bol rozbaleny." && return 0
  if [[ -e $staged ]]; then
    echo "`red`Uz nejaky existuje, ale nie je dorozbalovany.`op`"
    ! stale $staged && echo "Tak si to nejako zmenezuj." && return 1
  fi
  local oldpwd="`pwd`"
  mkdir -p $staged
  bsdtar -xf $1 -C $staged
  echo "Hotovo."
  rozbalene=( `find $staged -mindepth 1 -maxdepth 1` )
  if [[ "${#rozbalene[@]}" != 1 ]]; then
    echo "`red`$archive obsahuje viacero top-level poloziek:`op`"
    for i in "${rozbalene[@]}"; do echo "${i#$staged/}"; done
    echo "Tu su niektore mozne riesenia. Potom ma pusti znovu."
    echo "A) `yellow`Rucne ten archiv uprav, daj tam nieco poriadne.`op`."
    # TODO chce to adaptovat funkciu 'message' na farebny text a pouzit ju
    echo "B) `yellow`Vymaz $staged, vymaz ten archiv a vymaz jeho adresu`op`"
    echo "   `yellow`zo zoznamu updatov v $STATE/available.`op`"
    return 1
  fi
  patch "$id"
}

check_maintenance_mode () {
  header "Overujem maintenance mode"
  # AFAIK vzdy ked je stranka v maintenance mode, pouziva tento styl.
  curl -s $URL | grep -q modules/system/maintenance.css &&
    echo "Maintenance mode: `white`zapnuty`op`." && return 0
  # To hore mozno nemusi spravne fungovat, ked je nastavena divna tema...
  # takze touto premennou sa to da overridnut, nech ide dalej.
  [[ $MAINTENANCE_CHECK == OVERRIDE ]] &&
    echo "Maintenance mode: `white`vypnuty`op` (`red`override`op`, pokracujem)." && return 0
  echo "Maintenance mode: `red`vypnuty`op`.

V administracnom rozhrani `yellow`zapni maintenance mode`op`.
(V slovencine `yellow`udrzba webu`op`).
Ak som to zle zdetekoval, alebo proste si drsnak a je to len maly upgrade, daj
`white`export MAINTENANCE_CHECK=OVERRIDE`op`
BTW, treba sa `yellow`prihlasit ako user #1`op`, inak neotvoris update.php!"
  return 1
}

backup_database () {
  [[ $DB_BACKUP == DONE ]] && return 0
  [[ -f $STATE/dbdump && ! -f $(< $STATE/dbdump) ]] &&
    rm $STATE/dbdump
  [[ -f $STATE/dbdump ]] && return 0
  header "Zaloha databazy"
  local answer dinfo dbname dbdump; local -a info
  # Pomocou PHP CLI nacitam settings.php, parsnem $db_url a vypisem po
  # riadkoch zaujimave casti. Pouzivatel ani nemusi zadat heslo databazy.
  dinfo=`php -r 'require $argv[1]; $parsed = parse_url($db_url);
  foreach(explode(" ", "scheme host user pass path") as $key)
    echo $parsed[$key] . "\n";' "sites/$SITEID/settings.php"`
  info=($dinfo)   # rozkuskujem po riadkoch (IFS=$'\n') a dam do pola
  if [[ ${info[0]} != mysql* ]]; then
    echo "`red`Zatial podporujem iba zalohovanie MySQL.`op`"
    echo "Zazalohuj sam, sprav `white`export DB_BACKUP=DONE`op` a skus znovu."
    return 1
  fi
  dbname=${info[4]}; dbname=${dbname#/}
  dbdump=$STATE/db-$dbname-`date +%Y%m%d`
  [[ -f $dbdump ]] && ! stale $dbdump &&
    echo "OK, nejako si to zmenezuj." && return 1
  # Nastavim ten subor na 600 este pred spustenim mysqldump. Aj keby sa
  # mysqldump uprostred prerusil, nestane sa ziadna strasna katastrofa,
  # lebo ten dump nebude verejne citatelny. Len to bude strasit na disku.
  mktemp $dbdump >/dev/null
  mysqldump -h "${info[1]}" -u "${info[2]}" -p"${info[3]}" \
      "$dbname" >$dbdump
  echo $dbdump >$STATE/dbdump
  echo "Vytvorena zaloha: `white`$dbdump`op`"
}

backup_files () {
  [[ -f $STATE/backup-completed ]] && return 0
  header "Zaloha suborov"
  [[ -f $STATE/backup.tar.gz ]] && ! stale $STATE/backup.tar.gz &&
    echo "OK, nejako si to zmenezuj." && return 1
  bsdtar -cf $STATE/backup.tar.gz `ls -A -I 'update-*'`
  touch $STATE/backup-completed
  echo "Vytvorena zaloha: `white`$STATE/backup.tar.gz`op`"
}

install_data () {    # $1=source $2=dest
  echo "'$1' -> '$2'"
  [[ -e $2 ]] && rm -rf $2
  mv $1 $2
}

install () {
  if [[ ! $1 ]]; then
    header "Instalujem baliky"
    local file; for file in `ls $STATE/staged/`; do
      if [[ ! -f $STATE/staged/.ready-$file ]]; then
        echo "`red`Preskakujem $file, asi je nedorozbalovany.`op`"
        echo "`red`(Nenasiel som totiz subor $STATE/staged/.ready-$file.)`op`"
      else
        install $file
      fi
    done
    return
  fi
  local staged="$STATE/staged/$1"
  echo "`yellow`Instalujem balik $1`op`"
  [[ -f $STATE/staged/.installed-$1 ]] && return 0   # uz nainstalovane
  [[ ! -f $STATE/staged/.ready-$1 ]] &&
    echo "`red`Balik nie je dorozbalovany!`op`" && return 1
  local file source
  for file in $staged/*; do
    if [[ $source ]]; then
      echo "`red`$staged obsahuje viacero veci!`op`"
      echo "Mal tam byt len jeden adresar. Ved som to kontroloval, nie?"
      return 1
    fi
    source=$file
  done
  if grep -Eq '^drupal-[0-9]' <<<"$1"; then
    # Instalujem jadro.
    for file in cron.php index.php install.php update.php xmlrpc.php \
        .htaccess robots.txt includes misc modules profiles scripts themes; do
      install_data $source/$file $file
    done
  else
    # Instalujem modul/temu
    # Najprv musim najst, ci je to modul, alebo tema, a kde je nainstalovany.
    local -a founddest
    for file in {$SITEID,sites/all}/{modules,themes}; do   # mozne lokacie
      [[ -e $file/${source##*/} ]] && founddest+=( $file )
    done
    if (( ${#founddest[@]} == 0 )); then
      echo "`red`Nikde nevidim adresar nazvany ${source##*/}.`op`"
      echo "Kam to mam nainstalovat? To instalujes novy balik? Skus spravit"
      echo "vhodny prazdny adresar (napr. sites/all/modules/${source##*/})."
      return 1
    fi
    if (( ${#founddest[@]} > 1 )); then
      echo "`red`Adresar nazvany ${source##*/} je na viacerych miestach.`op`"
      echo "Neviem, ktoru kopiu mam prepisat tymto novym balikom. Nejako ich"
      echo "pomaz alebo premenuj, nech zostane len na jednom mieste."
      for file in "${founddest[@]}"; do echo "`white`$file`op`"; done
      return 1
    fi
    install_data $source ${founddest[0]}/${source##*/}
  fi
  touch $STATE/staged/.installed-$1
}

conclude () {
  header 'Hotovo'
  echo "Teraz uz len chod na `white`${URL}update.php`op`."
  echo "Potom `yellow`vymaz $STATE`op`."
}

main () {   # upgradneme proste vsetko
  available_updates
  download
  prepare
  check_maintenance_mode
  backup_database
  backup_files
  install
  conclude
}

(( $# == 0 )) && set main
"$@"

