extension_id)) { return $this->extension_id; } if ($fallbackParent) { if ($this->isTheme() && $this->isChildTheme()) { # Идентификатор исходной темы: return $this->getParentTheme()->getExtensionId($fallbackToName, $fallbackParent); } } return ($fallbackToName ? $this->getName() : ''); } /** * Расширение было зарегистрировано * @return bool */ public function isRegistered(): bool { return (!empty($this->extension_id) && mb_strlen($this->extension_id) === 40); } /** * Расширение было включено в режиме тестирования * @return bool */ public function isTestmode(): bool { return !empty($this->extension_testmode); } /** * Получение типа расширения * @param bool $asText в форме текста * @return int|string */ public function getExtensionType(bool $asText = false) { if ($asText) { switch ($this->extension_type) { case EXTENSION_TYPE_PLUGIN: return 'plugins'; case EXTENSION_TYPE_THEME: return 'themes'; case EXTENSION_TYPE_MODULE: return 'modules'; } return ''; } return $this->extension_type; } /** * Является ли расширение темой * @return bool */ public function isTheme(): bool { return $this->extension_type === EXTENSION_TYPE_THEME; } /** * Является ли расширение плагином * @return bool */ public function isPlugin(): bool { return $this->extension_type === EXTENSION_TYPE_PLUGIN; } /** * Установка совместимости * Допустимые форматы: 'A.B.C' (>=), ['do'=>'A.B.C'] (>=), ['do'=>['A.B.C','A.B.C']] (=) * @param bool|array $compatible * @return void */ protected function setCompatible($compatible) { $this->extension_compatible = $compatible; } /** * Проверка совместимости расширения с текущей версией продукта * @return bool совместимо (true) */ public function isCompatible(): bool { return Dev::productVersionCompatible($this->extension_compatible); } /** * Установка зависимостей * @param array $dependencies * [ * 'extension_id' => [ * 'type' => EXTENSION_TYPE_PLUGIN, # тип расширения * 'version' => '1.0.0', # минимально требуемая версия * 'title' => 'Название расширения', * ], * ... * ] * @return void */ public function setDependencies(array $dependencies = []) { $this->extension_dependencies = $dependencies; } /** * Получение списка зависимостей * @return array */ public function getDependencies(): array { return $this->extension_dependencies; } /** * Установка статуса зависимостей * @param bool $status проверка на наличие зависимостей была успешно пройдена * @return void */ public function setDependenciesStatus(bool $status) { $this->extension_dependencies_status = $status; } /** * Получение статуса зависимостей * @return bool */ public function getDependenciesStatus(): bool { return $this->extension_dependencies_status; } /** * Расширение является аддоном * @return bool */ public function isAddon(): bool { return ! empty($this->extension_addon_for) || ($this instanceof ThemeAddon) || ($this instanceof PluginAddon); } /** * Заявляем аддон для расширения * @param string $id идентификатор расширения или Theme::getBaseTheme() * @param string|mixed $version версия расширения * @return void */ public function setAddonFor(string $id, $version) { if (! empty($id) && ! empty($version)) { $this->extension_addon_for[$id] = ['version' => $version]; } } /** * Поддерживает ли аддон указанное расширение * @param self $extension объект расширения * @return bool */ public function isAddonFor($extension) { return array_key_exists( (is_object($extension) ? $extension->getExtensionId(true) : $extension), $this->extension_addon_for ); } /** * Закрепляем связь расширения с аддоном * @param self $addon * @return bool */ public function addAddon($addon): bool { if (is_array($this->extension_addons)) { $this->extension_addons = new SplObjectStorage(); } if (! $this->extension_addons->contains($addon)) { $this->extension_addons->attach($addon, $addon->getName()); return true; } return false; } /** * Поиск связанного аддона * @param \bff\extend\Extension|string $addon объект расширения или его название * @return \bff\extend\Extension|Object|bool объект расширения или false (найти не удалось) */ public function hasAddon($addon) { if (is_array($this->extension_addons) || ! $this->extension_addons->count()) { return false; } if (is_string($addon)) { $this->extension_addons->rewind(); while ($this->extension_addons->valid()) { if ($this->extension_addons->getInfo() === $addon) { return $this->extension_addons->current(); } $this->extension_addons->next(); } } elseif (is_object($addon)) { if ($this->extension_addons->contains($addon)) { return $addon; } } return false; } /** * Есть ли связанные аддоны * @return bool */ public function hasAddons(): bool { if (is_array($this->extension_addons) || ! $this->extension_addons->count()) { return false; } return true; } /** * Получаем список связанных аддонов * @param bool $toJson * @return array|Extension[] */ public function getAddons($toJson = false): array { if (! $this->hasAddons()) { return []; } $list = []; $this->extension_addons->rewind(); while ($this->extension_addons->valid()) { $addon = $this->extension_addons->current(); if ($toJson) { $list[] = [ 'name' => $this->extension_addons->getInfo(), 'id' => $addon->getExtensionId(), ]; } else { $list[$addon->getExtensionId()] = $addon; } $this->extension_addons->next(); } $this->extension_addons->rewind(); return $list; } /** * Добавление предупреждений * @param string|array $message * @return void */ protected function setWarnings($message) { if (is_array($message)) { foreach ($message as $v) { if (is_string($v)) { $this->extension_warnings[] = $v; } } } elseif (is_string($message)) { $this->extension_warnings[] = $message; } } /** * Очистка списка предупреждений * @return array список предупреждений */ protected function clearWarnings(): array { $list = $this->extension_warnings; $this->extension_warnings = []; return $list; } /** * Получение списка предупреждений * @return array */ public function getWarnings(): array { return $this->extension_warnings; } /** * Добавить сообщение в разделе "Состояние системы" * @param int $level тип сообщения HH::ERROR, ... * @param string $key уникальный текстовый ключ сообщения * @param string|array $message текст сообщения * @param array $opts * @return void */ public function systemMessage(int $level, string $key, $message, array $opts = []) { $opts['extension'] = $this; $this->app->hh()->set($level, $this->getExtensionId() . '_' . $key, $message, $opts); } /** * Удалить ранее добавленное сообщение в разделе "Состояние системы" * @param string $key уникальный текстовый ключ сообщение (ранее добавленного) * @param array $opts * @return void */ public function systemResolve(string $key, array $opts = []) { $this->app->hh()->resolve($this->getExtensionId() . '_' . $key); } /** * Метод вызываемый в процессе выполнения проверки в разделе "Состояние системы" * В данном методе дополнение может добавлять и удалять ранее добавленные сообщения * @return void */ public function systemCheck() { // } /** * Инициируем событие расширения * @param string $action * @param array $data * @return mixed */ public function triggerEvent(string $action, array $data = []) { return $this->triggerEventTo($this, $action, $data); } /** * Инициируем событие расширения * @param Extension $extension * @param string $action * @param array $data * @return mixed */ public function triggerEventTo($extension, string $action, array $data = []) { $data['__extension'] = $extension; $filter = join('.', [ $extension->getExtensionType(true), $action, $extension->getName(), ]); return $this->app->filter($filter, false, $data); } /** * Метод вызываемый перед стартом расширения * @return void */ public function onBeforeStart() { foreach ($this->extension_config_templates as $id => &$template) { if ($template['onStart'] !== null && ! empty($template['settings']) && $this->isActive()) { call_user_func($template['onStart'], $template['settings']); } } unset($template); } /** * Поля настроек расширения * @param array $settings настройки * 'уникальный ключ' => [ * 'title' => 'Название поля в форме', * 'input' => 'Тип поля в форме' - доступны: 'checkbox', 'select', 'password', 'text', 'textarea' * 'options' => 'Варианты значений для типа 'select' (выпадающий список)': * [ * 'ключ' => ['title'=>'название'] * ... * ], * 'default' => 'Значение по-умолчанию', * ] * @param array $options дополнительные параметры: [ * 'tabs' => [] * 'titleRow' => 120, // Ширина столбца с названиями полей настроек * ] * @return void */ protected function configSettings(array $settings = [], array $options = []) { if (! array_key_exists('tabs', $options)) { $options['tabs'] = []; } if (! empty($this->extension_config_templates)) { $tabsPlus = []; $settingsCount = sizeof($settings); foreach ($this->extension_config_templates as $id => $template) { if (empty($template['settings'])) { continue; } if (! empty($template['keys.overrideBy']) && $settingsCount > 0) { foreach ($template['keys.overrideBy'] as $k) { if (array_key_exists($k, $settings)) { continue 2; } } } foreach ($template['settings'] as &$sett) { $sett = config::merge($template['template'], $sett); if (empty($sett['tab'])) { $settingsCount++; } $settings[$sett['config.key']] = &$sett; } unset($sett); if (! empty($template['tab']['key'])) { $tabsPlus[$template['tab']['key']] = $template['tab']; } } if (($settingsCount > 0 || empty($tabsPlus)) && empty($options['tabs'])) { $options['tabs']['def'] = ['title' => _t('ext', 'General')]; } foreach ($tabsPlus as $k => $v) { $options['tabs'][$k] = $v; } } foreach ($this->extension_config_forms as $form) { if ($form instanceof Form) { $form->appendConfigSettings($settings); } } $this->extension_config = $settings; $this->extension_config_options = array_merge($this->extension_config_options, $options); $this->extension_config = config::extension( $this->extension_name, $this->extension_config, $this ); } /** * Добавление полей настроек расширения при отложенной инициализации формы * @param int $id * @return void */ public function appendConfigSettings($id) { if (! isset($this->extension_config_forms[$id])) { return; } $form = $this->extension_config_forms[$id]; if (! $form instanceof InvokeForm) { return; } $form()->appendConfigSettings($this->extension_config); $this->extension_config = config::extension( $this->extension_name, $this->extension_config, $this ); } /** * Форма настроек расширения * @param string $tab название таба * @param array $options: * int 'priority' - приоритет вывода таба * string 'name' - название формы, если не указано, берется id * @return Form|InvokeForm */ public function configSettingsForm(string $tab, array $options = []) { $id = sizeof($this->extension_config_forms) + 1; $init = false; if (isset($options['initPath'])) { $_o = $options; $init = function ($form) use ($_o) { include_once $_o['initPath']; }; } if (! $this->isAdminPanel() && $init) { $o = $options; $o['id'] = $id; $o['init'] = $init; $form = new InvokeForm($this, $o); } else { $form = new Form($id, $this); $form->init(); if (! empty($options['name'])) { $form->name($options['name']); } if ($init) { $init($form); } } if (! isset($options['priority'])) { $options['priority'] = $id * 10; } $this->extension_config_forms[$id] = $form; $this->settingsTab($tab, [$form, 'content'], [$form, 'submit'], $options); return $form; } /** * Обработчик AJAX запросов от формы настроек расширения * @param int $id идентификатор формы * @return void */ public function configSettingsFormAjax(int $id) { if (array_key_exists($id, $this->extension_config_forms)) { $this->extension_config_forms[$id]->ajax(); } } /** * Регистрация шаблона настроек расширения * @param string $id уникальный ключ шаблона * @param array $config настройки шаблона: [ * string 'key' - ключ настройки по умолчанию * array 'keys.overrideBy' - ключи настроек расширения, подавляющие данную шаблонную настройку * array 'template' - шаблон [ * string 'title' - название настройки * string 'input' - тип поля: 'select', 'checkbox', 'number', ... * int 'type' - тип данных: TYPE_NOTAGS, TYPE_UINT, TYPE_BOOL ... * ... * ] * array 'tab' - таб настройки [ * string 'key' - ключ таба * string 'title' - название таба * ] * ] * @param Closure|null $onStart функция вызываемая в момент старта плагина/темы * @param array $settings шаблонные настройки по умолчанию: [ * // * ] * @return void */ protected function configSettingsTemplateRegister( string $id, array $config, ?Closure $onStart = null, array $settings = [] ) { if (! array_key_exists($id, $this->extension_config_templates)) { $this->extension_config_templates[$id] = array_merge([ 'template' => [], 'key' => '_' . $id, 'tab' => [], 'keys.overrideBy' => [], 'onStart' => $onStart, 'settings' => [], ], $config); } if (! empty($settings)) { $this->configSettingsTemplate($id, $settings, false); } } /** * Определяем шаблонные настройки * @param string $id уникальный ключ шаблона * @param array $settings настройки * @param bool $extend дополнить * @return bool */ protected function configSettingsTemplate(string $id, array $settings, bool $extend = false): bool { if (! array_key_exists($id, $this->extension_config_templates)) { return false; } $template = &$this->extension_config_templates[$id]['settings']; if (! $extend) { $template = []; } foreach ($settings as $key => $params) { if (empty($key) || !is_string($key) || !is_array($params)) { continue; } if ($extend && array_key_exists($key, $template)) { $template[$key] = config::merge($template[$key], $params); } else { $template[$key] = $params; } if (empty($template[$key]['config.key'])) { $template[$key]['config.key'] = $this->extension_config_templates[$id]['key'] . '.' . $key; } } return true; } /** * Дополнительный таб настроек * @param string $name название таба * @param callable|string $content функция возвращающая HTML (callable) или путь к файлу шаблона (string) * @param callable $submit обработчик сабмита формы * @param array $options: * int 'priority' - приоритет вывода таба * @return void */ protected function settingsTab(string $name, $content, callable $submit, array $options = []) { if (! $this->isAdminPanel() || empty($name)) { return; } $hookPrefix = 'extensions.' . $this->getExtensionType(true) . '.' . $this->getName() . '.settings.'; $tabKey = 'custom_' . mb_strtolower(func::translit($name)); $this->app->hooksAdd([ $hookPrefix . 'tabs' => function ($tabs) use ($name, $tabKey, $options) { $tabs[$tabKey] = [ 'title' => $name, 'custom' => true, 'priority' => $options['priority'] ?? 1, ]; return $tabs; }, $hookPrefix . 'tabs.content' => function ($data) use ($content, $tabKey, $options) { $contentHTML = ''; if (is_callable($content)) { if (!empty($data['submitButtons'])) { $options['submitButtons'] = & $data['submitButtons']; } $contentHTML = call_user_func($content, $data, $options); } elseif (is_string($content)) { $data['config'] = $this->config([]); $contentHTML = $this->template($content, $data); } if (!empty($data['submitButtons'])) { $contentHTML .= $data['submitButtons']; } if (isset($data['wrapper']) && is_callable($data['wrapper'])) { echo call_user_func($data['wrapper'], $tabKey, $contentHTML); } else { echo '
'; echo $contentHTML; echo '
'; } }, $hookPrefix . 'submit' => function ($data) use ($tabKey, $submit, $options) { if (is_callable($submit) && $this->input->post('tab', TYPE_STR) === $tabKey) { call_user_func($submit, $data, $options); } }, ]); } /** * Сформировать форму основных настроек расширения * @param Module $controller * @param Form $form * @return Form */ public function configSettingsToForm($controller = null, $form = null) { if (is_null($controller)) { $controller = $this; } if (is_null($form)) { $form = AdminHelpers::form($this, null, ['_create' => function ($p) use ($controller) { return new Form($p['id'] ?? 1, $controller, $p['action'] ?? $this->app->router()->controllerMethod()); }]); } $form->setFolder($this->app->path('extensions', 'images')); $tabs = false; if (! empty($this->extension_config_options['tabs'])) { $tabs = true; foreach ($this->extension_config_options['tabs'] as $k => $v) { $form->tab($v['key'] ?? $k, $v['title'] ?? ''); } } foreach ($this->extension_config as $id => $field) { if (empty($field['input']) || in_array($field['input'], ['temp', 'sys', 'hidden', 'custom'])) { continue; } $name = $id; if (! empty($field['tab'])) { $name = str_replace($field['tab'] . '.', '', $name); $form->tab($field['tab']); } elseif ($tabs) { $form->tab('def'); } if (empty($name)) { $name = $id; } switch ($field['input']) { case 'checkbox': $form->checkbox($name, $field['title'] ?? '', $field['default'] ?? ''); if (! empty($field['description'])) { $form->label($field['description']); $field['description'] = ''; } break; case 'select': if (empty($field['options'])) { continue 2; } $options = $field['options']; $form->select($name, $field['title'] ?? '', $field['default'] ?? '', function () use ($options) { $result = []; foreach ($options as $k => $v) { if (empty($v['title'])) { continue; } $result[] = ['id' => $k, 'title' => $v['title']]; } return $result; }); break; case 'password': $form->password($name, $field['title'] ?? '', $field['default'] ?? ''); break; case 'text': $form->text($name, $field['title'] ?? '', $field['default'] ?? '', false); break; case 'textarea': $form->textarea($name, $field['title'] ?? '', $field['default'] ?? '', false); break; case 'number': $form->number( $name, $field['title'] ?? '', $field['min'] ?? 1, $field['max'] ?? 0, $field['step'] ?? 1, $field['default'] ?? '' ); break; case 'image': $form->images($name, $field['title'] ?? '', 1); $image = $field['image'] ?? []; if (! empty($image['extensionsAllowed']) && is_array($image['extensionsAllowed'])) { $form->param('extensionsAllowed', $image['extensionsAllowed']); } break; case 'file': $form->files($name, $field['title'] ?? '', 1); $file = $field['file'] ?? []; if (! empty($file['extensionsAllowed']) && is_string($file['extensionsAllowed'])) { $form->param('extensions', $file['extensionsAllowed']); } if (! empty($file['maxSize'])) { $form->param('maxSize', $file['maxSize']); } break; case 'divider': $form->divider(); break; case 'title': $form ->staticText($name, '', '' . ($field['title'] ?? '') . '') ->beforeFieldView(function ($p) { $p['data']['wrapper'] = ['colspan' => 2]; }) ; break; default: continue 2; } if (! empty($field['readonly'])) { $form->attr('readonly', true); } if (! empty($field['required'])) { $form->required(); } if (! empty($field['tip'])) { $form->tip($field['tip']); } if (! empty($field['description'])) { $form->htmlAfter($field['description']); } if (! empty($field['configKey'])) { $form->sysAdmin($field['configKey']); } if (! empty($field['placeholder'])) { $form->placeholder($field['placeholder']); } if (! empty($field['width'])) { $form->width($field['width']); } if (isset($field['callable']) && $field['callable'] instanceof Closure) { ($field['callable'])($form, $name); } } return $form; } /** * Метод вызываемый при сбросе настроек к значениям по умолчанию * @return void */ public function onSettingsReset() { } /** * Получение настройки расширения по ключу * @param string|array $key ключ (несколько ключей) * @param mixed $default значение по-умолчанию * @param mixed $opts * @return mixed */ public function config($key, $default = '', $opts = []) { $language = $opts['language'] ?? null; # ключ языка, для мультиязычных данных (null - текущий) if (is_string($key)) { if (isset($this->extension_config[$key])) { $setting = &$this->extension_config[$key]; if (isset($setting['dynamic'])) { return call_user_func($setting['dynamic'], $setting, $default, $language); } if (! empty($setting['lang'])) { if (! is_null($language)) { if ( isset($setting['data_edit'][$language]) && $setting['data_edit'][$language] !== '' ) { return $setting['data_edit'][$language]; } else { return $default; } } else { return ($setting['data'] !== '' ? $setting['data'] : $default); } } return $setting['data']; } else { return $default; } } elseif (is_array($key)) { $data = []; if (empty($key)) { foreach ($this->extension_config as $k => $v) { $key[$k] = $v['default'] ?? ''; } } foreach ($key as $k => $v) { if (is_string($k)) { # $key = ['key1'=>'default', 'key2'=>'default', ...] $data[$k] = $this->config($k, $v, $language); } elseif (is_string($v)) { # $key = ['key1','key2', ...], $default = default $data[$v] = $this->config($v, $default, $language); } } return $data; } return $default; } /** * Сохранение настройки расширения * @param string|array $key ключ * @param mixed $value значение * @param bool $dynamic динамическая настройка * @return void */ public function configUpdate($key, $value = '', bool $dynamic = false) { if (config::extensionSave($key, $value, $this->extension_name, $this->extension_config, $dynamic)) { if (is_array($key)) { foreach ($key as $k => $v) { $this->extension_config[$k]['data'] = $v; } } else { $this->extension_config[$key]['data'] = $value; } } } /** * Проверка наличия настройки расширения по ключу * @param string $key ключ * @return bool */ public function configExists(string $key): bool { return isset($this->extension_config[$key]); } /** * Перегрузка внутренних настроек расширения * @param string $key ключ * @param mixed $default значение по-умолчанию * @return mixed */ protected function configInternal(string $key, $default = '') { return $this->app->filter($this->extension_name . '.' . strval($key), $default); } /** * Объект настройки типа 'image' * @param string $key ключ настройки расширения (configSettings) * @param bool|string|array $size ключ требуемого размера * @return \bff\extend\extension\Image|string|array|bool */ public function configImages(string $key, $size = false) { /** @var \bff\extend\extension\Image $obj */ $obj = false; if ( isset($this->extension_config[$key]['object']) && is_a($this->extension_config[$key]['object'], extension\Image::class) ) { $obj = $this->extension_config[$key]['object']; } if ($size === false) { return $obj; } if ($obj === false || empty($this->extension_config[$key]['data'])) { return (is_array($size) ? [] : ''); } if ($obj->getLimit() < 2) { return $obj->getURL(current($this->extension_config[$key]['data']), $size); } $url = []; foreach ($this->extension_config[$key]['data'] as $file) { $url[] = $obj->getURL($file, $size); } return $url; } /** * Объект настройки типа 'file' * @param string $key ключ настройки расширения (configSettings) * @param bool $data возвращать данные о загруженных файлах * @return \bff\extend\extension\File|string|array|bool */ public function configFiles(string $key, bool $data = false) { $obj = false; if ( isset($this->extension_config[$key]['object']) && is_a($this->extension_config[$key]['object'], extension\File::class) ) { $obj = $this->extension_config[$key]['object']; } if ($data === false) { return $obj; } if ($obj === false) { return []; } return $obj->loadData(); } /** * Логирование сообщение * @param string|array $message сообщение * @param array|int $context or level * @param mixed $level * @return void */ public function log($message, $context = [], $level = null) { if (is_array($message)) { $message = print_r($message, true); } if (is_int($context)) { $level = $context; $context = []; } if (is_scalar($context) && !empty($context)) { $context = [$context]; } $this->logger()->log($level ?? Logger::ERROR, $message, $context); } /** * Получение сообщений расширения записанных в логи * @param int $limit кол-во строк, 0 - все доступные * @return array */ public function getLogs(int $limit = 0): array { return $this->logger()->getRotatingFilesContent($limit); } /** * Объект логгера, выполняющий функцию логирования * @return Logger */ protected function logger() { if (is_null($this->extension_logger)) { $this->extension_logger = $this->app->logger($this->extension_name, 'extensions.log'); } return $this->extension_logger; } /** * Формирование абсолютного пути к файлу в директории расширения * @param string $file относительный путь к файлу * @param array|bool $opts [mod] * @return string */ public function path(string $file, $opts = []): string { $opts = $this->defaults((!is_array($opts) ? ['mod' => $opts] : $opts), [ 'mod' => true, # разрешать модифицикацию ]); $file = str_replace('/', DIRECTORY_SEPARATOR, $this->extension_path . ltrim($file, '\/ ')); if ($opts['mod']) { return modification($file); } return $file; } /** * Формирование абсолютного пути к файлу в public директории расширения * @param string $file относительный путь к файлу * @param array $opts [mod, custom] * @return string */ public function pathPublic(string $file, $opts = []): string { $opts = $this->defaults($opts, [ 'mod' => true, # разрешать модифицикацию 'custom' => false, # путь к custom версии файла ]); $file = $this->app->publicPath( (!$opts['mod'] && $opts['custom'] ? 'custom/' : '') . static::dir($this->extension_type) . '/' . $this->getName() . '/' . ltrim($file, '\/ ') ); if ($opts['mod']) { return modification($file); } return $file; } /** * Формирование абсолютного пути к директории хранения файлов загружаемых расширением * @param bool $public true - директория доступная по ссылке, false - недоступная, для хранения системных файлов * @param bool $asUrl в формате URL (public only) * @return string */ public function pathUpload(bool $public = true, bool $asUrl = false): string { $name = $this->getName(); if ($public) { if ($asUrl) { return $this->app->url('extensions/' . $name); } return $this->app->path('extensions/' . $name); } return $this->app->basePath('files/extensions/' . $name . '/'); } /** * Формирование структуры файлов по указанному пути * @param string $path относительный путь * @param array|bool $filesOnly только файлы, список доступных расширений * @return array */ public function pathStructure(string $path = '/', $filesOnly = false): array { $path = $this->extension_path . ltrim($path, DS . ' '); $structure = []; $filter = [ 'file' => [], 'dir' => ['.git','.svn'], ]; $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), function ($v) use ($filter, $filesOnly) { if ($v->isFile()) { if (is_array($filesOnly) && ! in_array($v->getExtension(), $filesOnly)) { return false; } return !in_array($v->getFilename(), $filter['file']); } if ($v->isDir()) { return !in_array($v->getFilename(), $filter['dir']); } return false; } ), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $v) { if ($v->isFile()) { $structure[] = $iterator->getSubPathName(); } elseif ($v->isDir() && $filesOnly === false) { $structure[] = $iterator->getSubPathName(); } } return $structure; } /** * Получение названия директории расширения по типу * @param int $extensionType тип расширения * @return string */ public static function dir(int $extensionType): string { switch ($extensionType) { case EXTENSION_TYPE_PLUGIN: return 'plugins'; case EXTENSION_TYPE_THEME: return 'themes'; case EXTENSION_TYPE_MODULE: return 'modules'; } return ''; } /** * Регистрируем дополнительный модуль * @param string $name название модуля (латиницей) * @param string $class название класса модуля * @param array $opts дополнительные параметры * @return bool удалось ли зарегистрировать модуль */ public function moduleRegister(string $name, string $class = '', array $opts = []) { $opts['extension'] = $this; $opts['class'] = $class ?: $name; $opts['routes'] = $this->isActive(); return $this->app->moduleRegister($name, $this->path($opts['path'] ?? ''), $opts); } /** * Формирование URL файла * @param string $file название файла * @param string|null $version версия файла или null * @return string */ public function url(string $file, ?string $version = null) { $prefix = '/' . static::dir($this->extension_type) . '/' . $this->getName() . '/'; return $this->app->url( $prefix . ltrim($file, '/ '), $version ); } /** * Подключение javascript файла расширения * Файлы при этом следует хранить в ".../директория расширения/static/file.js" * @param string|array $file название javascript файла * @param array|null $data данные javascript * @param array $opts [ * string|null 'version' - версия файла (опционально) * bool 'admin' - admin-контекст подключения * bool 'top' - форсируем подключение * int|null 'priority' - приоритет подключения * ] * @return void */ public function js($file, $data = null, array $opts = []) { if (is_array($data)) { $this->view->jsData($data); } if (is_array($file)) { foreach ($file as $f) { $this->js($f, null, $opts); } return; } $opts = $this->defaults($opts, [ 'version' => null, 'admin' => false, 'top' => false, 'if' => null, 'priority' => null, ]); $this->app->hooks()->javascriptExtra(!$opts['admin'], function () use ($file, $opts) { if ($opts['if'] !== null) { if ($opts['if'] instanceof Closure) { $opts['if'] = $opts['if']($file, $opts); } if (! $opts['if']) { return; } } $this->view->script( $this->url($file, $opts['version'] ?? $this->getStaticFileManifestVersion($file)), [ 'top' => $opts['top'], ] ); }, $opts['priority'] ?? null); } /** * Подключение javascript файла расширения в панели администатора * @param string $file название javascript файла * @param array|null $data * @param array $opts * @return void */ public function jsAdmin(string $file, $data = null, array $opts = []) { $opts['admin'] = true; $this->js($file, $data, $opts); } /** * Подключение css файла расширения * Файлы при этом следует хранить в ".../директория расширения/static/file.css" * @param string $file название css файла * @param mixed $opts версия файла или [ * string|false 'version' - версия файла * string|array 'theme' - название темы, при активации которой следует подключать css файл * bool 'admin' - admin-контекст подключения * bool 'top' - форсируем подключение * int|null 'priority' - приоритет подключения * ] * @return void */ public function css(string $file, $opts = false) { if (! is_array($opts)) { $opts = ['version' => $opts]; } $opts = $this->defaults($opts, [ 'version' => false, 'theme' => [], 'admin' => false, 'top' => false, 'priority' => null, 'if' => null, ]); # Must match current theme if (! empty($opts['theme']) && ! Theme::compare($opts['theme'])) { return; } $this->app->hooks()->cssExtra(!$opts['admin'], function () use ($file, $opts) { if ($opts['if'] !== null) { if ($opts['if'] instanceof Closure) { $opts['if'] = $opts['if']($file, $opts); } if (! $opts['if']) { return; } } $this->view->style($this->url($file . (mb_substr($file, -4) !== '.css' ? '.css' : '')), [ 'version' => $opts['version'] ?: $this->getStaticFileManifestVersion($file), 'top' => $opts['top'], ]); }, $opts['priority'] ?? null); } /** * Подключение css файла расширения в панели администатора * @param string $file название css файла * @param mixed $opts версия файла или [ * string|false 'version' - версия файла * string|array 'theme' - название темы, при активации которой следует подключать css файл * bool 'admin' - admin-контекст подключения * bool 'top' - форсируем подключение * int|null 'priority' - приоритет подключения * ] * @return void */ public function cssAdmin(string $file, $opts = false) { if (! is_array($opts)) { $opts = ['version' => $opts]; } $opts['admin'] = true; $this->css($file, $opts); } /** * Устанавливаем/получаем CSS файлы стилей доступные для редактирования в настройках * @param array|null $files список файлов, или вернуть текущие (null) * [ * 'key' => [ * 'path' => '/static/css/main.css', * 'save' => false(readonly) | true(src+custom) | 'custom'(custom only), * 'title' => 'main.css', * ], * ... * ] * @param bool $extend дополнить текущий список * @return array */ public function cssEdit(?array $files = null, bool $extend = false): array { if (is_array($files)) { $update = []; foreach ($files as $key => $file) { if (!is_string($key) || !is_array($file) || empty($file['path'])) { continue; } # key $file['key'] = $key; # save if (! isset($file['save']) || ! in_array($file['save'], [false, true, 'custom'], true)) { $file['save'] = true; } # readonly $file['readonly'] = ($file['save'] === false); # path: custom if (! $file['readonly'] && ! isset($file['path_custom'])) { $file['path_custom'] = $this->pathPublic(strtr($file['path'], [ $this->extension_path . 'static' . DS => DS, $this->extension_path => DS, ]), ['mod' => false, 'custom' => true]); } $update[$key] = $file; } if ($extend) { $this->extension_css_edit = array_merge($this->extension_css_edit, $update); } else { $this->extension_css_edit = $update; } } else { if ($this->isTheme() && $this->isChildTheme()) { $parentCSS = $this->getParentTheme()->cssEdit(); $mainKey = static::CSS_FILE_MAIN; if (isset($parentCSS[$mainKey])) { $this->extension_css_edit[$mainKey] = array_merge($parentCSS[$mainKey], [ 'priority' => -1, 'readonly' => true, 'save' => false, ]); } } } return $this->extension_css_edit; } /** * Загружаем/сохраняем содержимое CSS файла для редактирования * @param string $fileKey ключ файла * @param string|bool $save содержимое для сохранения, false - получить текущее содержимое * @return string|bool|\bff\http\Response содержимое файла или false ошибка */ public function cssEditContent(string $fileKey, $save = false) { if ($save !== false && is_string($save)) { if (empty($this->extension_css_edit[$fileKey]['path'])) { return false; } $file = $this->extension_css_edit[$fileKey]; if ($file['readonly']) { return false; } $paths = []; $paths[] = $file['path_custom']; if ($file['save'] === true) { $paths[] = $file['path']; } if (is_string($save)) { TextParser::cleanUtf8($save); } $success = false; foreach ($paths as $path) { if (! file_exists($path)) { $pathDir = dirname($path); if (! is_dir($pathDir)) { if (! Files::makeDir($pathDir, 0775, true)) { continue; } } } if (file_put_contents($path, $save) !== false) { $success = true; } } return $success; } else { $content = ''; do { if (empty($this->extension_css_edit[$fileKey]['path'])) { break; } $file = $this->extension_css_edit[$fileKey]; $path = $file['path']; if (!$file['readonly'] && !empty($file['path_custom']) && file_exists($file['path_custom'])) { $path = $file['path_custom']; } if (file_exists($path)) { if ($save === -1) { $attachmentName = str_replace(';', '', pathinfo($path, PATHINFO_BASENAME)); return Response::file($path, 200, [ 'Content-disposition' => 'attachment; filename="' . $attachmentName . '"', 'Content-Length' => filesize($path), ]); } $content = file_get_contents($path); if ($content !== false) { TextParser::cleanUtf8($content); } else { $content = ''; } } } while (false); return $content; } } /** * Mix Manifest * @param string $file * @param array $options [hashOnly, manifestFile] * @return string */ public function getStaticFileManifestVersion(string $file, array $options = []) { return $this->view->manifestVersion( $file, $options['manifestFile'] ?? $this->pathPublic('/' . View::MANIFEST_FILE, ['mod' => false]), $this->path('static/hot'), $options ); } /** * Обновление файлов статики расширения * @param bool $install * @return void */ public function refreshStatic(bool $install) { $src = $this->extension_path . 'static' . DS; $dst = $this->pathPublic('', ['mod' => false]); if ($install) { $this->installStatic($src, $dst, true); } else { $this->uninstallStatic($dst, $src, false); } } /** * Копирование файлов статики расширения * @param string $src исходный путь * @param string $dst конечный путь * @param bool $rewrite переписывать существующие * @return bool */ protected function installStatic(string $src, string $dst, bool $rewrite = true): bool { $mode = 0777; if (! File::isDirectory($src)) { return false; } if (! File::exists($dst)) { if (! File::makeDirectory($dst, $mode, true)) { $this->errors->set(_t('system', 'Error creating directory [path]', ['path' => $dst])); return false; } } $filter = $this->app->filter('extension.static.install.ignore', [ 'file' => ['.gitignore'], 'ext' => ['config','php'], 'dir' => ['src','.git','.svn'], ]); $iterator = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($src, RecursiveDirectoryIterator::SKIP_DOTS), function ($v) use ($filter) { if ($v->isFile()) { return !( in_array($v->getFilename(), $filter['file'], true) || in_array($v->getExtension(), $filter['ext'], true) ); } if ($v->isDir()) { return !in_array($v->getFilename(), $filter['dir'], true); } return false; } ), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $v) { $to = $dst . $iterator->getSubPathName(); if (($toExists = File::exists($to)) && !$rewrite) { continue; } if ($v->isDir()) { if (! File::exists($to) && ! File::makeDirectory($to, $mode)) { $this->errors->set(_t('system', 'Error creating directory [path]', ['path' => $to])); return false; } } elseif ($v->isLink()) { continue; } else { $modifyTime = File::lastModified($v); if ($toExists && (File::size($v) === File::size($to)) && ($modifyTime === File::lastModified($to))) { continue; } if (! File::copy($v, $to)) { $this->errors->set(_t('system', 'Error creating file [path]', ['path' => $to])); return false; } else { touch($to, $modifyTime); } } } return true; } /** * Удаление файлов статики расширения * @param string $dir директория для удаления * @param string $dirOriginal оригинальная директория файлов * @param bool $keepChanges не выполнять удаление измененных и новых файлов * @return bool */ protected function uninstallStatic(string $dir, string $dirOriginal, bool $keepChanges = true): bool { if (! File::isDirectory($dir)) { return true; } if (! File::isReadable($dir)) { $this->errors->set(_t('system', 'Error deleting directory [path]', ['path' => $dir])); return false; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); $filesSkip = false; foreach ($iterator as $v) { $file = $v->getRealPath(); if ($v->isDir()) { if ($filesSkip) { if (! File::isReadable($file)) { continue; } $handle = opendir($file); while (false !== ($entry = readdir($handle))) { if ($entry != '.' && $entry != '..') { continue 2; } } } if (! File::deleteDirectory($file)) { $this->errors->set(_t('system', 'Error deleting directory [path]', ['path' => $file])); } } else { if ($keepChanges) { $fileOriginal = $dirOriginal . $iterator->getSubPathName(); if (! File::exists($fileOriginal)) { $filesSkip = true; continue; } if (File::hash($fileOriginal) !== File::hash($file)) { $filesSkip = true; continue; } } if (! File::delete($file)) { $filesSkip = true; $this->errors->set(_t('system', 'Error deleting file [path]', ['path' => $file])); } } } if ($filesSkip) { $handle = opendir($dir); while (false !== ($entry = readdir($handle))) { if ($entry != '.' && $entry != '..') { return true; } } } if (! File::deleteDirectory($dir)) { $this->errors->set(_t('system', 'Error deleting directory [path]', ['path' => $dir])); } return true; } /** * Установка SQL из файла * @param string $file абсолютный путь к файлу * @return bool */ protected function installSqlFile(string $file): bool { if (! File::exists($file)) { return false; } $sql = File::get($file); if (empty($sql)) { return false; } $sql = str_replace('/*TABLE_PREFIX*/', DB_PREFIX, $sql); $res = $this->db->exec($sql); return ($res !== false); } /** * Содержимое файла инструкции расширения * Приоритет: * 1) {fileName}-{currentLanguage}.md * 2) {fileName}-{defaultLanguage}.md * 3) {fileName}.md * @param string $fileName имя файла без расширения * @return string|bool */ public function instructionFile(string $fileName = 'readme') { $fileNames = []; $fileNames[] = $fileName . '-' . $this->locale->getCurrentLanguage(); $fileNames[] = $fileName . '-' . $this->locale->getDefaultLanguage(); $fileNames[] = $fileName; foreach ($fileNames as $v) { $file = $this->path($v . '.md'); if (File::exists($file)) { return File::get($file); } } return false; } /** * Поиск перевода для фразы * @param string $message фраза * @param array|string|bool $params подстановочные данные, string - язык локализации, true - все локализации * @param bool $escape выполнять квотирование, false - не выполнять, 'html' (true), 'js' * @param string|bool|null $language язык локализации, null - текущий, true - все локализации * @param array $opts дополнительные настройки * @return string|array */ public function lang( string $message, $params = [], $escape = false, $language = null, array $opts = [] ) { # translate $message = $this->locale->translate($this->extension_name, $message, $params, $language, $opts); # escape if ($escape === false) { return $message; } return HTML::escape($message, ($escape === true ? 'html' : $escape)); } /** * Поиск перевода для фразы интерфейса расширения в админ. панели * @param string $message фраза * @param array|string|bool $params подстановочные данные, string - язык локализации, true - все локализации * @param bool $escape выполнять квотирование, false - не выполнять, 'html' (true), 'js' * @param string|bool|null $language язык локализации, null - текущий, true - все локализации * @param array $opts дополнительные настройки * @return string|array */ public function langAdmin( string $message, $params = [], $escape = false, $language = null, array $opts = [] ) { return $this->lang($message, $params, $escape, $language, $opts); } /** * Директория хранения файлов перевода * @param bool $source исходные файлы перевода * @return string */ public function getLangStorePath(bool $source = false): string { if ($source) { return $this->path('lang'); } return $this->pathUpload(false) . 'lang'; } /** * Язык расширения по-умолчанию, null - язык сайта по-умолчанию * @return string|null ключ языка: 'en', ... */ public function getLangDefault() { return null; } /** * Устанавливаем список языков для которых расширение доступно * @param array $languages ключи доступных языков или пустой список (доступны все языки) * @return void */ public function setLanguagesOnly(array $languages) { $this->extension_languages_only = $languages; } /** * Получаем список языков для которых расширение доступно * @return array */ public function getLanguagesOnly(): array { return $this->extension_languages_only; } /** * Проверяем допустимый ли язык для расширения * @param string|null $language * @param bool $frontendOnly выполнять проверку только для frontend-контекста * @return bool */ public function isLanguageAllowed(?string $language, bool $frontendOnly = true): bool { if (! empty($this->extension_languages_only) && ! empty($language)) { if ($frontendOnly && $this->isAdminPanel()) { return true; } return in_array($language, $this->extension_languages_only, true); } return true; } /** * Формирование полного названия метода вызываемого роутом * @param string|callable $callback название метода плагина * @param string $params дополнительные параметры получаемые из URL * @return string|callable */ public function routeAction($callback, string $params = '') { if (is_string($callback) && mb_stripos($callback, '::') === false) { return $this->getName() . '/' . $callback . '/' . $params; } else { return $callback; } } /** * Добавление роута * @param string $id идентификатор роута * @param string|array $pattern * @param string|array|\Closure|null $callback * @param array $settings * @return \bff\base\Route|null */ public function routeAdd(string $id, $pattern, $callback = null, array $settings = []) { if (is_array($pattern) && is_null($callback)) { $settings = $pattern; } else { $settings['pattern'] = $pattern; $settings['callback'] = $callback; } return $this->router->add($id, $settings); } /** * Формирование URL для вызова метода плагина * @param string $methodName название публичного метода плагина * @param array $options доп. параметры: controller, query, escape * @param bool|null $adminPanel контекст вызова null - текущий, true - admin panel, false - frontend * @return string */ public function urlAction(string $methodName, array $options = [], ?bool $adminPanel = null): string { $controller = $options['controller'] ?? $this->getName(); if ($adminPanel ?? $this->isAdminPanel()) { return Url::admin($controller . '/' . $methodName, $options['query'] ?? [], $options); } return Url::direct($controller, $methodName, $options['query'] ?? [], $options); } /** * Добавление пункта меню в админ. панели * @param string|array|Closure $group название основного раздела меню или массив ['title'=>название, 'icon'=>клас иконки] * @param string $title название пункта меню * @param string $method название метода плагина * @param bool $visible показывать пункт меню по-умолчанию * @param int|bool $priority приоритет, определяет порядок подразделов в пределах раздела * @param array $opts дополнительные настройки * @return \bff\Admin\Menu */ public function adminMenu( $group, string $title = '', string $method = '', $visible = true, $priority = false, array $opts = [] ) { $menu = $this->app->adminMenu(); if ($group instanceof Closure) { $this->app->hooks()->adminMenuBuild($group); return $menu; } if (empty($title) || empty($method)) { return $menu; } $icon = false; if (is_array($group)) { if (! empty($group['icon'])) { $icon = $group['icon']; } if (! empty($group['title'])) { $group = $group['title']; } } if (! is_scalar($group)) { $group = 'unknown'; } $menu ->group($group, $icon) ->add($title, $this->getName(), $method, $priority, $opts) ->visible($visible); return $menu; } /** * Регистрация блока расширения * @param string $id ID блока (уникальный ключ в пределах расширения) * @param callable $callback функция обработчик, отвечающая за отрисовку блока * @return \bff\extend\extension\Block объект блока */ public function blockAdd(string $id, callable $callback) { return ($this->extension_blocks[$id] = new extension\Block( $this, mb_strtolower($this->getName()) . '_' . $id, $callback )); } /** * Удаление блока расширения * @param string $id ID блока * @return bool */ public function blockRemove(string $id): bool { if (array_key_exists($id, $this->extension_blocks)) { return $this->extension_blocks[$id]->remove(); } return false; } /** * Список зарегистрированных блоков * @return \bff\extend\extension\Block[] */ public function blocksList() { return $this->extension_blocks; } /** * Формирование шаблона расширения * @param string $view * @param array $data * @param array $opts * @return string|mixed */ public function template(string $view, array $data = [], array $opts = []) { # context $opts['this'] = $opts['this'] ?? $this; # hook prefix $opts['hookPrefix'] = 'view.' . $this->extension_name; # view.{type}.{name} # extension $opts['extension'] = $this->isPlugin(); return $this->view->render( $data, $view, (!empty($opts['path']) ? $opts['path'] : ($this->isTheme() ? DS : $this->extension_path)), $opts ); } /** * Формирование php-шаблона расширения * @deprecated use template() * @param array $data @ref данные, которые необходимо передать в шаблон * @param string $view название шаблона, без расширения * @param string|null $path путь к шаблону или путь к директории текущего расширения (по умолчанию) * @param array $opts * @return string|mixed */ public function viewPHP(array &$data, string $view, ?string $path = null, array $opts = []) { if (!empty($path)) { $opts['path'] = $path; } return $this->template($view, $data, $opts); } /** * Единоразовый запуск задачи при ближайшем запуске крон менеджера * @param string $method название публичного метода дополнения * @param array $params параметры передаваемые в метод * @param array $opts доп. параметры: * bool 'time' - выполнить после указанного времени (int метка времени Unix) * string|bool 'multiple' разрешать запуск нескольких однотипных задач * @return bool */ public function cronExecuteOnce(string $method, array $params = [], array $opts = []): bool { $cron = $this->app->cronManager(); if ($cron->isEnabled()) { if ($this->isPlugin()) { $opts['plugin'] = true; } elseif ($this->isTheme()) { $opts['theme'] = true; } $multiple = (array_key_exists('multiple', $opts) ? $opts['multiple'] : false); return $cron->executeOnce($this->getName(), $method, $params, $multiple, $opts); } else { return false; } } }