Visman 4 лет назад
Родитель
Сommit
8a445d6180

+ 33 - 0
app/Models/BBCodeList/Insert.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace ForkBB\Models\BBCodeList;
+
+use ForkBB\Models\Method;
+use ForkBB\Models\BBCodeList\Structure;
+use RuntimeException;
+
+class Insert extends Method
+{
+    /**
+     * Добавляет bb-код в базу
+     */
+    public function insert(Structure $structure): int
+    {
+        if (null !== $structure->getError()) {
+            throw new RuntimeException('BBCode structure has error');
+        }
+
+        $this->model->reset(); // ????
+
+        $vars = [
+            ':tag'       => $structure->tag,
+            ':structure' => $structure->toString(),
+        ];
+        $query = 'INSERT INTO ::bbcode (bb_tag, bb_edit, bb_delete, bb_structure)
+            VALUES (?s:tag, 1, 1, ?s:structure)';
+
+        $this->c->DB->exec($query, $vars);
+
+        return (int) $this->c->DB->lastInsertId();
+    }
+}

+ 335 - 42
app/Models/BBCodeList/Structure.php

@@ -5,9 +5,15 @@ namespace ForkBB\Models\BBCodeList;
 use ForkBB\Core\Container;
 use ForkBB\Models\Model as ParentModel;
 use RuntimeException;
+use Throwable;
 
 class Structure extends ParentModel
 {
+    const TAG_PATTERN  = '%^(?:ROOT|[a-z\*][a-z\d-]{0,10})$%D';
+    const ATTR_PATTERN = '%^[a-z-]{2,15}$%D';
+
+    const JSON_OPTIONS = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR;
+
     public function __construct(Container $container)
     {
         parent::__construct($container);
@@ -17,7 +23,7 @@ class Structure extends ParentModel
             'text only'    => ['text_only'],
             'tags only'    => ['tags_only'],
             'self nesting' => ['self_nesting'],
-            'attrs'        => ['no_attr', 'def_attr'],
+            'attrs'        => ['no_attr', 'def_attr', 'other_attrs'],
         ];
     }
 
@@ -28,7 +34,71 @@ class Structure extends ParentModel
 
     public function toString(): string
     {
+        $a = [
+            'tag'     => $this->tag,
+            'type'    => $this->type,
+            'parents' => $this->parents,
+        ];
+
+        if (! empty($this->handler) && \is_string($this->handler)) {
+            $a['handler'] = $this->handler;
+        }
+
+        if (! empty($this->text_handler) && \is_string($this->text_handler)) {
+            $a['text handler'] = $this->text_handler;
+        }
+
+        if (null !== $this->auto) {
+            $a['auto'] = (bool) $this->auto;
+        }
+
+        if (null !== $this->self_nesting) {
+            $a['self nesting'] = (int) $this->self_nesting > 0 ? (int) $this->self_nesting : false;
+        }
+
+        if (null !== $this->recursive) {
+            $a['recursive'] = true;
+        }
+
+        if (null !== $this->text_only) {
+            $a['text only'] = true;
+        }
+
+        if (null !== $this->tags_only) {
+            $a['tags only'] = true;
+        }
+
+        if (null !== $this->single) {
+            $a['single'] = true;
+        }
+
+        if (null !== $this->pre) {
+            $a['pre'] = true;
+        }
+
+        if (
+            \is_array($this->new_attr)
+            && ! empty($this->new_attr['allowed'])
+            && ! empty($this->new_attr['name'])
+        ) {
+            $this->setBBAttr($this->new_attr['name'], $this->new_attr, ['required', 'format', 'body format', 'text only']);
+        }
+
+        $a['attrs'] = $this->other_attrs;
+
+        if (null !== $this->no_attr) {
+            $a['attrs']['no attr'] = $this->no_attr;
+        }
+
+        if (null !== $this->def_attr) {
+            $a['attrs']['Def'] = $this->def_attr;
+        }
 
+        if (empty($a['attrs'])) {
+            unset($a['attrs']);
+        }
+
+        return \json_encode($a, self::JSON_OPTIONS);
     }
 
     protected function gettype(): string
@@ -131,8 +201,14 @@ class Structure extends ParentModel
         $this->setAttr('self nesting', $value);
     }
 
-    protected function getBBAttr(/* mixed */ $data, array $fields) /* : mixed */
+    protected function getBBAttr(string $name, array $fields) /* : mixed */
     {
+        if (empty($this->attrs[$name])) {
+            return null;
+        }
+
+        $data = $this->attrs[$name];
+
         if (true === $data) {
             return true;
         } elseif (! \is_array($data)) {
@@ -148,7 +224,7 @@ class Structure extends ParentModel
                         break;
                     case 'required':
                     case 'text only':
-                        $value = isset($data[$field]) && true === $data[$field] ? true : null;
+                        $value = isset($data[$field]) ? true : null;
                         break;
                     default:
                         throw new RuntimeException('Unknown attribute property');
@@ -162,74 +238,291 @@ class Structure extends ParentModel
         }
     }
 
-    protected function setBBAttr(/* mixed */ $data, array $fields) /* : mixed */
+    protected function setBBAttr(string $name, /* mixed */ $data, array $fields): void
     {
+        $attrs = $this->getAttr('attrs');
+
         if (
             empty($data['allowed'])
             || $data['allowed'] < 1
         ) {
-            return null;
-        }
+            unset($attrs[$name]);
+        } else {
+            $result = [];
+            foreach ($fields as $field) {
+                $key = \str_replace(' ', '_', $field);
 
-        $result = [];
-        foreach ($fields as $field) {
-            switch ($field) {
-                case 'format':
-                case 'body format':
-                    $value = isset($data[$field]) && \is_string($data[$field]) ? $data[$field] : null;
-                    break;
-                case 'required':
-                case 'text only':
-                    $value = isset($data[$field]) && true === $data[$field] ? true : null;
-                    break;
-                default:
-                    throw new RuntimeException('Unknown attribute property');
-            }
+                switch ($field) {
+                    case 'format':
+                    case 'body format':
+                        $value = ! empty($data[$key]) && \is_string($data[$key]) ? $data[$key] : null;
+                        break;
+                    case 'required':
+                    case 'text only':
+                        $value = ! empty($data[$key]) ? true : null;
+                        break;
+                    default:
+                        throw new RuntimeException('Unknown attribute property');
+                }
 
-            if (isset($value)) {
-                $key          = \str_replace(' ', '_', $field);
-                $result[$key] = $value;
+                if (isset($value)) {
+                    $result[$field] = $value;
+                }
             }
+
+            $attrs[$name] = empty($result) ? true : $result;
         }
 
-        return empty($result) ? true : $result;
+        $this->setAttr('attrs', $attrs);
     }
 
-    protected function getno_attr() /* mixed */
+    protected function getno_attr() /* : mixed */
     {
-        return $this->getBBAttr($this->attrs['no attr'] ?? null, ['body format', 'text only']);
+        return $this->getBBAttr('no attr', ['body format', 'text only']);
     }
 
     protected function setno_attr(array $value): void
     {
-        $value = $this->getBBAttr($value, ['body format', 'text only']);
+        $this->setBBAttr('no attr', $value, ['body format', 'text only']);
+    }
+
+    protected function getdef_attr() /* : mixed */
+    {
+        return $this->getBBAttr('Def', ['required', 'format', 'body format', 'text only']);
+    }
+
+    protected function setdef_attr(array $value): void
+    {
+        $this->setBBAttr('Def', $value, ['required', 'format', 'body format', 'text only']);
+    }
+
+    protected function getother_attrs(): array
+    {
         $attrs = $this->getAttr('attrs');
 
-        if (null === $value) {
-            unset($attrs['no attr']);
-        } else {
-            $attrs['no attr'] = $value;
+        if (! \is_array($attrs)) {
+            return [];
         }
 
-        $this->setAttr('attrs', $attrs);
+        unset($attrs['no attr'], $attrs['Def'], $attrs['New']);
+
+        $result = [];
+        foreach ($attrs as $name => $attr) {
+            $value = $this->getBBAttr($name, ['required', 'format', 'body format', 'text only']);
+
+            if (null === $value) {
+                continue;
+            }
+
+            $result[$name] = $value;
+        }
+
+        return $result;
     }
 
-    protected function getdef_attr() /* mixed */
+    protected function setother_attrs(array $attrs): void
     {
-        return $this->getBBAttr($this->attrs['Def'] ?? null, ['required', 'format', 'body format', 'text only']);
+        unset($attrs['no attr'], $attrs['Def']);
+
+        foreach ($attrs as $name => $attr) {
+            $this->setBBAttr($name, $attr, ['required', 'format', 'body format', 'text only']);
+        }
     }
 
-    protected function setdef_attr(array $value): void
+    /**
+     * Ищет ошибку в структуре bb-кода
+     */
+    public function getError(): ?array
     {
-        $value = $this->getBBAttr($value, ['required', 'format', 'body format', 'text only']);
-        $attrs = $this->getAttr('attrs');
+        if (
+            ! \is_string($this->tag)
+            || ! \preg_match(self::TAG_PATTERN, $this->tag)
+        ) {
+            return ['Tag name not specified'];
+        }
 
-        if (null === $value) {
-            unset($attrs['Def']);
-        } else {
-            $attrs['Def'] = $value;
+        $result = $this->testPHP($this->handler);
+        if (null !== $result) {
+            return ['PHP code error in Handler: %s', $result];
         }
 
-        $this->setAttr('attrs', $attrs);
+        $result = $this->testPHP($this->text_handler);
+        if (null !== $result ) {
+            return ['PHP code error in Text handler: %s', $result];
+        }
+
+        if (
+            null !== $this->recursive
+            && null !== $this->tags_only
+        ) {
+            return ['Recursive and Tags only are enabled at the same time'];
+        }
+
+        if (
+            null !== $this->recursive
+            && null !== $this->single
+        ) {
+            return ['Recursive and Single are enabled at the same time'];
+        }
+
+        if (
+            null !== $this->text_only
+            && null !== $this->tags_only
+        ) {
+            return ['Text only and Tags only are enabled at the same time'];
+        }
+
+        if (\is_array($this->attrs)) {
+            foreach ($this->attrs as $name => $attr) {
+                if (
+                    'no attr' !== $name
+                    && 'Def' !== $name
+                    && ! preg_match(self::ATTR_PATTERN, $name)
+                ) {
+                    return ['Attribute name %s is not valid', $name];
+                }
+
+                if (isset($attr['format'])) {
+                    if (
+                        ! \is_string($attr['format'])
+                        || false === @\preg_match($attr['format'], 'abcdef')
+                    ) {
+                        return ['Attribute %1$s, %2$s - regular expression error', $name, 'Format'];
+                    }
+                }
+
+                if (isset($attr['body format'])) {
+                    if (
+                        ! \is_string($attr['body format'])
+                        || false === @\preg_match($attr['body format'], 'abcdef')
+                    ) {
+                        return ['Attribute %1$s, %2$s - regular expression error', $name, 'Body format'];
+                    }
+                }
+            }
+        }
+
+        if (
+            \is_array($this->new_attr)
+            && ! empty($this->new_attr['allowed'])
+            && ! empty($this->new_attr['name'])
+        ) {
+            $name = $this->new_attr['name'];
+
+            if (
+                'no attr' === $name
+                || 'Def' === $name
+                || isset($this->attrs[$name])
+                || ! preg_match(self::ATTR_PATTERN, $name)
+            ) {
+                return ['Attribute name %s is not valid', $name];
+            }
+
+            if (isset($this->new_attr['format'])) {
+                if (
+                    ! \is_string($this->new_attr['format'])
+                    || false === @\preg_match($this->new_attr['format'], 'abcdef')
+                ) {
+                    return ['Attribute %1$s, %2$s - regular expression error', $name, 'Format'];
+                }
+            }
+
+            if (isset($this->new_attr['body format'])) {
+                if (
+                    ! \is_string($this->new_attr['body format'])
+                    || false === @\preg_match($this->new_attr['body format'], 'abcdef')
+                ) {
+                    return ['Attribute %1$s, %2$s - regular expression error', $name, 'Body format'];
+                }
+            }
+        }
+
+        return null;
+    }
+
+    protected function testPHP(?string $code): ?string
+    {
+        if (
+            null === $code
+            || '' === $code
+        ) {
+            return null;
+        }
+
+        // тест на парность скобок
+        $testCode = \preg_replace('%//[^\r\n]*+|#[^\r\n]*+|/\*.*?\*/|\'.*?(?<!\\\\)\'|".*?(?<!\\\\)"%s', '', $code);
+        if (false === \preg_match_all('%[(){}\[\]]%s', $testCode, $matches)) {
+            throw new RuntimeException('The preg_match_all() returned an error');
+        }
+
+        $round  = 0;
+        $square = 0;
+        $curly  = 0;
+
+        foreach ($matches[0] as $value) {
+            switch ($value) {
+                case '(':
+                    ++$round;
+                    break;
+                case ')':
+                    --$round;
+
+                    if ($round < 0) {
+                        return '\')\' > \'(\'.';
+                    }
+                    break;
+                case '[':
+                    ++$square;
+                    break;
+                case ']':
+                    --$square;
+
+                    if ($square < 0) {
+                        return '\']\' > \'[\'.';
+                    }
+                    break;
+                case '{':
+                    ++$curly;
+                    break;
+                case '}':
+                    --$curly;
+
+                    if ($curly < 0) {
+                        return '\'}\' > \'{\'.';
+                    }
+                    break;
+                default:
+                    throw new RuntimeException('Unknown bracket type');
+            }
+        }
+
+        if (0 !== $round) {
+            return '\'(\' != \')\'.';
+        }
+        if (0 !== $square) {
+            return '\'[\' != \']\'.';
+        }
+        if (0 !== $curly) {
+            return '\'{\' != \'}\'.';
+        }
+
+        // тест на выполнение DANGER! DANGER! DANGER! O_o
+        $testCode = "\$testVar = function(\$body, \$attrs, \$parser) { {$code} };\nreturn true;";
+
+        try {
+            $result = @eval($testCode);
+
+            if (true !== $result) {
+                $error = error_get_last();
+                $message = $error['message'] ?? 'Unknown error';
+                $line    = $error['line'] ?? '';
+
+                return "{$message}: [$line]";
+            }
+        } catch (Throwable $e) {
+            return "{$e->getMessage()}: [{$e->getLine()}]";
+        }
+
+        return null;
     }
 }

+ 34 - 0
app/Models/BBCodeList/Update.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace ForkBB\Models\BBCodeList;
+
+use ForkBB\Models\Method;
+use ForkBB\Models\BBCodeList\Model as BBCodeList;
+use ForkBB\Models\BBCodeList\Structure;
+use RuntimeException;
+
+class Update extends Method
+{
+    /**
+     * Обновляет структуру bb-кода
+     */
+    public function update(int $id, Structure $structure): BBCodeList
+    {
+        if (null !== $structure->getError()) {
+            throw new RuntimeException('BBCode structure has error');
+        }
+
+        $vars = [
+            ':id'        => $id,
+            ':tag'       => $structure->tag,
+            ':structure' => $structure->toString(),
+        ];
+        $query = 'UPDATE ::bbcode
+            SET bb_structure=?s:structure
+            WHERE id=?i:id AND bb_tag=?s:tag AND bb_edit=1';
+
+        $this->c->DB->exec($query, $vars);
+
+        return $this->model->reset();
+    }
+}

+ 199 - 79
app/Models/Pages/Admin/Parser/BBCode.php

@@ -246,19 +246,79 @@ class BBCode extends Parser
         }
         $this->bbTypes = $bbTypes;
 
-        if ('POST' === $method) {
-        }
-
-
         if ($id > 0) {
             $title            = __('Edit bbcode head');
-            $this->formAction = $this->c->Router->link('AdminBBCodeEdit', ['id' => $id]);
-            $this->formToken  = $this->c->Csrf->create('AdminBBCodeEdit', ['id' => $id]);
+            $page             = 'AdminBBCodeEdit';
+            $pageArgs         = ['id' => $id];
         } else {
             $title            = __('Add bbcode head');
-            $this->formAction = $this->c->Router->link('AdminBBCodeNew');
-            $this->formToken  = $this->c->Csrf->create('AdminBBCodeNew');
+            $page             = 'AdminBBCodeNew';
+            $pageArgs         = [];
+        }
+        $this->formAction = $this->c->Router->link($page, $pageArgs);
+        $this->formToken  = $this->c->Csrf->create($page, $pageArgs);
+
+        if ('POST' === $method) {
+            $v = $this->c->Validator->reset()
+                ->addValidators([
+                    'check_all'                 => [$this, 'vCheckAll'],
+                ])->addRules([
+                    'token'                     => 'token:' . $page,
+                    'tag'                       => $id > 0 ? 'absent' : 'required|string:trim|regex:%^[a-z\*][a-z\d-]{0,10}$%',
+                    'type'                      => 'required|string|in:' . \implode(',', $bbTypes),
+                    'type_new'                  => 'string:trim|regex:%^[a-z][a-z\d-]{0,19}$%',
+                    'parents.*'                 => 'required|string|in:' . \implode(',', $bbTypes),
+                    'handler'                   => 'string:trim|max:65535',
+                    'text_handler'              => 'string:trim|max:65535',
+                    'recursive'                 => 'required|integer|in:0,1',
+                    'text_only'                 => 'required|integer|in:0,1',
+                    'tags_only'                 => 'required|integer|in:0,1',
+                    'pre'                       => 'required|integer|in:0,1',
+                    'single'                    => 'required|integer|in:0,1',
+                    'auto'                      => 'required|integer|in:0,1',
+                    'self_nesting'              => 'required|integer|min:0|max:10',
+                    'no_attr.allowed'           => 'required|integer|in:0,1',
+                    'no_attr.body_format'       => 'string:trim|max:1024',
+                    'no_attr.text_only'         => 'required|integer|in:0,1',
+                    'def_attr.allowed'          => 'required|integer|in:0,1',
+                    'def_attr.required'         => 'required|integer|in:0,1',
+                    'def_attr.format'           => 'string:trim|max:1024',
+                    'def_attr.body_format'      => 'string:trim|max:1024',
+                    'def_attr.text_only'        => 'required|integer|in:0,1',
+                    'other_attrs.*.allowed'     => 'required|integer|in:0,1',
+                    'other_attrs.*.required'    => 'required|integer|in:0,1',
+                    'other_attrs.*.format'      => 'string:trim|max:1024',
+                    'other_attrs.*.body_format' => 'string:trim|max:1024',
+                    'other_attrs.*.text_only'   => 'required|integer|in:0,1',
+                    'new_attr.name'             => 'string:trim|regex:%^(?:\|[a-z-]{2,15})$%',
+                    'new_attr.allowed'          => 'required|integer|in:0,1',
+                    'new_attr.required'         => 'required|integer|in:0,1',
+                    'new_attr.format'           => 'string:trim|max:1024',
+                    'new_attr.body_format'      => 'string:trim|max:1024',
+                    'new_attr.text_only'        => 'required|integer|in:0,1',
+                    'save'                      => 'check_all',
+                ])->addAliases([
+                ])->addArguments([
+                    'token'                    => $pageArgs,
+                    'save'                     => $structure,
+                ])->addMessages([
+                ]);
+
+                if ($v->validation($_POST)) {
+                    if ($id > 0) {
+                        $this->c->bbcode->update($id, $structure);
+                        $message = 'BBCode updated redirect';
+                    } else {
+                        $id = $this->c->bbcode->insert($structure);
+                        $message = 'BBCode added redirect';
+                    }
+
+                    return $this->c->Redirect->page('AdminBBCodeEdit', ['id' => $id])->message($message);
+                }
+
+                $this->fIswev = $v->getErrors();
         }
+
         $this->aCrumbs[] = [
             $this->formAction,
             $title,
@@ -278,6 +338,38 @@ class BBCode extends Parser
         return $this;
     }
 
+    /**
+     * Проверяет данные bb-кода
+     */
+    public function vCheckAll(Validator $v, string $txt, $attrs, Structure $structure): string
+    {
+        if (! empty($v->getErrors())) {
+            return $txt;
+        }
+
+        $data = $v->getData();
+        unset($data['token'], $data['save']);
+
+        foreach ($data as $key => $value) {
+            if ('type_new' === $key) {
+                if (isset($value[0])) {
+                    $structure->type = $value;
+                }
+            } else {
+                $structure->{$key} = $value;
+            }
+        }
+
+        $error = $structure->getError();
+
+        if (\is_array($error)) {
+            $v->addError(__(...$error));
+        }
+
+        return $txt;
+    }
+
+
     /**
      * Формирует данные для формы
      */
@@ -298,9 +390,9 @@ class BBCode extends Parser
             ],
         ];
 
-        $yn     = [1 => __('Yes'), 0 => __('No')];
+        $yn = [1 => __('Yes'), 0 => __('No')];
 
-        $form['sets']["structure"] = [
+        $form['sets']['structure'] = [
             'class'  => 'structure',
 //            'legend' => ,
             'fields' => [
@@ -406,78 +498,106 @@ class BBCode extends Parser
 
         $tagStr = $id > 0 ? $structure->tag : 'TAG';
 
-        $form['sets']["no_attr"] = [
-            'class'  => ['attr', 'no_attr'],
-            'legend' => __('No attr subhead', $tagStr),
-            'fields' => [
-                'no_attr[allowed]' => [
-                    'type'    => 'radio',
-                    'value'   => null === $structure->no_attr ? 0 : 1,
-                    'values'  => $yn,
-                    'caption' => __('Allowed label'),
-                    'info'    => __('Allowed no_attr info'),
-                ],
-                'no_attr[body_format]' => [
-                    'class'     => 'format',
-                    'type'      => 'text',
-                    'value'     => $structure->no_attr['body_format'] ?? '',
-                    'caption'   => __('Body format label'),
-                    'info'      => __('Body format info'),
-                ],
-                'no_attr[text_only]' => [
-                    'type'    => 'radio',
-                    'value'   => empty($structure->no_attr['text_only']) ? 0 : 1,
-                    'values'  => $yn,
-                    'caption' => __('Text only label'),
-                    'info'    => __('Text only info'),
-                ],
-            ],
-        ];
+        $form['sets']['no_attr'] = $this->formEditSub(
+            $structure->no_attr,
+            'no_attr',
+            'no_attr',
+            __('No attr subhead', $tagStr),
+            __('Allowed no_attr info')
+        );
+
+        $form['sets']['def_attr'] = $this->formEditSub(
+            $structure->def_attr,
+            'def_attr',
+            'def_attr',
+            __('Def attr subhead', $tagStr),
+            __('Allowed def_attr info')
+        );
+
+        foreach ($structure->other_attrs as $name => $attr) {
+            $form['sets']["{$name}_attr"] = $this->formEditSub(
+                $attr,
+                $name,
+                "{$name}_attr",
+                __('Other attr subhead', $tagStr, $name),
+                __('Allowed %s attr info', $name)
+            );
+        }
 
-        $form['sets']["def_attr"] = [
-            'class'  => ['attr', 'def_attr'],
-            'legend' => __('Def attr subhead', $tagStr),
-            'fields' => [
-                'def_attr[allowed]' => [
-                    'type'    => 'radio',
-                    'value'   => null === $structure->def_attr ? 0 : 1,
-                    'values'  => $yn,
-                    'caption' => __('Allowed label'),
-                    'info'    => __('Allowed def_attr info'),
-                ],
-                'def_attr[required]' => [
-                    'type'    => 'radio',
-                    'value'   => empty($structure->def_attr['required']) ? 0 : 1,
-                    'values'  => $yn,
-                    'caption' => __('Required label'),
-                    'info'    => __('Required info'),
-                ],
-                'def_attr[format]' => [
-                    'class'     => 'format',
-                    'type'      => 'text',
-                    'value'     => $structure->def_attr['format'] ?? '',
-                    'caption'   => __('Format label'),
-                    'info'      => __('Format info'),
-                ],
-                'def_attr[body_format]' => [
-                    'class'     => 'format',
-                    'type'      => 'text',
-                    'value'     => $structure->def_attr['body_format'] ?? '',
-                    'caption'   => __('Body format label'),
-                    'info'      => __('Body format info'),
-                ],
-                'def_attr[text_only]' => [
-                    'type'    => 'radio',
-                    'value'   => empty($structure->def_attr['text_only']) ? 0 : 1,
-                    'values'  => $yn,
-                    'caption' => __('Text only label'),
-                    'info'    => __('Text only info'),
-                ],
-            ],
-        ];
+        $form['sets']['new_attr'] = $this->formEditSub(
+            $structure->new_attr,
+            'new_attr',
+            'new_attr',
+            __('New attr subhead'),
+            __('Allowed new_attr info')
+        );
+
+        return $form;
+    }
 
+    /**
+     * Формирует данные для формы
+     */
+    protected function formEditSub(/* mixed */ $data, string $name, string $class, string $legend, string $info): array
+    {
+        $yn     = [1 => __('Yes'), 0 => __('No')];
         $fields = [];
+        $other  = '_attr' !== \substr($name, -5);
+        $key    = $other ? "other_attrs[{$name}]" : $name;
+
+        if ('new_attr' === $name) {
+            $fields["{$key}[name]"] = [
+                'type'      => 'text',
+                'value'     => $data['name'] ?? '',
+                'caption'   => __('Attribute name label'),
+                'info'      => __('Attribute name info'),
+                'maxlength' => 15,
+                'pattern'   => '^[a-z-]{2,15}$',
+            ];
+        }
 
-        return $form;
+        $fields["{$key}[allowed]"] = [
+            'type'    => 'radio',
+            'value'   => null === $data ? 0 : 1,
+            'values'  => $yn,
+            'caption' => __('Allowed label'),
+            'info'    => $info,
+        ];
+        if ('no_attr' !== $name) {
+            $fields["{$key}[required]"] = [
+                'type'    => 'radio',
+                'value'   => empty($data['required']) ? 0 : 1,
+                'values'  => $yn,
+                'caption' => __('Required label'),
+                'info'    => __('Required info'),
+            ];
+            $fields["{$key}[format]"] = [
+                'class'     => 'format',
+                'type'      => 'text',
+                'value'     => $data['format'] ?? '',
+                'caption'   => __('Format label'),
+                'info'      => __('Format info'),
+            ];
+        }
+        $fields["{$key}[body_format]"] = [
+            'class'     => 'format',
+            'type'      => 'text',
+            'value'     => $data['body_format'] ?? '',
+            'caption'   => __('Body format label'),
+            'info'      => __('Body format info'),
+        ];
+        $fields["{$key}[text_only]"] = [
+            'type'    => 'radio',
+            'value'   => empty($data['text_only']) ? 0 : 1,
+            'values'  => $yn,
+            'caption' => __('Text only label'),
+            'info'    => __('Text only info'),
+        ];
+
+        return [
+            'class'  => ['attr', $class],
+            'legend' => $legend,
+            'fields' => $fields,
+        ];
     }
 }

+ 48 - 0
app/lang/en/admin_parser.po

@@ -269,3 +269,51 @@ msgstr "Format"
 
 msgid "Format info"
 msgstr "The regular expression, sets the rule for this attribute."
+
+msgid "Other attr subhead"
+msgstr "Settings for BBCode type [%1$s %2$s=<i>value</i>]..."
+
+msgid "Allowed %s attr info"
+msgstr "Allow use of this BBCode with <b><code>%s</code></b> attribute. No - this attribute will be deleted."
+
+msgid "New attr subhead"
+msgstr "New attribute"
+
+msgid "Allowed new_attr info"
+msgstr "Add new attribute"
+
+msgid "Attribute name label"
+msgstr "New attribute name"
+
+msgid "Attribute name info"
+msgstr "Format: <code>^[a-z-]{2,15}$</code>"
+
+msgid "Tag name not specified"
+msgstr "Tag name not specified."
+
+msgid "PHP code error in Handler: %s"
+msgstr "PHP code error in Handler: %s"
+
+msgid "PHP code error in Text handler: %s"
+msgstr "PHP code error in Text handler: %s"
+
+msgid "Recursive and Tags only are enabled at the same time"
+msgstr "Recursive and Tags only are enabled at the same time."
+
+msgid "Recursive and Single are enabled at the same time"
+msgstr "Recursive and Single are enabled at the same time."
+
+msgid "Text only and Tags only are enabled at the same time"
+msgstr "Text only and Tags only are enabled at the same time."
+
+msgid "Attribute %1$s, %2$s - regular expression error"
+msgstr "Attribute %1$s, field %2$s - regular expression error."
+
+msgid "Attribute name %s is not valid"
+msgstr "Attribute name %s is not valid."
+
+msgid "BBCode updated redirect"
+msgstr "BBCode updated."
+
+msgid "BBCode added redirect"
+msgstr "BBCode added."

+ 48 - 0
app/lang/ru/admin_parser.po

@@ -269,3 +269,51 @@ msgstr "Format"
 
 msgid "Format info"
 msgstr "Регулярное выражение, задает правило для атрибута."
+
+msgid "Other attr subhead"
+msgstr "Настройки для BB-кода вида [%1$s %2$s=<i>значение</i>]..."
+
+msgid "Allowed %s attr info"
+msgstr "Разрешить использовать этот BB-код с атрибутом <b><code>%s</code></b>. Нет - атрибут буден удален."
+
+msgid "New attr subhead"
+msgstr "Новый атрибут"
+
+msgid "Allowed new_attr info"
+msgstr "Добавить новый атрибут"
+
+msgid "Attribute name label"
+msgstr "Имя нового атрибута"
+
+msgid "Attribute name info"
+msgstr "Формат: <code>^[a-z-]{2,15}$</code>"
+
+msgid "Tag name not specified"
+msgstr "Имя тега не указано."
+
+msgid "PHP code error in Handler: %s"
+msgstr "Ошибка PHP кода в Handler: %s"
+
+msgid "PHP code error in Text handler: %s"
+msgstr "Ошибка PHP кода в Text handler: %s"
+
+msgid "Recursive and Tags only are enabled at the same time"
+msgstr "Одновременно включены Recursive и Tags only."
+
+msgid "Recursive and Single are enabled at the same time"
+msgstr "Одновременно включены Recursive и Single."
+
+msgid "Text only and Tags only are enabled at the same time"
+msgstr "Одновременно включены Text only и Tags only."
+
+msgid "Attribute %1$s, %2$s - regular expression error"
+msgstr "Атрибут %1$s, поле %2$s - ошибка в регулярном выражении."
+
+msgid "Attribute name %s is not valid"
+msgstr "Имя %s для атрибута недопустимо."
+
+msgid "BBCode updated redirect"
+msgstr "BB-код обновлен."
+
+msgid "BBCode added redirect"
+msgstr "BB-код добавлен."