fix: sanitize image filename before saving for course and jobs
This commit is contained in:
@@ -70,7 +70,7 @@ const props = withDefaults(
|
|||||||
modelValue: string
|
modelValue: string
|
||||||
label?: string
|
label?: string
|
||||||
description?: string
|
description?: string
|
||||||
type: 'image' | 'video'
|
type?: 'image' | 'video'
|
||||||
required?: boolean
|
required?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -281,22 +281,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
FormControl,
|
FormControl,
|
||||||
FileUploader,
|
|
||||||
Button,
|
Button,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
createResource,
|
createResource,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
call,
|
call,
|
||||||
Toast,
|
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { Image, Trash2 } from 'lucide-vue-next'
|
|
||||||
import { capture } from '@/telemetry'
|
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
|
||||||
import { sessionStore } from '../stores/session'
|
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
import {
|
import {
|
||||||
escapeHTML,
|
escapeHTML,
|
||||||
getMetaInfo,
|
getMetaInfo,
|
||||||
@@ -304,7 +295,14 @@ import {
|
|||||||
sanitizeHTML,
|
sanitizeHTML,
|
||||||
updateMetaInfo,
|
updateMetaInfo,
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Trash2 } from 'lucide-vue-next'
|
||||||
|
import { capture } from '@/telemetry'
|
||||||
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
|
import { sessionStore } from '../stores/session'
|
||||||
import Uploader from '@/components/Controls/Uploader.vue'
|
import Uploader from '@/components/Controls/Uploader.vue'
|
||||||
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
@@ -466,6 +464,7 @@ const validateFields = () => {
|
|||||||
!['description', 'batch_details'].includes(key) &&
|
!['description', 'batch_details'].includes(key) &&
|
||||||
typeof batch[key] === 'string'
|
typeof batch[key] === 'string'
|
||||||
) {
|
) {
|
||||||
|
console.log(key)
|
||||||
batch[key] = escapeHTML(batch[key])
|
batch[key] = escapeHTML(batch[key])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,58 +75,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
<div class="mb-4">
|
<Uploader
|
||||||
<div class="text-xs text-ink-gray-5 mb-2">
|
v-model="course.image"
|
||||||
{{ __('Course Image') }}
|
:label="__('Course Image')"
|
||||||
</div>
|
:required="false"
|
||||||
<FileUploader
|
/>
|
||||||
v-if="!course.course_image"
|
|
||||||
:fileTypes="['image/*']"
|
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(file) => saveImage(file)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div
|
|
||||||
class="border rounded-md w-fit py-5 px-20 cursor-pointer"
|
|
||||||
@click="openFileSelector"
|
|
||||||
>
|
|
||||||
<Image class="size-5 stroke-1 text-ink-gray-7" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<Button @click="openFileSelector">
|
|
||||||
{{ __('Upload') }}
|
|
||||||
</Button>
|
|
||||||
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
|
|
||||||
{{
|
|
||||||
__('Appears on the course card in the course list')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else class="mb-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
:src="course.course_image.file_url"
|
|
||||||
class="border rounded-md w-40"
|
|
||||||
/>
|
|
||||||
<div class="ml-4">
|
|
||||||
<Button @click="removeImage()">
|
|
||||||
{{ __('Remove') }}
|
|
||||||
</Button>
|
|
||||||
<div class="mt-2 text-ink-gray-5 text-sm">
|
|
||||||
{{
|
|
||||||
__('Appears on the course card in the course list')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ColorSwatches
|
<ColorSwatches
|
||||||
v-model="course.card_gradient"
|
v-model="course.card_gradient"
|
||||||
@@ -333,7 +286,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
createResource,
|
createResource,
|
||||||
FormControl,
|
FormControl,
|
||||||
FileUploader,
|
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
@@ -347,23 +299,23 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { Image, Trash2, X } from 'lucide-vue-next'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { capture, startRecording, stopRecording } from '@/telemetry'
|
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
|
||||||
import { sessionStore } from '../stores/session'
|
|
||||||
import {
|
import {
|
||||||
escapeHTML,
|
escapeHTML,
|
||||||
getMetaInfo,
|
getMetaInfo,
|
||||||
openSettings,
|
openSettings,
|
||||||
sanitizeHTML,
|
sanitizeHTML,
|
||||||
updateMetaInfo,
|
updateMetaInfo,
|
||||||
validateFile,
|
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import { Trash2, X } from 'lucide-vue-next'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { capture, startRecording, stopRecording } from '@/telemetry'
|
||||||
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
|
import { sessionStore } from '../stores/session'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
|
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
|
||||||
|
import Uploader from '@/components/Controls/Uploader.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const newTag = ref('')
|
const newTag = ref('')
|
||||||
@@ -386,7 +338,7 @@ const course = reactive({
|
|||||||
short_introduction: '',
|
short_introduction: '',
|
||||||
description: '',
|
description: '',
|
||||||
video_link: '',
|
video_link: '',
|
||||||
course_image: null,
|
image: null,
|
||||||
card_gradient: '',
|
card_gradient: '',
|
||||||
tags: '',
|
tags: '',
|
||||||
category: '',
|
category: '',
|
||||||
@@ -450,7 +402,7 @@ const courseCreationResource = createResource({
|
|||||||
return {
|
return {
|
||||||
doc: {
|
doc: {
|
||||||
doctype: 'LMS Course',
|
doctype: 'LMS Course',
|
||||||
image: course.course_image?.file_url || '',
|
image: course.image,
|
||||||
instructors: instructors.value.map((instructor) => ({
|
instructors: instructors.value.map((instructor) => ({
|
||||||
instructor: instructor,
|
instructor: instructor,
|
||||||
})),
|
})),
|
||||||
@@ -471,7 +423,7 @@ const courseEditResource = createResource({
|
|||||||
doctype: 'LMS Course',
|
doctype: 'LMS Course',
|
||||||
name: values.course,
|
name: values.course,
|
||||||
fieldname: {
|
fieldname: {
|
||||||
image: course.course_image?.file_url || '',
|
image: course.image,
|
||||||
instructors: instructors.value.map((instructor) => ({
|
instructors: instructors.value.map((instructor) => ({
|
||||||
instructor: instructor,
|
instructor: instructor,
|
||||||
})),
|
})),
|
||||||
@@ -521,24 +473,10 @@ const courseResource = createResource({
|
|||||||
course[key] = course[key] ? true : false
|
course[key] = course[key] ? true : false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.image) imageResource.reload({ image: data.image })
|
|
||||||
check_permission()
|
check_permission()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageResource = createResource({
|
|
||||||
url: 'lms.lms.api.get_file_info',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
file_url: values.image,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: false,
|
|
||||||
onSuccess(data) {
|
|
||||||
course.course_image = data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const validateFields = () => {
|
const validateFields = () => {
|
||||||
course.description = sanitizeHTML(course.description)
|
course.description = sanitizeHTML(course.description)
|
||||||
|
|
||||||
@@ -655,14 +593,6 @@ const removeTag = (tag) => {
|
|||||||
newTag.value = ''
|
newTag.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveImage = (file) => {
|
|
||||||
course.course_image = file
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeImage = () => {
|
|
||||||
course.course_image = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const check_permission = () => {
|
const check_permission = () => {
|
||||||
let user_is_instructor = false
|
let user_is_instructor = false
|
||||||
if (user.data?.is_moderator) return
|
if (user.data?.is_moderator) return
|
||||||
|
|||||||
@@ -83,47 +83,11 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<label class="block text-ink-gray-5 text-xs mb-1 mt-4">
|
<Uploader
|
||||||
{{ __('Company Logo') }}
|
v-model="job.company_logo"
|
||||||
<span class="text-ink-red-3">*</span>
|
:label="__('Company Logo')"
|
||||||
</label>
|
:required="false"
|
||||||
<FileUploader
|
/>
|
||||||
v-if="!job.image"
|
|
||||||
:fileTypes="['image/*']"
|
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(file) => saveImage(file)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
|
||||||
>
|
|
||||||
<div class="mb-4">
|
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
|
||||||
{{
|
|
||||||
uploading ? `Uploading ${progress}%` : 'Upload an image'
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else class="">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="border rounded-md p-2 mr-2">
|
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-ink-gray-7" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span>
|
|
||||||
{{ job.image.file_name }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-ink-gray-4 mt-1">
|
|
||||||
{{ getFileSize(job.image.file_size) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<X
|
|
||||||
@click="removeImage()"
|
|
||||||
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,15 +114,14 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
Button,
|
Button,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
FileUploader,
|
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, onMounted, reactive, inject } from 'vue'
|
import { computed, onMounted, reactive, inject } from 'vue'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { escapeHTML, getFileSize, sanitizeHTML, validateFile } from '@/utils'
|
import { escapeHTML, sanitizeHTML } from '@/utils'
|
||||||
|
import Uploader from '@/components/Controls/Uploader.vue'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -177,7 +140,7 @@ const newJob = createResource({
|
|||||||
return {
|
return {
|
||||||
doc: {
|
doc: {
|
||||||
doctype: 'Job Opportunity',
|
doctype: 'Job Opportunity',
|
||||||
company_logo: job.image?.file_url,
|
company_logo: job.company_logo,
|
||||||
...job,
|
...job,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -191,7 +154,7 @@ const updateJob = createResource({
|
|||||||
doctype: 'Job Opportunity',
|
doctype: 'Job Opportunity',
|
||||||
name: props.jobName,
|
name: props.jobName,
|
||||||
fieldname: {
|
fieldname: {
|
||||||
company_logo: job.image.file_url,
|
company_logo: job.company_logo,
|
||||||
...job,
|
...job,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -215,20 +178,6 @@ const jobDetail = createResource({
|
|||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
if (Object.hasOwn(job, key)) job[key] = data[key]
|
if (Object.hasOwn(job, key)) job[key] = data[key]
|
||||||
})
|
})
|
||||||
if (data.company_logo) imageResource.reload({ image: data.company_logo })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageResource = createResource({
|
|
||||||
url: 'lms.lms.api.get_file_info',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
file_url: values.image,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: false,
|
|
||||||
onSuccess(data) {
|
|
||||||
job.image = data
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -241,7 +190,7 @@ const job = reactive({
|
|||||||
status: 'Open',
|
status: 'Open',
|
||||||
company_name: '',
|
company_name: '',
|
||||||
company_website: '',
|
company_website: '',
|
||||||
image: null,
|
company_logo: null,
|
||||||
description: '',
|
description: '',
|
||||||
company_email_address: '',
|
company_email_address: '',
|
||||||
})
|
})
|
||||||
@@ -322,14 +271,6 @@ const validateJobFields = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveImage = (file) => {
|
|
||||||
job.image = file
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeImage = () => {
|
|
||||||
job.image = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobTypes = computed(() => {
|
const jobTypes = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: 'Full Time', value: 'Full Time' },
|
{ label: 'Full Time', value: 'Full Time' },
|
||||||
|
|||||||
Reference in New Issue
Block a user