User: Verify and improve Terms & Conditions and GDPR - refs #5121

pull/5189/head
christianbeeznst 2 years ago
parent 9b0e6fc6df
commit bfa7400628
  1. 53
      assets/vue/components/basecomponents/BaseEditor.vue
  2. 27
      assets/vue/components/basecomponents/BaseRadioButtons.vue
  3. 2
      assets/vue/router/index.js
  4. 18
      assets/vue/router/terms.js
  5. 6
      assets/vue/services/api.js
  6. 20
      assets/vue/services/languageService.js
  7. 37
      assets/vue/services/legalService.js
  8. 196
      assets/vue/views/terms/TermsEdit.vue
  9. 20
      assets/vue/views/terms/TermsLayout.vue
  10. 111
      assets/vue/views/terms/TermsList.vue
  11. 4
      public/main/admin/user_list_consent.php
  12. 10
      public/main/auth/inscription.php
  13. 73
      public/main/inc/lib/extra_field.lib.php
  14. 2
      public/main/inc/lib/extra_field_value.lib.php
  15. 2
      src/CoreBundle/Controller/Admin/IndexBlocksController.php
  16. 192
      src/CoreBundle/Controller/LegalController.php
  17. 48
      src/CoreBundle/Controller/SecurityController.php
  18. 14
      src/CoreBundle/Entity/Language.php
  19. 95
      src/CoreBundle/Entity/Legal.php
  20. 11
      src/CoreBundle/Repository/LegalRepository.php

@ -0,0 +1,53 @@
<template>
<div class="base-editor">
<label v-if="title" :for="editorId">{{ title }}</label>
<TinyEditor
:id="editorId"
:model-value="modelValue"
:init="editorConfig"
:required="required"
@update:model-value="updateValue"
@input="updateValue"
/>
<p v-if="helpText" class="help-text">{{ helpText }}</p>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const props = defineProps({
editorId: String,
modelValue: String,
required: Boolean,
editorConfig: Object,
title: String,
helpText: String
})
const emit = defineEmits(['update:modelValue'])
const updateValue = (value) => {
emit('update:modelValue', value)
document.getElementById(props.editorId).value = value
}
const defaultEditorConfig = {
skin_url: '/build/libs/tinymce/skins/ui/oxide',
content_css: '/build/libs/tinymce/skins/content/default/content.css',
branding: false,
relative_urls: false,
height: 280,
toolbar_mode: 'sliding',
file_picker_callback: browser,
autosave_ask_before_unload: true,
plugins: [
'fullpage advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste wordcount emoticons',
],
toolbar: 'undo redo | bold italic underline strikethrough | ...',
}
const editorConfig = computed(() => ({
...defaultEditorConfig,
...props.editorConfig
}))
function browser(callback, value, meta) {
}
</script>

@ -1,12 +1,12 @@
<template>
<div class="flex flex-col">
<label v-if="title" :for="name" class="mb-2">{{ title }}</label>
<div v-for="(option, index) in options" :key="option.value" class="flex items-center mr-2">
<RadioButton
:input-id="name + index"
:model-value="value"
:input-id="`${name}-${index}`"
v-model="value"
:name="name"
:value="option.value"
@update:model-value="value = $event"
/>
<label :for="name + index" class="ml-2 cursor-pointer">{{ option.label }}</label>
</div>
@ -14,19 +14,18 @@
</template>
<script setup>
import RadioButton from 'primevue/radiobutton';
import {ref} from "vue";
import RadioButton from 'primevue/radiobutton'
import { ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
type: [String, Number],
required: true
},
name: {
type: String,
required: true,
},
// Array with {label: x, value: y} for every option you want to support
title: String,
options: {
type: Array,
required: true,
@ -36,8 +35,12 @@ const props = defineProps({
default: ''
},
})
defineEmits(['update:modelValue'])
const value = ref(props.initialValue)
const emit = defineEmits(['update:modelValue'])
const value = ref(props.modelValue)
watch(() => props.modelValue, (newValue) => {
value.value = newValue
})
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
</script>

