Skip to content

Commit 7099e77

Browse files
authored
Merge pull request #7489 from christianbeeznest/ofaj-23212
Internal: Add cookie consent banner and legal menu links - refs BT#23212
2 parents d692415 + 6cf5a39 commit 7099e77

8 files changed

Lines changed: 299 additions & 8 deletions

File tree

assets/vue/components/layout/Sidebar.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<BaseSidebarPanelMenu v-model="menuItemsAfterMyCourse" />
4141
</div>
4242
<div class="app-sidebar__bottom">
43+
<CategoryLinks category="menu_links" />
4344
<PageList category-title="footer_private" />
4445

4546
<p v-html="t('Created with Chamilo copyright year', [currentYear])" />
@@ -82,6 +83,7 @@ import PageList from "../page/PageList.vue"
8283
import { useEnrolledStore } from "../../store/enrolledStore"
8384
import BaseIcon from "../basecomponents/BaseIcon.vue"
8485
import BaseSidebarPanelMenu from "../basecomponents/BaseSidebarPanelMenu.vue"
86+
import CategoryLinks from "../page/CategoryLinks.vue"
8587
8688
const { t } = useI18n()
8789
const securityStore = useSecurityStore()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<template>
2+
<div
3+
v-if="items.length"
4+
class="mb-2"
5+
>
6+
<div class="flex flex-col gap-1">
7+
<a
8+
v-for="it in items"
9+
:key="it.slug"
10+
class="text-sm text-gray-90 hover:underline"
11+
:href="`/pages/${it.slug}`"
12+
>
13+
{{ it.title }}
14+
</a>
15+
</div>
16+
</div>
17+
</template>
18+
19+
<script setup>
20+
import { onMounted, ref, watch } from "vue"
21+
import { useI18n } from "vue-i18n"
22+
23+
const props = defineProps({
24+
category: { type: String, required: true },
25+
})
26+
27+
const { locale } = useI18n()
28+
const items = ref([])
29+
30+
const load = async () => {
31+
const loc = (locale.value || "").toString()
32+
const url = `/pages/_category-links?category=${encodeURIComponent(props.category)}&locale=${encodeURIComponent(loc)}`
33+
const res = await fetch(url, { headers: { Accept: "application/json" } })
34+
if (!res.ok) {
35+
items.value = []
36+
return
37+
}
38+
const data = await res.json()
39+
items.value = Array.isArray(data.items) ? data.items : []
40+
}
41+
42+
onMounted(load)
43+
watch(() => locale.value, load)
44+
</script>

src/CoreBundle/Controller/PageController.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,25 @@ public function preview(int $id, PageRepository $pageRepo): Response
103103
'page' => $page,
104104
]);
105105
}
106+
107+
/**
108+
* Public endpoint used by Vue (sidebar + login page) to render legal menu links.
109+
*
110+
* GET /pages/_category-links?category=menu_links&locale=fr_FR
111+
*/
112+
#[Route('/_category-links', name: 'public_page_category_links', methods: ['GET'])]
113+
public function categoryLinks(Request $request, PageRepository $pageRepo): JsonResponse
114+
{
115+
$accessUrl = $this->accessUrlHelper->getCurrent();
116+
$locale = trim((string) $request->query->get('locale', ''));
117+
$category = trim((string) $request->query->get('category', ''));
118+
119+
if ('' === $category) {
120+
return $this->json(['items' => []]);
121+
}
122+
123+
$items = $pageRepo->findPublicLinksByCategoryWithLocaleFallback($accessUrl, $category, $locale);
124+
125+
return $this->json(['items' => $items]);
126+
}
106127
}

src/CoreBundle/Helpers/PageHelper.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,19 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
6464
;
6565
$this->pageCategoryRepository->update($indexCategory);
6666

67-
$indexCategory = (new PageCategory())
67+
$faqCategory = (new PageCategory())
6868
->setTitle('faq')
6969
->setType('grid')
7070
->setCreator($user)
7171
;
72-
$this->pageCategoryRepository->update($indexCategory);
72+
$this->pageCategoryRepository->update($faqCategory);
7373

74-
$indexCategory = (new PageCategory())
74+
$demoCategory = (new PageCategory())
7575
->setTitle('demo')
7676
->setType('grid')
7777
->setCreator($user)
7878
;
79-
$this->pageCategoryRepository->update($indexCategory);
79+
$this->pageCategoryRepository->update($demoCategory);
8080

8181
$page = (new Page())
8282
->setTitle('Welcome')
@@ -87,7 +87,6 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
8787
->setEnabled(true)
8888
->setUrl($url)
8989
;
90-
9190
$this->pageRepository->update($page);
9291

9392
$indexPage = (new Page())
@@ -106,17 +105,22 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
106105
->setType('grid')
107106
->setCreator($user)
108107
;
109-
110108
$this->pageCategoryRepository->update($footerPublicCategory);
111109

112110
$footerPrivateCategory = (new PageCategory())
113111
->setTitle('footer_private')
114112
->setType('grid')
115113
->setCreator($user)
116114
;
117-
118115
$this->pageCategoryRepository->update($footerPrivateCategory);
119116

