From 06ac31d02e6d0a50a6532a29bdcf2a6ea824e568 Mon Sep 17 00:00:00 2001 From: Bobby Wibowo Date: Tue, 18 Jun 2019 02:34:15 +0700 Subject: [PATCH] Updates (dashboard) + Better pagination. + Added more advanced filtering system in Manage Uploads. It now supports filtering with multiple usernames and/or IPs. It also supports refining the matches with wildcards. Todo? Perhaps add simple file name filtering for regular users in the future? --- controllers/uploadController.js | 129 ++++++++++++++++++++++---------- public/js/dashboard.js | 82 ++++++++++++++------ views/_globals.njk | 2 +- 3 files changed, 150 insertions(+), 63 deletions(-) diff --git a/controllers/uploadController.js b/controllers/uploadController.js index 923c010..b252d53 100644 --- a/controllers/uploadController.js +++ b/controllers/uploadController.js @@ -684,34 +684,86 @@ uploadsController.list = async (req, res) => { // Headers is string-only, this seem to be the safest and lightest const all = req.headers.all === '1' - const uploader = req.headers.uploader + const filters = req.headers.filters const ismoderator = perms.is(user, 'moderator') - if ((all || uploader) && !ismoderator) return res.status(403).end() + if ((all || filters) && !ismoderator) return res.status(403).end() const basedomain = config.domain - // For filtering by uploader's username - let uploaderID = null - if (uploader) { - uploaderID = await db.table('users') - .where('username', uploader) + // For filtering uploads + const _filters = { + uploaders: [], + names: [], + ips: [], + flags: { + nouser: false, + noip: false + } + } + + // Perhaps this can be simplified even further? + if (filters) { + const usernames = [] + filters + .split(' ') + .map((v, i, a) => { + if (/[^\\]\\$/.test(v) && a[i + 1]) { + const tmp = `${v.slice(0, -1)} ${a[i + 1]}` + a[i + 1] = '' + return tmp + } + return v.replace(/\\\\/, '\\') + }) + .map((v, i) => { + const x = v.indexOf(':') + if (x >= 0 && v.substring(x + 1)) + return [v.substring(0, x), v.substring(x + 1)] + else if (v.startsWith('-')) + return [v] + }) + .forEach(v => { + if (!v) return + if (v[0] === 'user') usernames.push(v[1]) + else if (v[0] === 'name') _filters.names.push(v[1]) + else if (v[0] === 'ip') _filters.ips.push(v[1]) + else if (v[0] === '-user') _filters.flags.nouser = true + else if (v[0] === '-ip') _filters.flags.noip = true + }) + _filters.uploaders = await db.table('users') + .whereIn('username', usernames) .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.' }) + .then(rows => rows.map(v => v.id)) } function filter () { - if (req.params.id === undefined) - this.where('id', '<>', '') // TODO: Why is this necessary? - else + if (req.params.id !== undefined) { this.where('albumid', req.params.id) - if (!all) + } else if (!all) { this.where('userid', user.id) - else if (uploaderID) - this.where('userid', uploaderID) + } else { + // Fisrt, look for uploads matching ANY of the supplied 'user' OR 'ip' filters + // Then, refined the matches using the supplied 'name' filters + const raw = [] + const source = [] + if (_filters.uploaders.length) + source.push(`\`userid\` in (${_filters.uploaders.map(v => `'${v}'`).join(', ')})`) + if (_filters.ips.length) + source.push(`\`ip\` in (${_filters.ips.map(v => `'${v}'`).join(', ')})`) + if (_filters.flags.nouser) + source.push('(`userid` is null or \'\')') + if (_filters.flags.noip) + source.push('(`ip` is null or \'\')') + if (source.length) + raw.push(`(${source.join(' or ')})`) + if (_filters.names.length) + raw.push(`(${_filters.names.map(v => { + if (v.includes('*')) + return `\`name\` like '${v.replace(/\*/g, '%')}'` + else + return `\`name\` = '${v}'` + }).join(' or ')})`) + this.whereRaw(raw.join(' and ')) + } } // Query uploads count for pagination @@ -763,32 +815,29 @@ uploadsController.list = async (req, res) => { } // If we are a regular user, or we are not listing all uploads, send response + // TODO: !ismoderator is probably redundant (?) if (!ismoderator || !all) return res.json({ success: true, files, count, albums, basedomain }) // 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 }) + const userids = files + .map(file => file.userid) + .filter((v, i, a) => { + return v !== null && v !== undefined && v !== '' && a.indexOf(v) === i + }) - // 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 - }) - } + // If there are no uploads attached to a registered user, send response + if (userids.length === 0) return res.json({ success: true, files, count, basedomain }) + + // Query usernames of user IDs from currently selected files + const users = await db.table('users') + .whereIn('id', userids) + .select('id', 'username') + .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 }) } diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 0a1d3c2..836bbc5 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -36,7 +36,7 @@ const page = { // config of uploads view (all) uploadsAll: { type: localStorage[lsKeys.viewType.uploadsAll], - uploader: null, // uploader's name + filters: null, // uploads' filters pageNum: null, // page num all: true }, @@ -259,8 +259,10 @@ page.domClick = function (event) { return page.editUser(id) case 'disable-user': return page.disableUser(id) - case 'filter-by-uploader': - return page.filterByUploader(element) + case 'filters-help': + return page.filtersHelp(element) + case 'filter-uploads': + return page.filterUploads(element) case 'view-user-uploads': return page.viewUserUploads(id) case 'page-prev': @@ -298,7 +300,7 @@ page.switchPage = function (action, element) { func = page.getUploads views.album = page.views[page.currentView].album views.all = page.views[page.currentView].all - views.uploader = page.views[page.currentView].uploader + views.filters = page.views[page.currentView].filters } switch (action) { @@ -321,10 +323,10 @@ page.switchPage = function (action, element) { } } -page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { +page.getUploads = function ({ pageNum, album, all, filters } = {}, element) { if (element) page.isLoading(element, true) - if ((all || uploader) && !page.permissions.moderator) + if ((all || filters) && !page.permissions.moderator) return swal('An error occurred!', 'You can not do this!', 'error') if (typeof pageNum !== 'number' || pageNum < 0) @@ -336,7 +338,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { const headers = {} if (all) headers.all = '1' - if (uploader) headers.uploader = uploader + if (filters) headers.filters = filters axios.get(url, { headers }).then(function (response) { if (response.data.success === false) if (response.data.description === 'No token provided') { @@ -360,16 +362,23 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { const basedomain = response.data.basedomain const pagination = page.paginate(response.data.count, 25, pageNum) - let filter = '' + let filter = '
' if (all) filter = ` -
+
- +
+
+ @@ -381,7 +390,6 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { const extraControls = `
${filter} -
@@ -456,8 +464,8 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { 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] : '' + else files[i].appendix = files[i].albumid ? albums[files[i].albumid] : '' } if (page.views[page.currentView].type === 'thumbs') { @@ -594,7 +602,7 @@ page.getUploads = function ({ pageNum, album, all, uploader } = {}, element) { } if (page.currentView === 'uploads') page.views.uploads.album = album - if (page.currentView === 'uploadsAll') page.views.uploadsAll.uploader = uploader + if (page.currentView === 'uploadsAll') page.views.uploadsAll.filters = filters page.views[page.currentView].pageNum = files.length ? pageNum : 0 }).catch(function (error) { if (element) page.isLoading(element, false) @@ -770,16 +778,46 @@ page.clearSelection = function () { }) } -page.filterByUploader = function (element) { - const uploader = document.getElementById('uploader').value - page.getUploads({ all: true, uploader }, element) +page.filtersHelp = function (element) { + const content = document.createElement('div') + content.style = 'text-align: left' + content.innerHTML = ` + This supports 3 filter keys, namely user (username), ip and name (file name). + Each key can be specified more than once. + Backlashes should be used if the usernames have spaces. + There are also 2 additional flags, namely -user and -ip, which will match uploads by non-registered users and have no IPs respectively. + + How does it work? + First, it will filter uploads matching ANY of the supplied user or ip keys. + Then, it will refine the matches using the supplied name keys. + + Examples: + + Uploads from user with username "demo": + user:demo + + Uploads from users with username either "John Doe" OR "demo": + user:John\\ Doe user:demo + + Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip": + ip:127.0.0.1 name:*.rar name:*.zip + + Uploads from user with username "test" OR from non-registered users: + user:test -user + `.trim().replace(/^ {6}/gm, '').replace(/\n/g, '
') + swal({ content }) +} + +page.filterUploads = function (element) { + const filters = document.getElementById('filters').value + page.getUploads({ all: true, filters }, element) } page.viewUserUploads = function (id) { const user = page.cache.users[id] if (!user) return page.setActiveMenu(document.getElementById('itemManageUploads')) - page.getUploads({ all: true, uploader: user.username }) + page.getUploads({ all: true, filters: `user:${user.username.replace(/ /g, '\\ ')}` }) } page.deleteFile = function (id) { @@ -1956,17 +1994,17 @@ page.paginate = function (totalItems, itemsPerPage, currentPage) { } } - if (elementsToShow >= numPages) { + if (elementsToShow + 1 >= numPages) { add.pageNum(1, numPages) } else if (currentPage < elementsToShow) { add.pageNum(1, elementsToShow) add.endDots() - } else if (currentPage > numPages - elementsToShow) { + } else if (currentPage > numPages - elementsToShow + 1) { add.startDots() - add.pageNum(numPages - elementsToShow, numPages) + add.pageNum(numPages - elementsToShow + 1, numPages) } else { add.startDots() - add.pageNum(currentPage - step, currentPage, step) + add.pageNum(currentPage - step + 1, currentPage + step - 1) add.endDots() } diff --git a/views/_globals.njk b/views/_globals.njk index bdf7f2a..d0af24d 100644 --- a/views/_globals.njk +++ b/views/_globals.njk @@ -16,7 +16,7 @@ v3: CSS and JS files (libs such as bulma, lazyload, etc). v4: Renders in /public/render/* directories (to be used by render.js). #} -{% set v1 = "uDNOxxQGxC" %} +{% set v1 = "sAfUXhJ9Gz" %} {% set v2 = "hiboQUzAzp" %} {% set v3 = "DKoamSTKbO" %} {% set v4 = "43gxmxi7v8" %}