Skip to content

Commit

Permalink
feat(account): better image format rules on profile pic creation (#2892)
Browse files Browse the repository at this point in the history
  • Loading branch information
josephmcg committed Apr 20, 2022
1 parent 98210ae commit aaf20c3
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 68 deletions.
4 changes: 2 additions & 2 deletions components/views/user/create/Create.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/>
<div v-if="showCropper" class="cropper-mask" />
<div class="modal-body">
<form v-on:submit="confirm">
<form @submit="confirm">
<div class="custom-modal-content">
<div class="columns">
<div class="column image">
Expand All @@ -23,7 +23,7 @@
class="input-file"
type="file"
@change="selectImage"
accept="image/*"
:accept="acceptableImageFormats"
/>
<InteractablesButton
:action="() => $refs.file.click()"
Expand Down
107 changes: 63 additions & 44 deletions components/views/user/create/Create.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
<template src="./Create.html"></template>

<script lang="ts">
import Vue, { PropType } from 'vue'
import { UserRegistrationData } from '~/types/ui/user'
import { isEmbeddableImage } from '~/utilities/FileType'
import Vue from 'vue'
import { isEmbeddableImage, isHeic } from '~/utilities/FileType'
import blobToBase64 from '~/utilities/BlobToBase64'
import { FILE_TYPE } from '~/libraries/Files/types/file'
import { PlatformTypeEnum } from '~/libraries/Enums/enums'
const convert = require('heic-convert')
export default Vue.extend({
name: 'CreateUser',
props: {
onConfirm: {
type: Function as PropType<(userData: UserRegistrationData) => void>,
required: true,
},
},
data() {
return {
showCropper: false,
Expand All @@ -37,6 +34,27 @@ export default Vue.extend({
}
return true
},
/**
* @method acceptableImageFormats
* @description embeddable types plus HEIC since we can convert
* ios doesn't support advanced <input> accept
* @returns {string} comma separated list of types
*/
acceptableImageFormats(): string {
return this.$envinfo.currentPlatform === PlatformTypeEnum.IOS
? 'image/*'
: [
FILE_TYPE.APNG,
FILE_TYPE.AVIF,
FILE_TYPE.GIF,
FILE_TYPE.JPG,
FILE_TYPE.PNG,
FILE_TYPE.WEBP,
FILE_TYPE.SVG,
FILE_TYPE.HEIC,
FILE_TYPE.HEIF,
].join(',')
},
},
methods: {
/**
Expand All @@ -54,65 +72,66 @@ export default Vue.extend({
* @example
*/
async selectImage(e: Event) {
this.error = ''
this.isLoading = true
const target = e.target as HTMLInputElement
if (target.value === null) {
// make sure there's file data available
if (target.value === null || !target.files?.length) {
this.isLoading = false
return
}
const files = target.files
// only one file allowed on this upload, this is an easier variable name to deal with
let file = target.files[0]
if (!files?.length) {
// stop upload if picture is too large for nsfw scan
if (file.size > this.$Config.nsfwPictureLimit) {
this.error = this.$t('errors.accounts.file_too_large') as string
this.isLoading = false
return
}
const isEmbeddable = await isEmbeddableImage(files[0])
if (!isEmbeddable) {
this.error = this.$t('errors.sign_in.invalid_file') as string
this.isLoading = false
return
// if heic, convert and then set file to png version
if (await isHeic(file)) {
const buffer = new Uint8Array(await file.arrayBuffer())
const oBuffer = await convert({
buffer,
format: 'PNG', // output format
quality: 1,
})
file = new File([oBuffer.buffer], 'profilePic.png', {
type: 'image/png',
})
}
// stop upload if picture is too large for nsfw scan
if (files[0].size > this.$Config.nsfwPictureLimit) {
this.error = this.$t('errors.accounts.file_too_large') as string
// if invalid file type, prevent upload. this needs to be added since safari mobile doesn't fully support <input> accept
if (!(await isEmbeddableImage(file))) {
this.error = this.$t('errors.accounts.invalid_file') as string
this.resetFileInput()
this.isLoading = false
return
}
// stop upload if picture is nsfw
// if nsfw, prevent upload
try {
const nsfw = await this.$Security.isNSFW(files[0])
if (nsfw) {
if (await this.$Security.isNSFW(file)) {
this.error = this.$t('errors.chat.contains_nsfw') as string
this.resetFileInput()
this.isLoading = false
return
}
} catch (err: any) {
this.$Logger.log('error', 'file upload error')
this.error = this.$t(err.message) as string
} catch (e: any) {
this.$Logger.log('error', 'file upload error', e)
this.error = this.$t('errors.accounts.invalid_file') as string
this.resetFileInput()
this.isLoading = false
return
}
this.error = ''
const reader = new FileReader()
reader.onload = (e) => {
if (e.target?.result) {
this.imageUrl = e.target.result.toString()
this.toggleCropper()
this.isLoading = false
}
}
reader.readAsDataURL(files[0])
this.imageUrl = await blobToBase64(file)
this.toggleCropper()
this.isLoading = false
},
/**
* @method resetFileInput
Expand All @@ -136,7 +155,6 @@ export default Vue.extend({
*/
setCroppedImage(image: string) {
this.croppedImage = image
this.resetFileInput()
},
/**
Expand All @@ -147,19 +165,20 @@ export default Vue.extend({
*/
confirm(e: Event) {
e.preventDefault()
if (this.isLoading) return false
if (this.isLoading) {
return false
}
if (!this.accountValidLength) {
this.error = this.$t('user.registration.username_error') as string
return false
}
this.error = ''
this.onConfirm({
this.$emit('confirm', {
username: this.name,
photoHash: this.croppedImage,
status: this.status,
})
return true
},
},
})
Expand Down
22 changes: 4 additions & 18 deletions libraries/Files/TextileFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Bucket } from './remote/textile/Bucket'
import { Config } from '~/config'
import { EnvInfo } from '~/utilities/EnvInfo'
import { mimeType, isHeic, isMimeEmbeddableImage } from '~/utilities/FileType'
import blobToBase64 from '~/utilities/BlobToBase64'
const convert = require('heic-convert')

export class TextileFileSystem extends FilSystem {
Expand Down Expand Up @@ -74,7 +75,7 @@ export class TextileFileSystem extends FilSystem {
if (await this._tooLarge(fileJpg)) {
return
}
return this._fileToData(await skaler(fileJpg, { width: 400 }))
return blobToBase64(await skaler(fileJpg, { width: 400 }))
}

// to catch non-embeddable image files, set blank thumbnail
Expand All @@ -83,27 +84,12 @@ export class TextileFileSystem extends FilSystem {
}
// svg cannot be used with skaler, set thumbnail based on full size
if (type === FILE_TYPE.SVG) {
return this._fileToData(file)
return blobToBase64(file)
}
if (await this._tooLarge(file)) {
return
}
return this._fileToData(await skaler(file, { width: 400 }))
}

/**
* @method _fileToData
* @description convert File to base64 string
* @param {File} file
* @returns {Promise<string>} base64 thumbnail
*/
private _fileToData(file: File): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => resolve(reader.result?.toString() || '')
reader.onerror = (error) => reject(error)
})
return blobToBase64(await skaler(file, { width: 400 }))
}

/**
Expand Down
4 changes: 1 addition & 3 deletions locales/en-US.js
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,7 @@ export default {
mnemonic_not_present: 'Problem with passphrase, please try again.',
file_too_large:
'File is too large, please upload a file smaller than 8MB.',
},
sign_in: {
invalid_file: 'Unable to upload, invalid file.',
invalid_file: 'Please upload a valid image type (jpg, png, svg, etc..)',
},
friends: {
request_already_sent: 'You have already sent a request to this user',
Expand Down
2 changes: 1 addition & 1 deletion pages/auth/register/Register.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="container">
<div class="registration-body">
<UserCreate :onConfirm="confirm" v-if="hasToRegister" />
<UserCreate v-if="hasToRegister" @confirm="confirm" />
<UiLoadersPageLoader
:is-loading="!hasToRegister && !allPrerequisitesReady"
:title="$t('pages.loading.loading')"
Expand Down
14 changes: 14 additions & 0 deletions utilities/BlobToBase64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @method blobToBase64
* @description convert File/Blob to base64 string
* @param {File} file
* @returns {Promise<string>} base64 thumbnail
*/
export default function blobToBase64(file: File): Promise<string> {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => resolve(reader.result?.toString() || '')
reader.onerror = (error) => reject(error)
})
}

0 comments on commit aaf20c3

Please sign in to comment.