From 117e72f8adde3218d7b6b1cc15b21917a04c7d44 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Thu, 3 Oct 2024 14:21:58 -0500 Subject: [PATCH 1/7] [MWPW-159826] Check image type based on binary starting signatures (#246) --- .../image-dropzone/image-dropzone.js | 20 ++++++---- ecc/scripts/image-validator.js | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 ecc/scripts/image-validator.js diff --git a/ecc/components/image-dropzone/image-dropzone.js b/ecc/components/image-dropzone/image-dropzone.js index 197bc8ce..42a571f1 100644 --- a/ecc/components/image-dropzone/image-dropzone.js +++ b/ecc/components/image-dropzone/image-dropzone.js @@ -1,5 +1,6 @@ /* eslint-disable import/prefer-default-export */ /* eslint-disable class-methods-use-this */ +import { isImageTypeValid, isImageSizeValid } from '../../scripts/image-validator.js'; import { LIBS } from '../../scripts/scripts.js'; import { style } from './image-dropzone.css.js'; @@ -22,16 +23,21 @@ export class ImageDropzone extends LitElement { this.handleDelete = this.handleDelete || null; } - setFile(files) { + async setFile(files) { const [file] = files; - if (file.size > 26214400) { + + if (!isImageSizeValid(file, 26214400)) { this.dispatchEvent(new CustomEvent('show-error-toast', { detail: { error: { message: 'File size should be less than 25MB' } }, bubbles: true, composed: true })); return; } - if (file.type.startsWith('image/')) { + + const isValid = await isImageTypeValid(file); + if (isValid) { this.file = file; this.file.url = URL.createObjectURL(file); this.requestUpdate(); + } else { + this.dispatchEvent(new CustomEvent('show-error-toast', { detail: { error: { message: 'Invalid file type. The image file should be in one of the following format: .jpeg, .jpg, .png, .svg' } }, bubbles: true, composed: true })); } } @@ -39,22 +45,22 @@ export class ImageDropzone extends LitElement { return this.file; } - handleImageDrop(e) { + async handleImageDrop(e) { e.preventDefault(); e.stopPropagation(); const { files } = e.dataTransfer; if (files.length > 0) { - this.setFile(files); + await this.setFile(files); this.handleImage(); } } - onImageChange(e) { + async onImageChange(e) { const { files } = e.currentTarget; if (files.length > 0) { - this.setFile(files); + await this.setFile(files); this.handleImage(); } diff --git a/ecc/scripts/image-validator.js b/ecc/scripts/image-validator.js new file mode 100644 index 00000000..54163770 --- /dev/null +++ b/ecc/scripts/image-validator.js @@ -0,0 +1,39 @@ +export async function isImageTypeValid(file) { + const validTypes = ['jpeg', 'jpg', 'png', 'svg']; + let currentFileType = ''; + + const blob = file.slice(0, 128); + + const arrayBuffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + + const signatures = { + jpeg: [0xFF, 0xD8, 0xFF], + png: [0x89, 0x50, 0x4E, 0x47], + }; + + if (signatures.jpeg.every((byte, i) => byte === bytes[i])) { + const extension = file.name.split('.').pop().toLowerCase(); + if (extension === 'jpg' || extension === 'jpeg') { + currentFileType = extension; + } + + currentFileType = 'jpg'; + } + + if (signatures.png.every((byte, i) => byte === bytes[i])) { + currentFileType = 'png'; + } + + const text = await blob.text(); + + if (text.trim().startsWith(' Date: Fri, 4 Oct 2024 09:42:44 -0500 Subject: [PATCH 2/7] Expect ESL error format difference (#249) --- ecc/blocks/form-handler/form-handler.js | 6 +++--- ecc/scripts/esp-controller.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ecc/blocks/form-handler/form-handler.js b/ecc/blocks/form-handler/form-handler.js index ee73e6bd..ed341575 100644 --- a/ecc/blocks/form-handler/form-handler.js +++ b/ecc/blocks/form-handler/form-handler.js @@ -104,8 +104,8 @@ export function buildErrorMessage(props, resp) { }); }); } else if (errorMessage) { - if (resp.status === 409) { - const toast = createTag('sp-toast', { open: true, variant: 'negative' }, errorMessage, { parent: toastArea }); + if (resp.status === 409 || resp.error.message === 'Request to ESP failed: {"message":"Event update invalid, event has been modified since last fetch"}') { + const toast = createTag('sp-toast', { open: true, variant: 'negative' }, 'The event has been updated by a different session since your last save.', { parent: toastArea }); const url = new URL(window.location.href); url.searchParams.set('eventId', getFilteredCachedResponse().eventId); @@ -113,7 +113,7 @@ export function buildErrorMessage(props, resp) { slot: 'action', variant: 'overBackground', href: `${url.toString()}`, - }, 'See the latest version.', { parent: toast }); + }, 'See the latest version', { parent: toast }); toast.addEventListener('close', () => { toast.remove(); diff --git a/ecc/scripts/esp-controller.js b/ecc/scripts/esp-controller.js index 4f5d31e7..e6e28413 100644 --- a/ecc/scripts/esp-controller.js +++ b/ecc/scripts/esp-controller.js @@ -215,7 +215,7 @@ export async function createVenue(eventId, venueData) { return { ok: response.ok, status: response.status, error: data }; } - return data; + return data.espProvider || data; } catch (error) { window.lana?.log('Failed to create venue. Error:', error); return { ok: false, status: 'Network Error', error: error.message }; @@ -236,7 +236,7 @@ export async function replaceVenue(eventId, venueId, venueData) { return { ok: response.ok, status: response.status, error: data }; } - return data; + return data.espProvider || data; } catch (error) { window.lana?.log('Failed to replace venue. Error:', error); return { ok: false, status: 'Network Error', error: error.message }; @@ -257,7 +257,7 @@ export async function createEvent(payload) { return { ok: response.ok, status: response.status, error: data }; } - return data; + return data.espProvider || data; } catch (error) { window.lana?.log('Failed to create event. Error:', error); return { ok: false, status: 'Network Error', error: error.message }; From 30b7152380a42eba3c3e058d40f4763e9e599a21 Mon Sep 17 00:00:00 2001 From: Qiyun Dai Date: Wed, 9 Oct 2024 14:34:58 -0500 Subject: [PATCH 3/7] [MWPW-160113] Prevent invalid input caused by leading / trailing spaces (#250) --- .../controllers/event-community-link-component-controller.js | 2 +- .../controllers/registration-details-component-controller.js | 4 ++-- ecc/components/partner-selector/partner-selector.js | 4 ++-- ecc/components/profile/profile.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js b/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js index ac54ce5b..2156bc87 100644 --- a/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js +++ b/ecc/blocks/form-handler/controllers/event-community-link-component-controller.js @@ -7,7 +7,7 @@ export function onSubmit(component, props) { const checkbox = component.querySelector('#checkbox-community'); if (checkbox.checked) { - const communityTopicUrl = component.querySelector('#community-url-details').value; + const communityTopicUrl = component.querySelector('#community-url-details')?.value?.trim(); props.payload = { ...props.payload, communityTopicUrl }; } else { const tempPayload = { ...props.payload }; diff --git a/ecc/blocks/form-handler/controllers/registration-details-component-controller.js b/ecc/blocks/form-handler/controllers/registration-details-component-controller.js index cde7c625..90d12af3 100644 --- a/ecc/blocks/form-handler/controllers/registration-details-component-controller.js +++ b/ecc/blocks/form-handler/controllers/registration-details-component-controller.js @@ -41,10 +41,10 @@ function prefillFields(component, props) { export function onSubmit(component, props) { if (component.closest('.fragment')?.classList.contains('hidden')) return; - const attendeeLimitVal = component.querySelector('#attendee-count-input')?.value; + const attendeeLimitVal = component.querySelector('#attendee-count-input')?.value?.trim(); const allowWaitlisting = component.querySelector('#registration-allow-waitlist')?.checked; const contactHost = component.querySelector('#registration-contact-host')?.checked; - const hostEmail = component.querySelector('#event-host-email-input')?.value; + const hostEmail = component.querySelector('#event-host-email-input')?.value?.trim(); const rsvpDescription = component.querySelector('#rsvp-form-detail-description')?.value; const attendeeLimit = Number.isNaN(+attendeeLimitVal) ? null : +attendeeLimitVal; diff --git a/ecc/components/partner-selector/partner-selector.js b/ecc/components/partner-selector/partner-selector.js index fe1227bf..d0829f6a 100644 --- a/ecc/components/partner-selector/partner-selector.js +++ b/ecc/components/partner-selector/partner-selector.js @@ -149,13 +149,13 @@ export default class PartnerSelector extends LitElement {
{ - this.updatePartner({ name: event.detail.value }); + this.updatePartner({ name: event.detail.value?.trim() }); }} @entry-selected=${this.handleAutocomplete} searchdata=${JSON.stringify(this.seriesPartners)} identifier='sponsorId'>
{ - this.updatePartner({ link: event.target.value }); + this.updatePartner({ link: event.target.value?.trim() }); // FIXME: I really shouldn't need to do this, but the pattern attribute doesn't reset properly. }} ?valid=${this.partner.link?.match(LINK_REGEX)}>
diff --git a/ecc/components/profile/profile.js b/ecc/components/profile/profile.js index df7db933..82498b3a 100644 --- a/ecc/components/profile/profile.js +++ b/ecc/components/profile/profile.js @@ -223,7 +223,7 @@ export class Profile extends LitElement { profile.socialMedia, (socialMedia, index) => html`