diff --git a/assets/css/app.scss b/assets/css/app.scss index d00a49e5738..394756bed91 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -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; diff --git a/assets/css/themes/chamilo/default.css b/assets/css/themes/chamilo/default.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/assets/css/themes/chamilo/fonts/OpenSans-Bold.woff b/assets/css/themes/chamilo/fonts/OpenSans-Bold.woff deleted file mode 100644 index a0a331e74df..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans-Bold.woff and /dev/null differ diff --git a/assets/css/themes/chamilo/fonts/OpenSans-Bold.woff2 b/assets/css/themes/chamilo/fonts/OpenSans-Bold.woff2 deleted file mode 100644 index 9ab88e241b0..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans-Bold.woff2 and /dev/null differ diff --git a/assets/css/themes/chamilo/fonts/OpenSans-Light.woff b/assets/css/themes/chamilo/fonts/OpenSans-Light.woff deleted file mode 100644 index 70646281e99..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans-Light.woff and /dev/null differ diff --git a/assets/css/themes/chamilo/fonts/OpenSans-Light.woff2 b/assets/css/themes/chamilo/fonts/OpenSans-Light.woff2 deleted file mode 100644 index 90b828fe493..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans-Light.woff2 and /dev/null differ diff --git a/assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff b/assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff deleted file mode 100644 index 6304fc979bc..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff and /dev/null differ diff --git a/assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff2 b/assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff2 deleted file mode 100644 index 7e5e42c2677..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans-Semibold.woff2 and /dev/null differ diff --git a/assets/css/themes/chamilo/fonts/OpenSans.woff b/assets/css/themes/chamilo/fonts/OpenSans.woff deleted file mode 100644 index e9ce2f320ef..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans.woff and /dev/null differ diff --git a/assets/css/themes/chamilo/fonts/OpenSans.woff2 b/assets/css/themes/chamilo/fonts/OpenSans.woff2 deleted file mode 100644 index bf65567c27d..00000000000 Binary files a/assets/css/themes/chamilo/fonts/OpenSans.woff2 and /dev/null differ diff --git a/assets/css/themes/chamilo/learnpath.css b/assets/css/themes/chamilo/learnpath.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/assets/vue/AppInstaller.vue b/assets/vue/AppInstaller.vue index 10ca32b7a59..ebcfdab18be 100644 --- a/assets/vue/AppInstaller.vue +++ b/assets/vue/AppInstaller.vue @@ -6,9 +6,9 @@ href="index.php" > Chamilo + />
  1. -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") @@ -10,7 +11,7 @@ const siteName = platformConfigStore.getSetting("platform.site_name") diff --git a/assets/vue/components/platform/ColorThemeSelector.vue b/assets/vue/components/platform/ColorThemeSelector.vue new file mode 100644 index 00000000000..b99c2e5f4bd --- /dev/null +++ b/assets/vue/components/platform/ColorThemeSelector.vue @@ -0,0 +1,50 @@ + + + diff --git a/assets/vue/composables/theme.js b/assets/vue/composables/theme.js index c8053000a38..dc934a1f753 100644 --- a/assets/vue/composables/theme.js +++ b/assets/vue/composables/theme.js @@ -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, + } +} diff --git a/assets/vue/services/colorThemeService.js b/assets/vue/services/colorThemeService.js index e74ee17df90..d1e86a6581b 100644 --- a/assets/vue/services/colorThemeService.js +++ b/assets/vue/services/colorThemeService.js @@ -5,12 +5,10 @@ const url = "/api/color_themes" /** * Gets the color themes * - * @returns {Promise} + * @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} + */ +async function changePlatformColorTheme(iri) { + return baseService.post("/api/access_url_rel_color_themes", { + colorTheme: iri, + }) +} + export default { - getThemes, updateTheme, + findAllByCurrentUrl, + changePlatformColorTheme, } diff --git a/assets/vue/store/platformConfig.js b/assets/vue/store/platformConfig.js index 15934a17614..8a0a19def70 100644 --- a/assets/vue/store/platformConfig.js +++ b/assets/vue/store/platformConfig.js @@ -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, } }) diff --git a/assets/vue/views/admin/AdminConfigureColors.vue b/assets/vue/views/admin/AdminConfigureColors.vue index ed7e0349d34..69e0044b682 100644 --- a/assets/vue/views/admin/AdminConfigureColors.vue +++ b/assets/vue/views/admin/AdminConfigureColors.vue @@ -3,404 +3,416 @@
    -
    - + - - + - -
    -
    - - - - -
    + -
    - - - -
    +
    +
    +

    -
    - - -
    + -
    - - - -
    + +
    +
    + + + + +
    -
    - - - -
    +
    + + + +
    -
    - - - -
    +
    + + +
    -
    - - -
    +
    + + + +
    -
    - -
    -
    +
    + + + +
    - -
    -
    - - - -
    +
    + + + +
    -
    - - - - -
    +
    + + +
    -
    - +
    + +
    -
    - -
    - -
    -
    - -
    -

    + +
    +
    + + + +
    - +
    + + + + +
    -
    -
    {{ t("You can see examples of how chamilo will look here") }}
    +
    + +
    +
    -
    -

    {{ t("Buttons") }}

    -
    - +
    - -
    -
    - - + +
    - -
    -
    - -
    -
    -

    {{ t("Dropdowns") }}

    -
    - - - + + +
    +
    {{ t("You can see examples of how chamilo will look here") }}
    + +
    +

    {{ t("Buttons") }}

    +
    + + + + +
    +
    + + + + +
    +
    + + +
    -
    -
    -

    {{ t("Checkbox and radio buttons") }}

    -
    - -
    - - +

    {{ t("Dropdowns") }}

    +
    + + +
    -
    -
    -

    {{ t("Toggle") }}

    - -
    +
    +

    {{ t("Checkbox and radio buttons") }}

    +
    + +
    + + +
    +
    +
    -
    -

    {{ t("Forms") }}

    - - - -
    +
    +

    {{ t("Toggle") }}

    + +
    -
    -

    {{ t("Dialogs") }}

    - - -
    -
    -

    {{ t("Some more elements") }}

    -
    -
    @@ -412,7 +424,7 @@ 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) + } +} diff --git a/config/packages/oneup_flysystem.yaml b/config/packages/oneup_flysystem.yaml index e2035fc00a4..b35965a6d02 100644 --- a/config/packages/oneup_flysystem.yaml +++ b/config/packages/oneup_flysystem.yaml @@ -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 diff --git a/public/main/admin/settings.lib.php b/public/main/admin/settings.lib.php index 951fae56065..69699e5223a 100644 --- a/public/main/admin/settings.lib.php +++ b/public/main/admin/settings.lib.php @@ -19,7 +19,7 @@ * * @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 ''; } -/** - * This function allows the platform admin to choose the default stylesheet. - * - * @author Patrick Cool , Ghent University - * @author Julio Montoya , 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'), - '' - ); - } - $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 ''; - } - } 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 ''; - } 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 ''; - } elseif (isset($_POST['logo_upload']) && is_file($dir.$newLogoFileName)) { - echo ''; - } - } 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 , 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. * diff --git a/public/main/admin/settings.php b/public/main/admin/settings.php index 1407ecf0d75..516b79d50c9 100644 --- a/public/main/admin/settings.php +++ b/public/main/admin/settings.php @@ -57,7 +57,7 @@ // 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); diff --git a/public/main/inc/lib/api.lib.php b/public/main/inc/lib/api.lib.php index 04e60381ca4..06230c2c7a6 100644 --- a/public/main/inc/lib/api.lib.php +++ b/public/main/inc/lib/api.lib.php @@ -12,6 +12,7 @@ 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; @@ -2695,8 +2696,6 @@ function api_get_setting($variable, $isArray = false, $key = null) } return 'prod'; - case 'stylesheets': - $variable = 'platform.theme'; // deprecated settings // no break case 'openid_authentication': @@ -3735,83 +3734,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(); } /** diff --git a/public/main/inc/lib/banner.lib.php b/public/main/inc/lib/banner.lib.php index 40640c467ac..393cce88e28 100644 --- a/public/main/inc/lib/banner.lib.php +++ b/public/main/inc/lib/banner.lib.php @@ -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 diff --git a/public/main/inc/lib/display.lib.php b/public/main/inc/lib/display.lib.php index 148e639ea2c..bd7577efab9 100644 --- a/public/main/inc/lib/display.lib.php +++ b/public/main/inc/lib/display.lib.php @@ -9,7 +9,7 @@ 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 @@ public static function return_icon( 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 @@ public static function getFrameReadyBlock( 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.'", diff --git a/public/main/inc/lib/pdf.lib.php b/public/main/inc/lib/pdf.lib.php index e453cd81122..20827a3dc79 100644 --- a/public/main/inc/lib/pdf.lib.php +++ b/public/main/inc/lib/pdf.lib.php @@ -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 @@ public function content_to_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); diff --git a/public/main/inc/lib/template.lib.php b/public/main/inc/lib/template.lib.php index 50ac04c80d4..01c87bf2adc 100644 --- a/public/main/inc/lib/template.lib.php +++ b/public/main/inc/lib/template.lib.php @@ -761,27 +761,6 @@ public function set_js_files_post() } } - /** - * @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. */ diff --git a/public/main/lp/learnpath.class.php b/public/main/lp/learnpath.class.php index 8945c178d99..c5eb2f62186 100644 --- a/public/main/lp/learnpath.class.php +++ b/public/main/lp/learnpath.class.php @@ -6,6 +6,7 @@ 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 @@ public static function getAccumulateWorkTimeTotal($courseId) /** * 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/'; + $theme = Container::$container->get(ThemeHelper::class)->getVisualTheme(); + $filesystem = Container::$container->get('oneup_flysystem.themes_filesystem'); + + if (!$filesystem->directoryExists("$theme/lp_icons")) { + return []; + } + $icons = ['' => get_lang('Please select an option')]; - 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(); - } + $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 @@ public static function getSelectedIcon($lpId) 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 @@ public static function getSelectedIconHtml($lpId) 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); } diff --git a/public/main/template/default/gradebook/custom_certificate.html.twig b/public/main/template/default/gradebook/custom_certificate.html.twig index 214ea306300..cdec2866431 100644 --- a/public/main/template/default/gradebook/custom_certificate.html.twig +++ b/public/main/template/default/gradebook/custom_certificate.html.twig @@ -9,30 +9,30 @@
    - +
    - + - + @@ -99,12 +99,12 @@
    - + {{ 'Certificate of participation' | trans | raw }} - +
    @@ -42,7 +42,7 @@
    - +

    {{ complete_name }} @@ -75,7 +75,7 @@

    - +
    @@ -86,12 +86,12 @@ + +
    - {{ 'Certificate Footer' | trans | raw }} -
    - +
    - +
    diff --git a/src/CoreBundle/Controller/PlatformConfigurationController.php b/src/CoreBundle/Controller/PlatformConfigurationController.php index b4194faaa1f..cf76368176a 100644 --- a/src/CoreBundle/Controller/PlatformConfigurationController.php +++ b/src/CoreBundle/Controller/PlatformConfigurationController.php @@ -8,6 +8,7 @@ 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 @@ public function list(SettingsManager $settingsManager): Response 'settings' => [], 'studentview' => $requestSession->get('studentview'), 'plugins' => [], + 'visual_theme' => $this->themeHelper->getVisualTheme(), ]; $variables = []; @@ -45,7 +48,6 @@ public function list(SettingsManager $settingsManager): Response $variables = [ 'platform.site_name', 'platform.timezone', - 'platform.theme', 'platform.registered', 'platform.donotlistcampus', 'platform.load_term_conditions_section', diff --git a/src/CoreBundle/Controller/ThemeController.php b/src/CoreBundle/Controller/ThemeController.php index 1c813ee6e2c..703b9fbfa5d 100644 --- a/src/CoreBundle/Controller/ThemeController.php +++ b/src/CoreBundle/Controller/ThemeController.php @@ -6,37 +6,53 @@ 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; } diff --git a/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php b/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php index 4300bcee825..2282c50efb8 100644 --- a/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php +++ b/src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php @@ -1114,7 +1114,7 @@ public static function getExistingSettings(): array [ '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' => [ diff --git a/src/CoreBundle/Entity/AccessUrl.php b/src/CoreBundle/Entity/AccessUrl.php index 9be75c9dc2c..293ff09865e 100644 --- a/src/CoreBundle/Entity/AccessUrl.php +++ b/src/CoreBundle/Entity/AccessUrl.php @@ -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 + */ + #[ORM\OneToMany(mappedBy: 'url', targetEntity: AccessUrlRelColorTheme::class, cascade: ['persist'], orphanRemoval: true)] + private Collection $colorThemes; + public function __construct() { $this->description = ''; @@ -159,6 +165,7 @@ public function __construct() $this->sessionCategories = new ArrayCollection(); $this->courseCategory = new ArrayCollection(); $this->children = new ArrayCollection(); + $this->colorThemes = new ArrayCollection(); } public function __toString(): string @@ -490,4 +497,54 @@ public function setResourceName(string $name): self { 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 + */ + 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; + } } diff --git a/src/CoreBundle/Entity/AccessUrlRelColorTheme.php b/src/CoreBundle/Entity/AccessUrlRelColorTheme.php new file mode 100644 index 00000000000..4620506b0c1 --- /dev/null +++ b/src/CoreBundle/Entity/AccessUrlRelColorTheme.php @@ -0,0 +1,98 @@ + ['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; + } +} diff --git a/src/CoreBundle/Entity/ColorTheme.php b/src/CoreBundle/Entity/ColorTheme.php index f1f1de7dbbe..ce30fecab13 100644 --- a/src/CoreBundle/Entity/ColorTheme.php +++ b/src/CoreBundle/Entity/ColorTheme.php @@ -7,11 +7,12 @@ 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 @@ 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 + */ + #[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 @@ public function setSlug(string $slug): static return $this; } - public function isActive(): ?bool + /** + * @return Collection + */ + public function getUrls(): Collection { - return $this->active; + return $this->urls; + } + + public function addUrl(AccessUrlRelColorTheme $url): static + { + if (!$this->urls->contains($url)) { + $this->urls->add($url); + $url->setColorTheme($this); + } + + return $this; } - public function setActive(bool $active): static + public function removeUrl(AccessUrlRelColorTheme $url): static { - $this->active = $active; + if ($this->urls->removeElement($url)) { + // set the owning side to null (unless already changed) + if ($url->getColorTheme() === $this) { + $url->setColorTheme(null); + } + } return $this; } diff --git a/src/CoreBundle/EventListener/TwigListener.php b/src/CoreBundle/EventListener/TwigListener.php index 55ff5cf8c69..923b1a6bb5d 100644 --- a/src/CoreBundle/EventListener/TwigListener.php +++ b/src/CoreBundle/EventListener/TwigListener.php @@ -6,10 +6,8 @@ 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 @@ public function __construct( 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 @@ public function __invoke(ControllerEvent $event): void $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 = ''; - } - - $this->twig->addGlobal('color_theme_link', $link); } } diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20170627122900.php b/src/CoreBundle/Migrations/Schema/V200/Version20170627122900.php index f94fd01cba9..ea0fced8422 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20170627122900.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20170627122900.php @@ -96,7 +96,6 @@ public function up(Schema $schema): void '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 @@ public function up(Schema $schema): void 'users_copy_files' => 'document', 'timezone' => 'platform', 'enable_profile_user_address_geolocalization' => 'profile', - 'theme' => 'platform', 'exercise_hide_label' => 'exercise', ]; @@ -319,6 +317,7 @@ public function up(Schema $schema): void 'sso_force_redirect', 'activate_email_template', 'sso_authentication_subclass', + 'stylesheets', ]; foreach ($settings as $setting) { diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20231110194300.php b/src/CoreBundle/Migrations/Schema/V200/Version20231110194300.php index 05eb7e3f8df..a23e8e71aa2 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20231110194300.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20231110194300.php @@ -17,12 +17,9 @@ public function getDescription(): string 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 @@ public function up(Schema $schema): void '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); - } - 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); - } + if ($filesystem->directoryExists($themeFolderName)) { + continue; } - } - closedir($dir); - } - private function updateWebpackConfig(string $rootPath, array $newThemes): void - { - $webpackConfigPath = $rootPath.'/webpack.config.js'; + $filesystem->createDirectory($themeFolderName); - if (!file_exists($webpackConfigPath)) { - return; - } - - $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]; - }; + $directory = (new Finder())->in($folder->getRealPath()); - $newContent = preg_replace_callback($pattern, $replacement, $content); + foreach ($directory as $file) { + if (!$file->isFile()) { + continue; + } - file_put_contents($webpackConfigPath, $newContent); + $newFileRelativePathname = $themeFolderName.DIRECTORY_SEPARATOR.$file->getRelativePathname(); + $fileContents = $file->getContents(); + $filesystem->write($newFileRelativePathname, $fileContents); + } + } } } diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240318105600.php b/src/CoreBundle/Migrations/Schema/V200/Version20240318105600.php index 4bcb4b3e665..d78cd4f96ac 100644 --- a/src/CoreBundle/Migrations/Schema/V200/Version20240318105600.php +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240318105600.php @@ -9,10 +9,18 @@ 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)"); } } diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240704185300.php b/src/CoreBundle/Migrations/Schema/V200/Version20240704185300.php new file mode 100644 index 00000000000..af64c29be07 --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240704185300.php @@ -0,0 +1,48 @@ +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()); + } + } + } +} \ No newline at end of file diff --git a/src/CoreBundle/Repository/AccessUrlRelColorThemeRepository.php b/src/CoreBundle/Repository/AccessUrlRelColorThemeRepository.php new file mode 100644 index 00000000000..9482ca586b1 --- /dev/null +++ b/src/CoreBundle/Repository/AccessUrlRelColorThemeRepository.php @@ -0,0 +1,27 @@ + + * + * @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); + } +} diff --git a/src/CoreBundle/Repository/ColorThemeRepository.php b/src/CoreBundle/Repository/ColorThemeRepository.php index 80d6d85fc8b..12ceed45d71 100644 --- a/src/CoreBundle/Repository/ColorThemeRepository.php +++ b/src/CoreBundle/Repository/ColorThemeRepository.php @@ -14,20 +14,4 @@ public function __construct(ManagerRegistry $registry) { 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'] - ); - } } diff --git a/src/CoreBundle/Resources/config/settings.yml b/src/CoreBundle/Resources/config/settings.yml index b2c044e5265..6580ed6818f 100644 --- a/src/CoreBundle/Resources/config/settings.yml +++ b/src/CoreBundle/Resources/config/settings.yml @@ -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 } diff --git a/src/CoreBundle/Resources/views/Layout/base-layout.html.twig b/src/CoreBundle/Resources/views/Layout/base-layout.html.twig index f1bcd98b3e0..7f5d344808d 100644 --- a/src/CoreBundle/Resources/views/Layout/base-layout.html.twig +++ b/src/CoreBundle/Resources/views/Layout/base-layout.html.twig @@ -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 %} diff --git a/src/CoreBundle/Resources/views/Layout/head.html.twig b/src/CoreBundle/Resources/views/Layout/head.html.twig index cc25c3425ff..90be7f71ce4 100644 --- a/src/CoreBundle/Resources/views/Layout/head.html.twig +++ b/src/CoreBundle/Resources/views/Layout/head.html.twig @@ -4,7 +4,7 @@ - {# #} + {%- 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') }} diff --git a/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig b/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig index 2e64ede374f..ff72237cc64 100644 --- a/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig +++ b/src/CoreBundle/Resources/views/Mailer/Default/header.html.twig @@ -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%">   diff --git a/src/CoreBundle/ServiceHelper/AccessUrlHelper.php b/src/CoreBundle/ServiceHelper/AccessUrlHelper.php index 2b5d9521bc4..2bb6c6f1ba1 100644 --- a/src/CoreBundle/ServiceHelper/AccessUrlHelper.php +++ b/src/CoreBundle/ServiceHelper/AccessUrlHelper.php @@ -25,6 +25,13 @@ public function isMultipleEnabled(): bool 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 @@ public function getCurrent(): AccessUrl 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); diff --git a/src/CoreBundle/ServiceHelper/MailHelper.php b/src/CoreBundle/ServiceHelper/MailHelper.php index 520c9c2c57b..19b7f3c702e 100644 --- a/src/CoreBundle/ServiceHelper/MailHelper.php +++ b/src/CoreBundle/ServiceHelper/MailHelper.php @@ -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 @@ public function send( 'link' => $additionalParameters['link'] ?? '', 'automatic_email_text' => $automaticEmailText, 'content' => $body, - 'theme' => api_get_visual_theme(), ]; if (!empty($recipientEmail)) { diff --git a/src/CoreBundle/ServiceHelper/ThemeHelper.php b/src/CoreBundle/ServiceHelper/ThemeHelper.php new file mode 100644 index 00000000000..62354d46376 --- /dev/null +++ b/src/CoreBundle/ServiceHelper/ThemeHelper.php @@ -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); + } +} diff --git a/src/CoreBundle/Settings/PlatformSettingsSchema.php b/src/CoreBundle/Settings/PlatformSettingsSchema.php index a8cedbb798e..0df3f71ed3d 100644 --- a/src/CoreBundle/Settings/PlatformSettingsSchema.php +++ b/src/CoreBundle/Settings/PlatformSettingsSchema.php @@ -40,7 +40,6 @@ public function buildSettings(AbstractSettingsBuilder $builder): void 'institution_address' => '', 'site_name' => 'Chamilo site', 'timezone' => 'Europe/Paris', - 'theme' => 'chamilo', 'gravatar_enabled' => 'false', 'gravatar_type' => 'mm', 'gamification_mode' => ' ', @@ -120,7 +119,6 @@ public function buildForm(FormBuilderInterface $builder): void ->add('institution_address') ->add('site_name') ->add('timezone', TimezoneType::class) - ->add('theme') ->add('gravatar_enabled', YesNoType::class) ->add( 'gravatar_type', diff --git a/src/CoreBundle/Settings/SettingsManager.php b/src/CoreBundle/Settings/SettingsManager.php index f598ef9175f..a741083a2ee 100644 --- a/src/CoreBundle/Settings/SettingsManager.php +++ b/src/CoreBundle/Settings/SettingsManager.php @@ -139,7 +139,7 @@ public function updateSetting(string $name, $value): void * 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 @@ private function getVariablesAndCategories(): array // '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 @@ private function renameVariable($variable) 'siteName' => 'site_name', 'InstitutionUrl' => 'institution_url', 'registration' => 'required_profile_fields', - 'stylesheets' => 'theme', 'platformLanguage' => 'platform_language', 'languagePriority1' => 'language_priority_1', 'languagePriority2' => 'language_priority_2', diff --git a/src/CoreBundle/Settings/StylesheetsSettingsSchema.php b/src/CoreBundle/Settings/StylesheetsSettingsSchema.php deleted file mode 100644 index fd0392f932d..00000000000 --- a/src/CoreBundle/Settings/StylesheetsSettingsSchema.php +++ /dev/null @@ -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)); - } -} diff --git a/src/CoreBundle/State/AccessUrlRelColorThemeStateProcessor.php b/src/CoreBundle/State/AccessUrlRelColorThemeStateProcessor.php new file mode 100644 index 00000000000..4be89c45278 --- /dev/null +++ b/src/CoreBundle/State/AccessUrlRelColorThemeStateProcessor.php @@ -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; + } +} diff --git a/src/CoreBundle/State/AccessUrlRelColorThemeStateProvider.php b/src/CoreBundle/State/AccessUrlRelColorThemeStateProvider.php new file mode 100644 index 00000000000..c1d3cbab77a --- /dev/null +++ b/src/CoreBundle/State/AccessUrlRelColorThemeStateProvider.php @@ -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; + } +} diff --git a/src/CoreBundle/State/ColorThemeStateProcessor.php b/src/CoreBundle/State/ColorThemeStateProcessor.php index e0a6473c8e8..d70e38f7d6f 100644 --- a/src/CoreBundle/State/ColorThemeStateProcessor.php +++ b/src/CoreBundle/State/ColorThemeStateProcessor.php @@ -8,10 +8,13 @@ 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 @@ public function process($data, Operation $operation, array $uriVariables = [], a $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) ); } diff --git a/src/CoreBundle/Twig/Extension/ChamiloExtension.php b/src/CoreBundle/Twig/Extension/ChamiloExtension.php index df000b59e61..5fcf06a0733 100644 --- a/src/CoreBundle/Twig/Extension/ChamiloExtension.php +++ b/src/CoreBundle/Twig/Extension/ChamiloExtension.php @@ -10,6 +10,7 @@ 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 @@ public function getFunctions(): array 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 @@ public function getName(): string { 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); + } } diff --git a/tests/CoreBundle/Controller/ThemeControllerTest.php b/tests/CoreBundle/Controller/ThemeControllerTest.php new file mode 100644 index 00000000000..09b6db30390 --- /dev/null +++ b/tests/CoreBundle/Controller/ThemeControllerTest.php @@ -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); + } +} diff --git a/tests/CoreBundle/Twig/SettingsHelperTest.php b/tests/CoreBundle/Twig/SettingsHelperTest.php index 321e41a4bcb..59811d33af1 100644 --- a/tests/CoreBundle/Twig/SettingsHelperTest.php +++ b/tests/CoreBundle/Twig/SettingsHelperTest.php @@ -26,8 +26,8 @@ public function testGetSettings(): void $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); } } diff --git a/var/themes/chamilo/colors.css b/var/themes/chamilo/colors.css new file mode 100644 index 00000000000..9700b2c1ff0 --- /dev/null +++ b/var/themes/chamilo/colors.css @@ -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; +} diff --git a/assets/css/themes/chamilo/images/avatar.svg b/var/themes/chamilo/images/avatar.svg similarity index 100% rename from assets/css/themes/chamilo/images/avatar.svg rename to var/themes/chamilo/images/avatar.svg diff --git a/assets/css/themes/chamilo/images/favicon.ico b/var/themes/chamilo/images/favicon.ico similarity index 100% rename from assets/css/themes/chamilo/images/favicon.ico rename to var/themes/chamilo/images/favicon.ico diff --git a/assets/css/themes/chamilo/images/header-logo.png b/var/themes/chamilo/images/header-logo.png similarity index 100% rename from assets/css/themes/chamilo/images/header-logo.png rename to var/themes/chamilo/images/header-logo.png diff --git a/assets/css/themes/chamilo/images/header-logo.svg b/var/themes/chamilo/images/header-logo.svg similarity index 100% rename from assets/css/themes/chamilo/images/header-logo.svg rename to var/themes/chamilo/images/header-logo.svg diff --git a/webpack.config.js b/webpack.config.js index 3ba39fe3ddf..24e12a1f529 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -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: {