commit
45bd9ae5ee
@ -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)">×</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> |
||||||
@ -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> |
<template> |
||||||
<Toolbar :class="toolbarClass"> |
<Toolbar :class="toolbarClass"> |
||||||
<template #start> |
<template v-slot:start> |
||||||
<slot></slot> |
<slot name="start"></slot> |
||||||
|
</template> |
||||||
|
<template v-slot:end> |
||||||
|
<slot name="end"></slot> |
||||||
</template> |
</template> |
||||||
</Toolbar> |
</Toolbar> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script setup> |
<script setup> |
||||||
import Toolbar from "primevue/toolbar"; |
import Toolbar from "primevue/toolbar" |
||||||
import { computed } from "vue"; |
import { computed } from "vue" |
||||||
|
|
||||||
const props = defineProps({ |
const props = defineProps({ |
||||||
showTopBorder: { |
showTopBorder: { |
||||||
type: Boolean, |
type: Boolean, |
||||||
default: false, |
default: false, |
||||||
}, |
}, |
||||||
}); |
}) |
||||||
|
|
||||||
const toolbarClass = computed(() => { |
const toolbarClass = computed(() => { |
||||||
if (props.showTopBorder) { |
if (props.showTopBorder) { |
||||||
return "pt-5 border-t border-b"; |
return "pt-5 border-t border-b" |
||||||
} |
} |
||||||
return ""; |
return "p-toolbar" |
||||||
}); |
}) |
||||||
</script> |
</script> |
||||||
|
|||||||
@ -1,174 +1,174 @@ |
|||||||
<template> |
<template> |
||||||
<v-form> |
<div class="course-form-container"> |
||||||
<v-container fluid> |
<div class="form-header"> |
||||||
<v-row> |
<BaseInputText |
||||||
<v-col cols="12" sm="6" md="6"> |
id="course-name" |
||||||
<v-text-field |
:label="t('Course name')" |
||||||
v-model="item.title" |
:help-text="t('Write a short and striking course name, For example: Innovation Management')" |
||||||
:error-messages="titleErrors" |
v-model="courseName" |
||||||
:label="$t('Title')" |
:error-text="courseNameError" |
||||||
required |
:is-invalid="isCourseNameInvalid" |
||||||
@input="$v.item.title.$touch()" |
required |
||||||
@blur="$v.item.title.$touch()" |
/> |
||||||
/> |
<BaseAdvancedSettingsButton v-model="showAdvancedSettings"></BaseAdvancedSettingsButton> |
||||||
</v-col> |
</div> |
||||||
|
<div v-if="showAdvancedSettings" class="advanced-settings"> |
||||||
<v-col cols="12" sm="6" md="6"> |
<BaseMultiSelect |
||||||
<v-text-field |
id="category-multiselect" |
||||||
v-model="item.code" |
v-model="courseCategory" |
||||||
:error-messages="codeErrors" |
:options="categoryOptions" |
||||||
:label="$t('code')" |
:label="t('Category')" |
||||||
required |
input-id="multiselect-category" |
||||||
@input="$v.item.code.$touch()" |
/> |
||||||
@blur="$v.item.code.$touch()" |
<BaseInputText |
||||||
/> |
id="course-code" |
||||||
</v-col> |
:label="t('Course code')" |
||||||
</v-row> |
:help-text="t('Only letters (a-z) and numbers (0-9)')" |
||||||
|
v-model="courseCode" |
||||||
<v-row> |
:maxlength="40" |
||||||
<v-col cols="12" sm="6" md="6"> |
:error-text="courseCodeError" |
||||||
<v-combobox |
:is-invalid="isCodeInvalid" |
||||||
v-model="item.category" |
validation-message="Only letters (a-z) and numbers (0-9) are allowed." |
||||||
:items="categorySelectItems" |
/> |
||||||
:error-messages="categoryErrors" |
<BaseDropdown |
||||||
:no-data-text="$t('No results')" |
name="language" |
||||||
:label="$t('category')" |
v-model="courseLanguage" |
||||||
item-text="name" |
:options="languageOptions" |
||||||
item-value="@id" |
:placeholder="t('Select Language')" |
||||||
/> |
input-id="language-dropdown" |
||||||
</v-col> |
:label="t('Language')" |
||||||
|
option-label="name" |
||||||
<v-col cols="12" sm="6" md="6"> |
/> |
||||||
<v-text-field |
<BaseCheckbox |
||||||
v-model.number="item.visibility" |
id="demo-content" |
||||||
:error-messages="visibilityErrors" |
:label="t('Fill with demo content')" |
||||||
:label="$t('visibility')" |
v-model="fillDemoContent" |
||||||
required |
name="" |
||||||
@input="$v.item.visibility.$touch()" |
/> |
||||||
@blur="$v.item.visibility.$touch()" |
<BaseAutocomplete |
||||||
/> |
id="template" |
||||||
</v-col> |
v-model="courseTemplate" |
||||||
</v-row> |
:label="t('Select Template')" |
||||||
</v-container> |
:search="searchTemplates" |
||||||
</v-form> |
/> |
||||||
|
</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> |
</template> |
||||||
|
|
||||||
<script> |
<script setup> |
||||||
import has from 'lodash/has'; |
import { onMounted, ref, watch } from "vue" |
||||||
import useVuelidate from '@vuelidate/core'; |
import BaseInputText from "../basecomponents/BaseInputText.vue" |
||||||
import { required } from '@vuelidate/validators'; |
import BaseAdvancedSettingsButton from "../basecomponents/BaseAdvancedSettingsButton.vue" |
||||||
import { mapActions } from 'vuex'; |
import BaseDropdown from "../basecomponents/BaseDropdown.vue" |
||||||
import { mapFields } from 'vuex-map-fields'; |
import BaseCheckbox from "../basecomponents/BaseCheckbox.vue" |
||||||
|
import BaseButton from "../basecomponents/BaseButton.vue" |
||||||
export default { |
import { useRouter } from "vue-router" |
||||||
name: 'CourseForm', |
import courseService from "../../services/courseService" |
||||||
setup () { |
import languageService from "../../services/languageService" |
||||||
return { v$: useVuelidate() } |
import BaseAutocomplete from "../basecomponents/BaseAutocomplete.vue" |
||||||
}, |
import BaseMultiSelect from "../basecomponents/BaseMultiSelect.vue" |
||||||
props: { |
import { useI18n } from "vue-i18n" |
||||||
values: { |
|
||||||
type: Object, |
const { t } = useI18n() |
||||||
required: true |
const courseName = ref('') |
||||||
}, |
const courseCategory = ref([]) |
||||||
|
const courseCode = ref('') |
||||||
errors: { |
const courseLanguage = ref(null) |
||||||
type: Object, |
const fillDemoContent = ref(false) |
||||||
default: () => {} |
const courseTemplate = ref(null); |
||||||
}, |
const showAdvancedSettings = ref(false) |
||||||
|
const router = useRouter() |
||||||
initialValues: { |
|
||||||
type: Object, |
const categoryOptions = ref([]) |
||||||
default: () => {} |
const languageOptions = ref([]) |
||||||
} |
|
||||||
}, |
const courseNameError = ref('') |
||||||
data() { |
const courseCodeError = ref('') |
||||||
return { |
const isCodeInvalid = ref(false) |
||||||
title: null, |
const isCourseNameInvalid = ref(false) |
||||||
code: null, |
|
||||||
category: null, |
const formSubmitted = ref(false) |
||||||
visibility: null, |
|
||||||
}; |
const emit = defineEmits(['submit']) |
||||||
}, |
|
||||||
computed: { |
const validateCourseCode = () => { |
||||||
...mapFields('coursecategory', { |
const pattern = /^[a-zA-Z0-9]*$/ |
||||||
categorySelectItems: 'selectItems' |
if (!pattern.test(courseCode.value)) { |
||||||
}), |
isCodeInvalid.value = true |
||||||
|
courseCodeError.value = 'Only letters (a-z) and numbers (0-9) are allowed.' |
||||||
// eslint-disable-next-line |
return false |
||||||
item() { |
} |
||||||
return this.initialValues || this.values; |
courseCodeError.value = '' |
||||||
}, |
return true |
||||||
|
} |
||||||
titleErrors() { |
|
||||||
const errors = []; |
const submitForm = () => { |
||||||
|
formSubmitted.value = true |
||||||
if (!this.$v.item.title.$dirty) return errors; |
if (!courseName.value) { |
||||||
|
isCourseNameInvalid.value = true |
||||||
has(this.violations, 'title') && errors.push(this.violations.title); |
courseNameError.value = 'This field is required' |
||||||
|
return |
||||||
!this.$v.item.title.required && errors.push(this.$t('Field is required')); |
} |
||||||
|
|
||||||
return errors; |
if (!validateCourseCode()) { |
||||||
}, |
return |
||||||
codeErrors() { |
} |
||||||
const errors = []; |
|
||||||
|
emit('submit', { |
||||||
if (!this.$v.item.code.$dirty) return errors; |
name: courseName.value, |
||||||
|
category: courseCategory.value ? courseCategory.value : null, |
||||||
has(this.violations, 'code') && errors.push(this.violations.code); |
code: courseCode.value, |
||||||
|
language: courseLanguage.value, |
||||||
!this.$v.item.code.required && errors.push(this.$t('Field is required')); |
template: courseTemplate.value ? courseTemplate.value.value : null, |
||||||
|
fillDemoContent: fillDemoContent.value |
||||||
return errors; |
}) |
||||||
}, |
} |
||||||
categoryErrors() { |
|
||||||
const errors = []; |
onMounted(async () => { |
||||||
|
try { |
||||||
if (!this.$v.item.category.$dirty) return errors; |
const categoriesResponse = await courseService.getCategories('categories'); |
||||||
|
categoryOptions.value = categoriesResponse.map(category => ({ |
||||||
has(this.violations, 'category') && errors.push(this.violations.category); |
name: category.name, |
||||||
|
id: category.id, |
||||||
|
})) |
||||||
return errors; |
|
||||||
}, |
const languagesResponse = await languageService.findAll() |
||||||
visibilityErrors() { |
const data = await languagesResponse.json() |
||||||
const errors = []; |
languageOptions.value = data['hydra:member'].map(language => ({ |
||||||
|
name: language.englishName, |
||||||
if (!this.$v.item.visibility.$dirty) return errors; |
id: language.isocode, |
||||||
|
})) |
||||||
has(this.violations, 'visibility') && errors.push(this.violations.visibility); |
|
||||||
|
} catch (error) { |
||||||
!this.$v.item.visibility.required && errors.push(this.$t('Field is required')); |
console.error('Failed to load dropdown data', error) |
||||||
|
} |
||||||
return errors; |
}); |
||||||
}, |
|
||||||
|
const searchTemplates = async (query) => { |
||||||
violations() { |
if (query && query.length >= 3) { |
||||||
return this.errors || {}; |
return courseService.searchTemplates(query) |
||||||
} |
} else { |
||||||
}, |
return [] |
||||||
mounted() { |
} |
||||||
this.categoryGetSelectItems(); |
} |
||||||
}, |
|
||||||
methods: { |
const goBack = () => { |
||||||
...mapActions({ |
router.go(-1) |
||||||
categoryGetSelectItems: 'coursecategory/load' |
} |
||||||
}), |
|
||||||
}, |
|
||||||
validations: { |
|
||||||
item: { |
|
||||||
title: { |
|
||||||
required, |
|
||||||
}, |
|
||||||
code: { |
|
||||||
required, |
|
||||||
}, |
|
||||||
category: { |
|
||||||
}, |
|
||||||
visibility: { |
|
||||||
required, |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
</script> |
</script> |
||||||
|
|||||||
@ -1,53 +1,64 @@ |
|||||||
<template> |
<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 |
<CourseForm |
||||||
ref="createForm" |
ref="createForm" |
||||||
:errors="violations" |
:errors="violations" |
||||||
:values="item" |
:values="item" |
||||||
|
@submit="submitCourse" |
||||||
/> |
/> |
||||||
<Loading :visible="isLoading" /> |
<Loading :visible="isLoading" /> |
||||||
|
|
||||||
<Toolbar |
|
||||||
:handle-reset="resetForm" |
|
||||||
:handle-submit="onSendForm" |
|
||||||
></Toolbar> |
|
||||||
</div> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script> |
<script setup> |
||||||
import { mapActions } from "vuex" |
import { ref, computed } from 'vue' |
||||||
import { createHelpers } from "vuex-map-fields" |
import { useStore } from 'vuex' |
||||||
import CourseForm from "../../components/course/Form.vue" |
import CourseForm from '../../components/course/Form.vue' |
||||||
import Loading from "../../components/Loading.vue" |
import Loading from '../../components/Loading.vue' |
||||||
import Toolbar from "../../components/Toolbar.vue" |
import { useRouter } from "vue-router" |
||||||
import CreateMixin from "../../mixins/CreateMixin" |
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({ |
const isLoading = computed(() => store.getters['course/getField']('isLoading')) |
||||||
getterType: "course/getField", |
const violations = computed(() => store.getters['course/getField']('violations')) |
||||||
mutationType: "course/updateField", |
const courseData = ref({}) |
||||||
}) |
|
||||||
|
|
||||||
export default { |
const submitCourse = async (formData) => { |
||||||
name: "CourseCreate", |
isLoading.value = true |
||||||
servicePrefix, |
try { |
||||||
mixins: [CreateMixin], |
let tempResponse = await courseService.createCourse(formData) |
||||||
components: { |
if (tempResponse.success) { |
||||||
Loading, |
const courseId = tempResponse.courseId |
||||||
Toolbar, |
const sessionId = 0 |
||||||
CourseForm, |
await router.push(`/course/${courseId}/home?sid=${sessionId}`) |
||||||
}, |
} else { |
||||||
data() { |
console.error(tempResponse.message) |
||||||
return { |
} |
||||||
item: {}, |
} catch (error) { |
||||||
|
console.error(error) |
||||||
|
if (error.response && error.response.data) { |
||||||
|
violations.value = error.response.data |
||||||
|
} else { |
||||||
|
console.error('An unexpected error occurred.') |
||||||
} |
} |
||||||
}, |
} finally { |
||||||
computed: { |
isLoading.value = false |
||||||
...mapFields(["error", "isLoading", "created", "violations"]), |
} |
||||||
}, |
|
||||||
methods: { |
|
||||||
...mapActions("course", ["create", "reset"]), |
|
||||||
}, |
|
||||||
} |
} |
||||||
</script> |
</script> |
||||||
|
|||||||
@ -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'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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…
Reference in new issue