Added initial code to add custom GUID via API and implemented the WebUI form for it.
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12">
|
||||
<form id="page_form" @submit.prevent="addIgnoreRule">
|
||||
<form id="page_form" @submit.prevent="addNewGuid">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title is-unselectable is-justify-center">Add Custom GUID</p>
|
||||
@@ -20,21 +20,33 @@
|
||||
<div class="card-content">
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_ignore_id">Name</label>
|
||||
<label class="label is-unselectable" for="form_guid_name">Name</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_guid_name" type="text" v-model="form.name" placeholder="guid_foobar">
|
||||
<div class="icon is-small is-left"><i class="fas fa-passport"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>All GUIDs names must start with <code>guid_</code>. For example,
|
||||
<code>guid_foobar</code>. You cannot use the same name as an existing GUID.
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The internal GUID reference name. The name must starts with <code>guid</code>, followed by
|
||||
<code>_</code>, <code>lower case [a-z]</code>, <code>0-9</code>, <code>no space</code>.
|
||||
For example, <code>guid_imdb</code>.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_description">Description</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_description" type="text" v-model="form.description"
|
||||
placeholder="This GUID is based on ... db reference">
|
||||
<div class="icon is-small is-left"><i class="fas fa-envelope-open-text"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID description, For information purposes only.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_select_type">Type</label>
|
||||
<div class="control has-icons-left">
|
||||
@@ -49,25 +61,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>We currently only support <code>string</code> type.</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_description">Description</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_description" type="text" v-model="form.description"
|
||||
placeholder="This GUID is based on ... db reference">
|
||||
<div class="icon is-small is-left"><i class="fas fa-envelope-open-text"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID description, For information purposes only.</span>
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>We currently only support <code>string</code> type.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -79,17 +74,28 @@
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
A Valid regular expression to check the value GUID value. To test your patterns, you can use this
|
||||
website
|
||||
<NuxtLink target="_blank" to="https://regex101.com/#php73" v-text="'regex101.com'"/>
|
||||
.
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
A Valid regular expression to check the value GUID value. To test your patterns, you can use this
|
||||
website
|
||||
<NuxtLink target="_blank" to="https://regex101.com/#php73" v-text="'regex101.com'"/>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="form_validation_example">Value example</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" id="form_validation_example" type="text" v-model="form.validator.example"
|
||||
placeholder="(number)">
|
||||
<div class="icon is-small is-left"><i class="fas fa-ear-deaf"></i></div>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>The example to show when invalid value was checked. For example, <code>(number)</code>. For
|
||||
information purposes only.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable">
|
||||
@@ -100,7 +106,8 @@
|
||||
<template v-for="(_, index) in form.validator.tests.valid" :key="`valid-${index}`">
|
||||
<div class="column is-11">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="form.validator.tests.valid[index]">
|
||||
<input class="input" type="text" :id="`valid-${index}`"
|
||||
v-model="form.validator.tests.valid[index]">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,13 +121,12 @@
|
||||
</template>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
The values added here must match the pattern defined above. Example: <code>123</code>.
|
||||
Additionally, the pattern also must support <code>/</code> being part of the value. as we used it
|
||||
for relative GUIDs. There must be a minimum of 1 correct value.
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>
|
||||
The values added here must match the pattern defined above. Example: <code>123</code>.
|
||||
Additionally, the pattern also must support <code>/</code> being part of the value. as we used it
|
||||
for relative GUIDs. The <code>(number)/1/1</code> refers to a relative GUID.
|
||||
There must be a minimum of 1 correct value.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -134,7 +140,8 @@
|
||||
<template v-for="(_, index) in form.validator.tests.invalid" :key="`valid-${index}`">
|
||||
<div class="column is-11">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" v-model="form.validator.tests.invalid[index]">
|
||||
<input class="input" type="text" :id="`invalid-${index}`"
|
||||
v-model="form.validator.tests.invalid[index]">
|
||||
<div class="icon is-small is-left"><i class="fas fa-check"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,18 +155,17 @@
|
||||
</template>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID values with should not match the pattern defined above. Example: <code>abc</code>. There
|
||||
must be a minimum of 1 incorrect value.</span>
|
||||
</span>
|
||||
<span class="icon"><i class="fas fa-info"></i></span>
|
||||
<span>GUID values with should not match the pattern defined above. Example: <code>abc</code>. There
|
||||
must be a minimum of 1 incorrect value.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === checkForm">
|
||||
<button class="button is-fullwidth is-primary" type="submit" :disabled="false === validForm || isSaving"
|
||||
:class="{'is-loading':isSaving}">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
<span>Save</span>
|
||||
@@ -167,7 +173,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer-item">
|
||||
<button class="button is-fullwidth is-danger" type="button" @click="cancelForm">
|
||||
<button class="button is-fullwidth is-danger" type="button" @click="navigateTo('/custom')">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-cancel"></i></span>
|
||||
<span>Cancel</span>
|
||||
@@ -201,15 +207,21 @@ useHead({title: 'Add Custom GUID'})
|
||||
|
||||
const empty_form = {
|
||||
name: '',
|
||||
type: '',
|
||||
type: 'string',
|
||||
description: '',
|
||||
validator: {pattern: '', example: '', tests: {valid: [''], invalid: ['']}}
|
||||
validator: {
|
||||
pattern: '/^[0-9\\\\/]+$/i',
|
||||
example: '(number)',
|
||||
tests: {
|
||||
valid: ['1234567', '1234567/1/1'],
|
||||
invalid: ['1234567a', 'a1234567']
|
||||
}
|
||||
}
|
||||
}
|
||||
const show_page_tips = useStorage('show_page_tips', true)
|
||||
|
||||
const items = ref([])
|
||||
const form = ref(JSON.parse(JSON.stringify(empty_form)))
|
||||
const guids = ref([])
|
||||
const isSaving = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -220,38 +232,121 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
const addIgnoreRule = async () => {
|
||||
const val = guids.value.find(g => g.guid === form.value.db)
|
||||
if (val && val?.validator && val.validator.pattern) {
|
||||
if (!stringToRegex(val.validator.pattern).test(form.value.id)) {
|
||||
notification('error', 'Error', `Invalid GUID value, must match the pattern: '${val.validator.pattern}'. Example ${val.validator.example}`, 5000)
|
||||
return
|
||||
}
|
||||
const addNewGuid = async () => {
|
||||
if (!validForm.value) {
|
||||
notification('error', 'Error', 'Invalid form data.', 5000)
|
||||
return
|
||||
}
|
||||
|
||||
let data = form.value
|
||||
|
||||
data.name = data.name.trim();
|
||||
|
||||
if (data.name.toLowerCase() !== data.name) {
|
||||
notification('error', 'Error', `GUID name must be lowercase.`, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
if (false === stringToRegex('/^[a-z0-9_]+$/').test(data.name)) {
|
||||
notification('error', 'Error', `GUID name must be in ASCII, rules are [lower case, a-z, 0-9, no space] starts with guid_`, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.name.includes(' ')) {
|
||||
notification('error', 'Error', `GUID name must not contain spaces.`, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.name.startsWith('guid_')) {
|
||||
notification('error', 'Error', `GUID name must start with 'guid_'.`, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data.type = data.type.trim().toLowerCase();
|
||||
if (!['string'].includes(data.type)) {
|
||||
notification('error', 'Error', `Invalid GUID type.`, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(`/ignore`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(form.value)
|
||||
toRaw(guids.value).forEach(g => {
|
||||
const name = data.name.split('_')[1]
|
||||
if (g.guid === name) {
|
||||
throw new Error(`GUID with name '${data.name}' already exists.`)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `${e}`, 5000)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const validator = stringToRegex(data.validator.pattern);
|
||||
|
||||
for (let i = 0; i < data.validator.tests.valid.length; i++) {
|
||||
if (!validator.test(data.validator.tests.valid[i])) {
|
||||
notification('error', 'Error', `Correct value '${i}' '${data.validator.tests.valid[i]}' did not match '${data.validator.pattern}'.`, 5000)
|
||||
return false
|
||||
}
|
||||
if (!validator.test(data.validator.tests.valid[i] + '/1')) {
|
||||
notification('error', 'Error', `Correct value '${i}' with relative info '${data.validator.tests.valid[i] + '/1'}' did not match '${data.validator.pattern}'.`, 5000)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.validator.tests.invalid.length; i++) {
|
||||
const invalid = data.validator.tests.invalid[i]
|
||||
if (validator.test(data.validator.tests.invalid[i])) {
|
||||
notification('error', 'Error', `Incorrect value '${i}' '${invalid}' matched '${data.validator.pattern}'.`, 5000)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `Invalid regex pattern.`, 5000)
|
||||
return false
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
try {
|
||||
const response = await request('/system/guids/custom', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
const json = await response.json()
|
||||
const json = await parse_api_response(response)
|
||||
|
||||
if (!response.ok) {
|
||||
notification('error', 'Error', `${json.error.code}: ${json.error.message}`, 5000)
|
||||
return
|
||||
}
|
||||
|
||||
items.value.push(json)
|
||||
|
||||
notification('success', 'Success', 'Successfully added new ignore rule.', 5000)
|
||||
notification('success', 'Success', 'Successfully added new GUID.', 5000)
|
||||
await navigateTo('/custom')
|
||||
} catch (e) {
|
||||
notification('error', 'Error', `Request error. ${e.message}`, 5000)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const checkForm = computed(() => {
|
||||
const {id, type, backend, db} = form.value
|
||||
return '' !== id && '' !== type && '' !== backend && '' !== db
|
||||
const validForm = computed(() => {
|
||||
const data = form.value
|
||||
|
||||
if (!data.name || !data.type || !data.description) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!data.validator.pattern || !data.validator.example) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!data.validator.tests.valid.length || !data.validator.tests.invalid.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !(!data.validator.tests.valid[0] || !data.validator.tests.invalid[0]);
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -13,9 +13,11 @@ use App\Libs\Container;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use App\Libs\Guid;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
final class Guids
|
||||
{
|
||||
@@ -53,7 +55,102 @@ final class Guids
|
||||
{
|
||||
$params = DataUtil::fromRequest($request);
|
||||
|
||||
return api_response(Status::OK, $request->getParsedBody());
|
||||
$requiredFields = [
|
||||
'name',
|
||||
'type',
|
||||
'description',
|
||||
'validator.pattern',
|
||||
'validator.example',
|
||||
'validator.tests.valid',
|
||||
'validator.tests.invalid'
|
||||
];
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!$params->get($field)) {
|
||||
return api_error(r("Field '{field}' is required. And is missing from request.", [
|
||||
'field' => $field
|
||||
]), Status::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->validateName($params->get('name'));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return api_error($e->getMessage(), Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
$pattern = stripslashes($params->get('validator.pattern'));
|
||||
try {
|
||||
preg_match($pattern, '');
|
||||
} catch (Throwable) {
|
||||
return api_error(r("Invalid regex pattern: '{pattern}'.", ['pattern' => $pattern]), Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (count($params->get('validator.tests.valid')) < 1) {
|
||||
return api_error('At least one valid test is required.', Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
foreach ($params->get('validator.tests.valid', []) as $index => $test) {
|
||||
if (empty($test)) {
|
||||
return api_error(r("Empty value {index} - '{test}' is not allowed.", [
|
||||
'index' => $index,
|
||||
'test' => $test
|
||||
]), Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (1 === preg_match($pattern, (string)$test)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return api_error(
|
||||
r("Correct value {index} - '{test}' did not match given pattern '{pattern}'.", [
|
||||
'index' => $index,
|
||||
'test' => $test,
|
||||
'pattern' => $pattern,
|
||||
]),
|
||||
Status::BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (count($params->get('validator.tests.invalid')) < 1) {
|
||||
return api_error('At least one invalid test is required.', Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
foreach ($params->get('validator.tests.invalid', []) as $index => $test) {
|
||||
if (1 !== preg_match($pattern, (string)$test)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return api_error(r("Incorrect value {index} - '{test}' matched given pattern '{pattern}'.", [
|
||||
'index' => $index,
|
||||
'test' => $test,
|
||||
'pattern' => $pattern
|
||||
]), Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $params->get('name'),
|
||||
'type' => $params->get('type'),
|
||||
'description' => $params->get('description'),
|
||||
'validator' => [
|
||||
'pattern' => $params->get('validator.pattern'),
|
||||
'example' => $params->get('validator.example'),
|
||||
'tests' => [
|
||||
'valid' => $params->get('validator.tests.valid'),
|
||||
'invalid' => $params->get('validator.tests.invalid')
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$file = ConfigFile::open(Config::get('guid.file'), 'yaml', autoCreate: true, autoBackup: true);
|
||||
|
||||
if (!$file->has('guids') || !is_array($file->get('guids'))) {
|
||||
$file->set('guids', []);
|
||||
}
|
||||
|
||||
$file->set('guids.' . count($file->get('guids', [])), $data)->persist();
|
||||
|
||||
return api_response(Status::OK, $data);
|
||||
}
|
||||
|
||||
#[Delete(self::URL . '/custom/{index:number}[/]', name: 'system.guids.custom.guid.remove')]
|
||||
@@ -124,4 +221,19 @@ final class Guids
|
||||
|
||||
return $guids;
|
||||
}
|
||||
|
||||
private function validateName(string $name): void
|
||||
{
|
||||
if (false === preg_match('/^[a-z0-9_]+$/i', $name)) {
|
||||
throw new InvalidArgumentException('Name must be alphanumeric and underscores only.');
|
||||
}
|
||||
|
||||
if (strtolower($name) !== $name) {
|
||||
throw new InvalidArgumentException('Name must be lowercase.');
|
||||
}
|
||||
|
||||
if (str_contains($name, ' ')) {
|
||||
throw new InvalidArgumentException('Name must not contain spaces.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user