123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- <?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.symlinks' => 'array',
- 'extra.symlinks.*.type' => 'required|string|in:public',
- 'extra.symlinks.*.target' => 'required|string',
- 'extra.symlinks.*.link' => 'required|string',
- '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->setSymlinks($ext);
- $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,
- ]);
- $this->removeSymlinks($ext);
- 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,
- ]);
- $this->removeSymlinks($ext);
- if (true !== $this->updateCommon($ext)) {
- $this->error = 'An error occurred in updateCommon';
- return false;
- }
- $this->setSymlinks($ext);
- 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->setSymlinks($ext);
- $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->removeSymlinks($ext);
- $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;
- }
- protected function setSymlinks(Extension $ext): bool
- {
- $data = $this->loadDataFromFile($this->commonFile);
- $symlinks = $data[$ext->name]['symlinks'] ?? [];
- foreach ($symlinks as $target => $link) {
- \symlink($target, $link);
- }
- return true;
- }
- protected function removeSymlinks(Extension $ext): bool
- {
- $data = $this->loadDataFromFile($this->commonFile);
- $symlinks = $data[$ext->name]['symlinks'] ?? [];
- foreach ($symlinks as $target => $link) {
- if (\is_link($link)) {
- \is_file($link) ? \unlink($link) : \rmdir($link);
- }
- }
- return true;
- }
- }
|