mirror of
https://github.com/typemill/typemill.git
synced 2025-07-25 00:02:28 +02:00
Fixed merge conflicts
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
# Publish Status
|
||||
# Publish Status
|
||||
|
||||
In Typemill you can save drafts, publish pages, unpublish pages, and delete pages with a sticky publish panel at the bottom of each page. For published pages, you can also save modifications, discard modifications and publish modifications.
|
||||
In Typemill, you can save drafts, publish pages, unpublish pages, and delete pages using a sticky publish panel at the bottom of each page. For published pages, you can also save modifications, discard modifications, and publish modifications.
|
||||
|
||||
The current status of the page is indicated with colors in the publish panel and in the navigation. This way, an author can always see the status of each page and the current state of the whole website.
|
||||
|
||||
* **Save a draft**: The page is not published and not accessible in the frontend. The status color is red.
|
||||
* **Publish a page**: The page is published and accessible in the frontend. The status color is green.
|
||||
* **Unpublish a page**: The page is not accessible in the frontend and only accessible in the author interface. The status color is red again.
|
||||
* **Delete a page**: The page is completely deleted in frontend and in the author interface.
|
||||
* **Save modivications**: The page is published and accessible in the frontend. The modified draft is only visible in the author interface. The status color is orange.
|
||||
* **Discard modifications**: The modifications are deleted and the live version is restored in the author interface. The status color is green again.
|
||||
* **Publish modifications**: The modifications are published to the live version. The status color is green again.
|
||||
|
||||
- **Save a draft**: The page is not published and not accessible on the frontend. The status color is red.
|
||||
- **Publish a page**: The page is published and accessible on the frontend. The status color is green.
|
||||
- **Unpublish a page**: The page is not accessible on the frontend and only accessible in the author interface. The status color is red again.
|
||||
- **Delete a page**: The page is completely deleted from the frontend and the author interface.
|
||||
- **Save modifications**: The page is published and accessible on the frontend. The modified draft is only visible in the author interface. The status color is orange.
|
||||
- **Discard modifications**: The modifications are deleted, and the live version is restored in the author interface. The status color returns to green.
|
||||
- **Publish modifications**: The modifications are published to the live version. The status color returns to green.
|
377
system/typemill/Controllers/ControllerApiKixote.php
Normal file
377
system/typemill/Controllers/ControllerApiKixote.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace Typemill\Controllers;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Typemill\Models\Validation;
|
||||
use Typemill\Models\License;
|
||||
use Typemill\Models\Settings;
|
||||
use Typemill\Models\User;
|
||||
use Typemill\Models\ApiCalls;
|
||||
use Typemill\Static\Translations;
|
||||
|
||||
class ControllerApiKixote extends Controller
|
||||
{
|
||||
private $error = false;
|
||||
|
||||
public function getKixoteSettings(Request $request, Response $response)
|
||||
{
|
||||
$settingsModel = new Settings();
|
||||
$kixoteSettings = $settingsModel->getKixoteSettings();
|
||||
|
||||
if(!$kixoteSettings)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'could not load kixote settings.'
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
# send to Kixote
|
||||
$response->getBody()->write(json_encode([
|
||||
'settings' => $kixoteSettings
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
|
||||
}
|
||||
|
||||
public function updateKixoteSettings(Request $request, Response $response)
|
||||
{
|
||||
$params = $request->getParsedBody();
|
||||
$kixoteSettings = $params['kixotesettings'] ?? false;
|
||||
$validate = new Validation();
|
||||
|
||||
if(isset($kixoteSettings['promptlist']))
|
||||
{
|
||||
$promptErrors = false;
|
||||
foreach($kixoteSettings['promptlist'] as $name => $values)
|
||||
{
|
||||
$validInput = $validate->kixotePrompt($values);
|
||||
if($validInput !== true)
|
||||
{
|
||||
$promptErrors = true;
|
||||
$kixoteSettings['promptlist'][$name]['errors'] = $validInput;
|
||||
}
|
||||
else
|
||||
{
|
||||
unset($kixoteSettings['promptlist'][$name]['errors']);
|
||||
}
|
||||
}
|
||||
|
||||
if($promptErrors)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'please correct the errors in the form',
|
||||
'kixotesettings' => $kixoteSettings
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$settingsModel = new Settings();
|
||||
$result = $settingsModel->updateKixoteSettings($kixoteSettings);
|
||||
|
||||
if(!$result)
|
||||
{
|
||||
# restore the current kixote-settings
|
||||
$kixoteSettings = $settingsModel->getKixoteSettings();
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'error while saving settings.',
|
||||
'kixotesettings' => $kixoteSettings
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
# send to Kixote
|
||||
$response->getBody()->write(json_encode([
|
||||
'kixotesettings' => $kixoteSettings
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
|
||||
# initial token statistics
|
||||
public function getTokenStats(Request $request, Response $response): Response
|
||||
{
|
||||
$aiservice = false;
|
||||
$tokenstats = 0;
|
||||
$useragreement = false;
|
||||
$user = new User();
|
||||
$username = $request->getAttribute('c_username');
|
||||
|
||||
if(!$user->setUser($username))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('We did not find the a user.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if(isset($this->settings['aiservice']) && $this->settings['aiservice'] !== 'none')
|
||||
{
|
||||
$aiservice = $this->settings['aiservice'];
|
||||
}
|
||||
|
||||
if($aiservice)
|
||||
{
|
||||
$userdata = $user->getUserData();
|
||||
if(isset($userdata['aiservices']) && in_array($aiservice, $userdata['aiservices']))
|
||||
{
|
||||
$useragreement = true;
|
||||
}
|
||||
}
|
||||
|
||||
# get toke stats for AI service
|
||||
if($aiservice && $useragreement)
|
||||
{
|
||||
switch ($aiservice)
|
||||
{
|
||||
case 'chatgpt':
|
||||
$tokenstats = [
|
||||
'url' => 'https://platform.openai.com/settings/organization/billing/overview',
|
||||
'label' => 'ChatGPT Billing'
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
$tokenstats = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if($tokenstats === false)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Could not get tokenstats.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Success',
|
||||
'aiservice' => $aiservice,
|
||||
'useragreement' => $useragreement,
|
||||
'tokenstats' => $tokenstats
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
|
||||
# initial token statistics
|
||||
public function agreeToAiService(Request $request, Response $response): Response
|
||||
{
|
||||
$aiservice = false;
|
||||
$user = new User();
|
||||
$username = $request->getAttribute('c_username');
|
||||
|
||||
if(!$user->setUserWithPassword($username))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('We did not find the a user or usermail.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if(isset($this->settings['aiservice']) && $this->settings['aiservice'] !== 'none')
|
||||
{
|
||||
$aiservice = $this->settings['aiservice'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('No valid ai service has been selected.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$agreements = $user->getValue('aiservices');
|
||||
|
||||
if(!$agreements)
|
||||
{
|
||||
$agreements = [$aiservice];
|
||||
}
|
||||
elseif(!isset($agreements[$aiservice]))
|
||||
{
|
||||
$agreements[] = $aiservice;
|
||||
}
|
||||
|
||||
$user->setValue('aiservices', $agreements);
|
||||
if($user->updateUser() !== true)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('We could not update your user settings, please try again or agree to ' . $aiservice . ' in your user profile.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Success'
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
|
||||
private function getKixoteJWT(Request $request, Response $response)
|
||||
{
|
||||
# this will authenticate from service.typemill.net (e.g. for template service)
|
||||
$license = new License();
|
||||
$jwt = $license->getToken();
|
||||
if($jwt)
|
||||
{
|
||||
$this->error = $license->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
# if no agb-confirmation
|
||||
$confirm = $settings['kixote_confirm'] ?? false;
|
||||
if(!$confirm)
|
||||
{
|
||||
$this->error = 'Please read and accept the AGB before you start with our service.';
|
||||
return false;
|
||||
}
|
||||
|
||||
return $jwt;
|
||||
}
|
||||
|
||||
public function promptKixote(Request $request, Response $response)
|
||||
{
|
||||
$jwt = $this->getKixoteJWT();
|
||||
if(!$jwt)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => $this->error
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$params = $request->getParsedBody();
|
||||
|
||||
$params['name'] = ''; # will trigger some cool stuff in kixote
|
||||
$params['prompt'] = ''; # the prompt itself
|
||||
$params['article'] = ''; # the current article
|
||||
$params['tone'] = ''; # the tone
|
||||
|
||||
if(!isset($params['prompt']) OR !is_array($params['article']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Prompt or article missing.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
# validate input
|
||||
$validate = new Validation();
|
||||
$validationresult = $validate->newLicense($params['license']);
|
||||
if($validationresult !== true)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Please correct your input.'),
|
||||
'errors' => $validate->returnFirstValidationErrors($validationresult)
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
# send to Kixote
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Licence has been stored'),
|
||||
'licensedata' => $licensedata
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
|
||||
public function promptChatGPT(Request $request, Response $response): Response
|
||||
{
|
||||
# check if user has accepted
|
||||
|
||||
$params = $request->getParsedBody();
|
||||
|
||||
$params['name'] = $params['name'] ?? '';
|
||||
$params['prompt'] = $params['prompt'] ?? '';
|
||||
$params['article'] = $params['article'] ?? '';
|
||||
$params['tone'] = $params['tone'] ?? '';
|
||||
|
||||
$settingsModel = new Settings();
|
||||
$model = $this->settings['chatgptModel'] ?? false;
|
||||
$apikey = $settingsModel->getSecret('chatgptKey');
|
||||
|
||||
if (empty($params['prompt']) || !is_string($params['prompt']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Prompt is missing or invalid.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if (empty($params['article']) || !is_string($params['article']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Article is missing or invalid.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if (!$model || !$apikey)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Model or api key for chatgpt is missing, please add it in the system settings.'
|
||||
]));
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$url = 'https://api.openai.com/v1/chat/completions';
|
||||
$authHeader = "Authorization: Bearer $apikey";
|
||||
|
||||
$postdata = [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => 'You are a content editor and writing assistant. If the user prompt does not explicitly specify otherwise, apply the prompt to the provided article and return only the updated article in Markdown syntax, without any extra comments or explanations. If you find the tag <focus></focus>, modify only the content inside these tags and leave everything else unchanged. Always return the full article.'
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $params['prompt'] . "\n" . $params['article']
|
||||
],
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
'max_tokens' => 2000,
|
||||
];
|
||||
|
||||
$apiservice = new ApiCalls();
|
||||
$apiResponse = $apiservice->makePostCall($url, $postdata, $authHeader);
|
||||
|
||||
if (!$apiResponse)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Failed to communicate with ChatGPT',
|
||||
'error' => $apiservice->getError()
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
$data = json_decode($apiResponse, true);
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => 'Success',
|
||||
'data' => $data,
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
|
||||
}
|
||||
}
|
@@ -20,7 +20,7 @@ class ControllerApiSystemPlugins extends Controller
|
||||
$extension = new Extension();
|
||||
$formdefinitions = $extension->getPluginDefinition($pluginname);
|
||||
$formdefinitions = $this->addDatasets($formdefinitions['forms']['fields']);
|
||||
$plugindata = [];
|
||||
# $plugindata = [];
|
||||
|
||||
# validate input
|
||||
$validator = new Validation();
|
||||
@@ -43,8 +43,20 @@ class ControllerApiSystemPlugins extends Controller
|
||||
}
|
||||
|
||||
# store updated settings here
|
||||
$settings = new Settings();
|
||||
$updatedSettings = $settings->updateSettings($validatedOutput, 'plugins', $pluginname);
|
||||
$settingsModel = new Settings();
|
||||
$securityFields = $settingsModel->findSecurityDefinitions($formdefinitions);
|
||||
if(!empty($securityFields))
|
||||
{
|
||||
$splitSettings = $settingsModel->extractSecuritySettings($validatedOutput, $securityFields);
|
||||
$validatedOutput = $splitSettings['settings'];
|
||||
|
||||
if($splitSettings['securitySettings'] && !empty($splitSettings['securitySettings']))
|
||||
{
|
||||
$settingsModel->updateSecuritySettings($splitSettings['securitySettings'], 'plugins', $pluginname);
|
||||
}
|
||||
}
|
||||
|
||||
$updatedSettings = $settingsModel->updateSettings($validatedOutput, 'plugins', $pluginname);
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('settings have been saved')
|
||||
|
@@ -64,7 +64,19 @@ class ControllerApiSystemSettings extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
# store updated settings here
|
||||
$securityFields = $settingsModel->findSecurityDefinitions($formdefinitions);
|
||||
|
||||
if(!empty($securityFields))
|
||||
{
|
||||
$splitSettings = $settingsModel->extractSecuritySettings($validatedOutput, $securityFields);
|
||||
$validatedOutput = $splitSettings['settings'];
|
||||
|
||||
if($splitSettings['securitySettings'] && !empty($splitSettings['securitySettings']))
|
||||
{
|
||||
$settingsModel->updateSecuritySettings($splitSettings['securitySettings']);
|
||||
}
|
||||
}
|
||||
|
||||
$updatedSettings = $settingsModel->updateSettings($validatedOutput);
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
|
@@ -40,8 +40,20 @@ class ControllerApiSystemThemes extends Controller
|
||||
unset($validatedOutput['customcss']);
|
||||
|
||||
# store updated settings here
|
||||
$settings = new Settings();
|
||||
$updatedSettings = $settings->updateSettings($validatedOutput, 'themes', $themename);
|
||||
$settingsModel = new Settings();
|
||||
$securityFields = $settingsModel->findSecurityDefinitions($formdefinitions);
|
||||
if(!empty($securityFields))
|
||||
{
|
||||
$splitSettings = $settingsModel->extractSecuritySettings($validatedOutput, $securityFields);
|
||||
$validatedOutput = $splitSettings['settings'];
|
||||
|
||||
if($splitSettings['securitySettings'] && !empty($splitSettings['securitySettings']))
|
||||
{
|
||||
$settingsModel->updateSecuritySettings($splitSettings['securitySettings'], 'themes', $themename);
|
||||
}
|
||||
}
|
||||
|
||||
$updatedSettings = $settingsModel->updateSettings($validatedOutput, 'themes', $themename);
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('settings have been saved')
|
||||
|
106
system/typemill/Models/ApiCalls.php
Normal file
106
system/typemill/Models/ApiCalls.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Typemill\Models;
|
||||
|
||||
class ApiCalls
|
||||
{
|
||||
private $error = null;
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function makePostCall(string $url, array $data, $authHeader = '')
|
||||
{
|
||||
if (in_array('curl', get_loaded_extensions())) {
|
||||
return $this->makeCurlCall($url, 'POST', $data, $authHeader);
|
||||
}
|
||||
|
||||
return $this->makeFileGetContentsCall($url, 'POST', $data, $authHeader);
|
||||
}
|
||||
|
||||
public function makeGetCall($url, $authHeader = '')
|
||||
{
|
||||
if (in_array('curl', get_loaded_extensions())) {
|
||||
return $this->makeCurlCall($url, 'GET', null, $authHeader);
|
||||
}
|
||||
|
||||
return $this->makeFileGetContentsCall($url, 'GET', null, $authHeader);
|
||||
}
|
||||
|
||||
private function makeCurlCall($url, $method, $data = false, $authHeader = '')
|
||||
{
|
||||
$this->error = null;
|
||||
|
||||
$headers = [
|
||||
"Content-Type: application/json",
|
||||
];
|
||||
|
||||
if (!empty($authHeader)) {
|
||||
$headers[] = $authHeader;
|
||||
}
|
||||
|
||||
$curl = curl_init($url);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
if ($method === 'POST' && $data) {
|
||||
$postdata = json_encode($data);
|
||||
if ($postdata === false) {
|
||||
$this->error = "JSON encoding error: " . json_last_error_msg();
|
||||
return false;
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $postdata);
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_FAILONERROR, true);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
|
||||
if ($response === false) {
|
||||
$this->error = curl_error($curl);
|
||||
}
|
||||
curl_close($curl);
|
||||
|
||||
return $response !== false ? $response : false;
|
||||
}
|
||||
|
||||
private function makeFileGetContentsCall($url, $method, $data = null, $authHeader = '')
|
||||
{
|
||||
$this->error = null;
|
||||
|
||||
$headers = [
|
||||
"Content-Type: application/json"
|
||||
];
|
||||
|
||||
if (!empty($authHeader)) {
|
||||
$headers[] = $authHeader;
|
||||
}
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'method' => $method,
|
||||
'ignore_errors' => true,
|
||||
'header' => implode("\r\n", $headers),
|
||||
]
|
||||
];
|
||||
|
||||
if ($method === 'POST' && $data !== null) {
|
||||
$postdata = json_encode($data);
|
||||
if ($postdata === false) {
|
||||
$this->error = "JSON encoding error: " . json_last_error_msg();
|
||||
return false;
|
||||
}
|
||||
$options['http']['content'] = $postdata;
|
||||
}
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$response = file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
$this->error = 'file_get_contents failed for ' . $method . ' request.';
|
||||
}
|
||||
|
||||
return $response !== false ? $response : false;
|
||||
}
|
||||
}
|
@@ -87,7 +87,6 @@ class License
|
||||
|
||||
# check if license data are valid and not manipulated
|
||||
$licenseStatus = $this->validateLicense($licensedata);
|
||||
|
||||
if($licenseStatus !== true)
|
||||
{
|
||||
$this->message = Translations::translate('The license data are invalid. ') . $this->message;
|
||||
@@ -97,7 +96,6 @@ class License
|
||||
|
||||
# check if website uses licensed domain
|
||||
$licenseDomain = $this->checkLicenseDomain($licensedata['domain'], $urlinfo);
|
||||
|
||||
if(!$licenseDomain)
|
||||
{
|
||||
$this->message = Translations::translate('The website is running not under the domain of your license.');
|
||||
@@ -107,7 +105,6 @@ class License
|
||||
|
||||
# check if subscription period is paid
|
||||
$subscriptionPaid = $this->checkLicenseDate($licensedata['payed_until']);
|
||||
|
||||
if(!$subscriptionPaid)
|
||||
{
|
||||
$storage = new StorageWrapper('\Typemill\Models\Storage');
|
||||
|
@@ -107,6 +107,47 @@ class Settings
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getKixoteSettings()
|
||||
{
|
||||
$defaultSettings = $this->storage->getYaml('systemSettings', '', 'kixote.yaml');
|
||||
$userSettings = $this->storage->getYaml('settingsFolder', '', 'kixote.yaml');
|
||||
|
||||
if ($userSettings)
|
||||
{
|
||||
foreach ($defaultSettings['promptlist'] as $key => $prompt)
|
||||
{
|
||||
if (isset($userSettings['promptlist'][$key]))
|
||||
{
|
||||
# Use active setting from user but keep system settings intact
|
||||
$active = $userSettings['promptlist'][$key]['active'];
|
||||
$userSettings['promptlist'][$key] = $prompt;
|
||||
$userSettings['promptlist'][$key]['active'] = $active;
|
||||
}
|
||||
else
|
||||
{
|
||||
# New prompt from system settings, add it to user settings
|
||||
$userSettings['promptlist'][$key] = $prompt;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$userSettings = $defaultSettings;
|
||||
}
|
||||
|
||||
return $userSettings;
|
||||
}
|
||||
|
||||
public function updateKixoteSettings($kixoteSettings)
|
||||
{
|
||||
if($this->storage->updateYaml('settingsFolder', '', 'kixote.yaml', $kixoteSettings))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getObjectSettings($objectType, $objectName)
|
||||
{
|
||||
$objectSettings = $this->storage->getYaml($objectType, $objectName, $objectName . '.yaml');
|
||||
@@ -245,4 +286,101 @@ class Settings
|
||||
return false;
|
||||
}
|
||||
|
||||
public function findSecurityDefinitions($definitions, $securityDefinitions = [])
|
||||
{
|
||||
foreach ($definitions as $fieldname => $definition)
|
||||
{
|
||||
if (isset($definition['fields']))
|
||||
{
|
||||
$securityDefinitions = $this->findSecurityDefinitions($definition['fields'], $securityDefinitions);
|
||||
}
|
||||
|
||||
if (isset($definition['type']) && $definition['type'] === 'password')
|
||||
{
|
||||
$securityDefinitions[] = $fieldname;
|
||||
}
|
||||
}
|
||||
|
||||
return $securityDefinitions;
|
||||
}
|
||||
|
||||
public function extractSecuritySettings($settings, $securityFields)
|
||||
{
|
||||
$securitySettings = [];
|
||||
|
||||
foreach ($securityFields as $fieldname)
|
||||
{
|
||||
if (isset($settings[$fieldname]))
|
||||
{
|
||||
$securitySettings[$fieldname] = $settings[$fieldname];
|
||||
unset($settings[$fieldname]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'settings' => $settings,
|
||||
'securitySettings' => $securitySettings
|
||||
];
|
||||
}
|
||||
|
||||
public function updateSecuritySettings($newSecuritySettings, $themeorplugin = null, $themeorpluginname = null)
|
||||
{
|
||||
# problem that settings with same name will overwrite (e.g. from theme and plugins)
|
||||
$securitySettings = $this->getSecuritySettings();
|
||||
foreach($newSecuritySettings as $fieldname => $value)
|
||||
{
|
||||
if($themeorplugin && $themeorpluginname)
|
||||
{
|
||||
$securitySettings[$themeorplugin][$themeorpluginname][$fieldname] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$securitySettings[$fieldname] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$secrets = $this->storage->updateYaml('settingsFolder', '', 'secrets.yaml', $securitySettings);
|
||||
if($secrets)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getSecuritySettings()
|
||||
{
|
||||
$secrets = $this->storage->getYaml('settingsFolder', '', 'secrets.yaml');
|
||||
|
||||
if($secrets)
|
||||
{
|
||||
return $secrets;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getSecret(string $fieldname, $objecttype = null, $objectname = null)
|
||||
{
|
||||
$secrets = $this->storage->getYaml('settingsFolder', '', 'secrets.yaml');
|
||||
|
||||
if(!$secrets)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if($fieldname && $objecttype && $objectname)
|
||||
{
|
||||
if(isset($secrets[$objecttype][$objectname][$fieldname]))
|
||||
{
|
||||
return $secrets[$objecttype][$objectname][$fieldname];
|
||||
}
|
||||
}
|
||||
|
||||
if($fieldname && isset($secrets[$fieldname]))
|
||||
{
|
||||
return $secrets[$fieldname];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -682,6 +682,26 @@ class Validation
|
||||
return $v->errors();
|
||||
}
|
||||
|
||||
public function kixotePrompt(array $params)
|
||||
{
|
||||
$v = new Validator($params);
|
||||
|
||||
$v->rule('required', 'title', 'content', 'active', 'system');
|
||||
$v->rule('regex', 'title', '/^[a-z0-9 ]+$/i');
|
||||
$v->rule('lengthBetween', 'title', 2,20);
|
||||
$v->rule('noHTML', 'content');
|
||||
$v->rule('lengthBetween', 'content',2,5000);
|
||||
$v->rule('boolean', 'active');
|
||||
$v->rule('boolean', 'system');
|
||||
|
||||
if($v->validate())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return $v->errors();
|
||||
}
|
||||
|
||||
/**
|
||||
* validation for password recovery
|
||||
*
|
||||
@@ -848,7 +868,7 @@ class Validation
|
||||
$v->rule('lengthMax', $fieldName, 10000);
|
||||
break;
|
||||
case "password":
|
||||
$v->rule('lengthMax', $fieldName, 100);
|
||||
$v->rule('lengthMax', $fieldName, 500);
|
||||
break;
|
||||
case "radio":
|
||||
$v->rule('in', $fieldName, $fieldDefinitions['options']);
|
||||
|
@@ -1,5 +1,79 @@
|
||||
.iconwrapper {
|
||||
position:relative;
|
||||
height:60px;
|
||||
width:60px;
|
||||
overflow: hidden;
|
||||
border-radius: 30%;
|
||||
}
|
||||
.magicicon{
|
||||
position: absolute;
|
||||
top:10px;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
color: white;
|
||||
animation: glow .5s infinite alternate;
|
||||
}
|
||||
@keyframes glow {
|
||||
0% {
|
||||
fill: black;
|
||||
filter: drop-shadow(0 0 5px black);
|
||||
}
|
||||
100% {
|
||||
fill: rgb(15, 50, 46);
|
||||
filter: drop-shadow(0 0 15px rgb(15, 50, 46));
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 1px;
|
||||
height:80px;
|
||||
background: #ccc;
|
||||
box-shadow: 0 0 60px 10px #eee;
|
||||
transform: translate(-20px);
|
||||
clip-path: inset(0);
|
||||
animation:
|
||||
l4-1 1s ease-in-out infinite alternate,
|
||||
l4-2 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes l4-1 {
|
||||
100% {transform: translateX(80px)}
|
||||
}
|
||||
@keyframes l4-2 {
|
||||
33% {clip-path: inset(0 0 0 -100px)}
|
||||
50% {clip-path: inset(0 0 0 0) }
|
||||
83% {clip-path: inset(0 -100px 0 0)}
|
||||
}
|
||||
|
||||
|
||||
@keyframes wobble-line-with-trailing-shadow {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.8),
|
||||
0 20px 30px rgba(255, 255, 255, 0.6),
|
||||
0 40px 60px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(60px);
|
||||
box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.8),
|
||||
0 20px 30px rgba(255, 255, 255, 0.6),
|
||||
0 40px 60px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.8),
|
||||
0 20px 30px rgba(255, 255, 255, 0.6),
|
||||
0 40px 60px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
#loading-overlay .animate-wobble-line-with-trailing-shadow {
|
||||
animation: wobble-line-with-trailing-shadow 2s ease-in-out infinite;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/********************
|
||||
* SVG ICONS *
|
||||
* SVG ICONS *
|
||||
********************/
|
||||
|
||||
.icon {
|
||||
@@ -12,7 +86,7 @@
|
||||
}
|
||||
.icon.baseline{
|
||||
top: 0.125em;
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -862,6 +862,10 @@ video {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@@ -1026,18 +1030,10 @@ video {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-3\/4 {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.w-3\/5 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
@@ -1054,10 +1050,6 @@ video {
|
||||
width: 83.333333%;
|
||||
}
|
||||
|
||||
.w-54rem {
|
||||
width: 54rem;
|
||||
}
|
||||
|
||||
.w-6 {
|
||||
width: 1.5rem;
|
||||
}
|
||||
@@ -1082,10 +1074,6 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-48 {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.max-w-4xl {
|
||||
max-width: 56rem;
|
||||
}
|
||||
@@ -1153,6 +1141,10 @@ video {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.list-decimal {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.list-disc {
|
||||
list-style-type: disc;
|
||||
}
|
||||
@@ -1201,12 +1193,24 @@ video {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1225,6 +1229,14 @@ video {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@@ -1309,6 +1321,11 @@ video {
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(107 114 128 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
@@ -1349,6 +1366,11 @@ video {
|
||||
border-color: rgb(250 250 249 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-stone-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(120 113 108 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-stone-700 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(68 64 60 / var(--tw-border-opacity));
|
||||
@@ -1399,6 +1421,11 @@ video {
|
||||
border-top-color: rgb(255 255 255 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-black\/75 {
|
||||
background-color: rgb(0 0 0 / 0.75);
|
||||
}
|
||||
@@ -1453,6 +1480,11 @@ video {
|
||||
background-color: rgb(13 148 136 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-teal-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(15 118 110 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -1467,6 +1499,10 @@ video {
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-80 {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
|
||||
.bg-opacity-90 {
|
||||
--tw-bg-opacity: 0.9;
|
||||
}
|
||||
@@ -1517,11 +1553,6 @@ video {
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.px-16 {
|
||||
padding-left: 4rem;
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -1562,16 +1593,6 @@ video {
|
||||
padding-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.py-10 {
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.py-16 {
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
@@ -1597,6 +1618,10 @@ video {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pb-0 {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.pb-3 {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
@@ -1753,6 +1778,10 @@ video {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -1929,6 +1958,18 @@ video {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-colors {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -1993,6 +2034,11 @@ video {
|
||||
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-stone-600:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(87 83 78 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-stone-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(68 64 60 / var(--tw-bg-opacity));
|
||||
@@ -2028,6 +2074,11 @@ video {
|
||||
background-color: rgb(202 138 4 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-rose-500:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(244 63 94 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-stone-100:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(245 245 244 / var(--tw-text-opacity));
|
||||
@@ -2048,6 +2099,16 @@ video {
|
||||
color: rgb(41 37 36 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-teal-300:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(94 234 212 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-teal-500:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(20 184 166 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
@@ -2314,27 +2375,22 @@ video {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.lg\:my-8 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
.lg\:ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.lg\:mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.lg\:mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.lg\:ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.lg\:mr-3 {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.lg\:mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.lg\:block {
|
||||
display: block;
|
||||
}
|
||||
@@ -2359,14 +2415,42 @@ video {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.lg\:w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.lg\:w-1\/4 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.lg\:w-2\/5 {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.lg\:w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.lg\:w-3\/4 {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.lg\:w-3\/5 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.lg\:w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.lg\:w-48 {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.lg\:w-54rem {
|
||||
width: 54rem;
|
||||
}
|
||||
|
||||
.lg\:w-80 {
|
||||
width: 20rem;
|
||||
}
|
||||
@@ -2375,34 +2459,6 @@ video {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.lg\:w-48 {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.lg\:w-2\/5 {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.lg\:w-3\/5 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.lg\:w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.lg\:w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.lg\:w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.lg\:w-54rem {
|
||||
width: 54rem;
|
||||
}
|
||||
|
||||
.lg\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -2420,11 +2476,6 @@ video {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.lg\:px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.lg\:px-12 {
|
||||
padding-left: 3rem;
|
||||
padding-right: 3rem;
|
||||
@@ -2435,9 +2486,9 @@ video {
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
.lg\:py-16 {
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 4rem;
|
||||
.lg\:px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.lg\:px-6 {
|
||||
@@ -2445,22 +2496,27 @@ video {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.lg\:pb-3 {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.lg\:pt-4 {
|
||||
padding-top: 1rem;
|
||||
.lg\:py-16 {
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.lg\:pb-0 {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.lg\:pb-3 {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.lg\:pr-3 {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.lg\:pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.lg\:text-black {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(0 0 0 / var(--tw-text-opacity));
|
||||
|
@@ -1,3 +1,12 @@
|
||||
app.component('component-paragraph', {
|
||||
props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'],
|
||||
template: `<div :class="css ? css : ''" class="w-full mt-5 mb-5">
|
||||
<p :for="name" class="block mb-1 font-medium">{{ $filters.translate(label) }}</p>
|
||||
<p v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
})
|
||||
|
||||
|
||||
app.component('component-text', {
|
||||
props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors'],
|
||||
template: `<div :class="css ? css : ''" class="w-full mt-5 mb-5">
|
||||
@@ -15,7 +24,7 @@ app.component('component-text', {
|
||||
:value="value"
|
||||
@input="update($event, name)"><slot></slot>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -40,7 +49,7 @@ app.component('component-textarea', {
|
||||
:value="value"
|
||||
@input="update($event, name)"></textarea><slot></slot>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -84,7 +93,7 @@ app.component('component-codearea', {
|
||||
<pre aria-hidden="true" class="highlight hljs"><code data-el="highlight" v-html="highlighted"></code></pre>
|
||||
</div>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
mounted: function()
|
||||
{
|
||||
@@ -152,7 +161,7 @@ app.component('component-select', {
|
||||
<option v-for="option,optionkey in options" v-bind:value="optionkey">{{option}}</option>
|
||||
</select><slot></slot>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -184,7 +193,7 @@ app.component('component-checkbox', {
|
||||
<span class="ml-2 text-sm">{{ $filters.translate(checkboxlabel) }}</span>
|
||||
</label><slot></slot>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
mounted: function()
|
||||
{
|
||||
@@ -220,7 +229,7 @@ app.component('component-checkboxlist', {
|
||||
<span class="ml-2 text-sm">{{ $filters.translate(option) }}</span>
|
||||
</label><slot></slot>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
mounted: function()
|
||||
{
|
||||
@@ -260,7 +269,7 @@ app.component('component-radio', {
|
||||
<span class="ml-2 text-sm">{{ $filters.translate(option) }}</span>
|
||||
</label><slot></slot>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function(picked, name)
|
||||
@@ -288,7 +297,7 @@ app.component('component-number', {
|
||||
:value="value"
|
||||
@input="update($event, name)"><slot></slot>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -320,7 +329,7 @@ app.component('component-date', {
|
||||
@input="update($event, name)"><slot></slot>
|
||||
</div>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -354,7 +363,7 @@ app.component('component-email', {
|
||||
@input="update($event, name)"><slot></slot>
|
||||
</div>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -387,7 +396,7 @@ app.component('component-tel', {
|
||||
@input="update($event, name)"><slot></slot>
|
||||
</div>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -421,7 +430,7 @@ app.component('component-url', {
|
||||
@input="update($event, name)"><slot></slot>
|
||||
</div>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -454,7 +463,7 @@ app.component('component-color', {
|
||||
@input="update($event, name)"><slot></slot>
|
||||
</div>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>`,
|
||||
methods: {
|
||||
update: function($event, name)
|
||||
@@ -509,7 +518,7 @@ app.component('component-password', {
|
||||
<div class="flex justify-between text-xs">
|
||||
<div class="w-2/3">
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>
|
||||
<div v-if="generator" class="w-1/3 text-right">
|
||||
<button @click.prevent="generatePassword()" class="text-teal-600">generate a password</button>
|
||||
@@ -791,7 +800,7 @@ app.component('component-image', {
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="errors[name]" class="text-xs text-red-500">{{ errors[name] }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1026,7 +1035,7 @@ app.component('component-file', {
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="error" class="text-xs text-red-500">{{ error }}</p>
|
||||
<p v-else class="text-xs">{{ $filters.translate(description) }}</p>
|
||||
<p v-else class="text-xs" v-html="$filters.translate(description)"></p>
|
||||
|
||||
<Transition name="initial" appear>
|
||||
<div v-if="showmedialib" class="fixed top-0 left-0 right-0 bottom-0 bg-stone-100 z-50">
|
||||
|
File diff suppressed because one or more lines are too long
@@ -51,15 +51,18 @@ const translatefilter = {
|
||||
.replace(/^_+|_+$/g, "") // Trim underscores from the start and end of the string
|
||||
.toUpperCase(); // Convert to uppercase
|
||||
|
||||
|
||||
let translation_value = data.labels[translation_key];
|
||||
|
||||
if(!translation_value || translation_value.length === 0)
|
||||
{
|
||||
return value
|
||||
}
|
||||
else
|
||||
{
|
||||
return data.labels[translation_key]
|
||||
translation_value = value;
|
||||
}
|
||||
|
||||
/* process markdown links */
|
||||
translation_value = translation_value.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-teal-500">$1</a>');
|
||||
|
||||
return translation_value;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ const app = Vue.createApp({
|
||||
template: `<Transition name="initial" appear>
|
||||
<div class="w-full">
|
||||
<form class="w-full my-8">
|
||||
{{formdata}}
|
||||
<div v-for="(fieldDefinition, fieldname) in formDefinitions">
|
||||
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
|
||||
<legend class="text-lg font-medium">{{ $filters.translate(fieldDefinition.legend) }}</legend>
|
||||
|
@@ -20,6 +20,7 @@ use Typemill\Controllers\ControllerApiAuthorBlock;
|
||||
use Typemill\Controllers\ControllerApiAuthorMeta;
|
||||
use Typemill\Controllers\ControllerApiAuthorShortcode;
|
||||
use Typemill\Controllers\ControllerApiTestmail;
|
||||
use Typemill\Controllers\ControllerApiKixote;
|
||||
|
||||
$app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) {
|
||||
|
||||
@@ -88,11 +89,18 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) {
|
||||
$group->post('/meta', ControllerApiAuthorMeta::class . ':updateMeta')->setName('api.metadata.update')->add(new ApiAuthorization($acl, 'mycontent', 'update'));
|
||||
|
||||
# KIXOTE
|
||||
$group->get('/kixotesettings', ControllerApiKixote::class . ':getKixoteSettings')->setName('api.kixotesettings.get')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
$group->put('/kixotesettings', ControllerApiKixote::class . ':updateKixoteSettings')->setName('api.kixotesettings.put')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
$group->get('/securitylog', ControllerApiGlobals::class . ':showSecurityLog')->setName('api.securitylog.show')->add(new ApiAuthorization($acl, 'system', 'update')); # manager
|
||||
$group->delete('/securitylog', ControllerApiGlobals::class . ':deleteSecurityLog')->setName('api.securitylog.delete')->add(new ApiAuthorization($acl, 'system', 'update')); # manager
|
||||
$group->delete('/cache', ControllerApiGlobals::class . ':deleteCache')->setName('api.cache.delete')->add(new ApiAuthorization($acl, 'system', 'update')); # manager
|
||||
$group->delete('/clearnavigation', ControllerApiGlobals::class . ':clearNavigation')->setName('api.navigation.clear')->add(new ApiAuthorization($acl, 'system', 'update')); # manager
|
||||
|
||||
# KIXOTE Remote Services
|
||||
$group->get('/tokenstats', ControllerApiKixote::class . ':getTokenStats')->setName('api.kixote.tokenstats')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
$group->post('/agreetoaiservice', ControllerApiKixote::class . ':agreeToAiService')->setName('api.kixote.serviceagreement')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
$group->post('/chatgpt', ControllerApiKixote::class . ':promptChatGPT')->setName('api.kixote.chatgpt')->add(new ApiAuthorization($acl, 'mycontent', 'update')); # author
|
||||
|
||||
# API USED ONLY EXTERNALLY
|
||||
$group->get('/systemnavi', ControllerApiGlobals::class . ':getSystemnavi')->setName('api.systemnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member
|
||||
$group->get('/mainnavi', ControllerApiGlobals::class . ':getMainnavi')->setName('api.mainnavi.get')->add(new ApiAuthorization($acl, 'account', 'read')); # member
|
||||
@@ -104,6 +112,7 @@ $app->group('/api/v1', function (RouteCollectorProxy $group) use ($acl) {
|
||||
|
||||
})->add(new ApiAuthentication($settings));
|
||||
|
||||
|
||||
# api-routes from plugins
|
||||
if(isset($routes['api']) && !empty($routes['api']))
|
||||
{
|
||||
|
41
system/typemill/settings/kixote.yaml
Normal file
41
system/typemill/settings/kixote.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
promptlist:
|
||||
brainstorm:
|
||||
title: 'brainstorm'
|
||||
content: "Act as an experienced copywriter and brainstorm 10 compelling article ideas based on the provided topic. The goal is to create content that resonates with the target audience and ranks well on search engines. Identify relevant keywords and trends to align with user interests. Each idea should provide valuable insights, answer common questions, or solve specific problems. Craft engaging headlines that capture attention and match user search intent."
|
||||
active: true
|
||||
system: true
|
||||
outline:
|
||||
title: 'outline'
|
||||
content: "You are an experienced copywriter skilled in crafting compelling articles tailored to a specific target audience. Conduct in-depth research on the following article idea. Identify the most relevant information, authoritative sources, and key insights. Analyze search intent to determine what readers are looking for when they search for this topic. Define the article’s target audience, ideal length, tone, and structure.\n\nBased on your research, create a detailed structured outline for the article, including:\n\n* A compelling title and meta description.\n* Main headlines (H2, H3) that logically structure the content.\n* Bullet points under each headline, summarizing the key points to be covered.\n\nAfter the outline add a divider list, the headline `Research` and add your research findings."
|
||||
active: true
|
||||
system: true
|
||||
outline2:
|
||||
title: 'outline2'
|
||||
content: "You are an experienced copywriter skilled in crafting compelling, well-structured articles tailored to a specific target audience. Based on the provided article idea, develop a comprehensive content outline that includes:\n\n- Title: A compelling, SEO-optimized headline.\n- User Intent: A one-sentence description of the intent, followed by the category in brackets (Informational, Navigational, Transactional, or Commercial).\n- Keywords: A comma-separated list, with the main keyword first, followed by up to five secondary keywords.\n- Target Audience: One sentence who the article is for and what they expect to gain.\n- Ideal Tone & Style: Concise, authoritative, engaging, personal, etc.\n\nBelow that, provide a detailed article structure:\n- Headings (H2, H3): A logical outline that organizes the content.\n- Bullet points under each heading: Summarizing key points to cover."
|
||||
active: true
|
||||
system: true
|
||||
write:
|
||||
title: 'write'
|
||||
content: "Act as an experienced copywriter tasked with creating a comprehensive and engaging article based on the provided outline or draft. If details such as keywords, user intent, target audience, tone, or style are included, ensure the article aligns with them. If no outline is given, structure the article logically based on best practices for the topic.\n\nCraft a compelling article with:\n- **An eye-catching, SEO-optimized title** featuring the primary keyword.\n- **A strong introduction** that immediately hooks the reader—use a compelling question, statistic, expert quote, or relatable scenario. Clearly state what the article will cover.\n- **A well-structured body** with clear subheadings (H2, H3) incorporating relevant keywords. Ensure smooth transitions between sections and provide thorough, engaging explanations.\n- **Bullet points or lists** where appropriate to enhance readability but prioritize a natural flow of ideas.\n- **A unique perspective** by integrating expert insights, statistics, case studies, or real-life examples.\n- **Natural keyword integration** that enhances SEO without disrupting readability.\n\nMaintain a tone and style that align with the article’s goals and audience, ensuring clarity, engagement, and credibility."
|
||||
active: true
|
||||
system: true
|
||||
proofread:
|
||||
title: 'proofread'
|
||||
content: "Review the text for grammar, spelling, and correct word usage. Correct any errors and ensure that the appropriate words are used, while maintaining the original tone, style, and phrasing. At the end of the article, provide a list of all corrections made."
|
||||
active: true
|
||||
system: true
|
||||
refine:
|
||||
title: 'refine'
|
||||
content: "Refine the text to enhance its wording and style. Ensure that the content is readable, clear, concise, and flows naturally. Feel free to rephrase sentences where necessary, but retain the original tone and unique language usage to avoid making the text sound generic."
|
||||
active: true
|
||||
system: true
|
||||
review:
|
||||
title: 'review'
|
||||
content: "Review the article below. Evaluate it based on readability, clarity, SEO best practices, engagement, and search intent. Provide actionable suggestions for improvement, focusing on areas where the text can be refined for better flow, clarity, and alignment with SEO goals."
|
||||
active: true
|
||||
system: true
|
||||
mermaid:
|
||||
title: 'mermaid'
|
||||
content: "Return pure mermaid syntax for a [pie, x, y] diagram."
|
||||
active: true
|
||||
system: true
|
@@ -309,4 +309,35 @@ fieldsetdeveloper:
|
||||
description: "If activated, you can allow login-links with a checkbox in the user profile. This is only available for guest-roles since guests do not have any rights. Login with a link can be helpful if you link from your software to a non-public documentation. Be aware of the low protection that this kind of logins has. If you integrate such links in a SaaS-software, then you should restrict access with login-links to your ips."
|
||||
trustedloginreferrer:
|
||||
type: text
|
||||
label: "Trusted IPs for the login-link-referrer (comma separated)"
|
||||
label: "Trusted IPs for the login-link-referrer (comma separated)"
|
||||
fieldsetai:
|
||||
type: fieldset
|
||||
legend: AI
|
||||
fields:
|
||||
aiprivacy:
|
||||
type: paragraph
|
||||
label: "Privacy Notice"
|
||||
description: "You can use the following AI features in the Kixote interface. If you enable any AI integrations, user inputs will be sent to external services for processing. Please review the details for each service and ensure compliance with your local data privacy regulations. Each user will be asked for confirmation before using a model in the Kixote AI interface."
|
||||
aiservice:
|
||||
type: radio
|
||||
label: 'Choose an AI service'
|
||||
options:
|
||||
none: 'None'
|
||||
# kixote: 'Kixote (not available yet)'
|
||||
chatgpt: 'ChatGPT'
|
||||
chatgptModel:
|
||||
type: select
|
||||
label: 'ChatGPT model'
|
||||
description: "Select the AI model you want to use. Pricing may vary over time, so please check the latest details on [OpenAI's pricing page](https://platform.openai.com/docs/pricing)"
|
||||
options:
|
||||
gpt-4o-mini: 'gpt-4o-mini ($0.6 / 1M output tokens)'
|
||||
gpt-3.5-turbo-0125: 'gpt-3.5-turbo-0125 ($1.50 / 1M output tokens)'
|
||||
gpt-4o: 'gpt-4o ($10.00 / 1M output tokens)'
|
||||
o1-mini: 'o1-mini ($12.00 / 1M output tokens)'
|
||||
o1: 'o1 ($60.00 / 1M output tokens)'
|
||||
chatgptKey:
|
||||
type: password
|
||||
autocomplete: new-password
|
||||
generator: false
|
||||
label: 'ChatGPT Api Key'
|
||||
description: "Enter your ChatGPT API key here. You can generate a new key on [OpenAI's platform](https://platform.openai.com/docs/pricing). For security reasons, your API key is secret and will not be visible again after you leave this page."
|
@@ -26,6 +26,12 @@ darkmode:
|
||||
label: 'Darkmode'
|
||||
checkboxlabel: 'Activate the darkmode for me'
|
||||
type: 'checkbox'
|
||||
aiservices:
|
||||
name: aiservices
|
||||
label: 'AI Services'
|
||||
type: 'checkboxlist'
|
||||
options:
|
||||
chatgpt: "Use ChatGPT and accept their terms and conditions."
|
||||
password:
|
||||
name: password
|
||||
label: 'Actual Password'
|
||||
@@ -36,4 +42,4 @@ newpassword:
|
||||
label: 'New Password'
|
||||
type: 'password'
|
||||
autocomplete: 'new-password'
|
||||
generator: true
|
||||
generator: true
|
||||
|
Reference in New Issue
Block a user