Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assets/vue/components/layout/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<BaseSidebarPanelMenu v-model="menuItemsAfterMyCourse" />
</div>
<div class="app-sidebar__bottom">
<CategoryLinks category="menu_links" />
<PageList category-title="footer_private" />

<p v-html="t('Created with Chamilo copyright year', [currentYear])" />
Expand Down Expand Up @@ -82,6 +83,7 @@ import PageList from "../page/PageList.vue"
import { useEnrolledStore } from "../../store/enrolledStore"
import BaseIcon from "../basecomponents/BaseIcon.vue"
import BaseSidebarPanelMenu from "../basecomponents/BaseSidebarPanelMenu.vue"
import CategoryLinks from "../page/CategoryLinks.vue"

const { t } = useI18n()
const securityStore = useSecurityStore()
Expand Down
44 changes: 44 additions & 0 deletions assets/vue/components/page/CategoryLinks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div
v-if="items.length"
class="mb-2"
>
<div class="flex flex-col gap-1">
<a
v-for="it in items"
:key="it.slug"
class="text-sm text-gray-90 hover:underline"
:href="`/pages/${it.slug}`"
>
{{ it.title }}
</a>
</div>
</div>
</template>

<script setup>
import { onMounted, ref, watch } from "vue"
import { useI18n } from "vue-i18n"

const props = defineProps({
category: { type: String, required: true },
})

const { locale } = useI18n()
const items = ref([])

const load = async () => {
const loc = (locale.value || "").toString()
const url = `/pages/_category-links?category=${encodeURIComponent(props.category)}&locale=${encodeURIComponent(loc)}`
const res = await fetch(url, { headers: { Accept: "application/json" } })
if (!res.ok) {
items.value = []
return
}
const data = await res.json()
items.value = Array.isArray(data.items) ? data.items : []
}

onMounted(load)
watch(() => locale.value, load)
</script>
21 changes: 21 additions & 0 deletions src/CoreBundle/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,25 @@ public function preview(int $id, PageRepository $pageRepo): Response
'page' => $page,
]);
}

/**
* Public endpoint used by Vue (sidebar + login page) to render legal menu links.
*
* GET /pages/_category-links?category=menu_links&locale=fr_FR
*/
#[Route('/_category-links', name: 'public_page_category_links', methods: ['GET'])]
public function categoryLinks(Request $request, PageRepository $pageRepo): JsonResponse
{
$accessUrl = $this->accessUrlHelper->getCurrent();
$locale = trim((string) $request->query->get('locale', ''));
$category = trim((string) $request->query->get('category', ''));

if ('' === $category) {
return $this->json(['items' => []]);
}

$items = $pageRepo->findPublicLinksByCategoryWithLocaleFallback($accessUrl, $category, $locale);

return $this->json(['items' => $items]);
}
}
19 changes: 11 additions & 8 deletions src/CoreBundle/Helpers/PageHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
;
$this->pageCategoryRepository->update($indexCategory);

$indexCategory = (new PageCategory())
$faqCategory = (new PageCategory())
->setTitle('faq')
->setType('grid')
->setCreator($user)
;
$this->pageCategoryRepository->update($indexCategory);
$this->pageCategoryRepository->update($faqCategory);

$indexCategory = (new PageCategory())
$demoCategory = (new PageCategory())
->setTitle('demo')
->setType('grid')
->setCreator($user)
;
$this->pageCategoryRepository->update($indexCategory);
$this->pageCategoryRepository->update($demoCategory);

$page = (new Page())
->setTitle('Welcome')
Expand All @@ -87,7 +87,6 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
->setEnabled(true)
->setUrl($url)
;

$this->pageRepository->update($page);

$indexPage = (new Page())
Expand All @@ -106,17 +105,22 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
->setType('grid')
->setCreator($user)
;

$this->pageCategoryRepository->update($footerPublicCategory);

