Display: Add support for color theme per access_url & custom logo - refs BT#21621 #5578

pull/5485/merge
Angel Fernando Quiroz Campos 1 year ago committed by GitHub
parent ee0040f5a9
commit bb0a675295
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 36
      assets/css/app.scss
  2. 0
      assets/css/themes/chamilo/default.css
  3. BIN
      assets/css/themes/chamilo/fonts/OpenSans-Bold.woff
  4. BIN
      assets/css/themes/chamilo/fonts/OpenSans-Bold.woff2
  5. BIN
      assets/css/themes/chamilo/fonts/OpenSans-Light.woff
  6. BIN
      assets/css/themes/chamilo/fonts/OpenSans-Light.woff2
  7. BIN
      assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff
  8. BIN
      assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff2
  9. BIN
      assets/css/themes/chamilo/fonts/OpenSans.woff
  10. BIN
      assets/css/themes/chamilo/fonts/OpenSans.woff2
  11. 0
      assets/css/themes/chamilo/learnpath.css
  12. 4
      assets/vue/AppInstaller.vue
  13. 5
      assets/vue/components/layout/PlatformLogo.vue
  14. 50
      assets/vue/components/platform/ColorThemeSelector.vue
  15. 16
      assets/vue/composables/theme.js
  16. 21
      assets/vue/services/colorThemeService.js
  17. 4
      assets/vue/store/platformConfig.js
  18. 78
      assets/vue/views/admin/AdminConfigureColors.vue
  19. 14
      config/packages/oneup_flysystem.yaml
  20. 292
      public/main/admin/settings.lib.php
  21. 2
      public/main/admin/settings.php
  22. 81
      public/main/inc/lib/api.lib.php
  23. 2
      public/main/inc/lib/banner.lib.php
  24. 16
      public/main/inc/lib/display.lib.php
  25. 5
      public/main/inc/lib/pdf.lib.php
  26. 21
      public/main/inc/lib/template.lib.php
  27. 43
      public/main/lp/learnpath.class.php
  28. 22
      public/main/template/default/gradebook/custom_certificate.html.twig
  29. 4
      src/CoreBundle/Controller/PlatformConfigurationController.php
  30. 56
      src/CoreBundle/Controller/ThemeController.php
  31. 2
      src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php
  32. 57
      src/CoreBundle/Entity/AccessUrl.php
  33. 98
      src/CoreBundle/Entity/AccessUrlRelColorTheme.php
  34. 44
      src/CoreBundle/Entity/ColorTheme.php
  35. 21
      src/CoreBundle/EventListener/TwigListener.php
  36. 3
      src/CoreBundle/Migrations/Schema/V200/Version20170627122900.php
  37. 85
      src/CoreBundle/Migrations/Schema/V200/Version20231110194300.php
  38. 12
      src/CoreBundle/Migrations/Schema/V200/Version20240318105600.php
  39. 48
      src/CoreBundle/Migrations/Schema/V200/Version20240704185300.php
  40. 27
      src/CoreBundle/Repository/AccessUrlRelColorThemeRepository.php
  41. 16
      src/CoreBundle/Repository/ColorThemeRepository.php
  42. 5
      src/CoreBundle/Resources/config/settings.yml
  43. 3
      src/CoreBundle/Resources/views/Layout/base-layout.html.twig
  44. 17
      src/CoreBundle/Resources/views/Layout/head.html.twig
  45. 2
      src/CoreBundle/Resources/views/Mailer/Default/header.html.twig
  46. 10
      src/CoreBundle/ServiceHelper/AccessUrlHelper.php
  47. 2
      src/CoreBundle/ServiceHelper/MailHelper.php
  48. 103
      src/CoreBundle/ServiceHelper/ThemeHelper.php
  49. 2
      src/CoreBundle/Settings/PlatformSettingsSchema.php
  50. 4
      src/CoreBundle/Settings/SettingsManager.php
  51. 76
      src/CoreBundle/Settings/StylesheetsSettingsSchema.php
  52. 45
      src/CoreBundle/State/AccessUrlRelColorThemeStateProcessor.php
  53. 32
      src/CoreBundle/State/AccessUrlRelColorThemeStateProvider.php
  54. 34
      src/CoreBundle/State/ColorThemeStateProcessor.php
  55. 22
      src/CoreBundle/Twig/Extension/ChamiloExtension.php
  56. 45
      tests/CoreBundle/Controller/ThemeControllerTest.php
  57. 4
      tests/CoreBundle/Twig/SettingsHelperTest.php
  58. 32
      var/themes/chamilo/colors.css
  59. 0
      var/themes/chamilo/images/avatar.svg
  60. 0
      var/themes/chamilo/images/favicon.ico
  61. 0
      var/themes/chamilo/images/header-logo.png
  62. 0
      var/themes/chamilo/images/header-logo.svg
  63. 12
      webpack.config.js

