Implemented stripping tags from images

... and optionally videos using ffmpeg (still experimental).

Users can choose whether to strip tags of their uploads or not from
the home uploader's Config tab (safe.fiery.me will have it disabled
by default).

The behavior will also be applied to the downloadable ShareX config.

Server owners can choose to force either behavior.

Make sure to add the new config from config.sample.js.

---

Fixed all instances of "e.i." to "e.g.".
My English sucks okay.

Bumped v1 version string.
This commit is contained in:
Bobby Wibowo 2019-11-29 20:42:53 +07:00
parent 337a0a61ff
commit d9ddfe8e9a
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
15 changed files with 160 additions and 24 deletions

View File

@ -39,7 +39,7 @@ Low priority:
* [x] Delete user feature.
* [ ] Bulk delete user feature.
* [ ] Bulk disable user feature.
* [ ] Strip EXIF from images. [#51](https://github.com/BobbyWibowo/lolisafe/issues/51)
* [x] Strip EXIF from images. [#51](https://github.com/BobbyWibowo/lolisafe/issues/51)
* [ ] DMCA request logs (bare text file will do), and link it in FAQ.
This should also include list of files that also had to be manually deleted due to triggering Google's SafeSearch (harmful downloads, etc).

View File

@ -325,7 +325,7 @@ module.exports = {
Cache file identifiers.
They will be used for stricter collision checks, such that a single identifier
may not be used by more than a single file (e.i. if "abcd.jpg" already exists, a new PNG
may not be used by more than a single file (e.g. if "abcd.jpg" already exists, a new PNG
file may not be named as "abcd.png").
If this is enabled, the safe will query files from the database during first launch,
@ -333,11 +333,11 @@ module.exports = {
Its downside is that it will use a bit more memory.
If this is disabled, collision check will become less strict.
As in, the same identifier may be used by multiple different extensions (e.i. if "abcd.jpg"
As in, the same identifier may be used by multiple different extensions (e.g. if "abcd.jpg"
already exists, new files can be possibly be named as "abcd.png", "abcd.mp4", etc).
Its downside will be, in the rare chance that multiple image/video files are sharing the same
identifier, they will end up with the same thumbnail in dashboard, since thumbnails will
only be saved as PNG in storage (e.i. "abcd.jpg" and "abcd.png" will share a single thumbnail
only be saved as PNG in storage (e.g. "abcd.jpg" and "abcd.png" will share a single thumbnail
named "abcd.png" in thumbs directory, in which case, the file that's uploaded the earliest will
be the source for the thumbnail).
@ -369,6 +369,28 @@ module.exports = {
placeholder: null
},
/*
Strip tags (e.g. EXIF).
"default" decides whether to strip tags or not by default,
as the behavior can be configured by users from home uploader's Config tab.
If "force" is set to true, the default behavior will be enforced.
"video" decides whether to also strip tags of vidoe files
(of course only if the default behavior is to strip tags).
However, this also requires ffmpeg (see option's note above),
and still experimental (thus use at your own risk).
NOTE: Other than setting both "default" and "force" to false,
you can also set stripTags itself to any falsy value to completely
disable this feature.
*/
stripTags: {
default: false,
video: false,
force: false
},
/*
Allow users to download a ZIP archive of all files in an album.
The file is generated when the user clicks the download button in the view

View File

@ -26,7 +26,7 @@ const maxFilesPerUpload = 20
const chunkedUploads = Boolean(config.uploads.chunkSize)
const chunksData = {}
// Hard-coded min chunk size of 1 MB (e.i. 50 MB = max 50 chunks)
// Hard-coded min chunk size of 1 MB (e.g. 50 MB = max 50 chunks)
const maxChunksCount = maxSize
const extensionsFilter = Array.isArray(config.extensionsFilter) &&
@ -106,7 +106,7 @@ const executeMulter = multer({
.catch(error => cb(error))
}
// index.extension (e.i. 0, 1, ..., n - will prepend zeros depending on the amount of chunks)
// index.extension (i.e. 0, 1, ..., n - will prepend zeros depending on the amount of chunks)
const digits = req.body.totalchunkcount !== undefined ? `${req.body.totalchunkcount - 1}`.length : 1
const zeros = new Array(digits + 1).join('0')
const name = (zeros + req.body.chunkindex).slice(-digits)
@ -183,6 +183,16 @@ self.parseUploadAge = age => {
return null
}
self.parseStripTags = stripTags => {
if (!config.uploads.stripTags)
return false
if (config.uploads.stripTags.force || stripTags === undefined)
return config.uploads.stripTags.default
return Boolean(parseInt(stripTags))
}
self.upload = async (req, res, next) => {
let user
if (config.private === true) {
@ -273,6 +283,8 @@ self.actuallyUploadFiles = async (req, res, user, albumid, age) => {
if (scanResult) throw scanResult
}
await self.stripTags(req, infoMap)
const result = await self.storeFilesToDb(req, res, user, infoMap)
await self.sendUploadResponse(req, res, result)
}
@ -471,6 +483,8 @@ self.actuallyFinishChunks = async (req, res, user) => {
if (scanResult) throw scanResult
}
await self.stripTags(req, infoMap)
const result = await self.storeFilesToDb(req, res, user, infoMap)
await self.sendUploadResponse(req, res, result)
} catch (error) {
@ -552,10 +566,31 @@ self.scanFiles = async (req, user, infoMap) => {
return results
}
self.stripTags = async (req, infoMap) => {
if (!self.parseStripTags(req.headers.striptags))
return
try {
await Promise.all(infoMap.map(info =>
utils.stripTags(info.data.filename, info.data.extname)
))
} catch (error) {
// Unlink all files when at least one threat is found OR any errors occurred
// Should continue even when encountering errors
await Promise.all(infoMap.map(info =>
utils.unlinkFile(info.data.filename).catch(logger.error)
))
// Re-throw error
throw error
}
}
self.storeFilesToDb = async (req, res, user, infoMap) => {
const files = []
const exists = []
const albumids = []
await Promise.all(infoMap.map(async info => {
// Create hash of the file
const hash = await new Promise((resolve, reject) => {

View File

@ -308,6 +308,52 @@ self.generateThumbs = async (name, extname, force) => {
return true
}
self.stripTags = async (name, extname) => {
const fullpath = path.join(paths.uploads, name)
if (self.imageExts.includes(extname)) {
const tmpfile = path.join(paths.uploads, `tmp-${name}`)
await paths.rename(fullpath, tmpfile)
try {
await sharp(tmpfile)
.toFile(fullpath)
await paths.unlink(tmpfile)
} catch (error) {
await paths.unlink(tmpfile)
// Re-throw error
throw error
}
} else if (config.uploads.stripTags.video && self.videoExts.includes(extname)) {
const tmpfile = path.join(paths.uploads, `tmp-${name}`)
await paths.rename(fullpath, tmpfile)
try {
await new Promise((resolve, reject) => {
ffmpeg(tmpfile)
.output(fullpath)
.outputOptions([
// Experimental.
'-c copy',
'-map_metadata:g -1:g',
'-map_metadata:s:v -1:g',
'-map_metadata:s:a -1:g'
])
.on('error', error => reject(error))
.on('end', () => resolve(true))
.run()
})
await paths.unlink(tmpfile)
} catch (error) {
await paths.unlink(tmpfile)
// Re-throw error
throw error
}
}
return true
}
self.unlinkFile = async (filename, predb) => {
try {
await paths.unlink(path.join(paths.uploads, filename))

2
dist/css/style.css vendored
View File

@ -1,2 +1,2 @@
html{background-color:#000;overflow-y:auto}body{color:#eff0f1;-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}@-webkit-keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}@keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}a{color:#209cee}a:hover{color:#67c3ff}hr{background-color:#585858}.message-body code,code{background-color:#000;border-radius:5px}.title{color:#eff0f1}.subtitle,.subtitle strong{color:#bdc3c7}.input,.select select,.textarea{color:#eff0f1;border-color:#585858;background-color:#000}.input::-moz-placeholder,.textarea::-moz-placeholder{color:#bdc3c7}.input::-webkit-input-placeholder,.textarea::-webkit-input-placeholder{color:#bdc3c7}.input:-moz-placeholder,.textarea:-moz-placeholder{color:#bdc3c7}.input:-ms-input-placeholder,.textarea:-ms-input-placeholder{color:#bdc3c7}.input.is-active,.input.is-focused,.input:active,.input:focus,.input:not([disabled]):hover,.select fieldset:not([disabled]) select:hover,.select select:not([disabled]):hover,.textarea.is-active,.textarea.is-focused,.textarea:active,.textarea:focus,.textarea:not([disabled]):hover,fieldset:not([disabled]) .input:hover,fieldset:not([disabled]) .select select:hover,fieldset:not([disabled]) .textarea:hover{border-color:#209cee}.input[disabled],.select fieldset[disabled] select,.select select[disabled],.textarea[disabled],fieldset[disabled] .input,fieldset[disabled] .select select,fieldset[disabled] .textarea{border-color:#585858;background-color:#2f2f2f}.label{color:#eff0f1;font-weight:400}.help{color:#bdc3c7}.progress{background-color:#585858}.button.is-info.is-hovered [class*=" icon-"]:before,.button.is-info.is-hovered [class^=icon-]:before,.button.is-info:hover [class*=" icon-"]:before,.button.is-info:hover [class^=icon-]:before{fill:#fff}.checkbox:hover,.radio:hover{color:#7f8c8d}.select:not(.is-multiple):not(.is-loading):after,.select:not(.is-multiple):not(.is-loading):hover:after{border-color:#eff0f1}.message{background-color:#2f2f2f}.message-body{color:#eff0f1;border:0}.hero.is-fullheight>.hero-body{min-height:100vh;height:100%}.hero.is-fullheight>.hero-body>.container{width:100%}
html{background-color:#000;overflow-y:auto}body{color:#eff0f1;-webkit-animation:fadeInOpacity .5s;animation:fadeInOpacity .5s}@-webkit-keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}@keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}a{color:#209cee}a:hover{color:#67c3ff}hr{background-color:#585858}.message-body code,code{background-color:#000;border-radius:5px}.title{color:#eff0f1}.subtitle,.subtitle strong{color:#bdc3c7}.input,.select select,.textarea{color:#eff0f1;border-color:#585858;background-color:#000}.input::-moz-placeholder,.textarea::-moz-placeholder{color:#bdc3c7}.input::-webkit-input-placeholder,.textarea::-webkit-input-placeholder{color:#bdc3c7}.input:-moz-placeholder,.textarea:-moz-placeholder{color:#bdc3c7}.input:-ms-input-placeholder,.textarea:-ms-input-placeholder{color:#bdc3c7}.input.is-active,.input.is-focused,.input:active,.input:focus,.input:not([disabled]):hover,.select fieldset:not([disabled]) select:hover,.select select:not([disabled]):hover,.textarea.is-active,.textarea.is-focused,.textarea:active,.textarea:focus,.textarea:not([disabled]):hover,fieldset:not([disabled]) .input:hover,fieldset:not([disabled]) .select select:hover,fieldset:not([disabled]) .textarea:hover{border-color:#209cee}.input[disabled],.select fieldset[disabled] select,.select select[disabled],.textarea[disabled],fieldset[disabled] .input,fieldset[disabled] .select select,fieldset[disabled] .textarea{border-color:#585858;background-color:#2f2f2f}.label{color:#eff0f1;font-weight:400}.help{color:#bdc3c7}.progress{background-color:#585858}.button.is-info.is-hovered [class*=" icon-"]:before,.button.is-info.is-hovered [class^=icon-]:before,.button.is-info:hover [class*=" icon-"]:before,.button.is-info:hover [class^=icon-]:before{fill:#fff}.checkbox:hover,.radio:hover{color:#7f8c8d}.select:not(.is-multiple):not(.is-loading):after,.select:not(.is-multiple):not(.is-loading):hover:after{border-color:#eff0f1}.select select[disabled]:hover,fieldset[disabled] .select select:hover{border-color:#585858}.message{background-color:#2f2f2f}.message-body{color:#eff0f1;border:0}.hero.is-fullheight>.hero-body{min-height:100vh;height:100%}.hero.is-fullheight>.hero-body>.container{width:100%}
/*# sourceMappingURL=style.css.map */

View File

@ -1 +1 @@
{"version":3,"sources":["css/style.css"],"names":[],"mappings":"AAAA,KACE,qBAAsB,CACtB,eACF,CAEA,KACE,aAAc,CACd,mCAA4B,CAA5B,2BACF,CAEA,iCACE,GACE,SACF,CAEA,GACE,SACF,CACF,CAEA,yBACE,GACE,SACF,CAEA,GACE,SACF,CACF,CAEA,EACE,aACF,CAEA,QACE,aACF,CAEA,GACE,wBACF,CAEA,wBAEE,qBAAsB,CACtB,iBACF,CAEA,OACE,aACF,CAMA,2BACE,aACF,CAEA,gCAGE,aAAc,CACd,oBAAqB,CACrB,qBACF,CAEA,qDAEE,aACF,CAEA,uEAEE,aACF,CAEA,mDAEE,aACF,CAEA,6DAEE,aACF,CAYA,qZAQE,oBACF,CAEA,yLAOE,oBAAqB,CACrB,wBACF,CAEA,OACE,aAAc,CACd,eACF,CAEA,MACE,aACF,CAEA,UACE,wBACF,CAEA,gMAIE,SACF,CAEA,6BAEE,aACF,CAMA,wGACE,oBACF,CAEA,SACE,wBACF,CAEA,cACE,aAAc,CACd,QACF,CAGA,+BACE,gBAAiB,CACjB,WACF,CAGA,0CACE,UACF","file":"style.css","sourcesContent":["html {\n background-color: #000;\n overflow-y: auto\n}\n\nbody {\n color: #eff0f1;\n animation: fadeInOpacity 0.5s\n}\n\n@-webkit-keyframes fadeInOpacity {\n 0% {\n opacity: 0\n }\n\n 100% {\n opacity: 1\n }\n}\n\n@keyframes fadeInOpacity {\n 0% {\n opacity: 0\n }\n\n 100% {\n opacity: 1\n }\n}\n\na {\n color: #209cee\n}\n\na:hover {\n color: #67c3ff\n}\n\nhr {\n background-color: #585858\n}\n\ncode,\n.message-body code {\n background-color: #000;\n border-radius: 5px\n}\n\n.title {\n color: #eff0f1\n}\n\n.subtitle {\n color: #bdc3c7\n}\n\n.subtitle strong {\n color: #bdc3c7\n}\n\n.input,\n.select select,\n.textarea {\n color: #eff0f1;\n border-color: #585858;\n background-color: #000\n}\n\n.input::-moz-placeholder,\n.textarea::-moz-placeholder {\n color: #bdc3c7\n}\n\n.input::-webkit-input-placeholder,\n.textarea::-webkit-input-placeholder {\n color: #bdc3c7\n}\n\n.input:-moz-placeholder,\n.textarea:-moz-placeholder {\n color: #bdc3c7\n}\n\n.input:-ms-input-placeholder,\n.textarea:-ms-input-placeholder {\n color: #bdc3c7\n}\n\n.input:not([disabled]):hover,\n.select fieldset:not([disabled]) select:hover,\n.select select:not([disabled]):hover,\n.textarea:not([disabled]):hover,\nfieldset:not([disabled]) .input:hover,\nfieldset:not([disabled]) .select select:hover,\nfieldset:not([disabled]) .textarea:hover {\n border-color: #209cee\n}\n\n.input.is-active,\n.input.is-focused,\n.input:active,\n.input:focus,\n.textarea.is-active,\n.textarea.is-focused,\n.textarea:active,\n.textarea:focus {\n border-color: #209cee\n}\n\n.input[disabled],\n.select fieldset[disabled] select,\n.select select[disabled],\n.textarea[disabled],\nfieldset[disabled] .input,\nfieldset[disabled] .select select,\nfieldset[disabled] .textarea {\n border-color: #585858;\n background-color: #2f2f2f\n}\n\n.label {\n color: #eff0f1;\n font-weight: normal\n}\n\n.help {\n color: #bdc3c7\n}\n\n.progress {\n background-color: #585858\n}\n\n.button.is-info.is-hovered [class^=\"icon-\"]::before,\n.button.is-info.is-hovered [class*=\" icon-\"]::before,\n.button.is-info:hover [class^=\"icon-\"]::before,\n.button.is-info:hover [class*=\" icon-\"]::before {\n fill: #fff\n}\n\n.checkbox:hover,\n.radio:hover {\n color: #7f8c8d\n}\n\n.select:not(.is-multiple):not(.is-loading)::after {\n border-color: #eff0f1\n}\n\n.select:not(.is-multiple):not(.is-loading):hover::after {\n border-color: #eff0f1\n}\n\n.message {\n background-color: #2f2f2f\n}\n\n.message-body {\n color: #eff0f1;\n border: 0\n}\n\n/* https://github.com/philipwalton/flexbugs#flexbug-3 */\n.hero.is-fullheight > .hero-body {\n min-height: 100vh;\n height: 100%\n}\n\n/* https://github.com/philipwalton/flexbugs#flexbug-2 */\n.hero.is-fullheight > .hero-body > .container {\n width: 100%\n}\n"]}
{"version":3,"sources":["css/style.css"],"names":[],"mappings":"AAAA,KACE,qBAAsB,CACtB,eACF,CAEA,KACE,aAAc,CACd,mCAA4B,CAA5B,2BACF,CAEA,iCACE,GACE,SACF,CAEA,GACE,SACF,CACF,CAEA,yBACE,GACE,SACF,CAEA,GACE,SACF,CACF,CAEA,EACE,aACF,CAEA,QACE,aACF,CAEA,GACE,wBACF,CAEA,wBAEE,qBAAsB,CACtB,iBACF,CAEA,OACE,aACF,CAMA,2BACE,aACF,CAEA,gCAGE,aAAc,CACd,oBAAqB,CACrB,qBACF,CAEA,qDAEE,aACF,CAEA,uEAEE,aACF,CAEA,mDAEE,aACF,CAEA,6DAEE,aACF,CAYA,qZAQE,oBACF,CAEA,yLAOE,oBAAqB,CACrB,wBACF,CAEA,OACE,aAAc,CACd,eACF,CAEA,MACE,aACF,CAEA,UACE,wBACF,CAEA,gMAIE,SACF,CAEA,6BAEE,aACF,CAMA,wGACE,oBACF,CAEA,uEAEE,oBACF,CAEA,SACE,wBACF,CAEA,cACE,aAAc,CACd,QACF,CAGA,+BACE,gBAAiB,CACjB,WACF,CAGA,0CACE,UACF","file":"style.css","sourcesContent":["html {\n background-color: #000;\n overflow-y: auto\n}\n\nbody {\n color: #eff0f1;\n animation: fadeInOpacity 0.5s\n}\n\n@-webkit-keyframes fadeInOpacity {\n 0% {\n opacity: 0\n }\n\n 100% {\n opacity: 1\n }\n}\n\n@keyframes fadeInOpacity {\n 0% {\n opacity: 0\n }\n\n 100% {\n opacity: 1\n }\n}\n\na {\n color: #209cee\n}\n\na:hover {\n color: #67c3ff\n}\n\nhr {\n background-color: #585858\n}\n\ncode,\n.message-body code {\n background-color: #000;\n border-radius: 5px\n}\n\n.title {\n color: #eff0f1\n}\n\n.subtitle {\n color: #bdc3c7\n}\n\n.subtitle strong {\n color: #bdc3c7\n}\n\n.input,\n.select select,\n.textarea {\n color: #eff0f1;\n border-color: #585858;\n background-color: #000\n}\n\n.input::-moz-placeholder,\n.textarea::-moz-placeholder {\n color: #bdc3c7\n}\n\n.input::-webkit-input-placeholder,\n.textarea::-webkit-input-placeholder {\n color: #bdc3c7\n}\n\n.input:-moz-placeholder,\n.textarea:-moz-placeholder {\n color: #bdc3c7\n}\n\n.input:-ms-input-placeholder,\n.textarea:-ms-input-placeholder {\n color: #bdc3c7\n}\n\n.input:not([disabled]):hover,\n.select fieldset:not([disabled]) select:hover,\n.select select:not([disabled]):hover,\n.textarea:not([disabled]):hover,\nfieldset:not([disabled]) .input:hover,\nfieldset:not([disabled]) .select select:hover,\nfieldset:not([disabled]) .textarea:hover {\n border-color: #209cee\n}\n\n.input.is-active,\n.input.is-focused,\n.input:active,\n.input:focus,\n.textarea.is-active,\n.textarea.is-focused,\n.textarea:active,\n.textarea:focus {\n border-color: #209cee\n}\n\n.input[disabled],\n.select fieldset[disabled] select,\n.select select[disabled],\n.textarea[disabled],\nfieldset[disabled] .input,\nfieldset[disabled] .select select,\nfieldset[disabled] .textarea {\n border-color: #585858;\n background-color: #2f2f2f\n}\n\n.label {\n color: #eff0f1;\n font-weight: normal\n}\n\n.help {\n color: #bdc3c7\n}\n\n.progress {\n background-color: #585858\n}\n\n.button.is-info.is-hovered [class^=\"icon-\"]::before,\n.button.is-info.is-hovered [class*=\" icon-\"]::before,\n.button.is-info:hover [class^=\"icon-\"]::before,\n.button.is-info:hover [class*=\" icon-\"]::before {\n fill: #fff\n}\n\n.checkbox:hover,\n.radio:hover {\n color: #7f8c8d\n}\n\n.select:not(.is-multiple):not(.is-loading)::after {\n border-color: #eff0f1\n}\n\n.select:not(.is-multiple):not(.is-loading):hover::after {\n border-color: #eff0f1\n}\n\n.select select[disabled]:hover,\nfieldset[disabled] .select select:hover {\n border-color: #585858\n}\n\n.message {\n background-color: #2f2f2f\n}\n\n.message-body {\n color: #eff0f1;\n border: 0\n}\n\n/* https://github.com/philipwalton/flexbugs#flexbug-3 */\n.hero.is-fullheight > .hero-body {\n min-height: 100vh;\n height: 100%\n}\n\n/* https://github.com/philipwalton/flexbugs#flexbug-2 */\n.hero.is-fullheight > .hero-body > .container {\n width: 100%\n}\n"]}

2
dist/js/home.js vendored

File diff suppressed because one or more lines are too long

2
dist/js/home.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
lsKeys.siBytes="siBytes",page.prepareShareX=function(){var e=page.token?{token:page.token||"",albumid:page.album||""}:{};e.filelength=page.fileLength||"",e.age=page.uploadAge||"";for(var t=[],a=Object.keys(e),n=0;n<a.length;n++)t.push(' "'+a[n]+'": "'+e[a[n]]+'"');var o=(location.hostname+location.pathname).replace(/\/(dashboard)?$/,""),r=o.replace(/\//g,"_"),i=document.querySelector("#ShareX"),l='{\n "Name": "'+r+'",\n "DestinationType": "ImageUploader, FileUploader",\n "RequestMethod": "POST",\n "RequestURL": "'+location.protocol+"//"+o+'/api/upload",\n "Headers": {\n'+t.join(",\n")+'\n },\n "Body": "MultipartFormData",\n "FileFormName": "files[]",\n "URL": "$json:files[0].url$",\n "ThumbnailURL": "$json:files[0].url$"\n}',s=new Blob([l],{type:"application/octet-binary"});i.setAttribute("href",URL.createObjectURL(s)),i.setAttribute("download",r+".sxcu")},page.getPrettyDate=function(e){return e.getFullYear()+"-"+(e.getMonth()<9?"0":"")+(e.getMonth()+1)+"-"+(e.getDate()<10?"0":"")+e.getDate()+" "+(e.getHours()<10?"0":"")+e.getHours()+":"+(e.getMinutes()<10?"0":"")+e.getMinutes()+":"+(e.getSeconds()<10?"0":"")+e.getSeconds()},page.getPrettyBytes=function(e){if("number"!=typeof e&&!isFinite(e))return e;var t="0"!==localStorage[lsKeys.siBytes],a=e<0?"-":"",n=t?1e3:1024;if(a&&(e=-e),e<n)return""+a+e+" B";var o=Math.min(Math.floor(Math.log(e)*Math.LOG10E/3),8);return""+a+Number((e/Math.pow(n,o)).toPrecision(3))+" "+((t?"kMGTPEZY":"KMGTPEZY").charAt(o-1)+(t?"":"i"))+"B"};
lsKeys.siBytes="siBytes",page.prepareShareX=function(){var e=page.token?{token:page.token||"",albumid:page.album||""}:{};e.filelength=page.fileLength||"",e.age=page.uploadAge||"",e.striptags=page.stripTags||"";for(var t=[],a=Object.keys(e),n=0;n<a.length;n++)t.push(' "'+a[n]+'": "'+e[a[n]]+'"');var o=(location.hostname+location.pathname).replace(/\/(dashboard)?$/,""),r=o.replace(/\//g,"_"),i=document.querySelector("#ShareX"),s='{\n "Name": "'+r+'",\n "DestinationType": "ImageUploader, FileUploader",\n "RequestMethod": "POST",\n "RequestURL": "'+location.protocol+"//"+o+'/api/upload",\n "Headers": {\n'+t.join(",\n")+'\n },\n "Body": "MultipartFormData",\n "FileFormName": "files[]",\n "URL": "$json:files[0].url$",\n "ThumbnailURL": "$json:files[0].url$"\n}',l=new Blob([s],{type:"application/octet-binary"});i.setAttribute("href",URL.createObjectURL(l)),i.setAttribute("download",r+".sxcu")},page.getPrettyDate=function(e){return e.getFullYear()+"-"+(e.getMonth()<9?"0":"")+(e.getMonth()+1)+"-"+(e.getDate()<10?"0":"")+e.getDate()+" "+(e.getHours()<10?"0":"")+e.getHours()+":"+(e.getMinutes()<10?"0":"")+e.getMinutes()+":"+(e.getSeconds()<10?"0":"")+e.getSeconds()},page.getPrettyBytes=function(e){if("number"!=typeof e&&!isFinite(e))return e;var t="0"!==localStorage[lsKeys.siBytes],a=e<0?"-":"",n=t?1e3:1024;if(a&&(e=-e),e<n)return""+a+e+" B";var o=Math.min(Math.floor(Math.log(e)*Math.LOG10E/3),8);return""+a+Number((e/Math.pow(n,o)).toPrecision(3))+" "+((t?"kMGTPEZY":"KMGTPEZY").charAt(o-1)+(t?"":"i"))+"B"};
//# sourceMappingURL=utils.js.map

View File

@ -1 +1 @@
{"version":3,"sources":["utils.js"],"names":["lsKeys","siBytes","page","prepareShareX","const","values","token","albumid","album","filelength","fileLength","age","uploadAge","headers","keys","Object","i","length","push","origin","location","hostname","pathname","replace","originClean","sharexElement","document","querySelector","sharexFile","join","sharexBlob","Blob","type","setAttribute","URL","createObjectURL","getPrettyDate","date","getFullYear","getMonth","getDate","getHours","getMinutes","getSeconds","getPrettyBytes","num","isFinite","si","localStorage","neg","scale","exponent","Math","min","floor","log","LOG10E","Number","pow","toPrecision","charAt"],"mappings":"AAGAA,OAAOC,QAAU,UAEjBC,KAAKC,cAAa,WAChBC,IAAMC,EAASH,KAAKI,MAAQ,CAC1BA,MAAOJ,KAAKI,OAAS,GACrBC,QAASL,KAAKM,OAAS,IACrB,GACJH,EAAOI,WAAaP,KAAKQ,YAAc,GACvCL,EAAOM,IAAMT,KAAKU,WAAa,GAI/B,IAFAR,IAAMS,EAAU,GACVC,EAAOC,OAAOD,KAAKT,GAChBW,EAAI,EAAGA,EAAIF,EAAKG,OAAQD,IAE/BH,EAAQK,KAAK,QAAQJ,EAAKE,GAAE,OAAOX,EAAOS,EAAKE,IAAG,KAEpDZ,IAAMe,GAAUC,SAASC,SAAWD,SAASE,UAAUC,QAAQ,kBAAmB,IAC5EC,EAAcL,EAAOI,QAAQ,MAAO,KAEpCE,EAAgBC,SAASC,cAAc,WACvCC,EAAa,iBACRJ,EAAW,yGAGLJ,SAAS,SAAQ,KAAKD,EAAM,kCAE7CN,EAAQgB,KAAK,OAAM,oJAQbC,EAAa,IAAIC,KAAK,CAACH,GAAa,CAAEI,KAAM,6BAElDP,EAAcQ,aAAa,OAAQC,IAAIC,gBAAgBL,IACvDL,EAAcQ,aAAa,WAAeT,EAAW,UAGvDtB,KAAKkC,cAAa,SAAGC,GACnB,OAAOA,EAAKC,cAAgB,KACzBD,EAAKE,WAAa,EAAI,IAAM,KAC5BF,EAAKE,WAAa,GAAK,KACvBF,EAAKG,UAAY,GAAK,IAAM,IAC7BH,EAAKG,UAAY,KAChBH,EAAKI,WAAa,GAAK,IAAM,IAC9BJ,EAAKI,WAAa,KACjBJ,EAAKK,aAAe,GAAK,IAAM,IAChCL,EAAKK,aAAe,KACnBL,EAAKM,aAAe,GAAK,IAAM,IAChCN,EAAKM,cAGTzC,KAAK0C,eAAc,SAAGC,GAGpB,GAAmB,iBAARA,IAAqBC,SAASD,GAAM,OAAOA,EAEtDzC,IAAM2C,EAAsC,MAAjCC,aAAahD,OAAOC,SACzBgD,EAAMJ,EAAM,EAAI,IAAM,GACtBK,EAAQH,EAAK,IAAO,KAE1B,GADIE,IAAKJ,GAAOA,GACZA,EAAMK,EAAO,MAAO,GAAGD,EAAMJ,EAAG,KAEpCzC,IAAM+C,EAAWC,KAAKC,IAAID,KAAKE,MAAOF,KAAKG,IAAIV,GAAOO,KAAKI,OAAU,GAAI,GAGzE,MAAO,GAAGP,EAFKQ,QAAQZ,EAAMO,KAAKM,IAAIR,EAAOC,IAAWQ,YAAY,IAE9C,MADTZ,EAAK,WAAa,YAAYa,OAAOT,EAAW,IAAMJ,EAAK,GAAK,MAChD","file":"utils.js","sourcesContent":["/* global lsKeys, page */\n\n// keys for localStorage\nlsKeys.siBytes = 'siBytes'\n\npage.prepareShareX = () => {\n const values = page.token ? {\n token: page.token || '',\n albumid: page.album || ''\n } : {}\n values.filelength = page.fileLength || ''\n values.age = page.uploadAge || ''\n\n const headers = []\n const keys = Object.keys(values)\n for (let i = 0; i < keys.length; i++)\n // Pad by 4 space\n headers.push(` \"${keys[i]}\": \"${values[keys[i]]}\"`)\n\n const origin = (location.hostname + location.pathname).replace(/\\/(dashboard)?$/, '')\n const originClean = origin.replace(/\\//g, '_')\n\n const sharexElement = document.querySelector('#ShareX')\n const sharexFile = `{\n \"Name\": \"${originClean}\",\n \"DestinationType\": \"ImageUploader, FileUploader\",\n \"RequestMethod\": \"POST\",\n \"RequestURL\": \"${location.protocol}//${origin}/api/upload\",\n \"Headers\": {\n${headers.join(',\\n')}\n },\n \"Body\": \"MultipartFormData\",\n \"FileFormName\": \"files[]\",\n \"URL\": \"$json:files[0].url$\",\n \"ThumbnailURL\": \"$json:files[0].url$\"\n}`\n\n const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' })\n /* eslint-disable-next-line compat/compat */\n sharexElement.setAttribute('href', URL.createObjectURL(sharexBlob))\n sharexElement.setAttribute('download', `${originClean}.sxcu`)\n}\n\npage.getPrettyDate = date => {\n return date.getFullYear() + '-' +\n (date.getMonth() < 9 ? '0' : '') + // month's index starts from zero\n (date.getMonth() + 1) + '-' +\n (date.getDate() < 10 ? '0' : '') +\n date.getDate() + ' ' +\n (date.getHours() < 10 ? '0' : '') +\n date.getHours() + ':' +\n (date.getMinutes() < 10 ? '0' : '') +\n date.getMinutes() + ':' +\n (date.getSeconds() < 10 ? '0' : '') +\n date.getSeconds()\n}\n\npage.getPrettyBytes = num => {\n // MIT License\n // Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n if (typeof num !== 'number' && !isFinite(num)) return num\n\n const si = localStorage[lsKeys.siBytes] !== '0'\n const neg = num < 0 ? '-' : ''\n const scale = si ? 1000 : 1024\n if (neg) num = -num\n if (num < scale) return `${neg}${num} B`\n\n const exponent = Math.min(Math.floor((Math.log(num) * Math.LOG10E) / 3), 8) // 8 is count of KMGTPEZY\n const numStr = Number((num / Math.pow(scale, exponent)).toPrecision(3))\n const pre = (si ? 'kMGTPEZY' : 'KMGTPEZY').charAt(exponent - 1) + (si ? '' : 'i')\n return `${neg}${numStr} ${pre}B`\n}\n"]}
{"version":3,"sources":["utils.js"],"names":["lsKeys","siBytes","page","prepareShareX","const","values","token","albumid","album","filelength","fileLength","age","uploadAge","striptags","stripTags","headers","keys","Object","i","length","push","origin","location","hostname","pathname","replace","originClean","sharexElement","document","querySelector","sharexFile","join","sharexBlob","Blob","type","setAttribute","URL","createObjectURL","getPrettyDate","date","getFullYear","getMonth","getDate","getHours","getMinutes","getSeconds","getPrettyBytes","num","isFinite","si","localStorage","neg","scale","exponent","Math","min","floor","log","LOG10E","Number","pow","toPrecision","charAt"],"mappings":"AAGAA,OAAOC,QAAU,UAEjBC,KAAKC,cAAa,WAChBC,IAAMC,EAASH,KAAKI,MAAQ,CAC1BA,MAAOJ,KAAKI,OAAS,GACrBC,QAASL,KAAKM,OAAS,IACrB,GACJH,EAAOI,WAAaP,KAAKQ,YAAc,GACvCL,EAAOM,IAAMT,KAAKU,WAAa,GAC/BP,EAAOQ,UAAYX,KAAKY,WAAa,GAIrC,IAFAV,IAAMW,EAAU,GACVC,EAAOC,OAAOD,KAAKX,GAChBa,EAAI,EAAGA,EAAIF,EAAKG,OAAQD,IAE/BH,EAAQK,KAAK,QAAQJ,EAAKE,GAAE,OAAOb,EAAOW,EAAKE,IAAG,KAEpDd,IAAMiB,GAAUC,SAASC,SAAWD,SAASE,UAAUC,QAAQ,kBAAmB,IAC5EC,EAAcL,EAAOI,QAAQ,MAAO,KAEpCE,EAAgBC,SAASC,cAAc,WACvCC,EAAa,iBACRJ,EAAW,yGAGLJ,SAAS,SAAQ,KAAKD,EAAM,kCAE7CN,EAAQgB,KAAK,OAAM,oJAQbC,EAAa,IAAIC,KAAK,CAACH,GAAa,CAAEI,KAAM,6BAElDP,EAAcQ,aAAa,OAAQC,IAAIC,gBAAgBL,IACvDL,EAAcQ,aAAa,WAAeT,EAAW,UAGvDxB,KAAKoC,cAAa,SAAGC,GACnB,OAAOA,EAAKC,cAAgB,KACzBD,EAAKE,WAAa,EAAI,IAAM,KAC5BF,EAAKE,WAAa,GAAK,KACvBF,EAAKG,UAAY,GAAK,IAAM,IAC7BH,EAAKG,UAAY,KAChBH,EAAKI,WAAa,GAAK,IAAM,IAC9BJ,EAAKI,WAAa,KACjBJ,EAAKK,aAAe,GAAK,IAAM,IAChCL,EAAKK,aAAe,KACnBL,EAAKM,aAAe,GAAK,IAAM,IAChCN,EAAKM,cAGT3C,KAAK4C,eAAc,SAAGC,GAGpB,GAAmB,iBAARA,IAAqBC,SAASD,GAAM,OAAOA,EAEtD3C,IAAM6C,EAAsC,MAAjCC,aAAalD,OAAOC,SACzBkD,EAAMJ,EAAM,EAAI,IAAM,GACtBK,EAAQH,EAAK,IAAO,KAE1B,GADIE,IAAKJ,GAAOA,GACZA,EAAMK,EAAO,MAAO,GAAGD,EAAMJ,EAAG,KAEpC3C,IAAMiD,EAAWC,KAAKC,IAAID,KAAKE,MAAOF,KAAKG,IAAIV,GAAOO,KAAKI,OAAU,GAAI,GAGzE,MAAO,GAAGP,EAFKQ,QAAQZ,EAAMO,KAAKM,IAAIR,EAAOC,IAAWQ,YAAY,IAE9C,MADTZ,EAAK,WAAa,YAAYa,OAAOT,EAAW,IAAMJ,EAAK,GAAK,MAChD","file":"utils.js","sourcesContent":["/* global lsKeys, page */\n\n// keys for localStorage\nlsKeys.siBytes = 'siBytes'\n\npage.prepareShareX = () => {\n const values = page.token ? {\n token: page.token || '',\n albumid: page.album || ''\n } : {}\n values.filelength = page.fileLength || ''\n values.age = page.uploadAge || ''\n values.striptags = page.stripTags || ''\n\n const headers = []\n const keys = Object.keys(values)\n for (let i = 0; i < keys.length; i++)\n // Pad by 4 space\n headers.push(` \"${keys[i]}\": \"${values[keys[i]]}\"`)\n\n const origin = (location.hostname + location.pathname).replace(/\\/(dashboard)?$/, '')\n const originClean = origin.replace(/\\//g, '_')\n\n const sharexElement = document.querySelector('#ShareX')\n const sharexFile = `{\n \"Name\": \"${originClean}\",\n \"DestinationType\": \"ImageUploader, FileUploader\",\n \"RequestMethod\": \"POST\",\n \"RequestURL\": \"${location.protocol}//${origin}/api/upload\",\n \"Headers\": {\n${headers.join(',\\n')}\n },\n \"Body\": \"MultipartFormData\",\n \"FileFormName\": \"files[]\",\n \"URL\": \"$json:files[0].url$\",\n \"ThumbnailURL\": \"$json:files[0].url$\"\n}`\n\n const sharexBlob = new Blob([sharexFile], { type: 'application/octet-binary' })\n /* eslint-disable-next-line compat/compat */\n sharexElement.setAttribute('href', URL.createObjectURL(sharexBlob))\n sharexElement.setAttribute('download', `${originClean}.sxcu`)\n}\n\npage.getPrettyDate = date => {\n return date.getFullYear() + '-' +\n (date.getMonth() < 9 ? '0' : '') + // month's index starts from zero\n (date.getMonth() + 1) + '-' +\n (date.getDate() < 10 ? '0' : '') +\n date.getDate() + ' ' +\n (date.getHours() < 10 ? '0' : '') +\n date.getHours() + ':' +\n (date.getMinutes() < 10 ? '0' : '') +\n date.getMinutes() + ':' +\n (date.getSeconds() < 10 ? '0' : '') +\n date.getSeconds()\n}\n\npage.getPrettyBytes = num => {\n // MIT License\n // Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n if (typeof num !== 'number' && !isFinite(num)) return num\n\n const si = localStorage[lsKeys.siBytes] !== '0'\n const neg = num < 0 ? '-' : ''\n const scale = si ? 1000 : 1024\n if (neg) num = -num\n if (num < scale) return `${neg}${num} B`\n\n const exponent = Math.min(Math.floor((Math.log(num) * Math.LOG10E) / 3), 8) // 8 is count of KMGTPEZY\n const numStr = Number((num / Math.pow(scale, exponent)).toPrecision(3))\n const pre = (si ? 'kMGTPEZY' : 'KMGTPEZY').charAt(exponent - 1) + (si ? '' : 'i')\n return `${neg}${numStr} ${pre}B`\n}\n"]}

View File

@ -13,7 +13,8 @@ routes.get('/check', (req, res, next) => {
maxSize: config.uploads.maxSize,
chunkSize: config.uploads.chunkSize,
temporaryUploadAges: config.uploads.temporaryUploadAges,
fileIdentifierLength: config.uploads.fileIdentifierLength
fileIdentifierLength: config.uploads.fileIdentifierLength,
stripTags: config.uploads.stripTags
})
})

View File

@ -151,6 +151,11 @@ fieldset[disabled] .textarea {
border-color: #eff0f1
}
.select select[disabled]:hover,
fieldset[disabled] .select select:hover {
border-color: #585858
}
.message {
background-color: #2f2f2f
}

View File

@ -7,7 +7,8 @@ const lsKeys = {
uploadsHistoryOrder: 'uploadsHistoryOrder',
previewImages: 'previewImages',
fileLength: 'fileLength',
uploadAge: 'uploadAge'
uploadAge: 'uploadAge',
stripTags: 'stripTags'
}
const page = {
@ -21,6 +22,7 @@ const page = {
chunkSize: null,
temporaryUploadAges: null,
fileIdentifierLength: null,
stripTagsConfig: null,
// store album id that will be used with upload requests
album: null,
@ -137,6 +139,7 @@ page.checkIfPublic = () => {
page.chunkSize = parseInt(response.data.chunkSize)
page.temporaryUploadAges = response.data.temporaryUploadAges
page.fileIdentifierLength = response.data.fileIdentifierLength
page.stripTagsConfig = response.data.stripTags
return page.preparePage()
}).catch(page.onInitError)
}
@ -343,6 +346,7 @@ page.prepareDropzone = () => {
if (page.album !== null) xhr.setRequestHeader('albumid', page.album)
if (page.fileLength !== null) xhr.setRequestHeader('filelength', page.fileLength)
if (page.uploadAge !== null) xhr.setRequestHeader('age', page.uploadAge)
if (page.stripTags !== null) xhr.setRequestHeader('striptags', page.stripTags)
}
if (!file.upload.chunked)
@ -430,7 +434,10 @@ page.prepareDropzone = () => {
}]
}, {
headers: {
token: page.token
token: page.token,
// Unlike the options above (e.g. albumid, filelength, etc.),
// strip tags can not yet be configured per file with this API
striptags: page.stripTags
}
}).catch(error => {
// Format error for display purpose
@ -581,7 +588,7 @@ page.updateTemplate = (file, response) => {
img.classList.remove('is-hidden')
img.onerror = event => {
// Hide image elements that fail to load
// Consequently include WEBP in browsers that do not have WEBP support (e.i. IE)
// Consequently include WEBP in browsers that do not have WEBP support (e.g. IE)
event.currentTarget.classList.add('is-hidden')
page.updateTemplateIcon(file.previewElement, 'icon-picture')
}
@ -708,7 +715,18 @@ page.prepareUploadConfig = () => {
display: temporaryUploadAges,
label: 'Upload age',
select: [],
help: 'This allows your files to automatically be deleted after a certain period of time.'
help: 'Whether to automatically delete your uploads after a certain amount of time.'
},
stripTags: {
display: page.stripTagsConfig,
label: 'Strip tags',
select: page.stripTagsConfig ? [
{ value: page.stripTagsConfig.default ? 'default' : '1', text: 'Yes' },
{ value: page.stripTagsConfig.default ? '0' : 'default', text: 'No' }
] : null,
help: `Whether to strip tags (e.g. EXIF) from your uploads.<br>
This only applies to regular image${page.stripTagsConfig && page.stripTagsConfig.video ? ' and video' : ''} uploads (i.e. not URL uploads).`,
disabled: page.stripTagsConfig && page.stripTagsConfig.force
},
chunkSize: {
display: !isNaN(page.chunkSize),
@ -736,8 +754,8 @@ page.prepareUploadConfig = () => {
{ value: 'default', text: 'Older files on top' },
{ value: '0', text: 'Newer files on top' }
],
help: `Newer files on top will use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction#Accessibility_Concerns" target="_blank" rel="noopener">a CSS technique</a>.<br>
Trying to select their texts manually from top to bottom will end up selecting the texts from bottom to top instead.`,
help: `"Newer files on top" will use a CSS technique, which unfortunately come with <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction#Accessibility_Concerns" target="_blank" rel="noopener">some undesirable side effects</a>.<br>
This also affects text selection, such as when trying to select text from top to bottom will result in them being selected from bottom to top instead, and vice versa.`,
valueHandler (value) {
if (value === '0') {
const uploadFields = document.querySelectorAll('.tab-content > .uploads')
@ -807,7 +825,11 @@ page.prepareUploadConfig = () => {
if (!isNaN(parsed))
value = parsed
} else {
value = localStorage[lsKeys[key]]
const stored = localStorage[lsKeys[key]]
if (Array.isArray(conf.select))
value = conf.select.find(sel => sel.value === stored) ? stored : undefined
else
value = stored
}
// If valueHandler function exists, defer to the function,
@ -826,7 +848,8 @@ page.prepareUploadConfig = () => {
const opts = []
for (let j = 0; j < conf.select.length; j++) {
const opt = conf.select[j]
const selected = value && (opt.value === String(value))
const selected = (value && (opt.value === String(value))) ||
(value === undefined && opt.value === 'default')
opts.push(`
<option value="${opt.value}"${selected ? ' selected' : ''}>
${opt.text}${opt.value === 'default' ? ' (default)' : ''}
@ -857,8 +880,11 @@ page.prepareUploadConfig = () => {
let help
if (conf.disabled) {
control.disabled = conf.disabled
help = 'This option is currently disabled.'
if (Array.isArray(conf.select))
control.querySelector('select').disabled = conf.disabled
else
control.disabled = conf.disabled
help = 'This option is currently not configurable.'
} else if (typeof conf.help === 'string') {
help = conf.help
} else if (conf.help === true && conf.number !== undefined) {

View File

@ -10,6 +10,7 @@ page.prepareShareX = () => {
} : {}
values.filelength = page.fileLength || ''
values.age = page.uploadAge || ''
values.striptags = page.stripTags || ''
const headers = []
const keys = Object.keys(values)

View File

@ -1,5 +1,5 @@
{
"1": "1575024012",
"1": "1575034913",
"2": "1568894058",
"3": "1568894058",
"4": "1568894058",