$footerPrivateCategory = (new PageCategory())
->setTitle('footer_private')
->setType('grid')
->setCreator($user)
;

$this->pageCategoryRepository->update($footerPrivateCategory);

$menuLinksCategory = (new PageCategory())
->setTitle('menu_links')
->setType('grid')
->setCreator($user)
;
$this->pageCategoryRepository->update($menuLinksCategory);

// Categories for extra content in admin blocks.
foreach (PageCategory::ADMIN_BLOCKS_CATEGORIES as $nameBlock) {
$usersAdminBlock = (new PageCategory())
Expand All @@ -132,7 +136,6 @@ public function createDefaultPages(User $user, AccessUrl $url, string $locale):
->setType('grid')
->setCreator($user)
;

$this->pageCategoryRepository->update($publicCategory);

$introductionCategory = (new PageCategory())
Expand Down
50 changes: 50 additions & 0 deletions src/CoreBundle/Migrations/Schema/V200/Version20260204142200.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/* For licensing terms, see /license.txt */

declare(strict_types=1);

namespace Chamilo\CoreBundle\Migrations\Schema\V200;

use Chamilo\CoreBundle\Entity\PageCategory;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;

final class Version20260204142200 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Adds the CMS PageCategory "menu_links" used to display legal information links in the sidebar and public homepage.';
}

public function up(Schema $schema): void
{
$pageCategoryRepo = $this->entityManager->getRepository(PageCategory::class);
$adminUser = $this->getAdmin();

// Create the "menu_links" category if it does not exist yet.
$existing = $pageCategoryRepo->findOneBy(['title' => 'menu_links']);
if ($existing) {
error_log('[MIGRATION] PageCategory "menu_links" already exists. Skipping.');

return;
}

$category = new PageCategory();
$category
->setTitle('menu_links')
->setType('grid')
->setCreator($adminUser)
;

$this->entityManager->persist($category);
$this->entityManager->flush();

error_log('[MIGRATION] PageCategory "menu_links" created successfully.');
}

public function down(Schema $schema): void
{
// Intentionally left empty (non-destructive migration).
}
}
67 changes: 67 additions & 0 deletions src/CoreBundle/Repository/PageRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,71 @@ public function delete(?Page $page = null): void
$this->getEntityManager()->flush();
}
}

public function findPublicLinksByCategoryWithLocaleFallback(
AccessUrl $accessUrl,
string $categoryTitle,
string $locale
): array {
$locale = trim($locale);
$prefix = '' !== $locale ? substr($locale, 0, 2) : '';

// Exact locale
if ('' !== $locale) {
$items = $this->createQueryBuilder('p')
->select('p.title AS title, p.slug AS slug')
->innerJoin('p.category', 'c')
->andWhere('p.enabled = true')
->andWhere('p.url = :url')
->andWhere('c.title = :cat')
->andWhere('p.locale = :loc')
->setParameter('url', $accessUrl)
->setParameter('cat', $categoryTitle)
->setParameter('loc', $locale)
->orderBy('p.title', 'ASC')
->getQuery()
->getArrayResult()
;

if (!empty($items)) {
return $items;
}
}

// Prefix locale (e.g. "fr")
if ('' !== $prefix) {
$items = $this->createQueryBuilder('p')
->select('p.title AS title, p.slug AS slug')
->innerJoin('p.category', 'c')
->andWhere('p.enabled = true')
->andWhere('p.url = :url')
->andWhere('c.title = :cat')
->andWhere('p.locale LIKE :prefix')
->setParameter('url', $accessUrl)
->setParameter('cat', $categoryTitle)
->setParameter('prefix', $prefix.'%')
->orderBy('p.title', 'ASC')
->getQuery()
->getArrayResult()
;

if (!empty($items)) {
return $items;
}
}

// Any locale
return $this->createQueryBuilder('p')
->select('p.title AS title, p.slug AS slug')
->innerJoin('p.category', 'c')
->andWhere('p.enabled = true')
->andWhere('p.url = :url')
->andWhere('c.title = :cat')
->setParameter('url', $accessUrl)
->setParameter('cat', $categoryTitle)
->orderBy('p.title', 'ASC')
->getQuery()
->getArrayResult()
;
}
}
3 changes: 3 additions & 0 deletions src/CoreBundle/Resources/views/Layout/base-layout.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@

