Filenames like .PPT or .Ppt bypassed the extension checks entirely because the comparison was case-sensitive. Lowercasing guessed_extension at the point of computation fixes this for all checks (legacy, untrusted, block_upload). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
494 lines
22 KiB
Svelte
494 lines
22 KiB
Svelte
<script lang="ts">
|
|
import { run, preventDefault } from 'svelte/legacy';
|
|
|
|
import { createEventDispatcher, onMount, tick } from 'svelte';
|
|
|
|
import type { key_val } from '$lib/stores/ae_stores';
|
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
|
// import { api } from '$lib/api';
|
|
import { check_hosted_file_obj_w_hash } from '$lib/ae_core/core__check_hosted_file_obj_w_hash';
|
|
import {
|
|
ae_loc,
|
|
ae_sess,
|
|
ae_api,
|
|
ae_trig,
|
|
slct,
|
|
slct_trigger
|
|
} from '$lib/stores/ae_stores';
|
|
import { LoaderCircle, Minus } from '@lucide/svelte';
|
|
interface Props {
|
|
// export let element_id = 'svelte_input_file_element';
|
|
container_class_li?: string[];
|
|
table_class_li?: string[];
|
|
untrusted_extension_list?: any;
|
|
legacy_extension_list?: any;
|
|
use_selected_file_table?: boolean;
|
|
input_file_list?: any;
|
|
file_list_status?: null | string;
|
|
processed_file_list?: any[];
|
|
}
|
|
|
|
let {
|
|
container_class_li = [],
|
|
table_class_li = ['table', 'table-sm', 'table-striped', '', 'text-sm'],
|
|
untrusted_extension_list = [
|
|
'bin',
|
|
'dmg',
|
|
'exe',
|
|
'js',
|
|
'msi',
|
|
'php',
|
|
'py',
|
|
'sh'
|
|
],
|
|
legacy_extension_list = ['avi', 'doc', 'ppt', 'xls', 'wmv'],
|
|
use_selected_file_table = true,
|
|
input_file_list = $bindable(null),
|
|
file_list_status = $bindable(null),
|
|
processed_file_list = $bindable([])
|
|
}: Props = $props();
|
|
|
|
// const dispatch = createEventDispatcher();
|
|
|
|
// let input_file_list_processed: any[] = [];
|
|
|
|
onMount(() => {
|
|
console.log('** Element Mounted: ** Element Input File');
|
|
});
|
|
|
|
async function process_file_list(file_list: FileList) {
|
|
console.log('*** process_file_list() ***');
|
|
|
|
file_list_status = 'processing';
|
|
processed_file_list = [];
|
|
|
|
if (!file_list) {
|
|
// file_list_processed = null;
|
|
file_list_status = 'none';
|
|
// await tick();
|
|
return processed_file_list;
|
|
}
|
|
|
|
// const forLoop = async _ => {
|
|
// console.log('*** Start ***');
|
|
|
|
// for (let [i, file_item] of file_list.entries()) { // Not sure why this does not work???
|
|
for await (const [i, file_item] of Array.prototype.entries.call(
|
|
file_list
|
|
)) {
|
|
console.log(i, file_item);
|
|
|
|
// NOTE: The file list is readonly. The filenames can not be changed here.
|
|
if (
|
|
file_item.name.endsWith('.odpmac') ||
|
|
file_item.name.endsWith('.odpwin') ||
|
|
file_item.name.endsWith('.pptmac') ||
|
|
file_item.name.endsWith('.pptwin') ||
|
|
file_item.name.endsWith('.pptxmac') ||
|
|
file_item.name.endsWith('.pptxwin')
|
|
) {
|
|
console.log(
|
|
'This file extension may need to be fixed? API upload will take care of it.'
|
|
);
|
|
// file_item.name = file_item.name.replace('.odpwin', '.odp');
|
|
}
|
|
|
|
let file_data: key_val = {};
|
|
|
|
let filename = file_item.name;
|
|
// console.log(filename);
|
|
file_data['filename'] = filename;
|
|
|
|
let guessed_extension = ae_util.guess_file_extension(filename).toLowerCase();
|
|
file_data['guessed_extension'] = guessed_extension;
|
|
|
|
file_data['type'] = file_item.type;
|
|
|
|
let modified_date = new Date(file_item.lastModified);
|
|
file_data['modified_date'] = modified_date;
|
|
let modified_datetime_string = ae_util.iso_datetime_formatter(
|
|
modified_date,
|
|
'datetime_medium'
|
|
);
|
|
file_data['modified_datetime_string'] = modified_datetime_string;
|
|
|
|
let file_size_bytes = file_item.size;
|
|
file_data['file_size_bytes'] = file_size_bytes;
|
|
let file_size_string = ae_util.format_bytes(file_item.size, 2);
|
|
file_data['file_size_string'] = file_size_string;
|
|
|
|
// // NOTE: Calculate the hash of the file before upload. Check if this exact file has already been uploaded.
|
|
// let file_reader = new FileReader();
|
|
// file_reader.onload = async function() {
|
|
|
|
// const hash_buffer = crypto.subtle.digest('SHA-256', file_reader.result);
|
|
// let hash_hex_test = hash_buffer.then(async function (result_buffer) {
|
|
// const hash_array = Array.from(new Uint8Array(result_buffer)); // convert buffer to byte array
|
|
// const hash_hex = hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
// console.log(`File hash hex? ${hash_hex}`);
|
|
// file_data['hash_sha256'] = hash_hex;
|
|
// return hash_hex;
|
|
// })
|
|
// .catch(function (error: any) {
|
|
// console.log('Something went wrong?', error);
|
|
// });
|
|
|
|
// return hash_hex_test;
|
|
|
|
// // file_data['hash_hex_test'] = hash_hex_test;
|
|
|
|
// // const hash_buffer = await crypto.subtle.digest('SHA-256', file_reader.result);
|
|
// // let hash_str = await new TextDecoder().decode(hash_buffer);
|
|
// // console.log(`File hash string? ${hash_str}`);
|
|
// // file_data['hash_str'] = hash_str;
|
|
|
|
// // const hash_array = Array.from(new Uint8Array(hash_buffer)); // convert buffer to byte array
|
|
// // const hash_hex = hash_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
// // console.log(`File hash hex? ${hash_hex}`);
|
|
|
|
// // file_data['hash_sha256'] = hash_hex;
|
|
// // return hash_hex_test;
|
|
// // return hash_hex;
|
|
// }
|
|
|
|
// // file_reader.then(function (result) {
|
|
// // console.log(`File hash hex? ${result}`);
|
|
// // return hash_hex;
|
|
// // })
|
|
// // .catch(function (error: any) {
|
|
// // console.log('No results returned or failed.', error);
|
|
// // });
|
|
// // file_data['hash_sha256'] = file_reader.readAsArrayBuffer(file_item);
|
|
|
|
let warning_untrusted_extension = false;
|
|
let warning_legacy_extension = false;
|
|
let warning_size = false;
|
|
let warning_message = null;
|
|
|
|
if (untrusted_extension_list.includes(guessed_extension)) {
|
|
console.log('This is an untrusted extension. Going to warn.');
|
|
warning_untrusted_extension = true;
|
|
warning_message =
|
|
'It appears this an untrusted file type and is likely meant to be an executable or installable application. It is <strong>strongly</strong> recommended that this file not be used.';
|
|
} else if (legacy_extension_list.includes(guessed_extension)) {
|
|
console.log('This is a legacy extension. Going to warn.');
|
|
warning_legacy_extension = true;
|
|
if (guessed_extension == 'ppt') {
|
|
warning_message =
|
|
'<strong>WARNING</strong>: It appears this is a legacy Microsoft PowerPoint file and has not been officially supported since Office PowerPoint <strong>2003</strong>. This file type is known to have issues and may not work well. It is <strong>strongly</strong> recommended that this file be saved using the modern PPTX format in PowerPoint. <a href="https://support.microsoft.com/en-us/office/saving-ppt-to-pptx-or-pptm-e8ebc946-98d1-41bb-bb3e-cf00c65e3a55" class="underline font-semibold text-sm">More Help?</a>';
|
|
} else if (guessed_extension == 'doc') {
|
|
warning_message =
|
|
'<strong>WARNING</strong>: It appears this is a legacy Microsoft Word Document file and has not been officially supported since Office Word <strong>2003</strong>. This file type is known to have issues and may not work well. It is <strong>strongly</strong> recommended that this file be saved using the modern DOCX format in Word. <a href="https://support.microsoft.com/en-us/office/saving-doc-to-docx-or-dorm-0107099d-dc1e-4897-8851-f8c9f483f7cd" class="underline font-semibold text-sm">More Help?</a>';
|
|
} else if (guessed_extension == 'avi') {
|
|
warning_message =
|
|
'<strong>WARNING</strong>: It appears this is a video file using the AVI format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG. The file will also likely be much smaller.';
|
|
} else if (guessed_extension == 'wmv') {
|
|
warning_message =
|
|
"<strong>WARNING</strong>: It appears this is a video file using Microsoft's WMV format. It is <strong>strongly</strong> recommended that this file be re-saved as an MP4, MOV, MKV, or MPG/MPEG.";
|
|
} else {
|
|
warning_message =
|
|
'<strong>WARNING</strong>: It appears this is a legacy or not very well supported file format. It is <strong>strongly</strong> recommended that it be saved in an alternative format if possible.';
|
|
}
|
|
} else if (file_size_bytes > 52428800) {
|
|
// 50 MB = 52428800 bytes
|
|
// 100 MB = 104857600 bytes
|
|
console.log(
|
|
`This is a large file size ${file_size_bytes / 1048576} MB (${file_size_bytes} bytes). Going to warn.`
|
|
);
|
|
warning_size = true;
|
|
if (file_size_bytes > 2147483648) {
|
|
// > 2 GB
|
|
warning_message = `This file size (${file_size_string}) is very large and will take at <strong>least</strong> a few minutes to upload depending on your network connection. In some cases it may be worth compressing the file or embedded media. Most audio, image, and video files can be compressed without a significant loss in quality. Be sure you have a stable network connection, especially if you are uploading over a wireless connection. Many business (convention centers, hotels, restaurants, etc) cap upload speeds significantly.`;
|
|
} else if (file_size_bytes > 209715200) {
|
|
// > 200 MB
|
|
warning_message = `This file size (${file_size_string}) is large and will likely take at <strong>least</strong> a few minutes to upload depending on your network connection. Be sure you have a stable network connection, especially if you are uploading over a wireless connection. Many business (convention centers, hotels, restaurants, etc) cap upload speeds significantly. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
|
} else if (file_size_bytes > 104857600) {
|
|
// 100 MB to 200 MB
|
|
warning_message = `This file size (${file_size_string}) is large and will likely take a few minutes to upload depending on your network connection. Be sure you have a stable network connection. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
|
} else {
|
|
// 50 to 100 MB
|
|
warning_message = `This file size (${file_size_string}) is large and may take a few minutes to upload depending on your network connection. In some cases it may be worth compressing the file or embedded media. Many audio, image, and video files can be compressed without a significant loss in quality.`;
|
|
}
|
|
} else {
|
|
}
|
|
|
|
file_data['warning_untrusted_extension'] = warning_untrusted_extension;
|
|
file_data['warning_legacy_extension'] = warning_legacy_extension;
|
|
file_data['warning_size'] = warning_size;
|
|
|
|
file_data['warning_message'] = warning_message;
|
|
|
|
// .ppt and .doc must be converted before upload — block, don't just warn
|
|
file_data['block_upload'] =
|
|
guessed_extension === 'ppt' || guessed_extension === 'doc' || guessed_extension === 'xls';
|
|
|
|
file_data['uploaded'] = null;
|
|
file_data['uploaded_bytes'] = null;
|
|
|
|
// input_file_list_processed.push(JSON.parse(JSON.stringify(file_data)));
|
|
// input_file_list_processed = input_file_list_processed;
|
|
|
|
// console.log(get_file_hash(file_item));
|
|
// console.log(await get_file_hash(file_item));
|
|
|
|
let file_hash = null;
|
|
|
|
// Only hash files less than 2 GB (2147483648 bytes)!!!
|
|
console.log(
|
|
`File size: ${file_size_bytes / 1048576} MB (${file_size_bytes} bytes)`
|
|
);
|
|
if (file_size_bytes < 2000000000) {
|
|
// > 2 GB 2 147 483 648
|
|
file_hash = await ae_util.get_file_hash(file_item);
|
|
} else {
|
|
// File size in MB
|
|
console.log(
|
|
`File is too large to hash. File size: ${file_size_bytes / 1048576} MB`
|
|
);
|
|
}
|
|
|
|
if (file_hash) {
|
|
console.log(
|
|
`Found file hash to lookup: ${ae_util.shorten_string({ string: file_hash })}`
|
|
);
|
|
file_data['hash_sha256'] = file_hash;
|
|
|
|
let check_hosted_file_obj_w_hash_result =
|
|
await check_hosted_file_obj_w_hash({
|
|
api_cfg: $ae_api,
|
|
hosted_file_hash: file_hash
|
|
});
|
|
|
|
// console.log(check_hosted_file_obj_w_hash_result);
|
|
|
|
if (
|
|
check_hosted_file_obj_w_hash_result &&
|
|
check_hosted_file_obj_w_hash_result.hosted_file_found_check
|
|
) {
|
|
console.log('Matching hash!!!');
|
|
file_data['hash_sha256_match'] = true;
|
|
// $ae_events.pres_mgmt.new_upload_list[i].hash_sha256_match = true;
|
|
// $ae_events = $ae_events;
|
|
}
|
|
} else {
|
|
file_data['hash_sha256'] = null;
|
|
file_data['hash_sha256_match'] = false;
|
|
}
|
|
|
|
processed_file_list.push(file_data);
|
|
// input_file_list_processed.push(file_data);
|
|
}
|
|
|
|
const has_blocked = processed_file_list.some((f) => f.block_upload);
|
|
// WHY: trusted_access users (Pres Mgmt admins) see a prominent warning but are
|
|
// not blocked from uploading — they may intentionally handle legacy formats.
|
|
// Non-trusted users are fully blocked. Both cases get a visible banner; only
|
|
// the color and button state differ.
|
|
if (has_blocked) {
|
|
file_list_status = $ae_loc.trusted_access ? 'warn_legacy' : 'blocked_legacy';
|
|
} else {
|
|
file_list_status = 'ready';
|
|
}
|
|
console.log(processed_file_list);
|
|
|
|
// return JSON.parse(JSON.stringify(processed_file_list));
|
|
return processed_file_list;
|
|
}
|
|
|
|
function remove_file_from_filelist(index: number) {
|
|
console.log('*** remove_file_from_filelist() ***');
|
|
|
|
// Can not use something like this because it is readonly:
|
|
const dt = new DataTransfer();
|
|
// let input = document.getElementById(input_element_id);
|
|
|
|
let input_element = document.querySelector(
|
|
'input[type="file"].svelte_input_file_element'
|
|
) as HTMLInputElement;
|
|
|
|
if (!input_element) {
|
|
console.error('Could not find the input element.');
|
|
return false;
|
|
}
|
|
|
|
let files = input_file_list;
|
|
|
|
if (!files || !files.length) {
|
|
console.error('No files found in the file list.');
|
|
file_list_status = null;
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
if (index !== i)
|
|
// Only include the file if it does not match the index value.
|
|
dt.items.add(file);
|
|
}
|
|
|
|
// NOTE: I thought just setting the input_element.files OR input_file_list would trigger the input_file_list change.
|
|
// input_element.files = Object.assign({}, dt.files);
|
|
input_element.files = dt.files; // Assign the updates list
|
|
// input_file_list = null;
|
|
input_file_list = dt.files; // I feel like this should not need to be done, but doing it anyways.
|
|
|
|
return true;
|
|
}
|
|
run(() => {
|
|
if (input_file_list) {
|
|
console.log(input_file_list);
|
|
|
|
process_file_list(input_file_list).then(function (result) {
|
|
// console.log(result);
|
|
|
|
if (!result || !result.length) {
|
|
processed_file_list = [];
|
|
file_list_status = 'none';
|
|
}
|
|
|
|
// Save the results to the file upload list to be displayed as a table.
|
|
// input_file_list_processed = result; // Includes file hash
|
|
|
|
// dispatch(
|
|
// 'input_file_list_updated',
|
|
// {
|
|
// element_id: element_id,
|
|
// input_file_list: input_file_list,
|
|
// input_file_list_processed: result, // Includes file hash
|
|
// }
|
|
// );
|
|
});
|
|
} else {
|
|
processed_file_list = [];
|
|
file_list_status = 'none';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="svelte_element ae_element ae_input_file flex flex-col items-center justify-center gap-1 {container_class_li.join(
|
|
' '
|
|
)} text-center">
|
|
{#if file_list_status == 'processing'}
|
|
<div
|
|
class="file_list_status ae_warning preset-tonal-warning border-warning-500 m-1 border p-1">
|
|
<LoaderCircle size="1em" class="m-1 animate-spin" /> Processing selected
|
|
file list...
|
|
</div>
|
|
{/if}
|
|
|
|
{#if file_list_status == 'blocked_legacy'}
|
|
<div class="m-1 rounded-md border-2 border-red-500 bg-red-100 p-3 text-sm text-red-800 dark:bg-red-900/30 dark:text-red-200">
|
|
<strong>⚠ Upload blocked — legacy file format detected.</strong>
|
|
This Microsoft file type has not been supported in more than 10 years. Please remove the flagged file(s) below and re-save in the modern format before uploading.
|
|
</div>
|
|
{/if}
|
|
|
|
{#if file_list_status == 'warn_legacy'}
|
|
<div class="m-1 rounded-md border-2 border-yellow-500 bg-yellow-50 p-3 text-sm text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-200">
|
|
<strong>⚠ Warning — legacy file format detected.</strong>
|
|
This Microsoft file type has not been supported in more than 10 years and may not work well. You can still upload, but it is strongly recommended to re-save in the modern format first. See details below.
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- {#await processed_file_list} -->
|
|
<!-- {:then} -->
|
|
{#if use_selected_file_table && processed_file_list && processed_file_list.length}
|
|
<strong>Files selected for upload</strong>
|
|
<table class="slct_file_list text-sm {table_class_li.join(' ')}">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-xs">Remove</th>
|
|
<th class="text-xs">Filename</th>
|
|
<th class="text-xs">Modified</th>
|
|
<th class="text-xs">Size</th>
|
|
<!-- <th>Type</th> -->
|
|
<th class="text-xs">Ext</th>
|
|
<th class="text-xs">Hash</th>
|
|
</tr>
|
|
</thead>
|
|
{#each processed_file_list as file_list_item, file_index (file_index)}
|
|
<tbody>
|
|
<tr class="border">
|
|
<td class="file_remove">
|
|
<button
|
|
onclick={preventDefault(() => {
|
|
remove_file_from_filelist(file_index);
|
|
})}
|
|
class="btn btn-md preset-tonal-warning hover:preset-filled-secondary-500 m-1"
|
|
title="Remove file from upload list">
|
|
<Minus size="1em" />
|
|
<span class="hidden">Remove</span>
|
|
</button>
|
|
</td>
|
|
<td
|
|
class="file_filename text-wrap break-all md:break-words">
|
|
{file_list_item.filename}
|
|
</td>
|
|
<td class="file_last_modified"
|
|
>{file_list_item.modified_datetime_string}</td>
|
|
<td
|
|
class="file_size"
|
|
class:bg-pink-200={file_list_item.warning_size}>
|
|
{file_list_item.file_size_string}
|
|
{#if $ae_sess.api_upload_kv[file_list_item.hash_sha256]}
|
|
<span class="text-xs"
|
|
>({$ae_sess.api_upload_kv[
|
|
file_list_item.hash_sha256
|
|
].percent_completed}%)</span>
|
|
{/if}
|
|
</td>
|
|
<!-- <td class="file_type" class:warning_file_untrusted_extension={file_list_item.warning_untrusted_extension} class:warning_file_legacy_extension={file_list_item.warning_legacy_extension}>{file_list_item.type}</td> -->
|
|
<td
|
|
class="file_extension"
|
|
class:bg-red-200={file_list_item.warning_untrusted_extension}
|
|
class:bg-pink-200={file_list_item.warning_legacy_extension}>
|
|
{file_list_item.guessed_extension}
|
|
</td>
|
|
<td
|
|
class="file_hash file_hash256"
|
|
class:bg-pink-200={file_list_item.hash_sha256_match}>
|
|
{ae_util.shorten_string({
|
|
string: file_list_item.hash_sha256,
|
|
begin_length: 5,
|
|
end_length: 4,
|
|
wildcard_length: 2
|
|
})}
|
|
</td>
|
|
</tr>
|
|
{#if file_list_item.warning_message}
|
|
<tr>
|
|
<td colspan="6"
|
|
class="text-left text-xs m-1 border border-red-500 p-3"
|
|
class:bg-red-100={file_list_item.block_upload}
|
|
class:text-red-800={file_list_item.block_upload}
|
|
class:bg-yellow-50={!file_list_item.block_upload}
|
|
class:text-yellow-900={!file_list_item.block_upload}>
|
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
{@html file_list_item.warning_message}
|
|
</td>
|
|
</tr>
|
|
{/if}
|
|
</tbody>
|
|
{/each}
|
|
</table>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
th {
|
|
text-align: center;
|
|
/* font-size: smaller; */
|
|
}
|
|
td {
|
|
text-align: center;
|
|
}
|
|
.file_last_modified {
|
|
font-size: smaller;
|
|
}
|
|
|
|
.file_hash {
|
|
font-family: 'Courier New', Courier, monospace;
|
|
}
|
|
</style>
|