From b17b24b15907051c7d1ac87f4cc5d6702b429099 Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 28 Jun 2022 12:03:49 +0700 Subject: [PATCH] feat: new page /file/:identifier this will display all information recorded from the specified file, but only to the users that own them (it requires token) this page also has a delete file button, allowing us to provide link to this page for sharex deletion url option once again, this is only for authenticated users, and will only show file that the users own, unless said user is a moderator or higher --- controllers/uploadController.js | 19 +-- lolisafe.js | 2 + routes/file.js | 15 ++ src/js/file.js | 233 ++++++++++++++++++++++++++++++++ src/js/misc/utils.js | 5 +- views/file.njk | 98 ++++++++++++++ 6 files changed, 354 insertions(+), 18 deletions(-) create mode 100644 routes/file.js create mode 100644 src/js/file.js create mode 100644 views/file.njk diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 2b9f3b1..79c1d59 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -953,13 +953,9 @@ self.sendUploadResponse = async (req, res, user, result) => { // If uploaded by user, add delete URL (intended for ShareX and its derivatives) // Homepage uploader will not use this (use dashboard instead) - // REVISION: I wasn't aware ShareX wouldn't do a basic GET request to this API, - // which I hoped would then use the token header in the downloadable ShareX config file. - // At its current state, this isn't really usable. - /* - if (user) - map.deleteUrl = `${config.homeDomain}/api/upload/delete/${file.name}` - */ + if (user) { + map.deleteUrl = `${config.homeDomain}/file/${file.name}?delete` + } return map }) @@ -978,14 +974,7 @@ self.delete = async (req, res, next) => { field: 'id', values: isNaN(id) ? undefined : [id] } - } /* else if (req.method === 'GET') { - // ShareX-compatible API (or other clients that require basic GET-based API) - const name = req.params.name - body = { - field: 'name', - values: name ? [name] : undefined - } - } */ + } req.body = body return self.bulkDelete(req, res, next) diff --git a/lolisafe.js b/lolisafe.js index 2e2545e..9bd46e1 100644 --- a/lolisafe.js +++ b/lolisafe.js @@ -46,6 +46,7 @@ const utils = require('./controllers/utilsController') const album = require('./routes/album') const api = require('./routes/api') +const file = require('./routes/file') const nojs = require('./routes/nojs') const player = require('./routes/player') @@ -243,6 +244,7 @@ safe.use('/', express.static(paths.public, { setHeaders })) safe.use('/', express.static(paths.dist, { setHeaders })) safe.use('/', album) +safe.use('/', file) safe.use('/', nojs) safe.use('/', player) safe.use('/api', api) diff --git a/routes/file.js b/routes/file.js new file mode 100644 index 0000000..7d8bba1 --- /dev/null +++ b/routes/file.js @@ -0,0 +1,15 @@ +const routes = require('express').Router() +const utils = require('../controllers/utilsController') +const config = require('../config') + +routes.get([ + '/file/:identifier' +], async (req, res, next) => { + // Uploads identifiers parsing, etc., are strictly handled by client-side JS at src/js/file.js + return res.render('file', { + config, + versions: utils.versionStrings + }) +}) + +module.exports = routes diff --git a/src/js/file.js b/src/js/file.js new file mode 100644 index 0000000..2202376 --- /dev/null +++ b/src/js/file.js @@ -0,0 +1,233 @@ +/* global swal, axios */ + +const lsKeys = { + token: 'token' +} + +const page = { + // user token + token: localStorage[lsKeys.token], + + urlPrefix: null, + urlIdentifier: null, + + messageElement: document.querySelector('#message'), + fileinfoContainer: document.querySelector('#fileinfo'), + + downloadBtn: document.querySelector('#downloadBtn'), + playerBtn: document.querySelector('#playerBtn'), + deleteBtn: document.querySelector('#deleteBtn'), + uploadRoot: null, + titleFormat: null, + + file: null +} + +page.updateMessageBody = content => { + page.messageElement.querySelector('.message-body').innerHTML = content + page.messageElement.classList.remove('is-hidden') +} + +// Handler for regular JS errors +page.onError = error => { + console.error(error) + page.updateMessageBody(` +

An error occurred!

+

${error.toString()}

+

Please check your console for more information.

+ `) +} + +// Handler for Axios errors +page.onAxiosError = error => { + // Better Cloudflare errors + const cloudflareErrors = { + 520: 'Unknown Error', + 521: 'Web Server Is Down', + 522: 'Connection Timed Out', + 523: 'Origin Is Unreachable', + 524: 'A Timeout Occurred', + 525: 'SSL Handshake Failed', + 526: 'Invalid SSL Certificate', + 527: 'Railgun Error', + 530: 'Origin DNS Error' + } + + const statusText = cloudflareErrors[error.response.status] || error.response.statusText + + const description = error.response.data && error.response.data.description + ? error.response.data.description + : '' + page.updateMessageBody(` +

${error.response.status} ${statusText}

+

${description}

+ `) +} + +page.deleteFile = () => { + if (!page.file) return + + const content = document.createElement('div') + content.innerHTML = '

You won\'t be able to recover this file!

' + + swal({ + title: 'Are you sure?', + content, + icon: 'warning', + dangerMode: true, + buttons: { + cancel: true, + confirm: { + text: 'Yes, nuke it!', + closeModal: false + } + } + }).then(proceed => { + if (!proceed) return + + axios.post('../api/upload/delete', { + id: page.file.id + }).then(response => { + if (!response) return + + if (response.data.success === false) { + return swal('An error occurred!', response.data.description, 'error') + } + + const failed = Array.isArray(response.data.failed) ? response.data.failed : [] + if (failed.length) { + swal('An error occurred!', 'Unable to delete this file.', 'error') + } else { + swal('Deleted!', 'This file has been deleted.', 'success', { + buttons: false + }) + } + }).catch(page.onAxiosError) + }) +} + +page.loadFileinfo = () => { + if (!page.urlIdentifier) return + + axios.get(`../api/upload/get/${page.urlIdentifier}`).then(response => { + if (![200, 304].includes(response.status)) { + return page.onAxiosError(response) + } + + page.file = response.data.file + + if (page.titleFormat) { + document.title = page.titleFormat.replace(/%identifier%/g, page.file.name) + } + + let rows = '' + const keys = Object.keys(page.file) + for (let i = 0; i < keys.length; i++) { + const value = page.file[keys[i]] + + let prettyValue = '' + if (value) { + if (['size'].includes(keys[i])) { + prettyValue = page.getPrettyBytes(value) + } else if (['timestamp', 'expirydate'].includes(keys[i])) { + prettyValue = page.getPrettyDate(new Date(value * 1000)) + } + } + + rows += ` + + ${keys[i]} + ${value} + ${prettyValue} + + ` + } + + document.querySelector('#title').innerText = page.file.name + page.fileinfoContainer.querySelector('.table-container').innerHTML = ` +
+ + + + + + + + + + ${rows} + +
FieldsValues
+
+ ` + + if (page.downloadBtn) { + page.downloadBtn.setAttribute('href', `${page.uploadRoot}/${page.file.name}`) + } + + const isimage = page.file.type.startsWith('image/') + const isvideo = page.file.type.startsWith('video/') + const isaudio = page.file.type.startsWith('audio/') + if (isimage) { + const img = page.fileinfoContainer.querySelector('img') + img.setAttribute('alt', page.file.name || '') + img.src = `${page.uploadRoot}/${page.file.name}` + img.parentNode.classList.remove('is-hidden') + img.onerror = event => event.currentTarget.classList.add('is-hidden') + } else if (isvideo || isaudio) { + page.playerBtn.setAttribute('href', `../v/${page.file.name}`) + page.playerBtn.parentNode.parentNode.classList.remove('is-hidden') + } + + page.fileinfoContainer.classList.remove('is-hidden') + page.messageElement.classList.add('is-hidden') + + if (page.urlParams.has('delete')) { + page.deleteBtn.click() + } + }).catch(error => { + if (typeof error.response !== 'undefined') page.onAxiosError(error) + else page.onError(error) + }) +} + +window.addEventListener('DOMContentLoaded', () => { + // Partial polyfill URLSearchParams.has() + // eslint-disable-next-line compat/compat + window.URLSearchParams = window.URLSearchParams || function (searchString) { + const self = this + self.has = function (name) { + const results = new RegExp('[?&]' + name).exec(self.searchString) + if (results == null) { + return false + } else { + return true + } + } + } + + axios.defaults.headers.common.token = page.token + + const mainScript = document.querySelector('#mainScript') + if (!mainScript || typeof mainScript.dataset.uploadRoot === 'undefined') return + + page.uploadRoot = mainScript.dataset.uploadRoot + page.titleFormat = mainScript.dataset.titleFormat + + let urlPrefix = window.location.protocol + '//' + window.location.host + const match = window.location.pathname.match(/.*\/(.*)$/) + if (!match || !match[1]) { + return page.updateMessageBody('

Failed to parse upload identifier from URL.

') + } + + page.urlIdentifier = match[1] + urlPrefix += window.location.pathname.substring(0, window.location.pathname.indexOf(match[1])) + page.urlPrefix = urlPrefix + + // eslint-disable-next-line compat/compat + page.urlParams = new URLSearchParams(window.location.search) + + page.deleteBtn.addEventListener('click', page.deleteFile) + + page.loadFileinfo() +}) diff --git a/src/js/misc/utils.js b/src/js/misc/utils.js index cfc024e..83a2250 100644 --- a/src/js/misc/utils.js +++ b/src/js/misc/utils.js @@ -35,10 +35,9 @@ page.prepareShareX = () => { ThumbnailURL: '$json:files[0].url$' } - /* - if (page.token) + if (page.token) { sharexConfObj.DeletionURL = '$json:files[0].deleteUrl$' - */ + } const sharexConfStr = JSON.stringify(sharexConfObj, null, 2) const sharexBlob = new Blob([sharexConfStr], { type: 'application/octet-binary' }) diff --git a/views/file.njk b/views/file.njk new file mode 100644 index 0000000..174177d --- /dev/null +++ b/views/file.njk @@ -0,0 +1,98 @@ +{%- import '_globals.njk' as globals -%} + +{% set metaTitle = "File" %} + +{% set uploadRoot = config.domain %} +{% set titleFormat = '%identifier% | ' + globals.name %} + +{% extends "_layout.njk" %} + +{% block stylesheets %} + + + + + +{% endblock %} + +{% block scripts %} + + + + +{# We assign an ID for this so that the script can find out proper root URL of uploaded files #} + + +{% endblock %} + +{% set noscriptRefreshUrl = null %} +{% block endmeta %} +{% include "_partial/noscript-refresh.njk" %} +{% endblock %} + +{% block content %} +{{ super() }} +
+
+ +
+ +
+
+

Loading…

+
+
+ + +
+
+ +{% set floatingHomeHref = '..' %} +{% include "_partial/floating-home.njk" %} +{% set noscriptMessage = null %} +{% include "_partial/noscript.njk" %} +{% endblock %}