From e0b1a0a94f77d42c03d670e733adde66cd870284 Mon Sep 17 00:00:00 2001 From: trendschau Date: Tue, 6 Dec 2022 21:26:30 +0100 Subject: [PATCH] Settings area V2 --- ...SystemApi.php => ControllerApiGlobals.php} | 20 +- .../ControllerApiSystemPlugins.php | 54 + .../ControllerApiSystemSettings.php | 76 + .../Controllers/ControllerApiSystemThemes.php | 54 + .../Controllers/ControllerApiSystemUsers.php | 337 ++++ .../typemill/Controllers/ControllerData.php | 189 ++- .../typemill/Controllers/ControllerSystem.php | 1254 -------------- system/typemill/Controllers/ControllerWeb.php | 73 - .../Controllers/ControllerWebSystem.php | 497 ++++++ .../typemill/Extensions/TwigUrlExtension.php | 20 +- system/typemill/Middleware/JsonBodyParser.php | 2 - .../Middleware/RedirectIfUnauthenticated.php | 30 +- .../typemill/Middleware/RestrictApiAccess.php | 35 +- system/typemill/Models/Storage.php | 12 +- system/typemill/Models/User.php | 173 +- system/typemill/Models/Validation.php | 62 +- system/typemill/Static/Helpers.php | 23 + system/typemill/Static/Translations.php | 4 +- system/typemill/author/auth/login.twig | 2 +- system/typemill/author/css/custom.css | 12 + system/typemill/author/css/output.css | 446 ++++- system/typemill/author/js/codejar.js | 449 +++++ system/typemill/author/js/vue-account.js | 87 + system/typemill/author/js/vue-eventbus.js | 31 + system/typemill/author/js/vue-plugins.js | 118 ++ system/typemill/author/js/vue-shared.js | 1457 +++++++++++++++++ system/typemill/author/js/vue-system.js | 137 +- system/typemill/author/js/vue-themes.js | 130 ++ system/typemill/author/js/vue-translate.js | 13 + system/typemill/author/js/vue-users.js | 407 +++++ .../typemill/author/layouts/layoutSystem.twig | 32 +- system/typemill/author/partials/mainNavi.twig | 2 +- .../typemill/author/partials/systemNavi.twig | 4 +- system/typemill/author/system/account.twig | 21 + system/typemill/author/system/plugins.twig | 21 + system/typemill/author/system/system.twig | 31 +- system/typemill/author/system/themes.twig | 21 + system/typemill/author/system/users.twig | 21 + system/typemill/routes/api.php | 23 +- system/typemill/routes/web.php | 47 +- system/typemill/settings/defaults.yaml | 1 + system/typemill/settings/system.yaml | 310 +++- system/typemill/settings/systemnavi.yaml | 2 +- system/typemill/settings/user.yaml | 34 + system/typemill/system.php | 33 +- tailwind.config.js | 8 +- themes/cyanine/cyanine.yaml | 103 +- 47 files changed, 5121 insertions(+), 1797 deletions(-) rename system/typemill/Controllers/{ControllerSystemApi.php => ControllerApiGlobals.php} (83%) create mode 100644 system/typemill/Controllers/ControllerApiSystemPlugins.php create mode 100644 system/typemill/Controllers/ControllerApiSystemSettings.php create mode 100644 system/typemill/Controllers/ControllerApiSystemThemes.php create mode 100644 system/typemill/Controllers/ControllerApiSystemUsers.php delete mode 100644 system/typemill/Controllers/ControllerSystem.php delete mode 100644 system/typemill/Controllers/ControllerWeb.php create mode 100644 system/typemill/Controllers/ControllerWebSystem.php create mode 100644 system/typemill/author/js/codejar.js create mode 100644 system/typemill/author/js/vue-account.js create mode 100644 system/typemill/author/js/vue-eventbus.js create mode 100644 system/typemill/author/js/vue-plugins.js create mode 100644 system/typemill/author/js/vue-shared.js create mode 100644 system/typemill/author/js/vue-themes.js create mode 100644 system/typemill/author/js/vue-translate.js create mode 100644 system/typemill/author/js/vue-users.js create mode 100644 system/typemill/author/system/account.twig create mode 100644 system/typemill/author/system/plugins.twig create mode 100644 system/typemill/author/system/themes.twig create mode 100644 system/typemill/author/system/users.twig create mode 100644 system/typemill/settings/user.yaml diff --git a/system/typemill/Controllers/ControllerSystemApi.php b/system/typemill/Controllers/ControllerApiGlobals.php similarity index 83% rename from system/typemill/Controllers/ControllerSystemApi.php rename to system/typemill/Controllers/ControllerApiGlobals.php index 45397fe..68ae64d 100644 --- a/system/typemill/Controllers/ControllerSystemApi.php +++ b/system/typemill/Controllers/ControllerApiGlobals.php @@ -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); + } } \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiSystemPlugins.php b/system/typemill/Controllers/ControllerApiSystemPlugins.php new file mode 100644 index 0000000..92e8d9b --- /dev/null +++ b/system/typemill/Controllers/ControllerApiSystemPlugins.php @@ -0,0 +1,54 @@ +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); + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiSystemSettings.php b/system/typemill/Controllers/ControllerApiSystemSettings.php new file mode 100644 index 0000000..6d6b11d --- /dev/null +++ b/system/typemill/Controllers/ControllerApiSystemSettings.php @@ -0,0 +1,76 @@ +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); + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiSystemThemes.php b/system/typemill/Controllers/ControllerApiSystemThemes.php new file mode 100644 index 0000000..b1d5e3f --- /dev/null +++ b/system/typemill/Controllers/ControllerApiSystemThemes.php @@ -0,0 +1,54 @@ +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); + } +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerApiSystemUsers.php b/system/typemill/Controllers/ControllerApiSystemUsers.php new file mode 100644 index 0000000..bfdf7f0 --- /dev/null +++ b/system/typemill/Controllers/ControllerApiSystemUsers.php @@ -0,0 +1,337 @@ +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); + } + } + */ +} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerData.php b/system/typemill/Controllers/ControllerData.php index 2a883ce..62ccb81 100644 --- a/system/typemill/Controllers/ControllerData.php +++ b/system/typemill/Controllers/ControllerData.php @@ -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]; + } + } + } + } } \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerSystem.php b/system/typemill/Controllers/ControllerSystem.php deleted file mode 100644 index 140268b..0000000 --- a/system/typemill/Controllers/ControllerSystem.php +++ /dev/null @@ -1,1254 +0,0 @@ -c->get('settings'); - $defaultSettings = \Typemill\Settings::getDefaultSettings(); - $copyright = $this->getCopyright(); - $languages = $this->getLanguages(); - $locale = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? substr($_SERVER["HTTP_ACCEPT_LANGUAGE"],0,2) : 'en'; - $route = $request->getAttribute('route'); - $navigation = $this->getMainNavigation(); - - # set navigation active - $navigation['System']['active'] = true; - - return $this->render($response, 'settings/system.twig', array( - 'settings' => $settings, - 'acl' => $this->c->acl, - 'navigation' => $navigation, - 'copyright' => $copyright, - 'languages' => $languages, - 'locale' => $locale, - 'formats' => $defaultSettings['formats'], - 'route' => $route->getName() - )); - - */ - -/* ENDPOINTS - - -> settings -> get - -> systemnavi -> get - -> mainnavi -> get - -> - -*/ - $user = new User(); - $user->setUser($_SESSION['username']); - $userdata = $user->getUserData(); - - $yaml = new Yaml('\Typemill\Models\Storage'); - $system = $yaml->getYaml('system/typemill/settings', 'system.yaml'); - - return $this->c->get('view')->render($response, 'system/system.twig', [ - 'basicauth' => $user->getBasicAuth(), - 'settings' => $this->settings, - 'mainnavi' => $this->getMainNavigation($userdata['userrole']), - 'systemnavi' => $this->getSystemNavigation($userdata['userrole']), - 'jsdata' => [ - 'settings' => $this->settings, - 'system' => $system, - ] - - # main navigation - # sidebar navigation - # settings area - - #'captcha' => $this->checkIfAddCaptcha(), - ]); - } - - -/* - public function showBlank($request, $response, $args) - { - $user = new User(); - $settings = $this->c->get('settings'); - $route = $request->getAttribute('route'); - $navigation = $this->getMainNavigation(); - - $content = '