117+
$menuLinksCategory = (new PageCategory())
118+
->setTitle('menu_links')
119+
->setType('grid')
120+
->setCreator($user)
121+
;
122+
$this->pageCategoryRepository->update($menuLinksCategory);
123+
120124
// Categories for extra content in admin blocks.
121125
foreach (PageCategory::ADMIN_BLOCKS_CATEGORIES as $nameBlock) {
122126
$usersAdminBlock = (new PageCategory())
@@ -132,7 +136,6 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
132136
->setType('grid')
133137
->setCreator($user)
134138
;
135-
136139
$this->pageCategoryRepository->update($publicCategory);
137140

138141
$introductionCategory = (new PageCategory())
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/* For licensing terms, see /license.txt */
4+
5+
declare(strict_types=1);
6+
7+
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
8+
9+
use Chamilo\CoreBundle\Entity\PageCategory;
10+
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
11+
use Doctrine\DBAL\Schema\Schema;
12+
13+
final class Version20260204142200 extends AbstractMigrationChamilo
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Adds the CMS PageCategory "menu_links" used to display legal information links in the sidebar and public homepage.';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
$pageCategoryRepo = $this->entityManager->getRepository(PageCategory::class);
23+
$adminUser = $this->getAdmin();
24+
25+
// Create the "menu_links" category if it does not exist yet.
26+
$existing = $pageCategoryRepo->findOneBy(['title' => 'menu_links']);
27+
if ($existing) {
28+
error_log('[MIGRATION] PageCategory "menu_links" already exists. Skipping.');
29+
30+
return;
31+
}
32+
33+
$category = new PageCategory();
34+
$category
35+
->setTitle('menu_links')
36+
->setType('grid')
37+
->setCreator($adminUser)
38+
;
39+
40+
$this->entityManager->persist($category);
41+
$this->entityManager->flush();
42+
43+
error_log('[MIGRATION] PageCategory "menu_links" created successfully.');
44+
}
45+
46+
public function down(Schema $schema): void
47+
{
48+
// Intentionally left empty (non-destructive migration).
49+
}
50+
}

src/CoreBundle/Repository/PageRepository.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,71 @@ public function delete(?Page $page = null): void
102102
$this->getEntityManager()->flush();
103103
}
104104
}
105+
106+
public function findPublicLinksByCategoryWithLocaleFallback(
107+
AccessUrl $accessUrl,
108+
string $categoryTitle,
109+
string $locale
110+
): array {
111+
$locale = trim($locale);
112+
$prefix = '' !== $locale ? substr($locale, 0, 2) : '';
113+
114+
// Exact locale
115+
if ('' !== $locale) {
116+
$items = $this->createQueryBuilder('p')
117+
->select('p.title AS title, p.slug AS slug')
118+
->innerJoin('p.category', 'c')
119+
->andWhere('p.enabled = true')
120+
->andWhere('p.url = :url')
121+
->andWhere('c.title = :cat')
122+
->andWhere('p.locale = :loc')
123+
->setParameter('url', $accessUrl)
124+
->setParameter('cat', $categoryTitle)
125+
->setParameter('loc', $locale)
126+
->orderBy('p.title', 'ASC')
127+
->getQuery()
128+
->getArrayResult()
129+
;
130+
131+
if (!empty($items)) {
132+
return $items;
133+
}
134+
}
135+
136+
// Prefix locale (e.g. "fr")
137+
if ('' !== $prefix) {
138+
$items = $this->createQueryBuilder('p')
139+
->select('p.title AS title, p.slug AS slug')
140+
->innerJoin('p.category', 'c')
141+
->andWhere('p.enabled = true')
142+
->andWhere('p.url = :url')
143+
->andWhere('c.title = :cat')
144+
->andWhere('p.locale LIKE :prefix')
145+
->setParameter('url', $accessUrl)
146+
->setParameter('cat', $categoryTitle)
147+
->setParameter('prefix', $prefix.'%')
148+
->orderBy('p.title', 'ASC')
149+
->getQuery()
150+
->getArrayResult()
151+
;
152+
153+
if (!empty($items)) {
154+
return $items;
155+
}
156+
}
157+
158+
// Any locale
159+
return $this->createQueryBuilder('p')
160+
->select('p.title AS title, p.slug AS slug')
161+
->innerJoin('p.category', 'c')
162+
->andWhere('p.enabled = true')
163+
->andWhere('p.url = :url')
164+
->andWhere('c.title = :cat')
165+
->setParameter('url', $accessUrl)
166+
->setParameter('cat', $categoryTitle)
167+
->orderBy('p.title', 'ASC')
168+
->getQuery()
169+
->getArrayResult()
170+
;
171+
}
105172
}

src/CoreBundle/Resources/views/Layout/base-layout.html.twig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626

2727
{{ modals_block }}
2828

