* @git Joomla Component Builder * @copyright Copyright (C) 2015 Vast Development Method. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace VDM\Component\Componentbuilder\Administrator\Model; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\HTML\HTMLHelper as Html; use Joomla\CMS\Session\Session; use Joomla\CMS\Uri\Uri; use Joomla\CMS\Application\CMSApplicationInterface; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\MVC\Model\ListModel; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Router\Route; use Joomla\CMS\User\User; use Joomla\Utilities\ArrayHelper; use Joomla\Input\Input; use VDM\Component\Componentbuilder\Administrator\Helper\ComponentbuilderHelper; use VDM\Joomla\Utilities\ArrayHelper as UtilitiesArrayHelper; use VDM\Joomla\Utilities\StringHelper; // No direct access to this file \defined('_JEXEC') or die; /** * Componentbuilder List Model * * @since 1.6 */ class ComponentbuilderModel extends ListModel { /** * Represents the current user object. * * @var User The user object representing the current user. * @since 3.2.0 */ protected User $user; /** * View groups of this component * * @var array * @since 5.1.1 */ protected array $viewGroups = [ 'main' => ['png.compiler', 'png.joomla_components', 'png.joomla_modules', 'png.joomla_plugins', 'png.powers', 'png.search', 'png.admin_views', 'png.custom_admin_views', 'png.site_views', 'png.template.add', 'png.templates', 'png.layouts', 'png.dynamic_get.add', 'png.dynamic_gets', 'png.custom_codes', 'png.placeholders', 'png.libraries', 'png.snippets', 'png.validation_rules', 'png.field.add', 'png.fields', 'png.fields.catid_qpo0O0oqp_com_componentbuilder_po0O0oq_field', 'png.fieldtypes', 'png.fieldtypes.catid_qpo0O0oqp_com_componentbuilder_po0O0oq_fieldtype', 'png.language_translations', 'png.languages', 'png.servers', 'png.repositories', 'png.help_documents'], ]; /** * View access array. * * @var array * @since 5.1.1 */ protected array $viewAccess = [ 'compiler.submenu' => 'compiler.submenu', 'compiler.dashboard_list' => 'compiler.dashboard_list', 'search.access' => 'search.access', 'search.submenu' => 'search.submenu', 'search.dashboard_list' => 'search.dashboard_list', 'joomla_component.create' => 'joomla_component.create', 'joomla_components.access' => 'joomla_component.access', 'joomla_component.access' => 'joomla_component.access', 'joomla_components.submenu' => 'joomla_component.submenu', 'joomla_components.dashboard_list' => 'joomla_component.dashboard_list', 'joomla_module.create' => 'joomla_module.create', 'joomla_modules.access' => 'joomla_module.access', 'joomla_module.access' => 'joomla_module.access', 'joomla_modules.submenu' => 'joomla_module.submenu', 'joomla_modules.dashboard_list' => 'joomla_module.dashboard_list', 'joomla_plugin.create' => 'joomla_plugin.create', 'joomla_plugins.access' => 'joomla_plugin.access', 'joomla_plugin.access' => 'joomla_plugin.access', 'joomla_plugins.submenu' => 'joomla_plugin.submenu', 'joomla_plugins.dashboard_list' => 'joomla_plugin.dashboard_list', 'joomla_power.create' => 'joomla_power.create', 'joomla_powers.access' => 'joomla_power.access', 'joomla_power.access' => 'joomla_power.access', 'joomla_powers.submenu' => 'joomla_power.submenu', 'power.create' => 'power.create', 'powers.access' => 'power.access', 'power.access' => 'power.access', 'powers.submenu' => 'power.submenu', 'powers.dashboard_list' => 'power.dashboard_list', 'admin_view.create' => 'admin_view.create', 'admin_views.access' => 'admin_view.access', 'admin_view.access' => 'admin_view.access', 'admin_views.submenu' => 'admin_view.submenu', 'admin_views.dashboard_list' => 'admin_view.dashboard_list', 'custom_admin_views.access' => 'custom_admin_view.access', 'custom_admin_view.access' => 'custom_admin_view.access', 'custom_admin_views.submenu' => 'custom_admin_view.submenu', 'custom_admin_views.dashboard_list' => 'custom_admin_view.dashboard_list', 'site_views.access' => 'site_view.access', 'site_view.access' => 'site_view.access', 'site_views.submenu' => 'site_view.submenu', 'site_views.dashboard_list' => 'site_view.dashboard_list', 'templates.access' => 'template.access', 'template.access' => 'template.access', 'templates.submenu' => 'template.submenu', 'templates.dashboard_list' => 'template.dashboard_list', 'template.dashboard_add' => 'template.dashboard_add', 'layouts.access' => 'layout.access', 'layout.access' => 'layout.access', 'layouts.submenu' => 'layout.submenu', 'layouts.dashboard_list' => 'layout.dashboard_list', 'dynamic_get.create' => 'dynamic_get.create', 'dynamic_gets.access' => 'dynamic_get.access', 'dynamic_get.access' => 'dynamic_get.access', 'dynamic_gets.submenu' => 'dynamic_get.submenu', 'dynamic_gets.dashboard_list' => 'dynamic_get.dashboard_list', 'dynamic_get.dashboard_add' => 'dynamic_get.dashboard_add', 'custom_code.create' => 'custom_code.create', 'custom_codes.access' => 'custom_code.access', 'custom_code.access' => 'custom_code.access', 'custom_codes.submenu' => 'custom_code.submenu', 'custom_codes.dashboard_list' => 'custom_code.dashboard_list', 'class_extends.create' => 'class_extends.create', 'class_extendings.access' => 'class_extends.access', 'class_extends.access' => 'class_extends.access', 'class_property.create' => 'class_property.create', 'class_properties.access' => 'class_property.access', 'class_property.access' => 'class_property.access', 'class_method.create' => 'class_method.create', 'class_methods.access' => 'class_method.access', 'class_method.access' => 'class_method.access', 'placeholder.create' => 'placeholder.create', 'placeholders.access' => 'placeholder.access', 'placeholder.access' => 'placeholder.access', 'placeholders.submenu' => 'placeholder.submenu', 'placeholders.dashboard_list' => 'placeholder.dashboard_list', 'library.create' => 'library.create', 'libraries.access' => 'library.access', 'library.access' => 'library.access', 'libraries.submenu' => 'library.submenu', 'libraries.dashboard_list' => 'library.dashboard_list', 'snippets.access' => 'snippet.access', 'snippet.access' => 'snippet.access', 'snippets.submenu' => 'snippet.submenu', 'snippets.dashboard_list' => 'snippet.dashboard_list', 'validation_rule.create' => 'validation_rule.create', 'validation_rules.access' => 'validation_rule.access', 'validation_rule.access' => 'validation_rule.access', 'validation_rules.submenu' => 'validation_rule.submenu', 'validation_rules.dashboard_list' => 'validation_rule.dashboard_list', 'field.create' => 'field.create', 'fields.access' => 'field.access', 'field.access' => 'field.access', 'fields.submenu' => 'field.submenu', 'fields.dashboard_list' => 'field.dashboard_list', 'field.dashboard_add' => 'field.dashboard_add', 'fieldtype.create' => 'fieldtype.create', 'fieldtypes.access' => 'fieldtype.access', 'fieldtype.access' => 'fieldtype.access', 'fieldtypes.submenu' => 'fieldtype.submenu', 'fieldtypes.dashboard_list' => 'fieldtype.dashboard_list', 'language_translation.create' => 'language_translation.create', 'language_translations.access' => 'language_translation.access', 'language_translation.access' => 'language_translation.access', 'language_translations.submenu' => 'language_translation.submenu', 'language_translations.dashboard_list' => 'language_translation.dashboard_list', 'language.create' => 'language.create', 'languages.access' => 'language.access', 'language.access' => 'language.access', 'languages.submenu' => 'language.submenu', 'languages.dashboard_list' => 'language.dashboard_list', 'server.create' => 'server.create', 'servers.access' => 'server.access', 'server.access' => 'server.access', 'servers.submenu' => 'server.submenu', 'servers.dashboard_list' => 'server.dashboard_list', 'repository.create' => 'repository.create', 'repositories.access' => 'repository.access', 'repository.access' => 'repository.access', 'repositories.submenu' => 'repository.submenu', 'repositories.dashboard_list' => 'repository.dashboard_list', 'help_document.create' => 'help_document.create', 'help_documents.access' => 'help_document.access', 'help_document.access' => 'help_document.access', 'help_documents.submenu' => 'help_document.submenu', 'help_documents.dashboard_list' => 'help_document.dashboard_list', 'admin_fields.create' => 'admin_fields.create', 'admins_fields.access' => 'admin_fields.access', 'admin_fields.access' => 'admin_fields.access', 'admin_fields_conditions.create' => 'admin_fields_conditions.create', 'admins_fields_conditions.access' => 'admin_fields_conditions.access', 'admin_fields_conditions.access' => 'admin_fields_conditions.access', 'admin_fields_relations.create' => 'admin_fields_relations.create', 'admins_fields_relations.access' => 'admin_fields_relations.access', 'admin_fields_relations.access' => 'admin_fields_relations.access', 'admin_custom_tabs.create' => 'admin_custom_tabs.create', 'admins_custom_tabs.access' => 'admin_custom_tabs.access', 'admin_custom_tabs.access' => 'admin_custom_tabs.access', 'component_admin_views.create' => 'component_admin_views.create', 'components_admin_views.access' => 'component_admin_views.access', 'component_admin_views.access' => 'component_admin_views.access', 'component_site_views.create' => 'component_site_views.create', 'components_site_views.access' => 'component_site_views.access', 'component_site_views.access' => 'component_site_views.access', 'component_custom_admin_views.create' => 'component_custom_admin_views.create', 'components_custom_admin_views.access' => 'component_custom_admin_views.access', 'component_custom_admin_views.access' => 'component_custom_admin_views.access', 'component_updates.create' => 'component_updates.create', 'components_updates.access' => 'component_updates.access', 'component_updates.access' => 'component_updates.access', 'component_mysql_tweaks.create' => 'component_mysql_tweaks.create', 'components_mysql_tweaks.access' => 'component_mysql_tweaks.access', 'component_mysql_tweaks.access' => 'component_mysql_tweaks.access', 'component_custom_admin_menus.create' => 'component_custom_admin_menus.create', 'components_custom_admin_menus.access' => 'component_custom_admin_menus.access', 'component_custom_admin_menus.access' => 'component_custom_admin_menus.access', 'component_router.create' => 'component_router.create', 'components_routers.access' => 'component_router.access', 'component_router.access' => 'component_router.access', 'component_config.create' => 'component_config.create', 'components_config.access' => 'component_config.access', 'component_config.access' => 'component_config.access', 'component_dashboard.create' => 'component_dashboard.create', 'components_dashboard.access' => 'component_dashboard.access', 'component_dashboard.access' => 'component_dashboard.access', 'component_files_folders.create' => 'component_files_folders.create', 'components_files_folders.access' => 'component_files_folders.access', 'component_files_folders.access' => 'component_files_folders.access', 'component_placeholders.create' => 'component_placeholders.create', 'components_placeholders.access' => 'component_placeholders.access', 'component_placeholders.access' => 'component_placeholders.access', 'component_plugins.create' => 'component_plugins.create', 'components_plugins.access' => 'component_plugins.access', 'component_plugins.access' => 'component_plugins.access', 'component_modules.create' => 'component_modules.create', 'components_modules.access' => 'component_modules.access', 'component_modules.access' => 'component_modules.access', 'snippet_type.create' => 'snippet_type.create', 'snippet_types.access' => 'snippet_type.access', 'snippet_type.access' => 'snippet_type.access', 'library_config.create' => 'library_config.create', 'libraries_config.access' => 'library_config.access', 'library_config.access' => 'library_config.access', 'library_files_folders_urls.create' => 'library_files_folders_urls.create', 'libraries_files_folders_urls.access' => 'library_files_folders_urls.access', 'library_files_folders_urls.access' => 'library_files_folders_urls.access', 'joomla_module_updates.create' => 'joomla_module_updates.create', 'joomla_modules_updates.access' => 'joomla_module_updates.access', 'joomla_module_updates.access' => 'joomla_module_updates.access', 'joomla_module_files_folders_urls.create' => 'joomla_module_files_folders_urls.create', 'joomla_modules_files_folders_urls.access' => 'joomla_module_files_folders_urls.access', 'joomla_module_files_folders_urls.access' => 'joomla_module_files_folders_urls.access', 'joomla_plugin_groups.access' => 'joomla_plugin_group.access', 'joomla_plugin_group.access' => 'joomla_plugin_group.access', 'joomla_plugin_updates.create' => 'joomla_plugin_updates.create', 'joomla_plugins_updates.access' => 'joomla_plugin_updates.access', 'joomla_plugin_updates.access' => 'joomla_plugin_updates.access', 'joomla_plugin_files_folders_urls.create' => 'joomla_plugin_files_folders_urls.create', 'joomla_plugins_files_folders_urls.access' => 'joomla_plugin_files_folders_urls.access', 'joomla_plugin_files_folders_urls.access' => 'joomla_plugin_files_folders_urls.access', ]; /** * The styles array. * * @var array * @since 4.3 */ protected array $styles = [ 'administrator/components/com_componentbuilder/assets/css/admin.css', 'administrator/components/com_componentbuilder/assets/css/dashboard.css' ]; /** * The scripts array. * * @var array * @since 4.3 */ protected array $scripts = [ 'administrator/components/com_componentbuilder/assets/js/admin.js' ]; /** * Constructor * * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). * @param ?MVCFactoryInterface $factory The factory. * * @since 1.6 * @throws \Exception */ public function __construct($config = [], MVCFactoryInterface $factory = null) { parent::__construct($config, $factory); $this->user ??= $this->getCurrentUser(); } /** * Get dashboard icons, grouped by view sections. * * @return array> * @since 5.1.1 */ public function getIcons(): array { $icons = []; foreach ($this->viewGroups as $group => $views) { if (!UtilitiesArrayHelper::check($views)) { $icons[$group][] = false; continue; } foreach ($views as $view) { $icon = $this->buildIconObject($view); if ($icon !== null) { $icons[$group][] = $icon; } } } return $icons; } /** * Method to get the styles that have to be included on the view * * @return array styles files * @since 4.3 */ public function getStyles(): array { return $this->styles; } /** * Method to set the styles that have to be included on the view * * @return void * @since 4.3 */ public function setStyles(string $path): void { $this->styles[] = $path; } /** * Method to get the script that have to be included on the view * * @return array script files * @since 4.3 */ public function getScripts(): array { return $this->scripts; } /** * Method to set the script that have to be included on the view * * @return void * @since 4.3 */ public function setScript(string $path): void { $this->scripts[] = $path; } /** * Build a single dashboard icon if access is granted. * * @param string $view The view string to parse. * * @return \stdClass|null The icon object or null if access denied. * @since 5.1.1 */ protected function buildIconObject(string $view): ?\stdClass { $parsed = $this->parseViewDefinition($view); if (!$parsed) { return null; } [ 'type' => $type, 'name' => $name, 'url' => $url, 'image' => $image, 'alt' => $alt, 'viewName' => $viewName, 'add' => $add, ] = $parsed; if (!$this->hasAccessToView($viewName, $add)) { return null; } return $this->createIconObject($url, $name, $image, $alt); } /** * Parse a view string into structured components. * * @param string $view The view definition string. * * @return array|null Parsed values or null on failure. * @since 5.1.1 */ protected function parseViewDefinition(string $view): ?array { $add = false; if (strpos($view, '||') !== false) { $parts = explode('||', $view); if (count($parts) === 3) { [$type, $name, $url] = $parts; return [ 'type' => $type, 'name' => 'COM_COMPONENTBUILDER_DASHBOARD_' . StringHelper::safe($name, 'U'), 'url' => $url, 'image' => "{$name}.{$type}", 'alt' => $name, 'viewName' => $name, 'add' => false, ]; } } if (strpos($view, '.') !== false) { $parts = explode('.', $view); $type = $parts[0] ?? ''; $name = $parts[1] ?? ''; $action = $parts[2] ?? null; $viewName = $name; if ($action) { if ($action === 'add') { $url = "index.php?option=com_componentbuilder&view={$name}&layout=edit"; $image = "{$name}_{$action}.{$type}"; $alt = "{$name} {$action}"; $name = 'COM_COMPONENTBUILDER_DASHBOARD_' . StringHelper::safe($name, 'U') . '_ADD'; $add = true; } else { if (strpos($action, '_qpo0O0oqp_') !== false) { [$action, $ext] = explode('_qpo0O0oqp_', $action); $extension = str_replace('_po0O0oq_', '.', $ext); } else { $extension = "com_componentbuilder.{$name}"; } $url = "index.php?option=com_categories&view=categories&extension={$extension}"; $image = "{$name}_{$action}.{$type}"; $alt = "{$name} {$action}"; $name = 'COM_COMPONENTBUILDER_DASHBOARD_' . StringHelper::safe($name, 'U') . '_' . StringHelper::safe($action, 'U'); } } else { $url = "index.php?option=com_componentbuilder&view={$name}"; $image = "{$name}.{$type}"; $alt = $name; $name = 'COM_COMPONENTBUILDER_DASHBOARD_' . StringHelper::safe($name, 'U'); } return compact('type', 'name', 'url', 'image', 'alt', 'viewName', 'add'); } return [ 'type' => 'png', 'name' => ucwords($view) . '

