#!/bin/bash

# Tento skript sa pokusi automaticky upgradovat stranku a zmenezovat vsetky
# zalohy a vsetko co k tomu patri. Je vhodne zdokumentovany, aby bol zhruba
# pochopitelny aj pre inych, ako som ja.
# TODO: doplnit na vhodne miesto, ze po update vymazat adresar update-tmp/

# Zalohujem, kolko mozem, ale neviem velmi predpovedat, co sa stane, ked
# uprostred prerusis "mv", apod. Takze skript v spravnom case sam upozorni,
# ze pre istotu by sa hodilo spravit globalnu zalohu pomocou "cp -a" (alebo
# byt pripraveny pouzit zalozny serverovy disk).
# Co sa tyka zalohy databazy, proste spustim mysqldump. Neviem presne, ako
# sa to obnovuje, ale to by sa malo dat vygooglit.
# Subory Drupalu su zalohovane do $STATE/backup/cesta~ku~suboru. To preto,
# ze vzdy sa zalohuju len tie, co sa menia. Keby miesto ~ bolo /, vznikol by
# takyto problem: povedzme ze skript zazalohoval sites/all/modules/cck do
# adresara $STATE/backup/sites/all/modules/cck. Teraz ked sa nieco pokazi,
# a niekto to ide opravovat, vidi vnutri backup adresar sites a pomysli si,
# jasne, sites je zazalohovany, zmazem ten normalny a obnovim ten zo zalohy.
# Ale v skutocnosti je zazalohovany len sites/all/modules/cck, nie cely
# sites, takze napriklad mas sites/default/settings.php v cudu.)
# Tu je, ako tu zalohu obnovit. Toto som nespravil ako funkciu skriptu, lebo
# ak je raz nieco pohnojene, mal by kontrolu prevziat clovek.
# set -e
# for file in $STATE/backup/*; do
#   basename=${file##*/}                   # dam prec "$STATE/backup/"
#   origpath=`tr '~' '/' <<<"$basename"`   # vsetky ~ zmenim na /
#   rm -r $origpath
#   mv $file $origpath
# done

# 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).

# Ked ste velmi odvazni, mozete skript pustat prikazom:
#   yes "" | sh upgrademain.sh [argumenty]    (alebo ako sa ten skript vola)
# yes "" je prikaz, co stale vypisuje prazdne riadky. Kedze skript je spraveny
# tak, ze pri prazdnom riadku zvoli defaultnu moznost, ma to za efekt, ze to
# bude totalna automatika (ledaze by sa nieco pokazilo). Inak sa skript moze
# niekedy nieco spytat, ale je toho skutocne velmi malo.

# Baliky sa mozu automaticky po stiahnuti patchovat. To sa hodi, ked je
# napriklad niekde upravene jadro (to sa vo vseobecnosti neodporuca, ale subor
# .htaccess sa upravuje pomerne bezne). Funguje to takto: vzdy, ked stiahnem
# balik, vojdem do jeho adresara, a spustim vsetky PATCH-* subory v hlavnom
# adresari stranky (tam, kde je index.php). To su nejake shell skripty, co ma
# tu vyhodu, ze mozu nielen patchovat, ale robit v podstate cokolvek. Ked
# naozaj chcu patchovat, ide to napriklad takto:
# #!/bin/bash
# # Povedzme ze ideme patchovat balik CCK. Take baliky maju vzdy nazov v tvare
# # napr. cck-6.x-2.1-beta1, proste dolezite je, ze meno momentalneho adresara
# # (kde som cd-cknuty) zacina na cck-. Ked nezacina, nic nechcem patchovat.
# # Ked zacina, spustim ten patch. Program patch vzdy odignoruje blaboly na
# # zaciatku suboru, takze mozeme ako patch zadat tento subor ($0). Este tam
# # dame parameter -d *. Kedze momentalne je tam len jeden adresar (vnutri
# # cck-6.x-2.1-beta1.tar.gz je vsetko v adresari cck/), tak * bude "cck",
# # cize ten adresar, kde su samotne subory CCK. -d znamena "cd-ckni sa tam".
# # (To sa mozno pri moduloch az tak nehodi, lebo kazda verzia CCK je
# # v adresari "cck", ale vnutri drupal-x.y.tar.gz je vzdy adresar drupal-x.y,
# # Takze tam tu hviezdicku treba.)
# [[ $(basename `pwd`) == cck-* ]] && patch -d * -p0 <"$0"
# exit 0
# --- ../cck.orig/content.module   2008-12-10 21:04:08.000000000 +0100
# +++ content.module   2008-12-10 21:10:12.000000000 +0100
# (atd, data co vygeneroval prikaz "diff -ru ../cck.orig .")

