diff --git a/src/content/mytinytodo.js b/src/content/mytinytodo.js index e1db3bb..b8eb01f 100644 --- a/src/content/mytinytodo.js +++ b/src/content/mytinytodo.js @@ -590,11 +590,14 @@ var mytinytodo = window.mytinytodo = _mtt = { // Settings - $("a[data-settings-link]").click(function(event){ + $(document).on('click', 'a[data-settings-link]', function(event) { var settingsPage = this.dataset.settingsLink; if (settingsPage == 'index') { showSettings( (event.metaKey || event.ctrlKey) ? 1 : 0 ); } + else if (settingsPage == 'ext-activate' || settingsPage == 'ext-deactivate') { + activateExtension(settingsPage == 'ext-activate' ? true : false, this.dataset.ext); + } return false; }); @@ -2631,6 +2634,19 @@ function saveSettings(frm) }, 'json'); } +function activateExtension(activate, ext) +{ + var params = { + 'activate': activate ? 1 : 0, + 'ext': ext + } + $.post(_mtt.mttUrl+'settings.php', params, function(json){ + if(json.saved) { + flashInfo(_mtt.lang.get('settingsSaved')); + showSettings(0); + } + }, 'json'); +} /* * Dialogs diff --git a/src/includes/class.config.php b/src/includes/class.config.php index b36f939..ea47a2e 100644 --- a/src/includes/class.config.php +++ b/src/includes/class.config.php @@ -86,6 +86,9 @@ class Config # Appearance: system default or always light 'appearance' => array('default'=>'system', 'type'=>'s', 'options'=>array('system','light')), + + # Array of activated extensions + 'extensions' => array('default'=>[], 'type'=>'a') ); /** @var mixed[] */ @@ -193,8 +196,15 @@ class Config elseif ( isset($v['options']) && !in_array(self::$config[$param], $v['options'])) $val = $v['default']; else $val = self::$config[$param]; - if ($v['type']=='i') $val = (int)$val; - else $val = strval($val); + if ($v['type'] == 'i') { + $val = (int)$val; + } + else if ($v['type'] == 'a') { + if (!is_array($val)) $val = []; + } + else { + $val = strval($val); + } $j[$param] = $val; } diff --git a/src/includes/classes.php b/src/includes/classes.php new file mode 100644 index 0000000..7297d29 --- /dev/null +++ b/src/includes/classes.php @@ -0,0 +1,162 @@ + + Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. +*/ + +class ApiRequest +{ + public $path; + public $method; + public $contentType; + public $jsonBody; + + function __construct() { + $this->path = $_SERVER['PATH_INFO'] ?? ''; + $this->method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + $this->contentType = $_SERVER['CONTENT_TYPE'] ?? ''; + } + + function decodeJsonBody() { + $this->jsonBody = json_decode( file_get_contents('php://input'), true, 10, JSON_INVALID_UTF8_SUBSTITUTE ); + return $this->jsonBody; + } +} + +class ApiResponse +{ + public $data = null; + public $contentType = 'application/json'; + public $code = null; + + function htmlContent(string $content, int $code = 200): ApiResponse + { + $this->contentType = 'text/html'; + $this->data = $content; + $this->code = $code; + return $this; + } + + function exit() + { + if (is_null($this->data) && is_null($this->code)) { + http_response_code(404); + } + if (!is_null($this->code)) { + http_response_code($this->code); + } + if ($this->contentType == 'text/html') { + print $this->data; + exit(); + } + jsonExit($this->data); + } +} + +abstract class ApiController +{ + /** @var ApiRequest */ + protected $req; + + /** @var ApiResponse */ + protected $response; + + function __construct(ApiRequest $req, ApiResponse $response) { + $this->req = $req; + $this->response = $response; + } +} + + + +abstract class MTTExtension +{ + const codename = ''; + const title = ''; + abstract function init(); +} + + + + +class MTTExtensionLoader +{ + private static $exts = []; + + public static function loadExtension(string $ext) + { + if (isset(self::$exts[$ext])) { + error_log("Extension '$ext' is already registered"); + return; + } + + $loader = MTT_EXT. $ext. '/loader.php'; + if (!file_exists($loader)) { + error_log("Failed to init extension '$ext': no loader.php"); + return; + } + + require_once(MTT_EXT. $ext. '/loader.php'); + $getInstance = 'mtt_ext_'. $ext. '_instance'; + + if (!function_exists($getInstance)) { + throw new Exception("Failed to init extension '$ext': no '$getInstance' function"); + } + + $instance = $getInstance(); + if ( ! ($instance instanceof MTTExtension) ) { + throw new Exception("Failed to init extension '$ext': incompatible instance"); + } + + $className = get_class($instance); + if (!defined("$className::codename") || !defined("$className::title")) { + throw new Exception("Failed to register extension '$ext': require class constants (codename, title)"); + } + if ($instance::codename != $ext) { + throw new Exception("Extension '$ext' codename does not equal to extension dir"); + } + + $instance->init(); + self::$exts[$ext] = $instance; + } + + /** + * @return MTTExtension[] + */ + public static function registeredExtensions(): array + { + $a = []; + foreach (self::$exts as $ext => $instance) { + $a[] = $instance; + } + return $a; + } + + /** + * @return string[] + */ + public static function bundles(): array + { + $a = []; + $files = array_diff(scandir(MTT_EXT) ?? [], ['.', '..']); + foreach ($files as $ext) { + if ( !is_dir(MTT_EXT. $ext) || !file_exists(MTT_EXT. $ext. '/loader.php') ) { + continue; + } + $a[] = $ext; + } + return $a; + } + + public static function extensionInstance(string $ext): ?MTTExtension + { + return self::$exts[$ext] ?? null; + } + + public static function isRegistered(string $ext): bool + { + return isset(self::$exts[$ext]); + } +} diff --git a/src/includes/common_classes.php b/src/includes/common_classes.php deleted file mode 100644 index 5fb36dd..0000000 --- a/src/includes/common_classes.php +++ /dev/null @@ -1,70 +0,0 @@ - - Licensed under the GNU GPL version 2 or any later. See file COPYRIGHT for details. -*/ - -class ApiRequest -{ - public $path; - public $method; - public $contentType; - public $jsonBody; - - function __construct() { - $this->path = $_SERVER['PATH_INFO'] ?? ''; - $this->method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; - $this->contentType = $_SERVER['CONTENT_TYPE'] ?? ''; - } - - function decodeJsonBody() { - $this->jsonBody = json_decode( file_get_contents('php://input'), true, 10, JSON_INVALID_UTF8_SUBSTITUTE ); - return $this->jsonBody; - } -} - -class ApiResponse -{ - public $data = null; - public $contentType = 'application/json'; - public $code = null; - - function htmlContent(string $content, int $code = 200): ApiResponse - { - $this->contentType = 'text/html'; - $this->data = $content; - $this->code = $code; - return $this; - } - - function exit() - { - if (is_null($this->data) && is_null($this->code)) { - http_response_code(404); - } - if (!is_null($this->code)) { - http_response_code($this->code); - } - if ($this->contentType == 'text/html') { - print $this->data; - exit(); - } - jsonExit($this->data); - } -} - -abstract class ApiController -{ - /** @var ApiRequest */ - protected $req; - - /** @var ApiResponse */ - protected $response; - - function __construct(ApiRequest $req, ApiResponse $response) { - $this->req = $req; - $this->response = $response; - } -} diff --git a/src/includes/lang/en.json b/src/includes/lang/en.json index 4f64cad..ca7c416 100644 --- a/src/includes/lang/en.json +++ b/src/includes/lang/en.json @@ -180,6 +180,9 @@ "set_appearance": "Appearance", "set_appearance_system": "Same as system", "set_appearance_light": "Light theme", + "set_extensions": "Extensions", + "set_activate": "Activate", + "set_deactivate": "Deactivate", "confirmDelete": "Are you sure you want to delete the task?", "confirmLeave": "There can be unsaved data. Do you really want to leave?", "actionNoteSave": "save", diff --git a/src/includes/lang/ru.json b/src/includes/lang/ru.json index c2b86c3..8bd7924 100644 --- a/src/includes/lang/ru.json +++ b/src/includes/lang/ru.json @@ -180,6 +180,9 @@ "set_appearance": "Тема оформления", "set_appearance_system": "Как в системе", "set_appearance_light": "Светлая", + "set_extensions": "Расширения", + "set_activate": "Активировать", + "set_deactivate": "Деактивировать", "confirmDelete": "Вы действительно хотите удалить задачу?", "confirmLeave": "На странице могут быть несохраненные данные. Вы действительно хотите закрыть страницу?", "actionNoteSave": "сохранить", diff --git a/src/init.php b/src/init.php index 6016961..b856cf7 100644 --- a/src/init.php +++ b/src/init.php @@ -25,7 +25,7 @@ else { } require_once(MTTINC. 'common.php'); -require_once(MTTINC. 'common_classes.php'); +require_once(MTTINC. 'classes.php'); require_once(MTTINC. 'version.php'); require_once(MTTINC. 'class.dbconnection.php'); require_once(MTTINC. 'class.dbcore.php'); @@ -58,6 +58,11 @@ if (need_auth() && !isset($dontStartSession)) { setup_and_start_session(); } +if (defined('MTT_ENABLE_EXT') && MTT_ENABLE_EXT) { + define('MTT_EXT', MTTPATH . 'ext/'); + loadExtensions(); +} + function requireConfig() { @@ -340,3 +345,22 @@ function logAndDie($userText, $errText = null) } exit(1); } + +function loadExtensions() +{ + $a = Config::get('extensions'); + if (!$a || !is_array($a)) { + return; + } + foreach ($a as $ext) { + if (is_string($ext)) { + try { + MTTExtensionLoader::loadExtension($ext); + } + catch (Exception $e) { + error_log($e->getMessage()); + } + + } + } +} diff --git a/src/settings.php b/src/settings.php index a7c0de9..f303ac9 100644 --- a/src/settings.php +++ b/src/settings.php @@ -24,7 +24,7 @@ if(isset($_POST['save'])) Config::set('lang', _post('lang')); // in Demo mode we can set only language by cookies - if(defined('MTTDEMO')) { + if (defined('MTTDEMO')) { setcookie('lang', Config::get('lang'), 0, url_dir(Config::get('url')=='' ? getRequestUri() : Config::getUrl('url'))); $t['saved'] = 1; jsonExit($t); @@ -56,6 +56,42 @@ if(isset($_POST['save'])) $t['saved'] = 1; jsonExit($t); } +else if (isset($_POST['activate'])) +{ + check_token(); + + $t = array('saved'=>0); + + // in Demo mode we do nothing + if (defined('MTTDEMO')) { + $t['saved'] = 1; + jsonExit($t); + } + + $activate = (int)_post('activate'); + $ext = _post('ext'); + + $exts = MTTExtensionLoader::bundles(); + if (in_array($ext, $exts)) { + $a = Config::get('extensions'); + if (!is_array($a)) $a = []; + if ($activate) { + try { + MTTExtensionLoader::loadExtension($ext); + $a[] = $ext; + } + catch (Exception $e) { + http_response_code(500); + logAndDie($e->getMessage()); + } + } + else $a = array_diff($a, [$ext]); + Config::set('extensions', $a); + Config::save(); + } + $t['saved'] = 1; + jsonExit($t); +} function _c($key) { @@ -144,6 +180,24 @@ function timezoneIdentifiers() return $a; } +function listExtensions() +{ + $exts = MTTExtensionLoader::bundles(); + $activatedExts = Config::get('extensions'); + if (!is_array($activatedExts)) $activatedExts = []; + foreach ($exts as $ext) { + $out = "$ext "; + if (in_array($ext, $activatedExts)) { + $out .= "". __('set_deactivate', true). ''; + } + else { + $out .= "". __('set_activate', true). ''; + } + $a[] = $out; + } + print( implode("
\n", $a) ); +} + header('Content-type:text/html; charset=utf-8'); ?> @@ -283,6 +337,18 @@ header('Content-type:text/html; charset=utf-8'); + +
+
:
+
+
+
+ +