+ add updater extension

This commit is contained in:
maxpozdeev 2022-11-29 20:56:23 +03:00
parent d36bab9e16
commit 56fa0fa039
6 changed files with 294 additions and 0 deletions

View file

@ -0,0 +1 @@
deny from all

View file

@ -0,0 +1,66 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
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' ];
}
}

View file

@ -0,0 +1,6 @@
{
"bundleId": "updater",
"name": "Updates",
"version": "0.9",
"description": "myTinyTodo self-updater"
}

View file

@ -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"
}

View file

@ -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": "Ошибка при обновлении"
}

195
src/ext/updater/loader.php Normal file
View file

@ -0,0 +1,195 @@
<?php declare(strict_types=1);
/*
This file is a part of myTinyTodo.
(C) Copyright 2022 Max Pozdeev <maxpozdeev@gmail.com>
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 = "<br> {$e('updater.updatet_version_avaialable')}: ". htmlspecialchars($version);
if ("1.7." == substr($version, 0, 4)) {
$updateStr .= "<br><br>\n <a href=\"#\" data-ext-settings-action=\"post:update\" data-ext=\"$ext\">{$e('updater.update')}</a> ";
}
}
else {
$updateStr = "<br>{$e('updater.no_updates')}";
}
}
$lastCheckStr = $lastCheck ? timestampToDatetime($lastCheck, true) : "";
return
<<<EOD
<div class="tr">
<div class="th"> {$e('updater.h_check_updates')} </div>
<div class="td">
{$e('updater.current_version')}: $curVersion <br>
{$e('updater.last_checked')}: $lastCheckStr &nbsp; <button type=button data-ext-settings-action="post:check" data-ext="$ext">{$e('updater.check')}</button> <br>
$updateStr
</div>
</div>
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;
}
}