Merge branch 'master' of github.com:chamilo/chamilo-lms

pull/5235/head
Angel Fernando Quiroz Campos 2 years ago
commit 45bd9ae5ee
  1. 2
      assets/css/scss/atoms/_checkbox.scss
  2. 9
      assets/css/scss/atoms/_dropdown.scss
  3. 10
      assets/vue/components/basecomponents/BaseAutocomplete.vue
  4. 8
      assets/vue/components/basecomponents/BaseDropdown.vue
  5. 58
      assets/vue/components/basecomponents/BaseInputTags.vue
  6. 34
      assets/vue/components/basecomponents/BaseInputText.vue
  7. 67
      assets/vue/components/basecomponents/BaseMultiSelect.vue
  8. 19
      assets/vue/components/basecomponents/BaseToolbar.vue
  9. 340
      assets/vue/components/course/Form.vue
  10. 7
      assets/vue/layouts/MyCourses.vue
  11. 38
      assets/vue/services/courseService.js
  12. 87
      assets/vue/views/course/Create.vue
  13. 29
      public/main/admin/extra_fields.php
  14. 10
      public/main/forum/forumfunction.inc.php
  15. 9
      public/main/inc/ajax/model.ajax.php
  16. 32
      public/main/inc/global.inc.php
  17. 601
      public/main/inc/lib/add_course.lib.inc.php
  18. 43
      public/main/inc/lib/course.lib.php
  19. 6
      public/main/inc/lib/events.lib.php
  20. 52
      public/main/inc/lib/extra_field.lib.php
  21. 43
      public/main/inc/lib/plugin.class.php
  22. 2
      public/main/inc/lib/statistics.lib.php
  23. 20
      public/main/inc/lib/usermanager.lib.php
  24. 105
      src/CoreBundle/Controller/CourseController.php
  25. 18
      src/CoreBundle/Entity/ExtraField.php
  26. 34
      src/CoreBundle/Migrations/Schema/V200/Version20240308225800.php
  27. 2
      src/CoreBundle/Repository/CourseCategoryRepository.php
  28. 18
      src/CoreBundle/Repository/Node/CourseRepository.php
  29. 487
      tests/scripts/synchronize_user_base_from_ldap.php

@ -2,7 +2,7 @@
@apply h-4 w-4;
.p-checkbox-box {
@apply border border-solid border-gray-50 bg-white h-4 rounded w-4 transition-none;
@apply border border-solid border-gray-50 bg-white h-4 rounded w-4 transition-none invisible;
.p-checkbox-icon {
@apply text-caption font-semibold text-white;

@ -1,4 +1,4 @@
.p-dropdown {
.p-dropdown, .p-multiselect {
@apply border border-support-3 bg-white rounded-lg transition w-full duration-200
hover:border-primary hover:text-gray-90 hover:outline-0 hover:outline-none;
@ -8,7 +8,7 @@
}
}
.p-dropdown-label {
.p-dropdown-label, .p-multiselect-label {
@apply bg-transparent border-none
focus:outline-0 focus:outline-none;
@ -29,7 +29,7 @@
}
}
.p-dropdown-panel {
.p-dropdown-panel, .p-autocomplete-panel, .p-multiselect-panel {
@apply bg-white rounded-lg text-gray-90 shadow-lg;
.p-dropdown-header {
@ -88,4 +88,7 @@
}
}
}
.p-multiselect-label-container {
@apply p-2;
}

@ -86,8 +86,12 @@ const baseModel = ref([])
const suggestions = ref([])
const onComplete = async (event) => {
const members = await props.search(event.query)
suggestions.value = members.length > 0 ? members : []
try {
const members = await props.search(event.query)
suggestions.value = members && members.length ? members : []
} catch (error) {
console.error('Error during onComplete:', error)
suggestions.value = []
}
}
</script>

@ -15,11 +15,12 @@
<label :for="inputId" v-text="label" />
</div>
<small v-if="isInvalid" :class="{ 'p-error': isInvalid }" v-text="errorText" />
<small v-if="helpText" class="form-text text-muted">{{ helpText }}</small>
</div>
</template>
<script setup>
import Dropdown from "primevue/dropdown";
import Dropdown from "primevue/dropdown"
defineProps({
name: {
@ -71,7 +72,8 @@ defineProps({
required: false,
default: false,
},
});
helpText: String,
})
defineEmits(["update:modelValue"]);
defineEmits(["update:modelValue"])
</script>

@ -0,0 +1,58 @@
<template>
<div class="border border-gray-300 p-1 rounded-md flex flex-wrap items-center">
<div class="flex flex-wrap items-center p-1 flex-grow min-h-[38px] outline-none border-none">
<div v-for="(tag, index) in tags" :key="index" class="bg-blue-500 text-white mr-1 mb-1 px-2.5 py-1 rounded-full flex items-center text-sm">
{{ tag }}
<span class="ml-2 cursor-pointer font-bold" @click.stop="removeTag(index)">&times;</span>
</div>
<input
ref="tagInput"
v-model="newTag"
@keyup="checkInputKey"
@keydown.delete="deleteLastTag"
placeholder="Add a tag"
class="flex-grow outline-none border-none p-0 m-0 text-sm"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const tags = ref([])
const newTag = ref('')
const tagInput = ref(null)
function focusInput() {
tagInput.value.focus()
}
function addTag() {
if (newTag.value.trim() && !tags.value.includes(newTag.value.trim())) {
tags.value.push(newTag.value.trim())
newTag.value = ''
}
}
function removeTag(index) {
tags.value.splice(index, 1)
}
function deleteLastTag(event) {
if (newTag.value === '' && event.key === 'Backspace' && tags.value.length > 0) {
tags.value.pop()
}
}
function checkInputKey(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
addTag()
}
}
onMounted(() => {
focusInput()
})
</script>

@ -7,22 +7,12 @@
:class="{ 'p-invalid': isInvalid }"
:aria-label="label"
type="text"
@update:model-value="$emit('update:modelValue', $event)"
/>
<label
v-t="label"
:class="{ 'p-error': isInvalid }"
:for="id"
class="text-primary/40"
@update:model-value="updateValue"
/>
<label :for="id" :class="{ 'p-error': isInvalid }">{{ label }}</label>
</div>
<slot name="errors">
<small
v-if="isInvalid"
v-t="errorText"
class="p-error"
/>
</slot>
<small v-if="formSubmitted && isInvalid" class="p-error">{{ errorText }}</small>
<small v-if="helpText" class="form-text text-muted">{{ helpText }}</small>
</div>
</template>
@ -47,14 +37,26 @@ const props = defineProps({
errorText: {
type: String,
required: false,
default: null,
default: "",
},
isInvalid: {
type: Boolean,
required: false,
default: false,
},
required: {
type: Boolean,
default: false,
},
helpText: String,
formSubmitted: {
type: Boolean,
default: false
},
})
defineEmits(["update:modelValue"])
const emits = defineEmits(["update:modelValue"])
const updateValue = (value) => {
emits("update:modelValue", value)
}
</script>

@ -0,0 +1,67 @@
<template>
<div class="field">
<div class="p-float-label">
<MultiSelect
id="multiSelect"
v-model="selectedValues"
:options="options"
optionLabel="name"
optionValue="id"
display="chip"
@update:model-value="updateModelValue"
@focus="isFocused = true"
@blur="isFocused = false"
panelClass="multi-select-panel"
/>
<label :for="inputId" v-text="label" />
</div>
<small v-if="isInvalid" :class="{ 'p-error': isInvalid }" v-text="errorText" />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import MultiSelect from 'primevue/multiselect'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
options: {
type: Array,
default: () => []
},
placeholder: String,
inputId: {
type: String,
required: true,
default: "",
},
label: {
type: String,
required: true,
default: "",
},
errorText: {
type: String,
required: false,
default: null,
},
isInvalid: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const selectedValues = ref([...props.modelValue])
const isFocused = ref(false)
watch(() => props.modelValue, (newValue) => {
selectedValues.value = [...newValue]
})
const updateModelValue = (newValue) => {
emit('update:modelValue', newValue)
}
</script>

@ -1,26 +1,29 @@
<template>
<Toolbar :class="toolbarClass">
<template #start>
<slot></slot>
<template v-slot:start>
<slot name="start"></slot>
</template>
<template v-slot:end>
<slot name="end"></slot>
</template>
</Toolbar>
</template>
<script setup>
import Toolbar from "primevue/toolbar";
import { computed } from "vue";
import Toolbar from "primevue/toolbar"
import { computed } from "vue"
const props = defineProps({
showTopBorder: {
type: Boolean,
default: false,
},
});
})
const toolbarClass = computed(() => {
if (props.showTopBorder) {
return "pt-5 border-t border-b";
return "pt-5 border-t border-b"
}
return "";
});
return "p-toolbar"
})
</script>

