diff --git a/config.sample.js b/config.sample.js
index 061f962..c20595a 100644
--- a/config.sample.js
+++ b/config.sample.js
@@ -207,6 +207,13 @@ module.exports = {
chunkSize: 64 * 1024
},
+ /*
+ Store uploader's IPs into the database.
+ NOTE: Dashboard's Manage Uploads will display IP column regardless of whether
+ this is set to true or false.
+ */
+ storeIP: true,
+
/*
Chunk size for chunk uploads. Needs to be in MB.
If this is enabled, every files uploaded from the homepage uploader will forcibly be chunked
diff --git a/controllers/uploadController.js b/controllers/uploadController.js
index 10fb1d0..923c010 100644
--- a/controllers/uploadController.js
+++ b/controllers/uploadController.js
@@ -545,7 +545,7 @@ uploadsController.formatInfoMap = (req, res, user, infoMap) => {
type: info.data.mimetype,
size: info.data.size,
hash: fileHash,
- ip: req.ip,
+ ip: config.uploads.storeIP !== false ? req.ip : null, // only disable if explicitly set to false
albumid: albumsAuthorized[info.data.albumid] ? info.data.albumid : null,
userid: user !== undefined ? user.id : null,
timestamp: Math.floor(Date.now() / 1000)
@@ -688,25 +688,33 @@ uploadsController.list = async (req, res) => {
const ismoderator = perms.is(user, 'moderator')
if ((all || uploader) && !ismoderator) return res.status(403).end()
+ const basedomain = config.domain
+
+ // For filtering by uploader's username
let uploaderID = null
- if (uploader)
+ if (uploader) {
uploaderID = await db.table('users')
.where('username', uploader)
.select('id')
.first()
.then(row => row ? row.id : null)
+ // Close request if the provided username is not valid
+ if (!uploaderID)
+ return res.json({ success: false, description: 'User with that username could not be found.' })
+ }
function filter () {
if (req.params.id === undefined)
- this.where('id', '<>', '')
+ this.where('id', '<>', '') // TODO: Why is this necessary?
else
this.where('albumid', req.params.id)
- if (!ismoderator || !all)
+ if (!all)
this.where('userid', user.id)
else if (uploaderID)
this.where('userid', uploaderID)
}
+ // Query uploads count for pagination
const count = await db.table('files')
.where(filter)
.count('id as count')
@@ -716,55 +724,73 @@ uploadsController.list = async (req, res) => {
let offset = req.params.page
if (offset === undefined) offset = 0
+ const columns = ['id', 'timestamp', 'name', 'userid', 'size']
+ // Only select IPs if we are listing all uploads
+ columns.push(all ? 'ip' : 'albumid')
+
const files = await db.table('files')
.where(filter)
.orderBy('id', 'DESC')
.limit(25)
.offset(25 * offset)
- .select('id', 'albumid', 'timestamp', 'name', 'userid', 'size')
-
- const albums = await db.table('albums')
- .where(function () {
- this.where('enabled', 1)
- if (!all || !ismoderator)
- this.where('userid', user.id)
- })
-
- const basedomain = config.domain
- const userids = []
+ .select(columns)
for (const file of files) {
- file.file = `${basedomain}/${file.name}`
-
- file.album = ''
- if (file.albumid !== undefined)
- for (const album of albums)
- if (file.albumid === album.id)
- file.album = album.name
-
- // Only push usernames if we are a moderator
- if (all && ismoderator)
- if (file.userid !== undefined && file.userid !== null && file.userid !== '')
- userids.push(file.userid)
-
file.extname = utils.extname(file.name)
if (utils.mayGenerateThumb(file.extname))
- file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png`
+ file.thumb = `/thumbs/${file.name.slice(0, -file.extname.length)}.png`
}
- // If we are a normal user, send response
- if (!ismoderator) return res.json({ success: true, files, count })
+ // If we are not listing all uploads, query album names
+ let albums = {}
+ if (!all) {
+ const albumids = files
+ .map(file => file.albumid)
+ .filter((v, i, a) => {
+ return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i
+ })
+ albums = await db.table('albums')
+ .whereIn('id', albumids)
+ .where('enabled', 1)
+ .where('userid', user.id)
+ .select('id', 'name')
+ .then(rows => {
+ // Build Object indexed by their IDs
+ const obj = {}
+ for (const row of rows) obj[row.id] = row.name
+ return obj
+ })
+ }
- // If we are a moderator but there are no uploads attached to a user, send response
- if (userids.length === 0) return res.json({ success: true, files, count })
+ // If we are a regular user, or we are not listing all uploads, send response
+ if (!ismoderator || !all) return res.json({ success: true, files, count, albums, basedomain })
- const users = await db.table('users').whereIn('id', userids)
- for (const dbUser of users)
- for (const file of files)
- if (file.userid === dbUser.id)
- file.username = dbUser.username
+ // Otherwise proceed to querying usernames
+ let users = {}
+ if (uploaderID) {
+ // If we are already filtering by username, manually build array
+ users[uploaderID] = uploader
+ } else {
+ const userids = files
+ .map(file => file.userid)
+ .filter((v, i, a) => {
+ return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i
+ })
+ // If there are no uploads attached to a registered user, send response
+ if (userids.length === 0) return res.json({ success: true, files, count, basedomain })
- return res.json({ success: true, files, count })
+ // Query usernames of user IDs from currently selected files
+ users = await db.table('users')
+ .whereIn('id', userids)
+ .then(rows => {
+ // Build Object indexed by their IDs
+ const obj = {}
+ for (const row of rows) obj[row.id] = row.username
+ return obj
+ })
+ }
+
+ return res.json({ success: true, files, count, users, basedomain })
}
module.exports = uploadsController
diff --git a/public/js/dashboard.js b/public/js/dashboard.js
index f6cf1bf..0a1d3c2 100644
--- a/public/js/dashboard.js
+++ b/public/js/dashboard.js
@@ -203,7 +203,7 @@ page.getItemID = function (element) {
page.domClick = function (event) {
// We are processing clicks this way to avoid using "onclick" attribute
// Apparently we will need to use "unsafe-inline" for "script-src" directive
- // of Content Security Policy (CSP), if we want ot use "onclick" attribute
+ // of Content Security Policy (CSP), if we want to use "onclick" attribute
// Though I think that only applies to some browsers (?)
// Either way, I personally would rather not
// Of course it wouldn't have mattered if we didn't use CSP to begin with
@@ -342,11 +342,12 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
+ if (element) page.isLoading(element, false)
return swal('An error occurred!', response.data.description, 'error')
}
- if (pageNum && (response.data.files.length === 0)) {
- // Only remove loading class here, since beyond this the entire page will be replaced anyways
+ const files = response.data.files
+ if (pageNum && (files === 0)) {
if (element) page.isLoading(element, false)
return swal('An error occurred!', `There are no more uploads to populate page ${pageNum + 1}.`, 'error')
}
@@ -354,6 +355,9 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
page.currentView = all ? 'uploadsAll' : 'uploads'
page.cache.uploads = {}
+ const albums = response.data.albums
+ const users = response.data.users
+ const basedomain = response.data.basedomain
const pagination = page.paginate(response.data.count, 25, pageNum)
let filter = ''
@@ -432,7 +436,30 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
`
+ // Set to true to tick "all files" checkbox in list view
let allSelected = true
+
+ for (let i = 0; i < files.length; i++) {
+ // Build full URLs
+ files[i].file = `${basedomain}/${files[i].name}`
+ if (files[i].thumb) files[i].thumb = `${basedomain}/${files[i].thumb}`
+ // Cache bare minimum data for thumbnails viewer
+ page.cache.uploads[files[i].id] = {
+ name: files[i].name,
+ thumb: files[i].thumb,
+ original: files[i].file
+ }
+ // Prettify
+ files[i].prettyBytes = page.getPrettyBytes(parseInt(files[i].size))
+ files[i].prettyDate = page.getPrettyDate(new Date(files[i].timestamp * 1000))
+ // Update selected status
+ files[i].selected = page.selected[page.currentView].includes(files[i].id)
+ if (allSelected && !files[i].selected) allSelected = false
+ // Appendix (display album or user)
+ files[i].appendix = files[i].albumid ? albums[files[i].albumid] : ''
+ if (all) files[i].appendix = files[i].userid ? users[files[i].userid] : ''
+ }
+
if (page.views[page.currentView].type === 'thumbs') {
page.dom.innerHTML = `
${pagination}
@@ -447,24 +474,8 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
const table = document.getElementById('table')
- for (let i = 0; i < response.data.files.length; i++) {
- const upload = response.data.files[i]
- const selected = page.selected[page.currentView].includes(upload.id)
- if (!selected && allSelected) allSelected = false
-
- page.cache.uploads[upload.id] = {
- name: upload.name,
- thumb: upload.thumb,
- original: upload.file
- }
-
- // Prettify
- upload.prettyBytes = page.getPrettyBytes(parseInt(upload.size))
- upload.prettyDate = page.getPrettyDate(new Date(upload.timestamp * 1000))
-
- let displayAlbumOrUser = upload.album
- if (all) displayAlbumOrUser = upload.username || ''
-
+ for (let i = 0; i < files.length; i++) {
+ const upload = files[i]
const div = document.createElement('div')
div.className = 'image-container column is-narrow'
div.dataset.id = upload.id
@@ -474,7 +485,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) {
div.innerHTML = `${upload.extname || 'N/A'}
`
div.innerHTML += `
-
+
${upload.name}
-${displayAlbumOrUser ? `${displayAlbumOrUser} – ` : ''}${upload.prettyBytes}
+${upload.appendix ? `${upload.appendix} – ` : ''}${upload.prettyBytes}