From 97664af3eb227d66682031664066ea31779aa229 Mon Sep 17 00:00:00 2001 From: Michael Lux Date: Mon, 29 Aug 2022 18:15:44 +0200 Subject: [PATCH] Native unzip/untar, big code refactoring, removed PushBullet --- README.md | 54 +++++++----- index.php | 242 +++++++++++++++++++++++++++++---------------------- push.inc.php | 104 ++++++++-------------- 3 files changed, 208 insertions(+), 192 deletions(-) diff --git a/README.md b/README.md index 5e42366..e497200 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,25 @@ Das hier geht an alle, die ChurchTools selbst hosten... ## Allgemein - -Michael Lux (@milux) und Ich hatten keine Lust mehr ChurchTools ständig von Hand per FTP zu aktualisieren, -deshalb haben wir einen "kleinen" Auto Updater für ChurchTools entwickelt. +Michael Lux (@milux) und ich hatten keine Lust mehr, ChurchTools ständig von Hand per FTP zu aktualisieren. +Deshalb haben wir einen "kleinen" Auto Updater für ChurchTools entwickelt. Die Benutzung unseres Updaters ist denkbar einfach und sollte auch ohne Programmier-Kenntnisse nicht überfordern. -## Funktionen +### WICHTIG: Update ab ChurchTools 3.89.0 +Das `.tar.gz`-Archiv mit den Updates enthält nun **Dateinamen mit >= 99 Zeichen Länge**! +Dies wird vom alten TAR-Standard "ustar" nicht unterstützt, weshalb das Archiv im Format "pax" (POSIX 2001) +geliefert wird. Unglücklicherweise gibt es derzeit **keine** PHP-Library, die Dateien aus pax-TARs mit vollständigen +Namen korrekt entpacken kann. +Die aktuelle Updater-Version hat jetzt eine Heuristik zur Erkennung falsch entpackter Update-TARs +(99 <= Dateiname <= 100 Zeichen, keine File-Extension) und bricht in diesem Fall das Update ab. +Aus diesem Grund gibt es in der aktuellen Version jetzt die Möglichkeit, das native `tar`-Programm per Kommandozeile +zu verwenden, sofern es vorhanden ist. +Auf allen typischen Linux-Installationen sollte dieses Programm vorhanden sein. Um das "native Entpacken" zu aktivieren, +muss in der `config.php` folgendes deklariert werden: `const NATIVE_EXTRACT = true;` (s. unten). +Diese Zeile kann auch nachträglich problemlos hinzugefügt werden. + +## Funktionen Folgender **Funktionsumfang** ist in unserem Skript momentan enthalten: + Schutz des Skripts mit per Query-String übergebenem Passwort. @@ -16,12 +28,12 @@ Folgender **Funktionsumfang** ist in unserem Skript momentan enthalten: + Herunterladen der ZIP-Datei vom ChurchTools SeaFile Server + Entpacken der ZIP-Datei und Ersetzung der index.php und des system-Verzeichnisses (Einspielen des Updates) + Lock, der verhindert, dass das Update mehrmals zur gleichen Zeit gestartet wird. -+ Push Benachrichtigung (über Pushover und / oder Pushbullet) ++ Push-Benachrichtigung über Erfolg/Misserfolg des Updates (via Pushover) Bei Fragen stehen wir gerne zur Verfügung! :) ## Installation -Hiermit wird ein `update` Verzeichnis in eurem Curchtools verzeichnis angelegt welches den Updater +Hiermit wird ein `update`-Verzeichnis in eurem ChurchTools-Verzeichnis angelegt welches den Updater und die von euch zu erstellende Konfigurationsdatei enthält und ganz einfach geupdated werden kann. ```bash cd @@ -42,32 +54,34 @@ Vor dem ersten Update müsst Ihr eure Konfiguration initialisieren und ein Passw Dafür zuerst durch den Aufruf von einen Passwort-Hash erzeugen! Den Inhalt der bei diesem Aufruf angezeigt wird als `update/config.php` speichern und noch bei -`define('SEAFILE_CODE', 'xyz1234567');` das `xyz1234567` ersetzen, dann ist das Skript einsatzbereit! +`const SEAFILE_CODE = 'xyz1234567';` das `xyz1234567` ersetzen, dann ist das Skript einsatzbereit! (Ihr erhaltet für Updates per E-Mail einen Link zu einer SeaFile-Seite, die i.d.R. so aussieht: https://seafile.churchtools.de/d/**xyz1234567**/, verwendet davon den hinteren Teil, hier fett gedruckt.) ### Push -Defaultmäßig ist Push deaktiviert, kann aber über die define()-Anweisungen am Beginn der `push.inc.php` eingerichtet -und über das Anpassen von `define('ENABLE_PUSH', false);` (`false` zu `true`) aktiviert werden. +Defaultmäßig ist Push deaktiviert, kann aber über die `const`-Deklarationen am Beginn der `push.inc.php` eingerichtet +und über das Anpassen von `const ENABLE_PUSH = false;` (`false` zu `true`) aktiviert werden. -**config.php:** (aktivierte Einträge sind Pflicht und müssen passen, die auskommentierten optional) +**config.php:** ```php 12)); echo << UpdateSuccessful'; - } -}); - $lockFile = __DIR__ . '/ctupdate.lock'; $ignoreLock = false; $acquiredLock = false; @@ -89,19 +88,67 @@ throw new Exception('Update already in progress!'); } - // Update is directly installed from local developer build - $updateArchive = __DIR__ . '/churchtools-LATEST.tar.gz'; - $version = 'Developer-Build'; - // If no local dev build found, download ZIP file from SeaFile server - for ($tries = 0; $tries < 3 && !file_exists($updateArchive); $tries++) { - list($downloadURL, $version, $ext) = getDownloadURL(); - $updateArchive = __DIR__ . '/update' . $ext; - copy($downloadURL, $updateArchive); + // Normalized ChurchTools root directory + $ctRoot = realpath(CT_ROOT_DIR); + // Temporary directory for extraction + $tmpDir = __DIR__ . '/tmp'; + try { + // Update is directly installed from local developer build archive, if existing + $updateArchive = __DIR__ . '/churchtools-LATEST.tar.gz'; + $version = 'Developer-Build'; + // If no local dev build archive found, download archive from SeaFile server + for ($tries = 0; $tries < 3 && !file_exists($updateArchive); $tries++) { + list($downloadURL, $version, $ext) = getDownloadURL($ctRoot); + $updateArchive = __DIR__ . '/update' . $ext; + copy($downloadURL, $updateArchive); + } + + // Extract files + $updateDir = extractArchive($updateArchive, $tmpDir); + + // Create a backup of system folder and index.php + makeBackup($ctRoot); + + debugLog("Delete old files and dirs..."); + if (file_exists($ctRoot . '/system')) { + delTree($ctRoot . '/system'); + } + if (file_exists($ctRoot . '/index.php')) { + unlink($ctRoot . '/index.php'); + } + + debugLog('Move new files/dirs to ' . $ctRoot . '...'); + rename($updateDir . '/system', $ctRoot . '/system'); + rename($updateDir . '/index.php', $ctRoot . '/index.php'); + // Set mtime of constants.php to avoid endless updating + touch($ctRoot . '/system/includes/constants.php'); + } finally { + if (is_dir($tmpDir)) { + debugLog("Remove temporary extraction archive..."); + delTree($tmpDir); + } + if (file_exists($updateArchive)) { + debugLog("Remove updateArchive..."); + unlink($updateArchive); + } + } + + if (error_get_last() === null) { + if (function_exists('push')) { + push('Update successful', 'Update to version ' . $version . ' has been successfully applied!'); + } + echo ' |--> UpdateSuccessful'; + } else { + if (function_exists('push')) { + push('Update with error(s)', 'Update completed with one or more error(s). Last error message: ' + . error_get_last()['message']); + } } - // Extract files - updateSystem($updateArchive, $version); } catch (Exception $e) { echo $e->getMessage(), "\n"; + if (function_exists('push')) { + push('Update failed', 'Update aborted due to the following exception: ' . $e->getMessage()); + } } finally { if ($acquiredLock || $ignoreLock) { // Unlock script if lock was actually acquired or ignored @@ -121,15 +168,11 @@ * @return array * @throws Exception If something went wrong with the download */ -function getDownloadURL() { +function getDownloadURL(string $ctRoot = CT_ROOT_DIR): array { $jsonUrl = SEAFILE_HOST . 'api/v2.1/share-links/' . SEAFILE_CODE . '/dirents'; $json = json_decode(file_get_contents($jsonUrl)); if ($json === null || !isset($json->dirent_list)) { - if (function_exists('push')) { - push('[Fehler] Download', "Kein gültiger ChurchTools 3 Download im JSON gefunden!" - . " $jsonUrl", 1); - } - throw new Exception('No valid ChurchTools 3 download found in JSON!'); + throw new Exception('No valid contents list found in JSON from ' . $jsonUrl . '!'); } // Find ChurchTools archive $item = null; @@ -147,29 +190,25 @@ function getDownloadURL() { break; } - // dont't find a matching file? + // Didn't find a matching file? if (!isset($version)) { - if (function_exists('push')) { - push('[Fehler] Download', "Kein gültiger ChurchTools 3 Download in der Dateiliste gefunden!" - . " $jsonUrl", 1); - } - throw new Exception('No valid ChurchTools 3 download found in FileList!'); + throw new Exception('No valid ChurchTools 3 download found in FileList from ' . $jsonUrl . '!'); } $downloadUrl = SEAFILE_HOST . 'd/' . SEAFILE_CODE . '/files/?p=' . $file . '&dl=1'; debugLog("Checking whether $version from $downloadUrl is newer than installed version..."); // Parse SeaFile timestamp - $timeStamp = DateTime::createFromFormat(DateTime::ATOM, $item->last_modified)->getTimeStamp(); + $timeStamp = DateTime::createFromFormat(DateTimeInterface::ATOM, $item->last_modified)->getTimeStamp(); // If SeaFile archive is older than modification date of constants.php, don't perform update - if (file_exists(CT_ROOT_DIR . '/system/includes/constants.php') && - filemtime(CT_ROOT_DIR . '/system/includes/constants.php') > $timeStamp) { + if (file_exists($ctRoot . '/system/includes/constants.php') && + filemtime($ctRoot . '/system/includes/constants.php') > $timeStamp) { throw new Exception('ChurchTools is already up-to-date (' . $version . ')!'); } return [$downloadUrl, $version, $ext]; } // Recursive deleting of directorys -function delTree($dir) { +function delTree($dir): bool { $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { is_dir("$dir/$file") ? delTree("$dir/$file") : unlink("$dir/$file"); @@ -177,19 +216,18 @@ function delTree($dir) { return rmdir($dir); } -function makeBackup($dir = CT_ROOT_DIR, $dest_dir = BACKUP_DIR) { - debugLog("Backup $dir to $dest_dir..."); +function makeBackup($dir = CT_ROOT_DIR, $dest_dir = BACKUP_DIR): void { // Root folder $root = realpath($dir); if (!is_dir($dest_dir)) { - mkdir($dest_dir); + mkdir($dest_dir, 0755, true); } // Initialize archive object $zip = new ZipArchive(); $backup_file = $dest_dir . '/backup_' . time() . '.zip'; $zip->open($backup_file, ZipArchive::CREATE | ZipArchive::OVERWRITE); - debugLog("Save backup to $backup_file..."); + debugLog("Backup $dir to $backup_file..."); // Backup systems folder if (file_exists($root . '/system')) { @@ -206,7 +244,6 @@ function makeBackup($dir = CT_ROOT_DIR, $dest_dir = BACKUP_DIR) { // Get real and relative path for current file $filePath = $file->getRealPath(); $relativePath = substr($filePath, strlen($root) + 1); - // Add current file to archive $zip->addFile($filePath, $relativePath); } @@ -223,64 +260,65 @@ function makeBackup($dir = CT_ROOT_DIR, $dest_dir = BACKUP_DIR) { } /** - * Extract 'system' and 'index.php' - * @param string $updateArchive Path to archive to unpack - * @param string $version ChurchTools version from filename - * @param string $targetDir Target directory for extraction - * @throws Exception If the extraction process encountered an error + * @throws Exception If extraction of update failed */ -function updateSystem($updateArchive, $version, $targetDir = CT_ROOT_DIR) { +function extractArchive(string $updateArchive, string $targetDir): string { + if (!is_dir($targetDir)) { + mkdir($targetDir, 0755, true); + } debugLog("Extract $updateArchive to $targetDir..."); - $zip = new PharData($updateArchive); - $zip->extractTo($targetDir); - $needle = 'churchtools'; - $dirName = null; - foreach (scandir('phar:///' . $updateArchive) as $entry) { - if (substr($entry, 0, strlen($needle)) === $needle) { - $dirName = $entry; + if (defined('NATIVE_EXTRACT') && NATIVE_EXTRACT) { + $archivePath = realpath($updateArchive); + $targetPath = realpath($targetDir); + $ret = -1; + if (str_ends_with($updateArchive, '.zip')) { + system('unzip ' . escapeshellarg($archivePath) . ' -d ' . escapeshellarg($targetPath), $ret); + } else { + system('tar -xf ' . escapeshellarg($archivePath) . ' -C ' . escapeshellarg($targetPath), $ret); } - } - debugLog("... from directory $dirName in archive file"); - - if (!(file_exists($targetDir . '/' . $dirName) && is_dir($targetDir . '/' . $dirName))) { - trigger_error('The ZIP archive does not contain directory "churchtools", or creation failed!', - E_USER_ERROR); - if (function_exists('push')) { - push('[Fehler] ZIP', 'Das Verzeichnis "churchtools" fehlt im ZIP Archiv!', 1); + if ($ret !== 0) { + throw new Exception('Native extraction failed with error code ' . $ret . '!'); } - // cleanup - if (is_dir($targetDir . '/' . $dirName)) { - delTree($targetDir . '/' . $dirName); + } else { + if (str_ends_with($updateArchive, '.zip')) { + $zip = new ZipArchive(); + $zip->open($updateArchive); + $zip->extractTo($targetDir); + } else { + $tar = new PharData($updateArchive); + $tar->extractTo($targetDir); + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($targetDir), + RecursiveIteratorIterator::LEAVES_ONLY + ); + /** @var SplFileInfo $file */ + foreach ($files as $file) { + $filename = $file->getFilename(); + $filenameLength = strlen($filename); + if ($filenameLength >= 99 && $filenameLength <= 100 && !str_contains($filename, '.')) { + throw new Exception('Detected filename with length >= 99 and without file extension.' + . ' This indicates an improperly extracted TAR archive. Exit update!'); + } + } } - throw new Exception('The ZIP archive does not contain directory "churchtools", or creation failed!'); } - // Check if directory system and index.php exist, if yes, rename them for backup - makeBackup(); - debugLog("Delete old files and dirs..."); - if (file_exists($targetDir . '/system')) { - delTree($targetDir . '/system'); - } - if (file_exists($targetDir . '/index.php')) { - unlink($targetDir . '/index.php'); + $needle = 'churchtools'; + foreach (scandir($targetDir) as $entry) { + if (str_starts_with($entry, $needle)) { + $updateDir = $targetDir . '/' . $entry; + break; + } } - - debugLog("Move new files/dirs to $targetDir..."); - rename($targetDir . '/' . $dirName . '/system', $targetDir . '/system'); - rename($targetDir . '/' . $dirName . '/index.php', $targetDir . '/index.php'); - delTree($targetDir . '/' . $dirName); - - if (function_exists('push')) { - push('Update erfolgreich', 'Ein Update wurde erfolgreich installiert: ' . $version . '!'); + if (!(isset($updateDir) && file_exists($updateDir) && is_dir($updateDir))) { + throw new Exception('Archive does not contain a directory starting with "churchtools", or creation failed!'); } + debugLog("Found extracted update directory $updateDir"); - debugLog("Remove updateArchive..."); - if (file_exists($updateArchive)) { - unlink($updateArchive); - } + return $updateDir; } -function debugLog($msg) { +function debugLog($msg): void { if (DEBUG) { echo "$msg\n"; } diff --git a/push.inc.php b/push.inc.php index 930d8a8..11c707f 100644 --- a/push.inc.php +++ b/push.inc.php @@ -1,82 +1,46 @@ '/']) . '.php'; -}); - // Set your ChurchTools URL. -define ('CT_URL', '...'); +const CT_URL = '...'; // Pushover, put in your own credentials below and set service to "true" if needed -define ('PUSHOVER_TOKEN', '...'); -define ('PUSHOVER_USER', '...'); -define ('PUSHOVER', false); - -/* - Pushbullet, put in your own credentials below and set service to "true" if needed - - Our pushbullet integrations requires the pushbullet libary of ivkos: - https://github.com/ivkos/Pushbullet-for-PHP - Import the Pushbullet libary into the root of your webspace, - if you want to use this service and remove the hastag from the subsequent line. -*/ -# use Pushbullet\Pushbullet; -define ('PUSHBULLET_TOKEN', '...'); -define ('PUSHBULLET_CHANNEL', '...'); -define ('PUSHBULLET', false); +const PUSHOVER = false; +const PUSHOVER_TOKEN = '...'; +const PUSHOVER_USER = '...'; // Pushover Integration -function pushover($title, $message, $priority = 0, $retry = null, $expire = null) { - if (PUSHOVER) { - if(!isset($title, $message)) return false; - - $c = curl_init(); - curl_setopt($c, CURLOPT_URL, 'https://api.pushover.net/1/messages.xml'); - curl_setopt($c, CURLOPT_HEADER, false); - curl_setopt($c, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($c, CURLOPT_RETURNTRANSFER, true); - curl_setopt($c, CURLOPT_POSTFIELDS, array( - 'token' => PUSHOVER_TOKEN, - 'user' => PUSHOVER_USER, - 'title' => $title, - 'message' => $message, - 'html' => 1, - 'device' => '', - 'priority' => $priority, - 'timestamp' => time(), - 'expire' => $expire, - 'retry' => $retry, - 'callback' => '', - 'url' => CT_URL, - 'sound' => '', - 'url_title' => CT_URL - )); - $response = curl_exec($c); - - $xml = simplexml_load_string($response); - return ($xml->status == 1) ? true : false; +function pushover($title, $message, $priority = 0, $retry = null, $expire = null): bool { + if(!isset($title, $message)) { + return false; } + $c = curl_init(); + curl_setopt($c, CURLOPT_URL, 'https://api.pushover.net/1/messages.xml'); + curl_setopt($c, CURLOPT_HEADER, false); + curl_setopt($c, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($c, CURLOPT_RETURNTRANSFER, true); + curl_setopt($c, CURLOPT_POSTFIELDS, array( + 'token' => PUSHOVER_TOKEN, + 'user' => PUSHOVER_USER, + 'title' => $title, + 'message' => $message, + 'html' => 1, + 'device' => '', + 'priority' => $priority, + 'timestamp' => time(), + 'expire' => $expire, + 'retry' => $retry, + 'callback' => '', + 'url' => CT_URL, + 'sound' => '', + 'url_title' => CT_URL + )); + $response = curl_exec($c); + $xml = simplexml_load_string($response); + return $xml->status == 1; } -// Pushbullet Integration -function pushbullet($title, $message) { - if ('PUSHBULLET') { - $push = new Pushbullet(PUSHBULLET_TOKEN); - $channel = $push->channel(PUSHBULLET_CHANNEL); - - $channel->pushNote($title, $message); +function push($title, $message, $priority = 0, $retry = null, $expire = null): void { + if (PUSHOVER) { + pushover($title, $message, $priority, $retry, $expire); } } - -// Combined Integration -function push($title, $message, $priority = 0, $retry = null, $expire = null) { - pushover($title, $message, $priority, $retry, $expire); - pushbullet(strip_tags($title), strip_tags($message)); -} - -function clientIp() { - if (! isset($_SERVER['HTTP_X_FORWARDED_FOR'])) $client_ip = $_SERVER['REMOTE_ADDR']; - else $client_ip = $_SERVER['HTTP_X_FORWARDED_FOR']; - return $client_ip; -}