{{ modals_block }}

{# Cookie banner (global) #}
{% include "@ChamiloCore/Layout/cookie_banner.html.twig" %}

{% include "@ChamiloCore/Layout/foot.html.twig" %}
</body>
</html>
101 changes: 101 additions & 0 deletions src/CoreBundle/Resources/views/Layout/cookie_banner.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{# Cookie banner (C1-compatible behavior) #}
{% set cookieWarningRaw = chamilo_settings_get('platform.cookie_warning')|default('')|trim|lower %}
{% set cookieWarningEnabled = (cookieWarningRaw == 'true') %}
{% set cookieAck = app.request.cookies.get('ChamiloUsesCookies') %}
{% set youAcceptCookiesText =
'By continuing to use this site, you declare you accept its use of cookies.'|trans
%}
{% set helpCookieUsageValidationHtml =
"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
%}
{% set noCookiesText =
'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
%}
{% if cookieWarningEnabled and cookieAck != 'ok' %}
<div id="chamilo-cookie-banner"
class="fixed inset-x-0 bottom-0 z-50 border-t border-warning/30 bg-warning/15 backdrop-blur shadow-lg">
<div class="mx-auto max-w-7xl px-4 py-3">
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div class="flex items-start gap-3">
<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"
aria-hidden="true">!</span>

<div class="text-sm text-gray-90">
<div class="leading-snug">
{{ youAcceptCookiesText }}
</div>

<div id="chamilo-cookie-banner-more"
class="mt-2 hidden text-sm leading-relaxed text-gray-90">
{{ helpCookieUsageValidationHtml|raw }}
</div>

<div id="chamilo-cookie-banner-nocookies"
class="mt-2 hidden text-sm font-semibold text-danger">
{{ noCookiesText }}
</div>
</div>
</div>

<div class="flex items-center gap-2 self-start md:self-auto">
<button type="button"
id="chamilo-cookie-banner-toggle"
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">
<span aria-hidden="true">+</span>
<span>{{ 'More'|trans }}</span>
</button>

<button type="button"
id="chamilo-cookie-banner-accept"
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">
<span>{{ 'Accept'|trans }}</span>
</button>
</div>
</div>
</div>
</div>

<script>
(function () {
"use strict";

const banner = document.getElementById("chamilo-cookie-banner");
if (!banner) {
return;
}

const more = document.getElementById("chamilo-cookie-banner-more");
const toggle = document.getElementById("chamilo-cookie-banner-toggle");
const accept = document.getElementById("chamilo-cookie-banner-accept");
const noCookies = document.getElementById("chamilo-cookie-banner-nocookies");

// If cookies are disabled, show warning and disable accept action.
if (typeof navigator !== "undefined" && navigator.cookieEnabled === false) {
if (noCookies) noCookies.classList.remove("hidden");
if (accept) accept.setAttribute("disabled", "disabled");
if (accept) accept.classList.add("opacity-50", "cursor-not-allowed");
return;
}

if (toggle && more) {
toggle.addEventListener("click", function () {
more.classList.toggle("hidden");
});
}

if (accept) {
accept.addEventListener("click", function () {
const maxAge = 60 * 60 * 24 * 365; // 1 year
let cookie = "ChamiloUsesCookies=ok; Path=/; Max-Age=" + maxAge + "; SameSite=Lax";

if (window.location && window.location.protocol === "https:") {
cookie += "; Secure";
}

document.cookie = cookie;
banner.remove();
});
}
})();
</script>
{% endif %}
Loading