', 'url' => "index.php?option=com_componentbuilder&view={$view}", 'image' => "{$view}.png", 'alt' => $view, 'viewName' => $view, 'add' => false, ]; } /** * Determine if the user has access to view or create the item. * * @param string $viewName The base name of the view. * @param bool $add If this is an add-action. * * @return bool * @since 5.1.1 */ protected function hasAccessToView(string $viewName, bool $add): bool { $viewAccess = $this->viewAccess; $accessAdd = $add && isset($viewAccess["{$viewName}.create"]) ? $viewAccess["{$viewName}.create"] : ($add ? 'core.create' : ''); $accessTo = $viewAccess["{$viewName}.access"] ?? ''; $dashboardAdd = isset($viewAccess["{$viewName}.dashboard_add"]) && $this->user->authorise($viewAccess["{$viewName}.dashboard_add"], 'com_componentbuilder'); $dashboardList = isset($viewAccess["{$viewName}.dashboard_list"]) && $this->user->authorise($viewAccess["{$viewName}.dashboard_list"], 'com_componentbuilder'); if ($add && StringHelper::check($accessAdd)) { return $this->user->authorise($accessAdd, 'com_componentbuilder') && $dashboardAdd; } if (StringHelper::check($accessTo)) { return $this->user->authorise($accessTo, 'com_componentbuilder') && $dashboardList; } return !$accessTo && !$accessAdd; } /** * Create a \stdClass icon object. * * @param string $url Icon URL. * @param string $name Language string or label. * @param string $image Image filename. * @param string $alt Alt text. * * @return \stdClass * @since 5.1.1 */ protected function createIconObject(string $url, string $name, string $image, string $alt): \stdClass { $icon = new \stdClass; $icon->url = $url; $icon->name = $name; $icon->image = $image; $icon->alt = $alt; return $icon; } /** * Load and display the wiki page content using an AJAX call to the component endpoint. * * This method injects an inline JavaScript script that asynchronously fetches the wiki page content * via a JSON API endpoint in the component. It uses the `marked` library to render markdown content * and inserts the result into the `wiki-md` container. Errors are displayed in a separate element. * * @return string HTML markup including a container for the wiki content and an error message area. * @since 3.9.0 */ public function getWiki() { // call the ajax get wiki endpoint $call_url = Uri::base() . 'index.php?option=com_componentbuilder&task=ajax.getWiki&format=json&raw=true&' . Session::getFormToken() . '=1&name=Home'; /** \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa->addInlineScript(' function getWikiPage(){ fetch("' . $call_url . '").then((response) => { if (response.ok) { return response.json(); } }).then((result) => { if (typeof result.page !== "undefined") { document.getElementById("wiki-md").innerHTML = marked.parse(result.page); } else if (typeof result.error !== "undefined") { document.getElementById("wiki-md-error").innerHTML = result.error } }); } setTimeout(getWikiPage, 1000);'); return '
'.Text::_('COM_COMPONENTBUILDER_THE_WIKI_IS_LOADING').'..
'; } /** * Load and display the component's README file using JavaScript fetch and markdown rendering. * * This method injects an inline script into the document that, once the DOM is fully loaded, * fetches the README.txt file located in the administrator component directory, parses it using * the `marked` JavaScript library, and inserts the HTML into the `readme-md` div. * * @return string HTML markup including a container for the README content and a loading message. * @since 3.9.0 */ public function getReadme() { // get readme text path $call_url = Uri::root() . 'administrator/components/com_componentbuilder/README.txt'; /** \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa->addInlineScript(' document.addEventListener("DOMContentLoaded", function () { fetch("'. $call_url . '") .then(response => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.text(); }) .then(readme => { document.getElementById("readme-md").innerHTML = marked.parse(readme); }) .catch(error => { console.error("There has been a problem with your fetch operation:", error); document.getElementById("readme-md").innerHTML = "'.Text::_('COM_COMPONENTBUILDER_PLEASE_CHECK_AGAIN_LATER').'."; }); });'); return '
'.Text::_('COM_COMPONENTBUILDER_THE_README_IS_LOADING').'..
'; } /** * Inject JavaScript that fetches and displays the current component version status. * * This method adds an inline script to the page which asynchronously calls the component's * AJAX endpoint to check the latest version. It updates the `#component-update-notice` element * with the fetched version notice or error message. * * @return void * @since 2.3.0 */ public function getVersion() { // call the ajax get version endpoint $call_url = Uri::base() . 'index.php?option=com_componentbuilder&task=ajax.getVersion&format=json&raw=true&' . Session::getFormToken() . '=1&version=1.0.0'; try { /** \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); $wa->addInlineScript(' function getComponentVersionStatus() { fetch("' . $call_url . '").then((response) => { if (response.ok) { return response.json(); } }).then((result) => { const target = document.getElementById("component-update-notice"); if (!target) return; if (typeof result.notice !== "undefined") { target.innerHTML = result.notice; } else if (typeof result.error !== "undefined") { target.innerHTML = result.error; } }); } setTimeout(getComponentVersionStatus, 800);'); } catch (\Throwable $e) { // we do nothing.... } } }