Hello

I am the showBlank method from the settings controller

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.

'; - - return $this->render($response, 'settings/blank.twig', array( - 'settings' => $settings, - 'acl' => $this->c->acl, - 'navigation' => $navigation, - 'content' => $content, - 'route' => $route->getName() - )); - } - - - public function saveSettings($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('settings.show')); - } - - $settings = \Typemill\Settings::getUserSettings(); - $defaultSettings = \Typemill\Settings::getDefaultSettings(); - $params = $request->getParams(); - $files = $request->getUploadedFiles(); - $newSettings = isset($params['settings']) ? $params['settings'] : false; - $validate = new Validation(); - $processImage = new ProcessImage(); - - if($newSettings) - { - # check for image settings - $imgwidth = isset($newSettings['images']['live']['width']) ? $newSettings['images']['live']['width'] : false; - $imgheight = isset($newSettings['images']['live']['height']) ? $newSettings['images']['live']['height'] : false; - - # make sure only allowed fields are stored - $newSettings = array( - 'title' => $newSettings['title'], - 'author' => $newSettings['author'], - 'copyright' => $newSettings['copyright'], - 'year' => $newSettings['year'], - 'language' => $newSettings['language'], - 'langattr' => $newSettings['langattr'], - 'editor' => $newSettings['editor'], - 'formats' => $newSettings['formats'], - 'access' => isset($newSettings['access']) ? true : null, - 'pageaccess' => isset($newSettings['pageaccess']) ? true : null, - 'hrdelimiter' => isset($newSettings['hrdelimiter']) ? true : null, - 'restrictionnotice' => $newSettings['restrictionnotice'], - 'wraprestrictionnotice' => isset($newSettings['wraprestrictionnotice']) ? true : null, - 'headlineanchors' => isset($newSettings['headlineanchors']) ? $newSettings['headlineanchors'] : null, - 'displayErrorDetails' => isset($newSettings['displayErrorDetails']) ? true : null, - 'twigcache' => isset($newSettings['twigcache']) ? true : null, - 'proxy' => isset($newSettings['proxy']) ? true : null, - 'trustedproxies' => $newSettings['trustedproxies'], - 'headersoff' => isset($newSettings['headersoff']) ? true : null, - 'urlschemes' => $newSettings['urlschemes'], - 'svg' => isset($newSettings['svg']) ? true : null, - 'recoverpw' => isset($newSettings['recoverpw']) ? true : null, - 'recoverfrom' => $newSettings['recoverfrom'], - 'recoversubject' => $newSettings['recoversubject'], - 'recovermessage' => $newSettings['recovermessage'], - 'securitylog' => isset($newSettings['securitylog']) ? true : null, - 'oldslug' => isset($newSettings['oldslug']) ? true : null, - 'refreshcache' => isset($newSettings['refreshcache']) ? true : null, - 'pingsitemap' => isset($newSettings['pingsitemap']) ? true : null, - ); - - # https://www.slimframework.com/docs/v3/cookbook/uploading-files.html; - - $copyright = $this->getCopyright(); - - $validate->settings($newSettings, $copyright, $defaultSettings['formats'], 'settings'); - - # use custom image settings - if( $imgwidth && ctype_digit($imgwidth) && (strlen($imgwidth) < 5) ) - { - $newSettings['images']['live']['width'] = $imgwidth; - } - if( $imgheight && ctype_digit($imgheight) && (strlen($imgheight) < 5) ) - { - $newSettings['images']['live']['height'] = $imgheight; - } - } - else - { - $this->c->flash->addMessage('error', 'Wrong Input'); - return $response->withRedirect($this->c->router->pathFor('settings.show')); - } - - if(isset($_SESSION['errors'])) - { - $this->c->flash->addMessage('error', 'Please correct the errors'); - return $response->withRedirect($this->c->router->pathFor('settings.show')); - } - - if(!$processImage->checkFolders()) - { - $this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.'); - return $response->withRedirect($this->c->router->pathFor('settings.show')); - } - - # handle single input with single file upload - $logo = $files['settings']['logo']; - $allowed = ['jpg', 'jpeg', 'png', 'svg']; - if($logo->getError() === UPLOAD_ERR_OK) - { - $extension = pathinfo($logo->getClientFilename(), PATHINFO_EXTENSION); - if(!in_array(strtolower($extension), $allowed)) - { - $_SESSION['errors']['settings']['logo'] = array('Only jpg, jpeg, png and svg allowed'); - $this->c->flash->addMessage('error', 'Please correct the errors'); - return $response->withRedirect($this->c->router->pathFor('settings.show')); - } - - foreach($allowed as $logoextension) - { - $processImage->deleteImage('logo.' . $logoextension); - } - - $newSettings['logo'] = $processImage->moveUploadedImage($logo, $overwrite = true, $name = 'logo'); - $processImage->copyImage('logo.' . $logoextension, $processImage->liveFolder, $processImage->thumbFolder); - } - elseif(isset($params['settings']['deletelogo']) && $params['settings']['deletelogo'] == 'delete') - { - foreach($allowed as $logoextension) - { - $processImage->deleteImage('logo.' . $logoextension); - } - $newSettings['logo'] = ''; - } - else - { - $newSettings['logo'] = isset($settings['logo']) ? $settings['logo'] : ''; - } - - # handle single input with single file upload - $favicon = $files['settings']['favicon']; - if ($favicon->getError() === UPLOAD_ERR_OK) - { - $extension = pathinfo($favicon->getClientFilename(), PATHINFO_EXTENSION); - if(strtolower($extension) != 'png') - { - $_SESSION['errors']['settings']['favicon'] = array('Only .png-files allowed'); - $this->c->flash->addMessage('error', 'Please correct the errors'); - return $response->withRedirect($this->c->router->pathFor('settings.show')); - } - - $processFavImage = new ProcessImage([ - '16' => ['width' => 16, 'height' => 16], - '32' => ['width' => 32, 'height' => 32], - '72' => ['width' => 72, 'height' => 72], - '114' => ['width' => 114, 'height' => 114], - '144' => ['width' => 144, 'height' => 144], - '180' => ['width' => 180, 'height' => 180], - ]); - $favicons = $processFavImage->generateSizesFromImageFile('favicon.png', $favicon->file); - - foreach($favicons as $key => $favicon) - { - imagepng( $favicon, $processFavImage->fileFolder . 'favicon-' . $key . '.png' ); - } - - $newSettings['favicon'] = 'favicon'; - } - elseif(isset($params['settings']['deletefav']) && $params['settings']['deletefav'] == 'delete') - { - $processFiles = new ProcessFile(); - $processFiles->deleteFileWithName('favicon-*.png'); - $newSettings['favicon'] = ''; - } - else - { - $newSettings['favicon'] = isset($settings['favicon']) ? $settings['favicon'] : ''; - } - - # store updated settings - \Typemill\Settings::updateSettings(array_merge($settings, $newSettings)); - - $this->c->flash->addMessage('info', 'Settings are stored'); - return $response->withRedirect($this->c->router->pathFor('settings.show')); - } - } - - /********************* - ** THEME SETTINGS ** - *********************/ - -/* - public function showThemes($request, $response, $args) - { - $userSettings = $this->c->get('settings'); - $themes = $this->getThemes(); - $themedata = array(); - $fieldsModel = new Fields($this->c); - - foreach($themes as $themeName) - { - # if theme is active, list it first - if($userSettings['theme'] == $themeName) - { - $themedata = array_merge(array($themeName => null), $themedata); - } - else - { - $themedata[$themeName] = null; - } - - $themeSettings = \Typemill\Settings::getObjectSettings('themes', $themeName); - - # 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.']; - - # load custom css-file - $write = new write(); - $customcss = $write->getFile('cache', $themeName . '-custom.css'); - $themeSettings['settings']['customcss'] = $customcss; - - - if($themeSettings) - { - # store them as default theme data with author, year, default settings and field-definitions - $themedata[$themeName] = $themeSettings; - } - - if(isset($themeSettings['forms']['fields'])) - { - $fields = $fieldsModel->getFields($userSettings, 'themes', $themeName, $themeSettings); - - # overwrite original theme form definitions with enhanced form objects - $themedata[$themeName]['forms']['fields'] = $fields; - } - - # add the preview image - $img = getcwd() . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $themeName . DIRECTORY_SEPARATOR . $themeName; - - $image = false; - if(file_exists($img . '.jpg')) - { - $image = $themeName . '.jpg'; - } - if(file_exists($img . '.png')) - { - $image = $themeName . '.png'; - } - - $themedata[$themeName]['img'] = $image; - } - - # add the users for navigation - $route = $request->getAttribute('route'); - $navigation = $this->getMainNavigation(); - - # set navigation active - $navigation['Themes']['active'] = true; - - return $this->render($response, 'settings/themes.twig', array( - 'settings' => $userSettings, - 'acl' => $this->c->acl, - 'navigation' => $navigation, - 'themes' => $themedata, - 'route' => $route->getName() - )); - } - - public function showPlugins($request, $response, $args) - { - $userSettings = $this->c->get('settings'); - $plugins = array(); - $fieldsModel = new Fields($this->c); - $fields = array(); - - # iterate through the plugins in the stored user settings - foreach($userSettings['plugins'] as $pluginName => $pluginUserSettings) - { - # add plugin to plugin Data, if active, set it first - # if plugin is active, list it first - if($userSettings['plugins'][$pluginName]['active'] == true) - { - $plugins = array_merge(array($pluginName => null), $plugins); - } - else - { - $plugins[$pluginName] = Null; - } - - # Check if the user has deleted a plugin. Then delete it in the settings and store the updated settings. - if(!is_dir($userSettings['rootPath'] . 'plugins' . DIRECTORY_SEPARATOR . $pluginName)) - { - # remove the plugin settings and store updated settings - \Typemill\Settings::removePluginSettings($pluginName); - continue; - } - - # load the original plugin definitions from the plugin folder (author, version and stuff) - $pluginOriginalSettings = \Typemill\Settings::getObjectSettings('plugins', $pluginName); - if($pluginOriginalSettings) - { - # store them as default plugin data with plugin author, plugin year, default settings and field-definitions - $plugins[$pluginName] = $pluginOriginalSettings; - } - - # check, if the plugin has been disabled in the form-session-data - if(isset($_SESSION['old']) && !isset($_SESSION['old'][$pluginName]['active'])) - { - $plugins[$pluginName]['settings']['active'] = false; - } - - # if the plugin defines forms and fields, so that the user can edit the plugin settings in the frontend - if(isset($pluginOriginalSettings['forms']['fields'])) - { - # get all the fields and prefill them with the dafault-data, the user-data or old input data - $fields = $fieldsModel->getFields($userSettings, 'plugins', $pluginName, $pluginOriginalSettings); - - # overwrite original plugin form definitions with enhanced form objects - $plugins[$pluginName]['forms']['fields'] = $fields; - } - } - - $route = $request->getAttribute('route'); - $navigation = $this->getMainNavigation(); - - # set navigation active - $navigation['Plugins']['active'] = true; - - return $this->render($response, 'settings/plugins.twig', array( - 'settings' => $userSettings, - 'acl' => $this->c->acl, - 'navigation' => $navigation, - 'plugins' => $plugins, - 'route' => $route->getName() - )); - } - - /************************************* - ** SAVE THEME- AND PLUGIN-SETTINGS ** - *************************************/ - - /* - public function saveThemes($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('themes.show')); - } - - $userSettings = \Typemill\Settings::getUserSettings(); - $params = $request->getParams(); - $themeName = isset($params['theme']) ? $params['theme'] : false; - $userInput = isset($params[$themeName]) ? $params[$themeName] : false; - $validate = new Validation(); - $themeSettings = \Typemill\Settings::getObjectSettings('themes', $themeName); - - if(isset($themeSettings['settings']['images'])) - { - # get the default settings - $defaultSettings = \Typemill\Settings::getDefaultSettings(); - - # merge the default image settings with the theme image settings, delete all others (image settings from old theme) - $userSettings['images'] = array_merge($defaultSettings['images'], $themeSettings['settings']['images']); - } - - # set theme name and delete theme settings from user settings for the case, that the new theme has no settings - $userSettings['theme'] = $themeName; - - # extract the custom css from user input - $customcss = isset($userInput['customcss']) ? $userInput['customcss'] : false; - - # delete custom css from userinput - unset($userInput['customcss']); - - $write = new write(); - - # make sure no file is set if there is no custom css - if(!$customcss OR $customcss == '') - { - # delete the css file if exists - $write->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . $themeName . '-custom.css'); - } - else - { - if ( $customcss != strip_tags($customcss) ) - { - $_SESSION['errors'][$themeName]['customcss'][] = 'custom css contains html'; - } - else - { - # store css - $write = new write(); - $write->writeFile('cache', $themeName . '-custom.css', $customcss); - } - } - - if($userInput) - { - # validate the user-input and return image-fields if they are defined - $imageFields = $this->validateInput('themes', $themeName, $userInput, $validate); - - # set user input as theme settings - $userSettings['themes'][$themeName] = $userInput; - } - - # handle images - $images = $request->getUploadedFiles(); - - if(!isset($_SESSION['errors']) && isset($images[$themeName])) - { - $userInput = $this->saveImages($imageFields, $userInput, $userSettings, $images[$themeName]); - - # set user input as theme settings - $userSettings['themes'][$themeName] = $userInput; - } - - # 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($this->c->router->pathFor('themes.show')); - } - - # store updated settings - \Typemill\Settings::updateSettings($userSettings); - - $this->c->flash->addMessage('info', 'Settings are stored'); - return $response->withRedirect($this->c->router->pathFor('themes.show')); - } - } - - public function savePlugins($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('plugins.show')); - } - - $userSettings = \Typemill\Settings::getUserSettings(); - $pluginSettings = array(); - $userInput = $request->getParams(); - $validate = new Validation(); - - # use the stored user settings and iterate over all original plugin settings, so we do not forget any... - foreach($userSettings['plugins'] as $pluginName => $pluginUserSettings) - { - # if there are no input-data for this plugin, then use the stored plugin settings - if(!isset($userInput[$pluginName])) - { - $pluginSettings[$pluginName] = $pluginUserSettings; - } - else - { - # fetch the original settings from the folder to get the field definitions - $originalSettings = \Typemill\Settings::getObjectSettings('plugins', $pluginName); - - # check if the plugin has dependencies - if(isset($userInput[$pluginName]['active']) && isset($originalSettings['dependencies'])) - { - foreach($originalSettings['dependencies'] as $dependency) - { - if(!isset($userInput[$dependency]['active']) OR !$userInput[$dependency]['active']) - { - $this->c->flash->addMessage('error', 'Activate the plugin ' . $dependency . ' before you activate the plugin ' . $pluginName); - return $response->withRedirect($this->c->router->pathFor('plugins.show')); - } - } - } - - # validate the user-input - $imageFields = $this->validateInput('plugins', $pluginName, $userInput[$pluginName], $validate, $originalSettings); - - # use the input data - $pluginSettings[$pluginName] = $userInput[$pluginName]; - } - - # handle images - $images = $request->getUploadedFiles(); - - if(!isset($_SESSION['errors']) && isset($images[$pluginName])) - { - $userInput[$pluginName] = $this->saveImages($imageFields, $userInput[$pluginName], $userSettings, $images[$pluginName]); - - # set user input as theme settings - $pluginSettings[$pluginName] = $userInput[$pluginName]; - } - - # deactivate the plugin, if there is no active flag - if(!isset($userInput[$pluginName]['active'])) - { - $pluginSettings[$pluginName]['active'] = false; - } - } - - if(isset($_SESSION['errors'])) - { - $this->c->flash->addMessage('error', 'Please correct the errors below'); - } - else - { - # if everything is valid, add plugin settings to base settings again - $userSettings['plugins'] = $pluginSettings; - - # store updated settings - \Typemill\Settings::updateSettings($userSettings); - - $this->c->flash->addMessage('info', 'Settings are stored'); - } - - return $response->withRedirect($this->c->router->pathFor('plugins.show')); - } - } - - /*********************** - ** USER MANAGEMENT ** - ***********************/ - - public function showAccount($request, $response, $args) - { - $username = $_SESSION['user']; - - $validate = new Validation(); - - if($validate->username($username)) - { - # get settings - $settings = $this->c->get('settings'); - - # get user with userdata - $user = new User(); - $userdata = $user->getSecureUser($username); - - # 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['Account']['active'] = true; - - 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 -# 'userrole' => false, // not needed ? -# 'username' => $args['username'], // not needed ? - 'route' => $route->getName() // needed to set link active - )); - } - - $this->c->flash->addMessage('error', 'User does not exists'); - return $response->withRedirect($this->c->router->pathFor('home')); - } - - 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 listUser($request, $response) - { - $user = new User(); - $users = $user->getUsers(); - $userdata = array(); - $route = $request->getAttribute('route'); - $settings = $this->c->get('settings'); - $navigation = $this->getMainNavigation(); - - # set navigation active - $navigation['Users']['active'] = true; - - # set standard template - $template = 'settings/userlist.twig'; - - # use vue template for many users - $totalusers = count($users); - - if($totalusers > 10) - { - $template = 'settings/userlistvue.twig'; - } - else - { - foreach($users as $username) - { - $newuser = $user->getSecureUser($username); - if($newuser) - { - $userdata[] = $newuser; - } - } - } - - return $this->render($response, $template, array( - 'settings' => $settings, - 'acl' => $this->c->acl, - 'navigation' => $navigation, - 'users' => $users, - 'userdata' => $userdata, - 'userroles' => $this->c->acl->getRoles(), - 'route' => $route->getName() - )); - } - - #returns userdata - public function getUsersByNames($request, $response, $args) - { - $params = $request->getParams(); - $user = new User(); - $userdata = []; - - if(isset($params['usernames'])) - { - foreach($params['usernames'] as $username) - { - $existinguser = $user->getSecureUser($username); - if($existinguser) - { - $userdata[] = $existinguser; - } - } - } - - return $response->withJson(['userdata' => $userdata]); - } - - # returns userdata - public function getUsersByEmail($request, $response, $args) - { - $params = $request->getParams(); - $user = new User(); - - $userdata = $user->findUsersByEmail($params['email']); - - return $response->withJson(['userdata' => $userdata ]); - } - - #returns userdata - public function getUsersByRole($request, $response, $args) - { - $params = $request->getParams(); - $user = new User(); - - $userdata = $user->findUsersByRole($params['role']); - - return $response->withJson(['userdata' => $userdata ]); - } - - 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)); - } - - private function getUserFields($role) - { - # 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->acl->hasRole($role)) - { - $role = 'member'; - } - - $fields = []; - $fields['username'] = ['label' => 'Username (read only)', 'type' => 'text', 'readonly' => true]; - $fields['firstname'] = ['label' => 'First Name', 'type' => 'text']; - $fields['lastname'] = ['label' => 'Last Name', 'type' => 'text']; - $fields['email'] = ['label' => 'E-Mail', 'type' => 'text', 'required' => true]; - $fields['userrole'] = ['label' => 'Role', 'type' => 'text', 'readonly' => true]; - $fields['password'] = ['label' => 'Actual Password', 'type' => 'password']; - $fields['newpassword'] = ['label' => 'New Password', 'type' => 'password']; - - # 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->acl->isAllowed($role, 'mycontent', 'create')) - { - $newfield['image'] = ['label' => 'Profile-Image', 'type' => 'image']; - $newfield['description'] = ['label' => 'Author-Description (Markdown)', 'type' => 'textarea']; - - $fields = array_slice($fields, 0, 1, true) + $newfield + array_slice($fields, 1, NULL, true); - # array_splice($fields,1,0,$newfield); - } - - # Only admin can change userroles - if($this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'write')) - { - $userroles = $this->c->acl->getRoles(); - $options = []; - - # we need associative array to make select-field with key/value work - foreach($userroles as $userrole) - { - $options[$userrole] = $userrole; - } - - $fields['userrole'] = ['label' => 'Role', 'type' => 'select', 'options' => $options]; - } - - $userform = []; - $userform['forms']['fields'] = $fields; - return $userform; - } - - private function getThemes() - { - $themeFolder = $this->c->get('settings')['rootPath'] . $this->c->get('settings')['themeFolder']; - $themeFolderC = scandir($themeFolder); - $themes = array(); - foreach ($themeFolderC as $key => $theme) - { - if (!in_array($theme, array(".",".."))) - { - if (is_dir($themeFolder . DIRECTORY_SEPARATOR . $theme)) - { - $themes[] = $theme; - } - } - } - return $themes; - } - - private function getCopyright() - { - return array( - "©", - "CC-BY", - "CC-BY-NC", - "CC-BY-NC-ND", - "CC-BY-NC-SA", - "CC-BY-ND", - "CC-BY-SA", - "None" - ); - } - - private function getLanguages() - { - return array( - 'en' => 'English', - 'ru' => 'Russian', - 'nl' => 'Dutch, Flemish', - 'de' => 'German', - 'it' => 'Italian', - 'fr' => 'French', - ); - } - - private function validateInput($objectType, $objectName, $userInput, $validate, $originalSettings = NULL) - { - if(!$originalSettings) - { - # fetch the original settings from the folder (plugin or theme) to get the field definitions - $originalSettings = \Typemill\Settings::getObjectSettings($objectType, $objectName); - } - - # images get special treatment - $imageFieldDefinitions = array(); - - if(isset($originalSettings['forms']['fields'])) - { - /* flaten the multi-dimensional array with fieldsets to a one-dimensional array */ - $originalFields = array(); - foreach($originalSettings['forms']['fields'] as $fieldName => $fieldValue) - { - if(isset($fieldValue['fields'])) - { - foreach($fieldValue['fields'] as $subFieldName => $subFieldValue) - { - $originalFields[$subFieldName] = $subFieldValue; - } - } - else - { - $originalFields[$fieldName] = $fieldValue; - } - } - - # if plugin is not active, then skip required - $skiprequired = false; - if($objectType == 'plugins' && !isset($userInput['active'])) - { - $skiprequired = true; - } - - /* take the user input data and iterate over all fields and values */ - foreach($userInput as $fieldName => $fieldValue) - { - /* get the corresponding field definition from original plugin settings */ - $fieldDefinition = isset($originalFields[$fieldName]) ? $originalFields[$fieldName] : false; - - if($fieldDefinition) - { - - # check if the field is a select field with dataset = userroles - if(isset($fieldDefinition['type']) && ($fieldDefinition['type'] == 'select' ) && isset($fieldDefinition['dataset']) && ($fieldDefinition['dataset'] == 'userroles' ) ) - { - $userroles = [null => null]; - foreach($this->c->acl->getRoles() as $userrole) - { - $userroles[$userrole] = $userrole; - } - $fieldDefinition['options'] = $userroles; - } - - /* validate user input for this field */ - $validate->objectField($fieldName, $fieldValue, $objectName, $fieldDefinition, $skiprequired); - - if($fieldDefinition['type'] == 'image') - { - # we want to return all images-fields for further processing - $imageFieldDefinitions[$fieldName] = $fieldDefinition; - } - } - if(!$fieldDefinition && $objectType != 'users' && $fieldName != 'active') - { - $_SESSION['errors'][$objectName][$fieldName] = array('This field is not defined!'); - } - } - } - - return $imageFieldDefinitions; - } - - 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; - } - -} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerWeb.php b/system/typemill/Controllers/ControllerWeb.php deleted file mode 100644 index 5275030..0000000 --- a/system/typemill/Controllers/ControllerWeb.php +++ /dev/null @@ -1,73 +0,0 @@ -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'); -*/ - } -} \ No newline at end of file diff --git a/system/typemill/Controllers/ControllerWebSystem.php b/system/typemill/Controllers/ControllerWebSystem.php new file mode 100644 index 0000000..eab55ed --- /dev/null +++ b/system/typemill/Controllers/ControllerWebSystem.php @@ -0,0 +1,497 @@ +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 = '

