mirror of
https://github.com/maxpozdeev/mytinytodo.git
synced 2026-03-11 08:55:27 +00:00
+ add basic support of extensions (can activate/deactivate in settings and load it) (experimental yet)
This commit is contained in:
parent
46c6e8f525
commit
c54c9ab774
8 changed files with 289 additions and 75 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
162
src/includes/classes.php
Normal file
162
src/includes/classes.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -180,6 +180,9 @@
|
|||
"set_appearance": "Тема оформления",
|
||||
"set_appearance_system": "Как в системе",
|
||||
"set_appearance_light": "Светлая",
|
||||
"set_extensions": "Расширения",
|
||||
"set_activate": "Активировать",
|
||||
"set_deactivate": "Деактивировать",
|
||||
"confirmDelete": "Вы действительно хотите удалить задачу?",
|
||||
"confirmLeave": "На странице могут быть несохраненные данные. Вы действительно хотите закрыть страницу?",
|
||||
"actionNoteSave": "сохранить",
|
||||
|
|
|
|||
26
src/init.php
26
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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 .= "<a href='#' data-settings-link='ext-deactivate' data-ext='". htmlspecialchars($ext). "'>". __('set_deactivate', true). '</a>';
|
||||
}
|
||||
else {
|
||||
$out .= "<a href='#' data-settings-link='ext-activate' data-ext='". htmlspecialchars($ext). "'>". __('set_activate', true). '</a>';
|
||||
}
|
||||
$a[] = $out;
|
||||
}
|
||||
print( implode("<br>\n", $a) );
|
||||
}
|
||||
|
||||
header('Content-type:text/html; charset=utf-8');
|
||||
?>
|
||||
|
||||
|
|
@ -283,6 +337,18 @@ header('Content-type:text/html; charset=utf-8');
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if (defined('MTT_ENABLE_EXT') && MTT_ENABLE_EXT) {
|
||||
?>
|
||||
<div class="tr">
|
||||
<div class="th"><?php _e('set_extensions');?>:</div>
|
||||
<div class="td"> <?php listExtensions(); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="form-bottom-buttons">
|
||||
<button type="submit"><?php _e('set_submit'); ?></button>
|
||||
<button type="button" class="mtt-back-button"><?php _e('set_cancel'); ?></button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue