Massively overhauled uploads filtering endpoint

Please consult the Help? button again to learn all the syntax changes!
The prompt will now also have its width expanded!

Updated dependency, knex: 0.20.13 -> 0.20.15.

Added new dependency: search-query-parser.

Updated all sub-dependencies.

Critical? Admins-only API /users/edit will no longer return NEW password
salt of the user when randomizing their password.

Added page.escape() function to js/misc/utils.js.
This will be used to escape input in upload filters input box.
The same function used in utilsController.js.

Pretty dates will now use / instead of - for date separator.
This is due to the fact that date range key for filtering uploads
can not accepts dates with - separator.
To avoid inconsistency, we will now use / separator.

Caching system of album public pages will now be disabled during
development (yarn develop).

Cleaned up domClick() function in js/dashboard.js.

If using date or expiry range keys when filtering uploads, attach
client's timezone offset to the API requets.
This will be used by the server to calculate timezone differences.

Success prompt when changing token will now auto-close.

Removed ID column from Manage Users.

Improved success prompt when editing users.
This will properly list all of the edited fields at once,
excluding user group change.
Success message for user group change will require a bit more changes
on the API endpoint, which is a bit annoying.

Rebuilt client-side assets and bumped v1 version string.
This commit is contained in:
Bobby Wibowo 2020-04-19 02:52:11 +07:00
parent 1c260c87b0
commit 3e3878b93c
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
13 changed files with 472 additions and 235 deletions

View File

@ -259,7 +259,7 @@ self.editUser = async (req, res, next) => {
utils.invalidateStatsCache('users') utils.invalidateStatsCache('users')
const response = { success: true, update } const response = { success: true, update }
if (password) response.password = password if (password) response.update.password = password
return res.json(response) return res.json(response)
} catch (error) { } catch (error) {
logger.error(error) logger.error(error)

View File

@ -4,6 +4,7 @@ const fs = require('fs')
const multer = require('multer') const multer = require('multer')
const path = require('path') const path = require('path')
const randomstring = require('randomstring') const randomstring = require('randomstring')
const searchQuery = require('search-query-parser')
const paths = require('./pathsController') const paths = require('./pathsController')
const perms = require('./permissionController') const perms = require('./permissionController')
const utils = require('./utilsController') const utils = require('./utilsController')
@ -758,94 +759,172 @@ self.list = async (req, res) => {
const all = Boolean(req.headers.all) const all = Boolean(req.headers.all)
const filters = req.headers.filters const filters = req.headers.filters
const minoffset = req.headers.minoffset
const ismoderator = perms.is(user, 'moderator') const ismoderator = perms.is(user, 'moderator')
if ((all || filters) && !ismoderator) if ((all || filters) && !ismoderator)
return res.status(403).end() return res.status(403).end()
const basedomain = config.domain const basedomain = config.domain
// For filtering uploads const filterObj = {
const _filters = {
uploaders: [], uploaders: [],
names: [], excludeUploaders: [],
ips: [], queries: {
flags: { exclude: {}
nouser: false,
noip: false
}, },
keywords: [] flags: {}
} }
// Cast column(s) to specific type if they're stored differently const orderByObj = {
const _orderByCasts = { // Cast columns to specific type if they are stored differently
casts: {
size: 'integer' size: 'integer'
} },
// Columns mapping
maps: {
date: 'timestamp',
expiry: 'expirydate'
},
// Columns with which to use SQLite's NULLS LAST option // Columns with which to use SQLite's NULLS LAST option
const _orderByNullsLast = [ nullsLast: [
'userid', 'userid',
'expirydate', 'expirydate',
'ip' 'ip'
] ],
const _orderBy = [] parsed: []
}
// Perhaps this can be simplified even further?
if (filters) { if (filters) {
const usernames = [] const keywords = [
filters 'ip',
.split(' ') 'user'
.map((v, i, a) => { ]
if (/[^\\]\\$/.test(v) && a[i + 1]) { const ranges = [
const tmp = `${v.slice(0, -1)} ${a[i + 1]}` 'date',
a[i + 1] = '' 'expiry'
return tmp ]
} filterObj.queries = searchQuery.parse(filters, {
return v.replace(/\\\\/, '\\') keywords: keywords.concat([
'orderby'
]),
ranges,
tokenize: true,
alwaysArray: true,
offsets: false
}) })
.map((v, i) => {
const x = v.indexOf(':') for (const key of keywords)
if (x >= 0 && v.substring(x + 1)) if (filterObj.queries[key]) {
return [v.substring(0, x), v.substring(x + 1)] // Make sure keyword arrays only contain unique values
else filterObj.queries[key] = filterObj.queries[key].filter((v, i, a) => a.indexOf(v) === i)
return v
}) // Flag to match NULL values
.forEach(v => { const index = filterObj.queries[key].indexOf('-')
if (Array.isArray(v)) { if (index !== -1) {
if (v[0] === 'user') { filterObj.flags[`no${key}`] = true
usernames.push(v[1]) filterObj.queries[key].splice(index, 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] === 'orderby') {
const tmp = v[1].split(':')
let col = tmp[0]
let dir = 'asc'
if (_orderByCasts[col])
col = `cast (\`${col}\` as ${_orderByCasts[col]})`
if (tmp[1] && /^d/i.test(tmp[1]))
dir = 'desc'
_orderBy.push(`${col} ${dir}${_orderByNullsLast.includes(col) ? ' nulls last' : ''}`)
} }
}
const parseDate = (date, minoffset, resetMs) => {
// [YYYY][/MM][/DD] [HH][:MM][:SS]
// e.g. 2020/01/01 00:00:00, 2018/01/01 06, 2019/11, 12:34:00
const match = date.match(/^(\d{4})?(\/\d{2})?(\/\d{2})?\s?(\d{2})?(:\d{2})?(:\d{2})?$/)
if (match) {
const offset = 60000 * (utils.timezoneOffset - minoffset)
const dateObj = new Date(Date.now() + offset)
if (match[1] !== undefined)
dateObj.setFullYear(Number(match[1]), // full year
match[2] !== undefined ? (Number(match[2].slice(1)) - 1) : 0, // month, zero-based
match[3] !== undefined ? Number(match[3].slice(1)) : 1) // date
if (match[4] !== undefined)
dateObj.setHours(Number(match[4]), // hours
match[5] !== undefined ? Number(match[5].slice(1)) : 0, // minutes
match[6] !== undefined ? Number(match[6].slice(1)) : 0) // seconds
if (resetMs)
dateObj.setMilliseconds(0)
// Calculate timezone differences
const newDateObj = new Date(dateObj.getTime() - offset)
return newDateObj
} else { } else {
if (v === '-user') return null
_filters.flags.nouser = true
else if (v === '-ip')
_filters.flags.noip = true
else
_filters.keywords.push(v[0])
} }
}) }
_filters.uploaders = await db.table('users')
// Parse dates to timestamps
for (const range of ranges)
if (filterObj.queries[range]) {
if (filterObj.queries[range].from) {
const parsed = parseDate(filterObj.queries[range].from, minoffset, true)
filterObj.queries[range].from = parsed ? Math.floor(parsed / 1000) : null
}
if (filterObj.queries[range].to) {
const parsed = parseDate(filterObj.queries[range].to, minoffset, true)
filterObj.queries[range].to = parsed ? Math.ceil(parsed / 1000) : null
}
}
// Query users table for user IDs
if (filterObj.queries.user || filterObj.queries.exclude.user) {
const usernames = []
.concat(filterObj.queries.user || [])
.concat(filterObj.queries.exclude.user || [])
const uploaders = await db.table('users')
.whereIn('username', usernames) .whereIn('username', usernames)
.select('id', 'username') .select('id', 'username')
// If no matches, or mismatched results
if (!uploaders || (uploaders.length !== usernames.length)) {
const notFound = usernames.filter(username => {
return !uploaders.find(uploader => uploader.username === username)
})
if (notFound)
return res.json({
success: false,
description: `User${notFound.length === 1 ? '' : 's'} not found: ${notFound.join(', ')}.`
})
} }
if (filters && !(_filters.uploaders.length || _filters.names.length || _filters.ips.length || _filters.flags.nouser || _filters.flags.noip || _orderBy.length)) for (const uploader of uploaders)
if (_filters.keywords.length) if (filterObj.queries.user && filterObj.queries.user.includes(uploader.username))
// TODO: Support filtering using keywords only filterObj.uploaders.push(uploader)
return res.json({ success: false, description: 'Filtering using keywords only is still work in progress. Please confirm valid filtering keys through the Help? button!' })
else else
return res.json({ success: false, description: 'No valid filter or sort keys were used. Please confirm the valid keys through the Help? button!' }) filterObj.excludeUploaders.push(uploader)
delete filterObj.queries.user
delete filterObj.queries.exclude.user
}
// Parse orderby keys
if (filterObj.queries.orderby) {
for (const obQuery of filterObj.queries.orderby) {
const tmp = obQuery.toLowerCase().split(':')
let column = orderByObj.maps[tmp[0]] || tmp[0]
let direction = 'asc'
if (orderByObj.casts[column])
column = `cast (\`${column}\` as ${orderByObj.casts[column]})`
if (tmp[1] && /^d/.test(tmp[1]))
direction = 'desc'
const suffix = orderByObj.nullsLast.includes(column) ? ' nulls last' : ''
orderByObj.parsed.push(`${column} ${direction}${suffix}`)
}
delete filterObj.queries.orderby
}
// For some reason, single value won't be in Array even with 'alwaysArray' option
if (typeof filterObj.queries.exclude.text === 'string')
filterObj.queries.exclude.text = [filterObj.queries.exclude.text]
}
function filter () { function filter () {
if (req.params.id !== undefined) if (req.params.id !== undefined)
@ -853,23 +932,53 @@ self.list = async (req, res) => {
else if (!all) else if (!all)
this.where('userid', user.id) this.where('userid', user.id)
else else
// Fisrt, look for uploads matching ANY of the supplied 'user' OR 'ip' filters // Sheesh, these look too overbearing...
// Then, refine the matches using the supplied 'name' filters
this.where(function () { this.where(function () {
if (_filters.uploaders.length) // Filter uploads matching any of the supplied 'user' keys and/or NULL flag
this.orWhereIn('userid', _filters.uploaders.map(v => v.id)) if (filterObj.uploaders.length)
if (_filters.ips.length) this.orWhereIn('userid', filterObj.uploaders.map(v => v.id))
this.orWhereIn('ip', _filters.ips) if (filterObj.excludeUploaders.length)
if (_filters.flags.nouser) this.orWhereNotIn('userid', filterObj.excludeUploaders.map(v => v.id))
if (filterObj.flags.nouser)
this.orWhereNull('userid') this.orWhereNull('userid')
if (_filters.flags.noip) }).orWhere(function () {
// Filter uploads matching any of the supplied 'ip' keys and/or NULL flag
if (filterObj.queries.ip)
this.orWhereIn('ip', filterObj.queries.ip)
if (filterObj.queries.exclude.ip)
this.orWhereNotIn('ip', filterObj.queries.exclude.ip)
if (filterObj.flags.noip)
this.orWhereNull('ip') this.orWhereNull('ip')
}).andWhere(function () { }).andWhere(function () {
for (const name of _filters.names) // Then, refine using the supplied 'date' and/or 'expiry' ranges
if (filterObj.queries.date)
if (typeof filterObj.queries.date.to === 'number')
this.andWhereBetween('timestamp', [filterObj.queries.date.from, filterObj.queries.date.to])
else
this.andWhere('timestamp', '>=', filterObj.queries.date.from)
if (filterObj.queries.expiry)
if (typeof filterObj.queries.expiry.to === 'number')
this.andWhereBetween('expirydate', [filterObj.queries.expiry.from, filterObj.queries.expiry.to])
else
this.andWhere('expirydate', '>=', filterObj.queries.date.from)
}).andWhere(function () {
// Then, refine using the supplied keywords against their file names
if (!filterObj.queries.text) return
for (const name of filterObj.queries.text)
if (name.includes('*')) if (name.includes('*'))
this.orWhere('name', 'like', name.replace(/\*/g, '%')) this.orWhere('name', 'like', name.replace(/\*/g, '%'))
else else
this.orWhere('name', name) // If no asterisks, assume partial
this.orWhere('name', 'like', `%${name}%`)
}).andWhere(function () {
// Finally, refine using the supplied exclusions against their file names
if (!filterObj.queries.exclude.text) return
for (const exclude of filterObj.queries.exclude.text)
if (exclude.includes('*'))
this.orWhere('name', 'not like', exclude.replace(/\*/g, '%'))
else
// If no asterisks, assume partial
this.orWhere('name', 'not like', `%${exclude}%`)
}) })
} }
@ -886,16 +995,18 @@ self.list = async (req, res) => {
if (offset === undefined) offset = 0 if (offset === undefined) offset = 0
const columns = ['id', 'name', 'userid', 'size', 'timestamp'] const columns = ['id', 'name', 'userid', 'size', 'timestamp']
if (temporaryUploads) if (temporaryUploads)
columns.push('expirydate') columns.push('expirydate')
// Only select IPs if we are listing all uploads // Only select IPs if we are listing all uploads
columns.push(all ? 'ip' : 'albumid') columns.push(all ? 'ip' : 'albumid')
const orderByRaw = orderByObj.parsed.length
? orderByObj.parsed.join(', ')
: '`id` desc'
const files = await db.table('files') const files = await db.table('files')
.where(filter) .where(filter)
.orderByRaw(_orderBy.length ? _orderBy.join(', ') : '`id` desc') .orderByRaw(orderByRaw)
.limit(25) .limit(25)
.offset(25 * offset) .offset(25 * offset)
.select(columns) .select(columns)
@ -936,8 +1047,8 @@ self.list = async (req, res) => {
return res.json({ success: true, files, count, albums, basedomain }) return res.json({ success: true, files, count, albums, basedomain })
// Otherwise proceed to querying usernames // Otherwise proceed to querying usernames
let _users = _filters.uploaders let usersTable = filterObj.uploaders
if (!_users.length) { if (!usersTable.length) {
const userids = files const userids = files
.map(file => file.userid) .map(file => file.userid)
.filter((v, i, a) => { .filter((v, i, a) => {
@ -949,13 +1060,13 @@ self.list = async (req, res) => {
return res.json({ success: true, files, count, basedomain }) return res.json({ success: true, files, count, basedomain })
// Query usernames of user IDs from currently selected files // Query usernames of user IDs from currently selected files
_users = await db.table('users') usersTable = await db.table('users')
.whereIn('id', userids) .whereIn('id', userids)
.select('id', 'username') .select('id', 'username')
} }
const users = {} const users = {}
for (const user of _users) for (const user of usersTable)
users[user.id] = user.username users[user.id] = user.username
return res.json({ success: true, files, count, users, basedomain }) return res.json({ success: true, files, count, users, basedomain })

View File

@ -31,7 +31,8 @@ const self = {
ffprobe: promisify(ffmpeg.ffprobe), ffprobe: promisify(ffmpeg.ffprobe),
albumsCache: {} albumsCache: {},
timezoneOffset: new Date().getTimezoneOffset()
} }
const statsCache = { const statsCache = {
@ -106,7 +107,7 @@ self.extname = filename => {
return extname + multi return extname + multi
} }
self.escape = (string) => { self.escape = string => {
// MIT License // MIT License
// Copyright(c) 2012-2013 TJ Holowaychuk // Copyright(c) 2012-2013 TJ Holowaychuk
// Copyright(c) 2015 Andreas Lubbe // Copyright(c) 2015 Andreas Lubbe

File diff suppressed because one or more lines are too long

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||"",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"}; 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),r=0;r<a.length;r++)t.push(' "'+a[r]+'": "'+e[a[r]]+'"');var n=(location.hostname+location.pathname).replace(/\/(dashboard)?$/,""),o=n.replace(/\//g,"_"),i=document.querySelector("#ShareX"),s='{\n "Name": "'+o+'",\n "DestinationType": "ImageUploader, FileUploader",\n "RequestMethod": "POST",\n "RequestURL": "'+location.protocol+"//"+n+'/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",o+".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?"-":"",r=t?1e3:1024;if(a&&(e=-e),e<r)return""+a+e+" B";var n=Math.min(Math.floor(Math.log(e)*Math.LOG10E/3),8);return""+a+Number((e/Math.pow(r,n)).toPrecision(3))+" "+((t?"kMGTPEZY":"KMGTPEZY").charAt(n-1)+(t?"":"i"))+"B"},page.escape=function(e){if(!e)return e;var t,a=String(e),r=/["'&<>]/.exec(a);if(!r)return a;var n="",o=0,i=0;for(o=r.index;o<a.length;o++){switch(a.charCodeAt(o)){case 34:t="&quot;";break;case 38:t="&amp;";break;case 39:t="&#39;";break;case 60:t="&lt;";break;case 62:t="&gt;";break;default:continue}i!==o&&(n+=a.substring(i,o)),i=o+1,n+=t}return i!==o?n+a.substring(i,o):n};
//# sourceMappingURL=utils.js.map //# sourceMappingURL=utils.js.map

File diff suppressed because one or more lines are too long

View File

@ -37,12 +37,13 @@
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"helmet": "^3.22.0", "helmet": "^3.22.0",
"jszip": "^3.3.0", "jszip": "^3.3.0",
"knex": "^0.20.13", "knex": "^0.20.15",
"multer": "^1.4.2", "multer": "^1.4.2",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"nunjucks": "^3.2.1", "nunjucks": "^3.2.1",
"randomstring": "^1.1.5", "randomstring": "^1.1.5",
"readline": "^1.3.0", "readline": "^1.3.0",
"search-query-parser": "^1.5.5",
"sharp": "^0.25.2", "sharp": "^0.25.2",
"sqlite3": "^4.1.1", "sqlite3": "^4.1.1",
"systeminformation": "^4.23.3" "systeminformation": "^4.23.3"

View File

@ -31,8 +31,10 @@ routes.get('/a/:identifier', async (req, res, next) => {
const nojs = req.query.nojs !== undefined const nojs = req.query.nojs !== undefined
let cacheid
if (process.env.NODE_ENV !== 'development') {
// Cache ID - we initialize a separate cache for No-JS version // Cache ID - we initialize a separate cache for No-JS version
const cacheid = nojs ? `${album.id}-nojs` : album.id cacheid = nojs ? `${album.id}-nojs` : album.id
if (!utils.albumsCache[cacheid]) if (!utils.albumsCache[cacheid])
utils.albumsCache[cacheid] = { utils.albumsCache[cacheid] = {
@ -55,6 +57,7 @@ routes.get('/a/:identifier', async (req, res, next) => {
// when an album is edited during this generation process. // when an album is edited during this generation process.
utils.albumsCache[cacheid].generating = true utils.albumsCache[cacheid].generating = true
utils.albumsCache[cacheid].generatedAt = Math.floor(Date.now() / 1000) utils.albumsCache[cacheid].generatedAt = Math.floor(Date.now() / 1000)
}
const files = await db.table('files') const files = await db.table('files')
.select('name', 'size') .select('name', 'size')
@ -89,12 +92,15 @@ routes.get('/a/:identifier', async (req, res, next) => {
files, files,
nojs nojs
}, (error, html) => { }, (error, html) => {
utils.albumsCache[cacheid].cache = error ? null : html const data = error ? null : html
if (cacheid) {
utils.albumsCache[cacheid].cache = data
utils.albumsCache[cacheid].generating = false utils.albumsCache[cacheid].generating = false
}
// Express should already send error to the next handler // Express should already send error to the next handler
if (error) return if (error) return
return res.send(utils.albumsCache[cacheid].cache) return res.send(data)
}) })
}) })

View File

@ -298,34 +298,34 @@ page.domClick = event => {
const action = element.dataset.action const action = element.dataset.action
switch (action) { switch (action) {
// Uploads
case 'view-list': case 'view-list':
return page.setUploadsView('list', element) return page.setUploadsView('list', element)
case 'view-thumbs': case 'view-thumbs':
return page.setUploadsView('thumbs', element) return page.setUploadsView('thumbs', element)
case 'clear-selection':
return page.clearSelection()
case 'add-selected-uploads-to-album':
return page.addSelectedUploadsToAlbum()
case 'select':
return page.select(element, event)
case 'select-all':
return page.selectAll(element)
case 'add-to-album': case 'add-to-album':
return page.addToAlbum(id) return page.addToAlbum(id)
case 'delete-upload': case 'delete-upload':
return page.deleteUpload(id) return page.deleteUpload(id)
case 'add-selected-uploads-to-album':
return page.addSelectedUploadsToAlbum()
case 'bulk-delete-uploads': case 'bulk-delete-uploads':
return page.bulkDeleteUploads() return page.bulkDeleteUploads()
case 'display-preview': case 'display-preview':
return page.displayPreview(id) return page.displayPreview(id)
// Manage uploads
case 'upload-filters-help':
return page.uploadFiltersHelp(element)
case 'filter-uploads':
return page.filterUploads(element)
// Manage your albums
case 'submit-album': case 'submit-album':
return page.submitAlbum(element) return page.submitAlbum(element)
case 'edit-album': case 'edit-album':
return page.editAlbum(id) return page.editAlbum(id)
case 'delete-album': case 'delete-album':
return page.deleteAlbum(id) return page.deleteAlbum(id)
case 'get-new-token': // Manage users
return page.getNewToken(element)
case 'create-user': case 'create-user':
return page.createUser() return page.createUser()
case 'edit-user': case 'edit-user':
@ -334,12 +334,24 @@ page.domClick = event => {
return page.disableUser(id) return page.disableUser(id)
case 'delete-user': case 'delete-user':
return page.deleteUser(id) return page.deleteUser(id)
case 'user-filters-help':
return page.userFiltersHelp(element)
case 'filter-uploads':
return page.filterUploads(element)
case 'view-user-uploads': case 'view-user-uploads':
return page.viewUserUploads(id, element) return page.viewUserUploads(id, element)
/* // WIP
case 'user-filters-help':
return page.userFiltersHelp(element)
case 'filter-users':
return page.filterUsers(element)
*/
// Others
case 'get-new-token':
return page.getNewToken(element)
// Uploads & Users
case 'clear-selection':
return page.clearSelection()
case 'select':
return page.select(element, event)
case 'select-all':
return page.selectAll(element)
case 'page-ellipsis': case 'page-ellipsis':
return page.focusJumpToPage() return page.focusJumpToPage()
case 'page-prev': case 'page-prev':
@ -429,6 +441,11 @@ page.getUploads = (params = {}) => {
filters: params.filters || '' filters: params.filters || ''
} }
// Send client timezone offset if using date filter
// Server will pretend client is on UTC if missing
if (headers.filters.includes('date:') || headers.filters.includes('expiry:'))
headers.minOffset = new Date().getTimezoneOffset()
axios.get(url, { headers }).then(response => { axios.get(url, { headers }).then(response => {
if (response.data.success === false) if (response.data.success === false)
if (response.data.description === 'No token provided') { if (response.data.description === 'No token provided') {
@ -464,10 +481,10 @@ page.getUploads = (params = {}) => {
<form class="prevent-default"> <form class="prevent-default">
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<input id="filters" class="input is-small" type="text" placeholder="Filters" value="${params.filters || ''}"> <input id="filters" class="input is-small" type="text" placeholder="Filters" value="${page.escape(params.filters || '')}">
</div> </div>
<div class="control"> <div class="control">
<button type="button" class="button is-small is-primary is-outlined" title="Help?" data-action="user-filters-help"> <button type="button" class="button is-small is-primary is-outlined" title="Help?" data-action="upload-filters-help">
<span class="icon"> <span class="icon">
<i class="icon-help-circled"></i> <i class="icon-help-circled"></i>
</span> </span>
@ -666,7 +683,7 @@ page.getUploads = (params = {}) => {
<thead> <thead>
<tr> <tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th> <th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th>File</th> <th>File name</th>
${params.album === undefined ? `<th>${params.all ? 'User' : 'Album'}</th>` : ''} ${params.album === undefined ? `<th>${params.all ? 'User' : 'Album'}</th>` : ''}
<th>Size</th> <th>Size</th>
${params.all ? '<th>IP</th>' : ''} ${params.all ? '<th>IP</th>' : ''}
@ -946,40 +963,68 @@ page.clearSelection = () => {
}) })
} }
page.userFiltersHelp = element => { page.uploadFiltersHelp = element => {
const content = document.createElement('div') const content = document.createElement('div')
content.style = 'text-align: left' content.style = 'text-align: left'
content.innerHTML = ` content.innerHTML = `
This supports 3 filter keys, namely <b>user</b> (username), <b>ip</b> and <b>name</b> (upload name). There are 2 filter keys, namely <b>user</b> (username) and <b>ip</b>.
Each key can be specified more than once. These keys can be specified more than once.
Backslashes should be used if the username contain whitespaces. For usernames with whitespaces, wrap them with double quotes (<b>"</b>).
There are 2 special flags, namely <b>-user</b> and <b>-ip</b>, which will match uploads by non-registered users and have no IPs respectively. There are 2 special filter keys, namely <b>user:-</b> and <b>ip:-</b>, to match uploads by non-registered users and have no IPs respectively.
Matches can also be sorted with <b>orderby:columnName[:direction]</b> key. To exclude certain users/ips while still listing every other uploads, add negation sign (<b>-</b>) before the keys.
This key requires using the internal column names used in the database (size, timestamp, expirydate, and so on). <b>Unfortunately</b>, this will not show uploads by non-registered users or have no IPs as well, instead you need to also use the special filter keys if you want to include them.
There are 2 range keys: <b>date</b> (upload date) and <b>expiry</b> (expiry date).
Their format is: <b>YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS</b> ("to" date is optional).
If any of the subsequent date or time units are not specified, their first value will be used (e.g. January for month, 1 for day, and so on).
If only time is specified, today's date will be used.
Meaning the following can be accepted: <b>2020/01/01 01:23</b>, <b>2018/01/01 06</b>, <b>2019/11</b>, <b>12:34:56</b>.
These keys can only be specified once each.
Matches can also be sorted with <b>orderby:columnName[:d[escending]]</b> keys.
This key require internal column names used in the database (id, userid, and so on), but there are 2 shortcuts, namely <b>date</b> for timestamp column and <b>expiry</b> for expirydate column.
This key can also be specified more than once, where their order will decide the sorting steps. This key can also be specified more than once, where their order will decide the sorting steps.
Any leftover keywords which do not use keys will be matched against the matches' file names.
Excluding certain keywords is also supported by adding negation sign (<b>-</b>) before the keywords.
<b>Internals:</b> <b>Internals:</b>
First, it will filter uploads matching ANY of the supplied filter keys AND/OR special flags, if any. First, it will filter uploads matching ANY of the supplied filter keys AND/OR special filter keys, if any.
Second, it will refine the matches using the supplied <b>name</b> keys, if any. Second, it will refine the matches using the supplied <b>date</b> AND/OR <b>expiry</b> range keys, if any.
Third, it will sort the matches using the supplied <b>orderby</b> keys, if any. Third, it will refine the matches using the leftover non-keyed keywords, if any.
Finally, it will sort the matches using the supplied <b>orderby</b> keys, if any.
<b>Examples:</b> <b>Examples:</b>
Uploads from user with username "demo": Uploads from user named "demo":
<code>user:demo</code> <code>user:demo</code>
Uploads from users with username "John Doe" AND/OR "demo": Uploads from users named "demo" AND/OR "John Doe" AND/OR non-registered users:
<code>user:John\\ Doe user:demo</code> <code>user:demo user:"John Doe" user:-</code>
Uploads from IP "127.0.0.1" AND which upload names match "*.rar" OR "*.zip": ALL uploads, including from non-registered users, but NOT the ones from user named "demo":
<code>ip:127.0.0.1 name:*.rar name:*.zip</code> <code>-user:demo user:-</code>
Uploads from user with username "test" AND/OR from non-registered users: Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip":
<code>user:test -user</code> <code>ip:127.0.0.1 *.rar *.zip</code>
Sort results by "size" column in ascending and descending order respectively: Uploads uploaded since "1 June 2019 00:00:00":
<code>orderby:expirydate</code> <code>date:2019/06</code>
<code>user:demo orderby:size</code> Uploads uploaded between "7 April 2020 00:00:00" and "7 April 2020 23:59:59":
<code>-user name:*.mp4 orderby:size:desc</code> <code>date:2020/04/07-2020/04/07 23:59:59</code>
Uploads which file names match "*.gz" but NOT "*.tar.gz":
<code>*.gz -*.tar.gz</code>
Sort matches by "size" column in ascending and descending order respectively:
<code>user:"John Doe" orderby:size</code>
<code>*.mp4 user:- orderby:size:d</code>
<b>Friendly reminder:</b> This window can be scrolled up!
`.trim().replace(/^ {6}/gm, '').replace(/\n/g, '<br>') `.trim().replace(/^ {6}/gm, '').replace(/\n/g, '<br>')
swal({ content })
swal({ content }).then(() => {
// Restore modal size
document.body.querySelector('.swal-overlay .swal-modal').classList.remove('is-expanded')
})
// Expand modal size
document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded')
} }
page.filterUploads = element => { page.filterUploads = element => {
@ -991,9 +1036,13 @@ page.viewUserUploads = (id, element) => {
const user = page.cache.users[id] const user = page.cache.users[id]
if (!user) return if (!user) return
element.classList.add('is-loading') element.classList.add('is-loading')
// Wrap username in quotes if it contains whitespaces
const username = user.username.includes(' ')
? `"${user.username}"`
: user.username
page.getUploads({ page.getUploads({
all: true, all: true,
filters: `user:${user.username.replace(/ /g, '\\ ')}`, filters: `user:${username}`,
trigger: document.querySelector('#itemManageUploads') trigger: document.querySelector('#itemManageUploads')
}) })
} }
@ -1682,7 +1731,9 @@ page.changeToken = (params = {}) => {
swal({ swal({
title: 'Woohoo!', title: 'Woohoo!',
text: 'Your token was successfully changed.', text: 'Your token was successfully changed.',
icon: 'success' icon: 'success',
buttons: false,
timer: 1500
}).then(() => { }).then(() => {
axios.defaults.headers.common.token = response.data.token axios.defaults.headers.common.token = response.data.token
localStorage[lsKeys.token] = response.data.token localStorage[lsKeys.token] = response.data.token
@ -1806,10 +1857,10 @@ page.getUsers = (params = {}) => {
<form class="prevent-default"> <form class="prevent-default">
<div class="field has-addons"> <div class="field has-addons">
<div class="control is-expanded"> <div class="control is-expanded">
<input id="filters" class="input is-small" type="text" placeholder="Filters (WIP)" value="${params.filters || ''}" disabled> <input id="filters" class="input is-small" type="text" placeholder="Filters (WIP)" value="${page.escape(params.filters || '')}" disabled>
</div> </div>
<div class="control"> <div class="control">
<button type="button" class="button is-small is-primary is-outlined" title="Help? (WIP)" data-action="upload-filters-help" disabled> <button type="button" class="button is-small is-primary is-outlined" title="Help? (WIP)" data-action="user-filters-help" disabled>
<span class="icon"> <span class="icon">
<i class="icon-help-circled"></i> <i class="icon-help-circled"></i>
</span> </span>
@ -1851,11 +1902,11 @@ page.getUsers = (params = {}) => {
const controls = ` const controls = `
<div class="columns"> <div class="columns">
<div class="column has-text-left"> <div class="column has-text-left">
<a class="button is-small is-primary is-outlined" title="Create user (WIP)" data-action="create-user"> <a class="button is-small is-primary is-outlined" title="Create new user" data-action="create-user">
<span class="icon"> <span class="icon">
<i class="icon-plus"></i> <i class="icon-plus"></i>
</span> </span>
<span>Create user</span> <span>Create new user</span>
</a> </a>
</div> </div>
<div class="column has-text-right"> <div class="column has-text-right">
@ -1891,7 +1942,6 @@ page.getUsers = (params = {}) => {
<thead> <thead>
<tr> <tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th> <th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th>ID</th>
<th>Username</th> <th>Username</th>
<th>Uploads</th> <th>Uploads</th>
<th>Usage</th> <th>Usage</th>
@ -1933,7 +1983,6 @@ page.getUsers = (params = {}) => {
tr.dataset.id = user.id tr.dataset.id = user.id
tr.innerHTML = ` tr.innerHTML = `
<td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${selected ? ' checked' : ''}></td> <td class="controls"><input type="checkbox" class="checkbox" title="Select" data-index="${i}" data-action="select"${selected ? ' checked' : ''}></td>
<th>${user.id}</th>
<th${enabled ? '' : ' class="is-linethrough"'}>${user.username}</td> <th${enabled ? '' : ' class="is-linethrough"'}>${user.username}</td>
<th>${user.uploads}</th> <th>${user.uploads}</th>
<td>${page.getPrettyBytes(user.usage)}</td> <td>${page.getPrettyBytes(user.usage)}</td>
@ -2133,26 +2182,37 @@ page.editUser = id => {
return swal('An error occurred!', response.data.description, 'error') return swal('An error occurred!', response.data.description, 'error')
} }
if (response.data.password) { let autoClose = true
const div = document.createElement('div') const div = document.createElement('div')
div.innerHTML = `
<p><b>${user.username}</b>'s new password is:</p> let displayName = user.username
<p><code>${response.data.password}</code></p> if (response.data.update.username !== user.username) {
div.innerHTML += `<p>${user.username} was renamed into: <b>${response.data.update.username}</b>.</p>`
autoClose = false
displayName = response.data.update.username
}
if (response.data.update.password) {
div.innerHTML += `
<p>${displayName}'s new password is:</p>
<p><code>${response.data.update.password}</code></p>
` `
autoClose = false
}
if (response.data.update.enabled !== user.enabled)
div.innerHTML += `<p>${displayName} has been ${response.data.update.enabled ? 'enabled' : 'disabled'}!</p>`
if (!div.innerHTML)
div.innerHTML = `<p>${displayName} was edited!</p>`
swal({ swal({
title: 'Success!', title: 'Success!',
icon: 'success', icon: 'success',
content: div content: div,
buttons: !autoClose,
timer: autoClose ? 1500 : null
}) })
} else if (response.data.update && response.data.update.username !== user.username) {
swal('Success!', `${user.username} was renamed into: ${response.data.update.username}.`, 'success')
} else {
swal('Success!', 'The user was edited!', 'success', {
buttons: false,
timer: 1500
})
}
page.getUsers(page.views.users) page.getUsers(page.views.users)
}).catch(page.onAxiosError) }).catch(page.onAxiosError)
}) })

View File

@ -43,9 +43,9 @@ ${headers.join(',\n')}
} }
page.getPrettyDate = date => { page.getPrettyDate = date => {
return date.getFullYear() + '-' + return date.getFullYear() + '/' +
(date.getMonth() < 9 ? '0' : '') + // month's index starts from zero (date.getMonth() < 9 ? '0' : '') + // month's index starts from zero
(date.getMonth() + 1) + '-' + (date.getMonth() + 1) + '/' +
(date.getDate() < 10 ? '0' : '') + (date.getDate() < 10 ? '0' : '') +
date.getDate() + ' ' + date.getDate() + ' ' +
(date.getHours() < 10 ? '0' : '') + (date.getHours() < 10 ? '0' : '') +
@ -72,3 +72,56 @@ page.getPrettyBytes = num => {
const pre = (si ? 'kMGTPEZY' : 'KMGTPEZY').charAt(exponent - 1) + (si ? '' : 'i') const pre = (si ? 'kMGTPEZY' : 'KMGTPEZY').charAt(exponent - 1) + (si ? '' : 'i')
return `${neg}${numStr} ${pre}B` return `${neg}${numStr} ${pre}B`
} }
page.escape = string => {
// MIT License
// Copyright(c) 2012-2013 TJ Holowaychuk
// Copyright(c) 2015 Andreas Lubbe
// Copyright(c) 2015 Tiancheng "Timothy" Gu
if (!string)
return string
const str = String(string)
const match = /["'&<>]/.exec(str)
if (!match)
return str
let escape
let html = ''
let index = 0
let lastIndex = 0
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '&quot;'
break
case 38: // &
escape = '&amp;'
break
case 39: // '
escape = '&#39;'
break
case 60: // <
escape = '&lt;'
break
case 62: // >
escape = '&gt;'
break
default:
continue
}
if (lastIndex !== index)
html += str.substring(lastIndex, index)
lastIndex = index + 1
html += escape
}
return lastIndex !== index
? html + str.substring(lastIndex, index)
: html
}

View File

@ -1,5 +1,5 @@
{ {
"1": "1587108179", "1": "1587239207",
"2": "1581416390", "2": "1581416390",
"3": "1581416390", "3": "1581416390",
"4": "1581416390", "4": "1581416390",

View File

@ -144,7 +144,7 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
"@babel/runtime@^7.7.7", "@babel/runtime@^7.8.7": "@babel/runtime@^7.7.7", "@babel/runtime@^7.9.2":
version "7.9.2" version "7.9.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
@ -286,9 +286,9 @@
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
"@types/node@*": "@types/node@*":
version "13.11.1" version "13.13.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.0.tgz#30d2d09f623fe32cde9cb582c7a6eda2788ce4a8"
integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g== integrity sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.0" version "2.4.0"
@ -930,9 +930,9 @@ buffer-from@^1.0.0:
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
buffer@^5.5.0: buffer@^5.5.0:
version "5.5.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww== integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
dependencies: dependencies:
base64-js "^1.0.2" base64-js "^1.0.2"
ieee754 "^1.1.4" ieee754 "^1.1.4"
@ -1037,14 +1037,14 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-db@^1.0.30001017: caniuse-db@^1.0.30001017:
version "1.0.30001040" version "1.0.30001042"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001040.tgz#dd0f810b5d078175e6bbddc860eb4e7917885362" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001042.tgz#ac3e6065c7c46fc0bdb0fb1a13d861af8399c3a4"
integrity sha512-rrwOYbMn4sTFNdqT4sD1gtrfOKr8CPyUEw3FMhlEYb4b+KHhBJY+a5NROsNTBDoABVHyW6v1EKG1iuJx+gosAg== integrity sha512-2RKrB2hkLCW/8Uj32oaXj0O+N9ROo0/BF0EueWHwgs6AeeSiL+rCSsbICR3ayBJOZavgcFx65ZCw7QiafsoUFQ==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001038, caniuse-lite@^1.0.30001039: caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001038, caniuse-lite@^1.0.30001039:
version "1.0.30001040" version "1.0.30001042"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001040.tgz#103fc8e6eb1d7397e95134cd0e996743353d58ea" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001042.tgz#c91ec21ec2d270bd76dbc2ce261260c292b8c93c"
integrity sha512-Ep0tEPeI5wCvmJNrXjE3etgfI+lkl1fTDU6Y3ZH1mhrjkPlVI9W4pcKbMo+BQLpEWKVYYp2EmYaRsqpPC3k7lQ== integrity sha512-igMQ4dlqnf4tWv0xjaaE02op9AJ2oQzXKjWf4EuAHFN694Uo9/EfPVIPJcmn2WkU9RqozCxx5e2KPcVClHDbDw==
caseless@~0.12.0: caseless@~0.12.0:
version "0.12.0" version "0.12.0"
@ -1183,9 +1183,9 @@ cli-cursor@^3.1.0:
restore-cursor "^3.1.0" restore-cursor "^3.1.0"
cli-width@^2.0.0: cli-width@^2.0.0:
version "2.2.0" version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
cliui@^3.2.0: cliui@^3.2.0:
version "3.2.0" version "3.2.0"
@ -1973,9 +1973,9 @@ ee-first@1.1.1:
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.390: electron-to-chromium@^1.3.390:
version "1.3.403" version "1.3.413"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.403.tgz#c8bab4e2e72bf78bc28bad1cc355c061f9cc1918" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.413.tgz#9c457a4165c7b42e59d66dff841063eb9bfe5614"
integrity sha512-JaoxV4RzdBAZOnsF4dAlZ2ijJW72MbqO5lNfOBHUWiBQl3Rwe+mk2RCUMrRI3rSClLJ8HSNQNqcry12H+0ZjFw== integrity sha512-Jm1Rrd3siqYHO3jftZwDljL2LYQafj3Kki5r+udqE58d0i91SkjItVJ5RwlJn9yko8i7MOcoidVKjQlgSdd1hg==
emoji-regex@^7.0.1: emoji-regex@^7.0.1:
version "7.0.3" version "7.0.3"
@ -2265,11 +2265,11 @@ esprima@^4.0.0:
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.0.1: esquery@^1.0.1:
version "1.2.0" version "1.3.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.2.0.tgz#a010a519c0288f2530b3404124bfb5f02e9797fe" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
integrity sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q== integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
dependencies: dependencies:
estraverse "^5.0.0" estraverse "^5.1.0"
esrecurse@^4.1.0: esrecurse@^4.1.0:
version "4.2.1" version "4.2.1"
@ -2283,10 +2283,10 @@ estraverse@^4.1.0, estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.0.0: estraverse@^5.1.0:
version "5.0.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A== integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
esutils@^2.0.2: esutils@^2.0.2:
version "2.0.3" version "2.0.3"
@ -4040,10 +4040,10 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
knex@^0.20.13: knex@^0.20.15:
version "0.20.13" version "0.20.15"
resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.13.tgz#056c310d963f7efce1b3c7397576add1323f1146" resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.15.tgz#b7e9e1efd9cf35d214440d9439ed21153574679d"
integrity sha512-YVl//Te0G5suc+d9KyeI6WuhtgVlxu6HXYQB+WqrccFkSZAbHqlqZlUMogYG3UoVq69c3kiFbbxgUNkrO0PVfg== integrity sha512-WHmvgfQfxA5v8pyb9zbskxCS1L1WmYgUbwBhHojlkmdouUOazvroUWlCr6KIKMQ8anXZh1NXOOtIUMnxENZG5Q==
dependencies: dependencies:
colorette "1.1.0" colorette "1.1.0"
commander "^4.1.1" commander "^4.1.1"
@ -6419,9 +6419,9 @@ resolve-url@^0.2.1:
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.4.0: resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2, resolve@^1.4.0:
version "1.15.1" version "1.16.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.16.1.tgz#49fac5d8bacf1fd53f200fa51247ae736175832c"
integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== integrity sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig==
dependencies: dependencies:
path-parse "^1.0.6" path-parse "^1.0.6"
@ -6527,6 +6527,11 @@ sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
search-query-parser@^1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/search-query-parser/-/search-query-parser-1.5.5.tgz#dfd829f4fb567a9a3a5b70327576dff4d5be071d"
integrity sha512-8hKmRbHShAc+LJtUu7mu4L/oWVgICG1GCzWZtlR11Vl3T/v4aTGv+Ie2bPg+8ueJn+l8zMHCQhzrBNMoV/t2qw==
semver-diff@^3.1.1: semver-diff@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
@ -6552,9 +6557,9 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.1.3: semver@^7.1.3:
version "7.2.2" version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.2.2.tgz#d01432d74ed3010a20ffaf909d63a691520521cd" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-Zo84u6o2PebMSK3zjJ6Zp5wi8VnQZnEaCP13Ul/lt1ANsLACxnJxq4EEm1PY94/por1Hm9+7xpIswdS5AkieMA== integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
send@0.17.1: send@0.17.1:
version "0.17.1" version "0.17.1"
@ -7937,16 +7942,16 @@ yallist@^4.0.0:
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.7.2: yaml@^1.7.2:
version "1.8.3" version "1.9.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.9.1.tgz#2df608ca571a0cf94e25e417e2795c08f48acdc5"
integrity sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw== integrity sha512-xbWX1ayUVoW8DPM8qxOBowac4XxSTi0mFLbiokRq880ViYglN+F3nJ4Dc2GdypXpykrknKS39d8I3lzFoHv1kA==
dependencies: dependencies:
"@babel/runtime" "^7.8.7" "@babel/runtime" "^7.9.2"
yargs-parser@^18.1.1: yargs-parser@^18.1.1:
version "18.1.2" version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ== integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies: dependencies:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"