1
0
mirror of https://github.com/typemill/typemill.git synced 2025-08-05 05:37:45 +02:00

Settings area V2

This commit is contained in:
trendschau
2022-12-06 21:26:30 +01:00
parent f1a2bbb673
commit e0b1a0a94f
47 changed files with 5121 additions and 1797 deletions

View File

@@ -5,17 +5,8 @@ namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
class ControllerSystemApi extends ControllerData
class ControllerApiGlobal extends ControllerData
{
public function getSettings(Request $request, Response $response)
{
$response->getBody()->write(json_encode([
'settings' => $this->settings
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
public function getSystemNavi(Request $request, Response $response)
{
# won't work because api has no session, instead you have to pass user
@@ -34,4 +25,13 @@ class ControllerSystemApi extends ControllerData
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
public function getTranslations(Request $request, Response $response)
{
$response->getBody()->write(json_encode([
'translations' => $this->c->get('translations'),
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\Validation;
use Typemill\Models\Yaml;
use Typemill\Models\User;
class ControllerApiSystemPlugins extends ControllerData
{
public function updatePlugin(Request $request, Response $response)
{
# minimum permission are admin rights
if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update'))
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update settings.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$params = $request->getParsedBody();
$pluginname = $params['plugin'];
$plugininput = $params['settings'];
$yaml = new Yaml('\Typemill\Models\Storage');
$formdefinitions = $yaml->getYaml('plugins' . DIRECTORY_SEPARATOR . $pluginname, $pluginname . '.yaml');
# validate input
$validator = new Validation();
$this->recursiveValidation($formdefinitions['forms']['fields'], $plugininput, $validator, $themeOrPlugin = 'plugins', $name = $pluginname);
if(!empty($this->errors))
{
$response->getBody()->write(json_encode([
'message' => 'Please correct tbe errors in form.',
'errors' => $this->errors
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
# store updated settings here
$yaml->updateYaml('settings', 'settings.yaml', $this->settings);
$response->getBody()->write(json_encode([
'message' => 'settings have been saved',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\Validation;
use Typemill\Models\Yaml;
use Typemill\Models\User;
# how to translate results in API call ???
# we should translate in backend instead of twig or vue
class ControllerApiSystemSettings extends ControllerData
{
public function getSettings(Request $request, Response $response)
{
# minimum permission are admin rights
if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update'))
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update settings.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$response->getBody()->write(json_encode([
'settings' => $this->settings
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
public function updateSettings(Request $request, Response $response)
{
# minimum permission are admin rights
if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update'))
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update settings.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$params = $request->getParsedBody();
$settingsinput = $params['settings'];
$yaml = new Yaml('\Typemill\Models\Storage');
$formdefinitions = $yaml->getYaml('system/typemill/settings', 'system.yaml');
# validate input
$validator = new Validation();
$this->recursiveValidation($formdefinitions, $settingsinput, $validator);
if(!empty($this->errors))
{
$response->getBody()->write(json_encode([
'message' => 'Please correct errors in form.',
'errors' => $this->errors
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
# store updated settings here
$yaml->updateYaml('settings', 'settings.yaml', $this->settings);
$response->getBody()->write(json_encode([
'message' => 'settings have been saved',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\Validation;
use Typemill\Models\Yaml;
use Typemill\Models\User;
class ControllerApiSystemThemes extends ControllerData
{
public function updateTheme(Request $request, Response $response)
{
# minimum permission are admin rights
if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update'))
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update settings.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$params = $request->getParsedBody();
$themename = $params['theme'];
$themeinput = $params['settings'];
$yaml = new Yaml('\Typemill\Models\Storage');
$formdefinitions = $yaml->getYaml('themes' . DIRECTORY_SEPARATOR . $themename, $themename . '.yaml');
# validate input
$validator = new Validation();
$this->recursiveValidation($formdefinitions['forms']['fields'], $themeinput, $validator, $themeOrPlugin = 'themes', $name = $themename);
if(!empty($this->errors))
{
$response->getBody()->write(json_encode([
'message' => 'Please correct tbe errors in form.',
'errors' => $this->errors
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
# store updated settings here
$yaml->updateYaml('settings', 'settings.yaml', $this->settings);
$response->getBody()->write(json_encode([
'message' => 'settings have been saved',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
}

View File

@@ -0,0 +1,337 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\Validation;
use Typemill\Models\Yaml;
use Typemill\Models\User;
class ControllerApiSystemUsers extends ControllerData
{
# getCurrentUser
# getUserByName
#returns userdata
public function getUsersByNames($request, $response, $args)
{
# minimum permission are admin rights
if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update'))
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update settings.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$usernames = $request->getQueryParams()['usernames'] ?? false;
$user = new User();
$userdata = [];
if($usernames)
{
foreach($usernames as $username)
{
$existinguser = $user->setUser($username);
if($existinguser)
{
$userdata[] = $user->getUserData();
}
}
}
$response->getBody()->write(json_encode([
'userdata' => $userdata
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
# returns userdata
public function getUsersByEmail($request, $response, $args)
{
# minimum permission are admin rights
if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update'))
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update settings.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$email = $request->getQueryParams()['email'] ?? false;
$user = new User();
$userdata = [];
$usernames = $user->findUsersByEmail($email);
if($usernames)
{
foreach($usernames as $username)
{
$user->setUser($username);
$userdata[] = $user->getUserData();
}
}
$response->getBody()->write(json_encode([
'userdata' => $userdata
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
#returns userdata
public function getUsersByRole($request, $response, $args)
{
# minimum permission are admin rights
if(!$this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'system', 'update'))
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update settings.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$role = $request->getQueryParams()['role'] ?? false;
$user = new User();
$userdata = [];
$usernames = $user->findUsersByRole($role);
if($usernames)
{
foreach($usernames as $username)
{
$user->setUser($username);
$userdata[] = $user->getUserData();
}
}
$response->getBody()->write(json_encode([
'userdata' => $userdata
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
public function updateUser($request, $response, $args)
{
$params = $request->getParsedBody();
$userdata = $params['userdata'] ?? false;
$username = $params['userdata']['username'] ?? false;
$isAdmin = $this->c->get('acl')->isAllowed($request->getAttribute('userrole'), 'userlist', 'write');
if(!$userdata OR !$username)
{
$response->getBody()->write(json_encode([
'message' => 'Userdata and username is required.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
# if a non-admin-user tries to update another account
if(!$isAdmin AND ($username !== $request->getAttribute('username')) )
{
$response->getBody()->write(json_encode([
'message' => 'You are not allowed to update another user.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
# make sure that invalid password input is stripped out
if(isset($userdata['password']) && $userdata['password'] == '' )
{
unset($userdata['password']);
unset($userdata['newpassword']);
}
$user = new User();
# make sure you set a user with password when you update, otherwise it will delete the password completely
$user->setUserWithPassword($username);
$userfields = $this->getUserFields($request->getAttribute('userrole'));
# validate input
$validator = new Validation();
# loop through form-definitions, ignores everything that is not defined in yaml
foreach($userfields as $fieldname => $fielddefinitions)
{
# if there is no value for a field
if(!isset($userdata[$fieldname]))
{
continue;
}
# ignore readonly-fields
if(isset($fielddefinitions['readonly']) && ($fielddefinitions['readonly'] !== false) )
{
continue;
}
# new password needs special validation
if($fieldname == 'password')
{
$validationresult = $validator->newPassword($userdata);
if($validationresult === true)
{
# encrypt new password
$newpassword = $user->generatePassword($userdata['newpassword']);
# if input is valid, overwrite value in original user
$user->setValue('password', $newpassword);
}
else
{
$this->errors[$fieldname] = $validationresult[$fieldname][0];
}
}
else
{
# standard validation
$validationresult = $validator->field($fieldname, $userdata[$fieldname], $fielddefinitions);
if($validationresult === true)
{
# if input is valid, overwrite value in original user
$user->setValue($fieldname, $userdata[$fieldname]);
}
else
{
$this->errors[$fieldname] = $validationresult[$fieldname][0];
}
}
}
if(!empty($this->errors))
{
$response->getBody()->write(json_encode([
'message' => 'Please correct tbe errors in form.',
'errors' => $this->errors
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
if(!$user->updateUser())
{
$response->getBody()->write(json_encode([
'message' => $user->getError()
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(422);
}
$response->getBody()->write(json_encode([
'message' => 'User has been updated.'
]));
return $response->withHeader('Content-Type', 'application/json');
}
/*
public function updateUser($request, $response, $args)
{
# check if user is allowed to view (edit) userlist and other users
if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write'))
{
# if an editor tries to update other userdata than its own
if($_SESSION['user'] !== $userdata['username'])
{
return $response->withRedirect($this->c->router->pathFor('user.account'));
}
# non admins cannot change their userrole, so set it to session-value
$userdata['userrole'] = $_SESSION['role'];
}
$params = $request->getParams();
$userdata = $params['user'];
$user = new User();
$validate = new Validation();
$userroles = $this->c->acl->getRoles();
$redirectRoute = ($userdata['username'] == $_SESSION['user']) ? $this->c->router->pathFor('user.account') : $this->c->router->pathFor('user.show', ['username' => $userdata['username']]);
# validate standard fields for users
if($validate->existingUser($userdata, $userroles))
{
# validate custom input fields and return images
$userfields = $this->getUserFields($userdata['userrole']);
$imageFields = $this->validateInput('users', 'user', $userdata, $validate, $userfields);
if(!empty($imageFields))
{
$images = $request->getUploadedFiles();
if(isset($images['user']))
{
# set image size
$settings = $this->c->get('settings');
$imageSizes = $settings['images'];
$imageSizes['live'] = ['width' => 500, 'height' => 500];
$settings->replace(['images' => $imageSizes]);
$imageresult = $this->saveImages($imageFields, $userdata, $settings, $images['user']);
if(isset($_SESSION['slimFlash']['error']))
{
return $response->withRedirect($redirectRoute);
}
elseif(isset($imageresult['username']))
{
$userdata = $imageresult;
}
}
}
# check for errors and redirect to path, if errors found
if(isset($_SESSION['errors']))
{
$this->c->flash->addMessage('error', 'Please correct the errors');
return $response->withRedirect($redirectRoute);
}
if(empty($userdata['password']) AND empty($userdata['newpassword']))
{
# make sure no invalid passwords go into model
unset($userdata['password']);
unset($userdata['newpassword']);
$user->updateUser($userdata);
$this->c->flash->addMessage('info', 'Saved all changes');
return $response->withRedirect($redirectRoute);
}
elseif($validate->newPassword($userdata))
{
$userdata['password'] = $userdata['newpassword'];
unset($userdata['newpassword']);
$user->updateUser($userdata);
$this->c->flash->addMessage('info', 'Saved all changes');
return $response->withRedirect($redirectRoute);
}
}
# change error-array for formbuilder
$errors = $_SESSION['errors'];
unset($_SESSION['errors']);
$_SESSION['errors']['user'] = $errors;#
$this->c->flash->addMessage('error', 'Please correct your input');
return $response->withRedirect($redirectRoute);
}
}
*/
}

View File

@@ -12,6 +12,8 @@ use Typemill\Events\OnSystemnaviLoaded;
class ControllerData extends Controller
{
protected $errors = [];
protected function getMainNavigation($userrole)
{
$yaml = new Yaml('\Typemill\Models\Storage');
@@ -27,11 +29,11 @@ class ControllerData extends Controller
if($acl->isAllowed($userrole, $naviitem['aclresource'], $naviitem['aclprivilege']))
{
# not nice: check if the navi-item is active (e.g if segments like "content" or "system" is in current url)
if($name == 'content' && strpos($this->settings['routepath'], 'tm/content'))
if($name == 'content' && strpos($this->c->get('urlinfo')['route'], 'tm/content'))
{
$naviitem['active'] = true;
}
elseif($name == 'account' && strpos($this->settings['routepath'], 'tm/account'))
elseif($name == 'account' && strpos($this->c->get('urlinfo')['route'], 'tm/account'))
{
$naviitem['active'] = true;
}
@@ -74,7 +76,7 @@ class ControllerData extends Controller
{
# check if the navi-item is active (e.g if segments like "content" or "system" is in current url)
# a bit fragile because url-segment and name/key in systemnavi.yaml and plugins have to be the same
if(strpos($this->settings['routepath'], 'tm/' . $name))
if(strpos($this->c->get('urlinfo')['route'], 'tm/' . $name))
{
$naviitem['active'] = true;
}
@@ -87,4 +89,185 @@ class ControllerData extends Controller
return $allowedsystemnavi;
}
protected function getThemeDetails()
{
$themes = $this->getThemes();
$themeDetails = [];
foreach($themes as $themeName)
{
$themeDetails[$themeName] = $this->getThemeDefinition($themeName);
}
return $themeDetails;
}
protected function getThemes()
{
$themeFolder = $this->c->get('settings')['rootPath'] . DIRECTORY_SEPARATOR . $this->c->get('settings')['themeFolder'];
$themeFolderC = scandir($themeFolder);
$themes = [];
foreach ($themeFolderC as $key => $theme)
{
if (!in_array($theme, [".",".."]))
{
if (is_dir($themeFolder . DIRECTORY_SEPARATOR . $theme))
{
$themes[] = $theme;
}
}
}
return $themes;
}
protected function getThemeDefinition($themeName)
{
$yaml = new Yaml('\Typemill\Models\Storage');
$themeSettings = $yaml->getYaml('themes' . DIRECTORY_SEPARATOR . $themeName, $themeName . '.yaml');
# add standard-textarea for custom css
$themeSettings['forms']['fields']['customcss'] = [
'type' => 'textarea',
'label' => 'Custom CSS',
'rows' => 10,
'class' => 'codearea',
'description' => 'You can overwrite the theme-css with your own css here.'
];
# add image preview file
$themeSettings['preview'] = 'http://localhost/typemill/themes/' . $themeName . '/' . $themeName . '.png';
return $themeSettings;
}
protected function getPluginDetails()
{
$plugins = $this->getPlugins();
$pluginDetails = [];
foreach($plugins as $pluginName)
{
$pluginDetails[$pluginName] = $this->getPluginDefinition($pluginName);
}
return $pluginDetails;
}
protected function getPlugins()
{
$pluginFolder = $this->c->get('settings')['rootPath'] . DIRECTORY_SEPARATOR . $this->c->get('settings')['pluginFolder'];
$pluginFolderC = scandir($pluginFolder);
$plugins = [];
foreach ($pluginFolderC as $key => $plugin)
{
if (!in_array($plugin, [".",".."]))
{
if (is_dir($pluginFolder . DIRECTORY_SEPARATOR . $plugin))
{
$plugins[] = $plugin;
}
}
}
return $plugins;
}
protected function getPluginDefinition($pluginName)
{
$yaml = new Yaml('\Typemill\Models\Storage');
$pluginSettings = $yaml->getYaml('plugins' . DIRECTORY_SEPARATOR . $pluginName, $pluginName . '.yaml');
return $pluginSettings;
}
protected function getUserFields($userrole,$inspectorrole = NULL)
{
if(!$inspectorrole)
{
# if there is no inspector-role we assume that it is the same role like the userrole
# for example account is always visible by the same user
# edit user can be done by another user like admin.
$inspectorrole = $userrole;
}
$yaml = new Yaml('\Typemill\Models\Storage');
$userfields = $yaml->getYaml('system/typemill/settings', 'user.yaml');
# if a plugin with a role has been deactivated, then users with the role throw an error, so set them back to member...
if(!$this->c->get('acl')->hasRole($userrole))
{
$userrole = 'member';
}
# dispatch fields;
#$fields = $this->c->dispatcher->dispatch('onUserfieldsLoaded', new OnUserfieldsLoaded($fields))->getData();
# only roles who can edit content need profile image and description
if($this->c->get('acl')->isAllowed($userrole, 'mycontent', 'create'))
{
$newfield['image'] = ['label' => 'Profile-Image', 'type' => 'image'];
$newfield['description'] = ['label' => 'Author-Description (Markdown)', 'type' => 'textarea'];
$userfields = array_slice($userfields, 0, 1, true) + $newfield + array_slice($userfields, 1, NULL, true);
# array_splice($fields,1,0,$newfield);
}
# Only admin can change userroles
if($this->c->get('acl')->isAllowed($inspectorrole, 'userlist', 'write'))
{
$definedroles = $this->c->get('acl')->getRoles();
$options = [];
# we need associative array to make select-field with key/value work
foreach($definedroles as $role)
{
$options[$role] = $role;
}
$userfields['userrole'] = ['label' => 'Role', 'type' => 'select', 'options' => $options];
}
return $userfields;
}
protected function recursiveValidation($formdefinitions, $input, $validator, $themeOrPlugin = false, $name = false)
{
# loop through form-definitions, ignores everything that is not defined in yaml
foreach($formdefinitions as $fieldname => $fielddefinitions)
{
if(is_array($fielddefinitions) && $fielddefinitions['type'] == 'fieldset')
{
$this->recursiveValidation($fielddefinitions['fields'], $input, $validator, $themeOrPlugin, $name);
}
$fieldvalue = isset($input[$fieldname]) ? $input[$fieldname] : false;
if($fieldvalue)
{
$validationresult = $validator->field($fieldname, $fieldvalue, $fielddefinitions);
if($validationresult === true)
{
# if input is valid, overwrite value in original settings
if($themeOrPlugin)
{
$this->settings[$themeOrPlugin][$name][$fieldname] = $fieldvalue;
}
else
{
$this->settings[$fieldname] = $fieldvalue;
}
}
else
{
$this->errors[$fieldname] = $validationresult[$fieldname][0];
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,73 +0,0 @@
<?php
namespace Typemill\Controllers;
use DI\Container;
use Slim\Views\Twig;
use Typemill\Events\OnTwigLoaded;
class ControllerWeb extends Controller
{
public function __construct(Container $container)
{
/*
parent::__construct($container);
echo '<br>add twig';
$settings = $this->settings;
$csrf = isset($_SESSION) ? $this->c->get('csrf') : false;
$this->c->set('view', function() use ($settings, $csrf)
{
$twig = Twig::create(
[
# path to templates
$settings['rootPath'] . $settings['authorFolder'],
$settings['rootPath'] . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $settings['theme'],
],
[
# settings
'cache' => ( isset($settings['twigcache']) && $settings['twigcache'] ) ? $settings['rootPath'] . '/cache/twig' : false,
'debug' => isset($settings['displayErrorDetails'])
]
);
# placeholder for flash and errors, will be filled later with middleware
$twig->getEnvironment()->addGlobal('errors', NULL);
$twig->getEnvironment()->addGlobal('flash', NULL);
# add extensions
$twig->addExtension(new \Twig\Extension\DebugExtension());
# $twig->addExtension(new \Nquire\Extensions\TwigUserExtension());
if($csrf)
{
$twig->addExtension(new \Typemill\Extensions\TwigCsrfExtension($csrf));
}
return $twig;
});
protected function setUrlCollection($uri)
{
$scheme = $uri->getScheme();
$authority = $uri->getAuthority();
$protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '');
$this->currentPath = $uri->getPath();
$this->fullBaseUrl = $protocol . $this->basePath;
$this->fullCurrentUrl = $protocol . $this->currentPath;
$this->urlCollection = [
'basePath' => $this->basePath,
'currentPath' => $this->currentPath,
'fullBaseUrl' => $this->fullBaseUrl,
'fullCurrentUrl' => $this->fullCurrentUrl
];
}
$this->c->get('dispatcher')->dispatch(new OnTwigLoaded(false), 'onTwigLoaded');
*/
}
}

View File

@@ -0,0 +1,497 @@
<?php
namespace Typemill\Controllers;
use Typemill\Models\Yaml;
use Typemill\Models\User;
class ControllerWebSystem extends ControllerData
{
public function showSettings($request, $response, $args)
{
$yaml = new Yaml('\Typemill\Models\Storage');
$systemfields = $yaml->getYaml('system/typemill/settings', 'system.yaml');
$translations = $this->c->get('translations');
# add full url for sitemap to settings
$this->settings['sitemap'] = $this->c->get('urlinfo')['baseurl'] . '/cache/sitemap.xml';
return $this->c->get('view')->render($response, 'system/system.twig', [
# 'basicauth' => $user->getBasicAuth(),
'settings' => $this->settings,
'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')),
'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')),
'jsdata' => [
'settings' => $this->settings,
'system' => $systemfields,
'labels' => $translations,
'urlinfo' => $this->c->get('urlinfo')
]
#'captcha' => $this->checkIfAddCaptcha(),
]);
}
public function showThemes($request, $response, $args)
{
$yaml = new Yaml('\Typemill\Models\Storage');
$translations = $this->c->get('translations');
$themeSettings = $this->getThemeDetails();
$themedata = [];
foreach($this->settings['themes'] as $themename => $themeinputs)
{
$themedata[$themename] = $themeinputs;
$themedata[$themename]['customcss'] = $yaml->getFile('cache', $themename . '-custom.css');
}
return $this->c->get('view')->render($response, 'system/themes.twig', [
'settings' => $this->settings,
'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')),
'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')),
'jsdata' => [
'settings' => $themedata,
'themes' => $themeSettings,
'labels' => $translations,
'urlinfo' => $this->c->get('urlinfo')
]
]);
}
public function showPlugins($request, $response, $args)
{
# $yaml = new Yaml('\Typemill\Models\Storage');
$translations = $this->c->get('translations');
$pluginSettings = $this->getPluginDetails();
$plugindata = [];
foreach($this->settings['plugins'] as $pluginname => $plugininputs)
{
$plugindata[$pluginname] = $plugininputs;
}
return $this->c->get('view')->render($response, 'system/plugins.twig', [
'settings' => $this->settings,
'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')),
'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')),
'jsdata' => [
'settings' => $plugindata,
'plugins' => $pluginSettings,
'labels' => $translations,
'urlinfo' => $this->c->get('urlinfo')
]
]);
}
public function showUsers($request, $response, $args)
{
$translations = $this->c->get('translations');
$user = new User();
$usernames = $user->getAllUsers();
$userdata = [];
$count = 0;
foreach($usernames as $username)
{
if($count == 10) break;
$user->setUser($username);
$userdata[] = $user->getUserData();
$count++;
}
return $this->c->get('view')->render($response, 'system/users.twig', [
'settings' => $this->settings,
'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')),
'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')),
'jsdata' => [
'totalusers' => count($usernames),
'usernames' => $usernames,
'userdata' => $userdata,
'userroles' => $this->c->get('acl')->getRoles(),
'labels' => $translations,
'urlinfo' => $this->c->get('urlinfo')
]
]);
}
public function showAccount($request, $response, $args)
{
$translations = $this->c->get('translations');
$username = $request->getAttribute('username');
$user = new User();
$user->setUser($username);
$userdata = $user->getUserData();
$userfields = $this->getUserFields($userdata['userrole']);
return $this->c->get('view')->render($response, 'system/account.twig', [
'settings' => $this->settings,
'mainnavi' => $this->getMainNavigation($request->getAttribute('userrole')),
'systemnavi' => $this->getSystemNavigation($request->getAttribute('userrole')),
'jsdata' => [
'userdata' => $userdata,
'userfields' => $userfields,
'userroles' => $this->c->get('acl')->getRoles(),
'labels' => $translations,
'urlinfo' => $this->c->get('urlinfo')
]
]);
}
/*
public function showBlank($request, $response, $args)
{
$user = new User();
$settings = $this->c->get('settings');
$route = $request->getAttribute('route');
$navigation = $this->getMainNavigation();
$content = '<h1>Hello</h1><p>I am the showBlank method from the settings controller</p><p>In most cases I have been called from a plugin. But if you see this content, then the plugin does not work or has forgotten to inject its own content.</p>';
return $this->render($response, 'settings/blank.twig', array(
'settings' => $settings,
'acl' => $this->c->acl,
'navigation' => $navigation,
'content' => $content,
'route' => $route->getName()
));
}
public function showUser($request, $response, $args)
{
# if user has no rights to watch userlist, then redirect to
if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'view') && $_SESSION['user'] !== $args['username'] )
{
return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $_SESSION['user']] ));
}
# get settings
$settings = $this->c->get('settings');
# get user with userdata
$user = new User();
$userdata = $user->getSecureUser($args['username']);
if(!$userdata)
{
$this->c->flash->addMessage('error', 'User does not exists');
return $response->withRedirect($this->c->router->pathFor('user.account'));
}
# instantiate field-builder
$fieldsModel = new Fields($this->c);
# get the field-definitions
$fieldDefinitions = $this->getUserFields($userdata['userrole']);
# prepare userdata for field-builder
$userSettings['users']['user'] = $userdata;
# generate the input form
$userform = $fieldsModel->getFields($userSettings, 'users', 'user', $fieldDefinitions);
$route = $request->getAttribute('route');
$navigation = $this->getMainNavigation();
# set navigation active
$navigation['Users']['active'] = true;
if(isset($userdata['lastlogin']))
{
$userdata['lastlogin'] = date("d.m.Y H:i:s", $userdata['lastlogin']);
}
return $this->render($response, 'settings/user.twig', array(
'settings' => $settings,
'acl' => $this->c->acl,
'navigation' => $navigation,
'usersettings' => $userSettings, // needed for image url in form, will overwrite settings for field-template
'userform' => $userform, // field model, needed to generate frontend-field
'userdata' => $userdata, // needed to fill form with data
'route' => $route->getName() // needed to set link active
));
}
public function newUser($request, $response, $args)
{
$user = new User();
$users = $user->getUsers();
$userroles = $this->c->acl->getRoles();
$route = $request->getAttribute('route');
$settings = $this->c->get('settings');
$navigation = $this->getMainNavigation();
# set navigation active
$navigation['Users']['active'] = true;
return $this->render($response, 'settings/usernew.twig', array(
'settings' => $settings,
'acl' => $this->c->acl,
'navigation' => $navigation,
'users' => $users,
'userrole' => $userroles,
'route' => $route->getName()
));
}
public function createUser($request, $response, $args)
{
if($request->isPost())
{
if( $request->getattribute('csrf_result') === false )
{
$this->c->flash->addMessage('error', 'The form has a timeout, please try again.');
return $response->withRedirect($this->c->router->pathFor('user.new'));
}
$params = $request->getParams();
$user = new User();
$validate = new Validation();
$userroles = $this->c->acl->getRoles();
if($validate->newUser($params, $userroles))
{
$userdata = array(
'username' => $params['username'],
'email' => $params['email'],
'userrole' => $params['userrole'],
'password' => $params['password']);
$user->createUser($userdata);
$this->c->flash->addMessage('info', 'Welcome, there is a new user!');
return $response->withRedirect($this->c->router->pathFor('user.list'));
}
$this->c->flash->addMessage('error', 'Please correct your input');
return $response->withRedirect($this->c->router->pathFor('user.new'));
}
}
public function updateUser($request, $response, $args)
{
if($request->isPost())
{
if( $request->getattribute('csrf_result') === false )
{
$this->c->flash->addMessage('error', 'The form has a timeout, please try again.');
return $response->withRedirect($this->c->router->pathFor('user.account'));
}
$params = $request->getParams();
$userdata = $params['user'];
$user = new User();
$validate = new Validation();
$userroles = $this->c->acl->getRoles();
$redirectRoute = ($userdata['username'] == $_SESSION['user']) ? $this->c->router->pathFor('user.account') : $this->c->router->pathFor('user.show', ['username' => $userdata['username']]);
# check if user is allowed to view (edit) userlist and other users
if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write'))
{
# if an editor tries to update other userdata than its own
if($_SESSION['user'] !== $userdata['username'])
{
return $response->withRedirect($this->c->router->pathFor('user.account'));
}
# non admins cannot change their userrole, so set it to session-value
$userdata['userrole'] = $_SESSION['role'];
}
# validate standard fields for users
if($validate->existingUser($userdata, $userroles))
{
# validate custom input fields and return images
$userfields = $this->getUserFields($userdata['userrole']);
$imageFields = $this->validateInput('users', 'user', $userdata, $validate, $userfields);
if(!empty($imageFields))
{
$images = $request->getUploadedFiles();
if(isset($images['user']))
{
# set image size
$settings = $this->c->get('settings');
$imageSizes = $settings['images'];
$imageSizes['live'] = ['width' => 500, 'height' => 500];
$settings->replace(['images' => $imageSizes]);
$imageresult = $this->saveImages($imageFields, $userdata, $settings, $images['user']);
if(isset($_SESSION['slimFlash']['error']))
{
return $response->withRedirect($redirectRoute);
}
elseif(isset($imageresult['username']))
{
$userdata = $imageresult;
}
}
}
# check for errors and redirect to path, if errors found
if(isset($_SESSION['errors']))
{
$this->c->flash->addMessage('error', 'Please correct the errors');
return $response->withRedirect($redirectRoute);
}
if(empty($userdata['password']) AND empty($userdata['newpassword']))
{
# make sure no invalid passwords go into model
unset($userdata['password']);
unset($userdata['newpassword']);
$user->updateUser($userdata);
$this->c->flash->addMessage('info', 'Saved all changes');
return $response->withRedirect($redirectRoute);
}
elseif($validate->newPassword($userdata))
{
$userdata['password'] = $userdata['newpassword'];
unset($userdata['newpassword']);
$user->updateUser($userdata);
$this->c->flash->addMessage('info', 'Saved all changes');
return $response->withRedirect($redirectRoute);
}
}
# change error-array for formbuilder
$errors = $_SESSION['errors'];
unset($_SESSION['errors']);
$_SESSION['errors']['user'] = $errors;#
$this->c->flash->addMessage('error', 'Please correct your input');
return $response->withRedirect($redirectRoute);
}
}
public function deleteUser($request, $response, $args)
{
if($request->isPost())
{
if( $request->getattribute('csrf_result') === false )
{
$this->c->flash->addMessage('error', 'The form has a timeout, please try again.');
return $response->withRedirect($this->c->router->pathFor('user.account'));
}
$params = $request->getParams();
$validate = new Validation();
$user = new User();
# check if user is allowed to view (edit) userlist and other users
if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write'))
{
# if an editor tries to delete other user than its own
if($_SESSION['user'] !== $params['username'])
{
return $response->withRedirect($this->c->router->pathFor('user.account'));
}
}
if($validate->username($params['username']))
{
$userdata = $user->getSecureUser($params['username']);
if(!$userdata)
{
$this->c->flash->addMessage('error', 'Ups, we did not find that user');
return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $params['username']]));
}
$user->deleteUser($params['username']);
$this->c->dispatcher->dispatch('onUserDeleted', new OnUserDeleted($userdata));
# if user deleted his own account
if($_SESSION['user'] == $params['username'])
{
session_destroy();
return $response->withRedirect($this->c->router->pathFor('auth.show'));
}
$this->c->flash->addMessage('info', 'Say goodbye, the user is gone!');
return $response->withRedirect($this->c->router->pathFor('user.list'));
}
$this->c->flash->addMessage('error', 'Ups, it is not a valid username');
return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $params['username']]));
}
}
public function clearCache($request, $response, $args)
{
$this->uri = $request->getUri()->withUserInfo('');
$dir = $this->settings['basePath'] . 'cache';
$error = $this->writeCache->deleteCacheFiles($dir);
if($error)
{
return $response->withJson(['errors' => $error], 500);
}
# create a new draft structure
$this->setFreshStructureDraft();
# create a new draft structure
$this->setFreshStructureLive();
# create a new draft structure
$this->setFreshNavigation();
# update the sitemap
$this->updateSitemap();
return $response->withJson(array('errors' => false));
}
protected function saveImages($imageFields, $userInput, $userSettings, $files)
{
# initiate image processor with standard image sizes
$processImages = new ProcessImage($userSettings['images']);
if(!$processImages->checkFolders('images'))
{
$this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.');
return false;
}
foreach($imageFields as $fieldName => $imageField)
{
if(isset($userInput[$fieldName]))
{
# handle single input with single file upload
$image = $files[$fieldName];
if($image->getError() === UPLOAD_ERR_OK)
{
# not the most elegant, but createImage expects a base64-encoded string.
$imageContent = $image->getStream()->getContents();
$imageData = base64_encode($imageContent);
$imageSrc = 'data: ' . $image->getClientMediaType() . ';base64,' . $imageData;
if($processImages->createImage($imageSrc, $image->getClientFilename(), $userSettings['images'], $overwrite = NULL))
{
# returns image path to media library
$userInput[$fieldName] = $processImages->publishImage();
}
}
}
}
return $userInput;
}
*/
}

View File

@@ -6,19 +6,11 @@ use Twig\Extension\AbstractExtension;
class TwigUrlExtension extends AbstractExtension
{
protected $uri;
protected $basepath;
protected $scheme;
protected $authority;
protected $protocol;
protected $urlinfo;
public function __construct($uri, $basepath)
public function __construct($urlinfo)
{
$this->uri = $uri;
$this->basepath = $basepath;
$this->scheme = $uri->getScheme();
$this->authority = $uri->getAuthority();
$this->protocol = ($this->scheme ? $this->scheme . ':' : '') . ($this->authority ? '//' . $this->authority : '');
$this->urlinfo = $urlinfo;
}
public function getFunctions()
@@ -32,16 +24,16 @@ class TwigUrlExtension extends AbstractExtension
public function baseUrl()
{
return $this->protocol . $this->basepath;
return $this->urlinfo['baseurl'];
}
public function currentUrl()
{
return $this->protocol . $this->uri->getPath();
return $this->urlinfo['currenturl'];
}
public function currentPath()
{
return $this->uri->getPath();
return $this->urlinfo['route'];
}
}

View File

@@ -11,8 +11,6 @@ class JsonBodyParser implements MiddlewareInterface
{
public function process(Request $request, RequestHandler $handler) :response
{
#echo '<br> JSON Body parser';
$contentType = $request->getHeaderLine('Content-Type');
if (strstr($contentType, 'application/json'))

View File

@@ -7,6 +7,7 @@ use Slim\Routing\RouteParser;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
use Typemill\Models\User;
class RedirectIfUnauthenticated implements MiddlewareInterface
{
@@ -23,17 +24,28 @@ class RedirectIfUnauthenticated implements MiddlewareInterface
)
? true : false;
if(!$authenticated)
if($authenticated)
{
# this executes only middleware code and not code from route
$response = new Response();
return $response->withHeader('Location', $this->router->urlFor('auth.show'))->withStatus(302);
# here we have to load userdata and pass them through request or response
$user = new User();
if($user->setUser($_SESSION['username']))
{
$userdata = $user->getUserData();
$request = $request->withAttribute('username', $userdata['username']);
$request = $request->withAttribute('userrole', $userdata['userrole']);
# this executes code from routes first and then executes middleware
$response = $handler->handle($request);
return $response;
}
}
# this executes code from routes first and then executes middleware
$response = $handler->handle($request);
return $response;
# this executes only middleware code and not code from route
$response = new Response();
return $response->withHeader('Location', $this->router->urlFor('auth.show'))->withStatus(302);
}
}

View File

@@ -6,6 +6,7 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Routing\RouteContext;
use Slim\Psr7\Response;
use Typemill\Models\User;
class RestrictApiAccess
{
@@ -14,38 +15,52 @@ class RestrictApiAccess
$routeContext = RouteContext::fromRequest($request);
$baseURL = $routeContext->getBasePath();
if ($request->hasHeader('X-Session-Auth')) {
# check if it a session based authentication
if ($request->hasHeader('X-Session-Auth'))
{
session_start();
$authenticated = (
(isset($_SESSION['username'])) &&
(isset($_SESSION['userrole'])) &&
(isset($_SESSION['login']))
)
? true : false;
if($authenticated)
{
$response = $handler->handle($request);
# here we have to load userdata and pass them through request or response
$user = new User();
return $response;
if($user->setUser($_SESSION['username']))
{
$userdata = $user->getUserData();
$request = $request->withAttribute('username', $userdata['username']);
$request = $request->withAttribute('userrole', $userdata['userrole']);
$response = $handler->handle($request);
return $response;
}
}
}
# elseif ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') {
# advantage: all xhr-calls to the api will be session based
# no direct calls from javascript possible
# only from server
# if you use this, then all xhr-calls need a session.
# no direct xhr calls without session are possible
# might increase security, but can have unwanted cases e.g. when you
# want to provide public api accessible for all by javascript (do you ever want??)
# }
# this is for api-key authentication
$user = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : false;
$apikey = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : false;
if($user && $apikey)
{
# get user
# get user with username
# or get user with apikey
# check if user has tmpApiKey
# check if user has permanentApiKey
# check if user has tmpApiKey

View File

@@ -75,10 +75,17 @@ class Storage
{
$this->error = "Could not open and read the file $filename in folder $folder.";
return true;
return false;
}
$writefile = fwrite($openfile, $data);
if(!$writefile)
{
$this->error = "Could not write to the file $filename in folder $folder.";
return false;
}
fwrite($openfile, $data);
fclose($openfile);
return true;
@@ -90,6 +97,7 @@ class Storage
{
# ??? should be with basepath???
$fileContent = file_get_contents($folder . DIRECTORY_SEPARATOR . $filename);
return $fileContent;
}

View File

@@ -18,14 +18,15 @@ class User
public function __construct()
{
$this->userDir = getcwd() . '/system/settings/users';
$this->userDir = getcwd() . '/settings/users';
$this->yaml = new Yaml('\Typemill\Models\Storage');
}
public function setUser(string $username)
{
if(!$this->user)
{
# if no user is set or requested user has a different username
# if(!$this->user OR ($this->user['username'] != $username))
# {
$this->user = $this->yaml->getYaml('settings/users', $username . '.yaml');
if(!$this->user)
@@ -40,7 +41,7 @@ class User
# delete password from public userdata
unset($this->user['password']);
}
# }
return $this;
}
@@ -62,22 +63,22 @@ class User
return $this;
}
public function getError()
{
return $this->error;
}
public function getUserData()
{
return $this->user;
}
public function getError()
{
return $this->error;
}
public function getAllUsers()
{
# check if users directory exists
if(!is_dir($this->userDir))
{
$this->error = 'Directory $this->userDir does not exist.';
$this->error = "Directory $this->userDir does not exist.";
return false;
}
@@ -116,6 +117,40 @@ class User
return false;
}
public function setValue($key, $value)
{
$this->user[$key] = $value;
}
public function unsetValue($key)
{
unset($this->user[$key]);
}
public function updateUser()
{
if($this->yaml->updateYaml('settings/users', $this->user['username'] . '.yaml', $this->user))
{
$this->deleteUserIndex();
return true;
}
$this->error = $this->yaml->getError();
return false;
}
public function unsetFromUser(array $keys)
{
if(empty($keys) OR !$this->user)
@@ -138,7 +173,7 @@ class User
return true;
}
public function updateUser()
public function updateUserOld()
{
# add password back to userdata before you store/update user
if($this->password)
@@ -156,6 +191,7 @@ class User
return false;
}
public function updateUserWithInput(array $input)
{
if(!isset($input['username']) OR !$this->user)
@@ -273,15 +309,15 @@ class User
# accepts email with or without asterix and returns userdata
public function findUsersByEmail($email)
{
$usernames = [];
$usernames = [];
# Make sure that we scan only the first 11 files even if there are some thousand users.
if ($dh = opendir($this->userDir))
{
$count = 1;
$count = 0;
$exclude = array('..', '.', '.logins', 'tmuserindex-mail.txt', 'tmuserindex-role.txt');
while ( ($userfile = readdir($dh)) !== false && $count <= 11 )
while ( ($userfile = readdir($dh)) !== false && $count <= 10 )
{
if(in_array($userfile, $exclude)){ continue; }
@@ -292,35 +328,46 @@ class User
closedir($dh);
}
$countusers = count($usernames);
if($countusers == 0)
if(count($usernames) == 0)
{
return false;
}
# use a simple dirty search if there are less than 10 users (only in use for new user registrations)
if($countusers <= 10)
elseif(count($usernames) <= 9)
{
foreach($usernames as $username)
# perform a simple search because we have less than 10 registered users
return $this->searchEmailSimple($usernames,$email);
}
else
{
# perform search in an index for many users
return $this->searchEmailByIndex($email);
}
}
private function searchEmailSimple($usernames, $email)
{
foreach($usernames as $username)
{
$this->setUser($username);
$user = $this->getUserData();
if($user['email'] == $email)
{
$userdata = $this->getSecureUser($username);
if($userdata['email'] == $email)
{
return $userdata;
}
return [$username];
}
return false;
}
return false;
}
private function searchEmailByIndex($email)
{
# if there are more than 10 users, search with an index
$usermails = $this->getUserMailIndex();
$usermails = $this->getUserMailIndex();
$usernames = [];
# search with starting asterix, ending asterix or without asterix
if($email[0] == '*')
{
$userdata = [];
$search = substr($email, 1);
$length = strlen($search);
@@ -328,17 +375,12 @@ class User
{
if(substr($usermail, -$length) == $search)
{
$userdata[] = $username;
$usernames[] = $username;
}
}
$userdata = empty($userdata) ? false : $userdata;
return $userdata;
}
elseif(substr($email, -1) == '*')
{
$userdata = [];
$search = substr($email, 0, -1);
$length = strlen($search);
@@ -346,21 +388,21 @@ class User
{
if(substr($usermail, 0, $length) == $search)
{
$userdata[] = $username;
$usernames[] = $username;
}
}
$userdata = empty($userdata) ? false : $userdata;
return $userdata;
}
elseif(isset($usermails[$email]))
{
$userdata[] = $usermails[$email];
return $userdata;
$usernames[] = $usermails[$email];
}
return false;
if(empty($usernames))
{
return false;
}
return $usernames;
}
public function getUserMailIndex()
@@ -376,12 +418,13 @@ class User
}
}
$usernames = $this->getUsers();
$usernames = $this->getAllUsers();
$usermailindex = [];
foreach($usernames as $username)
{
$userdata = $this->getSecureUser($username);
$this->setUser($username);
$userdata = $this->getUserData();
$usermailindex[$userdata['email']] = $username;
}
@@ -391,42 +434,8 @@ class User
return $usermailindex;
}
# accepts email with or without asterix and returns usernames
public function findUsersByRole($role)
{
/*
# get all user files
$usernames = $this->getUsers();
$countusers = count($usernames);
if($countusers == 0)
{
return false;
}
# use a simple dirty search if there are less than 10 users (not in use right now)
if($countusers <= 4)
{
$userdata = [];
foreach($usernames as $key => $username)
{
$userdetails = $this->getSecureUser($username);
if($userdetails['userrole'] == $role)
{
$userdata[] = $userdetails;
}
}
if(empty($userdata))
{
return false;
}
return $userdata;
}
*/
$userroles = $this->getUserRoleIndex();
if(isset($userroles[$role]))
@@ -449,7 +458,7 @@ class User
}
}
$usernames = $this->getUsers();
$usernames = $this->getAllUsers();
$userroleindex = [];
foreach($usernames as $username)

View File

@@ -47,7 +47,8 @@ class Validation
# checks if email is available if userdata is updated
Validator::addRule('emailChanged', function($field, $value, array $params, array $fields) use ($user)
{
$userdata = $user->getSecureUser($fields['username']);
$user->setUserWithPassword($fields['username']);
$userdata = $user->getUserData();
if($userdata['email'] == $value){ return true; } # user has not updated his email
$email = trim($value);
@@ -58,8 +59,8 @@ class Validation
# checks if username is free when create new user
Validator::addRule('userAvailable', function($field, $value, array $params, array $fields) use ($user)
{
$activeUser = $user->getUser($value);
$inactiveUser = $user->getUser("_" . $value);
$activeUser = $user->setUser($value);
$inactiveUser = $user->setUser("_" . $value);
if($activeUser OR $inactiveUser){ return false; }
return true;
}, 'taken');
@@ -67,8 +68,7 @@ class Validation
# checks if user exists when userdata is updated
Validator::addRule('userExists', function($field, $value, array $params, array $fields) use ($user)
{
$userdata = $user->getUser($value);
if($userdata){ return true; }
if($user->setUser($value)){ return true; }
return false;
}, 'does not exist');
@@ -108,8 +108,14 @@ class Validation
Validator::addRule('checkPassword', function($field, $value, array $params, array $fields) use ($user)
{
$userdata = $user->getUser($fields['username']);
if($userdata && password_verify($value, $userdata['password'])){ return true; }
if($user->setUserWithPassword($fields['username']))
{
$userdata = $user->getUserData();
if(password_verify($value, $userdata['password']))
{
return true;
}
}
return false;
}, 'wrong password');
@@ -249,7 +255,7 @@ class Validation
*
* @param array $params with form data.
* @return obj $v the validation object passed to a result method.
*/
*
public function newPassword(array $params)
{
@@ -260,6 +266,29 @@ class Validation
return $this->validationResult($v);
}
*/
/**
* validation for changing the password api case
*
* @param array $params with form data.
* @return obj $v the validation object passed to a result method.
*/
public function newPassword(array $params)
{
$v = new Validator($params);
$v->rule('required', ['password', 'newpassword']);
$v->rule('lengthBetween', 'newpassword', 5, 20);
$v->rule('checkPassword', 'password')->message("Password is wrong");
if($v->validate())
{
return true;
}
return $v->errors();
}
/**
* validation for password recovery
@@ -283,7 +312,7 @@ class Validation
*
* @param array $params with form data.
* @return obj $v the validation object passed to a result method.
*/
*
public function settings(array $params, array $copyright, array $formats, $name = false)
{
@@ -312,6 +341,7 @@ class Validation
return $this->validationResult($v, $name);
}
*/
/**
* validation for content editor
@@ -442,11 +472,11 @@ class Validation
* @return obj $v the validation object passed to a result method.
*/
public function objectField($fieldName, $fieldValue, $objectName, $fieldDefinitions, $skiprequired = NULL)
public function field($fieldName, $fieldValue, $fieldDefinitions)
{
$v = new Validator(array($fieldName => $fieldValue));
if(isset($fieldDefinitions['required']) && !$skiprequired)
if(isset($fieldDefinitions['required']))
{
$v->rule('required', $fieldName);
}
@@ -548,7 +578,15 @@ class Validation
$v->rule('lengthMax', $fieldName, 1000);
$v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u');
}
return $this->validationResult($v, $objectName);
if(!$v->validate())
{
return $v->errors();
}
return true;
return $this->validationResult($v);
}
/**

View File

@@ -6,6 +6,29 @@ use Typemill\Models\StorageWrapper;
class Helpers{
public static function urlInfo($uri)
{
$basepath = preg_replace('/(.*)\/.*/', '$1', $_SERVER['SCRIPT_NAME']);
$currentpath = $uri->getPath();
$route = str_replace($basepath, '', $currentpath);
$scheme = $uri->getScheme();
$authority = $uri->getAuthority();
$protocol = ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '');
$baseurl = $protocol . $basepath;
$currenturl = $protocol . $currentpath;
return [
'basepath' => $basepath,
'currentpath' => $currentpath,
'route' => $route,
'scheme' => $scheme,
'authority' => $authority,
'protocol' => $protocol,
'baseurl' => $baseurl,
'currenturl' => $currenturl
];
}
public static function getUserIP()
{
$client = @$_SERVER['HTTP_CLIENT_IP'];

View File

@@ -6,11 +6,11 @@ use Typemill\Models\Yaml;
class Translations
{
public static function loadTranslations($settings)
public static function loadTranslations($settings, $route)
{
$yaml = new Yaml($settings['storage']);
$urlsegments = explode('/',trim($settings['routepath'],'/'));
$urlsegments = explode('/',trim($route,'/'));
$environment = 'frontend';
if( ($urlsegments[0] === 'tm' OR $urlsegments[0] === 'setup') )

View File

@@ -6,7 +6,7 @@
<div class="flex justify-end">
<div class="bg-rose-600 text-white min-h-screen w-1/2 flex justify-center items-center">
<div class="bg-teal-600 text-white min-h-screen w-1/2 flex justify-center items-center">
<div class="max-w-md content-center">
<h1 class="text-6xl py-5">Login</h1>

View File

@@ -15,3 +15,15 @@
position: relative;
}
[v-cloak] {
display: none;
}
.initial-enter-active,
.initial-leave-active {
transition: opacity 0.2s ease;
}
.initial-enter-from,
.initial-leave-to {
opacity: 0;
}

View File

@@ -616,6 +616,10 @@ video {
}
}
.pointer-events-none {
pointer-events: none;
}
.static {
position: static;
}
@@ -628,6 +632,39 @@ video {
position: relative;
}
.inset-y-0 {
top: 0px;
bottom: 0px;
}
.left-0 {
left: 0px;
}
.right-0 {
right: 0px;
}
.top-0 {
top: 0px;
}
.top-1 {
top: 0.25rem;
}
.right-1 {
right: 0.25rem;
}
.bottom-0 {
bottom: 0px;
}
.top-3 {
top: 0.75rem;
}
.m-0 {
margin: 0px;
}
@@ -641,10 +678,59 @@ video {
margin-bottom: 0.5rem;
}
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.my-5 {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.my-1 {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.mt-6 {
margin-top: 1.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mt-5 {
margin-top: 1.25rem;
}
.mb-5 {
margin-bottom: 1.25rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
@@ -653,8 +739,16 @@ video {
margin-top: 1rem;
}
.mt-5 {
margin-top: 1.25rem;
.mb-4 {
margin-bottom: 1rem;
}
.mt-auto {
margin-top: auto;
}
.mt-8 {
margin-top: 2rem;
}
.mt-7 {
@@ -669,10 +763,6 @@ video {
margin-right: 0.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.block {
display: block;
}
@@ -689,6 +779,10 @@ video {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
@@ -697,6 +791,26 @@ video {
display: none;
}
.h-8 {
height: 2rem;
}
.h-12 {
height: 3rem;
}
.h-6 {
height: 1.5rem;
}
.h-5 {
height: 1.25rem;
}
.h-64 {
height: 16rem;
}
.min-h-screen {
min-height: 100vh;
}
@@ -709,22 +823,38 @@ video {
width: 100%;
}
.w-1\/5 {
width: 20%;
.w-half {
width: 48%;
}
.w-4\/5 {
width: 80%;
.w-6 {
width: 1.5rem;
}
.w-1\/4 {
width: 25%;
.w-5 {
width: 1.25rem;
}
.w-2\/3 {
width: 66.666667%;
}
.w-1\/3 {
width: 33.333333%;
}
.w-10 {
width: 2.5rem;
}
.w-3\/4 {
width: 75%;
}
.w-1\/4 {
width: 25%;
}
.max-w-md {
max-width: 28rem;
}
@@ -733,14 +863,6 @@ video {
max-width: 72rem;
}
.max-w-xl {
max-width: 36rem;
}
.max-w-sm {
max-width: 24rem;
}
.transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
@@ -749,10 +871,22 @@ video {
cursor: pointer;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.content-center {
align-content: center;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
@@ -769,6 +903,24 @@ video {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.overflow-auto {
overflow: auto;
}
.overflow-hidden {
overflow: hidden;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rounded {
border-radius: 0.25rem;
}
@@ -781,10 +933,18 @@ video {
border-width: 0px;
}
.border-2 {
border-width: 2px;
}
.border-b-2 {
border-bottom-width: 2px;
}
.border-b-4 {
border-bottom-width: 4px;
}
.border-l-2 {
border-left-width: 2px;
}
@@ -793,10 +953,6 @@ video {
border-right-width: 2px;
}
.border-b-4 {
border-bottom-width: 4px;
}
.border-l-4 {
border-left-width: 4px;
}
@@ -815,6 +971,16 @@ video {
border-color: rgb(231 229 228 / var(--tw-border-opacity));
}
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.border-stone-300 {
--tw-border-opacity: 1;
border-color: rgb(214 211 209 / var(--tw-border-opacity));
}
.border-stone-700 {
--tw-border-opacity: 1;
border-color: rgb(68 64 60 / var(--tw-border-opacity));
@@ -825,9 +991,19 @@ video {
border-color: rgb(245 245 244 / var(--tw-border-opacity));
}
.border-slate-100 {
.border-stone-50 {
--tw-border-opacity: 1;
border-color: rgb(241 245 249 / var(--tw-border-opacity));
border-color: rgb(250 250 249 / var(--tw-border-opacity));
}
.border-white {
--tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity));
}
.border-cyan-500 {
--tw-border-opacity: 1;
border-color: rgb(6 182 212 / var(--tw-border-opacity));
}
.border-slate-200 {
@@ -835,9 +1011,9 @@ video {
border-color: rgb(226 232 240 / var(--tw-border-opacity));
}
.bg-rose-600 {
.bg-teal-600 {
--tw-bg-opacity: 1;
background-color: rgb(225 29 72 / var(--tw-bg-opacity));
background-color: rgb(13 148 136 / var(--tw-bg-opacity));
}
.bg-white {
@@ -845,19 +1021,29 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-stone-100 {
--tw-bg-opacity: 1;
background-color: rgb(245 245 244 / var(--tw-bg-opacity));
}
.bg-stone-700 {
--tw-bg-opacity: 1;
background-color: rgb(68 64 60 / var(--tw-bg-opacity));
}
.bg-stone-50 {
.bg-teal-500 {
--tw-bg-opacity: 1;
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
background-color: rgb(20 184 166 / var(--tw-bg-opacity));
}
.bg-rose-500 {
--tw-bg-opacity: 1;
background-color: rgb(244 63 94 / var(--tw-bg-opacity));
}
.bg-stone-100 {
--tw-bg-opacity: 1;
background-color: rgb(245 245 244 / var(--tw-bg-opacity));
}
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
}
.bg-stone-200 {
@@ -865,28 +1051,35 @@ video {
background-color: rgb(231 229 228 / var(--tw-bg-opacity));
}
.bg-stone-400 {
.bg-stone-50 {
--tw-bg-opacity: 1;
background-color: rgb(168 162 158 / var(--tw-bg-opacity));
}
.bg-stone-300 {
--tw-bg-opacity: 1;
background-color: rgb(214 211 209 / var(--tw-bg-opacity));
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
}
.bg-clip-padding {
background-clip: padding-box;
}
.p-3 {
padding: 0.75rem;
.bg-center {
background-position: center;
}
.p-4 {
padding: 1rem;
}
.p-3 {
padding: 0.75rem;
}
.p-8 {
padding: 2rem;
}
.p-1 {
padding: 0.25rem;
}
.p-2 {
padding: 0.5rem;
}
@@ -916,11 +1109,49 @@ video {
padding-bottom: 0.75rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.pl-3 {
padding-left: 0.75rem;
}
.pl-10 {
padding-left: 2.5rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.pr-1 {
padding-right: 0.25rem;
}
.pr-10 {
padding-right: 2.5rem;
}
.pr-3 {
padding-right: 0.75rem;
}
.pl-8 {
padding-left: 2rem;
}
.pt-4 {
padding-top: 1rem;
}
@@ -929,12 +1160,8 @@ video {
padding-bottom: 0.75rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.pb-1 {
padding-bottom: 0.25rem;
.text-right {
text-align: right;
}
.text-6xl {
@@ -952,11 +1179,31 @@ video {
line-height: 1rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.font-normal {
font-weight: 400;
}
@@ -1000,17 +1247,51 @@ video {
color: rgb(225 29 72 / var(--tw-text-opacity));
}
.text-teal-500 {
--tw-text-opacity: 1;
color: rgb(20 184 166 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.text-teal-600 {
--tw-text-opacity: 1;
color: rgb(13 148 136 / var(--tw-text-opacity));
}
.underline {
-webkit-text-decoration-line: underline;
text-decoration-line: underline;
}
.no-underline {
-webkit-text-decoration-line: none;
text-decoration-line: none;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.outline {
outline-style: solid;
}
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.drop-shadow-md {
--tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
@@ -1044,23 +1325,26 @@ video {
transition-duration: 150ms;
}
.duration-100 {
transition-duration: 100ms;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.hover\:border-b-4:hover {
border-bottom-width: 4px;
}
.hover\:border-stone-700:hover {
--tw-border-opacity: 1;
border-color: rgb(68 64 60 / var(--tw-border-opacity));
}
.hover\:border-cyan-300:hover {
.hover\:border-teal-500:hover {
--tw-border-opacity: 1;
border-color: rgb(103 232 249 / var(--tw-border-opacity));
}
.hover\:border-cyan-500:hover {
--tw-border-opacity: 1;
border-color: rgb(6 182 212 / var(--tw-border-opacity));
border-color: rgb(20 184 166 / var(--tw-border-opacity));
}
.hover\:bg-gray-200:hover {
@@ -1068,9 +1352,24 @@ video {
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.hover\:bg-stone-700:hover {
.hover\:bg-stone-900:hover {
--tw-bg-opacity: 1;
background-color: rgb(68 64 60 / var(--tw-bg-opacity));
background-color: rgb(28 25 23 / var(--tw-bg-opacity));
}
.hover\:bg-teal-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(13 148 136 / var(--tw-bg-opacity));
}
.hover\:bg-stone-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(231 229 228 / var(--tw-bg-opacity));
}
.hover\:bg-stone-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(245 245 244 / var(--tw-bg-opacity));
}
.hover\:bg-stone-50:hover {
@@ -1078,9 +1377,9 @@ video {
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
}
.hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
.hover\:underline:hover {
-webkit-text-decoration-line: underline;
text-decoration-line: underline;
}
.focus\:border-blue-600:focus {
@@ -1093,11 +1392,6 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.focus\:bg-stone-700:focus {
--tw-bg-opacity: 1;
background-color: rgb(68 64 60 / var(--tw-bg-opacity));
}
.focus\:bg-stone-50:focus {
--tw-bg-opacity: 1;
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
@@ -1108,11 +1402,6 @@ video {
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.focus\:text-white:focus {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
@@ -1124,17 +1413,14 @@ video {
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.active\:bg-stone-700:active {
--tw-bg-opacity: 1;
background-color: rgb(68 64 60 / var(--tw-bg-opacity));
}
.active\:bg-stone-50:active {
--tw-bg-opacity: 1;
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
}
.active\:text-white:active {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
@media (prefers-color-scheme: dark) {
.dark\:text-gray-400 {
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity));
}
}

View File

@@ -0,0 +1,449 @@
const globalWindow = window;
export function CodeJar(editor, highlight, opt = {}) {
const options = Object.assign({ tab: '\t', indentOn: /[({\[]$/, moveToNewLine: /^[)}\]]/, spellcheck: false, catchTab: true, preserveIdent: true, addClosing: true, history: true, window: globalWindow }, opt);
const window = options.window;
const document = window.document;
let listeners = [];
let history = [];
let at = -1;
let focus = false;
let callback;
let prev; // code content prior keydown event
editor.setAttribute('contenteditable', 'plaintext-only');
editor.setAttribute('spellcheck', options.spellcheck ? 'true' : 'false');
editor.style.outline = 'none';
editor.style.overflowWrap = 'break-word';
editor.style.overflowY = 'auto';
editor.style.whiteSpace = 'pre-wrap';
let isLegacy = false; // true if plaintext-only is not supported
highlight(editor);
if (editor.contentEditable !== 'plaintext-only')
isLegacy = true;
if (isLegacy)
editor.setAttribute('contenteditable', 'true');
const debounceHighlight = debounce(() => {
const pos = save();
highlight(editor, pos);
restore(pos);
}, 30);
let recording = false;
const shouldRecord = (event) => {
return !isUndo(event) && !isRedo(event)
&& event.key !== 'Meta'
&& event.key !== 'Control'
&& event.key !== 'Alt'
&& !event.key.startsWith('Arrow');
};
const debounceRecordHistory = debounce((event) => {
if (shouldRecord(event)) {
recordHistory();
recording = false;
}
}, 300);
const on = (type, fn) => {
listeners.push([type, fn]);
editor.addEventListener(type, fn);
};
on('keydown', event => {
if (event.defaultPrevented)
return;
prev = toString();
if (options.preserveIdent)
handleNewLine(event);
else
legacyNewLineFix(event);
if (options.catchTab)
handleTabCharacters(event);
if (options.addClosing)
handleSelfClosingCharacters(event);
if (options.history) {
handleUndoRedo(event);
if (shouldRecord(event) && !recording) {
recordHistory();
recording = true;
}
}
if (isLegacy)
restore(save());
});
on('keyup', event => {
if (event.defaultPrevented)
return;
if (event.isComposing)
return;
if (prev !== toString())
debounceHighlight();
debounceRecordHistory(event);
if (callback)
callback(toString());
});
on('focus', _event => {
focus = true;
});
on('blur', _event => {
focus = false;
});
on('paste', event => {
recordHistory();
handlePaste(event);
recordHistory();
if (callback)
callback(toString());
});
function save() {
const s = getSelection();
const pos = { start: 0, end: 0, dir: undefined };
let { anchorNode, anchorOffset, focusNode, focusOffset } = s;
if (!anchorNode || !focusNode)
throw 'error1';
// Selection anchor and focus are expected to be text nodes,
// so normalize them.
if (anchorNode.nodeType === Node.ELEMENT_NODE) {
const node = document.createTextNode('');
anchorNode.insertBefore(node, anchorNode.childNodes[anchorOffset]);
anchorNode = node;
anchorOffset = 0;
}
if (focusNode.nodeType === Node.ELEMENT_NODE) {
const node = document.createTextNode('');
focusNode.insertBefore(node, focusNode.childNodes[focusOffset]);
focusNode = node;
focusOffset = 0;
}
visit(editor, el => {
if (el === anchorNode && el === focusNode) {
pos.start += anchorOffset;
pos.end += focusOffset;
pos.dir = anchorOffset <= focusOffset ? '->' : '<-';
return 'stop';
}
if (el === anchorNode) {
pos.start += anchorOffset;
if (!pos.dir) {
pos.dir = '->';
}
else {
return 'stop';
}
}
else if (el === focusNode) {
pos.end += focusOffset;
if (!pos.dir) {
pos.dir = '<-';
}
else {
return 'stop';
}
}
if (el.nodeType === Node.TEXT_NODE) {
if (pos.dir != '->')
pos.start += el.nodeValue.length;
if (pos.dir != '<-')
pos.end += el.nodeValue.length;
}
});
// collapse empty text nodes
editor.normalize();
return pos;
}
function restore(pos) {
const s = getSelection();
let startNode, startOffset = 0;
let endNode, endOffset = 0;
if (!pos.dir)
pos.dir = '->';
if (pos.start < 0)
pos.start = 0;
if (pos.end < 0)
pos.end = 0;
// Flip start and end if the direction reversed
if (pos.dir == '<-') {
const { start, end } = pos;
pos.start = end;
pos.end = start;
}
let current = 0;
visit(editor, el => {
if (el.nodeType !== Node.TEXT_NODE)
return;
const len = (el.nodeValue || '').length;
if (current + len > pos.start) {
if (!startNode) {
startNode = el;
startOffset = pos.start - current;
}
if (current + len > pos.end) {
endNode = el;
endOffset = pos.end - current;
return 'stop';
}
}
current += len;
});
if (!startNode)
startNode = editor, startOffset = editor.childNodes.length;
if (!endNode)
endNode = editor, endOffset = editor.childNodes.length;
// Flip back the selection
if (pos.dir == '<-') {
[startNode, startOffset, endNode, endOffset] = [endNode, endOffset, startNode, startOffset];
}
s.setBaseAndExtent(startNode, startOffset, endNode, endOffset);
}
function beforeCursor() {
const s = getSelection();
const r0 = s.getRangeAt(0);
const r = document.createRange();
r.selectNodeContents(editor);
r.setEnd(r0.startContainer, r0.startOffset);
return r.toString();
}
function afterCursor() {
const s = getSelection();
const r0 = s.getRangeAt(0);
const r = document.createRange();
r.selectNodeContents(editor);
r.setStart(r0.endContainer, r0.endOffset);
return r.toString();
}
function handleNewLine(event) {
if (event.key === 'Enter') {
const before = beforeCursor();
const after = afterCursor();
let [padding] = findPadding(before);
let newLinePadding = padding;
// If last symbol is "{" ident new line
if (options.indentOn.test(before)) {
newLinePadding += options.tab;
}
// Preserve padding
if (newLinePadding.length > 0) {
preventDefault(event);
event.stopPropagation();
insert('\n' + newLinePadding);
}
else {
legacyNewLineFix(event);
}
// Place adjacent "}" on next line
if (newLinePadding !== padding && options.moveToNewLine.test(after)) {
const pos = save();
insert('\n' + padding);
restore(pos);
}
}
}
function legacyNewLineFix(event) {
// Firefox does not support plaintext-only mode
// and puts <div><br></div> on Enter. Let's help.
if (isLegacy && event.key === 'Enter') {
preventDefault(event);
event.stopPropagation();
if (afterCursor() == '') {
insert('\n ');
const pos = save();
pos.start = --pos.end;
restore(pos);
}
else {
insert('\n');
}
}
}
function handleSelfClosingCharacters(event) {
const open = `([{'"`;
const close = `)]}'"`;
const codeAfter = afterCursor();
const codeBefore = beforeCursor();
const escapeCharacter = codeBefore.substr(codeBefore.length - 1) === '\\';
const charAfter = codeAfter.substr(0, 1);
if (close.includes(event.key) && !escapeCharacter && charAfter === event.key) {
// We already have closing char next to cursor.
// Move one char to right.
const pos = save();
preventDefault(event);
pos.start = ++pos.end;
restore(pos);
}
else if (open.includes(event.key)
&& !escapeCharacter
&& (`"'`.includes(event.key) || ['', ' ', '\n'].includes(charAfter))) {
preventDefault(event);
const pos = save();
const wrapText = pos.start == pos.end ? '' : getSelection().toString();
const text = event.key + wrapText + close[open.indexOf(event.key)];
insert(text);
pos.start++;
pos.end++;
restore(pos);
}
}
function handleTabCharacters(event) {
if (event.key === 'Tab') {
preventDefault(event);
if (event.shiftKey) {
const before = beforeCursor();
let [padding, start,] = findPadding(before);
if (padding.length > 0) {
const pos = save();
// Remove full length tab or just remaining padding
const len = Math.min(options.tab.length, padding.length);
restore({ start, end: start + len });
document.execCommand('delete');
pos.start -= len;
pos.end -= len;
restore(pos);
}
}
else {
insert(options.tab);
}
}
}
function handleUndoRedo(event) {
if (isUndo(event)) {
preventDefault(event);
at--;
const record = history[at];
if (record) {
editor.innerHTML = record.html;
restore(record.pos);
}
if (at < 0)
at = 0;
}
if (isRedo(event)) {
preventDefault(event);
at++;
const record = history[at];
if (record) {
editor.innerHTML = record.html;
restore(record.pos);
}
if (at >= history.length)
at--;
}
}
function recordHistory() {
if (!focus)
return;
const html = editor.innerHTML;
const pos = save();
const lastRecord = history[at];
if (lastRecord) {
if (lastRecord.html === html
&& lastRecord.pos.start === pos.start
&& lastRecord.pos.end === pos.end)
return;
}
at++;
history[at] = { html, pos };
history.splice(at + 1);
const maxHistory = 300;
if (at > maxHistory) {
at = maxHistory;
history.splice(0, 1);
}
}
function handlePaste(event) {
preventDefault(event);
const text = (event.originalEvent || event)
.clipboardData
.getData('text/plain')
.replace(/\r/g, '');
const pos = save();
insert(text);
highlight(editor);
restore({
start: Math.min(pos.start, pos.end) + text.length,
end: Math.min(pos.start, pos.end) + text.length,
dir: '<-',
});
}
function visit(editor, visitor) {
const queue = [];
if (editor.firstChild)
queue.push(editor.firstChild);
let el = queue.pop();
while (el) {
if (visitor(el) === 'stop')
break;
if (el.nextSibling)
queue.push(el.nextSibling);
if (el.firstChild)
queue.push(el.firstChild);
el = queue.pop();
}
}
function isCtrl(event) {
return event.metaKey || event.ctrlKey;
}
function isUndo(event) {
return isCtrl(event) && !event.shiftKey && event.code === 'KeyZ';
}
function isRedo(event) {
return isCtrl(event) && event.shiftKey && event.code === 'KeyZ';
}
function insert(text) {
text = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
document.execCommand('insertHTML', false, text);
}
function debounce(cb, wait) {
let timeout = 0;
return (...args) => {
clearTimeout(timeout);
timeout = window.setTimeout(() => cb(...args), wait);
};
}
function findPadding(text) {
// Find beginning of previous line.
let i = text.length - 1;
while (i >= 0 && text[i] !== '\n')
i--;
i++;
// Find padding of the line.
let j = i;
while (j < text.length && /[ \t]/.test(text[j]))
j++;
return [text.substring(i, j) || '', i, j];
}
function toString() {
return editor.textContent || '';
}
function preventDefault(event) {
event.preventDefault();
}
function getSelection() {
var _a;
if (((_a = editor.parentNode) === null || _a === void 0 ? void 0 : _a.nodeType) == Node.DOCUMENT_FRAGMENT_NODE) {
return editor.parentNode.getSelection();
}
return window.getSelection();
}
return {
updateOptions(newOptions) {
Object.assign(options, newOptions);
},
updateCode(code) {
editor.textContent = code;
highlight(editor);
},
onUpdate(cb) {
callback = cb;
},
toString,
save,
restore,
recordHistory,
destroy() {
for (let [type, fn] of listeners) {
editor.removeEventListener(type, fn);
}
},
};
}

View File

@@ -0,0 +1,87 @@
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<div class="w-full">
<form class="w-full my-8">
<div v-for="(fieldDefinition, fieldname) in formDefinitions">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<button type="submit" @click.prevent="save()" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Save</button>
</div>
</form>
</div>
</Transition>`,
data() {
return {
formDefinitions: data.userfields,
formData: data.userdata,
userroles: data.userroles,
message: '',
messageClass: '',
errors: {},
}
},
mounted() {
eventBus.$on('forminput', formdata => {
this.formData[formdata.name] = formdata.value;
});
},
methods: {
selectComponent: function(type)
{
return 'component-'+type;
},
save: function()
{
this.reset();
var self = this;
tmaxios.put('/api/v1/account',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'userdata': this.formData
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})

View File

@@ -0,0 +1,31 @@
class Event{
constructor(){
this.events = {};
}
$on(eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
$off(eventName, fn) {
if (this.events[eventName]) {
for (var i = 0; i < this.events[eventName].length; i++) {
if (this.events[eventName][i] === fn) {
this.events[eventName].splice(i, 1);
break;
}
};
}
}
$emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(function(fn) {
fn(data);
});
}
}
};
const eventBus = new Event();

View File

@@ -0,0 +1,118 @@
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<div class="w-full">
<ul>
<li v-for="(theme,themename) in formDefinitions" class="w-full my-4 bg-stone-100">
<div class="w-full p-8">
<div class="w-full">
<h2 class="text-xl font-bold mb-3">{{theme.name}}</h2>
<div class="text-xs my-3">author: <a :href="theme.homepage" class="hover:underline text-teal-500">{{theme.author}}</a> | version: {{theme.version}} | {{theme.licence}}</div>
<p>{{theme.description}}</p>
</div>
<div class="w-full mt-6 flex justify-between">
<button @click="setCurrent(themename)" class="w-half p-3 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Configure</button>
<button class="w-half p-3 bg-teal-500 hover:bg-teal-600 text-white cursor-pointer transition duration-100">Donate/Buy</button>
</div>
</div>
<form class="w-full p-8" v-if="current == themename">
<div v-for="(fieldDefinition, fieldname) in theme.forms.fields">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[themename][subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[themename][fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<div class="w-full">
<button type="submit" @click.prevent="save()" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Save</button>
<button @click.prevent="" class="w-full p-3 my-1 bg-teal-500 hover:bg-teal-600 text-white cursor-pointer transition duration-100">Donate/Buy</button>
</div>
</div>
</form>
</li>
</ul>
</div>
</Transition>`,
data() {
return {
current: '',
formDefinitions: data.plugins,
formData: data.settings,
message: '',
messageClass: '',
errors: {},
userroles: false
}
},
mounted() {
eventBus.$on('forminput', formdata => {
this.formData[this.current][formdata.name] = formdata.value;
});
},
methods: {
setCurrent: function(name)
{
if(this.current == name)
{
this.current = '';
}
else
{
this.current = name;
}
},
selectComponent: function(type)
{
return 'component-'+type;
},
save: function()
{
this.reset();
var self = this;
tmaxios.post('/api/v1/plugin',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'plugin': this.current,
'settings': this.formData[this.current]
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +1,98 @@
const { createApp } = Vue
createApp({
template: `<div><form>MyForm Here message
<div v-for="(field, legend) in formDefinitions">
{{ legend }}
<fieldset v-if="field.type == 'fieldset'" class="fs-formbuilder"><legend>{{field.legend}}</legend>
<component v-for="(subfield, index) in field.fields "
:key="index"
:is="selectComponent(subfield)"
:errors="errors"
:name="index"
:userroles="userroles"
v-model="formdata[index]"
v-bind="subfield">
</component>
</fieldset>
<component v-else
:key="index"
:is="selectComponent(field)"
:errors="errors"
:name="index"
:userroles="userroles"
v-model="formData[index]"
v-bind="field">
</component>
</div>
</form></div>`,
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<form class="inline-block w-full">
<ul class="flex mt-4 mb-4">
<li v-for="tab in tabs" class="">
<button class="px-2 py-2 border-b-2 border-stone-200 hover:border-b-4 hover:bg-stone-200 hover:border-stone-700 transition duration-100" :class="(tab == currentTab) ? 'border-b-4 border-stone-700 bg-stone-200' : ''" @click.prevent="activateTab(tab)">{{tab}}</button>
</li>
</ul>
<div v-for="(fieldDefinition, fieldname) in formDefinitions">
<fieldset class="flex flex-wrap justify-between" :class="(fieldDefinition.legend == currentTab) ? 'block' : 'hidden'" v-if="fieldDefinition.type == 'fieldset'">
<component v-for="(subfieldDefinition, fieldname) in fieldDefinition.fields"
:key="fieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[fieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<input type="submit" @click.prevent="save()" value="save" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">
</div>
</form>
</Transition>`,
data() {
return {
message: 'Add system forms with vue here',
root: document.getElementById("main").dataset.url,
currentTab: 'System',
tabs: [],
formDefinitions: data.system.fields,
formDefinitions: data.system,
formData: data.settings,
formErrors: {},
formErrorsReset: {},
item: false,
userroles: false,
saved: false,
message: '',
messageClass: '',
errors: {},
}
},
computed: {
currentTabComponent: function () {
if(this.currentTab == 'Content')
{
editor.showEditor = 'show';
posts.showPosts = 'show';
}
else
{
editor.showEditor = 'hidden';
posts.showPosts = 'hidden';
return 'tab-' + this.currentTab.toLowerCase()
}
}
},
mounted() {
mounted() {
for (var key in this.formDefinitions)
{
if (this.formDefinitions.hasOwnProperty(key))
{
this.tabs.push(key);
this.formErrors[key] = false;
this.tabs.push(this.formDefinitions[key].legend);
this.errors[key] = false;
}
}
this.formErrorsReset = this.formErrors;
eventBus.$on('forminput', formdata => {
this.formData[formdata.name] = formdata.value;
});
},
methods: {
selectComponent: function(field)
selectComponent: function(type)
{
return 'component-'+field.type;
return 'component-'+type;
},
},
}).mount('#systemsettings')
activateTab: function(tab){
this.currentTab = tab;
},
save: function()
{
this.reset();
var self = this;
tmaxios.post('/api/v1/settings',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'settings': this.formData
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})
/*

View File

@@ -0,0 +1,130 @@
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<div class="w-full">
<ul>
<li v-for="(theme,themename) in formDefinitions" class="w-full my-4 bg-stone-100">
<div class="w-full flex p-8">
<div class="w-1/2 h-64 overflow-hidden">
<img :src="theme.preview" class="w-full">
</div>
<div class="w-1/2 pl-8 flex flex-col">
<div class="w-full">
<h2 class="text-xl font-bold mb-3">{{theme.name}}</h2>
<div class="text-xs my-3">author: <a :href="theme.homepage" class="hover:underline text-teal-500">{{theme.author}}</a> | version: {{theme.version}} | {{theme.licence}}</div>
<p>{{theme.description}}</p>
</div>
<div class="w-full mt-auto flex justify-between">
<button @click="setCurrent(themename)" class="w-half p-3 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Configure</button>
<button class="w-half p-3 bg-teal-500 hover:bg-teal-600 text-white cursor-pointer transition duration-100">Donate/Buy</button>
</div>
</div>
</div>
<form class="w-full p-8" v-if="current == themename">
<div v-for="(fieldDefinition, fieldname) in theme.forms.fields">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[themename][subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[themename][fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<div class="w-full">
<button type="submit" @click.prevent="save()" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Save</button>
<button @click.prevent="" class="w-full p-3 my-1 bg-teal-500 hover:bg-teal-600 text-white cursor-pointer transition duration-100">Donate/Buy</button>
</div>
</div>
</form>
</li>
</ul>
</div>
</Transition>`,
data() {
return {
current: '',
formDefinitions: data.themes,
formData: data.settings,
message: '',
messageClass: '',
errors: {},
userroles: false
}
},
mounted() {
eventBus.$on('forminput', formdata => {
this.formData[this.current][formdata.name] = formdata.value;
});
},
methods: {
setCurrent: function(name)
{
if(this.current == name)
{
this.current = '';
}
else
{
this.current = name;
}
},
selectComponent: function(type)
{
return 'component-'+type;
},
save: function()
{
this.reset();
var self = this;
tmaxios.post('/api/v1/theme',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'theme': this.current,
'settings': this.formData[this.current]
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
self.updateCSS();
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
updateCSS: function()
{
/* check if css has been modified */
/* if so, send to api endpoint */
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})

View File

@@ -0,0 +1,13 @@
app.config.globalProperties.$filters = {
translate(value)
{
if (!value) return ''
translation_key = value.replace(/[ ]/g,"_").replace(/[.]/g, "_").replace(/[,]/g, "_").replace(/[-]/g, "_").replace(/[,]/g,"_").toUpperCase()
translation_value = data.labels[translation_key]
if(!translation_value || translation_value.length === 0){
return value
} else {
return data.labels[translation_key]
}
}
}

View File

@@ -0,0 +1,407 @@
const app = Vue.createApp({
template: `<div class="w-full">
<Transition name="initial" appear>
<searchbox :error="error"></searchbox>
</Transition>
</div>
<div class="w-full overflow-auto">
<Transition name="initial" appear>
<usertable :userdata="userdata"></usertable>
</Transition>
</div>
<ul class="w-full flex mt-4" v-if="showpagination">
<pagination
v-for="page in pages"
v-bind:key="page"
v-bind:page="page"
></pagination>
</ul>`,
data: function () {
return {
usernames: data.usernames,
holdusernames: data.usernames,
userdata: data.userdata,
holduserdata: data.userdata,
userroles: data.userroles,
pagenumber: 1,
pagesize: 10,
pages: 0,
error: false,
}
},
mounted: function(){
this.calculatepages();
},
computed: {
showpagination: function () {
return this.pages != 1;
}
},
methods: {
clear: function(filter)
{
this.usernames = this.holdusernames;
this.userdata = this.holduserdata;
this.calculatepages();
if(this.pages == 1)
{
this.showpagination = false;
}
},
calculatepages: function()
{
this.pages = Math.ceil(this.usernames.length / this.pagesize);
this.pagenumber = 1;
},
getusernamesforpage: function() {
// human-readable page numbers usually start with 1, so we reduce 1 in the first argument
return this.usernames.slice((this.pagenumber - 1) * this.pagesize, this.pagenumber * this.pagesize);
},
getuserdata: function(usernames)
{
var self = this;
tmaxios.get('/api/v1/users/getbynames',{
params: {
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'usernames': usernames,
}
})
.then(function (response) {
self.userdata = response.data.userdata;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
});
},
search: function(term,filter)
{
if(filter == 'username')
{
this.usernames = this.filterItems(this.holdusernames, term);
this.userdata = [];
this.calculatepages();
if(this.usernames.length > 0)
{
let usernames = this.getusernamesforpage();
this.getuserdata(usernames);
}
}
else if(filter == 'usermail')
{
this.usernames = [];
this.userdata = [];
this.calculatepages();
var self = this;
tmaxios.get('/api/v1/users/getbyemail',{
params: {
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'email': term,
}
})
.then(function (response)
{
self.userdata = response.data.userdata;
for(var x = 0; x <= self.userdata.length; x++)
{
self.usernames.push(self.userdata[x].username);
}
self.calculatepages();
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
});
}
else if(filter == 'userrole')
{
this.usernames = [];
this.userdata = [];
this.calculatepages();
var self = this;
tmaxios.get('/api/v1/users/getbyrole',{
params: {
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'role': term,
}
})
.then(function (response)
{
self.userdata = response.data.userdata;
for(var x = 0; x <= self.userdata.length; x++)
{
self.usernames.push(self.userdata[x].username);
}
self.calculatepages();
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
});
}
},
filterItems: function(arr, query)
{
return arr.filter(function(el){
return el.toLowerCase().indexOf(query.toLowerCase()) !== -1
})
},
}
})
app.component('searchbox', {
props: ['usernames', 'error'],
data: function () {
return {
filter: 'username',
searchterm: '',
userroles: data.userroles,
}
},
template: `<div>
<div>
<button @click.prevent="setFilter('username')" :class="checkActive('username')" class="px-2 py-2 border-b-4 hover:bg-stone-200 hover:border-stone-700 transition duration-100">username</button>
<button @click.prevent="setFilter('userrole')" :class="checkActive('userrole')" class="px-2 py-2 border-b-4 hover:bg-stone-200 hover:border-stone-700 transition duration-100">userrole</button>
<button @click.prevent="setFilter('usermail')" :class="checkActive('usermail')" class="px-2 py-2 border-b-4 hover:bg-stone-200 hover:border-stone-700 transition duration-100">e-mail</button>
</div>
<div class="w-100 flex">
<select v-if="this.filter == 'userrole'" v-model="searchterm" class="w-3/4 h-12 px-2 py-3 border border-stone-300 bg-stone-200">
<option v-for="role in userroles">{{role}}</option>
</select>
<input v-else type="text" class="usersearch" v-model="searchterm" class="w-3/4 h-12 px-2 py-3 border border-stone-300 bg-stone-200">
<div class="w-1/4 flex justify-around">
<button class="w-half bg-stone-200 hover:bg-stone-100" @click.prevent="clearSearch()">Clear</button>
<button class="w-half bg-stone-700 hover:bg-stone-900 text-white" @click.prevent="startSearch()">Search</button>
</div>
</div>
<div v-if="error" class="error pt1 f6">{{error}}</div>
<div v-if="this.filter == \'usermail\'" class="text-xs">You can use the asterisk (*) wildcard to search for name@* or *@domain.com.</div>
</div>`,
methods: {
startSearch: function()
{
this.$root.error = false;
if(this.searchterm.trim() != '')
{
if(this.searchterm.trim().length < 3)
{
this.$root.error = 'Please enter at least 3 characters';
return;
}
this.$root.search(this.searchterm, this.filter);
}
},
clearSearch: function()
{
this.$root.error = false;
this.searchterm = '';
this.$root.clear(this.filter);
},
setFilter: function(filter)
{
this.searchterm = '';
this.filter = filter;
if(filter == 'userrole')
{
this.searchterm = this.userroles[0];
}
},
checkActive: function(filter)
{
if(this.filter == filter)
{
return 'border-stone-700 bg-stone-200';
}
return 'border-stone-100 bg-stone-100';
}
}
})
app.component('usertable', {
props: ['userdata'],
template: `<table class="w-full mt-8" cellspacing="0">
<tr>
<th class="p-3 bg-stone-200 border-2 border-stone-50">Username</th>
<th class="p-3 bg-stone-200 border-2 border-stone-50">Userrole</th>
<th class="p-3 bg-stone-200 border-2 border-stone-50">E-Mail</th>
<th class="p-3 bg-stone-200 border-2 border-stone-50">Edit</th>
</tr>
<tr v-for="user,index in userdata" key="username">
<td class="p-3 bg-stone-100 border-2 border-white">{{ user.username }}</td>
<td class="p-3 bg-stone-100 border-2 border-white">{{ user.userrole }}</td>
<td class="p-3 bg-stone-100 border-2 border-white">{{ user.email }}</td>
<td class="p-3 bg-stone-100 border-2 border-white"><a :href="getEditLink(user.username)" class="link tm-red no-underline underline-hover">edit</a></td>
</tr>
</table>`,
methods: {
getEditLink: function(username){
return this.$root.$data.root + '/tm/user/' + username;
},
}
})
app.component('pagination', {
props: ['page'],
template: '<li><button class="p-1 border-2 border-stone-50 hover:bg-stone-200" :class="checkActive()" @click="goto(page)">{{ page }}</button></li>',
methods: {
goto: function(page){
this.$root.$data.pagenumber = page;
let usernames = this.$root.getusernamesforpage();
this.$root.getuserdata(usernames);
},
checkActive: function()
{
if(this.page == this.$root.$data.pagenumber)
{
return 'bg-stone-200';
}
return 'bg-stone-100';
}
}
})
/*
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<div class="w-full">
<ul>
<li v-for="(theme,themename) in formDefinitions" class="w-full my-4 bg-stone-100">
<div class="w-full flex p-8">
<div class="w-1/2 h-64 overflow-hidden">
<img :src="theme.preview" class="w-full">
</div>
<div class="w-1/2 pl-8 flex flex-col">
<div class="w-full">
<h2 class="text-xl font-bold mb-3">{{theme.name}}</h2>
<div class="text-xs my-3">author: <a :href="theme.homepage" class="hover:underline text-teal-500">{{theme.author}}</a> | version: {{theme.version}} | {{theme.licence}}</div>
<p>{{theme.description}}</p>
</div>
<div class="w-full mt-auto flex justify-between">
<button @click="setCurrent(themename)" class="w-half p-3 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Configure</button>
<button class="w-half p-3 bg-teal-500 hover:bg-teal-600 text-white cursor-pointer transition duration-100">Donate/Buy</button>
</div>
</div>
</div>
<form class="w-full p-8" v-if="current == themename">
<div v-for="(fieldDefinition, fieldname) in theme.forms.fields">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[themename][subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[themename][fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<div class="w-full">
<button type="submit" @click.prevent="save()" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Save</button>
<button @click.prevent="" class="w-full p-3 my-1 bg-teal-500 hover:bg-teal-600 text-white cursor-pointer transition duration-100">Donate/Buy</button>
</div>
</div>
</form>
</li>
</ul>
</div>
</Transition>`,
data() {
return {
current: '',
formDefinitions: data.themes,
formData: data.settings,
message: '',
messageClass: '',
errors: {},
userroles: false
}
},
mounted() {
eventBus.$on('forminput', formdata => {
this.formData[this.current][formdata.name] = formdata.value;
});
},
methods: {
setCurrent: function(name)
{
if(this.current == name)
{
this.current = '';
}
else
{
this.current = name;
}
},
selectComponent: function(type)
{
return 'component-'+type;
},
save: function()
{
this.reset();
var self = this;
tmaxios.post('/api/v1/theme',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'theme': this.current,
'settings': this.formData[this.current]
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
self.updateCSS();
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})
*/

View File

@@ -19,9 +19,6 @@
<link rel="stylesheet" href="{{ base_url() }}/system/typemill/author/css/output.css?v={{ settings.version }}" />
<link rel="stylesheet" href="{{ base_url() }}/system/typemill/author/css/custom.css?v={{ settings.version }}" />
<!-- <link rel="stylesheet" href="{{ base_url() }}/system/author/css/tachyons.min.css?v={{ settings.version }}" />
<link rel="stylesheet" href="{{ base_url() }}/system/author/css/style.css?v={{ settings.version }}" />
-->
{{ assets.renderCSS() }}
</head>
@@ -39,7 +36,7 @@
<aside class="w-1/4">
{% include 'partials/systemNavi.twig' %}
</aside>
<article class="w-3/4 bg-stone-50 drop-shadow-md">
<article class="w-3/4 bg-stone-50 drop-shadow-md p-8">
{% block content %}{% endblock %}
</article>
</div>
@@ -52,36 +49,21 @@
<script>
var data = {{ jsdata | json_encode() | raw }}
/* var eventBus = false; */
console.info(data.settings);
const data = {{ jsdata | json_encode() | raw }}
const labels = {{ translations|json_encode() }};
</script>
<script src="{{ base_url() }}/system/typemill/author/js/axios.min.js?v={{ settings.version }}"></script>
<script>
const tmaxios = axios.create();
tmaxios.defaults.baseURL = "{{ base_url() }}";
/* tmaxios.defaults.headers.common['Authorization'] = "Basic {{ basicauth }}"; */
/* header for session authentication in middleware */
tmaxios.defaults.headers.common['X-Session-Auth'] = "true";
/* tmaxios.defaults.headers.common['Authorization'] = "Basic {{ basicauth }}"; */
</script>
<!-- <script src="{{ base_url() }}/system/typemill/author/js/autosize.min.js?v={{ settings.version }}"></script> -->
<script src="{{ base_url() }}/system/typemill/author/js/vue.js?v={{ settings.version }}"></script>
<script>
const { eventBus } = Vue;
</script>
<!--
<script>
var eventBus = false;
const FormBus = new Vue();
</script>
<script src="{{ base_url() }}/system/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/author/js/author.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/author/js/typemillutils.js?v={{ settings.version }}"></script>
<script>
typemillUtilities.start()
</script>
-->
<script src="{{ base_url() }}/system/typemill/author/js/vue-eventbus.js?v={{ settings.version }}"></script>
{% block javascript %}{% endblock %}
{{ assets.renderJS() }}

View File

@@ -5,7 +5,7 @@
<ul class="flex border-l-2 border-stone-200">
{% for name,navitem in mainnavi %}
<li class="border-r-2 border-stone-200">
<a class="inline-block px-4 pt-4 pb-3 border-b-4 hover:border-stone-700 hover:bg-stone-50 focus:bg-stone-50 active:bg-stone-50{{ navitem.active ? ' bg-stone-50 border-stone-700' : ' border-stone-100' }}" href="{{ url_for(navitem.routename) }}">{{ translate(navitem.title)|capitalize }}</a>
<a class="inline-block px-4 pt-4 pb-3 border-b-4 hover:border-stone-700 hover:bg-stone-50 focus:bg-stone-50 active:bg-stone-50 transition duration-100{{ navitem.active ? ' bg-stone-50 border-stone-700' : ' border-stone-100' }}" href="{{ url_for(navitem.routename) }}">{{ translate(navitem.title)|capitalize }}</a>
</li>
{% endfor %}
</ul>

View File

@@ -1,10 +1,10 @@
<nav id="sidebar-menu" class="sidebar-menu">
<div id="mobile-menu" class="menu-action">{{ translate('Menu') }} <span class="button-arrow"></span></div>
<div id="mobile-menu" class="hidden menu-action">{{ translate('Menu') }} <span class="button-arrow"></span></div>
<ul class="mr-4">
{% for name,navitem in systemnavi %}
<li class="mb-1">
<a class="block p-2 border-l-4 border-slate-200 hover:bg-stone-50 hover:border-cyan-500{{ navitem.active ? ' active' : '' }}" href="{{ url_for(navitem.routename) }}">
<a class="block p-2 border-l-4 hover:bg-stone-50 hover:border-teal-500 transition duration-100{{ navitem.active ? ' active bg-stone-50 border-cyan-500' : ' border-slate-200' }}" href="{{ url_for(navitem.routename) }}">
<svg class="icon {{ navitem.icon }} mr-2"><use xlink:href="#{{ navitem.icon }}"></use></svg> {{ translate(name) }}
</a>
</li>

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/layoutSystem.twig' %}
{% block title %}{{ translate('Account') }}{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-4">{{ translate('Account') }} </h1>
<div id="account" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-account.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
app.mount('#account');
</script>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/layoutSystem.twig' %}
{% block title %}{{ translate('Plugin Settings') }}{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-4">{{ translate('Plugins') }} </h1>
<div id="plugins" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-plugins.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
app.mount('#plugins');
</script>
{% endblock %}

View File

@@ -3,32 +3,19 @@
{% block content %}
<div class="formWrapper">
<h1>{{ translate('System') }} </h1>
<div id="systemsettings" v-cloak>
<systemsettings :userdata="userdata">${ message }</systemsettings>
</div>
</div>
<h1 class="text-3xl font-bold mb-4">{{ translate('System') }} </h1>
<div id="system" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-system.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
tmaxios.get('/api/v1/mainnavi',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
})
.then(function (response) {
console.info(response);
})
.catch(function (error) {
console.info(error);
});
</script>
app.mount('#system');
</script>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/layoutSystem.twig' %}
{% block title %}{{ translate('Theme Settings') }}{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-4">{{ translate('Themes') }} </h1>
<div id="themes" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-themes.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
app.mount('#themes');
</script>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/layoutSystem.twig' %}
{% block title %}{{ translate('Users') }}{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-4">{{ translate('Users') }} </h1>
<div id="users" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-users.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
app.mount('#users');
</script>
{% endblock %}

View File

@@ -1,7 +1,23 @@
<?php
use Typemill\Controllers\ControllerSystemApi;
use Typemill\Middleware\RestrictApiAccess;
use Typemill\Controllers\ControllerApiGlobals;
use Typemill\Controllers\ControllerApiSystemSettings;
use Typemill\Controllers\ControllerApiSystemThemes;
use Typemill\Controllers\ControllerApiSystemPlugins;
use Typemill\Controllers\ControllerApiSystemUsers;
$app->get('/api/v1/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new RestrictApiAccess());
$app->get('/api/v1/mainnavi', ControllerApiGlobals::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new RestrictApiAccess());
$app->get('/api/v1/settings', ControllerApiSystemSettings::class . ':getSettings')->setName('api.settings.get')->add(new RestrictApiAccess());
$app->post('/api/v1/settings', ControllerApiSystemSettings::class . ':updateSettings')->setName('api.settings.set')->add(new RestrictApiAccess());
$app->post('/api/v1/theme', ControllerApiSystemThemes::class . ':updateTheme')->setName('api.theme.set')->add(new RestrictApiAccess());
$app->post('/api/v1/plugin', ControllerApiSystemPlugins::class . ':updatePlugin')->setName('api.plugin.set')->add(new RestrictApiAccess());
$app->get('/api/v1/users/getbynames', ControllerApiSystemUsers::class . ':getUsersByNames')->setName('api.usersbynames')->add(new RestrictApiAccess());
$app->get('/api/v1/users/getbyemail', ControllerApiSystemUsers::class . ':getUsersByEmail')->setName('api.usersbyemail')->add(new RestrictApiAccess());
$app->get('/api/v1/users/getbyrole', ControllerApiSystemUsers::class . ':getUsersByRole')->setName('api.usersbyrole')->add(new RestrictApiAccess());
$app->put('/api/v1/account', ControllerApiSystemUsers::class . ':updateUser')->setName('api.user.update')->add(new RestrictApiAccess());
# https://stackoverflow.blog/2021/10/06/best-practices-for-authentication-and-authorization-for-rest-apis/
@@ -19,11 +35,6 @@ use Typemill\Middleware\RestrictApiAccess;
# AUTHORIZATION: apikey username.apikey
$app->get('/api/v1/settings', ControllerSystemApi::class . ':getSettings')->setName('api.settings.get')->add(new RestrictApiAccess());
$app->get('/api/v1/systemnavi', ControllerSystemApi::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new RestrictApiAccess());
$app->get('/api/v1/mainnavi', ControllerSystemApi::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new RestrictApiAccess());
/*
use Typemill\Controllers\ControllerAuthorArticleApi;
use Typemill\Controllers\ControllerAuthorBlockApi;

View File

@@ -1,21 +1,30 @@
<?php
use Typemill\Controllers\ControllerWebFrontend;
#use Typemill\Controllers\ControllerWebFrontend;
use Typemill\Controllers\ControllerWebLogin;
use Typemill\Controllers\ControllerSystem;
use Typemill\Controllers\ControllerWebSystem;
use Typemill\Middleware\RedirectIfAuthenticated;
use Typemill\Middleware\RedirectIfUnauthenticated;
use Slim\Views\TwigMiddleware;
#use Slim\Views\TwigMiddleware;
$app->get('/tm/login', ControllerWebLogin::class . ':show')->setName('auth.show')->add(new RedirectIfAuthenticated($routeParser, $settings));
$app->post('/tm/login', ControllerWebLogin::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($routeParser, $settings));
$app->get('/tm/system', ControllerSystem::class . ':showSettings')->setName('settings.show')->add(new RedirectIfUnauthenticated($routeParser));
$app->get('/tm/themes', ControllerSystem::class . ':showThemes')->setName('themes.show');
$app->get('/tm/plugins', ControllerSystem::class . ':showPlugins')->setName('plugins.show');
$app->get('/tm/account', ControllerSystem::class . ':showAccount')->setName('user.account');
$app->get('/tm/user/new', ControllerSystem::class . ':newUser')->setName('user.new');
$app->get('/tm/users', ControllerSystem::class . ':listUser')->setName('user.list');
$app->get('/tm/system', ControllerWebSystem::class . ':showSettings')->setName('settings.show')->add(new RedirectIfUnauthenticated($routeParser));
$app->get('/tm/themes', ControllerWebSystem::class . ':showThemes')->setName('themes.show')->add(new RedirectIfUnauthenticated($routeParser));
$app->get('/tm/plugins', ControllerWebSystem::class . ':showPlugins')->setName('plugins.show')->add(new RedirectIfUnauthenticated($routeParser));
$app->get('/tm/account', ControllerWebSystem::class . ':showAccount')->setName('user.account')->add(new RedirectIfUnauthenticated($routeParser));
$app->get('/tm/users', ControllerWebSystem::class . ':showUsers')->setName('users.show')->add(new RedirectIfUnauthenticated($routeParser));
# $app->get('/tm/user/new', ControllerSystem::class . ':newUser')->setName('user.new');
# $app->get('/tm/users', ControllerSystem::class . ':listUser')->setName('user.list');
# $app->post('/tm/user/create', ControllerSettings::class . ':createUser')->setName('user.create')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create'));
# $app->post('/tm/user/update', ControllerSettings::class . ':updateUser')->setName('user.update')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'update'));
# $app->post('/tm/user/delete', ControllerSettings::class . ':deleteUser')->setName('user.delete')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'delete'));
# $app->get('/tm/user/{username}', ControllerSettings::class . ':showUser')->setName('user.show')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view'));
@@ -55,8 +64,6 @@ else
$app->post('/tm/formpost', ControllerFrontendForms::class . ':savePublicForm')->setName('form.save');
$app->get('/tm', ControllerFrontendAuth::class . ':redirect');
$app->get('/tm/login', ControllerFrontendAuth::class . ':show')->setName('auth.show')->add(new RedirectIfAuthenticated($container['router'], $container['settings']));
$app->post('/tm/login', ControllerFrontendAuth::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($container['router'], $container['settings']));
$app->get('/tm/logout', ControllerFrontendAuth::class . ':logout')->setName('auth.logout')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
if(isset($settings['settings']['recoverpw']) && $settings['settings']['recoverpw'])
@@ -67,22 +74,22 @@ if(isset($settings['settings']['recoverpw']) && $settings['settings']['recoverpw
$app->post('/tm/recoverpwnew', ControllerFrontendAuth::class . ':createrecoverpasswordnew')->setName('auth.recoverpwnew')->add(new RedirectIfAuthenticated($container['router'], $container['settings']));
}
/*
MIGRATED
$app->get('/tm/settings', ControllerSettings::class . ':showSettings')->setName('settings.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view'));
$app->post('/tm/settings', ControllerSettings::class . ':saveSettings')->setName('settings.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update'));
$app->get('/tm/themes', ControllerSettings::class . ':showThemes')->setName('themes.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view'));
$app->post('/tm/themes', ControllerSettings::class . ':saveThemes')->setName('themes.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update'));
$app->get('/tm/plugins', ControllerSettings::class . ':showPlugins')->setName('plugins.show')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'view'));
$app->post('/tm/plugins', ControllerSettings::class . ':savePlugins')->setName('plugins.save')->add(new accessMiddleware($container['router'], $container['acl'], 'system', 'update'));
$app->get('/tm/account', ControllerSettings::class . ':showAccount')->setName('user.account')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view'));
$app->get('/tm/user/new', ControllerSettings::class . ':newUser')->setName('user.new')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create'));
$app->post('/tm/user/create', ControllerSettings::class . ':createUser')->setName('user.create')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'create'));
$app->post('/tm/user/update', ControllerSettings::class . ':updateUser')->setName('user.update')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'update'));
$app->post('/tm/user/delete', ControllerSettings::class . ':deleteUser')->setName('user.delete')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'delete'));
$app->get('/tm/user/{username}', ControllerSettings::class . ':showUser')->setName('user.show')->add(new accessMiddleware($container['router'], $container['acl'], 'user', 'view'));
$app->get('/tm/users', ControllerSettings::class . ':listUser')->setName('user.list')->add(new accessMiddleware($container['router'], $container['acl'], 'userlist', 'view'));
$app->get('/tm/login', ControllerFrontendAuth::class . ':show')->setName('auth.show')->add(new RedirectIfAuthenticated($container['router'], $container['settings']));
$app->post('/tm/login', ControllerFrontendAuth::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($container['router'], $container['settings']));
$app->get('/tm/content/raw[/{params:.*}]', ControllerAuthorEditor::class . ':showContent')->setName('content.raw')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view'));
$app->get('/tm/content/visual[/{params:.*}]', ControllerAuthorEditor::class . ':showBlox')->setName('content.visual')->add(new accessMiddleware($container['router'], $container['acl'], 'content', 'view'));

View File

@@ -7,6 +7,7 @@ copyright: 'Copyright'
language: 'en'
langattr: 'en'
themeFolder: 'themes'
pluginFolder: 'plugins'
settingsFolder: 'settings'
contentFolder: 'content'
authorFolder: '/system/typemill/author'

View File

@@ -1,50 +1,262 @@
system:
fieldsetsystem:
type: fieldset
legend: System
fields:
fieldsetsystem:
type: fieldset
legend: System
fields:
title:
type: text
label: Website Title
class: medium
maxlength: 60
author:
type: text
label: Author
class: medium
maxlength: 60
copyright:
type: text
label: Copyright
class: medium
maxlength: 60
year:
type: text
label: Year
class: medium
maxlength: 60
fieldsetmedia:
type: fieldset
legend: Media
fields:
title:
type: text
label: Website Title
class: medium
maxlength: 60
author:
type: text
label: Author
class: medium
maxlength: 60
copyright:
type: text
label: Copyright
class: medium
maxlength: 60
year:
type: text
label: Year
class: medium
maxlength: 60
title:
type: text
label: Website Title
maxlength: 60
css: w-half
author:
type: text
label: Author
css: w-half
maxlength: 60
copyright:
type: select
label: Copyright
css: w-half
maxlength: 60
options:
'©': '©'
'CC-BY': 'CC-BY'
'CC-BY-NC': 'CC-BY-NC'
'CC-BY-NC-ND': 'CC-BY-NC-ND'
'CC-BY-NC-SA': 'CC-BY-NC-SA'
'CC-BY-ND': 'CC-BY-ND'
'CC-BY-SA': 'CC-BY-SA'
year:
type: text
label: Year
css: w-half
maxlength: 4
language:
type: select
label: Language (admin ui)
css: w-half
maxlength: 60
options:
'en': 'English'
'ru': 'Russian'
'nl': 'Dutch, Flemish'
'de': 'German'
'it': 'Italian'
'fr': 'French'
langattr:
type: text
label: Language attribute (website)
css: w-half
maxlength: 5
description: Please use ISO 639-1 codes like "en"
sitemap:
type: text
label: Google sitemap (readonly)
css: w-half
readonly: true
description: You can ping the sitemap with the following links to google and bing
pingsitemap:
type: checkbox
label: Ping sitemap
css: w-half
checkboxlabel: Ping sitemap automatically after publishing a page
fieldsetmedia:
type: fieldset
legend: Media
fields:
logo:
type: text
label: Logo
css: w-half
maxlength: 60
favicon:
type: text
label: Favicon
css: w-half
maxlength: 60
width:
type: text
label: Standard width for images
description: This applies only for future images in the content area.
css: w-half
maxlength: 60
height:
type: text
label: Standard height for images
description: If you add a value for the height, then the image will be cropped.
css: w-half
maxlength: 60
svg:
type: checkbox
label: Upload svg images
checkboxlabel: Allow upload of svg images (svg can be malicious, use trusted sources)
css: w-full
maxuploadsize:
type: number
label: Maximum size for file-uploads in MB
description: The maximum file size might be limited by your server settings.
css: w-half
fieldsetwriting:
type: fieldset
legend: Writing
fields:
editor:
type: radio
label: Standard editor mode
css: w-half
options:
'visual': 'visual editor'
'raw': 'raw editor'
formats:
type: checkboxlist
label: Format options for visual editor
css: w-half
options:
'markdown': 'markdown'
'headline': 'headline'
'ulist': 'numbered list'
'olist': 'bullet list'
'table': 'table'
'quote': 'quote'
'notice': 'notice'
'image': 'image'
'video': 'video'
'file': 'file'
'toc': 'table of contents'
'hr': 'horizontal line'
'definition': 'definition list'
'code': 'code'
'shortcode': 'shortcode'
headlineanchors:
type: checkbox
label: Headline anchors
checkboxlabel: Show anchors next to headline in frontend
css: w-full
urlschemes:
type: text
label: Url schemes
description: Add more url schemes for external links e.g. like dict:// (comma separated list)
css: w-full
maxlength: 60
fieldsetaccess:
type: fieldset
legend: Restrictions
fields:
access:
type: checkbox
label: Website restriction
checkboxlabel: Show the website only to authenticated users and redirect all other users to the login page.
css: w-full
pageaccess:
type: checkbox
label: Page restriction
checkboxlabel: Activate individual restrictions for pages in the meta-tab of each page.
css: w-full
hrdelimiter:
type: checkbox
label: Content break
checkboxlabel: Cut restricted content after the first hr-element on a page (per default content will be cut after title).
css: w-full
restrictionnotice:
type: textarea
label: Restriction notice (use markdown)
css: w-full
maxlength: 2000
wraprestrictionnotice:
type: checkbox
label: Wrap restriction notice
checkboxlabel: Wrap the restriction notice above into a notice-4 element (which can be designed as special box)
css: w-full
fieldsetrecovery:
type: fieldset
legend: PW recovery
fields:
recoverpw:
type: checkbox
label: Recover password
checkboxlabel: Activate the password recovery.
css: w-full
recoverfrom:
type: text
label: Sender email
placeholder: your@email.org
css: w-full
maxlength: 60
recoversubject:
type: text
label: Email subject
placeholder: Recover your password
css: w-full
maxlength: 60
recovermessage:
type: textarea
label: Text before recover link in email message
css: w-full
maxlength: 2000
fieldsetdeveloper:
type: fieldset
legend: Developer
fields:
displayErrorDetails:
type: checkbox
label: Error reporting
checkboxlabel: Display application errors
css: w-full
securitylog:
type: checkbox
label: Security log
checkboxlabel: Track spam and suspicious actions in a logfile
css: w-full
twigcache:
type: checkbox
label: Twig cache
checkboxlabel: Activate the cache for twig templates
css: w-full
refreshcache:
type: checkbox
label: Refresh cache
checkboxlabel: Refresh the cache every 10 minutes. Use this option if you change content-files via FTP.
css: w-full
proxy:
type: checkbox
label: Proxy
checkboxlabel: Use x-forwarded-header.
css: w-half
trustedproxies:
type: text
label: Trusted IPs for proxies (comma separated)
css: w-half
headersoff:
type: checkbox
label: Disable headers
checkboxlabel: Disable the typemill headers and send your owwn
css: w-full
fieldsetapi:
type: fieldset
legend: API
fields:
api:
type: checkbox
label: Activate api
checkboxlabel: Activate the api
css: w-full
apikey:
type: text
label: Api key
css: w-full
apirole:
type: select
label: Api role
css: w-full
options:
'admin': 'admin'
'editor': 'editor'
'author': 'author'
'member': 'member'
fieldsetlicence:
type: fieldset
legend: Licence
fields:
licencekey:
type: text
label: Your licence for maker or business features
css: w-full

View File

@@ -24,7 +24,7 @@
'aclprivilege': 'view'
'users':
'title': 'Users'
'routename': 'user.list'
'routename': 'users.show'
'icon': 'icon-group'
'aclresource': 'userlist'
'aclprivilege': 'view'

View File

@@ -0,0 +1,34 @@
username:
name: username
label: 'Username (read only)'
type: 'text'
readonly: true
firstname:
name: firstname
label: 'First Name'
type: 'text'
lastname:
name: lastname
label: 'Last Name'
type: 'text'
email:
name: email
label: 'E-Mail'
type: 'text'
required: true
userrole:
name: userrole
label: 'Role'
type: 'text'
readonly: true
password:
name: password
label: 'Actual Password'
type: 'password'
autocomplete: 'new-password'
newpassword:
name: newpassword
label: 'New Password'
type: 'password'
autocomplete: 'new-password'
generator: true

View File

@@ -17,6 +17,7 @@ use Typemill\Static\Plugins;
use Typemill\Static\Translations;
use Typemill\Static\Permissions;
use Typemill\Static\Session;
use Typemill\Static\Helpers;
use Typemill\Events\OnSettingsLoaded;
use Typemill\Events\OnPluginsLoaded;
use Typemill\Events\OnSessionSegmentsLoaded;
@@ -29,8 +30,6 @@ use Typemill\Extensions\TwigUrlExtension;
use Typemill\Extensions\TwigUserExtension;
use Typemill\Models\StorageWrapper;
# require __DIR__ . '/../vendor/autoload.php';
$timer = [];
$timer['start'] = microtime(true);
@@ -65,10 +64,7 @@ if(isset($settings['displayErrorDetails']) && $settings['displayErrorDetails'])
# ADD THEM TO THE SETTINGS AND YOU HAVE THEM EVERYWHERE??
$uriFactory = new UriFactory();
$uri = $uriFactory->createFromGlobals($_SERVER);
$settings['fullpath'] = $uri->getPath();
$settings['basepath'] = preg_replace('/(.*)\/.*/', '$1', $_SERVER['SCRIPT_NAME']);
$settings['routepath'] = str_replace($settings['basepath'], '', $settings['fullpath']);
$urlinfo = Helpers::urlInfo($uri);
$timer['settings'] = microtime(true);
@@ -89,8 +85,11 @@ $routeParser = $app->getRouteCollector()->getRouteParser();
# add route parser to container to use named routes in controller
$container->set('routeParser', $routeParser);
# set urlinfo
$container->set('urlinfo', $urlinfo);
# in slim 4 you alsways have to set application basepath
$app->setBasePath($settings['basepath']);
$app->setBasePath($urlinfo['basepath']);
$timer['container'] = microtime(true);
@@ -189,7 +188,7 @@ $timer['permissions'] = microtime(true);
if( ( isset($settings['access']) && $settings['access'] ) || ( isset($settings['pageaccess']) && $settings['pageaccess'] ) )
{
# activate session for all routes
$session_segments = [$settings['routepath']];
$session_segments = [$urlinfo['route']];
}
else
{
@@ -201,7 +200,7 @@ else
}
# start session
Session::startSessionForSegments($session_segments, $settings['routepath']);
Session::startSessionForSegments($session_segments, $urlinfo['route']);
$timer['session segments'] = microtime(true);
@@ -209,11 +208,15 @@ $timer['session segments'] = microtime(true);
* OTHER CONTAINER ITEMS *
****************************/
# translations
$translations = Translations::loadTranslations($settings, $urlinfo['route']);
$container->set('translations', $translations);
# dispatcher to container
$container->set('dispatcher', function() use ($dispatcher){ return $dispatcher; });
# asset function for plugins
$container->set('assets', function() use ($settings){ return new \Typemill\Assets($settings['basepath']); });
$container->set('assets', function() use ($urlinfo){ return new \Typemill\Assets($urlinfo['basepath']); });
# Register Middleware On Container
$csrf = false;
@@ -230,13 +233,12 @@ if(isset($_SESSION))
# $app->add(new ValidationErrors($container->get('view')));
}
/****************************
* TWIG TO CONTAINER *
****************************/
$container->set('view', function() use ($settings, $csrf, $uri) {
$container->set('view', function() use ($settings, $csrf, $urlinfo, $translations) {
$twig = Twig::create(
[
# path to templates
@@ -256,7 +258,7 @@ $container->set('view', function() use ($settings, $csrf, $uri) {
# add extensions
$twig->addExtension(new DebugExtension());
$twig->addExtension(new TwigUserExtension());
$twig->addExtension(new TwigUrlExtension($uri, $settings['basepath']));
$twig->addExtension(new TwigUrlExtension($urlinfo));
# $twig->addExtension(new \Nquire\Extensions\TwigUserExtension());
@@ -265,9 +267,6 @@ $container->set('view', function() use ($settings, $csrf, $uri) {
$twig->addExtension(new TwigCsrfExtension($csrf));
}
# translations
$translations = Translations::loadTranslations($settings);
# $twig->getEnvironment()->addGlobal('translations', $translations);
$twig->addExtension(new Typemill\Extensions\TwigLanguageExtension( $translations ));
return $twig;

View File

@@ -2,7 +2,11 @@
module.exports = {
content: ["./system/typemill/author/**/*.{html,js,twig}"],
theme: {
extend: {},
extend: {
width: {
'half': '48%',
}
},
},
plugins: [],
}
}

View File

@@ -18,31 +18,29 @@ settings:
forms:
fields:
layoutsize:
type: select
label: Layout Size
options:
standard: Standard
large: Large
full: Full Width
posts:
layout:
type: fieldset
legend: Configure Posts
legend: General Layout
fields:
layoutsize:
type: select
label: Layout Size
options:
standard: Standard
large: Large
full: Full Width
blogimage:
type: checkbox
label: Post-Images
checkboxlabel: Generally show hero images in all lists of posts
blog:
type: checkbox
checkboxlabel: Activate a list of posts on the homepage
bloghomepage:
type: fieldset
legend: Posts on Homepage
fields:
blog:
type: checkbox
checkboxlabel: Activate a list of posts on the homepage
blogintro:
type: checkbox
label: Intro Content
@@ -52,9 +50,13 @@ forms:
label: Enter the folder path with the posts
placeholder: /blog
landingpage:
type: checkbox
checkboxlabel: Activate a landing page with segments on the homepage
landing:
type: fieldset
legend: Landingpage
fields:
landingpage:
type: checkbox
checkboxlabel: Activate a landing page with segments on the homepage
landingpageIntro:
type: fieldset
@@ -64,11 +66,13 @@ forms:
type: number
label: Position of Intro Segment
description: Use 0 to disable the section
css: 'w-half'
introTitle:
type: text
label: Title for your landingpage intro
placeholder: Typemill
description: Leave empty to use the title of your base content page.
css: 'w-half'
introMarkdown:
type: textarea
label: Text for your landingpage intro (use markdown)
@@ -77,10 +81,12 @@ forms:
type: text
label: Link for startbutton
placeholder: /my/deeplink
css: 'w-half'
introButtonLabel:
type: text
label: Label for startbutton
placeholder: my label
css: 'w-half'
introFullsize:
type: checkbox
label: Full Screen
@@ -117,45 +123,51 @@ forms:
teaser1title:
type: text
label: Teaser 1 Title
css: 'w-half'
teaser1text:
type: text
label: Teaser 1 Text
css: 'w-half'
teaser1link:
type: text
label: Teaser 1 Link
fieldsize: half
css: 'w-half'
teaser1label:
type: text
label: Teaser 1 Label
fieldsize: half
css: 'w-half'
teaser2title:
type: text
label: Teaser 2 Title
css: 'w-half'
teaser2text:
type: text
label: Teaser 2 Text
css: 'w-half'
teaser2link:
type: text
label: Teaser 2 Link
fieldsize: half
css: 'w-half'
teaser2label:
type: text
label: Teaser 2 Label
fieldsize: half
css: 'w-half'
teaser3title:
type: text
label: Teaser 3 Title
css: 'w-half'
teaser3text:
type: text
label: Teaser 3 Text
css: 'w-half'
teaser3link:
type: text
label: Teaser 3 Link
fieldsize: half
css: 'w-half'
teaser3label:
type: text
label: Teaser 3 Label
fieldsize: half
css: 'w-half'
landingpageContrast:
type: fieldset
@@ -174,9 +186,11 @@ forms:
contrastLink:
type: text
label: Button Link
css: 'w-half'
contrastLabel:
type: text
label: Button Label
css: 'w-half'
landingpageNavi:
type: fieldset
@@ -201,19 +215,23 @@ forms:
type: number
label: Position of News Segment
description: Use 0 to disable the section
css: 'w-half'
newsHeadline:
type: text
label: Headline for news-segment
placeholder: News
css: 'w-half'
newsFolder:
type: text
label: List entries from folder
placeholder: /blog
description: Add a path to a folder from which you want to list entries
css: 'w-half'
newsLabel:
type: text
label: Label for read more link
placeholder: All News
css: 'w-half'
fieldsetAuthor:
type: fieldset
@@ -246,10 +264,12 @@ forms:
type: text
label: Date intro text
placeholder: Last Updated
css: 'w-half'
dateFormat:
type: select
label: Date format
css: 'w-half'
options:
'm/d/Y': 01/20/2020
'd.m.Y': 20.01.2020
@@ -318,18 +338,22 @@ forms:
type: text
label: Label for expand button
placeholder: expand navigation
css: 'w-half'
collapse:
type: text
label: Label for collapse button
placeholder: collapse navigation
css: 'w-half'
next:
type: text
label: Label for next link
placeholder: next
css: 'w-half'
previous:
type: text
label: Label for previous link
placeholder: previous
css: 'w-half'
fieldsetfooter:
type: fieldset
@@ -420,6 +444,7 @@ forms:
calisto,serif: calisto (serif)
garamond,serif: garamond (serif)
baskerville,serif: baskerville (serif)
fieldsetColors:
type: fieldset
legend: Colors
@@ -428,83 +453,83 @@ forms:
type: text
label: Background color for body
placeholder: 'leightseagreen'
fieldsize: half
css: 'w-half'
fontcolorprimary:
type: text
label: Font color for body
placeholder: 'white'
fieldsize: half
css: 'w-half'
newsbackground:
type: text
label: Background color for news-box
placeholder: 'white'
fieldsize: half
css: 'w-half'
newscolor:
type: text
label: Font color for news-box
placeholder: '#333'
fieldsize: half
css: 'w-half'
brandcolortertiary:
type: text
label: Background color for buttons
placeholder: 'lightseagreen'
fieldsize: half
css: 'w-half'
fontcolortertiary:
type: text
label: Font color for buttons
placeholder: '#F7F7F7'
fieldsize: half
css: 'w-half'
bordercolortertiary:
type: text
label: Border color for buttons
placeholder: '#F7F7F7'
fieldsize: half
css: 'w-half'
fontcolorlink:
type: text
label: Font color for content links
placeholder: '#007F7F'
fieldsize: half
css: 'w-half'
brandcolorsecondary:
type: text
label: Background color for content
placeholder: '#f7f7f7'
fieldsize: half
css: 'w-half'
fontcolorsecondary:
type: text
label: Font color for content
placeholder: '#333'
fieldsize: half
css: 'w-half'
codebackground:
type: text
label: Background color for code
placeholder: '#ddd'
fieldsize: half
css: 'w-half'
codecolor:
type: text
label: Font color for code
placeholder: '#333'
fieldsize: half
css: 'w-half'
contentnavihoverbackground:
type: text
label: Background color for hover of content navigation
placeholder: 'lightseagreen'
fieldsize: half
css: 'w-half'
contentnavihovercolor:
type: text
label: Font color for hover of content navigation
placeholder: 'white'
fieldsize: half
css: 'w-half'
thinbordercolor:
type: text
label: Thin border color
placeholder: 'lightgray'
description: Used for content navigation, table and horizontal line
fieldsize: half
css: 'w-half'
noticecolors:
type: checkbox
label: Color for notices
checkboxlabel: Use grayscale color schema for notices
fieldsize: half
css: 'w-half'
metatabs:
meta: