瀏覽代碼

Merge pull request #25 from forkbb/Extensions

Extensions
Visman 1 年之前
父節點
當前提交
da79516766
共有 49 個文件被更改,包括 1665 次插入12 次删除
  1. 1 0
      .gitignore
  2. 12 0
      app/Controllers/Routing.php
  3. 1 1
      app/Core/View/Compiler.php
  4. 191 0
      app/Models/Extension/Extension.php
  5. 547 0
      app/Models/Extension/Extensions.php
  6. 1 0
      app/Models/Pages/Admin.php
  7. 84 0
      app/Models/Pages/Admin/Extensions.php
  8. 15 0
      app/Models/Pages/Admin/Install.php
  9. 59 1
      app/Models/Pages/Admin/Update.php
  10. 4 3
      app/bootstrap.php
  11. 0 0
      app/config/ext/.gitkeep
  12. 6 0
      app/config/main.dist.php
  13. 3 0
      app/lang/en/admin.po
  14. 118 0
      app/lang/en/admin_extensions.po
  15. 3 0
      app/lang/ru/admin.po
  16. 118 0
      app/lang/ru/admin_extensions.po
  17. 148 0
      app/templates/_default/admin/extensions.forkbb.php
  18. 6 0
      app/templates/_default/ban.forkbb.php
  19. 6 0
      app/templates/_default/change_passphrase.forkbb.php
  20. 10 0
      app/templates/_default/email.forkbb.php
  21. 16 0
      app/templates/_default/forum.forkbb.php
  22. 10 0
      app/templates/_default/index.forkbb.php
  23. 12 0
      app/templates/_default/layouts/admin.forkbb.php
  24. 4 0
      app/templates/_default/layouts/crumbs.forkbb.php
  25. 4 0
      app/templates/_default/layouts/debug.forkbb.php
  26. 8 0
      app/templates/_default/layouts/form.forkbb.php
  27. 4 0
      app/templates/_default/layouts/iswev.forkbb.php
  28. 27 2
      app/templates/_default/layouts/main.forkbb.php
  29. 12 0
      app/templates/_default/layouts/pm.forkbb.php
  30. 6 0
      app/templates/_default/layouts/poll.forkbb.php
  31. 8 0
      app/templates/_default/layouts/redirect.forkbb.php
  32. 4 0
      app/templates/_default/layouts/stats.forkbb.php
  33. 14 0
      app/templates/_default/login.forkbb.php
  34. 2 0
      app/templates/_default/maintenance.forkbb.php
  35. 2 0
      app/templates/_default/message.forkbb.php
  36. 10 0
      app/templates/_default/moderate.forkbb.php
  37. 6 0
      app/templates/_default/passphrase_reset.forkbb.php
  38. 14 0
      app/templates/_default/post.forkbb.php
  39. 14 0
      app/templates/_default/profile.forkbb.php
  40. 10 0
      app/templates/_default/register.forkbb.php
  41. 10 0
      app/templates/_default/report.forkbb.php
  42. 12 0
      app/templates/_default/rules.forkbb.php
  43. 10 0
      app/templates/_default/search.forkbb.php
  44. 16 0
      app/templates/_default/topic.forkbb.php
  45. 10 0
      app/templates/_default/topic_in_search.forkbb.php
  46. 13 1
      app/templates/_default/userlist.forkbb.php
  47. 0 0
      ext/.gitkeep
  48. 71 4
      public/style/ForkBB/admin.css
  49. 3 0
      public/style/ForkBB/style.css

+ 1 - 0
.gitignore

@@ -3,6 +3,7 @@
 /app/config/main.php
 /app/config/_*
 /app/config/db/*
+/app/config/ext/*
 /app/cache/**/*.php
 /app/cache/**/*.lock
 /app/cache/**/*.tmp

+ 12 - 0
app/Controllers/Routing.php

@@ -838,6 +838,18 @@ class Routing
                 'AdminAntispam:view',
                 'AdminAntispam'
             );
+            $r->add(
+                $r::GET,
+                '/admin/extensions',
+                'AdminExtensions:info',
+                'AdminExtensions'
+            );
+            $r->add(
+                $r::PST,
+                '/admin/extensions/action',
+                'AdminExtensions:action',
+                'AdminExtensionsAction'
+            );
         }
 
         $uri = $_SERVER['REQUEST_URI'];

+ 1 - 1
app/Core/View/Compiler.php

@@ -171,7 +171,7 @@ class Compiler
 
 declare(strict_types=1);
 
-use function \ForkBB\{__, num, dt, size};
+use function \ForkBB\{__, num, dt, size, url};
 
 ?>
 EOD;

+ 191 - 0
app/Models/Extension/Extension.php

