diff --git a/public/main/inc/lib/urlmanager.lib.php b/public/main/inc/lib/urlmanager.lib.php index 929760b7bcd..99b5bfa5ae1 100644 --- a/public/main/inc/lib/urlmanager.lib.php +++ b/public/main/inc/lib/urlmanager.lib.php @@ -7,6 +7,7 @@ use Chamilo\CoreBundle\Entity\AccessUrlRelSession; use Chamilo\CoreBundle\Entity\AccessUrlRelUser; use Chamilo\CoreBundle\Entity\AccessUrlRelUserGroup; +use Chamilo\CoreBundle\Entity\SettingsCurrent; use Chamilo\CoreBundle\Entity\UserAuthSource; use Chamilo\CoreBundle\Framework\Container; use Doctrine\ORM\NonUniqueResultException; @@ -98,12 +99,13 @@ public static function delete($id) /* * $tableCourseCategory = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE_CATEGORY); - $sql = "DELETE FROM $tableCourseCategory WHERE access_url_id = ".$id; - Database::query($sql); - */ + * $sql = "DELETE FROM $tableCourseCategory WHERE access_url_id = ".$id; + * Database::query($sql); + */ $em = Container::getEntityManager(); $relEntities = [ + SettingsCurrent::class, AccessUrlRelCourse::class, AccessUrlRelSession::class, AccessUrlRelUserGroup::class, diff --git a/src/CoreBundle/Controller/Admin/SettingsController.php b/src/CoreBundle/Controller/Admin/SettingsController.php index 20c5ab131a4..4df5c498e94 100644 --- a/src/CoreBundle/Controller/Admin/SettingsController.php +++ b/src/CoreBundle/Controller/Admin/SettingsController.php @@ -7,6 +7,7 @@ namespace Chamilo\CoreBundle\Controller\Admin; use Chamilo\CoreBundle\Controller\BaseController; +use Chamilo\CoreBundle\Entity\AccessUrl; use Chamilo\CoreBundle\Entity\SearchEngineField; use Chamilo\CoreBundle\Entity\SettingsCurrent; use Chamilo\CoreBundle\Entity\SettingsValueTemplate; @@ -47,7 +48,85 @@ public function index(): Response } /** - * Edit configuration with given namespace. + * Toggle access_url_changeable for a given setting variable. + * Only platform admins on the main URL (ID = 1) are allowed to change it, + */ + #[IsGranted('ROLE_ADMIN')] + #[Route('/settings/toggle_changeable', name: 'settings_toggle_changeable', methods: ['POST'])] + public function toggleChangeable(Request $request, AccessUrlHelper $accessUrlHelper): JsonResponse + { + // Security: only admins. + if (!$this->isGranted('ROLE_ADMIN')) { + return $this->json([ + 'error' => 'Only platform admins can modify this flag.', + ], 403); + } + + $currentUrl = $accessUrlHelper->getCurrent(); + if (!$currentUrl) { + return $this->json([ + 'error' => 'Access URL not resolved.', + ], 500); + } + + $currentUrlId = (int) $currentUrl->getId(); + + if (1 !== $currentUrlId) { + return $this->json([ + 'error' => 'Only the main URL (ID 1) can toggle this setting.', + ], 403); + } + + $payload = json_decode($request->getContent(), true); + + if (!\is_array($payload) || !isset($payload['variable'], $payload['status'])) { + return $this->json([ + 'error' => 'Invalid payload.', + ], 400); + } + + $variable = trim((string) $payload['variable']); + $status = (int) $payload['status']; + $status = $status === 1 ? 1 : 0; + + if ('' === $variable) { + return $this->json([ + 'error' => 'Invalid variable.', + ], 400); + } + + $repo = $this->entityManager->getRepository(SettingsCurrent::class); + + // We search by variable + current main AccessUrl entity. + $setting = $repo->findOneBy([ + 'variable' => $variable, + 'url' => $currentUrl, + ]); + + if (!$setting) { + return $this->json([ + 'error' => 'Setting not found.', + ], 404); + } + + try { + $setting->setAccessUrlChangeable($status); + $this->entityManager->flush(); + + return $this->json([ + 'result' => 1, + 'status' => $status, + ]); + } catch (\Throwable $e) { + return $this->json([ + 'error' => 'Unable to update setting.', + 'details' => $e->getMessage(), + ], 500); + } + } + + /** + * Edit configuration with given namespace (search page). */ #[IsGranted('ROLE_ADMIN')] #[Route('/settings/search_settings', name: 'chamilo_platform_settings_search')] @@ -75,6 +154,47 @@ public function searchSetting(Request $request, AccessUrlHelper $accessUrlHelper $schemas = $manager->getSchemas(); [$ordered, $labelMap] = $this->computeOrderedNamespacesByTranslatedLabel($schemas, $request); + // Template map for current URL (existing behavior – JSON helper) + $settingsRepo = $this->entityManager->getRepository(SettingsCurrent::class); + + // Build template map: current URL overrides main URL when missing. + $currentUrlId = (int) $url->getId(); + $mainUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1); + + if ($mainUrl instanceof AccessUrl && 1 !== $currentUrlId) { + $mainRows = $settingsRepo->findBy(['url' => $mainUrl]); + foreach ($mainRows as $s) { + if ($s->getValueTemplate()) { + $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId(); + } + } + } + + $currentRows = $settingsRepo->findBy(['url' => $url]); + foreach ($currentRows as $s) { + if ($s->getValueTemplate()) { + $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId(); + } + } + + // MultiURL changeable flags: read from main URL (ID = 1) only + $changeableMap = []; + $mainUrlRows = $settingsRepo->createQueryBuilder('sc') + ->join('sc.url', 'u') + ->andWhere('u.id = :mainId') + ->setParameter('mainId', 1) + ->getQuery() + ->getResult(); + + foreach ($mainUrlRows as $row) { + if ($row instanceof SettingsCurrent) { + $changeableMap[$row->getVariable()] = $row->getAccessUrlChangeable(); + } + } + + // Only platform admins on the main URL can toggle the MultiURL flag. + $canToggleMultiUrlSetting = $this->isGranted('ROLE_ADMIN') && 1 === $currentUrlId; + if ('' === $keyword) { return $this->render('@ChamiloCore/Admin/Settings/search.html.twig', [ 'keyword' => $keyword, @@ -86,17 +206,12 @@ public function searchSetting(Request $request, AccessUrlHelper $accessUrlHelper 'template_map_by_category' => $templateMapByCategory, 'ordered_namespaces' => $ordered, 'namespace_labels' => $labelMap, + 'changeable_map' => $changeableMap, + 'current_url_id' => $currentUrlId, + 'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting, ]); } - $settingsRepo = $this->entityManager->getRepository(SettingsCurrent::class); - $settingsWithTemplate = $settingsRepo->findBy(['url' => $url]); - foreach ($settingsWithTemplate as $s) { - if ($s->getValueTemplate()) { - $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId(); - } - } - $settingsFromKeyword = $manager->getParametersFromKeywordOrderedByCategory($keyword); if (!empty($settingsFromKeyword)) { foreach ($settingsFromKeyword as $category => $parameterList) { @@ -145,6 +260,9 @@ public function searchSetting(Request $request, AccessUrlHelper $accessUrlHelper 'template_map_by_category' => $templateMapByCategory, 'ordered_namespaces' => $ordered, 'namespace_labels' => $labelMap, + 'changeable_map' => $changeableMap, + 'current_url_id' => $currentUrlId, + 'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting, ]); } @@ -230,17 +348,48 @@ public function updateSetting(Request $request, AccessUrlHelper $accessUrlHelper $templateMap = []; $settingsRepo = $this->entityManager->getRepository(SettingsCurrent::class); - $settingsWithTemplate = $settingsRepo->findBy(['url' => $url]); + $currentUrlId = (int) $url->getId(); + $mainUrl = $this->entityManager->getRepository(AccessUrl::class)->find(1); + // Build template map: fallback to main URL templates when sub-URL has no row for a locked setting. + if ($mainUrl instanceof AccessUrl && 1 !== $currentUrlId) { + $mainRows = $settingsRepo->findBy(['url' => $mainUrl]); + foreach ($mainRows as $s) { + if ($s->getValueTemplate()) { + $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId(); + } + } + } + + $settingsWithTemplate = $settingsRepo->findBy(['url' => $url]); foreach ($settingsWithTemplate as $s) { if ($s->getValueTemplate()) { $templateMap[$s->getVariable()] = $s->getValueTemplate()->getId(); } } + + // MultiURL changeable flags: read from main URL (ID = 1) only + $changeableMap = []; + $mainUrlRows = $settingsRepo->createQueryBuilder('sc') + ->join('sc.url', 'u') + ->andWhere('u.id = :mainId') + ->setParameter('mainId', 1) + ->getQuery() + ->getResult(); + + foreach ($mainUrlRows as $row) { + if ($row instanceof SettingsCurrent) { + $changeableMap[$row->getVariable()] = $row->getAccessUrlChangeable(); + } + } + $platform = [ 'server_type' => (string) $manager->getSetting('platform.server_type', true), ]; + // Only platform admins on the main URL can toggle the MultiURL flag. + $canToggleMultiUrlSetting = $this->isGranted('ROLE_ADMIN') && 1 === $currentUrlId; + return $this->render('@ChamiloCore/Admin/Settings/default.html.twig', [ 'schemas' => $schemas, 'settings' => $settings, @@ -251,6 +400,9 @@ public function updateSetting(Request $request, AccessUrlHelper $accessUrlHelper 'ordered_namespaces' => $ordered, 'namespace_labels' => $labelMap, 'platform' => $platform, + 'changeable_map' => $changeableMap, + 'current_url_id' => $currentUrlId, + 'can_toggle_multiurl_setting' => $canToggleMultiUrlSetting, 'search_diagnostics' => $searchDiagnostics, ]); } @@ -369,12 +521,6 @@ private function computeOrderedNamespacesByTranslatedLabel(array $schemas, Reque /** * Build environment diagnostics for the "search" settings page. - * - * This replicates the legacy Chamilo 1 behaviour: - * - Check Xapian PHP extension - * - Check the index directory and permissions - * - Check custom search fields - * - Check external converters (pdftotext, ps2pdf, ...) */ private function buildSearchDiagnostics(SettingsManager $manager): array { diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20251213154100.php b/src/CoreBundle/Migrations/Schema/V200/Version20251213154100.php new file mode 100644 index 00000000000..7dab8c0ce25 --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20251213154100.php @@ -0,0 +1,29 @@ +addSql('UPDATE settings SET access_url_locked = 0 WHERE access_url_locked IS NULL OR access_url_locked = 1'); + } + + public function down(Schema $schema): void + { + // Revert to previous "locked everywhere" behavior (not recommended, but reversible). + $this->addSql('UPDATE settings SET access_url_locked = 1 WHERE access_url_locked = 0'); + } +} diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20251215074200.php b/src/CoreBundle/Migrations/Schema/V200/Version20251215074200.php new file mode 100644 index 00000000000..3d3b6fbe068 --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20251215074200.php @@ -0,0 +1,77 @@ +addSql(" + UPDATE settings + SET access_url_locked = 1 + WHERE variable IN ( + 'permissions_for_new_directories', + 'permissions_for_new_files', + 'course_creation_form_set_extra_fields_mandatory', + 'access_url_specific_files', + 'cron_remind_course_finished_activate', + 'cron_remind_course_expiration_frequency', + 'cron_remind_course_expiration_activate', + 'donotlistcampus', + 'server_type', + 'chamilo_database_version', + 'unoconv_binaries', + 'session_admin_access_to_all_users_on_all_urls', + 'split_users_upload_directory', + 'multiple_url_hide_disabled_settings', + 'login_is_email', + 'proxy_settings', + 'login_max_attempt_before_blocking_account', + 'permanently_remove_deleted_files', + 'allow_use_sub_language' + ) + "); + } + + public function down(Schema $schema): void + { + // Unlock back (sub-URLs editable) for the same list. + $this->addSql(" + UPDATE settings + SET access_url_locked = 0 + WHERE variable IN ( + 'permissions_for_new_directories', + 'permissions_for_new_files', + 'course_creation_form_set_extra_fields_mandatory', + 'access_url_specific_files', + 'cron_remind_course_finished_activate', + 'cron_remind_course_expiration_frequency', + 'cron_remind_course_expiration_activate', + 'donotlistcampus', + 'server_type', + 'chamilo_database_version', + 'unoconv_binaries', + 'session_admin_access_to_all_users_on_all_urls', + 'split_users_upload_directory', + 'multiple_url_hide_disabled_settings', + 'login_is_email', + 'proxy_settings', + 'login_max_attempt_before_blocking_account', + 'permanently_remove_deleted_files', + 'allow_use_sub_language' + ) + "); + } +} diff --git a/src/CoreBundle/Resources/views/Admin/Settings/default.html.twig b/src/CoreBundle/Resources/views/Admin/Settings/default.html.twig index 634eddd4c3a..a0121fe9fdd 100644 --- a/src/CoreBundle/Resources/views/Admin/Settings/default.html.twig +++ b/src/CoreBundle/Resources/views/Admin/Settings/default.html.twig @@ -20,12 +20,11 @@
-
+
-
- {# Special notice for AI helpers namespace #} - {% if namespace == 'ai_helpers' %} -
+
+ {% if namespace == 'ai_helpers' %} +

{{ "EU AI Act – Transparency & risk notice"|trans }} @@ -51,10 +50,9 @@

{% endif %} - {{ form_errors(form) }}
+

{{ field.vars.label|trans }} @@ -85,23 +90,58 @@ {% endif %} + {# Debug icon on test servers (keep original behavior) #} {% set serverType = platform.server_type ?? (settings.platform.server_type ?? null) %} {% if serverType == 'test' %} {% endif %} + + {# MultiURL changeable eye #} + {% if changeable_map is defined %} + {% set changeable = changeable_map[fieldName] is defined ? changeable_map[fieldName] : 1 %} + + {% if can_toggle_multiurl_setting %} + {# Main URL + admin: clickable toggle #} + + {% else %} + {# Other URLs or non-privileged admins: read-only indicator #} + + {% if changeable %} + + {% else %} + + {% endif %} + + {% endif %} + {% endif %}

@@ -112,19 +152,25 @@
{% for child in field %} + {% set childAttr = isDisabledOnSubUrl ? { disabled: 'disabled' } : {} %} {% endfor %}
{% else %} - {{ form_widget(field, { - attr: { - class: 'w-full rounded border border-gray-25 focus:border-primary focus:ring focus:ring-primary/30 transition' - } - }) }} + {# Build base attributes and add disabled only when needed #} + {% set baseAttr = { + class: 'w-full rounded border border-gray-25 focus:border-primary focus:ring focus:ring-primary/30 transition' + } %} + {% set widgetAttr = isDisabledOnSubUrl + ? baseAttr|merge({ disabled: 'disabled' }) + : baseAttr + %} + + {{ form_widget(field, { attr: widgetAttr }) }} {% endif %}
@@ -245,7 +291,7 @@
- {# MODAL overlay for JSON templates #} + {# MODAL overlay #} @@ -326,7 +372,14 @@ sudo systemctl restart apache2 -{% endblock %} +{% endblock %} {% block javascripts %} {% set ns = app.request.get('namespace')|default('') %} {% set sectionLabel = diff --git a/src/CoreBundle/Resources/views/Admin/Settings/search.html.twig b/src/CoreBundle/Resources/views/Admin/Settings/search.html.twig index 6d2d82ca6e3..dc67e2086a2 100644 --- a/src/CoreBundle/Resources/views/Admin/Settings/search.html.twig +++ b/src/CoreBundle/Resources/views/Admin/Settings/search.html.twig @@ -1,11 +1,9 @@ {% extends "@ChamiloCore/Layout/layout_one_col.html.twig" %} {% from '@ChamiloCore/Admin/Settings/actions.html.twig' import update %} {% set namespace = app.request.get('namespace') %} -{# Use full width for inner containers #} {% set content_width = 'w-full' %} {% block content %} - {# Two-column layout: fixed sidebar + fluid content #}
{% include '@ChamiloCore/Admin/Settings/menu.html.twig' %} @@ -44,7 +42,7 @@ {{ form_start(form, { action: path('chamilo_platform_settings', { 'namespace': category, 'keyword': keyword }), method: 'POST', - attr: { class: 'form-horizontal', novalidate: 'novalidate' } + attr: { class: 'form-horizontal !w-full !max-w-none', novalidate: 'novalidate' } }) }} @@ -52,6 +50,13 @@ {% for field in form %} {% set fieldName = field.vars.name %} {% set isHidden = 'hidden' in field.vars.block_prefixes %} + {% set isDisabledOnSubUrl = + current_url_id is defined + and current_url_id != 1 + and changeable_map is defined + and changeable_map[fieldName] is defined + and changeable_map[fieldName] == 0 + %} {% if isHidden %} {{ form_widget(field) }} {% else %} @@ -59,29 +64,62 @@

{{ field.vars.label|trans }}

- {# Optional JSON template helper link #} - {% set templateId = null %} - {% if template_map is defined and template_map[fieldName] is defined %} - {% set templateId = template_map[fieldName] %} - {% elseif template_map_by_category is defined - and template_map_by_category[category] is defined - and template_map_by_category[category][fieldName] is defined %} - {% set templateId = template_map_by_category[category][fieldName] %} - {% endif %} +
+ {% set templateId = null %} + {% if template_map is defined and template_map[fieldName] is defined %} + {% set templateId = template_map[fieldName] %} + {% elseif template_map_by_category is defined + and template_map_by_category[category] is defined + and template_map_by_category[category][fieldName] is defined %} + {% set templateId = template_map_by_category[category][fieldName] %} + {% endif %} - {% if templateId %} - - - {{ 'Show JSON Template'|trans }} - - {% endif %} + {% if templateId %} + + + {{ 'Show JSON Template'|trans }} + + {% endif %} + + {% if changeable_map is defined %} + {% set changeable = changeable_map[fieldName] is defined ? changeable_map[fieldName] : 1 %} + + {% if can_toggle_multiurl_setting %} + + {% else %} + + {% if changeable %} + + {% else %} + + {% endif %} + + {% endif %} + {% endif %} +
{% set verticalChoiceFields = ['active_tools_on_create', 'course_hide_tools'] %} - {# only for ChoiceType + expanded + multiple #} {% set isChoice = 'choice' in field.vars.block_prefixes %} {% set isExpanded = isChoice and (field.vars.expanded is defined and field.vars.expanded) %} {% set isMultiple = isChoice and (field.vars.multiple is defined and field.vars.multiple) %} @@ -89,16 +127,23 @@ {% if (field.vars.name in verticalChoiceFields) and isExpanded and isMultiple %}
{% for child in field %} + {% set childAttr = isDisabledOnSubUrl ? { disabled: 'disabled' } : {} %} {% endfor %}
{% else %} - {{ form_widget(field, { attr: { + {% set baseAttr = { class: 'w-full max-w-none rounded border border-gray-25 focus:border-primary focus:ring focus:ring-primary/30 transition' - } }) }} + } %} + {% set widgetAttr = isDisabledOnSubUrl + ? baseAttr|merge({ disabled: 'disabled' }) + : baseAttr + %} + + {{ form_widget(field, { attr: widgetAttr }) }} {% endif %}
@@ -128,7 +173,6 @@
- {# Simple modal to preview JSON templates #}