@ -10,42 +10,6 @@
@import "tinymce/skins/ui/oxide/skin.css";
// TAILWIND COLOR DEFINITION https://tailwindcss.com/docs/customizing-colors#using-css-variables
@layer base {
:root {
--color-primary-base: 46 117 163;
--color-primary-gradient: 36 77 103;
--color-primary-button-text: 46 117 163;
--color-primary-button-alternative-text: 255 255 255;
--color-secondary-base: 243 126 47;
--color-secondary-gradient: 224 100 16;
--color-secondary-button-text: 255 255 255;
--color-tertiary-base: 51 51 51;
--color-tertiary-gradient: 0 0 0;
--color-tertiary-button-text: 255 255 255;
--color-success-base: 119 170 12;
--color-success-gradient: 83 127 0;
--color-success-button-text: 255 255 255;
--color-info-base: 13 123 253;
--color-info-gradient: 0 84 211;
--color-info-button-text: 255 255 255;
--color-warning-base: 245 206 1;
--color-warning-gradient: 186 152 0;
--color-warning-button-text: 0 0 0;
--color-danger-base: 223 59 59;
--color-danger-gradient: 180 0 21;
--color-danger-button-text: 255 255 255;
--color-form-base: 46 117 163;
}
}
@layer utilities {
.border-gray-300 {
--tw-border-opacity: 1;

@ -6,9 +6,9 @@
href="index.php"
>
<img
:src="'/themes/chamilo/images/header-logo.svg'"
alt="Chamilo"
src="/build/css/themes/chamilo/images/header-logo.png"
>
/>
</a>
<ol>
<li

@ -1,8 +1,9 @@
<script setup>
import headerLogoPath from "../../../../assets/css/themes/chamilo/images/header-logo.svg"
import { usePlatformConfig } from "../../store/platformConfig"
import { useVisualTheme } from "../../composables/theme"
const platformConfigStore = usePlatformConfig()
const { getThemeAssetUrl } = useVisualTheme()
const siteName = platformConfigStore.getSetting("platform.site_name")
</script>
@ -10,7 +11,7 @@ const siteName = platformConfigStore.getSetting("platform.site_name")
<template>
<img
:alt="siteName"
:src="headerLogoPath"
:title="siteName"
:src="getThemeAssetUrl('images/header-logo.png')"
/>
</template>

@ -0,0 +1,50 @@
<script setup>
import BaseSelect from "../basecomponents/BaseSelect.vue"
import { ref } from "vue"
import themeService from "../../services/colorThemeService"
import { useI18n } from "vue-i18n"
import { useNotification } from "../../composables/notification"
const modelValue = defineModel({
required: true,
type: Object,
})
const { t } = useI18n()
const { showErrorNotification } = useNotification()
const serverThemes = ref([])
const isServerThemesLoading = ref(true)
const loadThemes = async () => {
try {
const { items } = await themeService.findAllByCurrentUrl()
serverThemes.value = items.map((accessUrlRelColorTheme) => accessUrlRelColorTheme.colorTheme)
modelValue.value = items.find((accessUrlRelColorTheme) => accessUrlRelColorTheme.active)?.colorTheme["@id"]
} catch (e) {
showErrorNotification(t("We could not retrieve the themes"))
} finally {
isServerThemesLoading.value = false
}
}
defineExpose({
loadThemes,
})
loadThemes()
</script>
<template>
<BaseSelect
v-model="modelValue"
:is-loading="isServerThemesLoading"
:label="t('Color theme selected')"
:options="serverThemes"
allow-clear
option-label="title"
option-value="@id"
/>
</template>

@ -1,5 +1,6 @@
import { onMounted, ref, watch } from "vue"
import Color from "colorjs.io"
import { usePlatformConfig } from "../store/platformConfig"
export const useTheme = () => {
let colors = {}
@ -71,3 +72,18 @@ export const useTheme = () => {
setColors,
}
}
export function useVisualTheme() {
const platformConfigStore = usePlatformConfig()
const themeName = platformConfigStore.visualTheme
function getThemeAssetUrl(path) {
return `/themes/${platformConfigStore.visualTheme}/${path}`
}
return {
themeName,
getThemeAssetUrl,
}
}

@ -5,12 +5,10 @@ const url = "/api/color_themes"
/**
* Gets the color themes
*
* @returns {Promise<Array>}
* @returns {Promise<{totalItems, items}>}
*/
async function getThemes() {
const { items } = await baseService.getCollection(url)
return items
async function findAllByCurrentUrl() {
return await baseService.getCollection("/api/access_url_rel_color_themes")
}
/**
@ -35,7 +33,18 @@ async function updateTheme({ iri = null, title, colors }) {
})
}
/**
* @param {string} iri
* @returns {Promise<Object>}
*/
async function changePlatformColorTheme(iri) {
return baseService.post("/api/access_url_rel_color_themes", {
colorTheme: iri,
})
}
export default {
getThemes,
updateTheme,
findAllByCurrentUrl,
changePlatformColorTheme,
}

@ -7,6 +7,7 @@ export const usePlatformConfig = defineStore("platformConfig", () => {
const settings = ref([])
const studentView = ref("teacherview")
const plugins = ref([])
const visualTheme = ref("chamilo")
async function findSettingsRequest() {
isLoading.value = true
@ -14,6 +15,8 @@ export const usePlatformConfig = defineStore("platformConfig", () => {
try {
const { data } = await axios.get("/platform-config/list")
visualTheme.value = data.visual_theme
settings.value = data.settings
studentView.value = data.studentview
@ -44,5 +47,6 @@ export const usePlatformConfig = defineStore("platformConfig", () => {
initialize,
getSetting,
isStudentViewActive,
visualTheme,
}
})

@ -3,15 +3,26 @@
<SectionHeader :title="t('Configure Chamilo colors')" />
<div class="admin-colors__container">
<div class="admin-colors__settings">
<BaseSelect
<form class="admin-colors__form">
<ColorThemeSelector
ref="themeSelectorEl"
v-model="selectedTheme"
:is-loading="isServerThemesLoading"
:label="t('Theme selector')"
:options="serverThemes"
allow-clear
option-label="title"
option-value="@id"
/>
<BaseButton
:label="t('Save')"
icon="save"
type="primary"
@click="onClickChangeColorTheme"
/>
</form>
<BaseDivider />
<div class="admin-colors__settings">
<div class="admin-colors__settings-form">
<h3
v-t="selectedTheme ? 'Edit color theme' : 'Create color theme'"
class="admin-colors__settings-form-title"
/>
<BaseInputText
@ -202,7 +213,7 @@
<BaseDivider layout="vertical" />
<div class="admin-colors__preview">
<div class="admin-colors__settings-preview">
<h6>{{ t("You can see examples of how chamilo will look here") }}</h6>
<div>
@ -406,13 +417,14 @@
</div>
</div>
</div>
</div>
</template>
<script setup>
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import BaseMenu from "../../components/basecomponents/BaseMenu.vue"
import { onMounted, provide, ref, watch } from "vue"
import { provide, ref, watch } from "vue"
import BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue"
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue"
import BaseDialogConfirmCancel from "../../components/basecomponents/BaseDialogConfirmCancel.vue"
@ -425,14 +437,17 @@ import BaseInputDate from "../../components/basecomponents/BaseInputDate.vue"
import BaseToggleButton from "../../components/basecomponents/BaseToggleButton.vue"
import Color from "colorjs.io"
import themeService from "../../services/colorThemeService"
import BaseSelect from "../../components/basecomponents/BaseSelect.vue"
import BaseDivider from "../../components/basecomponents/BaseDivider.vue"
import SectionHeader from "../../components/layout/SectionHeader.vue"
import ColorThemeSelector from "../../components/platform/ColorThemeSelector.vue"
import colorThemeService from "../../services/colorThemeService"
const { t } = useI18n()
const { getColorTheme, getColors, setColors } = useTheme()
const { showSuccessNotification, showErrorNotification } = useNotification()
const themeSelectorEl = ref()
let colorPrimary = getColorTheme("--color-primary-base")
let colorPrimaryGradient = getColorTheme("--color-primary-gradient")
let colorPrimaryButtonText = getColorTheme("--color-primary-button-text")
@ -464,28 +479,9 @@ let colorDangerButtonText = getColorTheme("--color-danger-button-text")
let formColor = getColorTheme("--color-form-base")
const serverThemes = ref([])
const isServerThemesLoading = ref(true)
const themeTitle = ref()
const selectedTheme = ref()
onMounted(async () => {
await refreshThemes()
})
watch(selectedTheme, (newValue) => {
if (!newValue) {
themeTitle.value = ""
}
const found = serverThemes.value.find((e) => e["@id"] === newValue) ?? null
if (found) {
themeTitle.value = found.title
setColors(found.variables)
}
})
const saveColors = async () => {
try {
const updatedTheme = await themeService.updateTheme({
@ -496,7 +492,7 @@ const saveColors = async () => {
showSuccessNotification(t("Color updated"))
await refreshThemes()
await themeSelectorEl.value.loadThemes()
selectedTheme.value = updatedTheme["@id"]
} catch (error) {
@ -505,20 +501,6 @@ const saveColors = async () => {
}
}
const refreshThemes = async () => {
try {
serverThemes.value = await themeService.getThemes()
const found = serverThemes.value.find((e) => e.active) ?? null
if (found) {
selectedTheme.value = found["@id"]
}
isServerThemesLoading.value = false
} catch (error) {
showErrorNotification(t("We could not retrieve the themes"))
console.error(error)
}
}
const isAdvancedMode = ref(false)
watch(colorPrimary, (newValue) => {
@ -687,4 +669,10 @@ const isSorting = ref(false)
const isCustomizing = ref(false)
provide("isSorting", isSorting)
provide("isCustomizing", isCustomizing)
async function onClickChangeColorTheme() {
if (selectedTheme.value) {
await colorThemeService.changePlatformColorTheme(selectedTheme.value)
}
}
</script>

@ -14,6 +14,13 @@ oneup_flysystem:
local:
location: '%kernel.project_dir%/var/cache/resource'
themes_adapter:
local:
location: '%kernel.project_dir%/var/themes'
themes_cache_adapter:
local:
location: '%kernel.project_dir%/var/cache/themes'
filesystems:
asset:
adapter: asset_adapter
@ -30,3 +37,10 @@ oneup_flysystem:
adapter: resource_cache_adapter
mount: resource_cache
visibility: private
themes:
adapter: themes_adapter
mount: themes
themes_cache:
adapter: themes_cache_adapter
mount: themes_cache

@ -19,7 +19,7 @@ use Chamilo\CoreBundle\Component\Utils\StateIcon;
*
* @since Chamilo 1.8.7
*/
define('CSS_UPLOAD_PATH', api_get_path(SYS_PATH).'Resources/public/css/themes/');
define('CSS_UPLOAD_PATH', api_get_path(SYMFONY_SYS_PATH).'var/themes/');
/**
* This function allows easy activating and inactivating of regions.
@ -262,251 +262,6 @@ function handlePlugins()
echo '</form>';
}
/**
* This function allows the platform admin to choose the default stylesheet.
*
* @author Patrick Cool <patrick.cool@UGent.be>, Ghent University
* @author Julio Montoya <gugli100@gmail.com>, Chamilo
*/
function handleStylesheets()
{
$is_style_changeable = isStyleChangeable();
$allowedFileTypes = ['png'];
$form = new FormValidator(
'stylesheet_upload',
'post',
'settings.php?category=Stylesheets#tabs-3'
);
$form->addElement(
'text',
'name_stylesheet',
get_lang('Name of the stylesheet'),
['size' => '40', 'maxlength' => '40']
);
$form->addRule(
'name_stylesheet',
get_lang('Required field'),
'required'
);
$form->addElement(
'file',
'new_stylesheet',
get_lang('New stylesheet file')
);
$allowed_file_types = getAllowedFileTypes();
$form->addRule(
'new_stylesheet',
get_lang('Invalid extension').' ('.implode(',', $allowed_file_types).')',
'filetype',
$allowed_file_types
);
$form->addRule(
'new_stylesheet',
get_lang('Required field'),
'required'
);
$form->addButtonUpload(get_lang('Upload'), 'stylesheet_upload');
$show_upload_form = false;
$urlId = api_get_current_access_url_id();
if (!is_writable(CSS_UPLOAD_PATH)) {
echo Display::return_message(
CSS_UPLOAD_PATH.get_lang('is not writeable'),
'error',
false
);
} else {
// Uploading a new stylesheet.
if (1 == $urlId) {
$show_upload_form = true;
} else {
if ($is_style_changeable) {
$show_upload_form = true;
}
}
}
// Stylesheet upload.
if (isset($_POST['stylesheet_upload'])) {
if ($form->validate()) {
$values = $form->exportValues();
$picture_element = $form->getElement('new_stylesheet');
$picture = $picture_element->getValue();
$result = uploadStylesheet($values, $picture);
// Add event to the system log.
$user_id = api_get_user_id();
$category = $_GET['category'];
Event::addEvent(
LOG_CONFIGURATION_SETTINGS_CHANGE,
LOG_CONFIGURATION_SETTINGS_CATEGORY,
$category,
api_get_utc_datetime(),
$user_id
);
if ($result) {
echo Display::return_message(get_lang('The stylesheet has been added'));
}
}
}
// Current style.
$selected = $currentStyle = api_get_setting('stylesheets');
$styleFromDatabase = api_get_settings_params_simple(
['variable = ? AND access_url = ?' => ['stylesheets', api_get_current_access_url_id()]]
);
if ($styleFromDatabase) {
$selected = $currentStyle = $styleFromDatabase['selected_value'];
}
if (isset($_POST['preview'])) {
$selected = $currentStyle = Security::remove_XSS($_POST['style']);
}
$themeDir = Template::getThemeDir($selected);
$dir = api_get_path(SYS_PUBLIC_PATH).'css/'.$themeDir.'/images/';
$url = api_get_path(WEB_CSS_PATH).'/'.$themeDir.'/images/';
$logoFileName = 'header-logo.png';
$newLogoFileName = 'header-logo-custom'.api_get_current_access_url_id().'.png';
$webPlatformLogoPath = ChamiloApi::getPlatformLogoPath($selected);
$logoForm = new FormValidator(
'logo_upload',
'post',
'settings.php?category=Stylesheets#tabs-2'
);
$logoForm->addHtml(
Display::return_message(
sprintf(
get_lang('The logo must be of %s px in size and in %s format'),
'250 x 70',
'PNG'
),
'info'
)
);
if (null !== $webPlatformLogoPath) {
$logoForm->addLabel(
get_lang('Current logo'),
'<img id="header-logo-custom" src="'.$webPlatformLogoPath.'?'.time().'">'
);
}
$logoForm->addFile('new_logo', get_lang('Update logo'));
if ($is_style_changeable) {
$logoGroup = [
$logoForm->addButtonUpload(get_lang('Upload'), 'logo_upload', true),
$logoForm->addButtonCancel(get_lang('Reset'), 'logo_reset', true),
];
$logoForm->addGroup($logoGroup);
}
if (isset($_POST['logo_reset'])) {
if (is_file($dir.$newLogoFileName)) {
unlink($dir.$newLogoFileName);
echo Display::return_message(get_lang('Original logo recovered'));
echo '<script>'
.'$("#header-logo").attr("src","'.$url.$logoFileName.'");'
.'</script>';
}
} elseif (isset($_POST['logo_upload'])) {
$logoForm->addRule(
'new_logo',
get_lang('Invalid extension').' ('.implode(',', $allowedFileTypes).')',
'filetype',
$allowedFileTypes
);
$logoForm->addRule(
'new_logo',
get_lang('Required field'),
'required'
);
if ($logoForm->validate()) {
$imageInfo = getimagesize($_FILES['new_logo']['tmp_name']);
$width = $imageInfo[0];
$height = $imageInfo[1];
if ($width <= 250 && $height <= 70) {
if (is_file($dir.$newLogoFileName)) {
unlink($dir.$newLogoFileName);
}
$status = move_uploaded_file(
$_FILES['new_logo']['tmp_name'],
$dir.$newLogoFileName
);
if ($status) {
echo Display::return_message(get_lang('New logo uploaded'));
echo '<script>'
.'$("#header-logo").attr("src","'.$url.$newLogoFileName.'");'
.'</script>';
} else {
echo Display::return_message('Error - '.get_lang('No file was uploaded.'), 'error');
}
} else {
Display::return_message('Error - '.get_lang('Image dimensions do not match the requirements. Please check the suggestions next to the image field.'), 'error');
}
}
}
if (isset($_POST['download'])) {
generateCSSDownloadLink($selected);
}
$form_change = new FormValidator(
'stylesheet_upload',
'post',
api_get_self().'?category=Stylesheets',
null,
['id' => 'stylesheets_id']
);
$styles = $form_change->addSelectTheme(
'style',
get_lang('Name of the stylesheet')
);
$styles->setSelected($currentStyle);
if ($is_style_changeable) {
$group = [
$form_change->addButtonSave(get_lang('Save settings'), 'save', true),
$form_change->addButtonPreview(get_lang('Preview'), 'preview', true),
$form_change->addButtonDownload(get_lang('Download'), 'download', true),
];
$form_change->addGroup($group);
if ($show_upload_form) {
echo Display::tabs(
[get_lang('Update'), get_lang('Update logo'), get_lang('New stylesheet file')],
[$form_change->returnForm(), $logoForm->returnForm(), $form->returnForm()]
);
} else {
$form_change->display();
}
// Little hack to update the logo image in update form when submiting
if (isset($_POST['logo_reset'])) {
echo '<script>'
.'$("#header-logo-custom").attr("src","'.$url.$logoFileName.'");'
.'</script>';
} elseif (isset($_POST['logo_upload']) && is_file($dir.$newLogoFileName)) {
echo '<script>'
.'$("#header-logo-custom").attr("src","'.$url.$newLogoFileName.'");'
.'</script>';
}
} else {
$form_change->freeze();
}
}
/**
* Creates the folder (if needed) and uploads the stylesheet in it.
*
@ -634,7 +389,7 @@ function uploadStylesheet($values, $picture)
$fs = new Filesystem();
$fs->mirror(
CSS_UPLOAD_PATH,
api_get_path(SYS_PATH).'web/css/themes/',
api_get_path(SYMFONY_SYS_PATH).'var/themes/',
null,
['override' => true]
);
@ -708,27 +463,6 @@ function storePlugins()
}
}
/**
* This function allows the platform admin to choose which should be the default stylesheet.
*
* @author Patrick Cool <patrick.cool@UGent.be>, Ghent University
*/
function storeStylesheets()
{
// Insert the stylesheet.
if (isStyle($_POST['style'])) {
api_set_setting(
'stylesheets',
$_POST['style'],
null,
'stylesheets',
api_get_current_access_url_id()
);
}
return true;
}
/**
* This function checks if the given style is a recognize style that exists in the css directory as
* a standalone directory.
@ -1896,28 +1630,6 @@ function generateCSSDownloadLink($style)
}
}
/**
* Helper function to tell if the style is changeable in the current URL.
*
* @return bool $changeable Whether the style can be changed in this URL or not
*/
function isStyleChangeable()
{
$changeable = false;
$urlId = api_get_current_access_url_id();
if ($urlId) {
$style_info = api_get_settings('stylesheets', '', 1, 0);
$url_info = api_get_access_url($urlId);
if (1 == $style_info[0]['access_url_changeable'] && 1 == $url_info['active']) {
$changeable = true;
}
} else {
$changeable = true;
}
return $changeable;
}
/**
* Get all settings of one category prepared for display in admin/settings.php.
*

@ -57,7 +57,7 @@ $settings = null;
// Build the form.
if (!empty($_GET['category']) &&
!in_array($_GET['category'], ['Plugins', 'stylesheets', 'Search'])
!in_array($_GET['category'], ['Plugins', 'Search'])
) {
$my_category = isset($_GET['category']) ? $_GET['category'] : null;
$settings_array = getCategorySettings($my_category);

@ -12,6 +12,7 @@ use Chamilo\CoreBundle\Entity\UserCourseCategory;
use Chamilo\CoreBundle\Exception\NotAllowedException;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\ServiceHelper\MailHelper;
use Chamilo\CoreBundle\ServiceHelper\ThemeHelper;
use Chamilo\CourseBundle\Entity\CGroup;
use Chamilo\CourseBundle\Entity\CLp;
use ChamiloSession as Session;
@ -2699,8 +2700,6 @@ function api_get_setting($variable, $isArray = false, $key = null)
}
return 'prod';
case 'stylesheets':
$variable = 'platform.theme';
// deprecated settings
// no break
case 'openid_authentication':
@ -3739,83 +3738,13 @@ function api_get_language_from_iso($code)
}
/**
* Returns the name of the visual (CSS) theme to be applied on the current page.
* The returned name depends on the platform, course or user -wide settings.
*
* @return string The visual theme's name, it is the name of a folder inside web/css/themes
* Shortcut to ThemeHelper::getVisualTheme()
*/
function api_get_visual_theme()
function api_get_visual_theme(): string
{
static $visual_theme;
if (!isset($visual_theme)) {
// Get style directly from DB
/*$styleFromDatabase = api_get_settings_params_simple(
[
'variable = ? AND access_url = ?' => [
'stylesheets',
api_get_current_access_url_id(),
],
]
);
if ($styleFromDatabase) {
$platform_theme = $styleFromDatabase['selected_value'];
} else {
$platform_theme = api_get_setting('stylesheets');
}*/
$platform_theme = api_get_setting('stylesheets');
// Platform's theme.
$visual_theme = $platform_theme;
if ('true' == api_get_setting('user_selected_theme')) {
$user_info = api_get_user_info();
if (isset($user_info['theme'])) {
$user_theme = $user_info['theme'];
if (!empty($user_theme)) {
$visual_theme = $user_theme;
// User's theme.
}
}
}
$course_id = api_get_course_id();
if (!empty($course_id)) {
if ('true' == api_get_setting('allow_course_theme')) {
$course_theme = api_get_course_setting('course_theme', $course_id);
if (!empty($course_theme) && -1 != $course_theme) {
if (!empty($course_theme)) {
// Course's theme.
$visual_theme = $course_theme;
}
}
$allow_lp_theme = api_get_course_setting('allow_learning_path_theme');
if (1 == $allow_lp_theme) {
/*global $lp_theme_css, $lp_theme_config;
// These variables come from the file lp_controller.php.
if (!$lp_theme_config) {
if (!empty($lp_theme_css)) {
// LP's theme.
$visual_theme = $lp_theme_css;
}
}*/
}
}
}
if (empty($visual_theme)) {
$visual_theme = 'chamilo';
}
/*global $lp_theme_log;
if ($lp_theme_log) {
$visual_theme = $platform_theme;
}*/
}
$themeHelper = Container::$container->get(ThemeHelper::class);
return $visual_theme;
return $themeHelper->getVisualTheme();
}
/**

@ -45,7 +45,7 @@ function getCustomTabs()
/**
* Return the active logo of the portal, based on a series of settings.
*
* @param string $theme The name of the theme folder from web/css/themes/
* @param string $theme The name of the theme folder from var/themes/
* @param bool $responsive add class img-responsive
*
* @return string HTML string with logo as an HTML element

@ -9,7 +9,7 @@ use Chamilo\CoreBundle\Component\Utils\ToolIcon;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use Chamilo\CoreBundle\ServiceHelper\ThemeHelper;
use ChamiloSession as Session;
use Symfony\Component\HttpFoundation\Response;
@ -695,7 +695,7 @@ class Display
if (is_file($alternateCssPath.$theme.$image)) {
$icon = $alternateWebCssPath.$theme.$image;
}
// Checking the theme icons folder example: app/Resources/public/css/themes/chamilo/icons/XXX
// Checking the theme icons folder example: var/themes/chamilo/icons/XXX
if (is_file($alternateCssPath.$theme.$size_extra.$image)) {
$icon = $alternateWebCssPath.$theme.$size_extra.$image;
} elseif (is_file($code_path.'img/icons/'.$size_extra.$image)) {
@ -2672,15 +2672,13 @@ class Display
return false;
}
$colorThemeRepo = Container::$container->get(ColorThemeRepository::class);
$router = Container::getRouter();
$themeHelper = Container::$container->get(ThemeHelper::class);
$colorTheme = $colorThemeRepo->getActiveOne();
$colorThemeItem = '';
$themeColorsUrl = $themeHelper->getThemeAssetUrl('colors.css');
if ($colorTheme) {
$colorThemeItem = '{ type: "stylesheet", src: "'.$router->generate('chamilo_color_theme').'" },';
}
$colorThemeItem = $themeColorsUrl
? '{ type: "stylesheet", src: "'.$themeColorsUrl.'" },'
: '';
return '$.frameReady(function() {},
"'.$frameName.'",

@ -3,6 +3,8 @@
/* See license terms in /license.txt */
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\ServiceHelper\ThemeHelper;
use Masterminds\HTML5;
use Mpdf\Mpdf;
use Mpdf\Output\Destination;
@ -476,8 +478,9 @@ class PDF
}
if ($addDefaultCss) {
$themeHelper = Container::$container->get(ThemeHelper::class);
$basicStyles = [
api_get_path(SYS_PUBLIC_PATH).'build/css/themes/'.api_get_visual_theme().'/default.css',
$themeHelper->getThemeAssetUrl('default.css'),
];
foreach ($basicStyles as $style) {
$cssContent = file_get_contents($style);

@ -761,27 +761,6 @@ class Template
}
}
/**
* @param string $theme
*
* @return string
*/
public static function getPortalIcon($theme)
{
// Default root chamilo favicon
$icon = 'favicon.ico';
// Added to verify if in the current Chamilo Theme exist a favicon
$themeUrl = api_get_path(SYS_CSS_PATH).'themes/'.$theme.'/images/';
// If exist pick the current chamilo theme favicon.
if (is_file($themeUrl.'favicon.ico')) {
$icon = 'build/css/themes/'.$theme.'/images/favicon.ico';
}
return $icon;
}
/**
* Show footer js template.
*/

@ -6,6 +6,7 @@ use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\ResourceLink;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
use Chamilo\CoreBundle\ServiceHelper\ThemeHelper;
use Chamilo\CourseBundle\Entity\CLpRelUser;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
@ -8391,24 +8392,27 @@ class learnpath
/**
* In order to use the lp icon option you need to create the "lp_icon" LP extra field
* and put the images in.
*
* @return array
*/
public static function getIconSelect()
public static function getIconSelect(): array
{
$theme = api_get_visual_theme();
$path = api_get_path(SYS_PUBLIC_PATH).'css/themes/'.$theme.'/lp_icons/';
$icons = ['' => get_lang('Please select an option')];
$theme = Container::$container->get(ThemeHelper::class)->getVisualTheme();
$filesystem = Container::$container->get('oneup_flysystem.themes_filesystem');
if (is_dir($path)) {
$finder = new Finder();
$finder->files()->in($path);
$allowedExtensions = ['jpeg', 'jpg', 'png'];
/** @var SplFileInfo $file */
foreach ($finder as $file) {
if (in_array(strtolower($file->getExtension()), $allowedExtensions)) {
$icons[$file->getFilename()] = $file->getFilename();
if (!$filesystem->directoryExists("$theme/lp_icons")) {
return [];
}
$icons = ['' => get_lang('Please select an option')];
$iconFiles = $filesystem->listContents("$theme/lp_icons");
$allowedExtensions = ['image/jpeg', 'image/jpg', 'image/png'];
foreach ($iconFiles as $iconFile) {
$mimeType = $filesystem->mimeType($iconFile->path());
if (in_array($mimeType, $allowedExtensions)) {
$basename = basename($iconFile->path());
$icons[$basename] = $basename;
}
}
@ -8432,12 +8436,7 @@ class learnpath
return $icon;
}
/**
* @param int $lpId
*
* @return string
*/
public static function getSelectedIconHtml($lpId)
public static function getSelectedIconHtml(int $lpId): string
{
$icon = self::getSelectedIcon($lpId);
@ -8445,8 +8444,8 @@ class learnpath
return '';
}
$theme = api_get_visual_theme();
$path = api_get_path(WEB_PUBLIC_PATH).'css/themes/'.$theme.'/lp_icons/'.$icon;
$themeHelper = Container::$container->get(ThemeHelper::class);
$path = $themeHelper->getThemeAssetUrl("lp_icons/$icon");
return Display::img($path);
}

@ -9,30 +9,30 @@
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td colspan="3">
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/cuadro.png' }}">
<img src="{{ theme_asset('images/cuadre.png') }}">
</td>
</tr>
<tr>
<td>
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/cuadro.png' }}">
<img src="{{ theme_asset('images/cuadro.png') }}">
</td>
<td>
<table border="0" bgcolor="#92c647" cellpadding="0" cellspacing="0" align="center" width="100%">
<tr>
<td style="border-collapse: collapse; padding: 0;" bgcolor="#92c647"><img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/header_top.png' }}" style="display: block;"></td>
<td style="border-collapse: collapse; padding: 0;" bgcolor="#92c647"><img src="{{ theme_asset('images/header_top.png') }}" style="display: block;"></td>
</tr>
<tr>
<td style="border-collapse: collapse; padding: 0;">
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<tr>
<td style="border-collapse: collapse; padding: 0;" bgcolor="#92c647" width=58 height=91>
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/lado-b.png' }}" style="display:block;">
<img src="{{ theme_asset('images/lado-b.png') }}" style="display:block;">
</td>
<td bgcolor="#92c647" width=700 height=91 style="font-family:CourierSans-Light; font-weight: bold; line-height: 47px; color:#FFF; padding-bottom: 10px; font-size: 45px;">
{{ 'Certificate of participation' | trans | raw }}
</td>
<td style="border-collapse: collapse; padding: 0;" bgcolor="#92c647" width=58 height=91>
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/lado-header.png' }}" style="display:block;">
<img src="{{ theme_asset('/images/lado-header.png') }}" style="display:block;">
</td>
</tr>
</table>
@ -42,7 +42,7 @@
<td style="border-collapse: collapse; padding: 0;">
<table bgcolor="#FFFFFF" border="0" cellspacing="0" cellpadding="0" width="100%" height=900>
<tr>
<td bgcolor="#92c647" height=755 style="border-collapse: collapse; padding: 0;"><img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/lado-a.png' }}" style="display:block;"></td>
<td bgcolor="#92c647" height=755 style="border-collapse: collapse; padding: 0;"><img src="{{ theme_asset('images/lado-a.png') }}" style="display:block;"></td>
<td height=755 style="font-family:CourierSans-Light; line-height: 22px; color:#40ad49; padding: 40px; font-size: 18px;" valign="top">
<h3 style="color: #672290; font-size: 24px;">
{{ complete_name }}
@ -75,7 +75,7 @@
<br />
</td>
<td height=755 bgcolor="#92c647" style="border-collapse: collapse; padding: 0;">
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/lado-b.png' }}" style="display:block;">
<img src="{{ theme_asset('images/lado-b.png') }}" style="display:block;">
</td>
</tr>
</table>
@ -86,12 +86,12 @@
<table border="0" cellspacing="0" cellpadding="0" width="100%" height="91">
<tr>
<td bgcolor="#92c647" width=58 height=91 style="border-collapse: collapse; padding: 0;">
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/lado-b.png' }}" style="display:block;"></td>
<img src="{{ theme_asset('images/lado-b.png') }}" style="display:block;"></td>
<td bgcolor="#92c647" width=500 height=91 style="border-collapse: collapse; padding: 0; font-family:CourierSans-Light; line-height: 18px; color:#FFF;">
{{ 'Certificate Footer' | trans | raw }}
</td>
<td bgcolor="#92c647" width=245 height=91 style="border-collapse: collapse; padding: 0;">
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/lado-footer.png' }}" style="display:block; "></td>
<img src="{{ theme_asset('images/lado-footer.png') }}" style="display:block; "></td>
</tr>
</table>
</td>
@ -99,12 +99,12 @@
</table>
</td>
<td>
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/cuadro.png' }}">
<img src="{{ theme_asset('images/cuadro.png') }}">
</td>
</tr>
<tr>
<td colspan="3">
<img src="{{ url('index') ~ 'build/css/themes/' ~ stylesheet_name ~ '/images/cuadro.png' }}">
<img src="{{ theme_asset('images/cuadro.png') }}">
</td>
</tr>
</table>

@ -8,6 +8,7 @@ namespace Chamilo\CoreBundle\Controller;
use bbb;
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\ServiceHelper\ThemeHelper;
use Chamilo\CoreBundle\ServiceHelper\TicketProjectHelper;
use Chamilo\CoreBundle\ServiceHelper\UserHelper;
use Chamilo\CoreBundle\Settings\SettingsManager;
@ -27,6 +28,7 @@ class PlatformConfigurationController extends AbstractController
public function __construct(
private readonly TicketProjectHelper $ticketProjectHelper,
private readonly UserHelper $userHelper,
private readonly ThemeHelper $themeHelper,
) {}
#[Route('/list', name: 'platform_config_list', methods: ['GET'])]
@ -38,6 +40,7 @@ class PlatformConfigurationController extends AbstractController
'settings' => [],
'studentview' => $requestSession->get('studentview'),
'plugins' => [],
'visual_theme' => $this->themeHelper->getVisualTheme(),
];
$variables = [];
@ -45,7 +48,6 @@ class PlatformConfigurationController extends AbstractController
$variables = [
'platform.site_name',
'platform.timezone',
'platform.theme',
'platform.registered',
'platform.donotlistcampus',
'platform.load_term_conditions_section',

@ -6,37 +6,53 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/themes')]
class ThemeController extends AbstractController
{
public function __construct(
private readonly ParameterBagInterface $parameterBag,
private readonly ColorThemeRepository $colorThemeRepository,
) {}
/**
* @throws FilesystemException
*/
#[Route('/{name}/{path}', name: 'theme_asset', requirements: ['path' => '.+'])]
public function index(
string $name,
string $path,
#[Autowire(service: 'oneup_flysystem.themes_filesystem')] FilesystemOperator $filesystem
): Response {
$themeDir = basename($name);
if (!$filesystem->directoryExists($themeDir)) {
throw $this->createNotFoundException("The folder name does not exist.");
}
#[Route('/theme/colors.css', name: 'chamilo_color_theme', methods: ['GET'])]
public function colorTheme(): Response
{
$response = new Response('');
$filePath = $themeDir.DIRECTORY_SEPARATOR.$path;
$colorTheme = $this->colorThemeRepository->getActiveOne();
if (!$filesystem->fileExists($filePath)) {
throw $this->createNotFoundException("The requested file does not exist.");
}
if ($colorTheme) {
$fs = new Filesystem();
$path = $this->parameterBag->get('kernel.project_dir')."/var/theme/{$colorTheme->getSlug()}/colors.css";
$response = new StreamedResponse(function () use ($filesystem, $filePath) {
$outputStream = fopen('php://output', 'wb');
if ($fs->exists($path)) {
$response = $this->file($path);
}
}
$fileStream = $filesystem->readStream($filePath);
stream_copy_to_stream($fileStream, $outputStream);
});
$mimeType = $filesystem->mimeType($filePath);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_INLINE, basename($path));
$response->headers->add(['Content-Type' => 'text/css']);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Type', $mimeType ?: 'application/octet-stream');
return $response;
}

@ -1114,7 +1114,7 @@ class SettingsCurrentFixtures extends Fixture implements FixtureGroupInterface
[
'name' => 'pdf_logo_header',
'title' => 'PDF header logo',
'comment' => 'Whether to use the image at css/themes/[your-css]/images/pdf_logo_header.png as the PDF header logo for all PDF exports (instead of the normal portal logo)',
'comment' => 'Whether to use the image at var/themes/[your-theme]/images/pdf_logo_header.png as the PDF header logo for all PDF exports (instead of the normal portal logo)',
],
],
'mail' => [

@ -147,6 +147,12 @@ class AccessUrl extends AbstractResource implements ResourceInterface, Stringabl
#[ORM\Column(name: 'email', type: 'string', length: 255, nullable: true)]
protected ?string $email = null;
/**
* @var Collection<int, AccessUrlRelColorTheme>
*/
#[ORM\OneToMany(mappedBy: 'url', targetEntity: AccessUrlRelColorTheme::class, cascade: ['persist'], orphanRemoval: true)]
private Collection $colorThemes;
public function __construct()
{
$this->description = '';
@ -159,6 +165,7 @@ class AccessUrl extends AbstractResource implements ResourceInterface, Stringabl
$this->sessionCategories = new ArrayCollection();
$this->courseCategory = new ArrayCollection();
$this->children = new ArrayCollection();
$this->colorThemes = new ArrayCollection();
}
public function __toString(): string
@ -490,4 +497,54 @@ class AccessUrl extends AbstractResource implements ResourceInterface, Stringabl
{
return $this->setUrl($name);
}
public function getActiveColorTheme(): ?AccessUrlRelColorTheme
{
$criteria = Criteria::create();
$criteria->where(
Criteria::expr()->eq('active', true)
);
return $this->colorThemes->matching($criteria)->first() ?: null;
}
/**
* @return Collection<int, AccessUrlRelColorTheme>
*/
public function getColorThemes(): Collection
{
return $this->colorThemes;
}
public function addColorTheme(AccessUrlRelColorTheme $colorTheme): static
{
if (!$this->colorThemes->contains($colorTheme)) {
$this->colorThemes->add($colorTheme);
$colorTheme->setUrl($this);
}
return $this;
}
public function removeColorTheme(AccessUrlRelColorTheme $colorTheme): static
{
if ($this->colorThemes->removeElement($colorTheme)) {
// set the owning side to null (unless already changed)
if ($colorTheme->getUrl() === $this) {
$colorTheme->setUrl(null);
}
}
return $this;
}
public function getColorThemeByTheme(ColorTheme $theme): ?AccessUrlRelColorTheme
{
$criteria = Criteria::create();
$criteria->where(
Criteria::expr()->eq('colorTheme', $theme)
);
return $this->colorThemes->matching($criteria)->first() ?: null;
}
}

@ -0,0 +1,98 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use Chamilo\CoreBundle\Repository\AccessUrlRelColorThemeRepository;
use Chamilo\CoreBundle\State\AccessUrlRelColorThemeStateProcessor;
use Chamilo\CoreBundle\State\AccessUrlRelColorThemeStateProvider;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new Post(),
new GetCollection(),
],
normalizationContext: [
'groups' => ['access_url_rel_color_theme:read'],
],
denormalizationContext: [
'groups' => ['access_url_rel_color_theme:write'],
],
paginationEnabled: false,
security: "is_granted('ROLE_ADMIN')",
provider: AccessUrlRelColorThemeStateProvider::class,
processor: AccessUrlRelColorThemeStateProcessor::class,
)]
#[ORM\Entity(repositoryClass: AccessUrlRelColorThemeRepository::class)]
class AccessUrlRelColorTheme
{
use TimestampableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'colorThemes')]
#[ORM\JoinColumn(nullable: false)]
private ?AccessUrl $url = null;
#[Groups(['access_url_rel_color_theme:write', 'access_url_rel_color_theme:read'])]
#[ORM\ManyToOne(inversedBy: 'urls')]
#[ORM\JoinColumn(nullable: false)]
private ?ColorTheme $colorTheme = null;
#[Groups(['access_url_rel_color_theme:read'])]
#[ORM\Column]
private bool $active = false;
public function getId(): ?int
{
return $this->id;
}
public function getUrl(): ?AccessUrl
{
return $this->url;
}
public function setUrl(?AccessUrl $url): static
{
$this->url = $url;
return $this;
}
public function getColorTheme(): ?ColorTheme
{
return $this->colorTheme;
}
public function setColorTheme(?ColorTheme $colorTheme): static
{
$this->colorTheme = $colorTheme;
return $this;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): static
{
$this->active = $active;
return $this;
}
}

@ -7,11 +7,12 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Chamilo\CoreBundle\State\ColorThemeStateProcessor;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Serializer\Annotation\Groups;
@ -21,7 +22,6 @@ use Symfony\Component\Serializer\Annotation\Groups;
operations: [
new Post(),
new Put(),
new GetCollection(),
],
denormalizationContext: [
'groups' => ['color_theme:write'],
@ -39,7 +39,7 @@ class ColorTheme
#[ORM\Column]
private ?int $id = null;
#[Groups(['color_theme:write'])]
#[Groups(['color_theme:write', 'access_url_rel_color_theme:read'])]
#[ORM\Column(length: 255)]
private string $title;
@ -54,8 +54,16 @@ class ColorTheme
#[ORM\Column(length: 255)]
private ?string $slug = null;
#[ORM\Column]
private bool $active = false;
/**
* @var Collection<int, AccessUrlRelColorTheme>
*/
#[ORM\OneToMany(mappedBy: 'colorTheme', targetEntity: AccessUrlRelColorTheme::class, orphanRemoval: true)]
private Collection $urls;
public function __construct()
{
$this->urls = new ArrayCollection();
}
public function getId(): ?int
{
@ -98,14 +106,32 @@ class ColorTheme
return $this;
}
public function isActive(): ?bool
/**
* @return Collection<int, AccessUrlRelColorTheme>
*/
public function getUrls(): Collection
{
return $this->active;
return $this->urls;
}
public function setActive(bool $active): static
public function addUrl(AccessUrlRelColorTheme $url): static
{
$this->active = $active;
if (!$this->urls->contains($url)) {
$this->urls->add($url);
$url->setColorTheme($this);
}
return $this;
}
public function removeUrl(AccessUrlRelColorTheme $url): static
{
if ($this->urls->removeElement($url)) {
// set the owning side to null (unless already changed)
if ($url->getColorTheme() === $this) {
$url->setColorTheme(null);
}
}
return $this;
}

@ -6,10 +6,8 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\EventListener;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\SerializerInterface;
@ -25,8 +23,6 @@ class TwigListener
private readonly SerializerInterface $serializer,
private readonly TokenStorageInterface $tokenStorage,
private readonly LanguageRepository $languageRepository,
private readonly ColorThemeRepository $colorThemeRepository,
private readonly RouterInterface $router,
) {}
public function __invoke(ControllerEvent $event): void
@ -54,22 +50,5 @@ class TwigListener
$this->twig->addGlobal('user_json', $data ?? json_encode([]));
$this->twig->addGlobal('access_url_id', $request->getSession()->get('access_url_id'));
$this->twig->addGlobal('languages_json', json_encode($languages));
$this->loadColorTheme();
}
private function loadColorTheme(): void
{
$link = null;
$colorTheme = $this->colorThemeRepository->getActiveOne();
if ($colorTheme) {
$path = $this->router->generate('chamilo_color_theme');
$link = '<link rel="stylesheet" href="'.$path.'">';
}
$this->twig->addGlobal('color_theme_link', $link);
}
}

@ -96,7 +96,6 @@ class Version20170627122900 extends AbstractMigrationChamilo
'registration' => 'required_profile_fields',
'profile' => 'changeable_options',
'timezone_value' => 'timezone',
'stylesheets' => 'theme',
'platformLanguage' => 'platform_language',
'languagePriority1' => 'language_priority_1',
'languagePriority2' => 'language_priority_2',
@ -276,7 +275,6 @@ class Version20170627122900 extends AbstractMigrationChamilo
'users_copy_files' => 'document',
'timezone' => 'platform',
'enable_profile_user_address_geolocalization' => 'profile',
'theme' => 'platform',
'exercise_hide_label' => 'exercise',
];
@ -319,6 +317,7 @@ class Version20170627122900 extends AbstractMigrationChamilo
'sso_force_redirect',
'activate_email_template',
'sso_authentication_subclass',
'stylesheets',
];
foreach ($settings as $setting) {

@ -17,12 +17,9 @@ final class Version20231110194300 extends AbstractMigrationChamilo
return 'Copy custom theme folder to assets and update webpack.config';
}
public function up(Schema $schema): void
private function getDefaultThemeNames(): array
{
$kernel = $this->container->get('kernel');
$rootPath = $kernel->getProjectDir();
$customThemesFolders = [
return [
'academica',
'chamilo',
'chamilo_red',
@ -52,76 +49,50 @@ final class Version20231110194300 extends AbstractMigrationChamilo
'simplex',
'tasty_olive',
];
}
public function up(Schema $schema): void
{
$kernel = $this->container->get('kernel');
$rootPath = $kernel->getProjectDir();
$defaulThemesFolders = $this->getDefaultThemeNames();
$sourceDir = $rootPath.'/app/Resources/public/css/themes';
$destinationDir = $rootPath.'/assets/css/themes/';
$chamiloDefaultCssPath = $destinationDir.'chamilo/default.css';
if (!is_dir($sourceDir)) {
return;
}
$filesystem = $this->container->get('oneup_flysystem.themes_filesystem');
$finder = new Finder();
$finder->directories()->in($sourceDir)->depth('== 0');
$newThemes = [];
foreach ($finder as $folder) {
$folderName = $folder->getRelativePathname();
if (!\in_array($folderName, $customThemesFolders, true)) {
$sourcePath = $folder->getRealPath();
$destinationPath = $destinationDir.$folderName;
if (!file_exists($destinationPath)) {
$this->copyDirectory($sourcePath, $destinationPath);
$newThemes[] = $folderName;
foreach ($finder as $folder) {
$themeFolderName = $folder->getRelativePathname();
if (file_exists($chamiloDefaultCssPath)) {
$newThemeDefaultCssPath = $destinationPath.'/default.css';
copy($chamiloDefaultCssPath, $newThemeDefaultCssPath);
}
}
}
if (\in_array($themeFolderName, $defaulThemesFolders, true)) {
continue;
}
$this->updateWebpackConfig($rootPath, $newThemes);
if ($filesystem->directoryExists($themeFolderName)) {
continue;
}
private function copyDirectory($src, $dst): void
{
$dir = opendir($src);
@mkdir($dst);
while (false !== ($file = readdir($dir))) {
if (('.' !== $file) && ('..' !== $file)) {
if (is_dir($src.'/'.$file)) {
$this->copyDirectory($src.'/'.$file, $dst.'/'.$file);
} else {
copy($src.'/'.$file, $dst.'/'.$file);
}
}
}
closedir($dir);
}
$filesystem->createDirectory($themeFolderName);
private function updateWebpackConfig(string $rootPath, array $newThemes): void
{
$webpackConfigPath = $rootPath.'/webpack.config.js';
$directory = (new Finder())->in($folder->getRealPath());
if (!file_exists($webpackConfigPath)) {
return;
foreach ($directory as $file) {
if (!$file->isFile()) {
continue;
}
$content = file_get_contents($webpackConfigPath);
$pattern = '/(const themes = \\[\\s*")([^"\\]]+)("\\s*\\])/';
$replacement = function ($matches) use ($newThemes) {
$existingThemes = explode('", "', trim($matches[2], '"'));
$allThemes = array_unique(array_merge($existingThemes, $newThemes));
$newThemesString = implode('", "', $allThemes);
return $matches[1].$newThemesString.$matches[3];
};
$newContent = preg_replace_callback($pattern, $replacement, $content);
file_put_contents($webpackConfigPath, $newContent);
$newFileRelativePathname = $themeFolderName.DIRECTORY_SEPARATOR.$file->getRelativePathname();
$fileContents = $file->getContents();
$filesystem->write($newFileRelativePathname, $fileContents);
}
}
}
}

@ -9,10 +9,18 @@ namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
class Version20240318105600 extends AbstractMigrationChamilo
final class Version20240318105600 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Color theme migration';
}
public function up(Schema $schema): void
{
$this->addSql("CREATE TABLE color_theme (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, variables LONGTEXT NOT NULL COMMENT '(DC2Type:json)', slug VARCHAR(255) NOT NULL, active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime)', updated_at DATETIME NOT NULL COMMENT '(DC2Type:datetime)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC");
$this->addSql("CREATE TABLE color_theme (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, variables LONGTEXT NOT NULL COMMENT '(DC2Type:json)', slug VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime)', updated_at DATETIME NOT NULL COMMENT '(DC2Type:datetime)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC");
$this->addSql("CREATE TABLE access_url_rel_color_theme (id INT AUTO_INCREMENT NOT NULL, url_id INT NOT NULL, color_theme_id INT NOT NULL, active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime)', updated_at DATETIME NOT NULL COMMENT '(DC2Type:datetime)', INDEX IDX_D2A2E1C981CFDAE7 (url_id), INDEX IDX_D2A2E1C98587EFC5 (color_theme_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC");
$this->addSql("ALTER TABLE access_url_rel_color_theme ADD CONSTRAINT FK_D2A2E1C981CFDAE7 FOREIGN KEY (url_id) REFERENCES access_url (id)");
$this->addSql("ALTER TABLE access_url_rel_color_theme ADD CONSTRAINT FK_D2A2E1C98587EFC5 FOREIGN KEY (color_theme_id) REFERENCES color_theme (id)");
}
}

@ -0,0 +1,48 @@
<?php
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
class Version20240704185300 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return "Fix stylesheet and theme settings and move theme directory during development";
}
/**
* @inheritDoc
*/
public function up(Schema $schema): void
{
$this->addSql("DELETE FROM settings WHERE variable IN ('stylesheets', 'theme')");
$kernel = $this->container->get('kernel');
$rootPath = $kernel->getProjectDir();
$themeDirectory = $rootPath.'/var/theme';
$themesDirectory = $rootPath.'/var/themes';
$finder = new Finder();
$filesystem = new Filesystem();
$finder->directories()->in($themeDirectory)->depth('== 0');
foreach ($finder as $entry) {
if ($entry->isDir()) {
error_log(
sprintf(
"Moving theme directory: %s %s",
$entry->getRealPath(),
$themesDirectory.'/'
)
);
$filesystem->rename($entry->getRealPath(), $themesDirectory.'/'.$entry->getRelativePathname());
}
}
}
}

@ -0,0 +1,27 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\AccessUrlRelColorTheme;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AccessUrlRelColorTheme>
*
* @method AccessUrlRelColorTheme|null find($id, $lockMode = null, $lockVersion = null)
* @method AccessUrlRelColorTheme|null findOneBy(array $criteria, array $orderBy = null)
* @method AccessUrlRelColorTheme[] findAll()
* @method AccessUrlRelColorTheme[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
final class AccessUrlRelColorThemeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AccessUrlRelColorTheme::class);
}
}

@ -14,20 +14,4 @@ class ColorThemeRepository extends ServiceEntityRepository
{
parent::__construct($registry, ColorTheme::class);
}
public function deactivateAllExcept(ColorTheme $colorTheme): void
{
$this->getEntityManager()
->createQuery('UPDATE Chamilo\CoreBundle\Entity\ColorTheme t SET t.active = FALSE WHERE t.id <> :id')
->execute(['id' => $colorTheme->getId()])
;
}
public function getActiveOne(): ?ColorTheme
{
return $this->findOneBy(
['active' => true],
['createdAt' => 'DESC']
);
}
}

@ -193,8 +193,3 @@ services:
class: Chamilo\CoreBundle\Settings\WebServiceSettingsSchema
tags:
- {name: sylius.settings_schema, alias: chamilo_core.settings.webservice, namespace: webservice}
chamilo_core.settings.stylesheets:
class: Chamilo\CoreBundle\Settings\StylesheetsSettingsSchema
tags:
- { name: sylius.settings_schema, alias: chamilo_core.settings.stylesheets, namespace: stylesheets }

@ -4,8 +4,7 @@
{% import "@ChamiloCore/Macros/image.html.twig" as macro_image %}
{% import '@ChamiloCore/Macros/headers.html.twig' as macro_headers %}
{% import '@ChamiloCore/Macros/modals.html.twig' as macro_modals %}
{# Chamilo theme #}
{% set theme = 'chamilo' %}
{% set modals_block = macro_modals.global_modal('') %}
{% if not from_vue %}
<!DOCTYPE html>

@ -4,7 +4,7 @@
<link href="https://chamilo.org/the-association/" rel="author" />
<link href="https://chamilo.org/the-association/" rel="copyright" />
<link rel="apple-touch-icon" href="{{ url('index') ~ 'apple-touch-icon.png' }}" />
{# <link rel="shortcut icon" href="{{ url('index') ~ favico}}" type="image/x-icon" /> #}
<link rel="shortcut icon" href="{{ theme_asset('images/favicon.ico') }}" type="image/x-icon" />
<title>
{%- block title %}
{{ chamilo_settings_get('platform.institution') }} - {{ chamilo_settings_get('platform.site_name') }}
@ -35,20 +35,11 @@
{{ encore_entry_link_tags('css/scorm') }}
{% endif %}
{% if color_theme_link is defined %}
{{ color_theme_link|raw }}
{% endif %}
{{ theme_asset_link_tag('colors.css') }}
{# Files app.css is generated from "assets/css/app.scss" file using the file webpack.config.js #}
{# {{ encore_entry_link_tags('app') }} #}
{% if theme is defined %}
{# <link rel="stylesheet" href="{{ url('index') ~ 'build/css/themes/'~ theme ~'/default.css' }}"/> #}
{% endif %}
{# {{ encore_entry_link_tags('vue') }} #}
{# <link rel="stylesheet" href="{{ url('index') ~ 'build/css/print.css' }}" media="print" /> #}
{# <link rel="stylesheet" href="theme_asset('print.css')" media="print" /> #}
{% endblock %}
{# app.js is generated using the file webpack.config.js and using yarn read /assets/README.md for more info #}
{# <script src="{{ url('index') ~ 'build/libs/ckeditor/ckeditor.js' }}"></script> #}
{{ encore_entry_script_tags('legacy_free-jqgrid') }}
{{ encore_entry_script_tags('legacy_app') }}
{{ encore_entry_script_tags('legacy_lp') }}

@ -3,7 +3,7 @@
<tr>
<td width="245" {{ mail_header_style }}>
<img class="navbar-brand-full" width="130"
src="{{ url('index') ~ 'build/css/themes/'~ theme ~'/images/header-logo.png' }}"
src="{{ theme_asset('images/header-logo.svg', true) }}"
alt="Chamilo"/>
</td>
<td width="100%"> &nbsp;

@ -25,6 +25,13 @@ class AccessUrlHelper
return 1 === (int) $this->parameterBag->get('multiple_access_url');
}
public function getFirstAccessUrl(): AccessUrl
{
$urlId = $this->accessUrlRepository->getFirstId();
return $this->accessUrlRepository->find($urlId);
}
public function getCurrent(): AccessUrl
{
static $accessUrl;
@ -33,8 +40,7 @@ class AccessUrlHelper
return $accessUrl;
}
$urlId = $this->accessUrlRepository->getFirstId();
$accessUrl = $this->accessUrlRepository->find($urlId);
$accessUrl = $this->getFirstAccessUrl();
if ($this->isMultipleEnabled()) {
$url = $this->router->generate('index', [], UrlGeneratorInterface::ABSOLUTE_URL);

@ -19,6 +19,7 @@ final class MailHelper
public function __construct(
private readonly MailerInterface $mailer,
private readonly BodyRendererInterface $bodyRenderer,
private readonly ThemeHelper $themeHelper,
) {}
public function send(
@ -98,7 +99,6 @@ final class MailHelper
'link' => $additionalParameters['link'] ?? '',
'automatic_email_text' => $automaticEmailText,
'content' => $body,
'theme' => api_get_visual_theme(),
];
if (!empty($recipientEmail)) {

@ -0,0 +1,103 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\ServiceHelper;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Chamilo\CourseBundle\Settings\SettingsCourseManager;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
final class ThemeHelper
{
public const DEFAULT_THEME = 'chamilo';
public function __construct(
private readonly AccessUrlHelper $accessUrlHelper,
private readonly SettingsManager $settingsManager,
private readonly UserHelper $userHelper,
private readonly CidReqHelper $cidReqHelper,
private readonly SettingsCourseManager $settingsCourseManager,
private readonly RouterInterface $router,
#[Autowire(service: 'oneup_flysystem.themes_filesystem')] private readonly FilesystemOperator $filesystem,
) {}
/**
* Returns the name of the color theme configured to be applied on the current page.
* The returned name depends on the platform, course or user settings.
*/
public function getVisualTheme(): string
{
static $visualTheme;
global $lp_theme_css;
if (isset($visualTheme)) {
return $visualTheme;
}
$accessUrl = $this->accessUrlHelper->getCurrent();
$visualTheme = $accessUrl->getActiveColorTheme()?->getColorTheme()->getSlug();
if ('true' == $this->settingsManager->getSetting('profile.user_selected_theme')) {
$visualTheme = $this->userHelper->getCurrent()?->getTheme();
}
if ('true' == $this->settingsManager->getSetting('course.allow_course_theme')) {
$course = $this->cidReqHelper->getCourseEntity();
if ($course) {
$this->settingsCourseManager->setCourse($course);
$visualTheme = $this->settingsCourseManager->getCourseSettingValue('course_theme');
if (1 === (int) $this->settingsCourseManager->getCourseSettingValue('allow_learning_path_theme')) {
$visualTheme = $lp_theme_css;
}
}
}
if (empty($visualTheme)) {
return self::DEFAULT_THEME;
}
return $visualTheme;
}
public function getThemeAssetUrl(string $path, bool $absolute = false): string
{
$themeName = $this->getVisualTheme();
try {
if (!$this->filesystem->fileExists($themeName.DIRECTORY_SEPARATOR.$path)) {
return '';
}
} catch (FilesystemException) {
return '';
}
return $this->router->generate(
'theme_asset',
['name' => $themeName, 'path' => $path],
$absolute ? UrlGeneratorInterface::ABSOLUTE_URL : UrlGeneratorInterface::ABSOLUTE_PATH
);
}
public function getThemeAssetLinkTag(string $path, bool $absoluteUrl = false): string
{
$url = $this->getThemeAssetUrl($path, $absoluteUrl);
if (empty($url)) {
return '';
}
return sprintf('<link rel="stylesheet" href="%s">', $url);
}
}

@ -40,7 +40,6 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
'institution_address' => '',
'site_name' => 'Chamilo site',
'timezone' => 'Europe/Paris',
'theme' => 'chamilo',
'gravatar_enabled' => 'false',
'gravatar_type' => 'mm',
'gamification_mode' => ' ',
@ -120,7 +119,6 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
->add('institution_address')
->add('site_name')
->add('timezone', TimezoneType::class)
->add('theme')
->add('gravatar_enabled', YesNoType::class)
->add(
'gravatar_type',

@ -139,7 +139,7 @@ class SettingsManager implements SettingsManagerInterface
* Get a specific configuration setting, getting from the previously stored
* PHP session data whenever possible.
*
* @param string $name The setting name (composed if in a category, i.e. 'platform.theme')
* @param string $name The setting name (composed if in a category, i.e. 'platform.institution')
* @param bool $loadFromDb Whether to load from the database
*/
public function getSetting(string $name, bool $loadFromDb = false): mixed
@ -658,7 +658,6 @@ class SettingsManager implements SettingsManagerInterface
// 'donotlistcampus' =>'null',
'show_email_addresses' => 'Platform',
'service_ppt2lp' => 'NULL',
'stylesheets' => 'stylesheets',
'upload_extensions_list_type' => 'Security',
'upload_extensions_blacklist' => 'Security',
'upload_extensions_whitelist' => 'Security',
@ -920,7 +919,6 @@ class SettingsManager implements SettingsManagerInterface
'siteName' => 'site_name',
'InstitutionUrl' => 'institution_url',
'registration' => 'required_profile_fields',
'stylesheets' => 'theme',
'platformLanguage' => 'platform_language',
'languagePriority1' => 'language_priority_1',
'languagePriority2' => 'language_priority_2',

@ -1,76 +0,0 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Settings;
use Sylius\Bundle\SettingsBundle\Schema\AbstractSettingsBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Service\Attribute\Required;
class StylesheetsSettingsSchema extends AbstractSettingsSchema
{
private ParameterBagInterface $parameterBag;
#[Required]
public function setParameterBag(ParameterBagInterface $parameterBag): void
{
$this->parameterBag = $parameterBag;
}
public function buildSettings(AbstractSettingsBuilder $builder): void
{
$builder
->setDefaults(
[
'stylesheets' => 'chamilo',
]
)
;
$allowedTypes = [
'stylesheets' => ['string'],
];
$this->setMultipleAllowedTypes($allowedTypes, $builder);
}
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('stylesheets', ChoiceType::class, [
'choices' => $this->getThemeChoices(),
'label' => 'Select Stylesheet Theme',
])
;
$this->updateFormFieldsFromSettingsInfo($builder);
}
private function getThemeChoices(): array
{
$projectDir = $this->parameterBag->get('kernel.project_dir');
$themesDirectory = $projectDir.'/assets/css/themes/';
$finder = new Finder();
$choices = [];
$finder->directories()->in($themesDirectory)->depth('== 0');
if ($finder->hasResults()) {
foreach ($finder as $folder) {
$folderName = $folder->getRelativePathname();
$choices[$this->formatFolderName($folderName)] = $folderName;
}
}
return $choices;
}
private function formatFolderName(string $name): string
{
return ucwords(str_replace('_', ' ', $name));
}
}

@ -0,0 +1,45 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Chamilo\CoreBundle\Entity\AccessUrlRelColorTheme;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
use Doctrine\ORM\EntityManagerInterface;
final class AccessUrlRelColorThemeStateProcessor implements ProcessorInterface
{
public function __construct(
private readonly AccessUrlHelper $accessUrlHelper,
private readonly EntityManagerInterface $entityManager,
) {}
public function process($data, Operation $operation, array $uriVariables = [], array $context = []): AccessUrlRelColorTheme
{
assert($data instanceof AccessUrlRelColorTheme);
$accessUrl = $this->accessUrlHelper->getCurrent();
$accessUrl->getActiveColorTheme()?->setActive(false);
$accessUrlRelColorTheme = $accessUrl->getColorThemeByTheme($data->getColorTheme());
if ($accessUrlRelColorTheme) {
$accessUrlRelColorTheme->setActive(true);
} else {
$data->setActive(true);
$accessUrl->addColorTheme($data);
$accessUrlRelColorTheme = $data;
}
$this->entityManager->flush();
return $accessUrlRelColorTheme;
}
}

@ -0,0 +1,32 @@
<?php
namespace Chamilo\CoreBundle\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Chamilo\CoreBundle\Entity\AccessUrlRelColorTheme;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
/**
* @template-implements ProviderInterface<AccessUrlRelColorTheme>
*/
class AccessUrlRelColorThemeStateProvider implements ProviderInterface
{
public function __construct(
private readonly AccessUrlHelper $accessUrlHelper,
) {}
/**
* @inheritdoc
*/
public function provide(Operation $operation, array $uriVariables = [], array $context = [])
{
$colorThemes = $this->accessUrlHelper->getCurrent()->getColorThemes();
if (0 == $colorThemes->count()) {
$colorThemes = $this->accessUrlHelper->getFirstAccessUrl()->getColorThemes();
}
return $colorThemes;
}
}

@ -8,10 +8,13 @@ namespace Chamilo\CoreBundle\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Chamilo\CoreBundle\Entity\AccessUrlRelColorTheme;
use Chamilo\CoreBundle\Entity\ColorTheme;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
use Doctrine\ORM\EntityManagerInterface;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use const PHP_EOL;
@ -19,23 +22,27 @@ final class ColorThemeStateProcessor implements ProcessorInterface
{
public function __construct(
private readonly ProcessorInterface $persistProcessor,
private readonly ParameterBagInterface $parameterBag,
private readonly ColorThemeRepository $colorThemeRepository,
private readonly AccessUrlHelper $accessUrlHelper,
private readonly EntityManagerInterface $entityManager,
#[Autowire(service: 'oneup_flysystem.themes_filesystem')] private readonly FilesystemOperator $filesystem,
) {}
/**
* @throws FilesystemException
*/
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
\assert($data instanceof ColorTheme);
$data->setActive(true);
/** @var ColorTheme $colorTheme */
$colorTheme = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
if ($colorTheme) {
$this->colorThemeRepository->deactivateAllExcept($colorTheme);
$accessUrlRelColorTheme = (new AccessUrlRelColorTheme())->setColorTheme($colorTheme);
$this->accessUrlHelper->getCurrent()->addColorTheme($accessUrlRelColorTheme);
$projectDir = $this->parameterBag->get('kernel.project_dir');
$this->entityManager->flush();
$contentParts = [];
$contentParts[] = ':root {';
@ -46,12 +53,9 @@ final class ColorThemeStateProcessor implements ProcessorInterface
$contentParts[] = '}';
$dirName = $projectDir."/var/theme/{$colorTheme->getSlug()}";
$fs = new Filesystem();
$fs->mkdir($dirName);
$fs->dumpFile(
$dirName.'/colors.css',
$this->filesystem->createDirectory($colorTheme->getSlug());
$this->filesystem->write(
$colorTheme->getSlug().DIRECTORY_SEPARATOR.'colors.css',
implode(PHP_EOL, $contentParts)
);
}

@ -10,6 +10,7 @@ use Chamilo\CoreBundle\Component\Utils\NameConvention;
use Chamilo\CoreBundle\Entity\ResourceIllustrationInterface;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Chamilo\CoreBundle\ServiceHelper\ThemeHelper;
use Chamilo\CoreBundle\Twig\SettingsHelper;
use Security;
use Sylius\Bundle\SettingsBundle\Model\SettingsInterface;
@ -28,8 +29,13 @@ class ChamiloExtension extends AbstractExtension
private RouterInterface $router;
private NameConvention $nameConvention;
public function __construct(IllustrationRepository $illustrationRepository, SettingsHelper $helper, RouterInterface $router, NameConvention $nameConvention)
{
public function __construct(
IllustrationRepository $illustrationRepository,
SettingsHelper $helper,
RouterInterface $router,
NameConvention $nameConvention,
private readonly ThemeHelper $themeHelper
) {
$this->illustrationRepository = $illustrationRepository;
$this->helper = $helper;
$this->router = $router;
@ -66,6 +72,8 @@ class ChamiloExtension extends AbstractExtension
new TwigFunction('chamilo_settings_get', $this->getSettingsParameter(...)),
new TwigFunction('chamilo_settings_has', [$this, 'hasSettingsParameter']),
new TwigFunction('password_checker_js', [$this, 'getPasswordCheckerJs'], ['is_safe' => ['html']]),
new TwigFunction('theme_asset', $this->getThemeAssetUrl(...)),
new TwigFunction('theme_asset_link_tag', $this->getThemeAssetLinkTag(...), ['is_safe' => ['html']]),
];
}
@ -219,4 +227,14 @@ class ChamiloExtension extends AbstractExtension
{
return 'chamilo_extension';
}
public function getThemeAssetUrl(string $path, bool $absolute = false): string
{
return $this->themeHelper->getThemeAssetUrl($path, $absolute);
}
public function getThemeAssetLinkTag(string $path, bool $absoluteUrl = false): string
{
return $this->themeHelper->getThemeAssetLinkTag($path, $absoluteUrl);
}
}

@ -0,0 +1,45 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\Tests\CoreBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;
class ThemeControllerTest extends WebTestCase
{
public function testValidAccess(): void
{
$client = static::createClient();
$client->request('GET', '/themes/chamilo/colors.css');
$this->assertResponseIsSuccessful();
}
public function testInvalidAccess(): void
{
$client = static::createClient();
$client->request('GET', '/themes/chamilo/default.css');
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
}
public function testAccessToSystemFiles(): void
{
$client = static::createClient();
$client->request('GET', '/themes/chamilo/../../../../../../etc/passwd');
$this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR);
$client->request('GET', 'themes/chamilo/../../../.env');
$this->assertResponseStatusCodeSame(Response::HTTP_INTERNAL_SERVER_ERROR);
}
}

@ -26,8 +26,8 @@ class SettingsHelperTest extends AbstractApiTest
$this->assertInstanceOf(SettingsInterface::class, $settings);
$this->assertSame('chamilo_settings', $helper->getName());
$defaultTheme = $helper->getSettingsParameter('platform.theme');
$defaultTheme = $helper->getSettingsParameter('platform.institution');
$this->assertSame('chamilo', $defaultTheme);
$this->assertSame('Chamilo.org', $defaultTheme);
}
}

@ -0,0 +1,32 @@
:root {
--color-primary-base: 97 53 131;
--color-primary-gradient: 36 77 103;
--color-primary-button-text: 46 117 163;
--color-primary-button-alternative-text: 255 255 255;
--color-secondary-base: 243 126 47;
--color-secondary-gradient: 224 100 16;
--color-secondary-button-text: 255 255 255;
--color-tertiary-base: 51 51 51;
--color-tertiary-gradient: 0 0 0;
--color-tertiary-button-text: 255 255 255;
--color-success-base: 119 170 12;
--color-success-gradient: 83 127 0;
--color-success-button-text: 255 255 255;
--color-info-base: 13 123 253;
--color-info-gradient: 0 84 211;
--color-info-button-text: 255 255 255;
--color-warning-base: 245 206 1;
--color-warning-gradient: 186 152 0;
--color-warning-button-text: 0 0 0;
--color-danger-base: 223 59 59;
--color-danger-gradient: 180 0 21;
--color-danger-button-text: 255 255 255;
--color-form-base: 46 117 163;
}

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

@ -135,18 +135,6 @@ Encore.copyFiles({
to: "libs/select2/js/[name].[ext]",
})
const themes = ["chamilo"]
// Add Chamilo themes
themes.forEach(function (theme) {
Encore.addStyleEntry("css/themes/" + theme + "/default", "./assets/css/themes/" + theme + "/default.css")
// Copy images from themes into public/build
Encore.copyFiles({
from: "assets/css/themes/" + theme + "/images",
to: "css/themes/" + theme + "/images/[name].[ext]",
})
})
// Fix free-jqgrid languages files
// Encore.addPlugin(new FileManagerPlugin({
// onEnd: {

Loading…
Cancel
Save