Hello

I am the showBlank method from the settings controller

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.

'; + + 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; + } + */ + +} \ No newline at end of file diff --git a/system/typemill/Extensions/TwigUrlExtension.php b/system/typemill/Extensions/TwigUrlExtension.php index 0242db0..a8056f0 100644 --- a/system/typemill/Extensions/TwigUrlExtension.php +++ b/system/typemill/Extensions/TwigUrlExtension.php @@ -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']; } } \ No newline at end of file diff --git a/system/typemill/Middleware/JsonBodyParser.php b/system/typemill/Middleware/JsonBodyParser.php index 89a720f..f3d79de 100644 --- a/system/typemill/Middleware/JsonBodyParser.php +++ b/system/typemill/Middleware/JsonBodyParser.php @@ -11,8 +11,6 @@ class JsonBodyParser implements MiddlewareInterface { public function process(Request $request, RequestHandler $handler) :response { - #echo '
JSON Body parser'; - $contentType = $request->getHeaderLine('Content-Type'); if (strstr($contentType, 'application/json')) diff --git a/system/typemill/Middleware/RedirectIfUnauthenticated.php b/system/typemill/Middleware/RedirectIfUnauthenticated.php index 4c08441..7ba8c97 100644 --- a/system/typemill/Middleware/RedirectIfUnauthenticated.php +++ b/system/typemill/Middleware/RedirectIfUnauthenticated.php @@ -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); } } \ No newline at end of file diff --git a/system/typemill/Middleware/RestrictApiAccess.php b/system/typemill/Middleware/RestrictApiAccess.php index 82628d6..f7913cc 100644 --- a/system/typemill/Middleware/RestrictApiAccess.php +++ b/system/typemill/Middleware/RestrictApiAccess.php @@ -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 diff --git a/system/typemill/Models/Storage.php b/system/typemill/Models/Storage.php index 66090bf..0999f18 100644 --- a/system/typemill/Models/Storage.php +++ b/system/typemill/Models/Storage.php @@ -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; } diff --git a/system/typemill/Models/User.php b/system/typemill/Models/User.php index 5df1b3b..f82672b 100644 --- a/system/typemill/Models/User.php +++ b/system/typemill/Models/User.php @@ -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) diff --git a/system/typemill/Models/Validation.php b/system/typemill/Models/Validation.php index 09f5ccc..794fee3 100644 --- a/system/typemill/Models/Validation.php +++ b/system/typemill/Models/Validation.php @@ -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); } /** diff --git a/system/typemill/Static/Helpers.php b/system/typemill/Static/Helpers.php index 7dc2fda..3c09bf9 100644 --- a/system/typemill/Static/Helpers.php +++ b/system/typemill/Static/Helpers.php @@ -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']; diff --git a/system/typemill/Static/Translations.php b/system/typemill/Static/Translations.php index 10ebfbb..9ebb280 100644 --- a/system/typemill/Static/Translations.php +++ b/system/typemill/Static/Translations.php @@ -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') ) diff --git a/system/typemill/author/auth/login.twig b/system/typemill/author/auth/login.twig index fa2f5e7..01ae972 100644 --- a/system/typemill/author/auth/login.twig +++ b/system/typemill/author/auth/login.twig @@ -6,7 +6,7 @@
-
+

Login

diff --git a/system/typemill/author/css/custom.css b/system/typemill/author/css/custom.css index fceb575..4182565 100644 --- a/system/typemill/author/css/custom.css +++ b/system/typemill/author/css/custom.css @@ -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; +} \ No newline at end of file diff --git a/system/typemill/author/css/output.css b/system/typemill/author/css/output.css index 47ef2f6..978f593 100644 --- a/system/typemill/author/css/output.css +++ b/system/typemill/author/css/output.css @@ -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)); + } } \ No newline at end of file diff --git a/system/typemill/author/js/codejar.js b/system/typemill/author/js/codejar.js new file mode 100644 index 0000000..8260b3f --- /dev/null +++ b/system/typemill/author/js/codejar.js @@ -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

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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + 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); + } + }, + }; +} diff --git a/system/typemill/author/js/vue-account.js b/system/typemill/author/js/vue-account.js new file mode 100644 index 0000000..0dc05ad --- /dev/null +++ b/system/typemill/author/js/vue-account.js @@ -0,0 +1,87 @@ +const app = Vue.createApp({ + template: ` +
+
+
+
+ {{ fieldDefinition.legend }} + + +
+ + +
+
+
{{ message }}
+ +
+
+
+
`, + 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 = ''; + } + }, +}) \ No newline at end of file diff --git a/system/typemill/author/js/vue-eventbus.js b/system/typemill/author/js/vue-eventbus.js new file mode 100644 index 0000000..99e145c --- /dev/null +++ b/system/typemill/author/js/vue-eventbus.js @@ -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(); diff --git a/system/typemill/author/js/vue-plugins.js b/system/typemill/author/js/vue-plugins.js new file mode 100644 index 0000000..7f11a91 --- /dev/null +++ b/system/typemill/author/js/vue-plugins.js @@ -0,0 +1,118 @@ +const app = Vue.createApp({ + template: ` +
+
    +
  • +
    +
    +

    {{theme.name}}

    +
    author: {{theme.author}} | version: {{theme.version}} | {{theme.licence}}
    +

    {{theme.description}}

    +
    +
    + + +
    +
    +
    +
    +
    + {{ fieldDefinition.legend }} + + +
    + + +
    +
    +
    {{ message }}
    +
    + + +
    +
    +
    +
  • +
+
+
`, + 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 = ''; + } + }, +}) \ No newline at end of file diff --git a/system/typemill/author/js/vue-shared.js b/system/typemill/author/js/vue-shared.js new file mode 100644 index 0000000..cfe2949 --- /dev/null +++ b/system/typemill/author/js/vue-shared.js @@ -0,0 +1,1457 @@ +app.component('component-text', { + props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'], + template: `
+ + +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-textarea', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + data: function () { + return { + textareaclass: '' + } + }, + template: `
+ + +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + formatValue: function(value) + { + if(value !== null && typeof value === 'object') + { + this.textareaclass = 'codearea'; + return JSON.stringify(value, undefined, 4); + } + return value; + }, + }, +}) + +app.component('component-select', { + props: ['id', 'description', 'readonly', 'required', 'disabled', 'label', 'name', 'type', 'css', 'options', 'value', 'errors', 'dataset', 'userroles'], + template: `
+ + +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-checkbox', { + props: ['id', 'description', 'readonly', 'required', 'disabled', 'label', 'checkboxlabel', 'name', 'type', 'css', 'value', 'errors'], + template: `
+
{{ $filters.translate(label) }}
+ +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, value, name) + { + eventBus.$emit('forminput', {'name': name, 'value': value}); + }, + }, +}) + +app.component('component-checkboxlist', { + props: ['description', 'readonly', 'required', 'disabled', 'label', 'checkboxlabel', 'options', 'name', 'type', 'css', 'value', 'errors'], + template: `
+
{{ $filters.translate(label) }}
+ +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, value, optionvalue, name) + { + /* if value (array) for checkboxlist is not initialized yet */ + if(value === true || value === false) + { + value = [optionvalue]; + } + eventBus.$emit('forminput', {'name': name, 'value': value}); + }, + }, +}) + +app.component('component-radio', { + props: ['id', 'description', 'readonly', 'required', 'disabled', 'options', 'label', 'name', 'type', 'css', 'value', 'errors'], + template: `
+
{{ $filters.translate(label) }}
+ +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, value, name) + { + eventBus.$emit('forminput', {'name': name, 'value': value}); + }, + }, +}) + +app.component('component-number', { + props: ['id', 'description', 'min', 'max', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + template: `
+ + +

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-date', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + template: `
+ +
+
+ +
+ +
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-email', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + template: `
+ +
+
+ +
+ +
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-tel', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + template: `
+ +
+
+ +
+ +
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-url', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + template: `
+ +
+
+ +
+ +
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-color', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'css', 'value', 'errors'], + template: `
+ +
+
+ +
+ +
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
`, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-password', { + props: ['id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'autocomplete', 'generator', 'css', 'value', 'errors'], + data() { + return { + fieldType: "password" + }; + }, + template: `
+ +
+
+ +
+ +
+ + +
+
+
+
+

{{ errors[name] }}

+

{{ $filters.translate(description) }}

+
+
+ +
+
+
`, + methods: { + update: function(newvalue, name) + { + eventBus.$emit('forminput', {'name': name, 'value': newvalue}); + }, + toggleFieldType: function() + { + if (this.fieldType === "password") + { + this.fieldType = "text"; + } + else + { + this.fieldType = "password"; + } + }, + generatePassword: function() + { + const digits = '0123456789'; + const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const lower = upper.toLowerCase(); + const characters = digits + upper + lower; + const length = 40; + + const randomCharacters = Array.from({ length }, (_) => + this.getRandomCharacter(characters), + ).join('') + + const passwordLength = this.getRandomInt(30,40); + + const password = randomCharacters.substring(0,passwordLength); + + this.update(password, this.name); + }, + getRandomInt: function(min,max) + { + return Math.floor(Math.random() * (max - min + 1) + min); + }, + getRandomCharacter: function(characters) + { + let randomNumber + + do{ + randomNumber = crypto.getRandomValues(new Uint8Array(1))[0] + } while (randomNumber >= 256 - (256 % characters.length)) + + return characters[randomNumber % characters.length] + } + }, +}) + +app.component('component-hidden', { + props: ['id', 'maxlength', 'required', 'disabled', 'name', 'type', 'css', 'value', 'errors'], + template: ``, + methods: { + update: function($event, name) + { + eventBus.$emit('forminput', {'name': name, 'value': $event.target.value}); + }, + }, +}) + +app.component('component-customfields', { + props: ['id', 'description', 'readonly', 'required', 'disabled', 'options', 'label', 'name', 'type', 'value', 'errors'], + data: function () { + return { + fielderrors: false, + fielddetails: {}, + disableaddbutton: false, + cfvalue: [{}] + } + }, + template: `
+ +
{{ $filters.translate(description) }}
+
{{ errors[name] }}
+
{{ fielderrors }}
+ +
+ +
+ + +
+
+ +
`, + mounted: function(){ + if(typeof this.value === 'undefined' || this.value === null || this.value.length == 0) + { + // this.cfvalue = [{}]; + // this.update(this.cfvalue, this.name); + this.disableaddbutton = 'disabled'; + } + else + { + /* turn object { key:value, key:value } into array [[key,value][key,value]] */ + this.cfvalue = Object.entries(this.value); + /* and back into array of objects [ {key: key, value: value}{key:key, value: value }] */ + this.cfvalue = this.cfvalue.map(function(item){ return { 'key': item[0], 'value': item[1] } }); + } + }, + methods: { + update: function(value, name) + { + this.fielderrors = false; + this.errors = false; + + /* transform array of objects [{key:mykey, value:myvalue}] into array [[mykey,myvalue]] */ + var storedvalue = value.map(function(item){ return [item.key, item.value]; }); + + /* transform array [[mykey,myvalue]] into object { mykey:myvalue } */ + storedvalue = Object.fromEntries(storedvalue); + + FormBus.$emit('forminput', {'name': name, 'value': storedvalue}); + }, + updatePairKey: function(index,event) + { + this.cfvalue[index].key = event.target.value; + + var regex = /^[a-z0-9]+$/i; + + if(!this.keyIsUnique(event.target.value,index)) + { + this.cfvalue[index].keyerror = 'red'; + this.fielderrors = 'Error: The key already exists'; + this.disableaddbutton = 'disabled'; + return; + } + else if(!regex.test(event.target.value)) + { + this.cfvalue[index].keyerror = 'red'; + this.fielderrors = 'Error: Only alphanumeric for keys allowed'; + this.disableaddbutton = 'disabled'; + return; + } + + delete this.cfvalue[index].keyerror; + this.disableaddbutton = false; + this.update(this.cfvalue,this.name); + }, + keyIsUnique: function(keystring, index) + { + for(obj in this.cfvalue) + { + if( (obj != index) && (this.cfvalue[obj].key == keystring) ) + { + return false; + } + } + return true; + }, + updatePairValue: function(index, event) + { + this.cfvalue[index].value = event.target.value; + + var regex = /<.*(?=>)/gm; + if(event.target.value == '' || regex.test(event.target.value)) + { + this.cfvalue[index].valueerror = 'red'; + this.fielderrors = 'Error: No empty values or html tags are allowed'; + } + else + { + delete this.cfvalue[index].valueerror; + this.update(this.cfvalue,this.name); + } + }, + addField: function() + { + for(object in this.cfvalue) + { + if(Object.keys(this.cfvalue[object]).length === 0) + { + return; + } + } + this.cfvalue.push({}); + this.disableaddbutton = 'disabled'; + }, + deleteField: function(index) + { + this.cfvalue.splice(index,1); + this.disableaddbutton = false; + this.update(this.cfvalue,this.name); + }, + }, +}) + +app.component('component-image', { + props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'], + template: `
+ +
+
+
+ +
+
+
+
+ ' + +

{{ $filters.translate('upload an image') }}

'+ +
+
+ +
+
+ +
+ + +
+
+ +
+
+

{{ $filters.translate(description) }}

+
{{ errors[name] }}
+
+
+ + + +
`, + data: function(){ + return { + maxsize: 10, // megabyte + imgpreview: false, + showmedialib: false, + load: false, + quality: false, + qualitylabel: false, + } + }, + methods: { + getimagesrc: function(value) + { + if(value !== undefined && value !== null && value !== '') + { + var imgpreview = myaxios.defaults.baseURL + '/' + value; + if(value.indexOf("media/live") > -1 ) + { + this.quality = 'live'; + this.qualitylabel = 'switch quality to: original'; + } + else if(value.indexOf("media/original") > -1) + { + this.quality = 'original'; + this.qualitylabel = 'switch quality to: live'; + } + return imgpreview; + } + }, + update: function(value) + { + FormBus.$emit('forminput', {'name': this.name, 'value': value}); + }, + updatemarkdown: function(markdown, url) + { + /* is called from child component medialib */ + this.update(url); + }, + createmarkdown: function(url) + { + /* is called from child component medialib */ + this.update(url); + }, + deleteImage: function() + { + this.imgpreview = false; + this.update(''); + }, + switchQuality: function(value) + { + if(value !== null && value !== '') + { + if(this.quality == 'live') + { + var newUrl = value.replace("media/live", "media/original"); + this.update(newUrl); + this.quality = 'original'; + this.qualitylabel = 'switch quality to: live'; + } + else + { + var newUrl = value.replace("media/original", "media/live"); + this.update(newUrl); + this.quality = 'live'; + this.qualitylabel = 'switch quality to: original'; + } + } + }, + openmedialib: function() + { + this.showmedialib = true; + }, + onFileChange: function( e ) + { + if(e.target.files.length > 0) + { + let imageFile = e.target.files[0]; + let size = imageFile.size / 1024 / 1024; + + if (!imageFile.type.match('image.*')) + { + publishController.errors.message = "Only images are allowed."; + } + else if (size > this.maxsize) + { + publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB"; + } + else + { + sharedself = this; + + let reader = new FileReader(); + reader.readAsDataURL(imageFile); + reader.onload = function(e) + { + sharedself.imgpreview = e.target.result; + + myaxios.post('/api/v1/image',{ + 'url': document.getElementById("path").value, + 'image': e.target.result, + 'name': imageFile.name, + 'publish': true, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + }) + .then(function (response) { + sharedself.update(response.data.name); + }) + .catch(function (error) + { + sharedself.load = false; + if(error.response) + { + publishController.errors.message = error.response.data.errors; + } + }); + } + } + } + } + }, +}) + +app.component('component-file', { + props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'], + template: `
+ + + + +
+
+
+ +
+ + +
+
+
+ +

{{ $filters.translate('upload') }}

+
+
+ +
+
+
+
+ + + +
+
+
`, + data: function(){ + return { + maxsize: 20, // megabyte + showmedialib: false, + fileid: '', + load: false, + userroles: ['all'], + selectedrole: '', + } + }, + mounted: function(){ + this.getrestriction(); + }, + methods: { + update: function(value) + { + FormBus.$emit('forminput', {'name': this.name, 'value': value}); + }, + updatemarkdown: function(markdown, url) + { + /* is called from child component medialib if file has been selected */ + this.update(url); + this.getrestriction(url); + }, + createmarkdown: function(url) + { + /* is called from child component medialib */ + this.update(url); + }, + openmedialib: function() + { + this.showmedialib = true; + }, + deleteFile: function() + { + this.update(''); + this.selectedrole = 'all'; + }, + getrestriction: function(url) + { + var filename = this.value; + if(url) + { + filename = url; + } + + var myself = this; + + myaxios.get('/api/v1/filerestrictions',{ + params: { + 'url': document.getElementById("path").value, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'filename': filename, + } + }) + .then(function (response) { + myself.userroles = ['all']; + myself.userroles = myself.userroles.concat(response.data.userroles); + myself.selectedrole = response.data.restriction; + }) + .catch(function (error) + { + if(error.response) + { + } + }); + }, + updaterestriction: function() + { + myaxios.post('/api/v1/filerestrictions',{ + 'url': document.getElementById("path").value, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + 'filename': this.value, + 'role': this.selectedrole, + }) + .then(function (response) { + + }) + .catch(function (error) + { + if(error.response) + { + } + }); + }, + onFileChange: function( e ) + { + if(e.target.files.length > 0) + { + let uploadedFile = e.target.files[0]; + let size = uploadedFile.size / 1024 / 1024; + + if (size > this.maxsize) + { + publishController.errors.message = "The maximal size of a file is " + this.maxsize + " MB"; + } + else + { + sharedself = this; + + sharedself.load = true; + + let reader = new FileReader(); + reader.readAsDataURL(uploadedFile); + reader.onload = function(e) { + myaxios.post('/api/v1/file',{ + 'url': document.getElementById("path").value, + 'file': e.target.result, + 'name': uploadedFile.name, + 'publish': true, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + }) + .then(function (response) { + sharedself.load = false; + sharedself.selectedrole = 'all'; + sharedself.update(response.data.info.url); + }) + .catch(function (error) + { + sharedself.load = false; + if(error.response) + { + publishController.errors.message = error.response.data.errors; + } + }); + } + } + } + } + } +}) + +const medialib = app.component('medialib', { + props: ['parentcomponent'], + template: `
+
+
+ +
+
+
+ + +
+
+
{{errors}}
+ +
+ + click to select + +
+
{{ image.name }}
+ + +
+
+
+
+
+
+
+ +
+
+
+
Name
{{ imagedetaildata.name}}
+
URL
{{ getImageUrl(imagedetaildata.src_live)}}
+
+
+
Size
{{ getSize(imagedetaildata.bytes) }}
+
+
+
Dimensions
{{ imagedetaildata.width }}x{{ imagedetaildata.height }} px
+
+
+
Type
{{ imagedetaildata.type }}
+
+
+
Date
{{ getDate(imagedetaildata.timestamp) }}
+
+
+
+ + +
+
+
+ +
+

Image used in:

+ +
No pages found.
'+ +
+
+ +
+ +
+ +
+
+
{{ file.name }}
+ + +
+
+
+
+
+
+
+
{{ filedetaildata.info.extension }}
+
+
+
+
Name
{{ filedetaildata.name}}
+
URL
{{ filedetaildata.url}}
+
+
+
Size
{{ getSize(filedetaildata.bytes) }}
+
+
+
Type
{{ filedetaildata.info.extension }}
+
+
+
Date
{{ getDate(filedetaildata.timestamp) }}
+
+
+
+ + +
+
+
+ +
+

File used in:

+ +
No pages found.
'+ +
+
+
+
+
`, + data: function(){ + return { + imagedata: false, + showimages: true, + imagedetaildata: false, + showimagedetails: false, + filedata: false, + showfiles: false, + filedetaildata: false, + showfiledetails: false, + detailindex: false, + load: false, + baseurl: myaxios.defaults.baseURL, + adminurl: false, + search: '', + errors: false, + } + }, + mounted: function(){ + + if(this.parentcomponent == 'files') + { + this.showFiles(); + } + + this.errors = false; + var self = this; + + myaxios.get('/api/v1/medialib/images',{ + params: { + 'url': document.getElementById("path").value, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + self.imagedata = response.data.images; + }) + .catch(function (error) + { + if(error.response) + { + self.errors = error.response.data.errors; + } + }); + }, + computed: { + filteredImages() { + + var searchimages = this.search; + var filteredImages = {}; + var images = this.imagedata; + if(images) + { + Object.keys(images).forEach(function(key) { + var searchindex = key + ' ' + images[key].name; + if(searchindex.toLowerCase().indexOf(searchimages.toLowerCase()) !== -1) + { + filteredImages[key] = images[key]; + } + }); + } + return filteredImages; + }, + filteredFiles() { + + var searchfiles = this.search; + var filteredFiles = {}; + var files = this.filedata; + if(files) + { + Object.keys(files).forEach(function(key) { + var searchindex = key + ' ' + files[key].name; + if(searchindex.toLowerCase().indexOf(searchfiles.toLowerCase()) !== -1) + { + filteredFiles[key] = files[key]; + } + }); + } + return filteredFiles; + } + }, + methods: { + isImagesActive: function() + { + if(this.showimages) + { + return 'bg-tm-green white'; + } + return 'bg-light-gray black'; + }, + isFilesActive: function() + { + if(this.showfiles) + { + return 'bg-tm-green white'; + } + return 'bg-light-gray black'; + }, + closemedialib: function() + { + this.$parent.showmedialib = false; + }, + getBackgroundImage: function(image) + { + return 'background-image: url(' + this.baseurl + '/' + image.src_thumb + ');width:250px'; + }, + getImageUrl(relativeUrl) + { + return this.baseurl + '/' + relativeUrl; + }, + showImages: function() + { + this.errors = false; + this.showimages = true; + this.showfiles = false; + this.showimagedetails = false; + this.showfiledetails = false; + this.imagedetaildata = false; + this.detailindex = false; + }, + showFiles: function() + { + this.showimages = false; + this.showfiles = true; + this.showimagedetails = false; + this.showfiledetails = false; + this.imagedetaildata = false; + this.filedetaildata = false; + this.detailindex = false; + + if(!this.files) + { + this.errors = false; + var filesself = this; + + myaxios.get('/api/v1/medialib/files',{ + params: { + 'url': document.getElementById("path").value, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + filesself.filedata = response.data.files; + }) + .catch(function (error) + { + if(error.response) + { + filesself.errors = error.response.data.errors; + } + }); + } + }, + showImageDetails: function(image,index) + { + this.errors = false; + this.showimages = false; + this.showfiles = false; + this.showimagedetails = true; + this.detailindex = index; + this.adminurl = myaxios.defaults.baseURL + '/tm/content/visual'; + + var imageself = this; + + myaxios.get('/api/v1/image',{ + params: { + 'url': document.getElementById("path").value, + 'name': image.name, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + imageself.imagedetaildata = response.data.image; + }) + .catch(function (error) + { + if(error.response) + { + imageself.errors = error.response.data.errors; + } + }); + }, + showFileDetails: function(file,index) + { + this.errors = false; + this.showimages = false; + this.showfiles = false; + this.showimagedetails = false; + this.showfiledetails = true; + this.detailindex = index; + + this.adminurl = myaxios.defaults.baseURL + '/tm/content/visual'; + + var fileself = this; + + myaxios.get('/api/v1/file',{ + params: { + 'url': document.getElementById("path").value, + 'name': file.name, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + fileself.filedetaildata = response.data.file; + }) + .catch(function (error) + { + if(error.response) + { + fileself.errors = error.response.data.errors; + } + }); + }, + selectImage: function(image) + { + this.showImages(); + + if(this.parentcomponent == 'images') + { + var imgmarkdown = {target: {value: '![alt]('+ image.src_live +')' }}; + + this.$parent.imgfile = image.src_live; + this.$parent.imgpreview = this.baseurl + '/' + image.src_live; + this.$parent.imgmeta = true; + + this.$parent.showmedialib = false; + + this.$parent.createmarkdown(image.src_live); +/* this.$parent.updatemarkdown(imgmarkdown, image.src_live); */ + } + if(this.parentcomponent == 'files') + { + var filemarkdown = {target: {value: '[' + image.name + '](' + image.src_live +'){.tm-download}' }}; + + this.$parent.filemeta = true; + this.$parent.filetitle = image.name; + + this.$parent.showmedialib = false; + + this.$parent.updatemarkdown(filemarkdown, image.src_live); + } + }, + selectFile: function(file) + { + /* if image component is open */ + if(this.parentcomponent == 'images') + { + var imgextensions = ['png','jpg', 'jpeg', 'gif', 'svg', 'webp']; + if(imgextensions.indexOf(file.info.extension) == -1) + { + this.errors = "you cannot insert a file into an image component"; + return; + } + var imgmarkdown = {target: {value: '![alt]('+ file.url +')' }}; + + this.$parent.imgfile = file.url; + this.$parent.imgpreview = this.baseurl + '/' + file.url; + this.$parent.imgmeta = true; + + this.$parent.showmedialib = false; + + this.$parent.createmarkdown(file.url); +/* this.$parent.updatemarkdown(imgmarkdown, file.url);*/ + } + if(this.parentcomponent == 'files') + { + var filemarkdown = {target: {value: '['+ file.name +']('+ file.url +'){.tm-download file-' + file.info.extension + '}' }}; + + this.$parent.showmedialib = false; + + this.$parent.filemeta = true; + this.$parent.filetitle = file.info.filename + ' (' + file.info.extension.toUpperCase() + ')'; + + this.$parent.updatemarkdown(filemarkdown, file.url); + } + this.showFiles(); + }, + removeImage: function(index) + { + this.imagedata.splice(index,1); + }, + removeFile: function(index) + { + this.filedata.splice(index,1); + }, + deleteImage: function(image, index) + { + imageself = this; + + myaxios.delete('/api/v1/image',{ + data: { + 'url': document.getElementById("path").value, + 'name': image.name, + 'index': index, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + imageself.showImages(); + imageself.removeImage(index); + }) + .catch(function (error) + { + if(error.response) + { + imageself.errors = error.response.data.errors; + } + }); + }, + deleteFile: function(file, index) + { + fileself = this; + + myaxios.delete('/api/v1/file',{ + data: { + 'url': document.getElementById("path").value, + 'name': file.name, + 'index': index, + 'csrf_name': document.getElementById("csrf_name").value, + 'csrf_value': document.getElementById("csrf_value").value, + } + }) + .then(function (response) + { + fileself.showFiles(); + fileself.removeFile(index); + }) + .catch(function (error) + { + if(error.response) + { + fileself.errors = error.response.data.errors; + } + }); + }, + getDate(timestamp) + { + date = new Date(timestamp * 1000); + + datevalues = { + 'year': date.getFullYear(), + 'month': date.getMonth()+1, + 'day': date.getDate(), + 'hour': date.getHours(), + 'minute': date.getMinutes(), + 'second': date.getSeconds(), + }; + return datevalues.year + '-' + datevalues.month + '-' + datevalues.day; + }, + getSize(bytes) + { + var i = Math.floor(Math.log(bytes) / Math.log(1024)), + sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i]; + }, + isChecked: function(classname) + { + if(this.imgclass == classname) + { + return ' checked'; + } + }, + }, +}) diff --git a/system/typemill/author/js/vue-system.js b/system/typemill/author/js/vue-system.js index 0797004..55bfebb 100644 --- a/system/typemill/author/js/vue-system.js +++ b/system/typemill/author/js/vue-system.js @@ -1,79 +1,98 @@ -const { createApp } = Vue - -createApp({ - template: `
MyForm Here message -
- {{ legend }} -
{{field.legend}} - - -
- - -
-
`, +const app = Vue.createApp({ + template: ` +
+
    +
  • + +
  • +
+
+
+ + +
+
+
+
{{ message }}
+ +
+
+
`, 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 = ''; + } + }, +}) /* diff --git a/system/typemill/author/js/vue-themes.js b/system/typemill/author/js/vue-themes.js new file mode 100644 index 0000000..13b29de --- /dev/null +++ b/system/typemill/author/js/vue-themes.js @@ -0,0 +1,130 @@ +const app = Vue.createApp({ + template: ` +
+
    +
  • +
    +
    + +
    +
    +
    +

    {{theme.name}}

    +
    author: {{theme.author}} | version: {{theme.version}} | {{theme.licence}}
    +

    {{theme.description}}

    +
    +
    + + +
    +
    +
    +
    +
    +
    + {{ fieldDefinition.legend }} + + +
    + + +
    +
    +
    {{ message }}
    +
    + + +
    +
    +
    +
  • +
+
+
`, + 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 = ''; + } + }, +}) \ No newline at end of file diff --git a/system/typemill/author/js/vue-translate.js b/system/typemill/author/js/vue-translate.js new file mode 100644 index 0000000..9b06a45 --- /dev/null +++ b/system/typemill/author/js/vue-translate.js @@ -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] + } + } +} diff --git a/system/typemill/author/js/vue-users.js b/system/typemill/author/js/vue-users.js new file mode 100644 index 0000000..4e96261 --- /dev/null +++ b/system/typemill/author/js/vue-users.js @@ -0,0 +1,407 @@ +const app = Vue.createApp({ + template: `
+ + + +
+
+ + + +
+
    + +
`, + 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: `
+
+ + + +
+
+ + +
+ + +
+
+
{{error}}
+
You can use the asterisk (*) wildcard to search for name@* or *@domain.com.
+
`, + 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: ` + + + + + + + + + + + + +
UsernameUserroleE-MailEdit
{{ user.username }}{{ user.userrole }}{{ user.email }}edit
`, + methods: { + getEditLink: function(username){ + return this.$root.$data.root + '/tm/user/' + username; + }, + } +}) + +app.component('pagination', { + props: ['page'], + template: '
  • ', + 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: ` +
    +
      +
    • +
      +
      + +
      +
      +
      +

      {{theme.name}}

      +
      author: {{theme.author}} | version: {{theme.version}} | {{theme.licence}}
      +

      {{theme.description}}

      +
      +
      + + +
      +
      +
      +
      +
      +
      + {{ fieldDefinition.legend }} + + +
      + + +
      +
      +
      {{ message }}
      +
      + + +
      +
      +
      +
    • +
    +
    +
    `, + 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 = ''; + } + }, +}) +*/ \ No newline at end of file diff --git a/system/typemill/author/layouts/layoutSystem.twig b/system/typemill/author/layouts/layoutSystem.twig index 07dfd8f..c19bd0c 100644 --- a/system/typemill/author/layouts/layoutSystem.twig +++ b/system/typemill/author/layouts/layoutSystem.twig @@ -19,9 +19,6 @@ - {{ assets.renderCSS() }} @@ -39,7 +36,7 @@ -
    +
    {% block content %}{% endblock %}
    @@ -52,36 +49,21 @@ - - - + + {% block javascript %}{% endblock %} {{ assets.renderJS() }} diff --git a/system/typemill/author/partials/mainNavi.twig b/system/typemill/author/partials/mainNavi.twig index d895084..43882ea 100644 --- a/system/typemill/author/partials/mainNavi.twig +++ b/system/typemill/author/partials/mainNavi.twig @@ -5,7 +5,7 @@ diff --git a/system/typemill/author/partials/systemNavi.twig b/system/typemill/author/partials/systemNavi.twig index 8062f10..12b23cc 100644 --- a/system/typemill/author/partials/systemNavi.twig +++ b/system/typemill/author/partials/systemNavi.twig @@ -1,10 +1,10 @@