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')
const response = { success: true, update }
if (password) response.password = password
if (password) response.update.password = password
return res.json(response)
} catch (error) {
logger.error(error)

View File

@ -4,6 +4,7 @@ const fs = require('fs')
const multer = require('multer')
const path = require('path')
const randomstring = require('randomstring')
const searchQuery = require('search-query-parser')
const paths = require('./pathsController')
const perms = require('./permissionController')
const utils = require('./utilsController')
@ -758,94 +759,172 @@ self.list = async (req, res) => {
const all = Boolean(req.headers.all)
const filters = req.headers.filters
const minoffset = req.headers.minoffset
const ismoderator = perms.is(user, 'moderator')
if ((all || filters) && !ismoderator)
return res.status(403).end()
const basedomain = config.domain
// For filtering uploads
const _filters = {
const filterObj = {
uploaders: [],
names: [],
ips: [],
flags: {
nouser: false,
noip: false
excludeUploaders: [],
queries: {
exclude: {}
},
keywords: []
flags: {}
}
// Cast column(s) to specific type if they're stored differently
const _orderByCasts = {
const orderByObj = {
// Cast columns to specific type if they are stored differently
casts: {
size: 'integer'
}
},
// Columns mapping
maps: {
date: 'timestamp',
expiry: 'expirydate'
},
// Columns with which to use SQLite's NULLS LAST option
const _orderByNullsLast = [
nullsLast: [
'userid',
'expirydate',
'ip'
]
const _orderBy = []
],
parsed: []
}
// 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(/\\\\/, '\\')
const keywords = [
'ip',
'user'
]
const ranges = [
'date',
'expiry'
]
filterObj.queries = searchQuery.parse(filters, {
keywords: keywords.concat([
'orderby'
]),
ranges,
tokenize: true,
alwaysArray: true,
offsets: false
})
.map((v, i) => {
const x = v.indexOf(':')
if (x >= 0 && v.substring(x + 1))
return [v.substring(0, x), v.substring(x + 1)]
else
return v
})
.forEach(v => {
if (Array.isArray(v)) {
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] === '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' : ''}`)
for (const key of keywords)
if (filterObj.queries[key]) {
// Make sure keyword arrays only contain unique values
filterObj.queries[key] = filterObj.queries[key].filter((v, i, a) => a.indexOf(v) === i)
// Flag to match NULL values
const index = filterObj.queries[key].indexOf('-')
if (index !== -1) {
filterObj.flags[`no${key}`] = true
filterObj.queries[key].splice(index, 1)
}
}
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 {
if (v === '-user')
_filters.flags.nouser = true
else if (v === '-ip')
_filters.flags.noip = true
else
_filters.keywords.push(v[0])
return null
}
})
_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)
.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))
if (_filters.keywords.length)
// TODO: Support filtering using keywords only
return res.json({ success: false, description: 'Filtering using keywords only is still work in progress. Please confirm valid filtering keys through the Help? button!' })
for (const uploader of uploaders)
if (filterObj.queries.user && filterObj.queries.user.includes(uploader.username))
filterObj.uploaders.push(uploader)
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 () {
if (req.params.id !== undefined)
@ -853,23 +932,53 @@ self.list = async (req, res) => {
else if (!all)
this.where('userid', user.id)
else
// Fisrt, look for uploads matching ANY of the supplied 'user' OR 'ip' filters
// Then, refine the matches using the supplied 'name' filters
// Sheesh, these look too overbearing...
this.where(function () {
if (_filters.uploaders.length)
this.orWhereIn('userid', _filters.uploaders.map(v => v.id))
if (_filters.ips.length)
this.orWhereIn('ip', _filters.ips)
if (_filters.flags.nouser)
// Filter uploads matching any of the supplied 'user' keys and/or NULL flag
if (filterObj.uploaders.length)
this.orWhereIn('userid', filterObj.uploaders.map(v => v.id))
if (filterObj.excludeUploaders.length)
this.orWhereNotIn('userid', filterObj.excludeUploaders.map(v => v.id))
if (filterObj.flags.nouser)
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')
}).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('*'))
this.orWhere('name', 'like', name.replace(/\*/g, '%'))
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
const columns = ['id', 'name', 'userid', 'size', 'timestamp']
if (temporaryUploads)
columns.push('expirydate')
// Only select IPs if we are listing all uploads
columns.push(all ? 'ip' : 'albumid')
const orderByRaw = orderByObj.parsed.length
? orderByObj.parsed.join(', ')
: '`id` desc'
const files = await db.table('files')
.where(filter)
.orderByRaw(_orderBy.length ? _orderBy.join(', ') : '`id` desc')
.orderByRaw(orderByRaw)
.limit(25)
.offset(25 * offset)
.select(columns)
@ -936,8 +1047,8 @@ self.list = async (req, res) => {
return res.json({ success: true, files, count, albums, basedomain })
// Otherwise proceed to querying usernames
let _users = _filters.uploaders
if (!_users.length) {
let usersTable = filterObj.uploaders
if (!usersTable.length) {
const userids = files
.map(file => file.userid)
.filter((v, i, a) => {
@ -949,13 +1060,13 @@ self.list = async (req, res) => {
return res.json({ success: true, files, count, basedomain })
// Query usernames of user IDs from currently selected files
_users = await db.table('users')
usersTable = await db.table('users')
.whereIn('id', userids)
.select('id', 'username')
}
const users = {}
for (const user of _users)
for (const user of usersTable)
users[user.id] = user.username
return res.json({ success: true, files, count, users, basedomain })

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -31,8 +31,10 @@ routes.get('/a/:identifier', async (req, res, next) => {
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
const cacheid = nojs ? `${album.id}-nojs` : album.id
cacheid = nojs ? `${album.id}-nojs` : album.id
if (!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.
utils.albumsCache[cacheid].generating = true
utils.albumsCache[cacheid].generatedAt = Math.floor(Date.now() / 1000)
}
const files = await db.table('files')
.select('name', 'size')
@ -89,12 +92,15 @@ routes.get('/a/:identifier', async (req, res, next) => {
files,
nojs
}, (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
}
// Express should already send error to the next handler
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
switch (action) {
// Uploads
case 'view-list':
return page.setUploadsView('list', element)
case 'view-thumbs':
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':
return page.addToAlbum(id)
case 'delete-upload':
return page.deleteUpload(id)
case 'add-selected-uploads-to-album':
return page.addSelectedUploadsToAlbum()
case 'bulk-delete-uploads':
return page.bulkDeleteUploads()
case 'display-preview':
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':
return page.submitAlbum(element)
case 'edit-album':
return page.editAlbum(id)
case 'delete-album':
return page.deleteAlbum(id)
case 'get-new-token':
return page.getNewToken(element)
// Manage users
case 'create-user':
return page.createUser()
case 'edit-user':
@ -334,12 +334,24 @@ page.domClick = event => {
return page.disableUser(id)
case 'delete-user':
return page.deleteUser(id)
case 'user-filters-help':
return page.userFiltersHelp(element)
case 'filter-uploads':
return page.filterUploads(element)
case 'view-user-uploads':
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':
return page.focusJumpToPage()
case 'page-prev':
@ -429,6 +441,11 @@ page.getUploads = (params = {}) => {
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 => {
if (response.data.success === false)
if (response.data.description === 'No token provided') {
@ -464,10 +481,10 @@ page.getUploads = (params = {}) => {
<form class="prevent-default">
<div class="field has-addons">
<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 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">
<i class="icon-help-circled"></i>
</span>
@ -666,7 +683,7 @@ page.getUploads = (params = {}) => {
<thead>
<tr>
<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>` : ''}
<th>Size</th>
${params.all ? '<th>IP</th>' : ''}
@ -946,40 +963,68 @@ page.clearSelection = () => {
})
}
page.userFiltersHelp = element => {
page.uploadFiltersHelp = element => {
const content = document.createElement('div')
content.style = 'text-align: left'
content.innerHTML = `
This supports 3 filter keys, namely <b>user</b> (username), <b>ip</b> and <b>name</b> (upload name).
Each key can be specified more than once.
Backslashes should be used if the username contain whitespaces.
There are 2 filter keys, namely <b>user</b> (username) and <b>ip</b>.
These keys can be specified more than once.
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.
This key requires using the internal column names used in the database (size, timestamp, expirydate, and so on).
To exclude certain users/ips while still listing every other uploads, add negation sign (<b>-</b>) before the keys.
<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.
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>
First, it will filter uploads matching ANY of the supplied filter keys AND/OR special flags, if any.
Second, it will refine the matches using the supplied <b>name</b> keys, if any.
Third, it will sort the matches using the supplied <b>orderby</b> keys, 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>date</b> AND/OR <b>expiry</b> range 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>
Uploads from user with username "demo":
Uploads from user named "demo":
<code>user:demo</code>
Uploads from users with username "John Doe" AND/OR "demo":
<code>user:John\\ Doe user:demo</code>
Uploads from IP "127.0.0.1" AND which upload names match "*.rar" OR "*.zip":
<code>ip:127.0.0.1 name:*.rar name:*.zip</code>
Uploads from user with username "test" AND/OR from non-registered users:
<code>user:test -user</code>
Sort results by "size" column in ascending and descending order respectively:
<code>orderby:expirydate</code>
<code>user:demo orderby:size</code>
<code>-user name:*.mp4 orderby:size:desc</code>
Uploads from users named "demo" AND/OR "John Doe" AND/OR non-registered users:
<code>user:demo user:"John Doe" user:-</code>
ALL uploads, including from non-registered users, but NOT the ones from user named "demo":
<code>-user:demo user:-</code>
Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip":
<code>ip:127.0.0.1 *.rar *.zip</code>
Uploads uploaded since "1 June 2019 00:00:00":
<code>date:2019/06</code>
Uploads uploaded between "7 April 2020 00:00:00" and "7 April 2020 23:59:59":
<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>')
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 => {
@ -991,9 +1036,13 @@ page.viewUserUploads = (id, element) => {
const user = page.cache.users[id]
if (!user) return
element.classList.add('is-loading')
// Wrap username in quotes if it contains whitespaces
const username = user.username.includes(' ')
? `"${user.username}"`
: user.username
page.getUploads({
all: true,
filters: `user:${user.username.replace(/ /g, '\\ ')}`,
filters: `user:${username}`,
trigger: document.querySelector('#itemManageUploads')
})
}
@ -1682,7 +1731,9 @@ page.changeToken = (params = {}) => {
swal({
title: 'Woohoo!',
text: 'Your token was successfully changed.',
icon: 'success'
icon: 'success',
buttons: false,
timer: 1500
}).then(() => {
axios.defaults.headers.common.token = response.data.token
localStorage[lsKeys.token] = response.data.token
@ -1806,10 +1857,10 @@ page.getUsers = (params = {}) => {
<form class="prevent-default">
<div class="field has-addons">
<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 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">
<i class="icon-help-circled"></i>
</span>
@ -1851,11 +1902,11 @@ page.getUsers = (params = {}) => {
const controls = `
<div class="columns">
<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">
<i class="icon-plus"></i>
</span>
<span>Create user</span>
<span>Create new user</span>
</a>
</div>
<div class="column has-text-right">
@ -1891,7 +1942,6 @@ page.getUsers = (params = {}) => {
<thead>
<tr>
<th><input id="selectAll" class="checkbox" type="checkbox" title="Select all" data-action="select-all"></th>
<th>ID</th>
<th>Username</th>
<th>Uploads</th>
<th>Usage</th>
@ -1933,7 +1983,6 @@ page.getUsers = (params = {}) => {
tr.dataset.id = user.id
tr.innerHTML = `
<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>${user.uploads}</th>
<td>${page.getPrettyBytes(user.usage)}</td>
@ -2133,26 +2182,37 @@ page.editUser = id => {
return swal('An error occurred!', response.data.description, 'error')
}
if (response.data.password) {
let autoClose = true
const div = document.createElement('div')
div.innerHTML = `
<p><b>${user.username}</b>'s new password is:</p>
<p><code>${response.data.password}</code></p>
let displayName = user.username
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({
title: '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)
}).catch(page.onAxiosError)
})

View File

@ -43,9 +43,9 @@ ${headers.join(',\n')}
}
page.getPrettyDate = date => {
return date.getFullYear() + '-' +
return date.getFullYear() + '/' +
(date.getMonth() < 9 ? '0' : '') + // month's index starts from zero
(date.getMonth() + 1) + '-' +
(date.getMonth() + 1) + '/' +
(date.getDate() < 10 ? '0' : '') +
date.getDate() + ' ' +
(date.getHours() < 10 ? '0' : '') +
@ -72,3 +72,56 @@ page.getPrettyBytes = num => {
const pre = (si ? 'kMGTPEZY' : 'KMGTPEZY').charAt(exponent - 1) + (si ? '' : 'i')
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",
"3": "1581416390",
"4": "1581416390",

View File

@ -144,7 +144,7 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
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"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
@ -286,9 +286,9 @@
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
"@types/node@*":
version "13.11.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7"
integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==
version "13.13.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.0.tgz#30d2d09f623fe32cde9cb582c7a6eda2788ce4a8"
integrity sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@ -930,9 +930,9 @@ buffer-from@^1.0.0:
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
buffer@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce"
integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==
version "5.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
@ -1037,14 +1037,14 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-db@^1.0.30001017:
version "1.0.30001040"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001040.tgz#dd0f810b5d078175e6bbddc860eb4e7917885362"
integrity sha512-rrwOYbMn4sTFNdqT4sD1gtrfOKr8CPyUEw3FMhlEYb4b+KHhBJY+a5NROsNTBDoABVHyW6v1EKG1iuJx+gosAg==
version "1.0.30001042"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001042.tgz#ac3e6065c7c46fc0bdb0fb1a13d861af8399c3a4"
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:
version "1.0.30001040"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001040.tgz#103fc8e6eb1d7397e95134cd0e996743353d58ea"
integrity sha512-Ep0tEPeI5wCvmJNrXjE3etgfI+lkl1fTDU6Y3ZH1mhrjkPlVI9W4pcKbMo+BQLpEWKVYYp2EmYaRsqpPC3k7lQ==
version "1.0.30001042"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001042.tgz#c91ec21ec2d270bd76dbc2ce261260c292b8c93c"
integrity sha512-igMQ4dlqnf4tWv0xjaaE02op9AJ2oQzXKjWf4EuAHFN694Uo9/EfPVIPJcmn2WkU9RqozCxx5e2KPcVClHDbDw==
caseless@~0.12.0:
version "0.12.0"
@ -1183,9 +1183,9 @@ cli-cursor@^3.1.0:
restore-cursor "^3.1.0"
cli-width@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
cliui@^3.2.0:
version "3.2.0"
@ -1973,9 +1973,9 @@ ee-first@1.1.1:
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.390:
version "1.3.403"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.403.tgz#c8bab4e2e72bf78bc28bad1cc355c061f9cc1918"
integrity sha512-JaoxV4RzdBAZOnsF4dAlZ2ijJW72MbqO5lNfOBHUWiBQl3Rwe+mk2RCUMrRI3rSClLJ8HSNQNqcry12H+0ZjFw==
version "1.3.413"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.413.tgz#9c457a4165c7b42e59d66dff841063eb9bfe5614"
integrity sha512-Jm1Rrd3siqYHO3jftZwDljL2LYQafj3Kki5r+udqE58d0i91SkjItVJ5RwlJn9yko8i7MOcoidVKjQlgSdd1hg==
emoji-regex@^7.0.1:
version "7.0.3"
@ -2265,11 +2265,11 @@ esprima@^4.0.0:
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.2.0.tgz#a010a519c0288f2530b3404124bfb5f02e9797fe"
integrity sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==
version "1.3.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
dependencies:
estraverse "^5.0.0"
estraverse "^5.1.0"
esrecurse@^4.1.0:
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"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22"
integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==
estraverse@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
esutils@^2.0.2:
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"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
knex@^0.20.13:
version "0.20.13"
resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.13.tgz#056c310d963f7efce1b3c7397576add1323f1146"
integrity sha512-YVl//Te0G5suc+d9KyeI6WuhtgVlxu6HXYQB+WqrccFkSZAbHqlqZlUMogYG3UoVq69c3kiFbbxgUNkrO0PVfg==
knex@^0.20.15:
version "0.20.15"
resolved "https://registry.yarnpkg.com/knex/-/knex-0.20.15.tgz#b7e9e1efd9cf35d214440d9439ed21153574679d"
integrity sha512-WHmvgfQfxA5v8pyb9zbskxCS1L1WmYgUbwBhHojlkmdouUOazvroUWlCr6KIKMQ8anXZh1NXOOtIUMnxENZG5Q==
dependencies:
colorette "1.1.0"
commander "^4.1.1"
@ -6419,9 +6419,9 @@ resolve-url@^0.2.1:
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:
version "1.15.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
version "1.16.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.16.1.tgz#49fac5d8bacf1fd53f200fa51247ae736175832c"
integrity sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig==
dependencies:
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"
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:
version "3.1.1"
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==
semver@^7.1.3:
version "7.2.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.2.2.tgz#d01432d74ed3010a20ffaf909d63a691520521cd"
integrity sha512-Zo84u6o2PebMSK3zjJ6Zp5wi8VnQZnEaCP13Ul/lt1ANsLACxnJxq4EEm1PY94/por1Hm9+7xpIswdS5AkieMA==
version "7.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
send@0.17.1:
version "0.17.1"
@ -7937,16 +7942,16 @@ yallist@^4.0.0:
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.7.2:
version "1.8.3"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a"
integrity sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==
version "1.9.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.9.1.tgz#2df608ca571a0cf94e25e417e2795c08f48acdc5"
integrity sha512-xbWX1ayUVoW8DPM8qxOBowac4XxSTi0mFLbiokRq880ViYglN+F3nJ4Dc2GdypXpykrknKS39d8I3lzFoHv1kA==
dependencies:
"@babel/runtime" "^7.8.7"
"@babel/runtime" "^7.9.2"
yargs-parser@^18.1.1:
version "18.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1"
integrity sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"