diff --git a/src/ext/updater/class.controller.php b/src/ext/updater/class.controller.php index b5a038b..1658ebd 100644 --- a/src/ext/updater/class.controller.php +++ b/src/ext/updater/class.controller.php @@ -2,7 +2,7 @@ /* This file is a part of myTinyTodo. - (C) Copyright 2022 Max Pozdeev + (C) Copyright 2022-2023 Max Pozdeev Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ @@ -16,14 +16,22 @@ class Controller extends \ApiController function postCheck() { $prefs = UpdaterExtension::preferences(); - $a = UpdaterExtension::lastVersionInfo(); + $updater = new Updater; + $a = $updater->lastVersionInfo(); if ($a) { $prefs['lastCheck'] = time(); $prefs['version'] = $a['version'] ?? ''; $prefs['download'] = $a['download'] ?? ''; Config::saveDomain(UpdaterExtension::domain, $prefs); + $this->response->data = [ 'total' => 1 ]; + } + else { + $this->response->data = [ + 'total' => 0, + 'msg' => __("error"), + 'details' => $updater->lastErrorString ?? '' + ]; } - $this->response->data = [ 'total' => 1 ]; } function postUpdate() @@ -34,15 +42,22 @@ class Controller extends \ApiController $this->response->data = [ 'total' => 0, 'msg' => __("updater.download_error") ]; return; } + $updater = new Updater; $file = MTTPATH. 'update.tar.gz'; - $error = null; - if (!UpdaterExtension::download($url, $file, $error)) { - $this->response->data = [ 'total' => 0, 'msg' => __("updater.download_error"), 'details' => $error ]; + if (!$updater->download($url, $file)) { + $this->response->data = [ + 'total' => 0, + 'msg' => __("updater.download_error"), + 'details' => $updater->lastErrorString ?? '' + ]; return; } - $error = null; - if (!UpdaterExtension::extractAndReplace($file, $error)) { - $this->response->data = [ 'total' => 0, 'msg' => __("updater.update_error"), 'details' => $error ]; + if (!$updater->extractAndReplace($file)) { + $this->response->data = [ + 'total' => 0, + 'msg' => __("updater.update_error"), + 'details' => $updater->lastErrorString ?? '' + ]; return; } @unlink($file); diff --git a/src/ext/updater/class.updater.php b/src/ext/updater/class.updater.php new file mode 100644 index 0000000..5f32b9c --- /dev/null +++ b/src/ext/updater/class.updater.php @@ -0,0 +1,118 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +namespace UpdaterExtension; + +class Updater +{ + public $lastErrorString = null; + + public function lastVersionInfo(): ?array + { + $options = array( + 'http' => array( + 'header' => "Content-type: application/json\r\nUser-Agent: mytinytodo\r\n" + ) + ); + $context = stream_context_create($options); + set_error_handler(function ($errno, $message, $file, $line) { + throw new \ErrorException($message, $errno, $errno, $file, $line); + }); + $json = null; + $this->lastErrorString = null; + try { + $json = @file_get_contents("https://api.github.com/repos/maxpozdeev/mytinytodo/releases/latest", false, $context); + } + catch (\Exception $e) { + $this->lastErrorString = boolval(ini_get('html_errors')) ? htmlspecialchars_decode($e->getMessage()) : $e->getMessage(); + } + restore_error_handler(); + if ($json === false || $json == '') { + error_log("Failed to get last version info: ".$this->lastErrorString); + return null; + } + $a = json_decode($json, true) ?? []; + $ret = []; + if ( isset($a['name']) && isset($a['assets']) && + is_array($a['assets']) && count($a['assets']) > 0 && + ($asset = $a['assets'][0]) && isset($asset['browser_download_url']) ) + { + $ret['version'] = substr($a['name'], 1); //remove first 'v' + $ret['download'] = $asset['browser_download_url']; + } + else { + error_log("HTTP response contains unexpected content"); + $this->lastErrorString = "HTTP response contains unexpected content"; + } + return $ret; + } + + public function download(string $url, string $outfile): bool + { + $this->lastErrorString = null; + $dir = dirname($outfile); + if (!is_dir($dir) || !is_writable($dir)) { + $this->lastErrorString = "myTinyTodo directory is not writable"; + return false; + } + $f = @fopen($url, 'r'); + if ($f === false) { + $ea = error_get_last(); + $this->lastErrorString = $ea['message'] ?? "Failed to open stream"; + return false; + } + $bytes = @file_put_contents($outfile, $f, LOCK_EX); + $ea = error_get_last(); + fclose($f); + if ($bytes === false) { + $this->lastErrorString = $ea['message'] ?? "Can not save file"; + return false; + } + return true; + } + + public function extractAndReplace(string $filename): bool + { + $this->lastErrorString = null; + $dir = MTTPATH; + if (!is_dir($dir) || !is_writable($dir)) { + $this->lastErrorString = "myTinyTodo directory is not writable"; + return false; + } + + $output = null; + $retval = null; + $command = "tar xzf ". escapeshellarg($filename). " --strip-components 1 -C ". escapeshellarg($dir). " 2>&1"; + @exec($command, $output, $retval); + if ($retval != 0) { + $this->lastErrorString = "Failed to execute tar command ($retval): ". ($output ? implode("\n", $output) : "no output"); + error_log($this->lastErrorString); + return false; + } + + // Extensions + $dir = MTT_EXT; + $filename = $dir . 'extensions.tar.gz'; + if (file_exists($filename)) { + if (!is_writable($dir)) { + $this->lastErrorString = "Extensions directory is not writable"; + return false; + } + $command = "tar xzf ". escapeshellarg($filename). " -C ". escapeshellarg($dir). " 2>&1"; + @exec($command, $output, $retval); + if ($retval != 0) { + $this->lastErrorString = "Extensions: failed to execute tar command ($retval): ". ($output ? implode("\n", $output) : "no output"); + error_log($this->lastErrorString); + return false; + } + unlink($filename); + } + + return true; + } +} diff --git a/src/ext/updater/extension.json b/src/ext/updater/extension.json index 88f2086..ef3fc49 100644 --- a/src/ext/updater/extension.json +++ b/src/ext/updater/extension.json @@ -1,6 +1,6 @@ { "bundleId": "updater", "name": "Updates", - "version": "0.9", + "version": "0.9.1", "description": "myTinyTodo self-updater" } diff --git a/src/ext/updater/lang/en.json b/src/ext/updater/lang/en.json index a2ab622..9418506 100644 --- a/src/ext/updater/lang/en.json +++ b/src/ext/updater/lang/en.json @@ -1,5 +1,7 @@ { "ext.updater.name": "Updates", + "updater.urlconfigwarning": "Enable PHP 'allow_url_fopen' directive to be able to download updates.", + "updater.tarwarning": "Update is not possible, 'tar' utility is not found.", "updater.h_check_updates": "Check updates", "updater.check": "Check", "updater.no_updates": "No updates available", diff --git a/src/ext/updater/lang/ru.json b/src/ext/updater/lang/ru.json index 57a0a8a..92e2ee7 100644 --- a/src/ext/updater/lang/ru.json +++ b/src/ext/updater/lang/ru.json @@ -1,5 +1,7 @@ { "ext.updater.name": "Обновления", + "updater.urlconfigwarning": "Для получения обновлений требуется включить директиву 'allow_url_fopen' в настройках PHP .", + "updater.tarwarning": "Исполняемый файл 'tar' не найден, обновление невозможно.", "updater.h_check_updates": "Проверка обновлений", "updater.check": "Проверить", "updater.no_updates": "Нет обновлений", diff --git a/src/ext/updater/loader.php b/src/ext/updater/loader.php index 93148ac..f7e0165 100644 --- a/src/ext/updater/loader.php +++ b/src/ext/updater/loader.php @@ -2,7 +2,7 @@ /* This file is a part of myTinyTodo. - (C) Copyright 2022 Max Pozdeev + (C) Copyright 2022-2023 Max Pozdeev Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. */ @@ -11,6 +11,7 @@ if (!defined('MTTPATH')) { } require_once('class.controller.php'); +require_once('class.updater.php'); function mtt_ext_updater_instance(): MTTExtension { @@ -18,6 +19,7 @@ function mtt_ext_updater_instance(): MTTExtension } use UpdaterExtension\Controller; +use UpdaterExtension\Updater; class UpdaterExtension extends MTTExtension implements MTTExtensionSettingsInterface, MTTHttpApiExtender { @@ -53,31 +55,50 @@ class UpdaterExtension extends MTTExtension implements MTTExtensionSettingsInter $version = $prefs['version'] ?? ''; $updateStr = ''; $curVersion = htmlspecialchars(mytinytodo\Version::VERSION); + $err = null; if (time() - $lastCheck > 86400*7) { - $a = self::lastVersionInfo(); + $updater = new Updater; + $a = $updater->lastVersionInfo(); if ($a) { $lastCheck = $prefs['lastCheck'] = time(); $version = $prefs['version'] = $a['version'] ?? ''; $prefs['download'] = $a['download'] ?? ''; Config::saveDomain(self::domain, $prefs); } + else { + $err = $updater->lastErrorString; + } } + $warning = ''; if ($version != '') { if ( version_compare($version, mytinytodo\Version::VERSION) > 0 ) { $updateStr = "
{$e('updater.updatet_version_avaialable')}: ". htmlspecialchars($version); + # allow update to v1.7.x only if ("1.7." == substr($version, 0, 4)) { $updateStr .= "

\n {$e('updater.update')} "; } + $retval = 0; + $output = null; + unset($output); + @exec('tar --version', $output, $retval); + if ($retval != 0) { + $warning = "
⚠️ {$e('updater.tarwarning')}
"; + } } else { $updateStr = "
{$e('updater.no_updates')}"; } } - $lastCheckStr = $lastCheck ? timestampToDatetime($lastCheck, true) : ""; + $lastCheckStr = $err ? $e('updater.download_error') : ($lastCheck ? timestampToDatetime($lastCheck, true) : ""); + + if (!boolval(ini_get('allow_url_fopen'))) { + $warning .= "
⚠️ {$e('updater.urlconfigwarning')}
"; + } return <<
{$e('updater.h_check_updates')}
@@ -95,101 +116,9 @@ EOD; } - static function preferences(): array { $prefs = Config::requestDomain(self::domain); return $prefs; } - - static function lastVersionInfo(): ?array - { - $options = array( - 'http' => array( - 'header' => "Content-type: application/json\r\nUser-Agent: mytinytodo\r\n" - ) - ); - $context = stream_context_create($options); - $json = @file_get_contents("https://api.github.com/repos/maxpozdeev/mytinytodo/releases/latest", false, $context); - if ($json === false || $json == '') { - return null; - } - $a = json_decode($json, true) ?? []; - $ret = []; - if ( isset($a['name']) && isset($a['assets']) && - is_array($a['assets']) && count($a['assets']) > 0 && - ($asset = $a['assets'][0]) && isset($asset['browser_download_url']) ) - { - $ret['version'] = substr($a['name'], 1); //remove first 'v' - $ret['download'] = $asset['browser_download_url']; - } - else { - error_log("Unexpected content"); - } - return $ret; - } - - static function download(string $url, string $outfile, string &$error = null): bool - { - $dir = dirname($outfile); - if (!is_dir($dir) || !is_writable($dir)) { - $error = "myTinyTodo directory is not writable"; - return false; - } - $f = @fopen($url, 'r'); - if ($f === false) { - $ea = error_get_last(); - $error = ($ea && isset($ea['message'])) ? $ea['message'] : "Failed to open stream"; - return false; - } - $bytes = @file_put_contents($outfile, $f, LOCK_EX); - $ea = error_get_last(); - fclose($f); - if ($bytes === false) { - $error = ($ea && isset($ea['message'])) ? $ea['message'] : "Can not save file"; - return false; - } - return true; - } - - static function extractAndReplace(string $filename, string &$error = null): bool - { - $dir = MTTPATH; - if (!is_dir($dir) || !is_writable($dir)) { - $error = "myTinyTodo directory is not writable"; - return false; - } - - $output = null; - $retval = null; - $command = "tar xzf ". escapeshellarg($filename). " --strip-components 1 -C ". escapeshellarg($dir). " 2>&1"; - @exec($command, $output, $retval); - if ($retval != 0) { - $error = "Failed to execute tar command ($retval): ". ($output ? implode("\n", $output) : "no output"); - error_log($error); - return false; - } - - // Extensions - $dir = MTT_EXT; - $filename = $dir . 'extensions.tar.gz'; - if (file_exists($filename)) { - if (!is_writable($dir)) { - $error = "Extensions directory is not writable"; - return false; - } - $command = "tar xzf ". escapeshellarg($filename). " -C ". escapeshellarg($dir). " 2>&1"; - @exec($command, $output, $retval); - if ($retval != 0) { - $error = "Extensions: failed to execute tar command ($retval): ". ($output ? implode("\n", $output) : "no output"); - error_log($error); - return false; - } - unlink($filename); - } - - return true; - } - - }