fix: sanitize image filename before saving for course and jobs

This commit is contained in:
Jannat Patel
2026-01-14 17:54:23 +05:30
parent e2479cd787
commit e7ccf0a711
4 changed files with 33 additions and 163 deletions
@@ -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
}>(), }>(),
{ {
+8 -9
View File
@@ -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])
} }
}) })
+14 -84
View File
@@ -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
+10 -69
View File
@@ -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' },