29+
{# Cookie banner (global) #}
30+
{% include "@ChamiloCore/Layout/cookie_banner.html.twig" %}
31+
2932
{% include "@ChamiloCore/Layout/foot.html.twig" %}
3033
</body>
3134
</html>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{# Cookie banner (C1-compatible behavior) #}
2+
{% set cookieWarningRaw = chamilo_settings_get('platform.cookie_warning')|default('')|trim|lower %}
3+
{% set cookieWarningEnabled = (cookieWarningRaw == 'true') %}
4+
{% set cookieAck = app.request.cookies.get('ChamiloUsesCookies') %}
5+
{% set youAcceptCookiesText =
6+
'By continuing to use this site, you declare you accept its use of cookies.'|trans
7+
%}
8+
{% set helpCookieUsageValidationHtml =
9+
"In order for this site to work and be able to measure the use of content, this platform uses cookies.<br /><br />\nIf needed, the Help section of your browser indicates how to configure cookies.<br /><br />\nFor more information on cookies, you can visit the <a href=\"http://www.aboutcookies.org/\">About Cookies</a> website."|trans
10+
%}
11+
{% set noCookiesText =
12+
'You do not have cookies support enabled in your browser. Chamilo relies on the feature called "cookies" to store your connection details. This means you will not be able to login if cookies are not enabled. Please change your browser configuration (generally in the Edit -> Preferences menu) and reload this page.'|trans
13+
%}
14+
{% if cookieWarningEnabled and cookieAck != 'ok' %}
15+
<div id="chamilo-cookie-banner"
16+
class="fixed inset-x-0 bottom-0 z-50 border-t border-warning/30 bg-warning/15 backdrop-blur shadow-lg">
17+
<div class="mx-auto max-w-7xl px-4 py-3">
18+
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
19+
<div class="flex items-start gap-3">
20+
<span class="mt-0.5 inline-flex h-6 w-6 items-center justify-center rounded-full bg-warning/30 text-xs font-bold text-gray-90"
21+
aria-hidden="true">!</span>
22+
23+
<div class="text-sm text-gray-90">
24+
<div class="leading-snug">
25+
{{ youAcceptCookiesText }}
26+
</div>
27+
28+
<div id="chamilo-cookie-banner-more"
29+
class="mt-2 hidden text-sm leading-relaxed text-gray-90">
30+
{{ helpCookieUsageValidationHtml|raw }}
31+
</div>
32+
33+
<div id="chamilo-cookie-banner-nocookies"
34+
class="mt-2 hidden text-sm font-semibold text-danger">
35+
{{ noCookiesText }}
36+
</div>
37+
</div>
38+
</div>
39+
40+
<div class="flex items-center gap-2 self-start md:self-auto">
41+
<button type="button"
42+
id="chamilo-cookie-banner-toggle"
43+
class="inline-flex items-center gap-1 rounded-md border border-warning/40 bg-white px-3 py-1.5 text-xs font-semibold text-gray-90 shadow-sm hover:bg-warning/10 focus:outline-none focus:ring-2 focus:ring-warning/40">
44+
<span aria-hidden="true">+</span>
45+
<span>{{ 'More'|trans }}</span>
46+
</button>
47+
48+
<button type="button"
49+
id="chamilo-cookie-banner-accept"
50+
class="inline-flex items-center gap-1 rounded-md bg-warning px-3 py-1.5 text-xs font-semibold text-warning-button-text shadow-sm hover:bg-warning/90 focus:outline-none focus:ring-2 focus:ring-warning/40">
51+
<span>{{ 'Accept'|trans }}</span>
52+
</button>
53+
</div>
54+
</div>
55+
</div>
56+
</div>
57+
58+
<script>
59+
(function () {
60+
"use strict";
61+
62+
const banner = document.getElementById("chamilo-cookie-banner");
63+
if (!banner) {
64+
return;
65+
}
66+
67+
const more = document.getElementById("chamilo-cookie-banner-more");
68+
const toggle = document.getElementById("chamilo-cookie-banner-toggle");
69+
const accept = document.getElementById("chamilo-cookie-banner-accept");
70+
const noCookies = document.getElementById("chamilo-cookie-banner-nocookies");
71+
72+
// If cookies are disabled, show warning and disable accept action.
73+
if (typeof navigator !== "undefined" && navigator.cookieEnabled === false) {
74+
if (noCookies) noCookies.classList.remove("hidden");
75+
if (accept) accept.setAttribute("disabled", "disabled");
76+
if (accept) accept.classList.add("opacity-50", "cursor-not-allowed");
77+
return;
78+
}
79+
80+
if (toggle && more) {
81+
toggle.addEventListener("click", function () {
82+
more.classList.toggle("hidden");
83+
});
84+
}
85+
86+
if (accept) {
87+
accept.addEventListener("click", function () {
88+
const maxAge = 60 * 60 * 24 * 365; // 1 year
89+
let cookie = "ChamiloUsesCookies=ok; Path=/; Max-Age=" + maxAge + "; SameSite=Lax";
90+
91+
if (window.location && window.location.protocol === "https:") {
92+
cookie += "; Secure";
93+
}
94+
95+
document.cookie = cookie;
96+
banner.remove();
97+
});
98+
}
99+
})();
100+
</script>
101+
{% endif %}

0 commit comments

Comments
 (0)