diff --git a/src/ext/updater/.htaccess b/src/ext/updater/.htaccess new file mode 100644 index 0000000..8d2f256 --- /dev/null +++ b/src/ext/updater/.htaccess @@ -0,0 +1 @@ +deny from all diff --git a/src/ext/updater/class.controller.php b/src/ext/updater/class.controller.php new file mode 100644 index 0000000..35c51f9 --- /dev/null +++ b/src/ext/updater/class.controller.php @@ -0,0 +1,66 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +namespace UpdaterExtension; + +use \UpdaterExtension; +use \Config; + +class Controller extends \ApiController +{ + function postCheck() + { + $prefs = UpdaterExtension::preferences(); + $a = UpdaterExtension::lastVersionInfo(); + if ($a) { + $prefs['lastCheck'] = time(); + $prefs['version'] = $a['version'] ?? ''; + $prefs['download'] = $a['download'] ?? ''; + Config::saveDomain(UpdaterExtension::domain, $prefs); + } + $this->response->data = [ 'total' => 1 ]; + } + + function postUpdate() + { + $prefs = UpdaterExtension::preferences(); + $url = $prefs['download'] ?? ''; + if ($url == '') { + $this->response->data = [ 'total' => 0, 'msg' => __("updater.download_error") ]; + return; + } + $file = MTTPATH. 'update.tar.gz'; + $error = null; + if (!UpdaterExtension::download($url, $file, $error)) { + $this->response->data = [ 'total' => 0, 'msg' => __("updater.download_error"), 'details' => $error ]; + return; + } + $error = null; + if (!UpdaterExtension::extractAndReplace($file, $error)) { + $this->response->data = [ 'total' => 0, 'msg' => __("updater.update_error"), 'details' => $error ]; + return; + } + @unlink($file); + + if (function_exists("opcache_reset")) { + opcache_reset(); + } + + // TODO: need to run post-update by new version + // ... + + $prefs['version'] = ''; + $prefs['download'] = ''; + $prefs['lastCheck'] = 0; + Config::saveDomain(UpdaterExtension::domain, $prefs); + + $this->response->data = [ 'total' => 1, 'msg' => __("updater.updated"), 'reload' => 'ext-settings' ]; + } + + +} diff --git a/src/ext/updater/extension.json b/src/ext/updater/extension.json new file mode 100644 index 0000000..88f2086 --- /dev/null +++ b/src/ext/updater/extension.json @@ -0,0 +1,6 @@ +{ + "bundleId": "updater", + "name": "Updates", + "version": "0.9", + "description": "myTinyTodo self-updater" +} diff --git a/src/ext/updater/lang/en.json b/src/ext/updater/lang/en.json new file mode 100644 index 0000000..a2ab622 --- /dev/null +++ b/src/ext/updater/lang/en.json @@ -0,0 +1,13 @@ +{ + "ext.updater.name": "Updates", + "updater.h_check_updates": "Check updates", + "updater.check": "Check", + "updater.no_updates": "No updates available", + "updater.current_version": "Current version", + "updater.last_checked": "Last checked", + "updater.updatet_version_avaialable": "Updated version is available", + "updater.update": "Update", + "updater.updated": "Update has completed", + "updater.download_error": "Download failed", + "updater.update_error": "Update error" +} diff --git a/src/ext/updater/lang/ru.json b/src/ext/updater/lang/ru.json new file mode 100644 index 0000000..57a0a8a --- /dev/null +++ b/src/ext/updater/lang/ru.json @@ -0,0 +1,13 @@ +{ + "ext.updater.name": "Обновления", + "updater.h_check_updates": "Проверка обновлений", + "updater.check": "Проверить", + "updater.no_updates": "Нет обновлений", + "updater.current_version": "Текущая версия", + "updater.last_checked": "Последняя проверка", + "updater.updatet_version_avaialable": "Доступно обновление", + "updater.update": "Обновить", + "updater.updated": "Обновление завершено", + "updater.download_error": "Ошибка при загрузке", + "updater.update_error": "Ошибка при обновлении" +} diff --git a/src/ext/updater/loader.php b/src/ext/updater/loader.php new file mode 100644 index 0000000..93148ac --- /dev/null +++ b/src/ext/updater/loader.php @@ -0,0 +1,195 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +if (!defined('MTTPATH')) { + die("Unexpected usage."); +} + +require_once('class.controller.php'); + +function mtt_ext_updater_instance(): MTTExtension +{ + return new UpdaterExtension(); +} + +use UpdaterExtension\Controller; + +class UpdaterExtension extends MTTExtension implements MTTExtensionSettingsInterface, MTTHttpApiExtender +{ + //the same as dir name + const bundleId = 'updater'; + + // settings domain + const domain = "ext.updater.json"; + + function init() + { + } + + // MTTHttpApiExtender + function extendHttpApi(): array + { + return array( + '/check' => [ + 'POST' => [ Controller::class , 'postCheck' ], + ], + '/update' => [ + 'POST' => [ Controller::class , 'postUpdate' ], + ] + ); + } + + function settingsPage(): string + { + $e = function($s) { return __($s, true); }; + $ext = htmlspecialchars(self::bundleId); + $prefs = self::preferences(); + $lastCheck = $prefs['lastCheck'] ?? 0; + $version = $prefs['version'] ?? ''; + $updateStr = ''; + $curVersion = htmlspecialchars(mytinytodo\Version::VERSION); + if (time() - $lastCheck > 86400*7) { + $a = self::lastVersionInfo(); + if ($a) { + $lastCheck = $prefs['lastCheck'] = time(); + $version = $prefs['version'] = $a['version'] ?? ''; + $prefs['download'] = $a['download'] ?? ''; + Config::saveDomain(self::domain, $prefs); + } + } + if ($version != '') { + if ( version_compare($version, mytinytodo\Version::VERSION) > 0 ) { + $updateStr = "
{$e('updater.updatet_version_avaialable')}: ". htmlspecialchars($version); + if ("1.7." == substr($version, 0, 4)) { + $updateStr .= "

\n {$e('updater.update')} "; + } + } + else { + $updateStr = "
{$e('updater.no_updates')}"; + } + } + $lastCheckStr = $lastCheck ? timestampToDatetime($lastCheck, true) : ""; + + + return +<< +
{$e('updater.h_check_updates')}
+
+ {$e('updater.current_version')}: $curVersion
+ {$e('updater.last_checked')}: $lastCheckStr  
+ $updateStr +
+ +EOD; + } + + function saveSettings(array $params, ?string &$outMessage): bool + { + return true; + } + + + + 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; + } + + +}