@ -1,174 +1,174 @@
<template>
<v-form>
<v-container fluid>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.title"
:error-messages="titleErrors"
:label="$t('Title')"
required
@input="$v.item.title.$touch()"
@blur="$v.item.title.$touch()"
/>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.code"
:error-messages="codeErrors"
:label="$t('code')"
required
@input="$v.item.code.$touch()"
@blur="$v.item.code.$touch()"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-combobox
v-model="item.category"
:items="categorySelectItems"
:error-messages="categoryErrors"
:no-data-text="$t('No results')"
:label="$t('category')"
item-text="name"
item-value="@id"
/>
</v-col>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model.number="item.visibility"
:error-messages="visibilityErrors"
:label="$t('visibility')"
required
@input="$v.item.visibility.$touch()"
@blur="$v.item.visibility.$touch()"
/>
</v-col>
</v-row>
</v-container>
</v-form>
<div class="course-form-container">
<div class="form-header">
<BaseInputText
id="course-name"
:label="t('Course name')"
:help-text="t('Write a short and striking course name, For example: Innovation Management')"
v-model="courseName"
:error-text="courseNameError"
:is-invalid="isCourseNameInvalid"
required
/>
<BaseAdvancedSettingsButton v-model="showAdvancedSettings"></BaseAdvancedSettingsButton>
</div>
<div v-if="showAdvancedSettings" class="advanced-settings">
<BaseMultiSelect
id="category-multiselect"
v-model="courseCategory"
:options="categoryOptions"
:label="t('Category')"
input-id="multiselect-category"
/>
<BaseInputText
id="course-code"
:label="t('Course code')"
:help-text="t('Only letters (a-z) and numbers (0-9)')"
v-model="courseCode"
:maxlength="40"
:error-text="courseCodeError"
:is-invalid="isCodeInvalid"
validation-message="Only letters (a-z) and numbers (0-9) are allowed."
/>
<BaseDropdown
name="language"
v-model="courseLanguage"
:options="languageOptions"
:placeholder="t('Select Language')"
input-id="language-dropdown"
:label="t('Language')"
option-label="name"
/>
<BaseCheckbox
id="demo-content"
:label="t('Fill with demo content')"
v-model="fillDemoContent"
name=""
/>
<BaseAutocomplete
id="template"
v-model="courseTemplate"
:label="t('Select Template')"
:search="searchTemplates"
/>
</div>
<!-- Form Footer -->
<div class="form-footer">
<BaseButton
label="Back"
icon="back"
type="secondary"
@click="goBack"
class="mr-4"
/>
<BaseButton
:label="t('Create this course')"
icon="plus"
type="primary"
@click="submitForm"
/>
</div>
</div>
</template>
<script>
import has from 'lodash/has';
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { mapActions } from 'vuex';
import { mapFields } from 'vuex-map-fields';
export default {
name: 'CourseForm',
setup () {
return { v$: useVuelidate() }
},
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
}
},
data() {
return {
title: null,
code: null,
category: null,
visibility: null,
};
},
computed: {
...mapFields('coursecategory', {
categorySelectItems: 'selectItems'
}),
// eslint-disable-next-line
item() {
return this.initialValues || this.values;
},
titleErrors() {
const errors = [];
if (!this.$v.item.title.$dirty) return errors;
has(this.violations, 'title') && errors.push(this.violations.title);
!this.$v.item.title.required && errors.push(this.$t('Field is required'));
return errors;
},
codeErrors() {
const errors = [];
if (!this.$v.item.code.$dirty) return errors;
has(this.violations, 'code') && errors.push(this.violations.code);
!this.$v.item.code.required && errors.push(this.$t('Field is required'));
return errors;
},
categoryErrors() {
const errors = [];
if (!this.$v.item.category.$dirty) return errors;
has(this.violations, 'category') && errors.push(this.violations.category);
return errors;
},
visibilityErrors() {
const errors = [];
if (!this.$v.item.visibility.$dirty) return errors;
has(this.violations, 'visibility') && errors.push(this.violations.visibility);
!this.$v.item.visibility.required && errors.push(this.$t('Field is required'));
return errors;
},
violations() {
return this.errors || {};
}
},
mounted() {
this.categoryGetSelectItems();
},
methods: {
...mapActions({
categoryGetSelectItems: 'coursecategory/load'
}),
},
validations: {
item: {
title: {
required,
},
code: {
required,
},
category: {
},
visibility: {
required,
},
}
}
};
<script setup>
import { onMounted, ref, watch } from "vue"
import BaseInputText from "../basecomponents/BaseInputText.vue"
import BaseAdvancedSettingsButton from "../basecomponents/BaseAdvancedSettingsButton.vue"
import BaseDropdown from "../basecomponents/BaseDropdown.vue"
import BaseCheckbox from "../basecomponents/BaseCheckbox.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
import { useRouter } from "vue-router"
import courseService from "../../services/courseService"
import languageService from "../../services/languageService"
import BaseAutocomplete from "../basecomponents/BaseAutocomplete.vue"
import BaseMultiSelect from "../basecomponents/BaseMultiSelect.vue"
import { useI18n } from "vue-i18n"
const { t } = useI18n()
const courseName = ref('')
const courseCategory = ref([])
const courseCode = ref('')
const courseLanguage = ref(null)
const fillDemoContent = ref(false)
const courseTemplate = ref(null);
const showAdvancedSettings = ref(false)
const router = useRouter()
const categoryOptions = ref([])
const languageOptions = ref([])
const courseNameError = ref('')
const courseCodeError = ref('')
const isCodeInvalid = ref(false)
const isCourseNameInvalid = ref(false)
const formSubmitted = ref(false)
const emit = defineEmits(['submit'])
const validateCourseCode = () => {
const pattern = /^[a-zA-Z0-9]*$/
if (!pattern.test(courseCode.value)) {
isCodeInvalid.value = true
courseCodeError.value = 'Only letters (a-z) and numbers (0-9) are allowed.'
return false
}
courseCodeError.value = ''
return true
}
const submitForm = () => {
formSubmitted.value = true
if (!courseName.value) {
isCourseNameInvalid.value = true
courseNameError.value = 'This field is required'
return
}
if (!validateCourseCode()) {
return
}
emit('submit', {
name: courseName.value,
category: courseCategory.value ? courseCategory.value : null,
code: courseCode.value,
language: courseLanguage.value,
template: courseTemplate.value ? courseTemplate.value.value : null,
fillDemoContent: fillDemoContent.value
})
}
onMounted(async () => {
try {
const categoriesResponse = await courseService.getCategories('categories');
categoryOptions.value = categoriesResponse.map(category => ({
name: category.name,
id: category.id,
}))
const languagesResponse = await languageService.findAll()
const data = await languagesResponse.json()
languageOptions.value = data['hydra:member'].map(language => ({
name: language.englishName,
id: language.isocode,
}))
} catch (error) {
console.error('Failed to load dropdown data', error)
}
});
const searchTemplates = async (query) => {
if (query && query.length >= 3) {
return courseService.searchTemplates(query)
} else {
return []
}
}
const goBack = () => {
router.go(-1)
}
</script>

@ -6,6 +6,7 @@
:label="t('Course')"
class="p-button-secondary hidden md:inline-flex"
icon="pi pi-plus"
@click="redirectToCreateCourse"
/>
</div>
<hr>
@ -17,7 +18,13 @@ import Button from 'primevue/button';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from "pinia"
import { useSecurityStore } from "../store/securityStore"
import { useRouter } from "vue-router"
const { t } = useI18n();
const { isTeacher } = storeToRefs(useSecurityStore())
const router = useRouter()
const redirectToCreateCourse = () => {
router.push({ name: 'CourseCreate' })
}
</script>

