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');
+
+