module_title = _t('site', 'Settings'); } /** * Ссылка на системную настройку * @param string $module * @param string|array $opts * @param string $template * @return string */ public function settingsSystemLink($module, $opts = [], $template = '') { if (is_string($opts)) { $opts = ['sett' => $opts]; } func::array_defaults($opts, [ 'tab' => 'def', 'sett' => '', 'e' => true, 'local' => false, 'local-attr' => [], ]); $hash = '#' . strval($opts['tab']); if (! empty($opts['sett'])) { $hash .= ':' . $opts['sett']; } if (!empty($opts['local'])) { HTML::attributeAdd($opts['local-attr'], 'class', 'j-local-link'); return HTML::attributes(array_merge($opts['local-attr'], [ 'href' => HTML::escape($hash, $opts['e']), 'data-module' => strval($module), ])); } $link = tpl::adminLink('settingsSystemManager&tab=' . strval($module) . $hash, 'site', $opts['e']); if (!empty($template) && is_string($template)) { switch ($template) { case 'disabled': { $link = _t('site', 'This function is disabled in the System Settings and is not displayed to site users.', [ 'setting_link' => 'href="' . $link . '"', ]); } break; } } return $link; } /** * Форма редактирования системных настроек модуля * @param Module $module объект модуля * @param array $settings настройки * @param array $options @ref доп. настройки * @return string|array */ public function settingsSystemForm(Module $module, array $settings, array &$options = []) { if (empty($settings) && empty($options['tabs'])) { return ''; } # дополнительные настройки + фильтры (хуки) if (empty($options['extra'])) { $options['extra'] = []; } $options['extra'] = $this->app->filter('site.settings.system.extra.' . $module->module_name, $options['extra'], $module); if (!empty($options['extra']) && is_array($options['extra'])) { foreach ($options['extra'] as $k => $v) { if (!empty($k) && is_array($v) && isset($v['type']) && isset($v['default'])) { $settings[$k] = $v; } } } # табы $tabs = (!empty($options['tabs']) && is_array($options['tabs']) ? $options['tabs'] : []); if (! empty($settings)) { $tabs['def'] = $this->defaults($tabs['def'] ?? [], ['t' => _t('', 'General'), 'p' => 0]); } $theme = $this->app->theme(); if (!empty($options['dataOnly'])) { $data = []; foreach ($settings as $key => &$v) { if (empty($v['type']) || $v['type'] === TYPE_PASS || !isset($v['default'])) { continue; } if ($theme !== false && $theme->configExists($key)) { $data[$key] = $theme->config($key, $v['default']); continue; } $data[$key] = $this->config($key, $v['default'], $v['type']); } unset($v); $options['data'] = $data; return ''; } $fordev = false; foreach ($settings as $k => &$v) { if (! empty($v['fordev'])) { if (! $this->isAdminFordev()) { unset($settings[$k]); continue; } if (! $fordev) { $fordev = true; # добавляем таб "Разработчику" $tabs['fordev'] = ['t' => '', 'p' => 1000]; } $v['tab'] = 'fordev'; } if (!isset($v['tab']) || empty($v['tab']) || !isset($tabs[$v['tab']])) { # таб по умолчанию $v['tab'] = 'def'; } } unset($v); # сортируем табы в порядке priority if (sizeof($tabs) > 1) { $priorityOrder = []; $i = 1; foreach ($tabs as $k => $v) { $priorityOrder[$k] = $v['p'] ?? $i; $i++; } array_multisort($priorityOrder, SORT_ASC, $tabs); } $data = [ 'module' => $module, 'settings' => &$settings, 'options' => &$options, 'tabs' => &$tabs, 'theme' => $theme, ]; return $this->template('admin/settings.sys.form', $data, [ 'path' => $this->module_dir_tpl_core, ]); } /** * Получение данных, для формирования системных настроек * @param array $opts * @return array */ public function settingsSystemManagerData($opts = []) { $data = ['tabs' => []]; $modulesList = $this->app->getModulesList(); foreach (['test'] as $v) { if (isset($modulesList[$v])) { unset($modulesList[$v]); } } $indexes = []; $uniques = function ($name) use (&$indexes) { if (! isset($indexes[$name])) { $indexes[$name] = 0; return $name; } $indexes[$name]++; return $name . $indexes[$name]; }; $tabsMethod = 'settingsSystemTabs'; $defaultCallMethod = 'settingsSystem'; $i = 30; foreach ($modulesList as $moduleName => $moduleParams) { $module = $this->app->module($moduleName); $callMethods = []; if (method_exists($module, $tabsMethod)) { $callMethods = call_user_func([$module, $tabsMethod]); } if (empty($callMethods) || ! is_array($callMethods)) { $callMethods = [$moduleName => $defaultCallMethod]; } foreach ($callMethods as $tabName => $method) { if (! method_exists($module, $method)) { continue; } $options = $opts; $options['name'] = $tabName; $form = $module->$method($options); $name = $uniques($options['name'] ?? $tabName); $data['tabs'][$name] = [ 'name' => $name, 'title' => (!empty($options['title']) ? $options['title'] : $module->module_title), 'form' => $form, 'module' => $moduleName, 'priority' => (!empty($options['priority']) ? $options['priority'] : $i++), 'data' => $options['data'] ?? [], 'formTabs' => $options['tabs'] ?? [], ]; } } return $data; } /** * Получение всех системных настроек и их значений * @return array */ public function settingsSystemData() { $data = $this->settingsSystemManagerData(['dataOnly' => true]); $forms = []; $result = []; foreach ($data['tabs'] as $k => $v) { if (! empty($v['data'])) { $result = array_merge($result, $v['data']); } if (! empty($v['formTabs'])) { foreach ($v['formTabs'] as $vv) { if (! empty($vv['form'])) { $forms[] = ['m' => $v['module'], 'f' => $vv['form']]; } } } } foreach ($forms as $v) { $module = $this->app->module($v['m']); $form = tplAdmin::settingsForm($module, 'admin/settings.' . $v['f'] . '.form', ['action' => 'settingsSystemAjaxForm']); $data = $form->settingsSystemData(); if (! empty($data)) { $result = array_merge($result, $data); } } return $result; } /** * Системные настройки: настройки системы * @param array $extend @ref * @param string $tab */ public function settingsSystemSystem(&$extend = [], $tab = 'system') { if (! isset($extend['settings'])) { $extend['settings'] = []; } $languagesList = $this->locale->getLanguages(false); if (sizeof($languagesList) >= 1) { $extend['settings']['locale.accepted.languages'] = [ 'title' => _t('site', 'Language auto-detection'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => true, 'options' => [ true => ['title' => _t('', 'Enabled'), 'description' => _t('site', 'Autodetect localization when the user first visits the site')], false => ['title' => _t('', 'Disabled')], ], 'tab' => $tab, ]; } $extend['settings']['date.timezone'] = [ 'title' => _t('site', 'Time Zone'), 'type' => TYPE_STR, 'input' => 'select', 'default' => 'Europe/Kiev', 'options' => function () { $list = timezone_identifiers_list(); $options = []; foreach ($list as $v) { $options[$v] = ['title' => $v]; } return $options; }, 'tab' => $tab, ]; if ($this->app->request()->scheme(true) == 'https' || $this->config('https.only')) { $extend['settings']['https.only'] = [ 'title' => _t('site', 'HTTPS Mode'), 'children' => ['https.redirect'], 'type' => TYPE_BOOL, 'input' => 'select', 'default' => false, 'options' => [ true => ['title' => _t('', 'Enabled')], false => ['title' => _t('', 'Disabled')], ], 'tab' => $tab, ]; $extend['settings']['https.redirect'] = [ 'title' => _t('site', 'HTTP Redirect'), 'title-tip' => _t('site', 'Redirect from the HTTP version of the site to HTTPS'), 'parent' => ['id' => 'https.only', 'value' => true], 'type' => TYPE_BOOL, 'input' => 'select', 'default' => false, 'options' => [ true => ['title' => _t('', 'Enabled')], false => ['title' => _t('', 'Disabled')], ], 'tab' => $tab, ]; } $statisticURL = ''; # todo: mp3 docs if (BFF_PRODUCT === 'do') { $statisticURL = 'https://tamaranga.com/docs/doska/site-system-settings-28-189.html#p2'; } elseif (BFF_PRODUCT === 'freelance') { $statisticURL = 'https://tamaranga.com/docs/freelance/settings-system-manager-55-269.html#p2'; } elseif (BFF_PRODUCT === 'city') { $statisticURL = 'https://tamaranga.com/docs/portal/site-system-settings-75-287.html#p2'; } if (! empty($statisticURL)) { $extend['settings']['hh.stat.allowed'] = [ 'title' => _t('site', 'System operation statistics'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => true, 'options' => [ true => ['title' => _t('', 'Enabled'), 'description' => _t('site', 'Allow collection and subsequent analysis [a]of system performance statistics[/a]', [ 'a' => '', '/a' => '', ]), ], false => ['title' => _t('', 'Disabled')], ], 'tab' => $tab, 'onchange' => 'site/changeStatAllowed/', ]; } if ($this->isAdminFordev()) { $size = config::get('site.cache.size', '{}'); $size = json_decode($size, true); $size = ! empty($size) && isset($size['size']) ? tpl::filesize($size['size']) : tpl::filesize(0); $resetScript = ''; $extend['settings']['site.cache.reset'] = [ 'title' => _t('site', 'Site Cache'), 'input' => 'html', 'content' => $size . ' ' . $resetScript, 'tab' => $tab, ]; } } /** * Системные настройки: локализация * @param array $extend @ref * @param string $tab */ public function settingsSystemLocale(&$extend = [], $tab = 'locale') { if (! isset($extend['settings'])) { $extend['settings'] = []; } $languagesList = $this->locale->getLanguages(false); if (sizeof($languagesList) >= 1) { $extend['settings']['locale.accepted.languages'] = [ 'title' => _t('site', 'Language auto-detection'), 'type' => TYPE_BOOL, 'input' => 'select', 'default' => true, 'options' => [ true => [ 'title' => _t('', 'Enabled'), 'description' => _t('site', 'Autodetect localization when the user first visits the site'), ], false => ['title' => _t('', 'Disabled')], ], 'tab' => $tab, ]; } } /** * Get counters & code to view * @param int|null $position * @return array */ public function countersAndCode(?int $position = null) { return $this->app->filter('site.counters.view', $this->model->countersView($position), $position); } /** * Add site counters to layout * @param Layout $layout * @param array|null $counters * @return void */ public function countersToLayout(Layout $layout, ?array $counters = null) { $counters = $counters ?? $this->model->countersViewByPosition(); if (empty($counters)) { return; } if (! empty($counters[static::COUNTERS_POS_HEAD])) { $layout->afterHead(function () use (&$counters) { $code = ''; foreach ($counters[static::COUNTERS_POS_HEAD] as $data) { $code .= $data['code']; } return $code; }); } if (! empty($counters[static::COUNTERS_POS_BODY_START])) { $layout->beforeBody(function () use (&$counters) { $code = ''; foreach ($counters[static::COUNTERS_POS_BODY_START] as $data) { $code .= $data['code']; } return $code; }); } if (! empty($counters[static::COUNTERS_POS_BODY_FINISH])) { $layout->afterBody(function () use (&$counters) { $code = ''; foreach ($counters[static::COUNTERS_POS_BODY_FINISH] as $data) { $code .= $data['code']; } return $code; }); } } /** * Получаем данные об основной валюте * @deprecated use {@see Currency::default()} * @param string|bool $valueKey ключ требуемых данных, false - все данные * @return mixed */ public function currencyDefault($valueKey = 'title_short') { if ($valueKey == 'id') { return $this->config('currency.default', 1, TYPE_UINT); } return $this->currencyData(0, $valueKey); } /** * Получаем данные о необходимой валюте * @deprecated use {@see Currency::data()} * @param int $currencyID ID валюты, если 0 - возвращаем данные об основной валюте * @param string|bool $valueKey ключ требуемых данных, false - все данные * @param bool $enabledOnly только включенные валюты * @return mixed */ public function currencyData($currencyID = 0, $valueKey = false, $enabledOnly = true) { $data = $this->model->currencyData(false, false, $enabledOnly); if (empty($currencyID)) { $currencyID = $this->currencyDefault('id'); } if (!isset($data[$currencyID])) { return false; } return ($valueKey !== false && isset($data[$currencyID][$valueKey]) ? $data[$currencyID][$valueKey] : $data[$currencyID] ); } /** * Инициализация компонента Publicator для статических страниц * @return \bff\db\Publicator */ public function pagesPublicator() { $aSettings = [ 'title' => false, 'langs' => $this->locale->getLanguages(), 'images_path' => $this->app->path('pages', 'images'), 'images_path_tmp' => $this->app->path('tmp', 'images'), 'images_url' => $this->app->url('pages', 'images'), 'images_url_tmp' => $this->app->url('tmp', 'images'), 'photo_sz_view' => ['width' => 960], 'images_original' => true, // gallery 'gallery_sz_view' => [ 'width' => 960, 'height' => false, 'vertical' => ['width' => false, 'height' => 640], 'quality' => 95, 'sharp' => [] ], // no sharp ]; if ($this->pagesPublicatorEnabled()) { $configSettings = $this->config('pages.publicator.settings', []); if (!empty($configSettings) && is_array($configSettings)) { $aSettings = array_merge($aSettings, $configSettings); } } return $this->attachComponent('publicator', new Publicator($this->module_name, $aSettings)); } /** * Использовать Publicator для статических страниц * @return bool|int */ public function pagesPublicatorEnabled() { return $this->config('pages.publicator', false); # bool или int } /** * Расширения для статических страниц * @return string */ public function pagesUrlExtension() { return $this->config('pages.extension', '.html'); } /** * Защита от спама (частой отправки сообщений / выполнения действий) * @param string $key ключ выполняемого действия * @param int $timeout допустимая частота выполнения действия, в секундах * @param bool $setError устанавливать ошибку * @param array $opts * @return bool true - частота отправки превышает допустимый лимит, false - все ок */ public function preventSpam($key, $timeout = 20, $setError = true, array $opts = []) { if ($this->app->hooksAdded('site.prevent.spam')) { $hook = $this->app->filter('site.prevent.spam', [ 'key' => &$key, 'timeout' => &$timeout, 'setError' => &$setError, 'opts' => &$opts, ]); if (isset($hook['result'])) { return $hook['result']; } } elseif ($this->app->isTest()) { return false; } $timeout = intval($timeout); if ($timeout <= 0) { return false; } $userID = $opts['userId'] ?? User::id(); $ipAddress = $opts['ip'] ?? $this->request->remoteAddress(); $last = $this->model->requestGet($key, $userID, $ipAddress, false); if ($last > 0 && ((BFF_NOW - $last) < $timeout)) { if ($setError) { if ($timeout <= 70) { $this->errors->set(_t('', 'Please try again in one minute')); } else { $this->errors->set(_t('', 'Please try again in a few minutes')); } } return true; } else { $this->model->requestSet($key, $userID, $ipAddress); } return false; } /** * Защита от спама на основе допустимого кол-ва повторов выполненного действия * В случае привышения этого кол-ва включаем режим ожидание ($timeout) * @param string $key ключ выполняемого действия * @param int $limit допустимое кол-во повторов * @param int $timeout таймаут ожидания (cooldown) при достижении лимита попыток, в секундах * @param bool $setError устанавливать ошибку * @param array $opts * @return bool true - достигнут лимит повторов, false - все ок */ public function preventSpamCounter($key, $limit, $timeout = 20, $setError = true, array $opts = []) { if ($this->app->hooksAdded('site.prevent.spam.counter')) { $hook = $this->app->filter('site.prevent.spam.counter', [ 'key' => &$key, 'limit' => &$limit, 'timeout' => &$timeout, 'setError' => &$setError, 'opts' => &$opts, ]); if (isset($hook['result'])) { return $hook['result']; } } elseif ($this->app->isTest()) { return false; } $limit = intval($limit); $timeout = intval($timeout); if ($limit <= 0 || $timeout <= 0) { return false; } $userID = $opts['userId'] ?? User::id(); $ipAddress = $opts['ip'] ?? $this->request->remoteAddress(); $last = $this->model->requestGet($key, $userID, $ipAddress, true); # первое выполнение действия if (empty($last)) { $this->model->requestSet($key, $userID, $ipAddress, 1); return false; } $filter = ['user_action' => $key]; if ($userID) { $filter['user_id'] = $userID; } else { $filter['user_ip'] = $ipAddress; } $counter = intval($last['counter']); if ($counter < $limit) { # повтор: лимит не достигнут $this->model->requestUpdate($filter, [ 'counter' => $counter + 1, 'created' => $this->db->now(), ]); return false; } elseif ($counter === $limit) { # повтор: лимит достигнут # пропускаем + помечаем период ожидания для последующих попыток $this->model->requestUpdate($filter, [ 'counter' => $counter + 1, 'created' => date('Y-m-d H:i:s', strtotime('+ ' . $timeout . ' seconds')), ]); return false; } else { # период ожидания: просим подождать if (strtotime($last['created']) > BFF_NOW) { if ($setError) { if ($timeout <= 70) { $this->errors->set(_t('', 'Please try again in one minute')); } else { $this->errors->set(_t('', 'Please try again in a few minutes')); } } return true; } else { # завершаем период ожидания, сбрасываем счетчик попыток $this->model->requestUpdate($filter, [ 'counter' => 1, 'created' => $this->db->now(), ]); return false; } } } /** * Формирование URL для работы с сайтом в выключенном режиме * @param bool $secretOnly только ключ * @return string */ public function offlineUrl($secretOnly = false) { $secret = hash('sha256', $this->config('crypt.key', '') . $this->config('site.title')); if ($secretOnly) { return $secret; } return $this->router->url('index', ['offline' => $secret]); } /** * Обработка копирования данных локализации * @param string $from ключ языка * @param string $to ключ языка */ public function onLocaleDataCopy($from, $to) { # настройки сайта $configUpdate = []; $fromPostfixLen = mb_strlen('_' . $from); foreach (config::all() as $k => $v) { if (mb_strrpos($k, '_' . $from) === mb_strlen($k) - $fromPostfixLen) { $configUpdate[mb_substr($k, 0, -$fromPostfixLen) . '_' . $to] = $v; } } config::save($configUpdate); } /** * Формирование списка директорий/файлов требующих проверки на наличие прав записи * @return array */ public function writableCheck() { $dirs = [ $this->app->path('tmp', 'images') => 'dir-only', # временная директория загрузки изображений $this->app->path('images') => 'dir-only', # /public_html/files/images/ ]; $tmp1 = ini_get('upload_tmp_dir'); if (!empty($tmp1)) { $dirs[$tmp1] = ['type' => 'dir-only', 'title' => _t('site', 'temporary files directory during upload')]; # временная директория загрузки файлов } $tmp2 = sys_get_temp_dir(); if (!empty($tmp2) && $tmp1 !== $tmp2) { $dirs[$tmp2] = ['type' => 'dir-only', 'title' => _t('site', 'temporary files directory')]; # директория tmp файлов } if ($this->pagesPublicatorEnabled()) { $dirs[$this->app->path('pages', 'images')] = 'dir-only'; # статические страницы: публикатор } # файлы локализации $dirs[$this->locale->gettextDomain('path')] = 'file'; # domain.php foreach ($this->locale->getLanguages(true) as $lng) { $dirs[$this->locale->gettextPath($lng)] = 'dir-only'; # файлы переводов } # минимизация статики: js, css $dirs[$this->app->path('min')] = ['type' => 'dir+files', 'title' => _t('site', 'Directory for minified js/css')]; # плагины: $dirs[$this->app->pluginsPath()] = ['type' => 'dir-only']; $dirs[$this->app->publicPath('plugins')] = ['type' => 'dir-only']; # темы: $dirs[$this->app->themesPath()] = ['type' => 'dir-only']; $dirs[$this->app->publicPath('themes')] = ['type' => 'dir-only']; # custom $dirs[$this->app->publicPath('custom')] = ['type' => 'dir-only']; # расширения: настройки + изображения $dirs[$this->app->path('extensions', 'images')] = 'dir-only'; # расширения: файлы $dirs[$this->app->path('extensions')] = 'dir-only'; $dirs[$this->app->basePath('files/extensions')] = 'dir-only'; # vendor $dirs[$this->app->basePath('vendor')] = 'dir-only'; return array_merge(parent::writableCheck(), $dirs); } /** * Очистка директорий временных файлов * @param array $dirs полные путь директорий временных файлов * @param int $days кол-во дней от момента загрузки файлов, спустя которые их необходимо удалять */ public function temporaryDirsCleanup(array $dirs, $days = 3) { $days = ($days < 1 ? 1 : intval($days)) * 86400; $time = time(); foreach ($dirs as $v) { $files = Files::getFiles($v); foreach ($files as $f) { if ((filectime($f) + $days) < $time) { @unlink($f); } } } } /** * Favicon * @param array $options доп. параметры * @return string */ public function favicon(array $options = []): string { $list = $this->app->filter('site.favicon.list', [ 'ico' => ['rel' => 'icon', 'href' => $this->app->url('/favicon.ico'), 'type' => 'image/x-icon'], ]); $html = ''; if (!empty($list)) { foreach ($list as $v) { if (!empty($v['href'])) { $html .= ''; } } } return $html; } /** * Название сайта * Фильтр: "site.title.{language}" * @param string $position позиция вывода заголовка (не указана - '') * @param string $language ключ языка заголовка (не указан - текущий) * @param string $default заголовок по-умолчанию (не указан - SITEHOST) * @return string */ public function title($position = '', $language = '', $default = SITEHOST): string { if (empty($language)) { $language = $this->locale->getCurrentLanguage(); } $result = $this->config('site.title', $default); switch ($position) { case 'seo.template.macros': { $result = $this->config('site.title.seo', [], TYPE_ARRAY)[$language] ?? $default; } break; case 'sendmail.template.macros': { $result = $this->config('site.title.sendmail', [], TYPE_ARRAY)[$language] ?? $default; } break; } return (string)$this->app->filter('site.title', $result, $position, $language, $default); } /** * Заголовок сайта в шапке * Фильтр: "site.title.header.{language}", "site.title.header.admin.{language}" * @param string $position позиция вывода заголовка (не указана - '') * @param bool|null $adminPanel для админ. панели (null - определять по контексту) * @param string|null $language ключ языка заголовка (null - текущий) * @param string $default заголовок по-умолчанию * @return string */ public function titleHeader($position = 'header', $adminPanel = null, $language = null, $default = '') { if (is_null($adminPanel)) { $adminPanel = $this->isAdminPanel(); } if (empty($language)) { $language = $this->locale->current(); } if ($adminPanel) { return (string)$this->app->filter('site.title.header.admin', $this->config('site.title.header.admin', [], TYPE_ARRAY)[$language] ?? $default, $position, $adminPanel, $language, $default); } else { return (string)$this->app->filter('site.title.header', $this->config('site.title.header', [], TYPE_ARRAY)[$language] ?? $default, $position, $adminPanel, $language, $default); } } /** * Формирование текста копирайта * @param array $options доп. параметры * @return string */ public function copyright(array $options = []) { if (empty($options['lng'])) { $options['lng'] = $this->locale->current(); } return strtr($this->app->filter('site.copyright.text', $this->config('site.copyright.text', [], TYPE_ARRAY)[ $options['lng'] ] ?? '', $options), [ '{year}' => date('Y'), ]); } /** * Список доступных языков сайта для переключения * @param bool $adminPanel для вывода в админ. панели * @param array $options доп. параметры * @return array */ public function languagesList($adminPanel = false, array $options = []) { $exclude = (!$adminPanel ? $this->locale->getHiddenLocalesList() : []); $list = $this->app->filter('site.languages.list', $this->locale->getLanguages(false, $exclude), $options); if (is_array($list)) { func::sortByPriority($list); } return $list; } /** * Переключатель языка * @param array $options доп.параметры * @param string|null $currentLanguage * @return string HTML */ public function languagesSwitcher(array $options = [], ?string $currentLanguage = null) { static $i = 1; $data = []; $data['prefix'] = 'language-' . ($i++); $data['options'] = $options; $data['lang'] = $currentLanguage ?? $this->locale->current(); $data['languages'] = $this->languagesList(); foreach ($data['languages'] as $k => &$v) { $v['active'] = ($k == $data['lang']); $v['url'] = $this->app->urlLocaleChange($k); } unset($v); $data['template'] = (!empty($options['template']) ? $options['template'] : 'languages'); $data = $this->app->filter('site.languages.switcher', $data, $options); return $this->view->template($data['template'], $data); } /** * Menu to render (current theme & language) * @param string $id * @param string|null $template * @param bool $force * @return \bff\view\Menu */ public function menu(string $id, ?string $template = null, bool $force = false) { if (! array_key_exists($id, $this->menusRegistered) || $force) { $theme = $this->app->theme(); if (empty($this->menusRegistered)) { $this->menusRegister($theme); } $menu = $theme->menu($id); $settings = $this->menuSettings($id); foreach ($settings as $k => $v) { if (empty($v['is_system'])) { continue; } if ($menu->get($k) === null) { unset($settings[$k]); } } $menu->extend($settings); $this->app->hook('site.menu.' . $id, $menu); $this->menusRegistered[$id] = $menu; } else { $menu = $this->menusRegistered[$id]; } if ($template) { $menu->setTemplate($template); } return $menu; } /** * Register menus in modules & extensions * @param \Theme $theme * @return void */ protected function menusRegister($theme) { # Modules foreach ($this->app->getModulesList() as $module) { if ($file = $this->view->resolveFileInPath('menu.php', $module['path'])) { require $file; } } # Plugins & Addons foreach (Dev::getPluginsList() as $plugin) { if (! $plugin->isActive()) { continue; } if ($file = $this->view->resolveFileInPath('menu.php', $plugin->module_dir)) { require $file; } else { $plugin->menu($theme); } } } /** * Menu admin settings * @param string $id * @param string|null $lang * @return \bff\view\MenuItem[] */ protected function menuSettings(string $id, ?string $lang = null) { $lang = $lang ?? $this->locale->current(); $settings = Cache::rememberForever('site:menu:' . $id . ':' . $lang, function () use ($id, $lang) { $records = $this->model->menuListing( ['group_key' => $id], ['orderBy' => 'num', 'lang' => $lang] ); $settings = []; foreach ($records as $item) { # System created items (modules/extensions) if ($item['is_system']) { $settings[$item['item_key']] = $item; continue; } # Admin created items do { if ($item['type'] != static::MENU_TYPE_ITEM) { break; } if (! empty($item['pid'])) { break; } if (empty($item['title'])) { break; } if (empty($item['url'])) { break; } $data = []; foreach (['title', 'target', 'url', 'style', 'icon', 'num', 'enabled'] as $key) { $data[$key] = $item[$key]; } $settings[$item['id']] = $data; } while (false); } return $settings; }); # Macros # currenty available for admin items only foreach ($settings as $key => $item) { if (empty($item['is_system'])) { $settings[$key]['url'] = $this->menuMacrosReplace($item['url'], $lang); } } return $settings; } /** * Reset all menus cache */ public function menusResetCache() { $theme = $this->app->theme(); $this->menusRegister($theme); $languages = $this->locale->getLanguages(); foreach ($theme->menus() as $menu) { foreach ($languages as $lang) { Cache::delete('site:menu:' . $menu->id() . ':' . $lang); } } } /** * Замена макросов меню в строке * @param string $string * @param string|null $lang * @return string */ protected function menuMacrosReplace($string, ?string $lang = null) { return strtr($string, [ static::MENU_MACROS_SITEURL => $this->getMenuMacrosReplacement(static::MENU_MACROS_SITEURL, $lang), Url::HOST_PLACEHOLDER => $this->getMenuMacrosReplacement(Url::HOST_PLACEHOLDER, $lang), ]); } /** * Получаем замену макроса меню по его ключу * @param string $key ключ макроса * @param string|null $lang ключ языка * @return string */ protected function getMenuMacrosReplacement($key, ?string $lang = null) { switch ($key) { case static::MENU_MACROS_SITEURL: return SITEURL; case Url::HOST_PLACEHOLDER: $lang = $lang ?? $this->locale->current(); return SITEHOST . $this->locale->getLanguageUrlPrefix($lang, false); } return $key; } }