@@ -0,0 +1,191 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Extension;
+
+use ForkBB\Models\Model;
+use RuntimeException;
+
+class Extension extends Model
+{
+    const NOT_INSTALLED = 0;
+    const DISABLED      = 4;
+    const DISABLED_DOWN = 5;
+    const DISABLED_UP   = 6;
+    const ENABLED       = 8;
+    const ENABLED_DOWN  = 9;
+    const ENABLED_UP    = 10;
+    const CRASH         = 12;
+
+    /**
+     * Ключ модели для контейнера
+     */
+    protected string $cKey = 'Extension';
+
+    protected array $prepareData;
+
+    protected function getdispalyName(): string
+    {
+        return $this->dbData['extra']['display-name'] ?? $this->fileData['extra']['display-name'];
+    }
+
+    protected function getversion(): string
+    {
+        return $this->dbData['version'] ?? $this->fileData['version'];
+    }
+
+    protected function getfileVersion(): string
+    {
+        return $this->fileData['version'] ?? '-';
+    }
+
+    protected function getname(): string
+    {
+        return $this->dbData['name'] ?? $this->fileData['name'];
+    }
+
+    protected function getid(): string
+    {
+        return 'ext-' . \trim(\preg_replace('%\W+%', '-', $this->name), '-');
+    }
+
+    protected function getdescription(): string
+    {
+        return $this->dbData['description'] ?? $this->fileData['description'];
+    }
+
+    protected function gettime(): ?string
+    {
+        return $this->dbData['time'] ?? $this->fileData['time'];
+    }
+
+    protected function gethomepage(): ?string
+    {
+        return $this->dbData['homepage'] ?? $this->fileData['homepage'];
+    }
+
+    protected function getlicense(): ?string
+    {
+        return $this->dbData['license'] ?? $this->fileData['license'];
+    }
+
+    protected function getrequirements(): array
+    {
+        return $this->dbData['extra']['requirements'] ?? $this->fileData['extra']['requirements'];
+    }
+
+    protected function getauthors(): array
+    {
+        return $this->dbData['authors'] ?? $this->fileData['authors'];
+    }
+
+    protected function getstatus(): int
+    {
+        if (null === $this->dbStatus) {
+            return self::NOT_INSTALLED;
+        } elseif (empty($this->fileData['version'])) {
+            return self::CRASH;
+        }
+
+        switch (
+            \version_compare($this->fileData['version'], $this->dbData['version'])
+            + 4 * (1 === $this->dbStatus)
+        ) {
+            case -1:
+                return self::DISABLED_DOWN;
+            case 0:
+                return self::DISABLED;
+            case 1:
+                return self::DISABLED_UP;
+            case 3:
+                return self::ENABLED_DOWN;
+            case 4:
+                return self::ENABLED;
+            case 5:
+                return self::ENABLED_UP;
+            default:
+                throw new RuntimeException("Error in {$this->name} extension status");
+        }
+    }
+
+    protected function getcanInstall(): bool
+    {
+        return self::NOT_INSTALLED === $this->status;
+    }
+
+    protected function getcanUninstall(): bool
+    {
+        return \in_array($this->status, [self::DISABLED, self::DISABLED_DOWN, self::DISABLED_UP], true);
+    }
+
+    protected function getcanUpdate(): bool
+    {
+        return \in_array($this->status, [self::DISABLED_UP, self::ENABLED_UP], true);
+    }
+
+    protected function getcanDowndate(): bool
+    {
+        return \in_array($this->status, [self::DISABLED_DOWN, self::ENABLED_DOWN], true);
+    }
+
+    protected function getcanEnable(): bool
+    {
+        return self::DISABLED === $this->status;
+    }
+
+    protected function getcanDisable(): bool
+    {
+        return \in_array($this->status, [self::ENABLED, self::ENABLED_DOWN, self::ENABLED_UP, self::CRASH], true);
+    }
+
+    public function prepare(): bool|string|array
+    {
+        $this->prepareData = [];
+
+        if ($this->fileData['extra']['templates']) {
+            foreach ($this->fileData['extra']['templates'] as $cur) {
+                switch($cur['type']) {
+                    case 'pre':
+                        if (empty($cur['name'])) {
+                            return 'PRE name not found';
+                        } elseif (empty($cur['file'])) {
+                            return ['Template file \'%s\' not found', $cur['file']];
+                        }
+
+                        $path = $this->fileData['path'] . '/' . \ltrim($cur['file'], '\\/');
+
+                        if (! \is_file($path)) {
+                            return ['Template file \'%s\' not found', $cur['file']];
+                        }
+
+                        $data = \file_get_contents($path);
+
+                        foreach (\explode(',', $cur['template']) as $template) {
+                            $this->prepareData['templates']['pre'][$template][$cur['name']][] = [
+                                'priority' => $cur['priority'] ?: 0,
+                                'data'     => $data,
+                            ];
+                        }
+
+                        break;
+                    default:
+                        return 'Invalid template type';
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public function prepareData(): array
+    {
+        return $this->prepareData;
+    }
+}

+ 547 - 0
app/Models/Extension/Extensions.php

@@ -0,0 +1,547 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Extension;
+
+use ForkBB\Models\Extension\Extension;
+use ForkBB\Models\Manager;
+use FilesystemIterator;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
+use RuntimeException;
+
+class Extensions extends Manager
+{
+     /**
+     * Ключ модели для контейнера
+     */
+    protected string $cKey = 'Extensions';
+
+    /**
+     * Список отсканированных папок
+     */
+    protected array $folders = [];
+
+    /**
+     * Текст ошибки
+     */
+    protected string|array $error = '';
+
+    protected string $commonFile;
+    protected string $preFile;
+
+    /**
+     * Возвращает action (или свойство) по его имени
+     */
+    public function __get(string $name): mixed
+    {
+        return 'error' === $name ? $this->error : parent::__get($name);
+    }
+
+    /**
+     * Инициализирует менеджер
+     */
+    public function init(): Extensions
+    {
+        $this->commonFile = $this->c->DIR_CONFIG . '/ext/common.php';
+        $this->preFile    = $this->c->DIR_CONFIG . '/ext/pre.php';
+
+        $this->fromDB();
+
+        $list = $this->scan($this->c->DIR_EXT);
+
+        $this->fromList($this->prepare($list));
+
+        \uasort($this->repository, function (Extension $a, Extension $b) {
+            return $a->dispalyName <=> $b->dispalyName;
+        });
+
+        return $this;
+    }
+
+    /**
+     * Загружает в репозиторий из БД список расширений
+     */
+    protected function fromDB(): void
+    {
+        $query = 'SELECT ext_name, ext_status, ext_data
+            FROM ::extensions
+            ORDER BY ext_name';
+
+        $stmt = $this->c->DB->query($query);
+
+        while ($row = $stmt->fetch()) {
+            $model = $this->c->ExtensionModel->setModelAttrs([
+                'name'     => $row['ext_name'],
+                'dbStatus' => $row['ext_status'],
+                'dbData'   => \json_decode($row['ext_data'], true, 512, \JSON_THROW_ON_ERROR),
+            ]);
+
+            $this->set($row['ext_name'], $model);
+        }
+    }
+
+    /**
+     * Заполняет массив данными из файлов composer.json
+     */
+    protected function scan(string $folder, array $result = []): array
+    {
+        $folder = \rtrim($folder, '\\/');
+
+        if (
+            empty($folder)
+            || ! \is_dir($folder)
+        ) {
+            throw new RuntimeException("Not a directory: {$folder}");
+        }
+
+        $iterator = new RecursiveIteratorIterator(
+            new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS)
+        );
+        $files    = new RegexIterator($iterator, '%[\\\\/]composer\.json$%i', RegexIterator::MATCH);
+
+        foreach ($files as $file) {
+            $data = \file_get_contents($file->getPathname());
+
+            if (\is_string($data)) {
+                $data = \json_decode($data, true);
+            }
+
+            $result[$file->getPath()] = $data;
+        }
+
+        $this->folders[] = $folder;
+
+        return $result;
+    }
+
+    /**
+     * Подготавливает данные для моделей
+     */
+    protected function prepare(array $files): array
+    {
+        $v = clone $this->c->Validator;
+        $v = $v->reset()
+            ->addValidators([
+            ])->addRules([
+                'name'                       => 'required|string|regex:%^[a-z0-9](?:[_.-]?[a-z0-9]+)*/[a-z0-9](?:[_.-]?[a-z0-9]+)*$%',
+                'type'                       => 'required|string|in:forkbb-extension',
+                'description'                => 'required|string',
+                'homepage'                   => 'string',
+                'version'                    => 'required|string',
+                'time'                       => 'string',
+                'license'                    => 'string',
+                'authors'                    => 'required|array',
+                'authors.*.name'             => 'required|string',
+                'authors.*.email'            => 'string',
+                'authors.*.homepage'         => 'string',
+                'authors.*.role'             => 'string',
+                'autoload.psr-4'             => 'array',
+                'autoload.psr-4.*'           => 'required|string',
+                'require'                    => 'array',
+                'extra'                      => 'required|array',
+                'extra.display-name'         => 'required|string',
+                'extra.requirements'         => 'array',
+                'extra.templates'            => 'array',
+                'extra.templates.*.type'     => 'required|string|in:pre',
+                'extra.templates.*.template' => 'required|string',
+                'extra.templates.*.name'     => 'string',
+                'extra.templates.*.priority' => 'integer',
+                'extra.templates.*.file'     => 'string',
+            ])->addAliases([
+            ])->addArguments([
+            ])->addMessages([
+            ]);
+
+        $result = [];
+
+        foreach ($files as $path => $file) {
+            if (! \is_array($file)) {
+                continue;
+            } elseif (! $v->validation($file)) {
+                continue;
+            }
+
+            $data             = $v->getData(true);
+            $data['path']     = $path;
+            $result[$v->name] = $data;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Дополняет репозиторий данными из файлов composer.json
+     */
+    protected function fromList(array $list): void
+    {
+        foreach ($list as $name => $data) {
+            $model = $this->get($name);
+
+            if (! $model instanceof Extension) {
+                $model = $this->c->ExtensionModel->setModelAttrs([
+                    'name'     => $name,
+                    'fileData' => $data,
+                ]);
+
+                $this->set($name, $model);
+            } else {
+                $model->setModelAttr('fileData', $data);
+            }
+        }
+    }
+
+    /**
+     * Устанавливает расширение
+     */
+    public function install(Extension $ext): bool
+    {
+        if (true !== $ext->canInstall) {
+            $this->error = 'Invalid action';
+
+            return false;
+        }
+
+        $result = $ext->prepare();
+
+        if (true !== $result) {
+            $this->error = $result;
+
+            return false;
+        }
+
+        $vars = [
+            ':name' => $ext->name,
+            ':data' => \json_encode($ext->fileData, FORK_JSON_ENCODE),
+        ];
+        $query = 'INSERT INTO ::extensions (ext_name, ext_status, ext_data)
+            VALUES(?s:name, 1, ?s:data)';
+
+        $ext->setModelAttrs([
+            'name'     => $ext->name,
+            'dbStatus' => 1,
+            'dbData'   => $ext->fileData,
+            'fileData' => $ext->fileData,
+        ]);
+
+        if (true !== $this->updateCommon($ext)) {
+            $this->error = 'An error occurred in updateCommon';
+
+            return false;
+        }
+
+        $this->updateIndividual();
+
+        $this->c->DB->exec($query, $vars);
+
+        return true;
+    }
+
+    /**
+     * Удаляет расширение
+     */
+    public function uninstall(Extension $ext): bool
+    {
+        if (true !== $ext->canUninstall) {
+            $this->error = 'Invalid action';
+
+            return false;
+        }
+
+        $oldStatus = $ext->dbStatus;
+
+        $vars = [
+            ':name' => $ext->name,
+        ];
+        $query = 'DELETE
+            FROM ::extensions
+            WHERE ext_name=?s:name';
+
+        $ext->setModelAttrs([
+            'name'     => $ext->name,
+            'dbStatus' => null,
+            'dbData'   => null,
+            'fileData' => $ext->fileData,
+        ]);
+
+        if (true !== $this->updateCommon($ext)) {
+            $this->error = 'An error occurred in updateCommon';
+
+            return false;
+        }
+
+        if ($oldStatus) {
+            $this->updateIndividual();
+        }
+
+        $this->c->DB->exec($query, $vars);
+
+        return true;
+    }
+
+    /**
+     * Обновляет расширение
+     */
+    public function update(Extension $ext): bool
+    {
+        if (true === $ext->canUpdate) {
+            return $this->updown($ext);
+        } else {
+            $this->error = 'Invalid action';
+
+            return false;
+        }
+    }
+
+    /**
+     * Обновляет расширение
+     */
+    public function downdate(Extension $ext): bool
+    {
+        if (true === $ext->canDowndate) {
+            return $this->updown($ext);
+        } else {
+            $this->error = 'Invalid action';
+
+            return false;
+        }
+    }
+
+    protected function updown(Extension $ext): bool
+    {
+        $oldStatus = $ext->dbStatus;
+        $result    = $ext->prepare();
+
+        if (true !== $result) {
+            $this->error = $result;
+
+            return false;
+        }
+
+        $vars = [
+            ':name' => $ext->name,
+            ':data' => \json_encode($ext->fileData, FORK_JSON_ENCODE),
+        ];
+        $query = 'UPDATE ::extensions SET ext_data=?s:data
+            WHERE ext_name=?s:name';
+
+        $ext->setModelAttrs([
+            'name'     => $ext->name,
+            'dbStatus' => $ext->dbStatus,
+            'dbData'   => $ext->fileData,
+            'fileData' => $ext->fileData,
+        ]);
+
+        if (true !== $this->updateCommon($ext)) {
+            $this->error = 'An error occurred in updateCommon';
+
+            return false;
+        }
+
+        if ($oldStatus) {
+            $this->updateIndividual();
+        }
+
+        $this->c->DB->exec($query, $vars);
+
+        return true;
+    }
+
+    /**
+     * Включает расширение
+     */
+    public function enable(Extension $ext): bool
+    {
+        if (true !== $ext->canEnable) {
+            $this->error = 'Invalid action';
+
+            return false;
+        }
+
+        $vars = [
+            ':name' => $ext->name,
+        ];
+        $query = 'UPDATE ::extensions SET ext_status=1
+            WHERE ext_name=?s:name';
+
+        $ext->setModelAttrs([
+            'name'     => $ext->name,
+            'dbStatus' => 1,
+            'dbData'   => $ext->dbData,
+            'fileData' => $ext->fileData,
+        ]);
+
+        $this->updateIndividual();
+
+        $this->c->DB->exec($query, $vars);
+
+        return true;
+    }
+
+    /**
+     * Выключает расширение
+     */
+    public function disable(Extension $ext): bool
+    {
+        if (true !== $ext->canDisable) {
+            $this->error = 'Invalid action';
+
+            return false;
+        }
+
+        $vars = [
+            ':name' => $ext->name,
+        ];
+        $query = 'UPDATE ::extensions SET ext_status=0
+            WHERE ext_name=?s:name';
+
+        $ext->setModelAttrs([
+            'name'     => $ext->name,
+            'dbStatus' => 0,
+            'dbData'   => $ext->dbData,
+            'fileData' => $ext->fileData,
+        ]);
+
+        $this->updateIndividual();
+
+        $this->c->DB->exec($query, $vars);
+
+        return true;
+    }
+
+    /**
+     * Возвращает данные из файла с общими данными по расширениям
+     */
+    protected function loadDataFromFile(string $file): array
+    {
+        if (\is_file($file)) {
+            return include $file;
+        } else {
+            return [];
+        }
+    }
+
+    /**
+     * Обновляет файл с общими данными по расширениям
+     */
+    protected function updateCommon(Extension $ext): bool
+    {
+        $data = $this->loadDataFromFile($this->commonFile);
+
+        if ($ext::NOT_INSTALLED === $ext->status) {
+            unset($data[$ext->name]);
+        } else {
+            $data[$ext->name] = $ext->prepareData();
+        }
+
+        return $this->putData($this->commonFile, $data);
+    }
+
+    /**
+     * Записывает данные в указанный файл
+     */
+    protected function putData(string $file, mixed $data): bool
+    {
+        $content = "<?php\n\nreturn " . \var_export($data, true) . ";\n";
+
+        if (false === \file_put_contents($file, $content, \LOCK_EX)) {
+            return false;
+        } else {
+            if (\function_exists('\\opcache_invalidate')) {
+                \opcache_invalidate($file, true);
+            } elseif (\function_exists('\\apc_delete_file')) {
+                \apc_delete_file($file);
+            }
+
+            return true;
+        }
+    }
+
+    /**
+     * Обновляет индивидуальные файлы с данными по расширениям
+     */
+    protected function updateIndividual(): bool
+    {
+        $oldPre     = $this->loadDataFromFile($this->preFile);
+        $templates  = [];
+        $commonData = $this->loadDataFromFile($this->commonFile);
+        $pre        = [];
+        $newPre     = [];
+
+        // выделение данных
+        foreach ($this->repository as $ext) {
+            if (1 !== $ext->dbStatus) {
+                continue;
+            }
+
+            if (isset($commonData[$ext->name]['templates']['pre'])) {
+                $pre = \array_merge_recursive($pre, $commonData[$ext->name]['templates']['pre']);
+            }
+        }
+
+        // PRE-данные шаблонов
+        foreach ($pre as $template => $names) {
+            $templates[$template] = $template;
+
+            foreach ($names as $name => $list) {
+                \uasort($list, function (array $a, array $b) {
+                    return $b['priority'] <=> $a['priority'];
+                });
+
+                $result = '';
+
+                foreach ($list as $value) {
+                    $result .= $value['data'];
+                }
+
+                $newPre[$template][$name] = $result;
+            }
+        }
+
+        $this->putData($this->preFile, $newPre);
+
+        // удаление скомпилированных шаблонов
+        foreach (\array_merge($this->diffPre($oldPre, $newPre), $this->diffPre($newPre, $oldPre)) as $template) {
+            $this->c->View->delete($template);
+        }
+
+        return true;
+    }
+
+    /**
+     * Вычисляет расхождение для PRE-данных
+     */
+    protected function diffPre(array $a, array $b): array
+    {
+        $result = [];
+
+        foreach ($a as $template => $names) {
+            if (! isset($b[$template])) {
+                $result[$template] = $template;
+
+                continue;
+            }
+
+            foreach ($names as $name => $value) {
+                if (
+                    ! isset($b[$template][$name])
+                    || $value !== $b[$template][$name]
+                ) {
+                    $result[$template] = $template;
+
+                    continue 2;
+                }
+            }
+        }
+
+        return $result;
+    }
+}

+ 1 - 0
app/Models/Pages/Admin.php

@@ -82,6 +82,7 @@ abstract class Admin extends Page
                 'uploads'     => [$r->link('AdminUploads'), 'Uploads'],
                 'antispam'    => [$r->link('AdminAntispam'), 'Antispam'],
                 'logs'        => [$r->link('AdminLogs'), 'Logs'],
+                'extensions'  => [$r->link('AdminExtensions'), 'Extensions'],
                 'maintenance' => [$r->link('AdminMaintenance'), 'Maintenance'],
             ];
         }

+ 84 - 0
app/Models/Pages/Admin/Extensions.php

@@ -0,0 +1,84 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Models\Pages\Admin;
+
+use ForkBB\Models\Extension\Extension;
+use ForkBB\Models\Page;
+use ForkBB\Models\Pages\Admin;
+use Throwable;
+use function \ForkBB\__;
+
+class Extensions extends Admin
+{
+    /**
+     * Подготавливает данные для шаблона
+     */
+    public function info(array $args, string $method): Page
+    {
+        $this->c->Lang->load('admin_extensions');
+
+        $this->nameTpl    = 'admin/extensions';
+        $this->aIndex     = 'extensions';
+        $this->extensions = $this->c->extensions->repository;
+        $this->actionLink = $this->c->Router->link('AdminExtensionsAction');
+        $this->formsToken = $this->c->Csrf->create('AdminExtensionsAction');
+
+        return $this;
+    }
+
+    public function action(array $args, string $method): Page
+    {
+        $this->c->Lang->load('admin_extensions');
+
+        $v = $this->c->Validator->reset()
+            ->addRules([
+                'token'     => 'token:AdminExtensionsAction',
+                'name'      => 'required|string',
+                'confirm'   => 'required|string|in:1',
+                'install'   => 'string',
+                'uninstall' => 'string',
+                'update'    => 'string',
+                'downdate'  => 'string',
+                'enable'    => 'string',
+                'disable'   => 'string',
+            ])->addAliases([
+            ])->addMessages([
+                'confirm' => [FORK_MESS_WARN, 'No confirm redirect'],
+            ])->addArguments([
+            ]);
+
+        if (! $v->validation($_POST)) {
+            $message         = $this->c->Message;
+            $message->fIswev = $v->getErrors();
+
+            return $message->message('');
+        }
+
+        $ext = $this->c->extensions->get($v->name);
+
+        if (! $ext instanceof Extension) {
+            return $this->c->Message->message('Extension not found');
+        }
+
+        $actions = $v->getData(false, ['token', 'name', 'confirm']);
+        $action  = \array_key_first($actions);
+
+        if (empty($action)) {
+            return $this->c->Message->message('Invalid action');
+        }
+
+        if (true !== $this->c->extensions->{$action}($ext)) {
+            return $this->c->Message->message($this->c->extensions->error);
+        }
+
+        return $this->c->Redirect->page('AdminExtensions', ['#' => $ext->id])->message("Redirect {$action}", FORK_MESS_SUCC);
+    }
+}

+ 15 - 0
app/Models/Pages/Admin/Install.php

@@ -114,9 +114,12 @@ class Install extends Admin
         $folders = [
             $this->c->DIR_CONFIG,
             $this->c->DIR_CONFIG . '/db',
+            $this->c->DIR_CONFIG . '/ext',
             $this->c->DIR_CACHE,
+            $this->c->DIR_CACHE . '/polls',
             $this->c->DIR_PUBLIC . '/img/avatars',
             $this->c->DIR_PUBLIC . '/upload',
+            $this->c->DIR_LOG,
         ];
 
         foreach ($folders as $folder) {
@@ -805,6 +808,18 @@ class Install extends Admin
         ];
         $this->c->DB->createTable('::config', $schema);
 
+        // extensions
+        $schema = [
+            'FIELDS' => [
+                'ext_name'   => ['VARCHAR(190)', false, ''],
+                'ext_status' => ['TINYINT', false, 0],
+                'ext_data'   => ['TEXT', false],
+            ],
+            'PRIMARY KEY' => ['ext_name'],
+            'ENGINE' => $this->DBEngine,
+        ];
+        $this->c->DB->createTable('::extensions', $schema);
+
         // forum_perms
         $schema = [
             'FIELDS' => [

+ 59 - 1
app/Models/Pages/Admin/Update.php

@@ -25,7 +25,7 @@ class Update extends Admin
 {
     const PHP_MIN                    = '8.0.0';
     const REV_MIN_FOR_UPDATE         = 53;
-    const LATEST_REV_WITH_DB_CHANGES = 68;
+    const LATEST_REV_WITH_DB_CHANGES = 70;
     const LOCK_NAME                  = 'lock_update';
     const LOCK_TTL                   = 1800;
     const CONFIG_FILE                = 'main.php';
@@ -921,4 +921,62 @@ class Update extends Admin
 
         return null;
     }
+
+    /**
+     * rev.69 to rev.70
+     */
+    protected function stageNumber69(array $args): ?int
+    {
+        $coreConfig = new CoreConfig($this->configFile);
+
+        $coreConfig->add(
+            'shared=>%DIR_EXT%',
+            '\'%DIR_ROOT%/ext\'',
+            '%DIR_VIEWS%'
+        );
+
+        $coreConfig->add(
+            'multiple=>ExtensionModel',
+            '\\ForkBB\\Models\\Extension\\Extension::class',
+            'DBMapModel'
+        );
+
+        $coreConfig->add(
+            'multiple=>ExtensionManager',
+            '\\ForkBB\\Models\\Extension\\Extensions::class',
+            'ExtensionModel'
+        );
+
+        $coreConfig->add(
+            'shared=>extensions',
+            '\'@ExtensionManager:init\'',
+            'attachments'
+        );
+
+        $coreConfig->add(
+            'multiple=>AdminExtensions',
+            '\\ForkBB\\Models\\Pages\\Admin\\Extensions::class',
+            'AdminAntispam'
+        );
+
+        $coreConfig->add(
+            'shared=>View=>config=>preFile',
+            '\'%DIR_CONFIG%/ext/pre.php\''
+        );
+
+        $coreConfig->save();
+
+        // extensions
+        $schema = [
+            'FIELDS' => [
+                'ext_name'   => ['VARCHAR(190)', false, ''],
+                'ext_status' => ['TINYINT', false, 0],
+                'ext_data'   => ['TEXT', false],
+            ],
+            'PRIMARY KEY' => ['ext_name'],
+        ];
+        $this->c->DB->createTable('::extensions', $schema);
+
+        return null;
+    }
 }

+ 4 - 3
app/bootstrap.php

@@ -42,8 +42,7 @@ define('FORK_GEN_FEM', 2);
 
 define('FORK_JSON_ENCODE', \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR);
 
-require __DIR__ . '/../vendor/autoload.php';
-
+$loader       = require __DIR__ . '/../vendor/autoload.php';
 $errorHandler = new ErrorHandler();
 
 if (\is_file(__DIR__ . '/config/main.php')) {
@@ -54,6 +53,8 @@ if (\is_file(__DIR__ . '/config/main.php')) {
     throw new RuntimeException('Application is not configured');
 }
 
+$c->autoloader = $loader;
+
 $errorHandler->setContainer($c);
 
 require __DIR__ . '/functions.php';
@@ -69,7 +70,7 @@ if (
     $c->BASE_URL = \str_replace('https://', 'http://', $c->BASE_URL);
 }
 
-$c->FORK_REVISION = 69;
+$c->FORK_REVISION = 70;
 $c->START         = $forkStart;
 $c->PUBLIC_URL    = $c->BASE_URL . $forkPublicPrefix;
 

+ 0 - 0
app/config/ext/.gitkeep


+ 6 - 0
app/config/main.dist.php

@@ -84,6 +84,7 @@ return [
         '%DIR_LANG%'   => '%DIR_APP%/lang',
         '%DIR_LOG%'    => '%DIR_APP%/log',
         '%DIR_VIEWS%'  => '%DIR_APP%/templates',
+        '%DIR_EXT%'    => '%DIR_ROOT%/ext',
 
         'DB' => [
             'class' => \ForkBB\Core\DB::class,
@@ -109,6 +110,7 @@ return [
                 'cache'      => '%DIR_CACHE%',
                 'defaultDir' => '%DIR_VIEWS%/_default',
                 'userDir'    => '%DIR_VIEWS%/_user',
+                'preFile'    => '%DIR_CONFIG%/ext/pre.php',
             ],
         ],
         'Router' => [
@@ -185,6 +187,7 @@ return [
         ],
         'providerUser'  => \ForkBB\Models\ProviderUser\ProviderUser::class,
         'attachments'   => \ForkBB\Models\Attachment\Attachments::class,
+        'extensions'    => '@ExtensionManager:init',
 
         'Csrf' => [
             'class'   => \ForkBB\Core\Csrf::class,
@@ -409,6 +412,7 @@ return [
         'AdminLogs'          => \ForkBB\Models\Pages\Admin\Logs::class,
         'AdminUploads'       => \ForkBB\Models\Pages\Admin\Uploads::class,
         'AdminAntispam'      => \ForkBB\Models\Pages\Admin\Antispam::class,
+        'AdminExtensions'    => \ForkBB\Models\Pages\Admin\Extensions::class,
 
         'AdminListModel'    => \ForkBB\Models\AdminList\AdminList::class,
         'BanListModel'      => \ForkBB\Models\BanList\BanList::class,
@@ -417,6 +421,8 @@ return [
         'CensorshipModel'   => \ForkBB\Models\Censorship\Censorship::class,
         'ConfigModel'       => \ForkBB\Models\Config\Config::class,
         'DBMapModel'        => \ForkBB\Models\DBMap\DBMap::class,
+        'ExtensionModel'    => \ForkBB\Models\Extension\Extension::class,
+        'ExtensionManager'  => \ForkBB\Models\Extension\Extensions::class,
         'ForumModel'        => \ForkBB\Models\Forum\Forum::class,
         'ForumManager'      => \ForkBB\Models\Forum\Forums::class,
         'GroupModel'        => \ForkBB\Models\Group\Group::class,

+ 3 - 0
app/lang/en/admin.po

@@ -86,3 +86,6 @@ msgstr "Antispam"
 
 msgid "Maintenance only"
 msgstr "Available only in maintenance mode."
+
+msgid "Extensions"
+msgstr "Расширения"

+ 118 - 0
app/lang/en/admin_extensions.po

@@ -0,0 +1,118 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+
+msgid "Details"
+msgstr "Details:"
+
+msgid "Name"
+msgstr "Name of package"
+
+msgid "Description"
+msgstr "Description"
+
+msgid "Release date"
+msgstr "Release date"
+
+msgid "Homepage"
+msgstr "Homepage"
+
+msgid "Licence"
+msgstr "Licence"
+
+msgid "Requirements"
+msgstr "Requirements:"
+
+msgid "Authors"
+msgstr "Author(s):"
+
+msgid "php"
+msgstr "PHP version"
+
+msgid "forkbb"
+msgstr "ForkBB revision"
+
+msgid "Not installed"
+msgstr "Not installed"
+
+msgid "Disabled"
+msgstr "Disabled"
+
+msgid "Disabled, package changed"
+msgstr "Disabled, package changed"
+
+msgid "Enabled"
+msgstr "Enabled"
+
+msgid "Enabled, package changed"
+msgstr "Enabled, but package changed!"
+
+msgid "Crash"
+msgstr "Crash, package not found!"
+
+msgid "Install_"
+msgstr "Install"
+
+msgid "Uninstall_"
+msgstr "Uninstall"
+
+msgid "Enable_"
+msgstr "Enable"
+
+msgid "Disable_"
+msgstr "Disable"
+
+msgid "Update_"
+msgstr "Update"
+
+msgid "Downdate_"
+msgstr "Downdate"
+
+msgid "Package version"
+msgstr "Package version"
+
+msgid "Extension not found"
+msgstr "Extension not found."
+
+msgid "Invalid action"
+msgstr "Invalid action."
+
+msgid "Redirect install"
+msgstr "The extension is installed."
+
+msgid "Redirect uninstall"
+msgstr "The extension has been uninstalled."
+
+msgid "Redirect update"
+msgstr "The extension has been updated."
+
+msgid "Redirect downdate"
+msgstr "The extension version has been downgraded."
+
+msgid "Redirect enable"
+msgstr "The extension is enabled."
+
+msgid "Redirect disable"
+msgstr "The extension is disabled."
+
+msgid "Invalid template type"
+msgstr "Invalid template type."
+
+msgid "PRE name not found"
+msgstr "PRE name not found."
+
+msgid "Template file '%s' not found"
+msgstr "Template file '%s' not found."
+
+msgid "An error occurred in updateCommon"
+msgstr "An error occurred in updateCommon."

+ 3 - 0
app/lang/ru/admin.po

@@ -86,3 +86,6 @@ msgstr "Антиспам"
 
 msgid "Maintenance only"
 msgstr "Доступно только в режиме обслуживания."
+
+msgid "Extensions"
+msgstr "Расширения"

+ 118 - 0
app/lang/ru/admin_extensions.po

@@ -0,0 +1,118 @@
+#
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Project-Id-Version: ForkBB\n"
+"POT-Creation-Date: \n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: ForkBB <mio.visman@yandex.ru>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+
+msgid "Details"
+msgstr "Подробности:"
+
+msgid "Name"
+msgstr "Имя пакета"
+
+msgid "Description"
+msgstr "Описание"
+
+msgid "Release date"
+msgstr "Дата выпуска"
+
+msgid "Homepage"
+msgstr "Домашняя страница"
+
+msgid "Licence"
+msgstr "Лицензия"
+
+msgid "Requirements"
+msgstr "Требования:"
+
+msgid "Authors"
+msgstr "Автор(ы):"
+
+msgid "php"
+msgstr "Версия PHP"
+
+msgid "forkbb"
+msgstr "Ревизия ForkBB"
+
+msgid "Not installed"
+msgstr "Не установлено"
+
+msgid "Disabled"
+msgstr "Выключено"
+
+msgid "Disabled, package changed"
+msgstr "Выключено, пакет изменен"
+
+msgid "Enabled"
+msgstr "Включено"
+
+msgid "Enabled, package changed"
+msgstr "Включено, но пакет изменен!"
+
+msgid "Crash"
+msgstr "Сломано, пакет не найден!"
+
+msgid "Install_"
+msgstr "Установить"
+
+msgid "Uninstall_"
+msgstr "Удалить"
+
+msgid "Enable_"
+msgstr "Включить"
+
+msgid "Disable_"
+msgstr "Выключить"
+
+msgid "Update_"
+msgstr "Обновить"
+
+msgid "Downdate_"
+msgstr "Откатить"
+
+msgid "Package version"
+msgstr "Версия пакета"
+
+msgid "Extension not found"
+msgstr "Расширение не найдено."
+
+msgid "Invalid action"
+msgstr "Недопустимое действие."
+
+msgid "Redirect install"
+msgstr "Расширение установлено."
+
+msgid "Redirect uninstall"
+msgstr "Расширение деинсталлировано."
+
+msgid "Redirect update"
+msgstr "Расширение обновлено."
+
+msgid "Redirect downdate"
+msgstr "Версия расширения понижена."
+
+msgid "Redirect enable"
+msgstr "Расширение включено."
+
+msgid "Redirect disable"
+msgstr "Расширение выключено."
+
+msgid "Invalid template type"
+msgstr "Неверный тип шаблона."
+
+msgid "PRE name not found"
+msgstr "PRE-имя не найдено."
+
+msgid "Template file '%s' not found"
+msgstr "Файл шаблона '%s' не найден."
+
+msgid "An error occurred in updateCommon"
+msgstr "Возникла ошибка в updateCommon."

+ 148 - 0
app/templates/_default/admin/extensions.forkbb.php

@@ -0,0 +1,148 @@
+@extends ('layouts/admin')
+@isset ($p->extensions)
+      <section id="fork-extsinfo" class="f-admin">
+        <h2>{!! __('Extensions') !!}</h2>
+        <div>
+          <fieldset>
+            <ol>
+    @foreach ($p->extensions as $ext)
+              <li id="{{ $ext->id }}" class="f-extli f-ext-status{{ $ext->status }}">
+                <details class="f-extdtl">
+                  <summary class="f-extsu">
+                    <span>{{ $ext->dispalyName }}</span>
+                    -
+                    <span>{{ $ext->version }}</span>
+                    <span>/
+        @switch ($ext->status)
+            @case ($ext::NOT_INSTALLED)
+                    {!! __('Not installed') !!}
+                @break
+            @case ($ext::DISABLED)
+                    {!! __('Disabled') !!}
+                @break
+            @case ($ext::DISABLED_DOWN)
+            @case ($ext::DISABLED_UP)
+                    {!! __('Disabled, package changed') !!}
+                @break
+            @case ($ext::ENABLED)
+                    {!! __('Enabled') !!}
+                @break
+            @case ($ext::ENABLED_DOWN)
+            @case ($ext::ENABLED_UP)
+                    {!! __('Enabled, package changed') !!}
+                @break
+            @case ($ext::CRASH)
+                    {!! __('Crash') !!}
+                @break
+        @endswitch
+                    /<span>
+                  </summary>
+                  <div class="f-extdata f-fdiv">
+                    <form class="f-form" method="post" action="{{ $p->actionLink }}">
+                      <fieldset class="f-extfs-details">
+                        <legend class="f-fleg">{!! __('Details') !!}</legend>
+                        <dl>
+                          <dt>{!! __('Name') !!}</dt>
+                          <dd>{{ $ext->name }}</dd>
+                        </dl>
+                        <dl>
+                          <dt>{!! __('Package version') !!}</dt>
+                          <dd>{{ $ext->fileVersion }}</dd>
+                        </dl>
+                        <dl>
+                          <dt>{!! __('Description') !!}</dt>
+                          <dd>{{ $ext->description }}</dd>
+                        </dl>
+        @if ($ext->time)
+                        <dl>
+                          <dt>{!! __('Release date') !!}</dt>
+                          <dd>{{ $ext->time }}</dd>
+                        </dl>
+        @endif
+        @if ($ext->homepage)
+                        <dl>
+                          <dt>{!! __('Homepage') !!}</dt>
+                          <dd><a href="{{ url($ext->homepage) }}">{{ $ext->homepage }}</a></dd>
+                        </dl>
+        @endif
+        @if ($ext->license)
+                        <dl>
+                          <dt>{!! __('Licence') !!}</dt>
+                          <dd>{{ $ext->license }}</dd>
+                        </dl>
+        @endif
+                      </fieldset>
+                      <fieldset class="f-extfs-requirements">
+                        <legend class="f-fleg">{!! __('Requirements') !!}</legend>
+        @foreach ($ext->requirements as $k => $v)
+                        <dl>
+                          <dt>{!! __($k) !!}</dt>
+                          <dd>{{ $v }}</dd>
+                        </dl>
+        @endforeach
+                      </fieldset>
+                      <fieldset class="f-extfs-authors">
+                        <legend class="f-fleg">{!! __('Authors') !!}</legend>
+        @foreach ($ext->authors as $author)
+                        <dl>
+                          <dd class="f-extdd-author">
+                            <span>{{ $author['name'] }}</span>
+            @if (! empty($author['email']) || ! empty($author['homepage']))
+                            (
+                @if ($author['email'])
+                            <a href="{{ url('mailto:'.$author['email']) }}">{{ $author['email'] }}</a>
+                @endif
+                @if ($author['homepage'])
+                  @if ($author['email'])
+                            |
+                  @endif
+                            <a href="{{ url($author['homepage']) }}">{{ $author['homepage'] }}</a>
+                @endif
+                            )
+            @endif
+            @if ($author['role'])
+                            [ {{ $author['role'] }} ]
+            @endif
+                          </dd>
+                        </dl>
+        @endforeach
+                      </fieldset>
+                      <fieldset calss="f-extfs-confirm">
+                        <dl>
+                          <dd>
+                            <label class="f-flblch"><input name="confirm" class="f-ychk" type="checkbox" value="1">{!! __('Confirm action') !!}</label>
+                          </dd>
+                        </dl>
+                      </fieldset>
+                      <input type="hidden" name="name" value="{{ $ext->name }}">
+                      <input type="hidden" name="token" value="{{ $p->formsToken }}">
+                      <p class="f-btns">
+        @if ($ext->canInstall)
+                        <button class="f-btn f-fbtn" name="install" value="install" title="{{ __('Install_') }}"><span>{!! __('Install_') !!}</span></button>
+        @endif
+        @if ($ext->canUninstall)
+                        <button class="f-btn f-fbtn" name="uninstall" value="uninstall" title="{{ __('Uninstall_') }}"><span>{!! __('Uninstall_') !!}</span></button>
+        @endif
+        @if ($ext->canUpdate)
+                        <button class="f-btn f-fbtn" name="update" value="update" title="{{ __('Update_') }}"><span>{!! __('Update_') !!}</span></button>
+        @endif
+        @if ($ext->canDowndate)
+                        <button class="f-btn f-fbtn" name="downdate" value="downdate" title="{{ __('Downdate_') }}"><span>{!! __('Downdate_') !!}</span></button>
+        @endif
+        @if ($ext->canEnable)
+                        <button class="f-btn f-fbtn" name="enable" value="enable" title="{{ __('Enable_') }}"><span>{!! __('Enable_') !!}</span></button>
+        @endif
+        @if ($ext->canDisable)
+                        <button class="f-btn f-fbtn" name="disable" value="disable" title="{{ __('Disable_') }}"><span>{!! __('Disable_') !!}</span></button>
+        @endif
+                      </p>
+                    </form>
+                  </div>
+                </details>
+              </li>
+    @endforeach
+            </ol>
+          </fieldset>
+        </div>
+      </section>
+@endisset

+ 6 - 0
app/templates/_default/ban.forkbb.php

@@ -1,7 +1,11 @@
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __('Info') !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE mainBefore -->
     <div id="fork-ban" class="f-main">
 @if ($p->bannedIp)
       <p>{!! __('Your IP is blocked') !!}</p>
@@ -17,3 +21,5 @@
 @endif
       <p>{!! __(['Ban message contact %s', $p->adminEmail]) !!}</p>
     </div>
+    <!-- PRE mainAfter -->
+    <!-- PRE end -->

+ 6 - 0
app/templates/_default/change_passphrase.forkbb.php

@@ -1,9 +1,15 @@
 @extends ('layouts/main')
+    <!-- PRE start -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <section id="fork-changepass" class="f-main">
+      <!-- PRE mainStart -->
       <div class="f-fdiv f-lrdiv">
         <h2>{!! __('Change pass') !!}</h2>
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </section>
+    <!-- PRE mainAfter -->
 @endif
+    <!-- PRE end -->

+ 10 - 0
app/templates/_default/email.forkbb.php

@@ -1,16 +1,26 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __($p->legend) !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <section id="fork-sendemail" class="f-post-form">
+      <!-- PRE mainStart -->
       <h2>{!! __('Send email title') !!}</h2>
       <div class="f-fdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </section>
+    <!-- PRE mainAfter -->
 @endif
+    <!-- PRE end -->

+ 16 - 0
app/templates/_default/forum.forkbb.php

@@ -21,6 +21,8 @@
     @endif
 @endsection
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
     @if (\is_array($p->model->name))
       <h1 id="fork-h1">{!! __($p->model->name) !!}</h1>
@@ -31,10 +33,14 @@
       <p class="f-fdesc">{!! $p->model->forum_desc !!}</p>
     @endif
     </div>
+    <!-- PRE h1After -->
 @if ($forums = $p->model->subforums)
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
     @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
+    <!-- PRE subforumsBefore -->
     <section id="fork-subforums">
       <ol class="f-ftlist">
         <li id="id-subforums{{ $p->model->id }}" class="f-category">
@@ -50,7 +56,9 @@
         </li>
       </ol>
     </section>
+    <!-- PRE subforumsAfter -->
 @endif
+    <!-- PRE linksBBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
 @if ($p->model->canCreateTopic || $p->model->pagination)
@@ -66,7 +74,9 @@
       </div>
 @endif
     </div>
+    <!-- PRE linksBAfter -->
 @if ($p->topics)
+    <!-- PRE mainBefore -->
     <section id="fork-forum" class="f-main">
       <h2>{!! __('Topic list') !!}</h2>
       <div class="f-ftlist">
@@ -172,6 +182,8 @@
         </ol>
       </div>
     </section>
+    <!-- PRE mainAfter -->
+    <!-- PRE linksABefore -->
     <div class="f-nav-links">
     @if ($p->model->canCreateTopic || $p->model->pagination || $p->model->canMarkRead || $p->model->canSubscription)
       <div class="f-nlinks-a">
@@ -201,12 +213,16 @@
     @endif
     @yield ('crumbs')
     </div>
+    <!-- PRE linksAAfter -->
 @endif
 @if ($p->enableMod && $form = $p->formMod)
+    <!-- PRE modBefore -->
     <aside id="fork-mod" class="f-moderate">
       <h2>{!! __('Moderate') !!}</h2>
       <div class="f-fdivm">
     @include ('layouts/form')
       </div>
     </aside>
+    <!-- PRE modAfter -->
 @endif
+    <!-- PRE end -->

+ 10 - 0
app/templates/_default/index.forkbb.php

@@ -1,8 +1,12 @@
 @extends ('layouts/main')
+    <!-- PRE start -->
 @if ($p->categoryes)
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __('Forum list') !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE mainBefore -->
     <div class="f-main">
       <ol class="f-ftlist">
     @foreach ($p->categoryes as $id => $forums)
@@ -20,7 +24,9 @@
     @endforeach
       </ol>
     </div>
+    <!-- PRE mainAfter -->
     @if ($p->linkMarkRead)
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
       <div class="f-nlinks">
         <div class="f-actions-links">
@@ -30,6 +36,10 @@
         </div>
       </div>
     </div>
+    <!-- PRE linksAfter -->
     @endif
 @endif
+    <!-- PRE statsBefore -->
 @include ('layouts/stats')
+    <!-- PRE statsafter -->
+    <!-- PRE end -->

+ 12 - 0
app/templates/_default/layouts/admin.forkbb.php

@@ -1,12 +1,19 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __($p->adminHeader) !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links f-nav-admin{{ $p->mainSuffix or '' }}-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
+    <!-- PRE mainBefore -->
     <div class="f-main f-main-admin{{ $p->mainSuffix or '' }}">
+      <!-- PRE menuBefore -->
       <div id="fork-a-menu">
 @if ($p->aNavigation)
         <nav id="fork-a-nav" class="f-menu">
@@ -20,7 +27,12 @@
         </nav>
 @endif
       </div>
+      <!-- PRE menuAfter -->
+      <!-- PRE contentBefore -->
       <div id="forka">
 @yield ('content')
       </div>
+      <!-- PRE contentAfter -->
     </div>
+    <!-- PRE mainAfter -->
+    <!-- PRE end -->

+ 4 - 0
app/templates/_default/layouts/crumbs.forkbb.php

@@ -1,7 +1,9 @@
 @section ('crumbs')
+      <!-- PRE start -->
       <nav class="f-nav-crumbs">
         <ol class="f-crumbs" itemscope itemtype="https://schema.org/BreadcrumbList">
     @foreach ($p->crumbs as $cur)
+          <!-- PRE foreachStart -->
         @if (\is_object($cur[0]))
           <li class="f-crumb @if ($cur[0]->is_subscribed) f-subscribed @endif" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"><!-- inline -->
             <a class="f-crumb-a @if ($cur[2]) active" aria-current="page @endif" href="{{ $cur[0]->link }}" itemprop="item">
@@ -27,7 +29,9 @@
             <meta itemprop="position" content="{!! @iteration !!}">
           </li><!-- endinline -->
         @endif
+          <!-- PRE foreachEnd -->
     @endforeach
         </ol>
       </nav>
+      <!-- PRE end -->
 @endsection

+ 4 - 0
app/templates/_default/layouts/debug.forkbb.php

@@ -1,4 +1,6 @@
+      <!-- PRE start -->
       <aside id="fork-debug">
+        <!-- PRE inStart -->
         <p class="f-sim-header">{!! __('Debug table') !!}</p>
         <p id="id-fdebugtime">[ {!! __(['Generated in %1$s, %2$s queries', num(\microtime(true) - $p->start, 3), $p->numQueries]) !!} - {!! __(['Memory %1$s, Peak %2$s', size(\memory_get_usage()), size(\memory_get_peak_usage())]) !!} ]</p>
 @if ($p->queries)
@@ -23,4 +25,6 @@
           </tbody>
         </table>
 @endif
+        <!-- PRE inEnd -->
       </aside>
+      <!-- PRE end -->

+ 8 - 0
app/templates/_default/layouts/form.forkbb.php

@@ -1,5 +1,7 @@
+        <!-- PRE start -->
 @if ($form['action'])
         <form @if ($form['id']) id="{{ $form['id'] }}" @endif class="f-form" method="post" action="{{ $form['action'] }}" @if ($form['enctype']) enctype="{{ $form['enctype'] }}" @endif>
+          <!-- PRE formStart -->
 @endif
 @foreach ($form['sets'] as $setKey => $setVal)
     @if ($setVal['inform'])
@@ -31,6 +33,7 @@
               </dt>
               <dd>
                     @switch ($cur['type'])
+                <!-- PRE switchStart -->
                         @case ('text')
                         @case ('email')
                         @case ('number')
@@ -97,6 +100,7 @@
               </dd>
             </dl>
                     @break
+                <!-- PRE switchEnd -->
             @endswitch
         @endforeach
           </fieldset>
@@ -116,12 +120,16 @@
     @endif
           <p class="f-btns">
     @foreach ($form['btns'] as $key => $cur)
+            <!-- PRE btnsForeachStart -->
         @if ('submit' === $cur['type'])
             <button class="f-btn f-fbtn @if($cur['class']) {{ \implode(' ', $cur['class']) }} @endif" name="{{ $key }}" value="{{ $cur['value'] }}" @isset ($cur['accesskey']) accesskey="{{ $cur['accesskey'] }}" @endisset title="{{ $cur['value'] }}" @if ($cur['disabled']) disabled @endif><span>{{ $cur['value'] }}</span></button>
         @elseif ('btn'=== $cur['type'])
             <a class="f-btn f-fbtn @if($cur['class']) {{ \implode(' ', $cur['class']) }} @endif" data-name="{{ $key }}" href="{{ $cur['href'] }}" @isset ($cur['accesskey']) accesskey="{{ $cur['accesskey'] }}" @endisset title="{{ $cur['value'] }}"><span>{{ $cur['value'] }}</span></a>
         @endif
+            <!-- PRE btnsForeachEnd -->
     @endforeach
           </p>
+          <!-- PRE formEnd -->
         </form>
 @endif
+        <!-- PRE end -->

+ 4 - 0
app/templates/_default/layouts/iswev.forkbb.php

@@ -1,4 +1,6 @@
+      <!-- PRE start -->
       <aside class="f-iswev-wrap">
+        <!-- PRE inStart -->
 @if ($iswev[FORK_MESS_INFO])
         <div class="f-iswev f-info">
           <p class="f-sim-header">Info message:</p>
@@ -49,4 +51,6 @@
           </ul>
         </div>
 @endif
+        <!-- PRE inEnd -->
       </aside>
+      <!-- PRE end -->

+ 27 - 2
app/templates/_default/layouts/main.forkbb.php

@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <html lang="{{ __('lang_identifier') }}" dir="{{ __('lang_direction') }}">
 <head>
+  <!-- PRE headStart -->
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>{{! $p->pageTitle !}}</title>
@@ -36,28 +37,39 @@
   <{{ $pageHeader['type'] }} @foreach ($pageHeader['values'] as $key => $val) {{ $key }}="{{ $val }}" @endforeach>
     @endif
 @endforeach
+  <!-- PRE headEnd -->
 </head>
 <body>
+  <!-- PRE bodyStart -->
   <div id="fork" class="@if ($p->fNavigation)f-with-nav @endif @if($p->fPMFlash) f-pm-flash @endif">
+    <!-- PRE headerBefore -->
     <header id="fork-header">
       <p id="id-fhth1"><a id="id-fhtha" rel="home" href="{{ $p->fRootLink }}">{{ $p->fTitle }}</a></p>
 @if ('' != $p->fDescription)
       <p id="id-fhtdesc">{!! $p->fDescription !!}</p>
 @endif
     </header>
+    <!-- PRE headerAfter -->
+    <!-- PRE mainBefore -->
     <main id="fork-main">
 @if ($p->fAnnounce)
     <aside id="fork-announce">
+      <!-- PRE announceStart -->
       <p class="f-sim-header">{!! __('Announcement') !!}</p>
       <p id="id-facontent">{!! $p->fAnnounce !!}</p>
+      <!-- PRE announceEnd -->
     </aside>
 @endif
 @if ($iswev = $p->fIswev)
     @include ('layouts/iswev')
 @endif
+      <!-- PRE contentBefore -->
 @yield ('content')
+      <!-- PRE contentAfter -->
     </main>
+    <!-- PRE mainAfter -->
 @if ($p->fNavigation)
+    <!-- PRE navBefore -->
     <nav id="fork-nav" class="f-menu @if ($p->fNavigation['search']) f-main-nav-search @endif">
       <div id="fork-navdir">
         <input id="id-mn-checkbox" class="f-menu-checkbox" type="checkbox">
@@ -114,16 +126,27 @@
     @endif
       </div>
     </nav>
+    <!-- PRE navAfter -->
 @endif
+    <!-- PRE footerBefore -->
     <footer id="fork-footer">
       <p class="f-sim-header">{!! __('Board footer') !!}</p>
       <div id="fork-footer-in">
-        <div></div>
-        <div><p id="id-fpoweredby">{!! __('Powered by') !!}</p></div>
+        <div>
+          <!-- PRE footerFirstStart -->
+          <!-- PRE footerFirstEnd -->
+        </div>
+        <div>
+          <!-- PRE footerSecondStart -->
+          <p id="id-fpoweredby">{!! __('Powered by') !!}</p>
+          <!-- PRE footerSecondEnd -->
+        </div>
       </div>
 <!-- debuginfo -->
     </footer>
+    <!-- PRE footerAfter -->
   </div>
+  <!-- PRE scriptsBefore -->
 @foreach ($p->pageHeaders as $pageHeader)
     @if ('script' === $pageHeader['type'])
         @empty ($pageHeader['values']['inline'])
@@ -133,5 +156,7 @@
         @endempty
     @endif
 @endforeach
+  <!-- PRE scriptsAfter -->
+  <!-- PRE bodyEnd -->
 </body>
 </html>

+ 12 - 0
app/templates/_default/layouts/pm.forkbb.php

@@ -1,12 +1,19 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __($p->pmHeader) !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links f-nav-pm-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
+    <!-- PRE mainBefore -->
     <div class="f-main f-main-pm">
+      <!-- PRE menuBefore -->
       <div id="fork-pm-menu">
 @if ($p->pmNavigation)
         <nav id="fork-pm-nav" class="f-menu">
@@ -38,7 +45,12 @@
         </nav>
 @endif
       </div>
+      <!-- PRE menuAfter -->
+      <!-- PRE contentBefore -->
       <div id="forkpm">
 @yield ('content')
       </div>
+      <!-- PRE contentAfter -->
     </div>
+    <!-- PRE mainAfter -->
+    <!-- PRE end -->

+ 6 - 0
app/templates/_default/layouts/poll.forkbb.php

@@ -1,6 +1,9 @@
+            <!-- PRE start -->
             <div class="f-post-poll">
+              <!-- PRE inStart -->
 @if ($poll->canVote)
               <form class="f-form" method="post" action="{{ $poll->link }}">
+                <!-- PRE formStart -->
                 <input type="hidden" name="token" value="{{ $poll->token }}">
 @endif
 @foreach ($poll->question as $q => $question)
@@ -41,8 +44,11 @@
                 <p class="f-poll-btns">
                   <button class="f-btn" name="vote" value="{{ __('Vote') }}" title="{{ __('Vote') }}"><span>{!! __('Vote') !!}</span></button>
                 </p>
+                <!-- PRE formEnd -->
               </form>
 @elseif (null !== $poll->status)
               <p class="f-poll-status"><span>{!! __($poll->status) !!}</span></p>
 @endif
+              <!-- PRE inEnd -->
             </div>
+            <!-- PRE end -->

+ 8 - 0
app/templates/_default/layouts/redirect.forkbb.php

@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <html lang="{{ __('lang_identifier') }}" dir="{{ __('lang_direction') }}">
 <head>
+  <!-- PRE headStart -->
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta http-equiv="refresh" content="{{ $p->timeout }}; URL={{ $p->link }}">
@@ -16,9 +17,12 @@
   <{{ $pageHeader['type'] }} @foreach ($pageHeader['values'] as $key => $val) {{ $key }}="{{ $val }}" @endforeach>
     @endif
 @endforeach
+  <!-- PRE headEnd -->
 </head>
 <body>
+  <!-- PRE bodyStart -->
   <div id="fork">
+    <!-- PRE mainBefore -->
     <main id="fork-main">
       <aside id="fork-rdrct" class="f-main">
         <h2 id="id-rdrct-h2">{!! __('Redirecting') !!}</h2>
@@ -27,9 +31,13 @@
 @endif
       </aside>
     </main>
+    <!-- PRE mainAfter -->
+    <!-- PRE footerBefore -->
     <footer id="fork-footer">
 <!-- debuginfo -->
     </footer>
+    <!-- PRE footerAfter -->
   </div>
+  <!-- PRE bodyEnd -->
 </body>
 </html>

+ 4 - 0
app/templates/_default/layouts/stats.forkbb.php

@@ -1,4 +1,6 @@
+    <!-- PRE start -->
     <aside id="fork-stats">
+      <!-- PRE inStart -->
       <p class="f-sim-header">{!! __('Stats info') !!}</p>
 @if ($p->stats)
       <dl id="fork-stboard">
@@ -36,4 +38,6 @@
     @endforeach
       </dl><!-- endinline -->
 @endif
+      <!-- PRE inEnd -->
     </aside>
+    <!-- PRE end -->

+ 14 - 0
app/templates/_default/login.forkbb.php

@@ -1,25 +1,39 @@
 @extends ('layouts/main')
+    <!-- PRE start -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <section id="fork-login" class="f-main">
+      <!-- PRE mainStart -->
       <div class="f-fdiv f-lrdiv">
         <h2>{!! __('Login') !!}</h2>
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </section>
+    <!-- PRE mainAfter -->
 @endif
 @if ($form = $p->formOAuth)
+    <!-- PRE oauthBefore -->
     <div id="fork-oauth" class="f-main">
+      <!-- PRE oauthStart -->
       <div class="f-fdiv f-lrdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE oauthEnd -->
     </div>
+    <!-- PRE oauthAfter -->
 @endif
 @if ($p->regLink)
+    <!-- PRE lgrgBefore -->
     <div id="fork-lgrglnk" class="f-main">
+      <!-- PRE lgrgStart -->
       <div class="f-fdiv f-lrdiv">
         <div class="f-btns">
           <a class="f-btn f-fbtn" href="{{ $p->regLink }}">{!! __('Not registered') !!}</a>
         </div>
       </div>
+      <!-- PRE lgrgEnd -->
     </div>
+    <!-- PRE lgrgAfter -->
 @endif
+    <!-- PRE end -->

+ 2 - 0
app/templates/_default/maintenance.forkbb.php

@@ -1,4 +1,5 @@
 @extends ('layouts/main')
+      <!-- PRE start -->
       <div class="f-iswev-wrap">
         <section class="f-iswev f-info">
           <h2>Info message</h2>
@@ -7,3 +8,4 @@
           </ul>
         </section>
       </div>
+      <!-- PRE end -->

+ 2 - 0
app/templates/_default/message.forkbb.php

@@ -1,6 +1,8 @@
 @extends ('layouts/main')
+    <!-- PRE start -->
 @if ($p->back)
     <div id="fork-bcklnk">
       <p id="id-bcklnk-p"><a class="f-go-back" href="{{ $p->fRootLink }}">{!! __('Go back') !!}</a></p>
     </div>
 @endif
+    <!-- PRE end -->

+ 10 - 0
app/templates/_default/moderate.forkbb.php

@@ -1,15 +1,25 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __($p->formTitle) !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <div id="fork-modform" class="f-main">
+      <!-- PRE mainStart -->
       <div class="f-fdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </div>
+    <!-- PRE mainAfter -->
 @endif
+    <!-- PRE end -->

+ 6 - 0
app/templates/_default/passphrase_reset.forkbb.php

@@ -1,9 +1,15 @@
 @extends ('layouts/main')
+    <!-- PRE start -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <section id="fork-resetpass" class="f-main">
+      <!-- PRE mainStart -->
       <div class="f-fdiv f-lrdiv">
         <h2>{!! __('Passphrase reset') !!}</h2>
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </section>
+    <!-- PRE mainAfter -->
 @endif
+    <!-- PRE end -->

+ 14 - 0
app/templates/_default/post.forkbb.php

@@ -1,12 +1,18 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __($p->formTitle) !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
 @if ($p->previewHtml)
+    <!-- PRE previewBefore -->
     <section class="f-preview">
       <h2>{!! __('Post preview') !!}</h2>
       <div class="f-post-body">
@@ -18,16 +24,22 @@
         </div>
       </div>
     </section>
+    <!-- PRE previewAfter -->
 @endif
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <section class="f-post-form">
+      <!-- PRE mainStart -->
       <h2>{!! __($p->formTitle) !!}</h2>
       <div class="f-fdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </section>
+    <!-- PRE mainAfter -->
 @endif
 @if ($p->posts)
+    <!-- PRE postsBefore -->
     <section id="fork-view-posts">
       <h2>{!! __($p->postsTitle) !!}</h2>
     @foreach ($p->posts as $post)
@@ -58,4 +70,6 @@
         @endif
     @endforeach
     </section>
+    <!-- PRE postsAfter -->
 @endif
+    <!-- PRE end -->

+ 14 - 0
app/templates/_default/profile.forkbb.php

@@ -2,9 +2,13 @@
 @section ('avatar')<img class="f-avatar-img" src="{{ $p->curUser->avatar }}" alt="{{ $p->curUser->username }}"> @endsection
 @section ('signature') @if ($p->signatureSection){!! $p->curUser->htmlSign !!} @endif @endsection
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __(['%s\'s profile', $p->curUser->username]) !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
 @if ($p->actionBtns)
@@ -19,19 +23,29 @@
       </div>
 @endif
     </div>
+    <!-- PRE linksAfter -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <section id="fork-profile{{ $p->profileIdSuffix or '' }}" class="f-main f-main-profile">
+      <!-- PRE mainStart -->
       <h2>@if ($p->profileHeader === $p->curUser->username){{ $p->profileHeader }} @else{!! __($p->profileHeader) !!} @endif</h2>
       <div class="f-fdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </section>
+    <!-- PRE mainAfter -->
 @endif
 @if ($form = $p->formOAuth)
+    <!-- PRE oauthBefore -->
     <div id="fork-oauth" class="f-main">
+      <!-- PRE oauthStart -->
       <div class="f-fdiv f-lrdiv">
         <h2>{!! __('Add account') !!}</h2>
     @include ('layouts/form')
       </div>
+      <!-- PRE oauthEnd -->
     </div>
+    <!-- PRE oauthAfter -->
 @endif
+    <!-- PRE end -->

+ 10 - 0
app/templates/_default/register.forkbb.php

@@ -1,16 +1,26 @@
 @extends ('layouts/main')
+    <!-- PRE start -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <section id="fork-reg" class="f-main">
+      <!-- PRE mainStart -->
       <div class="f-fdiv f-lrdiv">
         <h2>{!! __('Register') !!}</h2>
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </section>
+    <!-- PRE mainAfter -->
 @endif
 @if ($form = $p->formOAuth)
+    <!-- PRE oauthBefore -->
     <div id="fork-oauth" class="f-main">
+      <!-- PRE oauthStart -->
       <div class="f-fdiv f-lrdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE oauthEnd -->
     </div>
+    <!-- PRE oauthAfter -->
 @endif
+    <!-- PRE end -->

+ 10 - 0
app/templates/_default/report.forkbb.php

@@ -1,15 +1,25 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __('Report post') !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <div id="fork-report" class="f-post-form">
+      <!-- PRE mainStart -->
       <div class="f-fdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </div>
+    <!-- PRE mainAfter -->
 @endif
+    <!-- PRE end -->

+ 12 - 0
app/templates/_default/rules.forkbb.php

@@ -1,18 +1,30 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __('Forum rules') !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
+    <!-- PRE mainBefore -->
     <div id="fork-rules" class="f-main">
       <div id="id-rules">{!! $p->rules !!}</div>
     </div>
+    <!-- PRE mainAfter -->
 @if ($form = $p->form)
+    <!-- PRE regBefore -->
     <div id="fork-rconf" class="f-main">
+      <!-- PRE regStart -->
       <div class="f-fdiv f-lrdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE regEnd -->
     </div>
+    <!-- PRE regAfter -->
 @endif
+    <!-- PRE end -->

+ 10 - 0
app/templates/_default/search.forkbb.php

@@ -1,15 +1,25 @@
 @include ('layouts/crumbs')
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __('Search') !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAfter -->
 @if ($form = $p->form)
+    <!-- PRE mainBefore -->
     <div id="fork-search" class="f-main">
+      <!-- PRE mainStart -->
       <div class="f-fdiv">
     @include ('layouts/form')
       </div>
+      <!-- PRE mainEnd -->
     </div>
+    <!-- PRE mainAfter -->
 @endif
+    <!-- PRE end -->

+ 16 - 0
app/templates/_default/topic.forkbb.php

@@ -21,9 +21,13 @@
     @endif
 @endsection
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{{ $p->model->name }}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
 @if ($p->model->canReply || $p->model->closed || $p->model->pagination)
@@ -45,6 +49,8 @@
       </div>
 @endif
     </div>
+    <!-- PRE linksBAfter -->
+    <!-- PRE mainBefore -->
     <section id="fork-topic" class="f-main">
       <h2>{!! __('Post list') !!}</h2>
 @foreach ($p->posts as $id => $post)
@@ -166,6 +172,8 @@
     @endif
 @endforeach
     </section>
+    <!-- PRE mainAfter -->
+    <!-- PRE linksABefore -->
     <div class="f-nav-links">
 @if ($p->model->canReply || $p->model->pagination || $p->model->canSubscription)
       <div class="f-nlinks-a">
@@ -191,22 +199,30 @@
 @endif
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAAfter -->
 @if ($p->enableMod && $form = $p->formMod)
+    <!-- PRE modBefore -->
     <aside id="fork-mod" class="f-moderate">
       <h2>{!! __('Moderate') !!}</h2>
       <div class="f-fdivm">
     @include ('layouts/form')
       </div>
     </aside>
+    <!-- PRE modAfter -->
 @endif
 @if ($p->online)
+    <!-- PRE statsBefore -->
     @include ('layouts/stats')
+    <!-- PRE statsAfter -->
 @endif
 @if ($form = $p->form)
+    <!-- PRE quickBefore -->
     <section class="f-post-form">
       <h2>{!! __('Quick post') !!}</h2>
       <div class="f-fdiv">
     @include ('layouts/form')
       </div>
     </section>
+    <!-- PRE quickAfter -->
 @endif
+    <!-- PRE end -->

+ 10 - 0
app/templates/_default/topic_in_search.forkbb.php

@@ -21,9 +21,13 @@
     @endif
 @endsection
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __($p->model->name) !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
 @if ($p->model->pagination)
@@ -32,6 +36,8 @@
       </div>
 @endif
     </div>
+    <!-- PRE linksBAfter -->
+    <!-- PRE mainBefore -->
     <section id="fork-topic-ins" class="f-main">
       <h2>{!! __('Post list') !!}</h2>
 @foreach ($p->posts as $id => $post)
@@ -95,6 +101,8 @@
     @endif
 @endforeach
     </section>
+    <!-- PRE mainAfter -->
+    <!-- PRE linksABefore -->
     <div class="f-nav-links">
 @if ($p->model->pagination)
       <div class="f-nlinks-a">
@@ -103,3 +111,5 @@
 @endif
 @yield ('crumbs')
     </div>
+    <!-- PRE linksAAfter -->
+    <!-- PRE end -->

+ 13 - 1
app/templates/_default/userlist.forkbb.php

@@ -21,9 +21,13 @@
     @endif
 @endsection
 @extends ('layouts/main')
+    <!-- PRE start -->
+    <!-- PRE h1Before -->
     <div class="f-mheader">
       <h1 id="fork-h1">{!! __('User list') !!}</h1>
     </div>
+    <!-- PRE h1After -->
+    <!-- PRE linksBBefore -->
     <div class="f-nav-links">
 @yield ('crumbs')
 @if ($p->pagination)
@@ -32,8 +36,10 @@
       </div>
 @endif
     </div>
+    <!-- PRE linksBAfter -->
 @if ($form = $p->form)
-    <section id="fork-usrlstform"  class="f-main">
+    <!-- PRE searchBefore -->
+    <section id="fork-usrlstform" class="f-main">
       <h2>{!! __($p->userRules->searchUsers ? 'User search head' : 'User sort head') !!}</h2>
       <details>
         <summary>{!! __($p->userRules->searchUsers ? 'User search head' : 'User sort head') !!}</summary>
@@ -42,8 +48,10 @@
         </div>
       </details>
     </section>
+    <!-- PRE searchAfter -->
 @endif
 @if ($p->userList)
+    <!-- PRE mainBefore -->
     <section id="fork-usrlst" class="f-main">
       <h2>{!! __('User_list') !!}</h2>
       <div class="f-ulist">
@@ -96,11 +104,15 @@
         </ol>
       </div>
     </section>
+    <!-- PRE mainAfter -->
     @if ($p->pagination)
+    <!-- PRE linksABefore -->
     <div class="f-nav-links">
       <div class="f-nlinks">
         @yield ('pagination')
       </div>
     </div>
+    <!-- PRE linksAAfter -->
     @endif
 @endif
+    <!-- PRE end -->

+ 0 - 0
ext/.gitkeep


+ 71 - 4
public/style/ForkBB/admin.css

@@ -1,5 +1,5 @@
 #forka {
-  --c-log-in-c: hsl(220,5%,12%);
+  --c-log-in-c: hsl(0,0%,0%);
 }
 
 /******************/
@@ -110,6 +110,7 @@
 
 #forka .f-fleg {
   margin-bottom: 0.3125rem;
+  background: linear-gradient(var(--bg-like-nav), var(--bg-fprimary), var(--bg-like-nav));
 }
 
 #forka .f-flblch {
@@ -410,7 +411,7 @@
 /* Админка/Пользователи */
 /************************/
 #fork-ausersrch-rs .f-btns {
-  text-align: end;
+  justify-content: flex-end;
 }
 
 #fork-ausersrch-rs .f-btns .f-fbtn,
@@ -1056,9 +1057,9 @@
   text-align: center;
 }
 
-#fork #fork-logview summary.f-lgsu {
+/*#fork #fork-logview summary.f-lgsu {
   margin: 0;
-}
+}*/
 
 #fork #fork-logview summary.f-lgsu::after {
   content: none;
@@ -1269,3 +1270,69 @@
     width: 100%;
   }
 }
+
+/****************************************/
+/* Админка/РАсширения                   */
+/****************************************/
+#forka #fork-extsinfo summary.f-extsu::after {
+  content: none;
+}
+
+#fork-extsinfo .f-extli:first-child {
+  border-top: 0.0625rem dotted var(--br-fprimary);
+}
+
+#fork-extsinfo .f-extli {
+  border-bottom: 0.0625rem dotted var(--br-fprimary);
+  padding: 0.625rem;
+}
+
+#fork-extsinfo .f-extsu {
+  display: block;
+}
+
+#fork-extsinfo .f-extdata {
+  color: var(--c-fprimary);
+  border: 0.0625rem solid var(--br-fprimary);
+  padding: 0.3125rem;
+}
+
+#fork-extsinfo .f-extdd-author {
+  width: 100%;
+}
+
+#fork-extsinfo .f-ext-status0 {
+  color: var(--c-log-in-c);
+  background-color: #90CAF9;
+}
+
+#fork-extsinfo .f-ext-status4,
+#fork-extsinfo .f-ext-status5,
+#fork-extsinfo .f-ext-status6 {
+  color: var(--c-log-in-c);
+  background-color: #1976D2;
+}
+
+#fork-extsinfo .f-ext-status8 {
+  color: var(--c-log-in-c);
+  background-color: #4CAF50;
+}
+
+#fork-extsinfo .f-ext-status9,
+#fork-extsinfo .f-ext-status10 {
+  color: var(--c-log-in-c);
+  background-color: #FF5722;
+}
+
+#fork-extsinfo .f-ext-status12 {
+  color: var(--c-log-in-c);
+  background-color: #D32F2F;
+}
+
+#forka #fork-extsinfo .f-fbtn {
+  width: auto;
+}
+
+#forka .f-fbtn[data-name="uninstall"]:not(.origin) {
+  color: red;
+}

+ 3 - 0
public/style/ForkBB/style.css

@@ -232,6 +232,9 @@ body,
 
 #fork summary {
   cursor: pointer;
+}
+
+#fork details[open] > summary {
   margin-bottom: 0.625rem;
 }