@ -1,6 +1,7 @@
import { ENTRYPOINT } from "../config/entrypoint"
import axios from "axios"
const API_URL = '/course';
const courseService = {
/**
* @param {number} courseId
@ -71,7 +72,7 @@ const courseService = {
*/
checkLegal: async (courseId, sessionId = 0) => {
const { data } = await axios.get(
`/course/${courseId}/checkLegal.json`,
`${API_URL}/${courseId}/checkLegal.json`,
{
params: {
sid: sessionId,
@ -81,6 +82,41 @@ const courseService = {
return data
},
/**
* Creates a new course with the provided data.
* @param {Object} courseData - The data for the course to be created.
* @returns {Promise<Object>} The server response after creating the course.
*/
createCourse: async (courseData) => {
const response = await axios.post(`${API_URL}/create`, courseData);
console.log('response create ::', response);
return response.data;
},
/**
* Fetches available categories for courses.
* @returns {Promise<Array>} A list of available categories.
*/
getCategories: async () => {
const response = await axios.get(`${API_URL}/categories`);
return response.data;
},
/**
* Searches for templates based on a provided search term.
* @param {string} searchTerm - The search term for the templates.
* @returns {Promise<Array>} A list of templates matching the search term.
*/
searchTemplates: async (searchTerm) => {
const response = await axios.get(`${API_URL}/search_templates`, {
params: { search: searchTerm }
});
return response.data.items.map(item => ({
name: item.name,
value: item.id
}));
},
}
export default courseService

@ -1,53 +1,64 @@
<template>
<div>
<div class="create-course-page m-10">
<div class="message-container mb-4">
<Message severity="info">
{{ t('Once you click on "Create a course", a course is created with a section for Tests, Project based learning, Assessments, Courses, Dropbox, Agenda and much more. Logging in as teacher provides you with editing privileges for this course.') }}
</Message>
</div>
<h1 class="page-title text-xl text-gray-90">{{ t('Add a new course') }}</h1>
<hr />
<CourseForm
ref="createForm"
:errors="violations"
:values="item"
@submit="submitCourse"
/>
<Loading :visible="isLoading" />
<Toolbar
:handle-reset="resetForm"
:handle-submit="onSendForm"
></Toolbar>
</div>
</template>
<script>
import { mapActions } from "vuex"
import { createHelpers } from "vuex-map-fields"
import CourseForm from "../../components/course/Form.vue"
import Loading from "../../components/Loading.vue"
import Toolbar from "../../components/Toolbar.vue"
import CreateMixin from "../../mixins/CreateMixin"
<script setup>
import { ref, computed } from 'vue'
import { useStore } from 'vuex'
import CourseForm from '../../components/course/Form.vue'
import Loading from '../../components/Loading.vue'
import { useRouter } from "vue-router"
import Message from 'primevue/message'
import courseService from "../../services/courseService"
import { useI18n } from "vue-i18n"
const servicePrefix = "Course"
const store = useStore()
const item = ref({})
const router = useRouter()
const { t } = useI18n()
const { mapFields } = createHelpers({
getterType: "course/getField",
mutationType: "course/updateField",
})
const isLoading = computed(() => store.getters['course/getField']('isLoading'))
const violations = computed(() => store.getters['course/getField']('violations'))
const courseData = ref({})
export default {
name: "CourseCreate",
servicePrefix,
mixins: [CreateMixin],
components: {
Loading,
Toolbar,
CourseForm,
},
data() {
return {
item: {},
const submitCourse = async (formData) => {
isLoading.value = true
try {
let tempResponse = await courseService.createCourse(formData)
if (tempResponse.success) {
const courseId = tempResponse.courseId
const sessionId = 0
await router.push(`/course/${courseId}/home?sid=${sessionId}`)
} else {
console.error(tempResponse.message)
}
} catch (error) {
console.error(error)
if (error.response && error.response.data) {
violations.value = error.response.data
} else {
console.error('An unexpected error occurred.')
}
},
computed: {
...mapFields(["error", "isLoading", "created", "violations"]),
},
methods: {
...mapActions("course", ["create", "reset"]),
},
} finally {
isLoading.value = false
}
}
</script>
</script>

@ -100,6 +100,13 @@ $(function() {
break;
}
});
function adjustGridWidth() {
var gridParentWidth = $("#gbox_' . $obj->type . '_fields").parent().width();
$("#' . $obj->type . '_fields").jqGrid("setGridWidth", gridParentWidth, true);
}
$(window).resize(adjustGridWidth);
setTimeout(adjustGridWidth, 500);
});
</script>';
@ -119,9 +126,12 @@ switch ($action) {
if ($form->validate()) {
$values = $form->exportValues();
unset($values['id']);
$values['auto_remove'] = isset($_POST['auto_remove']) ? 1 : 0;
$res = $obj->save($values);
if ($res) {
echo Display::return_message(get_lang('Item added'), 'confirmation');
Display::addFlash(Display::return_message(get_lang('Item added'), 'confirmation'));
header('Location: '.api_get_self().'?type='.$obj->type);
exit;
}
$obj->display();
} else {
@ -141,12 +151,19 @@ switch ($action) {
// The validation or display
if ($form->validate()) {
$values = $form->exportValues();
$values['auto_remove'] = isset($_POST['auto_remove']) ? 1 : 0;
$res = $obj->update($values);
echo Display::return_message(
sprintf(get_lang('Item updated'), $values['variable']),
'confirmation',
false
);
if ($res) {
Display::addFlash(
Display::return_message(
sprintf(get_lang('Item updated'), $values['variable']),
'confirmation',
false
)
);
header('Location: '.api_get_self().'?type='.$obj->type);
exit;
}
$obj->display();
} else {
$actions = '<a href="'.api_get_self().'?type='.$obj->type.'">'.

@ -2777,7 +2777,8 @@ function store_reply(CForum $forum, CForumThread $thread, $values, $courseId = 0
send_notification_mails(
$forum,
$thread,
$values
$values,
$course
);
add_forum_attachment_file('', $post);
@ -3175,9 +3176,14 @@ function get_unaproved_messages($forum_id)
* This function sends the notification mails to everybody who stated that they wanted to be informed when a new post
* was added to a given thread.
*/
function send_notification_mails(CForum $forum, CForumThread $thread, $reply_info)
function send_notification_mails(CForum $forum, CForumThread $thread, $reply_info, ?Course $course = null)
{
$courseEntity = api_get_course_entity();
if (null !== $course) {
$courseEntity = $course;
}
$courseId = $courseEntity->getId();
$sessionId = api_get_session_id();

@ -2401,6 +2401,10 @@ switch ($action) {
'filter',
'field_order',
];
if ($type === 'user') {
$columns[] = 'auto_remove';
}
$sidx = in_array($sidx, $columns) ? $sidx : 'display_text';
$result = $obj->getAllGrid($sidx, $sord, $start, $limit);
$new_result = [];
@ -2426,6 +2430,11 @@ switch ($action) {
$item['visible_to_self'] = $item['visibleToSelf'] ? $checkIcon : $timesIcon;
$item['visible_to_others'] = $item['visibleToOthers'] ? $checkIcon : $timesIcon;
$item['filter'] = $item['filter'] ? $checkIcon : $timesIcon;
if (isset($item['autoRemove'])) {
$item['auto_remove'] = $item['autoRemove'] ? $checkIcon : $timesIcon;
}
$new_result[] = $item;
}
$result = $new_result;

@ -43,17 +43,17 @@ $request = Request::createFromGlobals();
$request->request->set('load_legacy', true);
$currentBaseUrl = $request->getBaseUrl();
$kernel->boot();
$currentUri = $request->getRequestUri();
if (empty($currentBaseUrl)) {
$currentBaseUrl = $request->getSchemeAndHttpHost() . $request->getBasePath();
}
$container = $kernel->getContainer();
$router = $container->get('router');
$context = $router->getContext();
$router->setContext($context);
/** @var FlashBag $flashBag */
$saveFlashBag = null;
$flashBag = $container->get('session')->getFlashBag();
if (!empty($flashBag->keys())) {
$saveFlashBag = $flashBag->all();
}
$saveFlashBag = !empty($flashBag->keys()) ? $flashBag->all() : null;
$response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, false);
$context = Container::getRouter()->getContext();
@ -70,21 +70,25 @@ if ($isCli) {
if ($isCli && $baseUrl) {
$context->setBaseUrl($baseUrl);
} else {
$pos = strpos($currentBaseUrl, 'main');
$posPlugin = strpos($currentBaseUrl, 'plugin');
$posCertificate = strpos($currentBaseUrl, 'certificate');
$fullUrl = $currentBaseUrl . $currentUri;
$posMain = strpos($fullUrl, '/main');
$posPlugin = strpos($fullUrl, '/plugin');
$posCourse = strpos($fullUrl, '/course');
$posCertificate = strpos($fullUrl, '/certificate');
if (false === $pos && false === $posPlugin && false === $posCertificate) {
if (false === $posMain && false === $posPlugin && false === $posCourse && false === $posCertificate) {
echo 'Cannot load current URL';
exit;
}
if (false !== $pos) {
$newBaseUrl = substr($currentBaseUrl, 0, $pos - 1);
}elseif (false !== $posPlugin) {
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1);
if (false !== $posMain) {
$newBaseUrl = substr($fullUrl, 0, $posMain);
} elseif (false !== $posPlugin) {
$newBaseUrl = substr($fullUrl, 0, $posPlugin);
} elseif (false !== $posCourse) {
$newBaseUrl = substr($fullUrl, 0, $posCourse);
} elseif (false !== $posCertificate) {
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1);
$newBaseUrl = substr($fullUrl, 0, $posCertificate);
}
$context->setBaseUrl($newBaseUrl);

@ -4,6 +4,8 @@
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\GradebookCategory;
use Chamilo\CoreBundle\Entity\GradebookLink;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CourseBundle\Entity\CGroupCategory;
use Chamilo\CourseBundle\Entity\CToolIntro;
@ -224,33 +226,57 @@ class AddCourse
}
/**
* Fills the course database with some required content and example content.
* Populates the course with essential settings, group categories, example content, and installs course plugins.
*
* @param bool Whether to fill the course with example content
* @param int $authorId
* This method initializes a new course by setting up various course settings, creating a default group category,
* inserting example content if required, and installing course-specific plugins. The method can also fill the course
* with exemplary content based on the parameter provided or the system setting for example content creation.
*
* @return bool False on error, true otherwise
* @param Course $course The course entity to be populated.
* @param bool|null $fillWithExemplaryContent Determines whether to fill the course with exemplary content. If null,
* the system setting for example material course creation is used.
* @param int $authorId The ID of the user who is considered the author of the exemplary content. Defaults to the
* current user if not specified.
*
* @version 1.2
* @assert (null, '', '', null) === false
* @assert (1, 'ABC', null, null) === false
* @assert (1, 'TEST', 'spanish', true) === true
* @return bool Returns true if the course is successfully populated, false otherwise.
*
* @throws Exception Throws exception on error.
*/
public static function fillCourse(
Course $course,
$fill_with_exemplary_content = null,
$authorId = 0
) {
if (is_null($fill_with_exemplary_content)) {
$fill_with_exemplary_content = 'false' !== api_get_setting('example_material_course_creation');
public static function fillCourse(Course $course, bool $fillWithExemplaryContent = null, int $authorId = 0): bool
{
$entityManager = Database::getManager();
$authorId = $authorId ?: api_get_user_id();
self::insertCourseSettings($course);
self::createGroupCategory($course);
if ($fillWithExemplaryContent ?? api_get_setting('example_material_course_creation') !== 'false') {
self::insertExampleContent($course, $authorId, $entityManager);
}
$course_id = $course->getId();
$authorId = empty($authorId) ? api_get_user_id() : (int) $authorId;
self::installCoursePlugins($course->getId());
return true;
}
/**
* Inserts default and specified settings for a given course.
*
* This method takes a Course object as input and applies a predefined
* set of settings. These settings include configurations for email alerts,
* permissions for users to edit various components like agenda and announcements,
* theme settings, and more. It also handles the case where a course needs to
* enable certain features by default based on platform-wide settings.
*
* @param Course $course The course object to which the settings will be applied.
*
* @return void
* @throws Exception
*/
private static function insertCourseSettings(Course $course): void
{
$TABLESETTING = Database::get_course_table(TABLE_COURSE_SETTING);
$TABLEGRADEBOOK = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
$TABLEGRADEBOOKLINK = Database::get_main_table(TABLE_MAIN_GRADEBOOK_LINK);
$settingsManager = Container::getCourseSettingsManager();
$settingsManager->setCourse($course);
@ -294,13 +320,26 @@ class AddCourse
$title = $setting['title'] ?? '';
Database::query(
"INSERT INTO $TABLESETTING (c_id, title, variable, value, category)
VALUES ($course_id, '".$title."', '".$variable."', '".$setting['default']."', '".$setting['category']."')"
VALUES ({$course->getId()}, '".$title."', '".$variable."', '".$setting['default']."', '".$setting['category']."')"
);
$counter++;
}
}
/* Course homepage tools for platform admin only */
/* Group tool */
/**
* Creates a default group category for the specified course.
*
* This method initializes a new group category with a default title
* and associates it with the given course. The default group category
* is essential for organizing groups within the course, allowing for
* better management and classification of course participants.
*
* @param Course $course The course object for which the default group category is created.
*
* @return void
*/
private static function createGroupCategory(Course $course): void
{
$groupCategory = new CGroupCategory();
$groupCategory
->setTitle(get_lang('Default groups'))
@ -309,278 +348,302 @@ class AddCourse
;
Database::getManager()->persist($groupCategory);
Database::getManager()->flush();
}
/**
* Inserts example content into a given course.
*
* This method populates the specified course with a predefined set of example
* content, including documents, links, announcements, and exercises. This content
* serves as a template or starting point for instructors, showcasing the various
* types of materials and activities that can be included in a course. The content
* is added under the authority of the specified author ID.
*
* @param Course $course The course object into which the example content will be inserted.
* @param int $authorId The ID of the user who will be listed as the author of the inserted content.
*
* @return void
* @throws Exception
*/
private static function insertExampleContent(Course $course, int $authorId): void
{
$now = api_get_utc_datetime();
/*$files = [
['path' => '/shared_folder', 'title' => get_lang('Folders of users'), 'filetype' => 'folder', 'size' => 0],
[
'path' => '/chat_files',
'title' => get_lang('Chat conversations history'),
'filetype' => 'folder',
'size' => 0,
],
['path' => '/certificates', 'title' => get_lang('Certificates'), 'filetype' => 'folder', 'size' => 0],
$files = [
['path' => '/audio', 'title' => get_lang('Audio'), 'filetype' => 'folder', 'size' => 0],
['path' => '/images', 'title' => get_lang('Images'), 'filetype' => 'folder', 'size' => 0],
['path' => '/images/gallery', 'title' => get_lang('Gallery'), 'filetype' => 'folder', 'size' => 0],
['path' => '/video', 'title' => get_lang('Video'), 'filetype' => 'folder', 'size' => 0],
];
$paths = [];
$courseInfo = ['real_id' => $course->getId(), 'code' => $course->getCode()];
$counter = 1;
foreach ($files as $file) {
self::insertDocument($courseInfo, $counter, $file, $authorId);
$doc = self::insertDocument($courseInfo, $counter, $file, $authorId);
$paths[$file['path']] = $doc->getIid();
$counter++;
}*/
$certificateId = 'NULL';
/* Documents */
if ($fill_with_exemplary_content) {
$files = [
['path' => '/audio', 'title' => get_lang('Audio'), 'filetype' => 'folder', 'size' => 0],
//['path' => '/flash', 'title' => get_lang('Flash'), 'filetype' => 'folder', 'size' => 0],
['path' => '/images', 'title' => get_lang('Images'), 'filetype' => 'folder', 'size' => 0],
['path' => '/images/gallery', 'title' => get_lang('Gallery'), 'filetype' => 'folder', 'size' => 0],
['path' => '/video', 'title' => get_lang('Video'), 'filetype' => 'folder', 'size' => 0],
//['path' => '/video/flv', 'title' => 'flv', 'filetype' => 'folder', 'size' => 0],
];
$paths = [];
$courseInfo = ['real_id' => $course->getId()];
foreach ($files as $file) {
$doc = self::insertDocument($courseInfo, $counter, $file, $authorId);
$paths[$file['path']] = $doc->getIid();
$counter++;
}
$finder = new Symfony\Component\Finder\Finder();
$defaultPath = api_get_path(SYS_PUBLIC_PATH).'img/document';
$finder->in($defaultPath);
/** @var SplFileInfo $file */
foreach ($finder as $file) {
$parentName = dirname(str_replace($defaultPath, '', $file->getRealPath()));
if ('/' === $parentName || '/certificates' === $parentName) {
continue;
}
$title = $file->getFilename();
$parentId = $paths[$parentName];
}
if ($file->isDir()) {
$realPath = str_replace($defaultPath, '', $file->getRealPath());
$document = DocumentManager::addDocument(
$courseInfo,
$realPath,
'folder',
null,
$title,
'',
null,
null,
null,
null,
null,
false,
null,
$parentId,
$file->getRealPath()
);
$paths[$realPath] = $document->getIid();
} else {
$realPath = str_replace($defaultPath, '', $file->getRealPath());
$document = DocumentManager::addDocument(
$courseInfo,
$realPath,
'file',
$file->getSize(),
$title,
'',
null,
null,
null,
null,
null,
false,
null,
$parentId,
$file->getRealPath()
);
$finder = new Symfony\Component\Finder\Finder();
$defaultPath = api_get_path(SYS_PUBLIC_PATH).'img/document';
$finder->in($defaultPath);
if ($document && 'default.html' === $document->getTitle()) {
$certificateId = $document->getIid();
}
}
/** @var SplFileInfo $file */
foreach ($finder as $file) {
$parentName = dirname(str_replace($defaultPath, '', $file->getRealPath()));
if ('/' === $parentName || '/certificates' === $parentName) {
continue;
}
$agenda = new Agenda('course');
$agenda->set_course($courseInfo);
$agenda->addEvent(
$now,
$now,
0,
get_lang('Course creation'),
get_lang('This course was created at this time')
);
$title = $file->getFilename();
$parentId = $paths[$parentName];
if ($file->isDir()) {
$realPath = str_replace($defaultPath, '', $file->getRealPath());
$document = DocumentManager::addDocument(
$courseInfo,
$realPath,
'folder',
null,
$title,
'',
null,
null,
null,
null,
null,
false,
null,
$parentId,
$file->getRealPath()
);
$paths[$realPath] = $document->getIid();
} else {
$realPath = str_replace($defaultPath, '', $file->getRealPath());
$document = DocumentManager::addDocument(
$courseInfo,
$realPath,
'file',
$file->getSize(),
$title,
'',
null,
null,
null,
null,
null,
false,
null,
$parentId,
$file->getRealPath()
);
/* Links tool */
$link = new Link();
$link->setCourse($courseInfo);
$links = [
[
'c_id' => $course_id,
'url' => 'http://www.google.com',
'title' => 'Quick and powerful search engine',
'description' => get_lang('Quick and powerful search engine'),
'category_id' => 0,
'on_homepage' => 0,
'target' => '_self',
'session_id' => 0,
],
[
'c_id' => $course_id,
'url' => 'http://www.wikipedia.org',
'title' => 'Free online encyclopedia',
'description' => get_lang('Free online encyclopedia'),
'category_id' => 0,
'on_homepage' => 0,
'target' => '_self',
'session_id' => 0,
],
];
foreach ($links as $params) {
$link->save($params, false, false);
if ($document && 'default.html' === $document->getTitle()) {
$certificateId = $document->getIid();
}
}
}
/* Announcement tool */
AnnouncementManager::add_announcement(
$courseInfo,
0,
get_lang('This is an announcement example'),
get_lang('This is an announcement example. Only trainers are allowed to publish announcements.'),
['everyone' => 'everyone'],
null,
null,
$now
);
$manager = Database::getManager();
$agenda = new Agenda('course');
$agenda->set_course($courseInfo);
$agenda->addEvent(
$now,
$now,
0,
get_lang('Course creation'),
get_lang('This course was created at this time')
);
/* Introduction text */
$intro_text = '<p style="text-align: center;">
<img src="'.api_get_path(REL_CODE_PATH).'img/mascot.png" alt="Mr. Chamilo" title="Mr. Chamilo" />
<h2>'.get_lang('Introduction text').'</h2>
</p>';
/* Links tool */
$link = new Link();
$link->setCourse($courseInfo);
$links = [
[
'c_id' => $course->getId(),
'url' => 'http://www.google.com',
'title' => 'Quick and powerful search engine',
'description' => get_lang('Quick and powerful search engine'),
'category_id' => 0,
'on_homepage' => 0,
'target' => '_self',
'session_id' => 0,
],
[
'c_id' => $course->getId(),
'url' => 'http://www.wikipedia.org',
'title' => 'Free online encyclopedia',
'description' => get_lang('Free online encyclopedia'),
'category_id' => 0,
'on_homepage' => 0,
'target' => '_self',
'session_id' => 0,
],
];
$toolIntro = (new CToolIntro())
->setIntroText($intro_text)
->addCourseLink($course)
;
$manager->persist($toolIntro);
foreach ($links as $params) {
$link->save($params, false, false);
}
$toolIntro = (new CToolIntro())
->setIntroText(get_lang('This page allows users and groups to publish documents.'))
->addCourseLink($course)
;
$manager->persist($toolIntro);
$toolIntro = (new CToolIntro())
->setIntroText(
get_lang(
'The word Wiki is short for WikiWikiWeb. Wikiwiki is a Hawaiian word, meaning "fast" or "speed". In a wiki, people write pages together. If one person writes something wrong, the next person can correct it. The next person can also add something new to the page. Because of this, the pages improve continuously.'
)
)
->addCourseLink($course)
;
$manager->persist($toolIntro);
$manager->flush();
/* Announcement tool */
AnnouncementManager::add_announcement(
$courseInfo,
0,
get_lang('This is an announcement example'),
get_lang('This is an announcement example. Only trainers are allowed to publish announcements.'),
['everyone' => 'everyone'],
null,
null,
$now
);
/* Exercise tool */
$exercise = new Exercise($course_id);
$exercise->exercise = get_lang('Sample test');
$html = '<table width="100%" border="0" cellpadding="0" cellspacing="0">
/* Exercise tool */
$exercise = new Exercise($course->getId());
$exercise->exercise = get_lang('Sample test');
$html = '<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
<td width="220" valign="top" align="left">
<img src="'.api_get_path(WEB_PUBLIC_PATH).'img/document/images/mr_chamilo/doubts.png">
</td>
<td valign="top" align="left">'.get_lang('Irony').'</td></tr>
</table>';
$exercise->type = 1;
$exercise->setRandom(0);
$exercise->active = 1;
$exercise->results_disabled = 0;
$exercise->description = $html;
$exercise->save();
$exercise_id = $exercise->id;
$question = new MultipleAnswer();
$question->course = $courseInfo;
$question->question = get_lang('Socratic irony is...');
$question->description = get_lang('(more than one answer can be true)');
$question->weighting = 10;
$question->position = 1;
$question->course = $courseInfo;
$question->save($exercise);
$questionId = $question->id;
$answer = new Answer($questionId, $courseInfo['real_id']);
$answer->createAnswer(get_lang('Ridiculise one\'s interlocutor in order to have him concede he is wrong.'), 0, get_lang('No. Socratic irony is not a matter of psychology, it concerns argumentation.'), -5, 1);
$answer->createAnswer(get_lang('Admit one\'s own errors to invite one\'s interlocutor to do the same.'), 0, get_lang('No. Socratic irony is not a seduction strategy or a method based on the example.'), -5, 2);
$answer->createAnswer(get_lang('Compell one\'s interlocutor, by a series of questions and sub-questions, to admit he doesn\'t know what he claims to know.'), 1, get_lang('Indeed'), 5, 3);
$answer->createAnswer(get_lang('Use the Principle of Non Contradiction to force one\'s interlocutor into a dead end.'), 1, get_lang('This answer is not false. It is true that the revelation of the interlocutor\'s ignorance means showing the contradictory conclusions where lead his premisses.'), 5, 4);
$answer->save();
// Forums.
$params = [
'forum_category_title' => get_lang('Example Forum Category'),
'forum_category_comment' => '',
];
$forumCategoryId = saveForumCategory($params, $courseInfo, false);
$params = [
'forum_category' => $forumCategoryId,
'forum_title' => get_lang('Example Forum'),
'forum_comment' => '',
'default_view_type_group' => ['default_view_type' => 'flat'],
];
$forumId = store_forum($params, $courseInfo, true);
$repo = Container::getForumRepository();
$forumEntity = $repo->find($forumId);
$params = [
'post_title' => get_lang('Example Thread'),
'forum_id' => $forumId,
'post_text' => get_lang('Example ThreadContent'),
'calification_notebook_title' => '',
'numeric_calification' => '',
'weight_calification' => '',
'forum_category' => $forumCategoryId,
'thread_peer_qualify' => 0,
];
saveThread($forumEntity, $params, $courseInfo, false);
/* Gradebook tool */
$course_code = $courseInfo['code'];
// father gradebook
Database::query(
"INSERT INTO $TABLEGRADEBOOK (title, locked, generate_certificates, description, user_id, c_id, parent_id, weight, visible, certif_min_score, session_id, document_id)
VALUES ('$course_code','0',0,'',1,$course_id,0,100,0,75,NULL,$certificateId)"
);
$gbid = Database::insert_id();
Database::query(
"INSERT INTO $TABLEGRADEBOOK (title, locked, generate_certificates, description, user_id, c_id, parent_id, weight, visible, certif_min_score, session_id, document_id)
VALUES ('$course_code','0',0,'',1,$course_id,$gbid,100,1,75,NULL,$certificateId)"
);
$gbid = Database:: insert_id();
Database::query(
"INSERT INTO $TABLEGRADEBOOKLINK (type, ref_id, user_id, c_id, category_id, created_at, weight, visible, locked)
VALUES (1,$exercise_id,1,$course_id,$gbid,'$now',100,1,0)"
);
}
$exercise->type = 1;
$exercise->setRandom(0);
$exercise->active = 1;
$exercise->results_disabled = 0;
$exercise->description = $html;
$exercise->save();
$question = new MultipleAnswer();
$question->course = $courseInfo;
$question->question = get_lang('Socratic irony is...');
$question->description = get_lang('(more than one answer can be true)');
$question->weighting = 10;
$question->position = 1;
$question->course = $courseInfo;
$question->save($exercise);
$questionId = $question->id;
$answer = new Answer($questionId, $courseInfo['real_id']);
$answer->createAnswer(get_lang('Ridiculise one\'s interlocutor in order to have him concede he is wrong.'), 0, get_lang('No. Socratic irony is not a matter of psychology, it concerns argumentation.'), -5, 1);
$answer->createAnswer(get_lang('Admit one\'s own errors to invite one\'s interlocutor to do the same.'), 0, get_lang('No. Socratic irony is not a seduction strategy or a method based on the example.'), -5, 2);
$answer->createAnswer(get_lang('Compell one\'s interlocutor, by a series of questions and sub-questions, to admit he doesn\'t know what he claims to know.'), 1, get_lang('Indeed'), 5, 3);
$answer->createAnswer(get_lang('Use the Principle of Non Contradiction to force one\'s interlocutor into a dead end.'), 1, get_lang('This answer is not false. It is true that the revelation of the interlocutor\'s ignorance means showing the contradictory conclusions where lead his premisses.'), 5, 4);
$answer->save();
// Forums.
$params = [
'forum_category_title' => get_lang('Example Forum Category'),
'forum_category_comment' => '',
];
// Installing plugins in course
$app_plugin = new AppPlugin();
$app_plugin->install_course_plugins($course_id);
$forumCategoryId = saveForumCategory($params, $courseInfo, false);
return true;
$params = [
'forum_category' => $forumCategoryId,
'forum_title' => get_lang('Example Forum'),
'forum_comment' => '',
'default_view_type_group' => ['default_view_type' => 'flat'],
];
$forumId = store_forum($params, $courseInfo, true);
$repo = Container::getForumRepository();
$forumEntity = $repo->find($forumId);
$params = [
'post_title' => get_lang('Example Thread'),
'forum_id' => $forumId,
'post_text' => get_lang('Example ThreadContent'),
'calification_notebook_title' => '',
'numeric_calification' => '',
'weight_calification' => '',
'forum_category' => $forumCategoryId,
'thread_peer_qualify' => 0,
];
saveThread($forumEntity, $params, $courseInfo, false);
self::createGradebookStructure($course, $exercise->id);
}
/**
* Creates the gradebook structure for a course.
*
* This method sets up the initial gradebook categories and links for a new course.
* It creates a parent gradebook category representing the course itself and a child
* gradebook category for course activities. It then creates a gradebook link associated
* with a specific course activity, identified by the $refId parameter.
*
* @param Course $course The course entity for which the gradebook structure will be created.
* @param int $refId The reference ID of the course activity to link in the gradebook.
*
* @return void
*/
private static function createGradebookStructure(Course $course, int $refId): void
{
$manager = Database::getManager();
/* Gradebook tool */
$courseCode = $course->getCode();
$parentGradebookCategory = new GradebookCategory();
$parentGradebookCategory->setTitle($courseCode);
$parentGradebookCategory->setLocked(0);
$parentGradebookCategory->setGenerateCertificates(false);
$parentGradebookCategory->setDescription('');
$parentGradebookCategory->setCourse($course);
$parentGradebookCategory->setWeight(100);
$parentGradebookCategory->setVisible(false);
$parentGradebookCategory->setCertifMinScore(75);
$parentGradebookCategory->setUser(api_get_user_entity());
$manager->persist($parentGradebookCategory);
$manager->flush();
$childGradebookCategory = new GradebookCategory();
$childGradebookCategory->setTitle($courseCode);
$childGradebookCategory->setLocked(0);
$childGradebookCategory->setGenerateCertificates(false);
$childGradebookCategory->setDescription('');
$childGradebookCategory->setCourse($course);
$childGradebookCategory->setWeight(100);
$childGradebookCategory->setVisible(true);
$childGradebookCategory->setCertifMinScore(75);
$childGradebookCategory->setParent($parentGradebookCategory);
$childGradebookCategory->setUser(api_get_user_entity());
$manager->persist($childGradebookCategory);
$manager->flush();
$gradebookLink = new GradebookLink();
$gradebookLink->setType(1);
$gradebookLink->setRefId($refId);
$gradebookLink->setUser(api_get_user_entity());
$gradebookLink->setCourse($course);
$gradebookLink->setCategory($childGradebookCategory);
$gradebookLink->setCreatedAt(new \DateTime());
$gradebookLink->setWeight(100);
$gradebookLink->setVisible(1);
$gradebookLink->setLocked(0);
$manager->persist($gradebookLink);
$manager->flush();
}
/**
* Installs plugins for a given course.
*
* This method takes a course ID and uses the AppPlugin service to install
* all necessary or default plugins for that specific course. These plugins
* can enhance the functionality of the course by adding new features or
* tools that are not part of the core Chamilo platform.
*
* @param int $courseId The ID of the course for which the plugins will be installed.
*
* @return void
*/
private static function installCoursePlugins(int $courseId): void
{
$app_plugin = new AppPlugin();
$app_plugin->install_course_plugins($courseId);
}
/**

@ -50,28 +50,6 @@ class CourseManager
$accessUrlId = !empty($accessUrlId) ? (int) $accessUrlId : api_get_current_access_url_id();
$authorId = empty($authorId) ? api_get_user_id() : (int) $authorId;
// @todo Check that this was move inside the CourseListener in a pre persist throw an Exception
/*if (isset($_configuration[$accessUrlId]) && is_array($_configuration[$accessUrlId])) {
$return = self::checkCreateCourseAccessUrlParam(
$_configuration,
$accessUrlId,
'hosting_limit_courses',
'PortalCoursesLimitReached'
);
if (false != $return) {
return $return;
}
$return = self::checkCreateCourseAccessUrlParam(
$_configuration,
$accessUrlId,
'hosting_limit_active_courses',
'PortalActiveCoursesLimitReached'
);
if (false != $return) {
return $return;
}
}*/
if (empty($params['title'])) {
return false;
}
@ -97,7 +75,7 @@ class CourseManager
$params['directory'] = $keys['currentCourseRepository'];
$courseInfo = api_get_course_info($params['code']);
if (empty($courseInfo)) {
$course = AddCourse::register_course($params, $accessUrlId);
$course = AddCourse::register_course($params);
if (null !== $course) {
self::fillCourse($course, $params, $authorId);
@ -2373,14 +2351,6 @@ class CourseManager
GroupManager::delete_category($category['iid'], $course->getCode());
}
}
// Cleaning groups
// @todo should be cleaned by the resource.
/*$groups = GroupManager::get_groups($courseId);
if (!empty($groups)) {
foreach ($groups as $group) {
GroupManager::deleteGroup($group, $course['code']);
}
}*/
$course_tables = AddCourse::get_course_tables();
// Cleaning c_x tables
@ -2391,11 +2361,12 @@ class CourseManager
continue;
}
$table = Database::get_course_table($table);
//$sql = "DELETE FROM $table WHERE c_id = $courseId ";
//Database::query($sql);
}
}
$resourceLink = $course->getFirstResourceLink();
$resourceLinkId = $resourceLink?->getId();
// Unsubscribe all users from the course
$sql = "DELETE FROM $table_course_user WHERE c_id = $courseId";
Database::query($sql);
@ -2426,8 +2397,10 @@ class CourseManager
// but give information on the course history
//$sql = "DELETE FROM $table_stats_default WHERE c_id = $courseId";
//Database::query($sql);
$sql = "DELETE FROM $table_stats_downloads WHERE c_id = $courseId";
Database::query($sql);
if ($resourceLinkId) {
$sql = "DELETE FROM $table_stats_downloads WHERE resource_link_id = $resourceLinkId";
Database::query($sql);
}
$sql = "DELETE FROM $table_stats_links WHERE c_id = $courseId";
Database::query($sql);
$sql = "DELETE FROM $table_stats_uploads WHERE c_id = $courseId";

@ -1369,6 +1369,12 @@ class Event
$table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$table_track_attempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
$tblTrackAttemptQualify = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_QUALIFY);
if (!isset($lp_id)) {
$lp_id = '0';
}
if (!isset($lp_item_id)) {
$lp_item_id = '0';
}
if (!in_array(strtolower($order), ['asc', 'desc'])) {
$order = 'asc';

@ -54,6 +54,7 @@ class ExtraField extends Model
//Enable this when field_loggeable is introduced as a table field (2.0)
//'field_loggeable',
'created_at',
'auto_remove',
];
public $ops = [
@ -595,10 +596,13 @@ class ExtraField extends Model
case 'filter':
$sidx = 'e.filter';
break;
case 'auto_remove':
$sidx = 'e.autoRemove';
break;
}
$em = Database::getManager();
$query = $em->getRepository(EntityExtraField::class)->createQueryBuilder('e');
$query->select('e')
$query->select('e.id', 'e.itemType', 'e.valueType', 'e.variable', 'e.displayText', 'e.autoRemove', 'e.changeable', 'e.visibleToOthers', 'e.filter', 'e.visibleToSelf')
->where('e.itemType = :type')
->setParameter('type', $this->getItemType())
->orderBy($sidx, $sord)
@ -2197,7 +2201,7 @@ class ExtraField extends Model
*/
public function getJqgridColumnNames()
{
return [
$columns = [
get_lang('Name'),
get_lang('Field label'),
get_lang('Type'),
@ -2208,6 +2212,12 @@ class ExtraField extends Model
get_lang('Order'),
get_lang('Detail'),
];
if ($this->type === 'user') {
array_splice($columns, -1, 0, get_lang('Auto remove'));
}
return $columns;
}
/**
@ -2215,7 +2225,7 @@ class ExtraField extends Model
*/
public function getJqgridColumnModel()
{
return [
$columnModel = [
[
'name' => 'display_text',
'index' => 'display_text',
@ -2240,46 +2250,60 @@ class ExtraField extends Model
'name' => 'changeable',
'index' => 'changeable',
'width' => '35',
'align' => 'left',
'align' => 'center',
'sortable' => 'true',
],
[
'name' => 'visible_to_self',
'index' => 'visible_to_self',
'width' => '45',
'align' => 'left',
'align' => 'center',
'sortable' => 'true',
],
[
'name' => 'visible_to_others',
'index' => 'visible_to_others',
'width' => '35',
'align' => 'left',
'align' => 'center',
'sortable' => 'true',
],
[
'name' => 'filter',
'index' => 'filter',
'width' => '30',
'align' => 'left',
'align' => 'center',
'sortable' => 'true',
],
[
'name' => 'field_order',
'index' => 'field_order',
'width' => '25',
'align' => 'left',
'align' => 'center',
'sortable' => 'true',
],
[
'name' => 'actions',
'index' => 'actions',
'width' => '40',
'align' => 'left',
'align' => 'center',
'formatter' => 'action_formatter',
'sortable' => 'false',
],
];
if ($this->type === 'user') {
$autoRemoveColumnModel = [
'name' => 'auto_remove',
'index' => 'auto_remove',
'width' => '25',
'align' => 'center',
'sortable' => 'true',
];
array_splice($columnModel, -1, 0, [$autoRemoveColumnModel]);
}
return $columnModel;
}
/**
@ -2412,6 +2436,15 @@ class ExtraField extends Model
$form->addElement('text', 'field_order', get_lang('Order'));
if ($this->type == 'user') {
$form->addElement(
'checkbox',
'auto_remove',
get_lang('Remove on anonymisation'),
get_lang('Remove this value when anonymising a user, because it could otherwise help identify the user despite the anonymisation.')
);
}
if ('edit' == $action) {
$option = new ExtraFieldOption($this->type);
$defaults['field_options'] = $option->get_field_options_by_field_to_string($id);
@ -2421,6 +2454,7 @@ class ExtraField extends Model
$defaults['visible_to_others'] = 0;
$defaults['changeable'] = 0;
$defaults['filter'] = 0;
$defaults['auto_remove'] = 0;
$form->addButtonCreate(get_lang('Add'));
}

@ -1,6 +1,7 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Tool;
use Chamilo\CourseBundle\Entity\CTool;
/**
@ -1043,18 +1044,14 @@ class Plugin
* Add an link for a course tool.
*
* @param string $name The tool name
* @param int $courseId The course ID
* @param string $iconName Optional. Icon file name
* @param string $link Optional. Link URL
* @param int $courseId The course ID
* @param string|null $iconName Optional. Icon file name
* @param string|null $link Optional. Link URL
*
* @return CTool|null
*/
protected function createLinkToCourseTool(
$name,
$courseId,
$iconName = null,
$link = null
) {
protected function createLinkToCourseTool(string $name, int $courseId, string $iconName = null, string $link = null): ?CTool
{
if (!$this->addCourseTool) {
return null;
}
@ -1065,31 +1062,23 @@ class Plugin
$em = Database::getManager();
/** @var CTool $tool */
$tool = $em
->getRepository(CTool::class)
->findOneBy([
'title' => $name,
'course' => $courseId,
'category' => 'plugin',
]);
$tool = $em->getRepository(CTool::class)->findOneBy([
'title' => $name,
'course' => api_get_course_entity($courseId),
]);
if (!$tool) {
$cToolId = AddCourse::generateToolId($courseId);
$pluginName = $this->get_name();
$toolEntity = new Tool();
$toolEntity->setTitle($pluginName);
$tool = new CTool();
$tool
->setCourse(api_get_course_entity($courseId))
$tool->setCourse(api_get_course_entity($courseId))
->setTitle($name.$visibilityPerStatus)
->setLink($link ?: "$pluginName/start.php")
->setImage($iconName ?: "$pluginName.png")
->setVisibility($visibility)
->setAdmin(0)
->setAddress('squaregrey.gif')
->setAddedTool(false)
->setTarget('_self')
->setCategory('plugin')
->setSessionId(0);
->setTool($toolEntity)
->setPosition(0);
$em->persist($tool);
$em->flush();

@ -163,7 +163,7 @@ class Statistics
} else {
$sql = "SELECT COUNT(DISTINCT(id)) AS number
FROM $user_table
WHERE 1 = 1 AND active <> ".USER_SOFT_DELETED." AND $status_filter $active_filter";
WHERE 1 = 1 AND active <> ".USER_SOFT_DELETED." $status_filter $active_filter";
if (isset($categoryCode)) {
$categoryCode = Database::escape_string($categoryCode);
$status_filter = isset($status) ? ' AND status = '.intval($status) : '';

@ -15,6 +15,7 @@ use Chamilo\CoreBundle\Repository\GroupRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
use ChamiloSession as Session;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Chamilo\CoreBundle\Entity\ExtraFieldValues as EntityExtraFieldValues;
/**
* This library provides functions for user management.
@ -5664,6 +5665,25 @@ SQL;
error_log("Could not anonymize IP address for user $userId ($sql)");
}
}
$extraFieldRepository = $em->getRepository(EntityExtraField::class);
$autoRemoveFields = $extraFieldRepository->findBy([
'autoRemove' => 1,
'itemType' => EntityExtraField::USER_FIELD_TYPE
]);
foreach ($autoRemoveFields as $field) {
$extraFieldValueRepository = $em->getRepository(EntityExtraFieldValues::class);
$extraFieldValue = $extraFieldValueRepository->findOneBy([
'field' => $field,
'itemId' => $userId
]);
if ($extraFieldValue) {
$em->remove($extraFieldValue);
}
}
$em->persist($user);
$em->flush();
Event::addEvent(LOG_USER_ANONYMIZE, LOG_USER_ID, $userId);

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\CourseCategory;
use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\Session;
@ -15,12 +16,15 @@ use Chamilo\CoreBundle\Entity\Tag;
use Chamilo\CoreBundle\Entity\Tool;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Repository\LegalRepository;
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Chamilo\CoreBundle\Repository\TagRepository;
use Chamilo\CoreBundle\Security\Authorization\Voter\CourseVoter;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Chamilo\CoreBundle\Tool\ToolChain;
use Chamilo\CourseBundle\Controller\ToolBaseController;
@ -48,6 +52,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Contracts\Translation\TranslatorInterface;
use UserManager;
/**
@ -669,6 +674,106 @@ class CourseController extends ToolBaseController
]);
}
#[Route('/categories', name: 'chamilo_core_course_form_lists')]
public function getCategories(
SettingsManager $settingsManager,
AccessUrlHelper $accessUrlHelper,
CourseCategoryRepository $courseCategoriesRepo
): JsonResponse {
$allowBaseCourseCategory = 'true' === $settingsManager->getSetting('course.allow_base_course_category');
$accessUrlId = $accessUrlHelper->getCurrent()->getId();
$categories = $courseCategoriesRepo->findAllInAccessUrl(
$accessUrlId,
$allowBaseCourseCategory
);
$data = [];
$categoryToAvoid = '';
if (!$this->isGranted('ROLE_ADMIN')) {
$categoryToAvoid = $settingsManager->getSetting('course.course_category_code_to_use_as_model');
}
foreach ($categories as $category) {
$categoryCode = $category->getCode();
if (!empty($categoryToAvoid) && $categoryToAvoid == $categoryCode) {
continue;
}
$data[] = ['id' => $category->getId(), 'name' => $category->__toString()];
}
return new JsonResponse($data);
}
#[Route('/search_templates', name: 'chamilo_core_course_search_templates')]
public function searchCourseTemplates(
Request $request,
AccessUrlHelper $accessUrlHelper,
CourseRepository $courseRepository
): JsonResponse {
$searchTerm = $request->query->get('search', '');
$accessUrl = $accessUrlHelper->getCurrent();
/** @var User|null $user */
$user = $this->getUser();
$courseList = $courseRepository->getCoursesInfoByUser($user, $accessUrl, 1, $searchTerm);
$results = ['items' => []];
foreach ($courseList as $course) {
$title = $course['title'];
$results['items'][] = [
'id' => $course['code'],
'name' => $title.' ('.$course['code'].') ',
];
}
return new JsonResponse($results);
}
#[Route('/create', name: 'chamilo_core_course_create')]
public function createCourse(Request $request, TranslatorInterface $translator): JsonResponse
{
$courseData = json_decode($request->getContent(), true);
$wantedCode = $courseData['code'] ?? null;
$categoryCode = $courseData['category'] ?? null;
$title = $courseData['name'];
$courseLanguage = isset($courseData['language']) ? $courseData['language']['id'] : '';
$exemplaryContent = $courseData['fillDemoContent'] ?? false;
$template = $courseData['template'] ?? '';
if (empty($wantedCode)) {
$wantedCode = CourseManager::generate_course_code(substr($title, 0, CourseManager::MAX_COURSE_LENGTH_CODE));
}
$courseCodeOk = !CourseManager::course_code_exists($wantedCode);
if ($courseCodeOk) {
$params = [
'title' => $title,
'exemplary_content' => $exemplaryContent,
'wanted_code' => $wantedCode,
'course_language' => $courseLanguage,
'course_template' => $template,
];
if ($categoryCode) {
$params['course_categories'] = $categoryCode;
}
$course = CourseManager::create_course($params);
if ($course) {
return new JsonResponse([
'success' => true,
'message' => $translator->trans('Course created successfully.'),
'courseId' => $course->getId(),
]);
}
}
return new JsonResponse(['success' => false, 'message' => $translator->trans('An error occurred while creating the course.')]);
}
private function autoLaunch(): void
{
$autoLaunchWarning = '';

@ -125,6 +125,11 @@ class ExtraField
#[Gedmo\Timestampable(on: 'create')]
#[ORM\Column(name: 'created_at', type: 'datetime')]
protected DateTime $createdAt;
#[Groups(['extra_field:read'])]
#[ORM\Column(name: "auto_remove", type: "boolean", options: ["default" => false])]
protected bool $autoRemove = false;
public function __construct()
{
$this->options = new ArrayCollection();
@ -134,6 +139,7 @@ class ExtraField
$this->visibleToSelf = false;
$this->changeable = false;
$this->filter = false;
$this->autoRemove = false;
}
public function getId(): ?int
@ -318,4 +324,16 @@ class ExtraField
{
return $this->locale;
}
public function getAutoRemove(): bool
{
return $this->autoRemove;
}
public function setAutoRemove(bool $autoRemove): self
{
$this->autoRemove = $autoRemove;
return $this;
}
}

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
class Version20240308225800 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Adds the auto_remove field to the extra_field table to manage automatic deletion during anonymization, if it does not exist.';
}
public function up(Schema $schema): void
{
$table = $schema->getTable('extra_field');
if (!$table->hasColumn('auto_remove')) {
$this->addSql('ALTER TABLE extra_field ADD auto_remove TINYINT(1) NOT NULL DEFAULT 0');
}
}
public function down(Schema $schema): void
{
$table = $schema->getTable('extra_field');
if ($table->hasColumn('auto_remove')) {
$this->addSql('ALTER TABLE extra_field DROP COLUMN auto_remove');
}
}
}

@ -32,7 +32,7 @@ class CourseCategoryRepository extends ServiceEntityRepository
*
* @return CourseCategory[]
*/
public function findAllInAccessUrl(int $accessUrl, bool $allowBaseCategories = false, int $parentId = 0)
public function findAllInAccessUrl(int $accessUrl, bool $allowBaseCategories = false, int $parentId = 0): array
{
$qb = $this->createQueryBuilder('c');
$qb

@ -93,8 +93,7 @@ class CourseRepository extends ResourceRepository
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('DISTINCT c.id')
$qb->select('DISTINCT c.id, c.title, c.code')
->from(Course::class, 'c')
->innerJoin(CourseRelUser::class, 'courseRelUser')
->innerJoin('c.urls', 'accessUrlRelCourse')
@ -105,18 +104,19 @@ class CourseRepository extends ResourceRepository
'user' => $user,
'url' => $url,
'status' => $status,
])
;
]);
if (!empty($keyword)) {
$qb
->andWhere('c.title like = :keyword OR c.code like = :keyword')
->setParameter('keyword', $keyword)
;
$qb->andWhere($qb->expr()->orX(
$qb->expr()->like('c.title', ':keyword'),
$qb->expr()->like('c.code', ':keyword')
))
->setParameter('keyword', '%'.$keyword.'%');
}
$query = $qb->getQuery();
return $query->getResult();
return $query->getArrayResult();
}
/**

@ -0,0 +1,487 @@
<?php
/* For licensing terms, see /license.txt */
/*
User account synchronisation from LDAP
This script
creates new user accounts found in the LDAP directory (if multiURL is enable, it creates the user on the URL for which the LDAP has been configured)
disables user accounts not found in the LDAP directory (it disbales the user for all URLs)
or delete the user depending on the variable deleteUsersNotFoundInLDAP (only if the user has auth_source === extldap)
updates existing user accounts found in the LDAP directory, re-enabling them if disabled (it applies for all URLs) only if option reenableUsersFoundInLDAP is set to true.
anonymizes user accounts disabled for more than 3 years (applies for all URLs) only if the variable is set to true (by default).
This script can be run unattended.
It does not read any parameter from the command line, but uses the global configuration arrays
$extldap_config
and
$extldap_user_correspondance
defined in app/config/auth.conf.php or overriden in app/config/configuration.php in MultiURL case.
username field is used to identify and match LDAP and Chamilo accounts together.
($extldap_user_correspondance['username'])
*/
exit;
// Change this to the absolute path to chamilo root folder if you move the script out of tests/scripts
$chamiloRoot = __DIR__.'/../..';
// Set to true in order to get a trace of changes made by this script
$debug = false;
// Set to test mode by default to only show the output, put this test variable to 0 to enable creation, modificaction and deletion of users
$test = 1;
// It defines if the user not find in the LDAP but present in Chamilo should be deleted or disabled. By default it will be disabled.
// Set it to true for users to be deleted.
$deleteUsersNotFoundInLDAP = false;
// Re-enable users found in LDAP and that where present but inactivated in Chamilo
$reenableUsersFoundInLDAP = false;
// Anonymize user accounts disabled for more than 3 years
$anonymizeUserAccountsDisbaledFor3Years = false;
// List of username of accounts that should not be disabled or deleted if not present in LDAP
// For exemple the first admin and the anonymous user that has no username ('')
//$usernameListNotToTouchEvenIfNotInLDAP = ['admin','','test'];
// List of LDAP attributes that are not in extldap_user_correspondance but are needed in this script
//$extraLdapAttributes[0][] = 'description';
//$extraLdapAttributes[0][] = 'userAccountControl';
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\TrackEDefault;
use Chamilo\UserBundle\Entity\User;
use Doctrine\DBAL\FetchMode;
use Doctrine\ORM\OptimisticLockException;
if (php_sapi_name() !== 'cli') {
die("this script is supposed to be run from the command-line\n");
}
require $chamiloRoot.'/cli-config.php';
require_once $chamiloRoot.'/main/inc/lib/api.lib.php';
require_once $chamiloRoot.'/app/config/auth.conf.php';
require_once $chamiloRoot.'/main/inc/lib/database.constants.inc.php';
require_once $chamiloRoot.'/main/auth/external_login/ldap.inc.php';
ini_set('memory_limit', -1);
// Retreive information from $extldap_user_correspondance and extra fields
// into $tableFields, $extraFields, $allFields and $ldapAttributes
$generalTableFieldMap = $extldap_user_correspondance;
$multipleUrlLDAPConfig = false;
$allLdapUsers = [];
const EXTRA_ARRAY_KEY = 'extra';
// read all users from the internal database
$userRepository = Database::getManager()->getRepository('ChamiloUserBundle:User');
$dbUsers = [];
foreach ($userRepository->findAll() as $user) {
if ($user->getId() > 1) {
$username = strtolower($user->getUsername());
array_key_exists($username, $dbUsers) and die("duplicate username $username found in the database\n");
$dbUsers[$username] = $user;
}
}
if ($debug) {
echo count($dbUsers) . " users with id > 1 found in internal database\n";
}
if (api_is_multiple_url_enabled()) {
$accessUrls = api_get_access_urls(0,100000,'id');
$multipleUrlLDAPConfig = true;
if (!empty($extldap_config) && array_key_exists('host', $extldap_config) && !empty($extldap_config['host'])) {
$multipleUrlLDAPConfig = false;
}
}
if (!$multipleUrlLDAPConfig) {
$accessUrls[0]['id'] = 0;
$generalTableFieldMap[0] = $generalTableFieldMap;
}
if ($debug) {
echo "accessUrls = " . print_r($accessUrls,1);
}
foreach ($accessUrls as $accessUrl) {
$tableFields = [];
$extraFields = [];
$extraFieldMap = [];
$accessUrlId = $accessUrl['id'];
global $_configuration;
$_configuration['access_url'] = $accessUrlId;
$extldap_config[$accessUrlId] = api_get_configuration_value('extldap_config');
$generalTableFieldMap[$accessUrlId] = $extldap_user_correspondance[$accessUrlId] = api_get_configuration_value('extldap_user_correspondance');
$ldapAttributes = $extraLdapAttributes[$accessUrlId];
if (array_key_exists($accessUrlId, $generalTableFieldMap) && is_array($generalTableFieldMap[$accessUrlId])) {
$tableFieldMap = $generalTableFieldMap[$accessUrlId];
if (array_key_exists(EXTRA_ARRAY_KEY, $tableFieldMap) and is_array($tableFieldMap[EXTRA_ARRAY_KEY])) {
$extraFieldMap = $tableFieldMap[EXTRA_ARRAY_KEY];
unset($tableFieldMap[EXTRA_ARRAY_KEY]);
}
$extraFieldRepository = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraField');
$extraFieldValueRepository = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraFieldValues');
foreach ([false => $tableFieldMap, true => $extraFieldMap] as $areExtra => $fields) {
foreach ($fields as $name => $value) {
$userField = (object)[
'name' => $name,
'constant' => '!' === $value[0] ? substr($value, 1) : null,
'function' => 'func' === $value,
'ldapAttribute' => ('!' !== $value[0] and 'func' !== $value) ? $value : null,
];
if (!$userField->constant and !$userField->function) {
$ldapAttributes[] = $value;
}
if ($areExtra) {
$userField->extraField = $extraFieldRepository->findOneBy(
[
'extraFieldType' => ExtraField::USER_FIELD_TYPE,
'variable' => $name,
]
) or die("Cannot find user extra field '$name'\n");
foreach ($extraFieldValueRepository->findBy(['field' => $userField->extraField]) as $extraFieldValue) {
$userField->extraFieldValues[$extraFieldValue->getItemId()] = $extraFieldValue;
}
$extraFields[] = $userField;
} elseif ($name !== 'admin') {
try {
$userField->getter = new ReflectionMethod(
'\Chamilo\UserBundle\Entity\User',
'get' . str_replace('_', '', ucfirst($name))
);
$userField->setter = new ReflectionMethod(
'\Chamilo\UserBundle\Entity\User',
'set' . str_replace('_', '', ucfirst($name))
);
} catch (ReflectionException $exception) {
die($exception->getMessage() . "\n");
}
$tableFields[] = $userField;
}
}
}
$allFields = array_merge($tableFields, $extraFields);
}
// Retrieve source information from LDAP
if ($debug) {
echo ' Entering ldap search ' . "\n";
echo ' extldap_config = ' . print_r($extldap_config,1) . "\n";
}
if (!$multipleUrlLDAPConfig) {
$extldap_config[$accessUrlId] = $extldap_config;
}
$ldap = false;
if (array_key_exists($accessUrlId, $extldap_config) && is_array($extldap_config[$accessUrlId])) {
foreach ($extldap_config[$accessUrlId]['host'] as $ldapHost) {
$ldap = array_key_exists('port', $extldap_config)
? ldap_connect($ldapHost, $extldap_config['port'])
: ldap_connect($ldapHost);
if (false !== $ldap) {
break;
}
}
if (false === $ldap) {
die("ldap_connect() failed\n");
}
if ($debug) {
echo "Connected to LDAP server $ldapHost.\n";
}
ldap_set_option(
$ldap,
LDAP_OPT_PROTOCOL_VERSION,
array_key_exists('protocol_version', $extldap_config[$accessUrlId]) ? $extldap_config[$accessUrlId]['protocol_version'] : 2
);
ldap_set_option(
$ldap,
LDAP_OPT_REFERRALS,
array_key_exists('referrals', $extldap_config[$accessUrlId]) ? $extldap_config[$accessUrlId]['referrals'] : false
);
ldap_bind($ldap, $extldap_config[$accessUrlId]['admin_dn'], $extldap_config[$accessUrlId]['admin_password'])
or die('ldap_bind() failed: ' . ldap_error($ldap) . "\n");
if ($debug) {
$adminDn = $extldap_config[$accessUrlId]['admin_dn'];
echo "Bound to LDAP server as $adminDn .\n";
}
$baseDn = $extldap_config[$accessUrlId]['base_dn']
or die("cannot read the LDAP directory base DN where to search for user entries\n");
if (!$multipleUrlLDAPConfig) {
$extldap_user_correspondance[$accessUrlId] = $extldap_user_correspondance;
}
$ldapUsernameAttribute = $extldap_user_correspondance[$accessUrlId]['username']
or die("cannot read the name of the LDAP attribute where to find the username\n");
$filter = "$ldapUsernameAttribute=*";
if (array_key_exists('filter', $extldap_config[$accessUrlId])) {
$filter = '(&('.$filter.')('.$extldap_config[$accessUrlId]['filter'].'))';
}
$searchResult = ldap_search($ldap, $baseDn, $filter, $ldapAttributes)
or die("ldap_search(\$ldap, '$baseDn', '$filter', [".join(',', $ldapAttributes).']) failed: '.ldap_error($ldap)."\n");
if ($debug) {
echo ldap_count_entries($ldap, $searchResult) . " LDAP entries found\n";
}
$ldapUsers = [];
$entry = ldap_first_entry($ldap, $searchResult);
while (false !== $entry) {
$attributes = ldap_get_attributes($ldap, $entry);
$ldapUser = [];
foreach ($allFields as $userField) {
if (!is_null($userField->constant)) {
$value = $userField->constant;
} elseif ($userField->function) {
$func = "extldap_get_$userField->name";
if (function_exists($func)) {
$value = extldap_purify_string($func($attributes));
} else {
die("'func' not implemented for $userField->name\n");
}
} else {
if (array_key_exists($userField->ldapAttribute, $attributes)) {
$values = ldap_get_values($ldap, $entry, $userField->ldapAttribute)
or die(
'could not read value of attribute ' . $userField->ldapAttribute
. ' of entry ' . ldap_get_dn($ldap, $entry)
. "\n"
);
(1 === $values['count'])
or die(
$values['count'] . ' values found (expected only one)'
. ' in attribute ' . $userField->ldapAttribute
. ' of entry ' . ldap_get_dn($ldap, $entry)
. "\n"
);
$value = $values[0];
} else {
$value = '';
}
}
$ldapUser[$userField->name] = $value;
}
$username = strtolower($ldapUser['username']);
array_key_exists($username, $ldapUsers) and die("duplicate username '$username' found in LDAP\n");
$ldapUsers[$username] = $ldapUser;
if ($debug) {
echo 'Adding user ' . $username . ' to ldapUsersArray ' . "\n";
echo "ldapUser = " . print_r($ldapUser,1) . "\n";
}
$entry = ldap_next_entry($ldap, $entry);
}
ldap_close($ldap);
if ($debug) {
echo "ldapUsers = " . print_r($ldapUsers,1) . "\n";
}
// create new user accounts found in the LDAP directory and update the existing ones, re-enabling if necessary
foreach ($ldapUsers as $username => $ldapUser) {
if (array_key_exists($username, $dbUsers)) {
$user = $dbUsers[$username];
if ($debug) {
echo "User in DB = " . $username . " and user id = " . $user->getId() . "\n";
}
} else {
if (!$test) {
$user = new User();
$dbUsers[$username] = $user;
$user->setUsernameCanonical($username);
}
if ($debug) {
echo 'Created ' . $username . "\n";
echo "ldapUser = " . print_r($ldapUser,1) . "\n";
}
}
if ($test) {
if ($debug) {
echo 'Updated ' . $username . ' fields '."\n";
}
} else {
foreach ($tableFields as $userField) {
$value = $ldapUser[$userField->name];
if ($userField->getter->invoke($user) !== $value) {
$userField->setter->invoke($user, $value);
if ($debug) {
echo 'Updated ' . $username . ' field '.$userField->name."\n";
}
if ($userField->name == 'email') {
$user->setEmailCanonical($value);
}
}
}
if (!$user->isActive() and $reenableUsersFoundInLDAP) {
$user->setActive(true);
}
Database::getManager()->persist($user);
try {
Database::getManager()->flush();
} catch (OptimisticLockException $exception) {
die($exception->getMessage()."\n");
}
if($debug) {
echo 'Sent to DB ' . $username . " with user id = " . $user->getId() . "\n";
}
if ($multipleUrlLDAPConfig) {
UrlManager::add_user_to_url($user->getId(), $accessUrlId);
} elseif (!api_is_multiple_url_enabled()) {
//we are adding by default the access_url_user table with access_url_id = 1
UrlManager::add_user_to_url($user->getId(), 1);
}
}
}
// also update extra field values
if ($test) {
if ($debug) {
echo 'Updated ' . $username . ' extra fields ' . "\n";
}
} else {
foreach ($ldapUsers as $username => $ldapUser) {
$user = $dbUsers[$username];
foreach ($extraFields as $userField) {
$value = $ldapUser[$userField->name];
if (array_key_exists($user->getId(), $userField->extraFieldValues)) {
/**
* @var ExtraFieldValues $extraFieldValue
*/
$extraFieldValue = $userField->extraFieldValues[$user->getId()];
if ($extraFieldValue->getValue() !== $value) {
$extraFieldValue->setValue($value);
Database::getManager()->persist($extraFieldValue);
if ($debug) {
echo 'Updated ' . $username . ' extra field ' . $userField->name . "\n";
}
}
} else {
$extraFieldValue = new ExtraFieldValues();
$extraFieldValue->setValue($value);
$extraFieldValue->setField($userField->extraField);
$extraFieldValue->setItemId($user->getId());
Database::getManager()->persist($extraFieldValue);
$userField->extraFieldValues[$user->getId()] = $extraFieldValue;
if ($debug) {
echo 'Created ' . $username . ' extra field ' . $userField->name . "\n";
}
}
}
}
try {
Database::getManager()->flush();
} catch (OptimisticLockException $exception) {
die($exception->getMessage()."\n");
}
}
$allLdapUsers = array_merge($allLdapUsers, $ldapUsers);
}
}
// disable or delete user accounts not found in the LDAP directories depending on $deleteUsersNotFoundInLDAP
$now = new DateTime();
foreach (array_diff(array_keys($dbUsers), array_keys($allLdapUsers)) as $usernameToDisable) {
if (in_array($usernameToDisable, $usernameListNotToTouchEvenIfNotInLDAP)) {
if ($debug) {
echo 'User not modified even if not present in LDAP : ' . $usernameToDisable . "\n";
}
} else {
$user = $dbUsers[$usernameToDisable];
if ($deleteUsersNotFoundInLDAP) {
if (!$test) {
if (!UserManager::delete_user($user->getId())) {
if ($debug) {
echo 'Unable to delete user ' . $usernameToDisable . "\n";
}
}
}
if ($debug) {
echo 'Deleted user ' . $usernameToDisable . "\n";
}
} else {
if (!$test) {
if ($user->isActive()) {
// In order to avoid slow individual SQL updates, we do not call
// UserManager::disable($user->getId());
$user->setActive(false);
Database::getManager()->persist($user);
// In order to avoid slow individual SQL updates, we do not call
// Event::addEvent(LOG_USER_DISABLE, LOG_USER_ID, $user->getId());
$trackEDefault = new TrackEDefault();
$trackEDefault->setDefaultUserId(1);
$trackEDefault->setDefaultDate($now);
$trackEDefault->setDefaultEventType(LOG_USER_DISABLE);
$trackEDefault->setDefaultValueType(LOG_USER_ID);
$trackEDefault->setDefaultValue($user->getId());
Database::getManager()->persist($trackEDefault);
}
}
if ($debug) {
echo 'Disabled ' . $user->getUsername() . "\n";
}
}
}
}
if (!$test) {
try {
// Saving everything together
Database::getManager()->flush();
} catch (OptimisticLockException $exception) {
die($exception->getMessage()."\n");
}
}
// anonymize user accounts disabled for more than 3 years
if ($anonymizeUserAccountsDisbaledFor3Years) {
$longDisabledUserIds = [];
foreach (Database::query(
'select default_value
from track_e_default
where default_event_type=\'user_disable\' and default_value_type=\'user_id\'
group by default_value
having max(default_date) < date_sub(now(), interval 3 year)'
)->fetchAll(FetchMode::COLUMN) as $userId) {
$longDisabledUserIds[] = $userId;
}
$anonymizedUserIds = [];
foreach (Database::query(
'select distinct default_value
from track_e_default
where default_event_type=\'user_anonymized\' and default_value_type=\'user_id\''
)->fetchAll(FetchMode::COLUMN) as $userId) {
$anonymizedUserIds[] = $userId;
}
foreach (array_diff($longDisabledUserIds, $anonymizedUserIds) as $userId) {
$user = $userRepository->find($userId);
if ($user && !$user->isEnabled()) {
if (!$test) {
try {
UserManager::anonymize($userId)
or die("could not anonymize user $userId\n");
} catch (Exception $exception) {
die($exception->getMessage()."\n");
}
}
if ($debug) {
echo "Anonymized user $userId\n";
}
}
}
}
Loading…
Cancel
Save