summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--package-lock.json13
-rw-r--r--package.json1
-rw-r--r--src/components/ContactDetails/ContactDetailsAvatar.vue86
3 files changed, 89 insertions, 11 deletions
diff --git a/package-lock.json b/package-lock.json
index ddf26a42..cb73e045 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1204,6 +1204,11 @@
"to-fast-properties": "^2.0.0"
}
},
+ "@mattkrick/sanitize-svg": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@mattkrick/sanitize-svg/-/sanitize-svg-0.2.1.tgz",
+ "integrity": "sha512-9T5xb8pq0GLNuKmKbXLvILOi1bQeu9FzAup+dB3zWRgzOVh40yE0YqWY/lrKzBrpj968ZaKTxegTwU1zyRtfBA=="
+ },
"@nextcloud/auth": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-1.2.1.tgz",
@@ -1575,9 +1580,9 @@
"dev": true
},
"acorn": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
- "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
+ "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
"dev": true
},
"acorn-jsx": {
@@ -4800,7 +4805,7 @@
"dependencies": {
"minimist": {
"version": "1.1.3",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz",
"integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=",
"dev": true
}
diff --git a/package.json b/package.json
index 519de6c2..d773a8fd 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"stylelint:fix": "stylelint src --fix"
},
"dependencies": {
+ "@mattkrick/sanitize-svg": "^0.2.1",
"@nextcloud/auth": "^1.2.1",
"@nextcloud/dialogs": "^1.2.1",
"@nextcloud/initial-state": "^1.1.0",
diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue
index b5b2adaf..e2a03b9b 100644
--- a/src/components/ContactDetails/ContactDetailsAvatar.vue
+++ b/src/components/ContactDetails/ContactDetailsAvatar.vue
@@ -91,6 +91,7 @@ import { ActionLink, ActionButton } from '@nextcloud/vue'
import { getFilePickerBuilder } from '@nextcloud/dialogs'
import { generateRemoteUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
+import sanitizeSVG from '@mattkrick/sanitize-svg'
const axios = () => import('axios')
@@ -147,23 +148,93 @@ export default {
if (file && file.size && file.size <= 1 * 1024 * 1024) {
const reader = new FileReader()
const self = this
+ let type = ''
- reader.onload = function(e) {
- // only getting the raw binary base64
- self.setPhoto(reader.result.split(',').pop(), file.type)
+ reader.onloadend = async function(e) {
+ try {
+ // We got an ArrayBuffer, checking the true mime type...
+ if (typeof e.target.result === 'object') {
+ const uint = new Uint8Array(e.target.result)
+ const bytes = []
+ uint.forEach((byte) => {
+ bytes.push(byte.toString(16))
+ })
+ const hex = bytes.join('').toUpperCase()
+
+ if (self.getMimetype(hex).startsWith('image/')) {
+ type = self.getMimetype(hex)
+ // we got a valid image, read it again as base64
+ reader.readAsDataURL(file)
+ return
+ }
+ throw new Error('Wrong image mimetype')
+ }
+
+ // else we got the base64 and we're good to go!
+ const imageBase64 = e.target.result.split(',').pop()
+
+ if (e.target.result.indexOf('image/svg') > -1) {
+ const imageSvg = atob(imageBase64)
+ const cleanSvg = await sanitizeSVG(imageSvg)
+ if (!cleanSvg) {
+ throw new Error('Unsafe svg image')
+ }
+ }
+
+ // All is well! Set the photo
+ self.setPhoto(imageBase64, type)
+ } catch (error) {
+ console.error(error)
+ OC.Notification.showTemporary(t('contacts', 'Invalid image'))
+ } finally {
+ self.resetPicker()
+ }
}
- reader.readAsDataURL(file)
+ // start by reading the magic bytes to detect proper photo mimetype
+ const blob = file.slice(0, 4)
+ reader.readAsArrayBuffer(blob)
} else {
OC.Notification.showTemporary(t('contacts', 'Image is too big (max 1MB).'))
- // reset input
- event.target.value = ''
- this.loading = false
+ this.resetPicker()
}
}
},
/**
+ * Reset image pciker input
+ */
+ resetPicker() {
+ // reset input
+ this.$refs.uploadInput.value = ''
+ this.loading = false
+ },
+
+ /**
+ * Return the mimetype based on the first magix byte
+ *
+ * @param {string} signature the first 4 bytes
+ * @returns {string} the mimetype
+ */
+ getMimetype(signature) {
+ switch (signature) {
+ case '89504E47':
+ return 'image/png'
+ case '47494638':
+ return 'image/gif'
+ case '3C3F786D':
+ case '3C737667':
+ return 'image/svg+xml'
+ case 'FFD8FFDB':
+ case 'FFD8FFE0':
+ case 'FFD8FFE1':
+ return 'image/jpeg'
+ default:
+ return 'application/octet-stream'
+ }
+ },
+
+ /**
* Update the contact photo
*
* @param {String} data the photo as base64 binary string
@@ -269,6 +340,7 @@ export default {
this.updateHeightWidth(this.$refs.img.naturalHeight, this.$refs.img.naturalWidth)
}
},
+
/**
* Updates the current height and width data
* based on the viewer maximum size