@ -11,6 +11,7 @@ import calendarEventRoutes from "./ccalendarevent"
import toolIntroRoutes from "./ctoolintro"
import pageRoutes from "./page"
import socialNetworkRoutes from "./social"
import termsRoutes from "./terms"
//import courseCategoryRoutes from './coursecategory';
import documents from "./documents"
@ -139,6 +140,7 @@ const router = createRouter({
component: MySessionListUpcoming,
meta: { requiresAuth: true },
},
termsRoutes,
socialNetworkRoutes,
catalogueCourses,
catalogueSessions,

@ -0,0 +1,18 @@
export default {
path: '/resources/terms-conditions',
meta: { requiresAuth: true },
name: 'TermsConditions',
component: () => import('../views/terms/TermsLayout.vue'),
children: [
{
name: 'TermsConditionsList',
path: '',
component: () => import('../views/terms/TermsList.vue')
},
{
name: 'TermsConditionsEdit',
path: 'edit',
component: () => import('../views/terms/TermsEdit.vue')
}
]
}

@ -1,7 +1,7 @@
import fetch from '../utils/fetch';
export default function makeService(endpoint) {
return {
export default function makeService(endpoint, extensions = {}) {
const baseService = {
find(id, params) {
console.log('api.js find');
const currentParams = new URLSearchParams(window.location.search);
@ -65,4 +65,6 @@ export default function makeService(endpoint) {
});
}
};
return { ...baseService, ...extensions };
}

@ -0,0 +1,20 @@
import makeService from './api'
import { ENTRYPOINT } from "../config/entrypoint"
const legalExtensions = {
async findAllAvailable() {
const url = new URL(`${ENTRYPOINT}languages`)
url.searchParams.append("available", "true")
try {
const response = await fetch(url.toString())
if (!response.ok) {
throw new Error('Network response was not ok')
}
return await response.json()
} catch (error) {
console.error('Error fetching available languages:', error)
throw error
}
},
}
export default makeService('languages', legalExtensions)

@ -0,0 +1,37 @@
import makeService from './api';
import { ENTRYPOINT } from "../config/entrypoint"
const legalExtensions = {
async findAllByLanguage(languageId) {
const params = new URLSearchParams({
languageId: languageId,
'order[version]': 'desc'
});
return fetch(`${ENTRYPOINT}legals?${params.toString()}`);
},
async saveOrUpdateLegal(payload) {
console.log('Saving or updating legal terms');
return fetch(`/legal/save`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
},
async fetchExtraFields(termId = null) {
try {
const url = termId ? `/legal/extra-fields?termId=${termId}` : `/legal/extra-fields`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error loading extra fields:', error);
throw error;
}
},
};
export default makeService('legals', legalExtensions);

@ -0,0 +1,196 @@
<template>
<div class="terms-edit-view mb-8">
<Message severity="info" icon="pi pi-send" :closable="false" class="mt-5">
{{ t('Display a Terms & Conditions statement on the registration page, require visitor to accept the T&C to register') }}
</Message>
<BaseToolbar showTopBorder>
<div class="flex justify-between w-full items-center">
<BaseDropdown
class="w-96 mb-0"
:options="languages"
v-model="selectedLanguage"
optionLabel="name"
placeholder="Select a language"
inputId="language-dropdown"
label="Language"
name="language"
/>
<BaseButton :label="t('Load')" @click="loadTermsByLanguage" icon="search" type="button" class="ml-4"/>
<BaseButton :label="t('All versions')" type="secondary" @click="backToList" icon="back" class="ml-4" />
</div>
</BaseToolbar>
<div v-if="termsLoaded">
<form @submit.prevent="saveTerms">
<BaseEditor
:editorId="'item_content'"
v-model="termData.content"
:title="t('Personal Data Collection')"
>
<template #help-text>
<p>{{ t('Why do we collect this data?') }}</p>
</template>
</BaseEditor>
<BaseRadioButtons
:options="typeOptions"
v-model="termData.type"
name="termsType"
:title="t('Type of Terms')"
/>
<Dialog v-model:visible="dialogVisible" :style="{ width: '50vw' }" :header="t('Preview')" :modal="true">
<div v-html="previewContent" />
</Dialog>
<!-- Extra fields -->
<div v-for="field in extraFields" :key="field.id" class="extra-field">
<component :is="getFieldComponent(field.type)" v-bind="field.props" @update:modelValue="field.props.modelValue = $event">
<template v-if="field.type === 'editor'" #help-text>
<p>{{ field.props.helpText }}</p>
</template>
</component>
</div>
<BaseTextArea
id="changes"
label="Explain changes"
v-model="termData.changes"
/>
<div class="form-actions">
<BaseButton label="Back" type="secondary" @click="backToList" icon="back" class="mr-4" />
<BaseButton label="Preview" type="primary" @click="previewTerms" icon="search" class="mr-4" />
<BaseButton label="Save" type="success" isSubmit icon="save" class="mr-4" />
</div>
</form>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from "vue"
import { useRouter } from "vue-router"
import BaseToolbar from "../../components/basecomponents/BaseToolbar.vue"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import BaseTextArea from "../../components/basecomponents/BaseTextArea.vue"
import Message from "primevue/message"
import BaseDropdown from "../../components/basecomponents/BaseDropdown.vue"
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue"
import BaseInputText from "../../components/basecomponents/BaseInputText.vue"
import BaseEditor from "../../components/basecomponents/BaseEditor.vue"
import { useI18n } from "vue-i18n"
import languageService from "../../services/languageService"
import legalService from "../../services/legalService"
const { t } = useI18n()
const router = useRouter()
const languages = ref([])
const selectedLanguage = ref(null)
const termsLoaded = ref(false)
const termData = ref({
language: '',
content: '',
type: '0',
changes: '',
})
const dialogVisible = ref(false)
const previewContent = ref('')
const typeOptions = ref([
{ label: 'HTML', value: '0' },
{ label: 'Page Link', value: '1' }
])
const loadTermsByLanguage = async () => {
if (!selectedLanguage.value) return
termsLoaded.value = false
try {
const response = await legalService.findAllByLanguage(selectedLanguage.value.id)
if (response.ok) {
const data = await response.json()
const latestTerm = data['hydra:member'].length ? data['hydra:member'][0] : null
termData.value = latestTerm ? {
id: latestTerm.id,
content: latestTerm.content,
type: latestTerm.type.toString(),
changes: latestTerm.changes,
} : {
content: '',
type: '0',
changes: '',
}
extraFields.value = await legalService.fetchExtraFields(latestTerm ? latestTerm.id : null)
}
} catch (error) {
console.error('Error loading terms:', error)
} finally {
termsLoaded.value = true
}
}
const saveTerms = async () => {
const payload = {
lang: selectedLanguage.value.id,
content: termData.value.content,
type: termData.value.type.toString(),
changes: termData.value.changes,
extraFields: {},
}
extraFields.value.forEach(field => {
payload.extraFields[field.id] = field.props.modelValue
})
try {
const response = await legalService.saveOrUpdateLegal(payload)
if (response.ok) {
await router.push({ name: 'TermsConditionsList' })
} else {
console.error('Error saving or updating legal terms:', response.statusText)
}
} catch (error) {
console.error('Error when making request:', error)
}
}
const previewTerms = () => {
previewContent.value = termData.value.content
dialogVisible.value = true
}
const closePreview = () => {
dialogVisible.value = false
}
function backToList() {
router.push({ name: 'TermsConditionsList' })
}
const extraFields = ref([])
function getFieldComponent(type) {
const componentMap = {
text: BaseInputText,
select: BaseDropdown,
editor: BaseEditor,
// Add more mappings as needed
}
return componentMap[type] || 'div'
}
watch(selectedLanguage, () => {
termsLoaded.value = false
})
onMounted(async () => {
try {
const response = await languageService.findAll()
if (!response.ok) {
throw new Error('Network response was not ok')
}
const data = await response.json()
languages.value = data['hydra:member'].map(lang => ({
name: lang.englishName,
id: lang.id,
}))
} catch (error) {
console.error('Error loading languages:', error)
}
})
</script>

@ -0,0 +1,20 @@
<template>
<div class="terms-layout">
<header>
<h1>{{ t('Terms and conditions') }}</h1>
</header>
<main>
<router-view />
</main>
<footer>
</footer>
</div>
</template>
<script setup>
import { useI18n } from "vue-i18n"
const { t } = useI18n()
</script>

@ -0,0 +1,111 @@
<template>
<div class="terms-list-view mt-4">
<BaseToolbar>
<BaseButton
:label="t('Edit Terms and Conditions')"
icon="edit"
type="primary"
@click="editTerms"
/>
</BaseToolbar>
<Message severity="warn" :closable="false">
{{ t('You should create the Term and Conditions for all the available languages.') }}
</Message>
<DataTable :value="terms" :loading="isLoading">
<Column field="version" header="Version"></Column>
<Column field="language" header="Language"></Column>
<Column header="Content">
<template #body="slotProps">
<div v-html="slotProps.data.content"></div>
</template>
</Column>
<Column field="changes" header="Changes"></Column>
<Column field="typeLabel" header="Type"></Column>
<Column field="date" header="Date">
<template #body="slotProps">
{{ formatDate(slotProps.data.date) }}
</template>
</Column>
</DataTable>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue"
import DataTable from "primevue/datatable"
import Column from "primevue/column"
import BaseToolbar from "../../components/basecomponents/BaseToolbar.vue"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import { useRouter } from "vue-router"
import Message from "primevue/message"
import languageService from "../../services/languageService"
import legalService from "../../services/legalService"
const { t } = useI18n()
const router = useRouter()
const terms = ref([])
const isLoading = ref(false)
async function fetchLanguageName(languageId) {
try {
const response = await languageService.find("/api/languages/" + languageId)
if (response.ok) {
const languageData = await response.json()
return languageData.originalName
}
} catch (error) {
console.error("Error loading language details:", error)
}
return null
}
onMounted(async () => {
isLoading.value = true
try {
const response = await legalService.findAll()
if (response.ok) {
const data = await response.json()
terms.value = await Promise.all(data['hydra:member'].map(async (term) => {
const languageName = await fetchLanguageName(term.languageId)
return {
...term,
language: languageName,
typeLabel: getTypeLabel(term.type),
}
}))
} else {
console.error("The request to the API was not successful:", response.statusText)
}
} catch (error) {
console.error("Error loading legal terms:", error)
} finally {
isLoading.value = false
}
})
function getTypeLabel(typeValue) {
const typeMap = {
'0': t('HTML'),
'1': t('Page Link'),
}
return typeMap[typeValue] || 'Unknown'
}
function formatDate(timestamp) {
const date = new Date(timestamp * 1000)
const day = date.getDate().toString().padStart(2, '0')
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const year = date.getFullYear()
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`
}
function editTerms() {
router.push({ name: 'TermsConditionsEdit' })
}
</script>

@ -78,7 +78,7 @@ function prepare_user_sql_query($getCount)
ON (
u.id = v.item_id AND
(field_id = $extraFieldId OR field_id = $extraFieldIdDeleteAccount) AND
v.value = 1
v.field_value = 1
) ";
$keywordList = [
@ -317,7 +317,7 @@ function modify_filter($user_id, $url_params, $row): string
$result .= Display::url(
Display::getMdiIcon(ActionIcon::SEND_MESSAGE, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Send message')),
api_get_path(WEB_CODE_PATH).'messages/new_message.php?send_to_user='.$user_id
api_get_path(WEB_PATH).'resources/messages/new'
);
$result .= '&nbsp;&nbsp;';
$extraFields = Session::read('data_privacy_extra_fields');

@ -41,6 +41,8 @@ $webserviceUrl = '';
$hash = '';
if ($isTccEnabled) {
// Configure TCC plugin settings and JavaScript for the form
// (This section includes the JavaScript code for the TCC plugin integration)
$webserviceUrl = api_get_plugin_setting('logintcc', 'webservice_url');
$hash = api_get_plugin_setting('logintcc', 'hash');
$htmlHeadXtra[] = '<script>
@ -580,7 +582,7 @@ if ('true' === api_get_setting('allow_terms_conditions')) {
$termPreview = LegalManager::get_last_condition($language);
if (!$termPreview) {
//look for the default language
$language = api_get_setting('platformLanguage');
$language = api_get_setting('language.platform_language');
$language = api_get_language_id($language);
$termPreview = LegalManager::get_last_condition($language);
}
@ -599,7 +601,7 @@ if ('true' === api_get_setting('allow_terms_conditions')) {
} else {
echo get_lang('Coming soon...');
}
Display::display_footer();
//Display::display_footer();
exit;
}
}
@ -675,13 +677,13 @@ if ('true' === api_get_setting('allow_terms_conditions')) {
}
// Ofaj
if (!api_is_anonymous() || 'login' === api_get_setting('load_term_conditions_section')) {
if (!api_is_anonymous() || 'course' !== api_get_setting('platform.load_term_conditions_section')) {
$language = api_get_language_isocode();
$language = api_get_language_id($language);
$termPreview = LegalManager::get_last_condition($language);
if (!$termPreview) {
//we load from the platform
$language = api_get_setting('platformLanguage');
$language = api_get_setting('language.platform_language');
$language = api_get_language_id($language);
if (!empty($language)) {
$termPreview = LegalManager::get_last_condition($language);

@ -926,6 +926,79 @@ class ExtraField extends Model
return $extraFields;
}
/**
* Fetches extra field data with various display and permission checks.
*
* This function retrieves the data for extra fields, applies various filters
* and checks to determine if each field should be displayed based on
* admin permissions, visibility settings, and specific field inclusion or
* exclusion lists. It also handles ordering of fields if an order list is provided.
*/
public function getExtraFieldsData(
array $extraData,
bool $adminPermissions = false,
array $extra = [],
array $exclude = [],
array $showOnlyTheseFields = [],
array $orderFields = []
): array {
$fieldsData = [];
if (!empty($extra)) {
$orderedExtraFields = [];
if (!empty($orderFields)) {
foreach ($orderFields as $order) {
foreach ($extra as $fieldDetails) {
if ($order == $fieldDetails['variable']) {
$orderedExtraFields[] = $fieldDetails;
}
}
}
$extra = $orderedExtraFields;
}
foreach ($extra as $fieldDetails) {
$variable = $fieldDetails['variable'];
if (!empty($showOnlyTheseFields) && !in_array($variable, $showOnlyTheseFields)) {
continue;
}
if (!$adminPermissions && 0 == $fieldDetails['visible_to_self']) {
continue;
}
if (in_array($variable, $exclude)) {
continue;
}
$fieldData = [
'type' => $fieldDetails['value_type'],
'variable' => $variable,
'title' => get_lang($fieldDetails['display_text']),
'defaultValue' => $fieldDetails['field_default_value'] ?? '',
];
if (!empty($fieldDetails['options'])) {
$fieldData['options'] = array_map(function ($option) {
return [
'value' => $option['option_value'],
'label' => $option['display_text'],
];
}, $fieldDetails['options']);
}
if (isset($extraData['extra_' . $variable])) {
$fieldData['value'] = $extraData['extra_' . $variable];
}
$fieldsData[] = $fieldData;
}
}
return $fieldsData;
}
/**
* Add an element that matches the given extra field to the given $form object.
*

@ -912,7 +912,7 @@ class ExtraFieldValue extends Model
$itemId = (int) $itemId;
$extraFieldType = $this->getExtraField()->getItemType();
$sql = "SELECT s.field_value, s.field_value as value, sf.variable, sf.value_type, sf.id, sf.display_text
$sql = "SELECT s.field_value, s.field_value as value, sf.variable, sf.value_type, sf.id, sf.display_text, s.asset_id
FROM {$this->table} s
INNER JOIN {$this->table_handler_field} sf
ON (s.field_id = sf.id)

@ -414,7 +414,7 @@ class IndexBlocksController extends BaseController
if ('true' === $this->settingsManager->getSetting('registration.allow_terms_conditions')) {
$items[] = [
'class' => 'item-terms-and-conditions',
'url' => $this->generateUrl('legacy_main', ['name' => 'admin/legal_add.php']),
'url' => '/resources/terms-conditions',
'label' => $this->translator->trans('Terms and Conditions'),
];
}

@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\Legal;
use Chamilo\CoreBundle\Repository\LegalRepository;
use Doctrine\ORM\EntityManagerInterface;
use ExtraField;
use ExtraFieldValue;
use LegalManager;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('/legal')]
class LegalController
{
#[Route('/save', name: 'chamilo_core_legal_save', methods: ['POST'])]
public function saveLegal(
Request $request,
EntityManagerInterface $entityManager,
LegalRepository $legalRepository
): Response {
$data = json_decode($request->getContent(), true);
$lang = $data['lang'] ?? null;
$content = $data['content'] ?? null;
$type = isset($data['type']) ? (int)$data['type'] : null;
$changes = $data['changes'] ?? '';
$extraFields = $data['extraFields'] ?? [];
$lastLegal = $legalRepository->findLastConditionByLanguage($lang);
$extraFieldValue = new ExtraFieldValue('terms_and_condition');
$newVersionRequired = !$lastLegal || $lastLegal->getContent() !== $content || $this->hasExtraFieldsChanged($extraFieldValue, $lastLegal->getId(), $extraFields);
$typeUpdateRequired = $lastLegal && $lastLegal->getType() !== $type;
$legalToUpdate = $lastLegal;
if ($newVersionRequired) {
$legal = new Legal();
$legal->setLanguageId($lang);
$legal->setContent($content);
$legal->setType($type);
$legal->setChanges($changes);
$legal->setDate(time());
$version = $lastLegal ? $lastLegal->getVersion() + 1 : 1;
$legal->setVersion($version);
$entityManager->persist($legal);
$legalToUpdate = $legal;
} elseif ($typeUpdateRequired) {
$lastLegal->setType($type);
$lastLegal->setChanges($changes);
}
$entityManager->flush();
if ($newVersionRequired || $typeUpdateRequired) {
$this->updateExtraFields($extraFieldValue, $legalToUpdate->getId(), $extraFields);
}
return new Response('Term and condition saved or updated successfully', Response::HTTP_OK);
}
#[Route('/extra-fields', name: 'chamilo_core_get_extra_fields')]
public function getExtraFields(Request $request): JsonResponse
{
$extraField = new ExtraField('terms_and_condition');
$types = LegalManager::getTreatmentTypeList();
foreach ($types as $variable => $name) {
$label = 'PersonalData'.ucfirst($name).'Title';
$params = [
'variable' => $variable,
'display_text' => $label,
'value_type' => ExtraField::FIELD_TYPE_TEXTAREA,
'default_value' => '',
'visible' => true,
'changeable' => true,
'filter' => true,
'visible_to_self' => true,
'visible_to_others' => true,
];
$extraField->save($params);
}
$termId = $request->query->get('termId');
$extraData = $extraField->get_handler_extra_data($termId ?? 0);
$extraFieldsDefinition = $extraField->get_all();
$fieldsData = $extraField->getExtraFieldsData(
$extraData,
true,
$extraFieldsDefinition
);
$prefix = 'extra_';
$extraFields = [];
foreach ($fieldsData as $field) {
$fieldType = $this->mapFieldType($field['type']);
$extraField = [
'id' => $prefix.$field['variable'],
'type' => $fieldType,
'props' => [
'title' => $field['title'],
'defaultValue' => $field['defaultValue'],
],
];
switch ($fieldType) {
case 'editor':
$extraField['props']['editorId'] = $prefix.$field['variable'];
$extraField['props']['modelValue'] = $field['value'] ?? '';
$extraField['props']['helpText'] = 'Specific help text for ' . $field['title'];
break;
case 'text':
$extraField['props']['label'] = $field['title'];
$extraField['props']['modelValue'] = $field['value'] ?? '';
break;
case 'select':
$extraField['props']['label'] = $field['title'];
$extraField['props']['options'] = [];
$extraField['props']['modelValue'] = $field['value'] ?? '';
break;
}
$extraFields[] = $extraField;
}
return new JsonResponse($extraFields);
}
/**
* Checks if the extra field values have changed.
*
* This function compares the new values for extra fields against the old ones to determine
* if there have been any changes. It is useful for triggering events or updates only when
* actual changes to data occur.
*/
private function hasExtraFieldsChanged(ExtraFieldValue $extraFieldValue, int $legalId, array $newValues): bool
{
$oldValues = $extraFieldValue->getAllValuesByItem($legalId);
$oldValues = array_column($oldValues, 'value', 'variable');
foreach ($newValues as $key => $newValue) {
if (isset($oldValues[$key]) && $newValue != $oldValues[$key]) {
return true;
} elseif (!isset($oldValues[$key])) {
return true;
}
}
return false;
}
/**
* Updates the extra fields with new values for a specific item.
*/
private function updateExtraFields(ExtraFieldValue $extraFieldValue, int $legalId, array $values): void
{
$values['item_id'] = $legalId;
$extraFieldValue->saveFieldValues($values);
}
/**
* Maps an integer representing a field type to its corresponding string value.
*/
private function mapFieldType(int $type): string
{
switch ($type) {
case ExtraField::FIELD_TYPE_TEXT:
return 'text';
case ExtraField::FIELD_TYPE_TEXTAREA:
return 'editor';
case ExtraField::FIELD_TYPE_SELECT_MULTIPLE:
case ExtraField::FIELD_TYPE_DATE:
case ExtraField::FIELD_TYPE_DATETIME:
case ExtraField::FIELD_TYPE_DOUBLE_SELECT:
case ExtraField::FIELD_TYPE_RADIO:
// Manage as needed
break;
case ExtraField::FIELD_TYPE_SELECT:
return 'select';
}
return 'text';
}
}

@ -14,20 +14,36 @@ use Chamilo\CoreBundle\Repository\TrackELoginRecordRepository;
use Chamilo\CoreBundle\Settings\SettingsManager;
use DateTime;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Serializer\SerializerInterface;
class SecurityController extends AbstractController
{
private $entityManager;
private $settingsManager;
private $tokenStorage;
private $authorizationChecker;
public function __construct(
private readonly SerializerInterface $serializer,
private readonly TrackELoginRecordRepository $trackELoginRecordRepository
) {}
private SerializerInterface $serializer,
private TrackELoginRecordRepository $trackELoginRecordRepository,
EntityManagerInterface $entityManager,
SettingsManager $settingsManager,
TokenStorageInterface $tokenStorage,
AuthorizationCheckerInterface $authorizationChecker
) {
$this->entityManager = $entityManager;
$this->settingsManager = $settingsManager;
$this->tokenStorage = $tokenStorage;
$this->authorizationChecker = $authorizationChecker;
}
#[Route('/login_json', name: 'login_json', methods: ['POST'])]
public function loginJson(Request $request, EntityManager $entityManager, SettingsManager $settingsManager, TokenStorageInterface $tokenStorage): Response
@ -43,11 +59,11 @@ class SecurityController extends AbstractController
/** @var User $user */
$user = $this->getUser();
$extraFieldValuesRepository = $entityManager->getRepository(ExtraFieldValues::class);
$legalTermsRepo = $entityManager->getRepository(Legal::class);
$extraFieldValuesRepository = $this->entityManager->getRepository(ExtraFieldValues::class);
$legalTermsRepo = $this->entityManager->getRepository(Legal::class);
if ($user->hasRole('ROLE_STUDENT')
&& 'true' === $settingsManager->getSetting('allow_terms_conditions')
&& 'login' === $settingsManager->getSetting('load_term_conditions_section')
&& 'true' === $this->settingsManager->getSetting('allow_terms_conditions')
&& 'login' === $this->settingsManager->getSetting('load_term_conditions_section')
) {
$termAndConditionStatus = false;
$extraValue = $extraFieldValuesRepository->findLegalAcceptByItemId($user->getId());
@ -61,24 +77,24 @@ class SecurityController extends AbstractController
}
if (false === $termAndConditionStatus) {
$request->getSession()->set('term_and_condition', ['user_id' => $user->getId()]);
} else {
$request->getSession()->remove('term_and_condition');
}
$tempTermAndCondition = ['user_id' => $user->getId()];
$this->tokenStorage->setToken(null);
$request->getSession()->invalidate();
$request->getSession()->start();
$request->getSession()->set('term_and_condition', $tempTermAndCondition);
$termsAndCondition = $request->getSession()->get('term_and_condition');
if (null !== $termsAndCondition) {
$tokenStorage->setToken(null);
$responseData = [
'redirect' => '/main/auth/inscription.php',
'load_terms' => true,
];
return new JsonResponse($responseData, Response::HTTP_OK);
} else {
$request->getSession()->remove('term_and_condition');
}
}
// $error = $authenticationUtils->getLastAuthenticationError();
// $lastUsername = $authenticationUtils->getLastUsername();
$data = null;
if ($user) {

@ -6,36 +6,49 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Platform languages.
*/
#[ApiResource]
#[ApiFilter(BooleanFilter::class, properties: ['available'])]
#[ApiFilter(OrderFilter::class, properties: ['english_name' => 'DESC'])]
#[ORM\Table(name: 'language', options: ['row_format' => 'DYNAMIC'])]
#[ORM\Entity(repositoryClass: LanguageRepository::class)]
class Language
{
#[Groups(['language:read'])]
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue]
protected ?int $id = null;
#[Groups(['language:read', 'language:write'])]
#[Assert\NotBlank]
#[ORM\Column(name: 'original_name', type: 'string', length: 255, nullable: true)]
protected ?string $originalName = null;
#[Groups(['language:read', 'language:write'])]
#[Assert\NotBlank]
#[ORM\Column(name: 'english_name', type: 'string', length: 255)]
protected string $englishName;
#[Groups(['language:read', 'language:write'])]
#[Assert\NotBlank]
#[ORM\Column(name: 'isocode', type: 'string', length: 10)]
protected string $isocode;
#[Groups(['language:read', 'language:write'])]
#[ORM\Column(name: 'available', type: 'boolean', nullable: false)]
protected bool $available;
@ -43,6 +56,7 @@ class Language
#[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', nullable: true)]
protected ?Language $parent = null;
#[Groups(['language:read', 'language:write'])]
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
protected Collection $subLanguages;

@ -1,48 +1,55 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use Chamilo\CoreBundle\Repository\LegalRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* Legal.
*/
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['languageId' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['version' => 'DESC'])]
#[ORM\Table(name: 'legal')]
#[ORM\Entity(repositoryClass: LegalRepository::class)]
class Legal
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
protected ?int $id = null;
#[ORM\Column(name: 'date', type: 'integer', nullable: false)]
#[Groups(['legal:read', 'legal:write'])]
#[ORM\Column(type: 'integer')]
protected int $date;
#[ORM\Column(name: 'content', type: 'text', nullable: true)]
#[Groups(['legal:read', 'legal:write'])]
#[ORM\Column(type: 'text', nullable: true)]
protected ?string $content = null;
#[ORM\Column(name: 'type', type: 'integer', nullable: false)]
#[Groups(['legal:read', 'legal:write'])]
#[ORM\Column(type: 'integer')]
protected int $type;
#[ORM\Column(name: 'changes', type: 'text', nullable: false)]
#[Groups(['legal:read', 'legal:write'])]
#[ORM\Column(type: 'text')]
protected string $changes;
#[ORM\Column(name: 'version', type: 'integer', nullable: true)]
#[Groups(['legal:read', 'legal:write'])]
#[ORM\Column(type: 'integer', nullable: true)]
protected ?int $version = null;
#[ORM\Column(name: 'language_id', type: 'integer')]
#[Groups(['legal:read', 'legal:write'])]
#[ORM\Column(type: 'integer')]
protected int $languageId;
/**
* @return int
*/
public function getId()
public function getId(): ?int
{
return $this->id;
}
@ -59,26 +66,26 @@ class Legal
*
* @return int
*/
public function getDate()
public function getDate(): int
{
return $this->date;
}
public function setContent(string $content): self
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): self
{
$this->content = $content;
return $this;
}
/**
* Get content.
*
* @return string
*/
public function getContent()
public function getType(): int
{
return $this->content;
return $this->type;
}
public function setType(int $type): self
@ -88,14 +95,9 @@ class Legal
return $this;
}
/**
* Get type.
*
* @return int
*/
public function getType()
public function getChanges(): string
{
return $this->type;
return $this->changes;
}
public function setChanges(string $changes): self
@ -105,31 +107,20 @@ class Legal
return $this;
}
/**
* Get changes.
*
* @return string
*/
public function getChanges()
public function getVersion(): ?int
{
return $this->changes;
return $this->version;
}
public function setVersion(int $version): self
public function setVersion(?int $version): self
{
$this->version = $version;
return $this;
}
/**
* Get version.
*
* @return int
*/
public function getVersion()
public function getLanguageId(): int
{
return $this->version;
return $this->languageId;
}
public function setLanguageId(int $languageId): self
@ -138,14 +129,4 @@ class Legal
return $this;
}
/**
* Get languageId.
*
* @return int
*/
public function getLanguageId()
{
return $this->languageId;
}
}

@ -149,6 +149,17 @@ class LegalRepository extends ServiceEntityRepository
}
}
public function findLastConditionByLanguage(int $languageId): ?Legal
{
return $this->createQueryBuilder('l')
->andWhere('l.languageId = :languageId')
->setParameter('languageId', $languageId)
->orderBy('l.version', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
/**
* Replace tags in content.
*

Loading…
Cancel
Save