mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-19 01:31:34 +00:00
Initialize upload filters for regular users (WIP)
Updated ESLint's ECMA version to 9 (2018). I'll need to use some lookbehind regex directives from now on. It's supported since Node 10, which is the oldest version I'll support. Refactored "can not" -> "cannot". Filtering for regular users is still work in progress. Some features aren't working as expected yet.
This commit is contained in:
parent
0eb425e216
commit
1980d536db
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 8
|
"ecmaVersion": 9
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"node": true
|
"node": true
|
||||||
|
@ -761,11 +761,16 @@ self.list = async (req, res) => {
|
|||||||
const filters = req.headers.filters
|
const filters = req.headers.filters
|
||||||
const minoffset = req.headers.minoffset
|
const minoffset = req.headers.minoffset
|
||||||
const ismoderator = perms.is(user, 'moderator')
|
const ismoderator = perms.is(user, 'moderator')
|
||||||
if ((all || filters) && !ismoderator)
|
if (all && !ismoderator)
|
||||||
return res.status(403).end()
|
return res.status(403).end()
|
||||||
|
|
||||||
const basedomain = config.domain
|
const basedomain = config.domain
|
||||||
|
|
||||||
|
// Thresholds for regular users
|
||||||
|
const MAX_WILDCARDS_IN_KEY = 2
|
||||||
|
const MAX_TEXT_QUERIES = 3 // non-keyed keywords
|
||||||
|
const MAX_SORT_KEYS = 1
|
||||||
|
|
||||||
const filterObj = {
|
const filterObj = {
|
||||||
uploaders: [],
|
uploaders: [],
|
||||||
excludeUploaders: [],
|
excludeUploaders: [],
|
||||||
@ -794,15 +799,47 @@ self.list = async (req, res) => {
|
|||||||
parsed: []
|
parsed: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse glob wildcards into SQL wildcards
|
||||||
|
function sqlLikeParser (pattern) {
|
||||||
|
// Escape SQL operators
|
||||||
|
const escaped = pattern
|
||||||
|
.replace(/(?<!\\)%/g, '\\%')
|
||||||
|
.replace(/(?<!\\)_/g, '\\_')
|
||||||
|
|
||||||
|
// Look for any glob operators
|
||||||
|
const match = pattern.match(/(?<!\\)(\*|\?)/g)
|
||||||
|
if (match && match.length) {
|
||||||
|
logger.debug(pattern, match)
|
||||||
|
return {
|
||||||
|
count: match.length,
|
||||||
|
// Replace glob operators with their SQL equivalents
|
||||||
|
escaped: escaped
|
||||||
|
.replace(/(?<!\\)\*/g, '%')
|
||||||
|
.replace(/(?<!\\)\?/g, '_')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
// Assume partial match
|
||||||
|
escaped: `%${escaped}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (filters) {
|
if (filters) {
|
||||||
const keywords = [
|
let keywords = []
|
||||||
'ip',
|
// Only allow filtering by 'ip' and 'user' keys when listing all uploads
|
||||||
'user'
|
if (all)
|
||||||
]
|
keywords = keywords.concat([
|
||||||
|
'ip',
|
||||||
|
'user'
|
||||||
|
])
|
||||||
|
|
||||||
const ranges = [
|
const ranges = [
|
||||||
'date',
|
'date',
|
||||||
'expiry'
|
'expiry'
|
||||||
]
|
]
|
||||||
|
|
||||||
filterObj.queries = searchQuery.parse(filters, {
|
filterObj.queries = searchQuery.parse(filters, {
|
||||||
keywords: keywords.concat([
|
keywords: keywords.concat([
|
||||||
'sort'
|
'sort'
|
||||||
@ -813,6 +850,45 @@ self.list = async (req, res) => {
|
|||||||
offsets: false
|
offsets: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
|
||||||
|
let textQueries = 0
|
||||||
|
if (filterObj.queries.text) textQueries += filterObj.queries.text.length
|
||||||
|
if (filterObj.queries.exclude.text) textQueries += filterObj.queries.exclude.text.length
|
||||||
|
|
||||||
|
// Regular user threshold check
|
||||||
|
if (!ismoderator && textQueries > MAX_TEXT_QUERIES)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
description: `Users are only allowed to use ${MAX_TEXT_QUERIES} non-keyed keyword${MAX_TEXT_QUERIES === 1 ? '' : 's'} at a time.`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (filterObj.queries.text)
|
||||||
|
for (let i = 0; i < filterObj.queries.text.length; i++) {
|
||||||
|
const result = sqlLikeParser(filterObj.queries.text[i])
|
||||||
|
if (result.count > MAX_WILDCARDS_IN_KEY)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
description: `Users are only allowed to use ${MAX_WILDCARDS_IN_KEY} wildcard${MAX_WILDCARDS_IN_KEY === 1 ? '' : 's'} per key.`
|
||||||
|
})
|
||||||
|
filterObj.queries.text[i] = result.escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterObj.queries.exclude.text) {
|
||||||
|
textQueries += filterObj.queries.exclude.text.length
|
||||||
|
for (let i = 0; i < filterObj.queries.exclude.text.length; i++) {
|
||||||
|
const result = sqlLikeParser(filterObj.queries.exclude.text[i])
|
||||||
|
if (result.count > MAX_WILDCARDS_IN_KEY)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
description: `Users are only allowed to use ${MAX_WILDCARDS_IN_KEY} wildcard${MAX_WILDCARDS_IN_KEY === 1 ? '' : 's'} per key.`
|
||||||
|
})
|
||||||
|
filterObj.queries.exclude.text[i] = result.escaped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const key of keywords) {
|
for (const key of keywords) {
|
||||||
let queryIndex = -1
|
let queryIndex = -1
|
||||||
let excludeIndex = -1
|
let excludeIndex = -1
|
||||||
@ -833,8 +909,18 @@ self.list = async (req, res) => {
|
|||||||
if (inQuery || inExclude) {
|
if (inQuery || inExclude) {
|
||||||
// Prioritize exclude keys when both types found
|
// Prioritize exclude keys when both types found
|
||||||
filterObj.flags[`${key}Null`] = inExclude ? false : inQuery
|
filterObj.flags[`${key}Null`] = inExclude ? false : inQuery
|
||||||
if (inQuery) filterObj.queries[key].splice(queryIndex, 1)
|
if (inQuery)
|
||||||
if (inExclude) filterObj.queries.exclude[key].splice(excludeIndex, 1)
|
if (filterObj.queries[key].length === 1)
|
||||||
|
// Delete key to avoid unexpected behavior
|
||||||
|
delete filterObj.queries[key]
|
||||||
|
else
|
||||||
|
filterObj.queries[key].splice(queryIndex, 1)
|
||||||
|
if (inExclude)
|
||||||
|
if (filterObj.queries.exclude[key].length === 1)
|
||||||
|
// Delete key to avoid unexpected behavior
|
||||||
|
delete filterObj.queries.exclude[key]
|
||||||
|
else
|
||||||
|
filterObj.queries.exclude[key].splice(excludeIndex, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -909,107 +995,137 @@ self.list = async (req, res) => {
|
|||||||
else
|
else
|
||||||
filterObj.excludeUploaders.push(uploader)
|
filterObj.excludeUploaders.push(uploader)
|
||||||
|
|
||||||
|
// Delete keys to avoid unexpected behavior
|
||||||
delete filterObj.queries.user
|
delete filterObj.queries.user
|
||||||
delete filterObj.queries.exclude.user
|
delete filterObj.queries.exclude.user
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse sort keys
|
// Parse sort keys
|
||||||
if (filterObj.queries.sort) {
|
if (filterObj.queries.sort) {
|
||||||
|
let allowed = [
|
||||||
|
'albumid',
|
||||||
|
'expirydate',
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'size',
|
||||||
|
'timestamp'
|
||||||
|
]
|
||||||
|
// Only allow sorting by 'ip' and 'userid' columns when listing all uploads
|
||||||
|
if (all)
|
||||||
|
allowed = allowed.concat([
|
||||||
|
'ip',
|
||||||
|
'userid'
|
||||||
|
])
|
||||||
|
|
||||||
for (const obQuery of filterObj.queries.sort) {
|
for (const obQuery of filterObj.queries.sort) {
|
||||||
const tmp = obQuery.toLowerCase().split(':')
|
const tmp = obQuery.toLowerCase().split(':')
|
||||||
|
const column = sortObj.maps[tmp[0]] || tmp[0]
|
||||||
|
|
||||||
let column = sortObj.maps[tmp[0]] || tmp[0]
|
if (!allowed.includes(column))
|
||||||
let direction = 'asc'
|
// Alert users if using disallowed/missing columns
|
||||||
|
return res.json({ success: false, description: `Column \`${column}\` cannot be used for sorting.\n\nTry the following instead:\n${allowed.join(', ')}` })
|
||||||
|
|
||||||
if (sortObj.casts[column])
|
sortObj.parsed.push({
|
||||||
column = `cast (\`${column}\` as ${sortObj.casts[column]})`
|
column,
|
||||||
if (tmp[1] && /^d/.test(tmp[1]))
|
order: (tmp[1] && /^d/.test(tmp[1])) ? 'desc' : 'asc',
|
||||||
direction = 'desc'
|
clause: sortObj.nullsLast.includes(column) ? 'nulls last' : '',
|
||||||
|
cast: sortObj.casts[column] || null
|
||||||
const suffix = sortObj.nullsLast.includes(column) ? ' nulls last' : ''
|
})
|
||||||
sortObj.parsed.push(`${column} ${direction}${suffix}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regular user threshold check
|
||||||
|
if (!ismoderator && sortObj.parsed.length > MAX_SORT_KEYS)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
description: `Users are only allowed to use ${MAX_SORT_KEYS} sort key${MAX_SORT_KEYS === 1 ? '' : 's'} at a time.`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete key to avoid unexpected behavior
|
||||||
delete filterObj.queries.sort
|
delete filterObj.queries.sort
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
this.where('albumid', req.params.id)
|
this.where('albumid', req.params.id)
|
||||||
else if (!all)
|
} else {
|
||||||
this.where('userid', user.id)
|
// Sheesh, these look so overbearing...
|
||||||
else
|
if (!all)
|
||||||
// Sheesh, these look too overbearing...
|
// If not listing all uploads, list user's uploads
|
||||||
this.where(function () {
|
this.where('userid', user.id)
|
||||||
// Filter uploads matching any of the supplied 'user' keys and/or NULL flag
|
else
|
||||||
// Prioritze exclude keys when both types found
|
this.where(function () {
|
||||||
if (filterObj.excludeUploaders.length)
|
// Filter uploads matching any of the supplied 'user' keys and/or NULL flag
|
||||||
this.orWhereNotIn('userid', filterObj.excludeUploaders.map(v => v.id))
|
// Prioritze exclude keys when both types found
|
||||||
else if (filterObj.uploaders.length)
|
this.orWhere(function () {
|
||||||
this.orWhereIn('userid', filterObj.uploaders.map(v => v.id))
|
if (filterObj.excludeUploaders.length)
|
||||||
// Such overbearing logic for NULL values, smh...
|
this.orWhereNotIn('userid', filterObj.excludeUploaders.map(v => v.id))
|
||||||
if ((filterObj.excludeUploaders.length && filterObj.flags.userNull !== false) ||
|
else if (filterObj.uploaders.length)
|
||||||
(filterObj.uploaders.length && filterObj.flags.userNull) ||
|
this.orWhereIn('userid', filterObj.uploaders.map(v => v.id))
|
||||||
(!filterObj.excludeUploaders.length && !filterObj.uploaders.length && filterObj.flags.userNull))
|
// Such overbearing logic for NULL values, smh...
|
||||||
this.orWhereNull('userid')
|
if ((filterObj.excludeUploaders.length && filterObj.flags.userNull !== false) ||
|
||||||
else if (filterObj.flags.userNull === false)
|
(filterObj.uploaders.length && filterObj.flags.userNull) ||
|
||||||
this.orWhereNotNull('userid')
|
(!filterObj.excludeUploaders.length && !filterObj.uploaders.length && filterObj.flags.userNull))
|
||||||
}).orWhere(function () {
|
this.orWhereNull('userid')
|
||||||
// Filter uploads matching any of the supplied 'ip' keys and/or NULL flag
|
else if (filterObj.flags.userNull === false)
|
||||||
// Same prioritization logics as above
|
this.orWhereNotNull('userid')
|
||||||
if (filterObj.queries.exclude.ip)
|
})
|
||||||
this.orWhereNotIn('ip', filterObj.queries.exclude.ip)
|
|
||||||
else if (filterObj.queries.ip)
|
// Filter uploads matching any of the supplied 'ip' keys and/or NULL flag
|
||||||
this.orWhereIn('ip', filterObj.queries.ip)
|
// Same prioritization logic as above
|
||||||
// ...
|
this.orWhere(function () {
|
||||||
if ((filterObj.queries.exclude.ip && filterObj.flags.ipNull !== false) ||
|
if (filterObj.queries.exclude.ip)
|
||||||
(filterObj.queries.ip && filterObj.flags.ipNull) ||
|
this.orWhereNotIn('ip', filterObj.queries.exclude.ip)
|
||||||
(!filterObj.queries.exclude.ip && !filterObj.queries.ip && filterObj.flags.ipNull))
|
else if (filterObj.queries.ip)
|
||||||
this.orWhereNull('ip')
|
this.orWhereIn('ip', filterObj.queries.ip)
|
||||||
else if (filterObj.flags.ipNull === false)
|
// ...
|
||||||
this.orWhereNotNull('ip')
|
if ((filterObj.queries.exclude.ip && filterObj.flags.ipNull !== false) ||
|
||||||
}).andWhere(function () {
|
(filterObj.queries.ip && filterObj.flags.ipNull) ||
|
||||||
// Then, refine using the supplied 'date' and/or 'expiry' ranges
|
(!filterObj.queries.exclude.ip && !filterObj.queries.ip && filterObj.flags.ipNull))
|
||||||
if (filterObj.queries.date)
|
this.orWhereNull('ip')
|
||||||
if (typeof filterObj.queries.date.from === 'number')
|
else if (filterObj.flags.ipNull === false)
|
||||||
if (typeof filterObj.queries.date.to === 'number')
|
this.orWhereNotNull('ip')
|
||||||
this.andWhereBetween('timestamp', [filterObj.queries.date.from, filterObj.queries.date.to])
|
})
|
||||||
else
|
})
|
||||||
this.andWhere('timestamp', '>=', filterObj.queries.date.from)
|
|
||||||
|
// Then, refine using the supplied 'date' ranges
|
||||||
|
this.andWhere(function () {
|
||||||
|
if (!filterObj.queries.date) return
|
||||||
|
if (typeof filterObj.queries.date.from === 'number')
|
||||||
|
if (typeof filterObj.queries.date.to === 'number')
|
||||||
|
this.andWhereBetween('timestamp', [filterObj.queries.date.from, filterObj.queries.date.to])
|
||||||
else
|
else
|
||||||
this.andWhere('timestamp', '<=', filterObj.queries.date.to)
|
this.andWhere('timestamp', '>=', filterObj.queries.date.from)
|
||||||
if (filterObj.queries.expiry)
|
else
|
||||||
if (typeof filterObj.queries.expiry.from === 'number')
|
this.andWhere('timestamp', '<=', filterObj.queries.date.to)
|
||||||
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)
|
|
||||||
else
|
|
||||||
this.andWhere('expirydate', '<=', filterObj.queries.date.to)
|
|
||||||
}).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
|
|
||||||
// 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.andWhere('name', 'not like', exclude.replace(/\*/g, '%'))
|
|
||||||
else
|
|
||||||
// If no asterisks, assume partial
|
|
||||||
this.andWhere('name', 'not like', `%${exclude}%`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Then, refine using the supplied 'expiry' ranges
|
||||||
|
this.andWhere(function () {
|
||||||
|
if (!filterObj.queries.expiry) return
|
||||||
|
if (typeof filterObj.queries.expiry.from === 'number')
|
||||||
|
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)
|
||||||
|
else
|
||||||
|
this.andWhere('expirydate', '<=', filterObj.queries.date.to)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then, refine using the supplied keywords against their file names
|
||||||
|
this.andWhere(function () {
|
||||||
|
if (!filterObj.queries.text) return
|
||||||
|
for (const pattern of filterObj.queries.text)
|
||||||
|
this.orWhere('name', 'like', pattern)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Finally, refine using the supplied exclusions against their file names
|
||||||
|
this.andWhere(function () {
|
||||||
|
if (!filterObj.queries.exclude.text) return
|
||||||
|
for (const pattern of filterObj.queries.exclude.text)
|
||||||
|
this.orWhere('name', 'not like', pattern)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1032,12 +1148,22 @@ self.list = async (req, res) => {
|
|||||||
// 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 sortRaw = sortObj.parsed.length
|
// Build raw query for order by (sorting) operation
|
||||||
? sortObj.parsed.join(', ')
|
let orderByRaw
|
||||||
: '`id` desc'
|
if (sortObj.parsed.length)
|
||||||
|
orderByRaw = sortObj.parsed.map(sort => {
|
||||||
|
// Use Knex.raw() to sanitize user inputs
|
||||||
|
if (sort.cast)
|
||||||
|
return db.raw(`cast (?? as ${sort.cast}) ${sort.order} ${sort.clause}`.trim(), sort.column)
|
||||||
|
else
|
||||||
|
return db.raw(`?? ${sort.order} ${sort.clause}`.trim(), sort.column)
|
||||||
|
}).join(', ')
|
||||||
|
else
|
||||||
|
orderByRaw = '`id` desc'
|
||||||
|
|
||||||
const files = await db.table('files')
|
const files = await db.table('files')
|
||||||
.where(filter)
|
.where(filter)
|
||||||
.orderByRaw(sortRaw)
|
.orderByRaw(orderByRaw)
|
||||||
.limit(25)
|
.limit(25)
|
||||||
.offset(25 * offset)
|
.offset(25 * offset)
|
||||||
.select(columns)
|
.select(columns)
|
||||||
|
2
dist/js/dashboard.js
vendored
2
dist/js/dashboard.js
vendored
File diff suppressed because one or more lines are too long
2
dist/js/dashboard.js.map
vendored
2
dist/js/dashboard.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/js/home.js.map
vendored
2
dist/js/home.js.map
vendored
File diff suppressed because one or more lines are too long
@ -457,8 +457,8 @@ page.getUploads = (params = {}) => {
|
|||||||
if (params === undefined)
|
if (params === undefined)
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
if ((params.all || params.filters) && !page.permissions.moderator)
|
if (params.all && !page.permissions.moderator)
|
||||||
return swal('An error occurred!', 'You can not do this!', 'error')
|
return swal('An error occurred!', 'You cannot do this!', 'error')
|
||||||
|
|
||||||
page.updateTrigger(params.trigger, 'loading')
|
page.updateTrigger(params.trigger, 'loading')
|
||||||
|
|
||||||
@ -474,9 +474,9 @@ page.getUploads = (params = {}) => {
|
|||||||
filters: params.filters || ''
|
filters: params.filters || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send client timezone offset if using date filter
|
// Send client timezone offset if using filters
|
||||||
// Server will pretend client is on UTC if missing
|
// Server will pretend client is on UTC if missing
|
||||||
if (headers.filters.includes('date:') || headers.filters.includes('expiry:'))
|
if (headers.filters)
|
||||||
headers.minOffset = new Date().getTimezoneOffset()
|
headers.minOffset = new Date().getTimezoneOffset()
|
||||||
|
|
||||||
axios.get(url, { headers }).then(response => {
|
axios.get(url, { headers }).then(response => {
|
||||||
@ -507,33 +507,31 @@ page.getUploads = (params = {}) => {
|
|||||||
const basedomain = response.data.basedomain
|
const basedomain = response.data.basedomain
|
||||||
const pagination = page.paginate(response.data.count, 25, params.pageNum)
|
const pagination = page.paginate(response.data.count, 25, params.pageNum)
|
||||||
|
|
||||||
let filter = '<div class="column is-hidden-mobile"></div>'
|
const filter = `
|
||||||
if (params.all)
|
<div class="column">
|
||||||
filter = `
|
<form class="prevent-default">
|
||||||
<div class="column">
|
<div class="field has-addons">
|
||||||
<form class="prevent-default">
|
<div class="control is-expanded">
|
||||||
<div class="field has-addons">
|
<input id="filters" class="input is-small" type="text" placeholder="Filters" value="${page.escape(params.filters || '')}">
|
||||||
<div class="control is-expanded">
|
|
||||||
<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="upload-filters-help">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="icon-help-circled"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button type="submit" class="button is-small is-info is-outlined" title="Filter uploads" data-action="filter-uploads">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="icon-filter"></i>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div class="control">
|
||||||
</div>
|
<button type="button" class="button is-small is-primary is-outlined" title="Help?" data-action="upload-filters-help"${params.all ? ' data-all="true"' : ''}">
|
||||||
`
|
<span class="icon">
|
||||||
|
<i class="icon-help-circled"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-small is-info is-outlined" title="Filter uploads" data-action="filter-uploads">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="icon-filter"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
const extraControls = `
|
const extraControls = `
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
${filter}
|
${filter}
|
||||||
@ -1012,9 +1010,10 @@ page.clearSelection = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
page.uploadFiltersHelp = element => {
|
page.uploadFiltersHelp = element => {
|
||||||
|
const all = Boolean(element.dataset.all)
|
||||||
const content = document.createElement('div')
|
const content = document.createElement('div')
|
||||||
content.style = 'text-align: left'
|
content.style = 'text-align: left'
|
||||||
content.innerHTML = `
|
content.innerHTML = `${all ? `
|
||||||
There are 2 filter keys, namely <b>user</b> (username) and <b>ip</b>.
|
There are 2 filter keys, namely <b>user</b> (username) and <b>ip</b>.
|
||||||
These keys can be specified more than once.
|
These keys can be specified more than once.
|
||||||
For usernames with whitespaces, wrap them with double quotes (<code>"</code>).
|
For usernames with whitespaces, wrap them with double quotes (<code>"</code>).
|
||||||
@ -1022,10 +1021,12 @@ page.uploadFiltersHelp = element => {
|
|||||||
|
|
||||||
To exclude certain users/ips while still listing every other uploads, add negation sign (<code>-</code>) before the keys.
|
To exclude certain users/ips while still listing every other uploads, add negation sign (<code>-</code>) before the keys.
|
||||||
Negation sign can also be used to exclude the special cases mentioned above (i.e. <code>-user:-</code> or <code>-ip:-</code>).
|
Negation sign can also be used to exclude the special cases mentioned above (i.e. <code>-user:-</code> or <code>-ip:-</code>).
|
||||||
|
` : ''}
|
||||||
There are 2 range keys: <b>date</b> (upload date) and <b>expiry</b> (expiry date).
|
There are 2 range keys: <b>date</b> (upload date) and <b>expiry</b> (expiry date).
|
||||||
Their format is: <code>YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS</code> ("from" date and "to" date respectively).
|
Their format is: <code>YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS</code> ("from" date and "to" date respectively).
|
||||||
You can specify only one date. If "to" date is missing, 'now' will be used. If "from" date is missing, 'beginning of time' will be used.
|
You may specify only one of the dates.
|
||||||
|
If "to" date is missing, 'now' will be used.
|
||||||
|
If "from" date is missing, 'beginning of time' will be used.
|
||||||
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 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.
|
If only time is specified, today's date will be used.
|
||||||
In conclusion, the following examples are all valid: <code>date:2020/01/01 01:23-2018/01/01 06</code>, <code>expiry:-2020/05</code>, <code>date:12:34:56</code>.
|
In conclusion, the following examples are all valid: <code>date:2020/01/01 01:23-2018/01/01 06</code>, <code>expiry:-2020/05</code>, <code>date:12:34:56</code>.
|
||||||
@ -1036,41 +1037,44 @@ page.uploadFiltersHelp = element => {
|
|||||||
|
|
||||||
Matches can also be sorted with <b>sort</b> keys.
|
Matches can also be sorted with <b>sort</b> keys.
|
||||||
Its format is: <code>sort:columnName[:d[escending]]</code>, where <code>:d[escending]</code> is an optional tag to set the direction to descending.
|
Its format is: <code>sort:columnName[:d[escending]]</code>, where <code>:d[escending]</code> is an optional tag to set the direction to descending.
|
||||||
This key must be used with internal column names used in the database (<code>id</code>, <code>userid</code>, and so on),
|
This key must be used with internal column names used in the database (<code>id</code>, <code>${all ? 'userid' : 'albumid'}</code>, and so on),
|
||||||
but there are 2 shortcuts available: <b>date</b> for <code>timestamp</code> column and <b>expiry</b> for <code>expirydate</code> column.
|
but there are 2 shortcuts available: <b>date</b> for <code>timestamp</code> column and <b>expiry</b> for <code>expirydate</code> 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 (non-keyed keywords) will be matched against the matches' file names.
|
Any leftover keywords which do not use keys (non-keyed keywords) will be matched against the matches' file names.
|
||||||
Excluding certain keywords is also supported by adding negation sign (<b>-</b>) before the keywords.
|
Excluding certain keywords is also supported by adding negation sign (<b>-</b>) before the keywords.
|
||||||
|
|
||||||
<b>Internals:</b>
|
<b>Internal steps:</b>
|
||||||
First, query uploads passing ALL exclusion filter keys OR matching ANY filter keys, if any.
|
${all ? `- Query uploads passing ALL exclusion filter keys OR matching ANY filter keys, if any.
|
||||||
Second, refine matches using range keys, if any.
|
- Refine matches` : '- Filter uploads'} using date key, if any.
|
||||||
Third, refine matches using ANY non-keyed keywords, if any.
|
- Refine matches using expiry key, if any.
|
||||||
Fourth, filter matches using ALL exclusion non-keyed keywords, if any.
|
- Refine matches using ANY non-keyed keywords, if any.
|
||||||
Fifth, sort matches using sorting keys, if any.
|
- Filter matches using ALL exclusion non-keyed keywords, if any.
|
||||||
|
- Sort matches using sorting keys, if any.
|
||||||
|
|
||||||
<b>Examples:</b>
|
<b>Examples:</b>
|
||||||
Uploads from users named "demo" AND/OR "John Doe" AND/OR non-registered users:
|
${all ? `- Uploads from users named "demo" AND/OR "John Doe" AND/OR non-registered users:
|
||||||
<code>user:demo user:"John Doe" user:-</code>
|
<code>user:demo user:"John Doe" user:-</code>
|
||||||
ALL uploads, but NOT the ones from user named "demo" AND "John Doe":
|
- ALL uploads, but NOT the ones from user named "demo" AND "John Doe":
|
||||||
<code>-user:demo -user:"John Doe"</code>
|
<code>-user:demo -user:"John Doe"</code>
|
||||||
Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip":
|
- Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip":
|
||||||
<code>ip:127.0.0.1 *.rar *.zip</code>
|
<code>ip:127.0.0.1 *.rar *.zip</code>
|
||||||
Uploads uploaded since "1 June 2019 00:00:00":
|
` : ''}- Uploads uploaded since "1 June 2019 00:00:00":
|
||||||
<code>date:2019/06</code>
|
<code>date:2019/06</code>
|
||||||
Uploads uploaded between "7 April 2020 12:00:00" and "7 April 2020 23:59:59":
|
- Uploads uploaded between "7 April 2020 12:00:00" and "7 April 2020 23:59:59":
|
||||||
<code>date:2020/04/07 12-2020/04/07 23:59:59</code>
|
<code>date:2020/04/07 12-2020/04/07 23:59:59</code>
|
||||||
Uploads uploaded before "5 February 2020 00:00:00":
|
- Uploads uploaded before "5 February 2020 00:00:00":
|
||||||
<code>date:-2020/02/05</code>
|
<code>date:-2020/02/05</code>
|
||||||
Uploads which file names match "*.gz" but NOT "*.tar.gz":
|
- Uploads which file names match "*.gz" but NOT "*.tar.gz":
|
||||||
<code>*.gz -*.tar.gz</code>
|
<code>*.gz -*.tar.gz</code>
|
||||||
Sort matches by "size" column in ascending and descending order respectively:
|
- Sort matches by "size" column in ascending and descending order respectively:
|
||||||
<code>user:"John Doe" sort:size</code>
|
<code>${all ? 'user:"John Doe"' : '*.txt'} sort:size</code>
|
||||||
<code>*.mp4 user:- sort:size:d</code>
|
<code>*.mp4 ${all ? 'user:- ' : ''}sort:size:d</code>
|
||||||
|
${!page.permissions.moderator ? `
|
||||||
|
<b>Notice:</b> Regular users may face some limitations in the amount of keys that can be used at a time.
|
||||||
|
` : ''}
|
||||||
<b>Friendly reminder:</b> This window can be scrolled up!
|
<b>Friendly reminder:</b> This window can be scrolled up!
|
||||||
`.trim().replace(/^ {6}/gm, '').replace(/\n/g, '<br>')
|
`.trim().replace(/^\s*/g, '').replace(/\n/g, '<br>')
|
||||||
|
|
||||||
swal({ content }).then(() => {
|
swal({ content }).then(() => {
|
||||||
// Restore modal size
|
// Restore modal size
|
||||||
@ -1083,7 +1087,11 @@ page.uploadFiltersHelp = element => {
|
|||||||
|
|
||||||
page.filterUploads = element => {
|
page.filterUploads = element => {
|
||||||
const filters = document.querySelector(`#${element.dataset.filtersid || 'filters'}`).value.trim()
|
const filters = document.querySelector(`#${element.dataset.filtersid || 'filters'}`).value.trim()
|
||||||
page.getUploads({ all: true, filters }, element)
|
// eslint-disable-next-line compat/compat
|
||||||
|
page.getUploads(Object.assign(page.views[page.currentView], {
|
||||||
|
filters,
|
||||||
|
pageNum: 0
|
||||||
|
}), element)
|
||||||
}
|
}
|
||||||
|
|
||||||
page.viewUserUploads = (id, element) => {
|
page.viewUserUploads = (id, element) => {
|
||||||
@ -1879,7 +1887,7 @@ page.getUsers = (params = {}) => {
|
|||||||
params.pageNum = 0
|
params.pageNum = 0
|
||||||
|
|
||||||
if (!page.permissions.admin)
|
if (!page.permissions.admin)
|
||||||
return swal('An error occurred!', 'You can not do this!', 'error')
|
return swal('An error occurred!', 'You cannot do this!', 'error')
|
||||||
|
|
||||||
const url = `api/users/${params.pageNum}`
|
const url = `api/users/${params.pageNum}`
|
||||||
axios.get(url).then(response => {
|
axios.get(url).then(response => {
|
||||||
@ -2170,7 +2178,7 @@ page.createUser = () => {
|
|||||||
content: div
|
content: div
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reload users list
|
// Load last page of users list
|
||||||
// eslint-disable-next-line compat/compat
|
// eslint-disable-next-line compat/compat
|
||||||
page.getUsers(Object.assign(page.views.users, {
|
page.getUsers(Object.assign(page.views.users, {
|
||||||
pageNum: -1
|
pageNum: -1
|
||||||
@ -2449,7 +2457,7 @@ page.paginate = (totalItems, itemsPerPage, currentPage) => {
|
|||||||
|
|
||||||
page.getStatistics = (params = {}) => {
|
page.getStatistics = (params = {}) => {
|
||||||
if (!page.permissions.admin)
|
if (!page.permissions.admin)
|
||||||
return swal('An error occurred!', 'You can not do this!', 'error')
|
return swal('An error occurred!', 'You cannot do this!', 'error')
|
||||||
|
|
||||||
page.updateTrigger(params.trigger, 'loading')
|
page.updateTrigger(params.trigger, 'loading')
|
||||||
|
|
||||||
|
@ -504,7 +504,7 @@ page.prepareDropzone = () => {
|
|||||||
headers: {
|
headers: {
|
||||||
token: page.token,
|
token: page.token,
|
||||||
// Unlike the options above (e.g. albumid, filelength, etc.),
|
// Unlike the options above (e.g. albumid, filelength, etc.),
|
||||||
// strip tags can not yet be configured per file with this API
|
// strip tags cannot yet be configured per file with this API
|
||||||
striptags: page.stripTags
|
striptags: page.stripTags
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"1": "1588434390",
|
"1": "1588448172",
|
||||||
"2": "1581416390",
|
"2": "1581416390",
|
||||||
"3": "1581416390",
|
"3": "1581416390",
|
||||||
"4": "1581416390",
|
"4": "1581416390",
|
||||||
|
Loading…
Reference in New Issue
Block a user