# Este to chce napisat, ako priblizne postupovat pri velkych upgradoch (t.j.
# ked sa Drupalu zvysi hlavne verziove cislo). V ramci jednoduchosti sa
# odporuca odlozit upgrade, az kym nebudu portovane vsetky moduly na novu
# verziu. Ak sa to prilis odklada, a treba viac velkych upgradov za sebou,
# najprv upgraduj raz a dosiahni uplne komplet funkcnu stranku, az *potom*
# zacinaj druhy upgrade. No, k veci:
# 1. Sprav kopiu databazy pod inym menom a kopiu suborov v inom adresari.
#    V tej kopii v settings.php vhodne zmen $base_url. Pocas upgradu pracuj
#    len s tou kopiou. Aha, a ubezpec sa, ze na stranke nikto nic nerobi,
#    lebo ked tu kopiu skopirujes naspat, stratilo by sa to.
# 2. Disablni vsetky moduly a temy, co nie su "Core" (t.j. dodavane priamo
#    s Drupalom).
# 3. Vyrob adresar update-tmp (alebo ako sa vola $STATE...) a v nom do suboru
#    "available" rucne napis adresy drupalu samotneho, a jednotlivych modulov
#    a tem. Z kazdeho modulu/temy najdi najnovsiu verziu pre dany core. (Cize
#    ked mas drupal 6.x, hladaj iba moduly, co vo filename maju 6.x.) Tieto
#    adresy najdi na drupal.org.
#    (Pointa je, ze neviem, co sa v takejto situacii pise v Update status, a
#    ci sa tam velke upgrady vobec pisu. Lepsie to bude takto.)
# 4. Vymysli, co s patchmi. Kto vie, ci na novom Drupale budu fungovat, tam
#    sa toho mohlo zmenit pomerne vela.
# 4. Spusti tento skript. Mali by sa stiahnut nove moduly a temy.
# 5. Spusti update.php. (Updatne to databazove tabulky jadra.)
# 6. Znovu zapni tie dalsie moduly a temy.
# 7. Spusti update.php. (Updatne to databazove tabulky modulov.)
# 8. Ked vsetko funguje, zrus staru databazu, premenuj tu novu na stary nazov,
#    presun subory na stare miesto, oprav naspat $base_url, atd.


# Defaultne hodnoty premennych. URL je adresa stranky, ako je na webe. SITEID
# je cesta ku vlastnym nastaveniam a modulom stranky. Momentalny adresar je
# Drupal root (tam, kde je index.php a modules/ a sites/all/). STATE je
# adresar, kde si tento skript odklada subory a zalohy.
# TODO $SITEID autodetection (ze spravim request na $URL a checknem ake
# konfiguraky to pouzilo).
[[ $SITEID ]] || SITEID=sites/default
[[ $STATE ]] || STATE=update-tmp

# TODO: ked na konci do prehliadaca zadas update.php, moze ti to vypisat
# Access denied, a neviem preco. Workaround je ist do Modules a tam je link
# na update.php, ten funguje. (Ked som to vyskusal znovu, uz to zrazu slo.)

set -e
IFS=$'\n'

die () { echo "$@"; exit 1; }

message () { sed 's/^ \+//g' | fmt -78; }

[[ "${1:0:1}" == "-" ]] && die "Help je na vrchu zdrojoveho kodu.
Usage: $0 nazov-funkcie argumenty pre funkciu
Ked nedas ziadne argumenty, pouzije sa funkcia: $0 main"

[[ -d /var/www/html ]] || die "Skript treba pustat na serveri!"
[[ -f xmlrpc.php ]] || die "Skript treba pustat z adresara s Drupalom!"
[[ -w xmlrpc.php ]] || die "Skript musi moct prepisovat Drupalove veci!"
[[ "$URL" ]] || die "Sprav: export URL=http://example.com/adresa/stranky/
A mozno aj: export SITEID=sites/nastaveniovy_adresar (std: sites/default)"
[[ -d $SITEID ]] || die "Neexistuje nastaveniovy adresar $SITEID!"

