diff --git a/adminforth/spa/src/utils/utils.ts b/adminforth/spa/src/utils/utils.ts index fc0ea955a..ca4d3f0cd 100644 --- a/adminforth/spa/src/utils/utils.ts +++ b/adminforth/spa/src/utils/utils.ts @@ -641,11 +641,21 @@ export function checkShowIf(c: AdminForthResourceColumnInputCommon, record: Reco } export function btoa_function(source: string): string { - return btoa(source); + // UTF-8 safe base64 encode: plain btoa() throws on characters outside the + const bytes = new TextEncoder().encode(source); + let binary = ''; + const chunkSize = 0x8000; + for (let i = 0; i < bytes.length; i += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); + } + return btoa(binary); } export function atob_function(source: string): string { - return atob(source); + // UTF-8 safe base64 decode, the counterpart of btoa_function above. + const binary = atob(source); + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)); + return new TextDecoder().decode(bytes); } export function compareOldAndNewRecord(oldRecord: Record, newRecord: Record): {ok: boolean, changedFields: Record} { diff --git a/adminforth/spa/src/views/CreateView.vue b/adminforth/spa/src/views/CreateView.vue index 5c3b0a08e..927310c4f 100644 --- a/adminforth/spa/src/views/CreateView.vue +++ b/adminforth/spa/src/views/CreateView.vue @@ -77,7 +77,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue'; import ResourceForm from '@/components/ResourceForm.vue'; import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue'; import { useCoreStore } from '@/stores/core'; -import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, formatComponent } from '@/utils'; +import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, formatComponent, atob_function } from '@/utils'; import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite'; import { onMounted, onBeforeMount, onBeforeUnmount, ref } from 'vue'; import { useRoute, useRouter } from 'vue-router'; @@ -177,14 +177,14 @@ onMounted(async () => { if (userUseMultipleEncoding) { initialValues.value = { ...initialValues.value, ...JSON.parse(decodeURIComponent((route.query.values as string))) }; } else { - initialValues.value = { ...initialValues.value, ...JSON.parse(atob(route.query.values as string)) }; + initialValues.value = { ...initialValues.value, ...JSON.parse(atob_function(route.query.values as string)) }; } } if (route.query.readonlyColumns) { if (userUseMultipleEncoding) { readonlyColumns.value = JSON.parse(decodeURIComponent((route.query.readonlyColumns as string))); } else { - readonlyColumns.value = JSON.parse(atob(route.query.readonlyColumns as string)); + readonlyColumns.value = JSON.parse(atob_function(route.query.readonlyColumns as string)); } } record.value = initialValues.value;