mkdir -p $STATE
chmod 700 $STATE

find_available_updates () {
  [[ -f "$STATE/available" ]] && return 0
  echo $'\n*** Hladam dostupne updaty'
  # Najprv najdem stare avail-*.php (vid nizsie) a vycistim ich.
  local file
  for file in avail-*.php; do if [[ $file != "avail-*.php" ]]; then
    if grep -q get_availability_data $file; then
      echo "Je tu stary subor $file, asi minule spustenie zle skoncilo."
      local answer; read -p "Vymazat? (Y/n) " answer; answer=${answer:0:1}
      [[ $answer != n && $answer != N ]] && rm -f $file
    fi
  fi; 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.
  # Heslo bude tento secret_cookie. To je jedno, co je, len nech je nahodny.
  secret_cookie=`head -c128 /dev/urandom | sha1sum | head -c40`
  # Teraz spravim jeho hash. Tuto uz sha1sum pouzivam naschval.
  hashed_cookie=`echo -n $secret_cookie | sha1sum | head -c40`
  # Potom v tom PHP checkneme, ci sha1($_GET['pw']) == $hashed_cookie.
  # Teraz vyrobim samotny PHP subor (tiez nahodne meno) a zapisem tam program.
  php_file=./avail-`head -c128 /dev/urandom | sha1sum | 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"]) != "'$hashed_cookie'")
      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_cookie -o $STATE/available
  rm $php_file; trap - SIGINT SIGTERM
  cat $STATE/available
}

download_package () {
  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"
}

download_available_packages () {
  echo $'\n*** Stahujem nove baliky'
  for file in $(< $STATE/available); do
    download_package "$file"
  done
}

patch_package () {
  [[ ! -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
  touch .ready
  cd "$oldpwd"
}

prepare_package () {
  # Najdem absolutnu cestu k suboru, nech funguje aj ked prejdem inde.
  [[ ! -f "$1" ]] && echo "$1 neexistuje!" && return 1
  local archive="`readlink -f "$1"`"
  # Vymyslim pekny nazov pre novy adresar. Normalne sa vsetko vnutri archivu
  # rozpakuje do jedneho adresara, ale tym si nemozem byt vzdy isty.
  local id="${1##*/}"
  local i; for i in .tgz .tar.gz .tar.bz2 .zip; do id=${id%$i}; done
  local staged="$STATE/staged/$id"
  if [[ -f $staged/.ready ]]; then
    echo "Nasiel som hotovy balik $id."
    return 0
  fi
  if [[ -e $staged ]]; then
    echo "Nasiel som nedorozbalovany $staged."
    local answer; read -p "Vymazat? (Y/n) " answer; answer=${answer:0:1}
    [[ $answer == N || $answer == n ]] &&
      echo "OK, nejako si to zmenezuj." && return 1
    rm -rf "$staged"
  fi
  local oldpwd="`pwd`"
  mkdir -p "$staged"; cd "$staged"
  echo -n "Rozbalujem ${archive##*/}... "
  # TODO keby na artusi bol bsdtar, pouzi ten (vie otvarat aj zip)
  # TODO keby to slo, nech neexcluduje subory .ready v podadresaroch
  tar -xf "$archive" --exclude=".ready"
  echo "hotovo."
  rozbalene=( `find -mindepth 1 -maxdepth 1` )
  cd "$oldpwd"
  if [[ "${#rozbalene[@]}" != 1 ]]; then
    echo "$archive obsahuje viacero top-level poloziek:"
    for i in "${rozbalene[@]}"; do echo "${i#./}"; done
    message <<<"Nejako dosiahni spravny koncovy stav - nech $staged
        obsahuje len jeden adresar. Potom sprav subor $staged/.ready,
        bud proste pomocou touch, alebo prikazom '$0 patch_package $id'.
        Ten ma este tu vyhodu, ze sa pokusi ten balik dobre zapatchovat,
        ale toto je necakana situacia, nie som si isty, co treba pouzit."
    return 1
  fi
  patch_package "$id"
}

prepare_downloaded_packages () {
  echo $'\n*** Pripravujem baliky na instalaciu'
  local file; for file in $STATE/packages/*; do
    prepare_package "$file"
  done
}

check_maintenance_mode () {
  echo $'\n*** Overujem maintenance mode'
  # AFAIK vzdy ked je stranka v maintenance mode, pouziva tento styl.
  if curl -s $URL | grep -q modules/system/maintenance.css; then
    echo "Maintenance mode: zapnuty."
  else
    # To hore mozno nemusi spravne fungovat, ked je nastavena divna tema...
    # takze touto premennou sa to da overridnut, nech ide dalej.
    if [[ "$MAINTENANCE_CHECK" == "OVERRIDE" ]]; then
      echo "Maintenance mode: vypnuty (override)."
      echo "Mame override, takze idem aj tak dalej."
      return 0
    fi
    echo "Maintenance mode: vypnuty."
    message <<<"Najprv daj stranku do maintenance modu. Je to na
        ${URL}admin/settings/site-maintenance

        Ak uz to je hotove, ale napr. pouzivas divnu temu a zle som to
        zdetekoval (inak pri upgradovani tam ma byt nejaka standardna tema!),
        skus nastavit premennu 'export MAINTENANCE_CHECK=OVERRIDE'.

        Ked je to len nejaky maly upgrade, mozes to risknut, nastavit tu
        premennu a na maintenance mode kaslat, ale neni to najlepsi napad."
    return 1
  fi
}

backup_database () {
  [[ -f "$STATE/dbdump" && ! -f "$(< "$STATE/dbdump")" ]] &&
    rm "$STATE/dbdump"
  [[ -f "$STATE/dbdump" ]] && return 0
  echo $'\n*** Zaloha databazy'
  local answer dinfo dbname dbdump; declare -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]."/settings.php";
  $parsed = parse_url($db_url);
  foreach(explode(" ", "scheme host user pass path") as $key)
    echo $parsed[$key] . "\n";' "$SITEID"`
  info=($dinfo)   # rozkuskujem po $'\n' a dam do pola
  [[ ${info[0]} != mysql* ]] &&
    echo "Zatial podporujem iba zalohovanie MySQL." && return 1
  dbname=${info[4]}; dbname=${dbname#/}
  dbdump=$STATE/db-$dbname-`date +%Y%m%d`
  echo "Dumpujem databazu do $dbdump"
  if [[ -f "$dbdump" ]]; then
    message <<<"Subor $dbdump uz existuje, asi posledny dump zle skoncil."
    read -p "Vymazat? (Y/n) " answer
    [[ $answer == N || $answer == n ]] &&
      echo "OK, nejako si to zmenezuj." && return 1
    rm -f "$dbdump"
  fi
  # 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 "Zazalohovane. Velkost: `du -h "$dbdump" | cut -f1`"
}

install_data () {    # $1=source $2=dest
  echo "'$1' -> '$2'"
  if [[ -e "$2" ]]; then
    if [[ $2 == *"~"* ]]; then   # je v uvodzovkach, nech sa nezmeni na $HOME
      # Iba vypisem warning... je to taka neobvykla situacia, ze hlbsie
      # sa nou zaoberat myslim nema zmysel.
      # Preco je to vlasne problem? Lebo ked zalohujem, tak / zmenim na ~.
      # Ale ked cesta/s~vlnovkou zmenim na cesta~s~vlnovkou, neda sa rozoznat,
      # ci to povodne bola cesta/s~vlnovkou, alebo cesta/s/vlnovkou. Ten
      # obnovovaci skriptik navrhnuty celkom hore potom nemusi fungovat.
      echo "POZOR: Cesta $2 obsahuje \"~\"!"
      echo "Kebyze treba obnovovat zo zalohy, ries to ako specialny pripad."
    fi
    # Zmenim vsetky / na ~ (vid dokumentaciu o zalohovani hore).
    # Pozn.: na artusi je stara verzia bashu, ${2////"~"} bohuzial nefunguje.
    local backup="$STATE/backup/`tr '/' '~' <<<"$2"`"
    if [[ -e "$backup" ]]; then
      message <<<"Chcel som zalohovat do $backup, ale tam uz nieco
          je. To je nejake podozrive. Mam to vymazat? (Inak to prepisem a bude
          to tam pomiesane.) Prinajhorsom ma mozes stopnut s ^C."
      # Su dva pokusy na zadanie odpovede. Pri pouziti 'yes "" |' sa to
      # nezacykli, ale pritom ak sa raz preklepnes, nic hrozne sa nestane.
      local answer; read -p "Vymazat? (y/n/^C) " answer
      [[ $answer != y && $answer != Y && $answer != n && $answer != N ]] &&
        read -p "Nerozumiem. Vymazat? (y/n/^C) " answer
      [[ $answer != y && $answer != Y && $answer != n && $answer != N ]] &&
        echo "Nerozumiem. Zatial koncim, pust ma znovu." && return 1
      [[ $answer == y || $answer == Y ]] && rm -rf "$backup"
    fi
    [[ -d "$STATE/backup" ]] || mkdir "$STATE/backup"
    mv $2 "$backup"
  fi
  mv "$1" "$2"
}

install_package () {
  local staged="$STATE/staged/$1"
  [[ ! -f $staged/.ready ]] &&
    echo "Balik $1 nemozem nainstalovat, je nedorozbalovany." && return 1
  [[ -f $staged/.installed ]] && return 0   # uz nainstalovane
  echo "Instalujem balik $1"
  local file source=""
  for file in "$staged"/*; do if [[ $file != "$staged/*" ]]; then
    if [[ "$source" ]]; then
      message <<<"Chyba: $staged obsahuje viacero veci! Mal tam byt len jeden
          adresar. (Je tam aj $source aj $file.)"
      return 1
    fi
    source="$file"
  fi; 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.
    declare -a founddest
    for file in {$SITEID,sites/all}/{modules,themes}; do
      [[ -e "$file/${source##*/}" ]] && founddest+=( $file )
    done
    if (( ${#founddest[@]} == 0 )); then
      message <<<"Neviem, kam to nainstalovat. Na vhodnom mieste (napr. v
          sites/all/modules) urob prazdny adresar ${source##*/} a skus znovu."
      return 1
    fi
    if (( ${#founddest[@]} > 1 )); then
      message <<<"Neviem, kam to nainstalovat. Adresar ${source##*/} existuje
          na viacerych miestach. Toto je neznama situacia, nejako to oprav,
          nech zostane taky adresar len na jednom mieste."
      for file in "${founddest[@]}"; do echo "$file"; done
      return 1
    fi
    install_data "$source" "${founddest[0]}/${source##*/}"
  fi
  touch "$staged/.installed"
}

install_prepared_packages () {
  echo $'\n*** Instalujem baliky'
  message <<<"Pozor: Idem zacat prepisovat samotne subory Drupalu. Priebezne
      sice robim zalohy do $STATE/backup, ale neriesim celkom vsetky pripady,
      napr. ak sa to prerusi zrovna pocas robenia zalohy. Ak chces byt extra
      zabezpeceny, sprav nieco typu 'cp -av . ~/moja-zaloha'. (Inak, aj keby
      to uprostred spadlo, malo by to ist znovu pustit a dalej upgradovat.
      Blbnut by to mohlo len, keby bolo treba downgradovat naspat.)"
  local answer; read -p "Mozem ist dalej? (Y/n) " answer
  [[ $answer == N || $answer == n ]] &&
    echo "OK, zatial pockam." && return 1
  local file; for file in $STATE/staged/*; do
    install_package "${file#$STATE/staged/}"
  done
}

print_conclusion () {
  echo $'\n*** Hotovo'
  # TODO: to s tym user #1 by sa patrilo povedat skorej
  message <<<"Vyzera, ze vsetky subory su nainstalovane. Teraz chod na
      ${URL}update.php kde to upgradne databazove veci. Potom veselo
      testuj, ci vsetko funguje. (Inak, update.php chce aby si bol
      prihlaseny ako user #1, absolutny admin. Ak nahodou nie si, mozes
      docasne update.php rucne zeditovat a na vrchu dat nech to necheckuje,
      ale je to potom nezabezpecene, lepsie je prihlasit sa.)"
}

main () {   # upgradneme proste vsetko
  find_available_updates
  download_available_packages
  prepare_downloaded_packages
  check_maintenance_mode
  backup_database
  install_prepared_packages
  print_conclusion
}

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

