/* global swal, axios, ClipboardJS, LazyLoad */
const lsKeys = {
token: 'token',
viewType: {
uploads: 'viewTypeUploads',
uploadsAll: 'viewTypeUploadsAll'
},
selected: {
uploads: 'selectedUploads',
uploadsAll: 'selectedUploadsAll',
albums: 'selectedAlbums',
albumsAll: 'selectedAlbumsAll',
users: 'selectedUsers'
},
originalNames: {
uploads: 'originalNames',
uploadsAll: 'originalNamesAll'
}
}
const page = {
// #dashboard
section: null,
// #page
dom: null,
// user token
token: localStorage[lsKeys.token],
// from api/tokens/verify
username: null,
permissions: null,
// sidebar menus
menusContainer: null,
menus: [],
currentView: null,
views: {
// params of uploads view
uploads: {
type: localStorage[lsKeys.viewType.uploads],
originalNames: localStorage[lsKeys.originalNames.uploads] === '1',
album: null, // album's id
pageNum: null
},
// params of uploads view (all)
uploadsAll: {
type: localStorage[lsKeys.viewType.uploadsAll],
originalNames: localStorage[lsKeys.originalNames.uploadsAll] === '1',
filters: null,
pageNum: null,
all: true
},
// params of albums view
albums: {
filters: null,
pageNum: null
},
// params of albums view (all)
albumsAll: {
filters: null,
pageNum: null,
all: true
},
// params of users view
users: {
filters: null,
pageNum: null
}
},
// ids of selected items (shared across pages and will be synced with localStorage)
selected: {
uploads: [],
uploadsAll: [],
albums: [],
albumsAll: [],
users: []
},
checkboxes: [],
lastSelected: [],
// select album dom for dialogs/modals
selectAlbumContainer: null,
// cache for dialogs/modals
cache: {},
clipboardJS: null,
lazyLoad: null,
imageExts: ['.webp', '.jpg', '.jpeg', '.gif', '.png', '.tiff', '.tif', '.svg'],
// TODO: Disable "Load original" button with non-streamable extensions
videoExts: ['.webm', '.mp4', '.wmv', '.avi', '.mov', '.mkv', '.m4v', '.m2ts'],
isSomethingLoading: false,
fadingIn: null,
albumTitleMaxLength: 70,
albumDescMaxLength: 4000
}
page.unhide = () => {
document.querySelector('#loader').classList.add('is-hidden')
page.section.classList.remove('is-hidden')
}
// Handler for regular JS errors
page.onError = error => {
console.error(error)
const content = document.createElement('div')
content.innerHTML = `${error.toString()}
`
return swal({
title: 'An error occurred!',
icon: 'error',
content
})
}
// Handler for Axios errors
page.onAxiosError = error => {
console.error(error)
// Better Cloudflare errors
const cloudflareErrors = {
520: 'Unknown Error',
521: 'Web Server Is Down',
522: 'Connection Timed Out',
523: 'Origin Is Unreachable',
524: 'A Timeout Occurred',
525: 'SSL Handshake Failed',
526: 'Invalid SSL Certificate',
527: 'Railgun Error',
530: 'Origin DNS Error'
}
const statusText = cloudflareErrors[error.response.status] || error.response.statusText
const description = error.response.data && error.response.data.description
? error.response.data.description
: 'There was an error with the request, please check the console for more information.'
return swal(`${error.response.status} ${statusText}`, description, 'error')
}
page.preparePage = () => {
if (page.token)
page.verifyToken(page.token, true)
else
window.location = 'auth'
}
page.checkClientVersion = apiVersion => {
const self = document.querySelector('#mainScript')
const match = self.src.match(/\?_=(\d+)$/)
if (match && match[1] && match[1] !== apiVersion)
return swal({
title: 'Update detected!',
text: 'Client assets have been updated. Reload to display the latest version?',
icon: 'info',
buttons: {
confirm: {
text: 'Reload',
closeModal: false
}
}
}).then(() => {
window.location.reload()
})
}
page.verifyToken = (token, reloadOnError) => {
axios.post('api/tokens/verify', { token }).then(response => {
if (response.data.success === false)
return swal({
title: 'An error occurred!',
text: response.data.description,
icon: 'error'
}).then(() => {
if (!reloadOnError) return
localStorage.removeItem(lsKeys.token)
window.location = 'auth'
})
axios.defaults.headers.common.token = token
localStorage[lsKeys.token] = token
if (response.data.version)
page.checkClientVersion(response.data.version)
page.token = token
page.username = response.data.username
page.permissions = response.data.permissions
page.prepareDashboard()
}).catch(page.onAxiosError)
}
page.prepareDashboard = () => {
page.section = document.querySelector('#dashboard')
page.dom = page.section.querySelector('#page')
// Capture all click events
page.dom.addEventListener('click', page.domClick, true)
// Capture all submit events
page.dom.addEventListener('submit', event => {
// Prevent default if necessary
if (event.target && event.target.classList.contains('prevent-default'))
return event.preventDefault()
}, true)
page.menusContainer = document.querySelector('#menu')
// All item menus in the sidebar
const itemMenus = [
{ selector: '#itemUploads', onclick: page.getUploads },
{ selector: '#itemDeleteUploadsByNames', onclick: page.deleteUploadsByNames },
{ selector: '#itemManageYourAlbums', onclick: page.getAlbums },
{ selector: '#itemManageToken', onclick: page.changeToken },
{ selector: '#itemChangePassword', onclick: page.changePassword },
{ selector: '#itemLogout', onclick: page.logout },
{ selector: '#itemManageUploads', onclick: page.getUploads, params: { all: true }, group: 'moderator' },
{ selector: '#itemManageAlbums', onclick: page.getAlbums, params: { all: true }, group: 'moderator' },
{ selector: '#itemStatistics', onclick: page.getStatistics, group: 'admin' },
{ selector: '#itemManageUsers', onclick: page.getUsers, group: 'admin' }
]
for (let i = 0; i < itemMenus.length; i++) {
// Skip item menu if not enough permission
if (itemMenus[i].group && !page.permissions[itemMenus[i].group])
continue
// Add onclick event listener
const item = document.querySelector(itemMenus[i].selector)
item.addEventListener('click', event => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
// eslint-disable-next-line compat/compat
itemMenus[i].onclick.call(null, Object.assign(itemMenus[i].params || {}, {
trigger: event.currentTarget
}))
})
item.classList.remove('is-hidden')
page.menus.push(item)
}
// If at least a moderator, show administration section
if (page.permissions.moderator) {
document.querySelector('#itemLabelAdmin').classList.remove('is-hidden')
document.querySelector('#itemListAdmin').classList.remove('is-hidden')
}
// Update text of logout button
document.querySelector('#itemLogout').innerHTML = `Logout ( ${page.username} )`
// Finally display dashboard
page.unhide()
// Load albums sidebar
page.getAlbumsSidebar()
if (typeof page.prepareShareX === 'function')
page.prepareShareX()
}
page.logout = params => {
page.updateTrigger(params.trigger, 'active')
localStorage.removeItem(lsKeys.token)
window.location = 'auth'
}
page.warnSomethingLoading = () => {
swal('Please wait!', 'Something else is still loading\u2026', 'warning', {
buttons: false,
timer: 3000
})
}
page.updateTrigger = (trigger, newState) => {
if (!trigger) return
// Disable menus container and pagination when loading
if (newState === 'loading') {
page.isSomethingLoading = true
page.section.classList.add('is-loading')
} else {
page.section.classList.remove('is-loading')
page.isSomethingLoading = false
}
if (newState === 'loading') {
trigger.classList.add('is-loading')
} else if (newState === 'active') {
if (trigger.parentNode.tagName === 'LI' && !trigger.className.includes('pagination-link')) {
for (let i = 0; i < page.menus.length; i++)
page.menus[i].classList.remove('is-active')
trigger.classList.remove('is-loading')
trigger.classList.add('is-active')
} else {
trigger.classList.remove('is-loading')
}
} else {
trigger.classList.remove('is-loading')
trigger.classList.remove('is-active')
}
}
page.getItemID = element => {
// This expects the item's parent to have the item's ID
let parent = element.parentNode
// If the element is part of a set of controls, use the container's parent instead
if (element.parentNode.classList.contains('controls')) parent = parent.parentNode
return parseInt(parent.dataset.id)
}
page.domClick = event => {
// We are processing clicks this way to avoid using "onclick" attribute
// Apparently we will need to use "unsafe-inline" for "script-src" directive
// of Content Security Policy (CSP), if we want to use "onclick" attribute
// Though I think that only applies to some browsers (?)
// Of course it wouldn't have mattered if we didn't use CSP to begin with
// Anyway, I personally would rather not use "onclick" attribute
let element = event.target
if (!element) return
// Delegate click events to their A or BUTTON parents
if (['I'].includes(element.tagName) && ['SPAN'].includes(element.parentNode.tagName))
element = element.parentNode
if (['SPAN'].includes(element.tagName) && ['A', 'BUTTON'].includes(element.parentNode.tagName))
element = element.parentNode
// Skip elements that have no action data
if (!element.dataset || !element.dataset.action) return
// Skip disabled elements
if (element.hasAttribute('disabled')) return
event.stopPropagation() // maybe necessary
const id = page.getItemID(element)
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 'toggle-original-names':
return page.toggleOriginalNames(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 'disable-album':
return page.disableAlbum(id)
case 'view-album-uploads':
return page.viewAlbumUploads(id, element)
// Manage users
case 'create-user':
return page.createUser()
case 'edit-user':
return page.editUser(id)
case 'disable-user':
return page.disableUser(id)
case 'delete-user':
return page.deleteUser(id)
case 'view-user-uploads':
return page.viewUserUploads(id, 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(element)
case 'page-prev':
case 'page-next':
case 'page-goto':
case 'jump-to-page':
return page.switchPage(action, element)
}
}
page.fadeAndScroll = disableFading => {
if (page.fadingIn) {
clearTimeout(page.fadingIn)
page.dom.classList.remove('fade-in')
}
if (!disableFading) {
page.dom.classList.add('fade-in')
page.fadingIn = setTimeout(() => {
page.dom.classList.remove('fade-in')
}, 500)
}
page.dom.scrollIntoView({
behavior: disableFading ? 'auto' : 'smooth',
block: 'start',
inline: 'nearest'
})
}
page.getByView = (view, get) => {
switch (view) {
case 'uploads':
case 'uploadsAll':
return {
type: 'uploads',
func: page.getUploads
}[get]
case 'albums':
case 'albumsAll':
return {
type: 'albums',
func: page.getAlbums
}[get]
case 'users':
return {
type: 'users',
func: page.getUsers
}[get]
default:
return null
}
}
page.switchPage = (action, element) => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
// eslint-disable-next-line compat/compat
const params = Object.assign(page.views[page.currentView], {
trigger: element
})
const func = page.getByView(page.currentView, 'func')
switch (action) {
case 'page-prev':
params.pageNum = page.views[page.currentView].pageNum - 1
if (params.pageNum < 0)
return swal('An error occurred!', 'This is already the first page.', 'error')
return func(params)
case 'page-next':
params.pageNum = page.views[page.currentView].pageNum + 1
return func(params)
case 'page-goto':
params.pageNum = parseInt(element.dataset.goto)
return func(params)
case 'jump-to-page': {
const jumpToPage = document.querySelector(`#${element.dataset.jumpid || 'jumpToPage'}`)
if (!jumpToPage.checkValidity()) return
const parsed = parseInt(jumpToPage.value)
params.pageNum = isNaN(parsed) ? 0 : (parsed - 1)
if (params.pageNum < 0) params.pageNum = 0
return func(params)
}
}
}
page.focusJumpToPage = element => {
const jumpToPage = document.querySelector(`#${element.dataset.jumpid || 'jumpToPage'}`)
if (!jumpToPage) return
jumpToPage.focus()
jumpToPage.select()
}
page.getUploads = (params = {}) => {
if (params && params.all && !page.permissions.moderator)
return swal('An error occurred!', 'You cannot do this!', 'error')
if (page.isSomethingLoading)
return page.warnSomethingLoading()
page.updateTrigger(params.trigger, 'loading')
if (typeof params.pageNum !== 'number' || params.pageNum < 0)
params.pageNum = 0
const url = params.album !== undefined
? `api/album/${params.album}/${params.pageNum}`
: `api/uploads/${params.pageNum}`
const headers = {}
if (params.all)
headers.all = '1'
if (params.filters) {
headers.filters = params.filters
// Send client timezone offset if properly using date: and/or :expiry filters
// Server will pretend client is on UTC if unset
if (/(^|\s)(date|expiry):[\d"]/.test(params.filters))
headers.minoffset = new Date().getTimezoneOffset()
}
axios.get(url, { headers }).then(response => {
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
page.updateTrigger(params.trigger)
return swal('An error occurred!', response.data.description, 'error')
}
const pages = Math.ceil(response.data.count / 25)
const files = response.data.files
if (params.pageNum && (files.length === 0)) {
page.updateTrigger(params.trigger)
if (params.autoPage) {
params.pageNum = pages - 1
return page.getUploads(params)
} else {
return swal('An error occurred!', `There are no more uploads to populate page ${params.pageNum + 1}.`, 'error')
}
}
page.currentView = params.all ? 'uploadsAll' : 'uploads'
page.cache = {}
const albums = response.data.albums
const users = response.data.users
const basedomain = response.data.basedomain
if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum)
const pagination = page.paginate(response.data.count, 25, params.pageNum)
const filter = `
`
const extraControls = `
`
const controls = `
`
// Do some string replacements for bottom controls
const bottomFiltersId = 'bFilters'
const bottomJumpId = 'bJumpToPage'
const bottomExtraControls = extraControls
.replace(/id="filters"/, `id="${bottomFiltersId}"`)
.replace(/(data-action="filter-uploads")/, `$1 data-filtersid="${bottomFiltersId}"`)
.replace(/id="jumpToPage"/, `id="${bottomJumpId}"`)
.replace(/(data-action="jump-to-page")/g, `$1 data-jumpid="${bottomJumpId}"`)
const bottomPagination = pagination
.replace(/(data-action="page-ellipsis")/g, `$1 data-jumpid="${bottomJumpId}"`)
// Whether there are any unselected items
let unselected = false
const showOriginalNames = page.views[page.currentView].originalNames
const hasExpiryDateColumn = files.some(file => {
return file.expirydate !== undefined
})
for (let i = 0; i < files.length; i++) {
// Build full URLs
files[i].file = `${basedomain}/${files[i].name}`
if (files[i].thumb)
files[i].thumb = `${basedomain}/${files[i].thumb}`
// Determine types
files[i].type = 'other'
const exec = /.[\w]+(\?|$)/.exec(files[i].file)
const extname = exec && exec[0] ? exec[0].toLowerCase() : null
if (page.imageExts.includes(extname))
files[i].type = 'picture'
else if (page.videoExts.includes(extname))
files[i].type = 'video'
// Cache bare minimum data for thumbnails viewer
page.cache[files[i].id] = {
name: files[i].name,
original: files[i].original,
thumb: files[i].thumb,
file: files[i].file,
type: files[i].type
}
// Prettify
files[i].prettyBytes = page.getPrettyBytes(parseInt(files[i].size))
files[i].prettyDate = page.getPrettyDate(new Date(files[i].timestamp * 1000))
if (hasExpiryDateColumn)
files[i].prettyExpiryDate = files[i].expirydate
? page.getPrettyDate(new Date(files[i].expirydate * 1000))
: null
// Update selected status
files[i].selected = page.selected[page.currentView].includes(files[i].id)
if (!files[i].selected) unselected = true
// Appendix (display album or user)
if (params.all)
files[i].appendix = files[i].userid
? users[files[i].userid] || ''
: ''
else if (params.album === undefined)
files[i].appendix = files[i].albumid
? albums[files[i].albumid] || ''
: ''
}
if (page.views[page.currentView].type === 'thumbs') {
page.dom.innerHTML = `
${pagination}
${extraControls}
${controls}
${controls}
${bottomExtraControls}
${bottomPagination}
`
const table = document.querySelector('#table')
for (let i = 0; i < files.length; i++) {
const upload = files[i]
const div = document.createElement('div')
div.className = 'image-container column'
div.dataset.id = upload.id
if (upload.thumb !== undefined)
div.innerHTML = ` `
else
div.innerHTML = `${upload.extname || 'N/A'} `
div.innerHTML += `
${upload.thumb ? `
` : ''}
${upload.name}
${showOriginalNames ? `
${upload.original}
` : ''}
${upload.appendix ? `${upload.appendix} – ` : ''}${upload.prettyBytes}
${hasExpiryDateColumn && upload.prettyExpiryDate ? `
EXP: ${upload.prettyExpiryDate}
` : ''}
`
table.appendChild(div)
page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
}
} else {
const allAlbums = params.all && params.filters && params.filters.includes('albumid:')
page.dom.innerHTML = `
${pagination}
${extraControls}
${controls}
${controls}
${bottomExtraControls}
${bottomPagination}
`
const table = document.querySelector('#table')
for (let i = 0; i < files.length; i++) {
const upload = files[i]
const tr = document.createElement('tr')
tr.dataset.id = upload.id
tr.innerHTML = `
${upload.name}
${showOriginalNames ? `${upload.original} ` : ''}
${params.album === undefined ? `${upload.appendix} ` : ''}
${allAlbums ? `${upload.albumid ? (albums[upload.albumid] || '') : ''} ` : ''}
${upload.prettyBytes}
${params.all ? `${upload.ip || ''} ` : ''}
${upload.prettyDate}
${hasExpiryDateColumn ? `${upload.prettyExpiryDate || '-'} ` : ''}
${params.all ? '' : `
`}
`
table.appendChild(tr)
page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
}
}
const selectAll = document.querySelector('#selectAll')
if (selectAll && !unselected && files.length) {
selectAll.checked = true
selectAll.title = 'Unselect all'
}
if (page.views[page.currentView].type === 'thumbs') {
page.fadeAndScroll(true)
page.lazyLoad.update()
} else {
page.fadeAndScroll()
}
page.updateTrigger(params.trigger, 'active')
if (page.currentView === 'uploads')
page.views.uploads.album = params.album
page.views[page.currentView].filters = params.filters
page.views[page.currentView].pageNum = files.length ? params.pageNum : 0
}).catch(error => {
page.updateTrigger(params.trigger)
page.onAxiosError(error)
})
}
page.setUploadsView = (view, element) => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
if (view === 'list') {
delete localStorage[lsKeys.viewType[page.currentView]]
page.views[page.currentView].type = undefined
} else {
localStorage[lsKeys.viewType[page.currentView]] = view
page.views[page.currentView].type = view
}
// eslint-disable-next-line compat/compat
page.getUploads(Object.assign(page.views[page.currentView], {
trigger: element
}))
}
page.toggleOriginalNames = element => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
if (page.views[page.currentView].originalNames) {
delete localStorage[lsKeys.originalNames[page.currentView]]
page.views[page.currentView].originalNames = false
} else {
localStorage[lsKeys.originalNames[page.currentView]] = '1'
page.views[page.currentView].originalNames = true
}
// eslint-disable-next-line compat/compat
page.getUploads(Object.assign(page.views[page.currentView], {
trigger: element
}))
}
page.displayPreview = id => {
const file = page.cache[id]
if (!file.thumb) return
const div = document.createElement('div')
div.innerHTML = `
${file.name}
${file.original}
`
if (file.file) {
const exec = /.[\w]+(\?|$)/.exec(file.file)
const extname = exec && exec[0] ? exec[0].toLowerCase() : null
const isimage = page.imageExts.includes(extname)
const isvideo = !isimage && page.videoExts.includes(extname)
if (isimage || isvideo) {
div.innerHTML += `
`
div.querySelector('#swalOriginal').addEventListener('click', event => {
const trigger = event.currentTarget
if (trigger.classList.contains('is-danger'))
return
trigger.classList.add('is-loading')
const thumb = div.querySelector('#swalThumb')
if (isimage) {
thumb.src = file.file
thumb.onload = () => {
trigger.classList.add('is-hidden')
document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded')
}
thumb.onerror = event => {
event.currentTarget.classList.add('is-hidden')
trigger.className = 'button is-danger is-fullwidth'
trigger.innerHTML = `
Unable to load original
`
}
} else if (isvideo) {
thumb.classList.add('is-hidden')
const video = document.createElement('video')
video.id = 'swalVideo'
video.controls = true
video.autoplay = true
video.src = file.file
thumb.insertAdjacentElement('afterend', video)
trigger.classList.add('is-hidden')
document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded')
}
})
}
}
return swal({
content: div,
buttons: false
}).then(() => {
// Destroy video, if necessary
const video = div.querySelector('#swalVideo')
if (video) video.remove()
// Restore modal size
document.body.querySelector('.swal-overlay .swal-modal').classList.remove('is-expanded')
})
}
page.selectAll = element => {
for (let i = 0; i < page.checkboxes.length; i++) {
const id = page.getItemID(page.checkboxes[i])
if (isNaN(id)) continue
if (page.checkboxes[i].checked !== element.checked) {
page.checkboxes[i].checked = element.checked
if (page.checkboxes[i].checked)
page.selected[page.currentView].push(id)
else
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
}
}
if (page.selected[page.currentView].length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
else
delete localStorage[lsKeys.selected[page.currentView]]
element.title = element.checked ? 'Unselect all' : 'Select all'
}
page.selectInBetween = (element, lastElement) => {
const thisIndex = parseInt(element.dataset.index)
const lastIndex = parseInt(lastElement.dataset.index)
const distance = Math.abs(thisIndex - lastIndex)
if (distance < 2)
return
for (let i = 0; i < page.checkboxes.length; i++)
if ((thisIndex > lastIndex && i > lastIndex && i < thisIndex) ||
(thisIndex < lastIndex && i > thisIndex && i < lastIndex)) {
// Check or uncheck depending on the state of the initial checkbox
const checked = page.checkboxes[i].checked = lastElement.checked
const id = page.getItemID(page.checkboxes[i])
if (!page.selected[page.currentView].includes(id) && checked)
page.selected[page.currentView].push(id)
else if (page.selected[page.currentView].includes(id) && !checked)
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
}
}
page.select = (element, event) => {
const id = page.getItemID(element)
if (isNaN(id)) return
if (event.shiftKey && page.lastSelected) {
page.selectInBetween(element, page.lastSelected)
// Check or uncheck depending on the state of the initial checkbox
element.checked = page.lastSelected.checked
} else {
page.lastSelected = element
}
if (!page.selected[page.currentView].includes(id) && element.checked)
page.selected[page.currentView].push(id)
else if (page.selected[page.currentView].includes(id) && !element.checked)
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
// Update local storage
if (page.selected[page.currentView].length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
else
delete localStorage[lsKeys.selected[page.currentView]]
}
page.clearSelection = () => {
const selected = page.selected[page.currentView]
const type = page.getByView(page.currentView, 'type')
const count = selected.length
if (!count)
return swal('An error occurred!', `You have not selected any ${type}.`, 'error')
const suffix = count === 1 ? type.substring(0, type.length - 1) : type
return swal({
title: 'Are you sure?',
text: `You are going to unselect ${count} ${suffix}.`,
buttons: true
}).then(proceed => {
if (!proceed) return
const checkboxes = page.checkboxes
for (let i = 0; i < checkboxes.length; i++)
if (checkboxes[i].checked)
checkboxes[i].checked = false
page.selected[page.currentView] = []
delete localStorage[lsKeys.selected[page.currentView]]
const selectAll = document.querySelector('#selectAll')
if (selectAll) selectAll.checked = false
return swal('Cleared selection!', `Unselected ${count} ${suffix}.`, 'success')
})
}
page.uploadFiltersHelp = element => {
const all = Boolean(element.dataset.all)
const content = document.createElement('div')
content.style = 'text-align: left'
content.innerHTML = `${all ? `
There are 2 filter keys, namely user (username) and ip .
These keys can be specified more than once.
For usernames with whitespaces, wrap them with double quotes ("
).
Special cases such as uploads by non-registered users or have no IPs respectively, use user:-
or ip:-
.
To exclude certain users/ips while still listing every other uploads, add negation sign (-
) before the keys.
Negation sign can also be used to exclude the special cases mentioned above (i.e. -user:-
or -ip:-
).
If you know the ID of a user's album, you can list its uploads with albumid key.
Negation sign works for this key as well.
` : `
There is only 1 filter key, namely albumid .
This key can be specified more than once.
Special case such as uploads with no albums, use albumid:-
.
To exclude certain albums while still listing every other uploads, add negation sign (-
) before the keys.
Negation sign can also be used to exclude the special case mentioned above (i.e. -albumid:-
).
`}
There are 2 range keys: date (upload date) and expiry (expiry date).
Their format is: "YYYY/MM/DD HH:MM:SS-YYYY/MM/DD HH:MM:SS"
("from" date and "to" date respectively).
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 only time is specified, today's date will be used.
If you do not need to specify both date and time, you may omit the double quotes.
In conclusion, the following examples are all valid: date:"2020/01/01 01:23-2018/01/01 06"
, expiry:-2020/05
, date:12:34:56
.
These keys can only be specified once each.
Timezone? Feel free to query the dates with your own timezone.
API requests to the filter endpoint will attach your browser's timezone offset, so the server will automatically calculate timezone differences.
Matches can also be sorted with sort keys.
Their formats are: sort:columnName[:d[escending]]
, where :d[escending]
is an optional tag to set the direction to descending.
This key must be used with internal column names used in the database (id
, ${all ? 'userid' : 'albumid'}
, and so on),
but there are 2 shortcuts available: date for timestamp
column and expiry for expirydate
column.
This key can also be specified more than once, where their order will decide the sorting steps.
Finally, there are type-is keys to refine by types.
You can use is:image
and is:video
to list images and videos respectively.
This will only use image/video extensions whose thumbnails can be generated by the safe.
Negation sign works for this key as well.
Mixing inclusion and exclusion is not allowed (i.e. is:image -is:video
, since the second key is redundant).
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 before the keywords.
Internal steps:
${all ? `- Query uploads passing ALL exclusion filter keys OR matching ANY filter keys, if any.
- Refine matches` : '- Filter uploads'} using date key, if any.
- Refine matches using expiry key, if any.
- Refine matches using type-is keys, if any.
- Refine matches using ANY non-keyed keywords, if any.
- Filter matches using ALL exclusion non-keyed keywords, if any.
- Sort matches using sorting keys, if any.
Examples:
${all ? `- Uploads from users named "demo" AND/OR "John Doe" AND/OR non-registered users:
user:demo user:"John Doe" user:-
- ALL uploads, but NOT the ones from user named "demo" AND "John Doe":
-user:demo -user:"John Doe"
- Uploads from IP "127.0.0.1" AND which file names match "*.rar" OR "*.zip":
ip:127.0.0.1 *.rar *.zip
` : ''}- Uploads without albums:
albumid:-
- ALL uploads, but NOT the ones from album with ID 69:
-albumid:69
- Uploads uploaded since "1 June 2019 00:00:00":
date:2019/06
- Uploads uploaded between "7 April 2020 12:00:00" and "7 April 2020 23:59:59":
date:"2020/04/07 12-2020/04/07 23:59:59"
- Uploads uploaded before "5 February 2020 00:00:00":
date:-2020/02/05
- Uploads which file names match "*.gz" but NOT "*.tar.gz":
*.gz -*.tar.gz
- Sort matches by "size" column in ascending and descending order respectively:
${all ? 'user:"John Doe"' : '*.txt'} sort:size
*.mp4 ${all ? 'user:- ' : ''}sort:size:d
${!page.permissions.moderator ? `
Notice: Regular users may face some limitations in the amount of keys that can be used at a time.
` : ''}
Friendly reminder: This window can be scrolled up!
`.trim().replace(/^\s*/g, '').replace(/\n/g, ' ')
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 => {
const filters = document.querySelector(`#${element.dataset.filtersid || 'filters'}`).value
.trim()
.replace(/\t/g, ' ')
.replace(/(^|\s)((albumid|ip|user|date|expiry|is|sort|orderby):)\s+/g, '$2')
// eslint-disable-next-line compat/compat
page.getUploads(Object.assign(page.views[page.currentView], {
filters,
pageNum: 0,
trigger: element
}))
}
page.viewUserUploads = (id, element) => {
const user = page.cache[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:${username}`,
trigger: document.querySelector('#itemManageUploads')
})
}
page.viewAlbumUploads = (id, element) => {
if (!page.cache[id]) return
element.classList.add('is-loading')
// eslint-disable-next-line compat/compat
const all = page.currentView === 'albumsAll' && page.permissions.moderator
page.getUploads({
all,
filters: `albumid:${id}`,
trigger: all
? document.querySelector('#itemManageUploads')
: document.querySelector('#itemUploads')
})
}
page.deleteUpload = id => {
page.postBulkDeleteUploads({
all: page.currentView === 'uploadsAll',
field: 'id',
values: [id],
cb (failed) {
// Remove from remembered checkboxes if necessary
if (!failed.length && page.selected[page.currentView].includes(id))
page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1)
// Update local storage
if (page.selected[page.currentView].length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
else
delete localStorage[lsKeys.selected[page.currentView]]
// Reload upload list
// eslint-disable-next-line compat/compat
page.getUploads(Object.assign(page.views[page.currentView], {
autoPage: true
}))
}
})
}
page.bulkDeleteUploads = () => {
const count = page.selected[page.currentView].length
if (!count)
return swal('An error occurred!', 'You have not selected any uploads.', 'error')
page.postBulkDeleteUploads({
all: page.currentView === 'uploadsAll',
field: 'id',
values: page.selected[page.currentView],
cb (failed) {
// Update state of checkboxes
if (failed.length)
page.selected[page.currentView] = page.selected[page.currentView]
.filter(id => {
return failed.includes(id)
})
else
page.selected[page.currentView] = []
// Update local storage
if (page.selected[page.currentView].length)
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
else
delete localStorage[lsKeys.selected[page.currentView]]
// Reload uploads list
// eslint-disable-next-line compat/compat
page.getUploads(Object.assign(page.views[page.currentView], {
autoPage: true
}))
}
})
}
page.deleteUploadsByNames = (params = {}) => {
let appendix = ''
if (page.permissions.moderator)
appendix = 'Hint: You can use this feature to delete uploads by other users.'
page.dom.innerHTML = `
`
page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active')
document.querySelector('#submitBulkDelete').addEventListener('click', () => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
const textArea = document.querySelector('#bulkDeleteNames')
// Clean up
const seen = {}
const names = textArea.value
.split(/\r?\n/)
.map(name => {
const trimmed = name.trim()
return /^[^\s]+$/.test(trimmed)
? trimmed
: ''
})
.filter(name => {
// Filter out invalid and duplicate names
return (!name || Object.prototype.hasOwnProperty.call(seen, name))
? false
: (seen[name] = true)
})
// Update textarea with cleaned names
textArea.value = names.join('\n')
if (!names.length)
return swal('An error occurred!', 'You have not entered any upload names.', 'error')
page.postBulkDeleteUploads({
all: true,
field: 'name',
values: names,
cb (failed) {
textArea.value = failed.join('\n')
}
})
})
}
page.postBulkDeleteUploads = (params = {}) => {
const count = params.values.length
const objective = `${params.values.length} upload${count === 1 ? '' : 's'}`
const boldObjective = objective.replace(/^(\d*)(.*)/, '$1 $2')
let text = `You won't be able to recover ${boldObjective}!
`
if (params.all) {
const obj1 = count === 1 ? 'an upload' : 'some uploads'
const obj2 = count === 1 ? 'another user' : 'other users'
text += `\nWarning: You may be nuking ${obj1} by ${obj2}!
`
}
const content = document.createElement('div')
content.innerHTML = text
swal({
title: 'Are you sure?',
content,
icon: 'warning',
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: `Yes, nuke ${params.values.length === 1 ? 'it' : 'them'}!`,
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('api/upload/bulkdelete', {
field: params.fields,
values: params.values
}).then(response => {
if (!response) return
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', response.data.description, 'error')
}
const failed = Array.isArray(response.data.failed) ? response.data.failed : []
if (failed.length === params.values.length)
swal('An error occurred!', `Unable to delete any of the ${objective}.`, 'error')
else if (failed.length && failed.length < params.values.length)
swal('Warning!', `From ${objective}, unable to delete ${failed.length} of them.`, 'warning')
else
swal('Deleted!', `${objective} ${count === 1 ? 'has' : 'have'} been deleted.`, 'success', {
buttons: false,
timer: 1500
})
if (typeof params.cb === 'function')
params.cb(failed)
}).catch(page.onAxiosError)
})
}
page.addSelectedUploadsToAlbum = () => {
if (page.currentView !== 'uploads')
return
const count = page.selected[page.currentView].length
if (!count)
return swal('An error occurred!', 'You have not selected any uploads.', 'error')
page.addUploadsToAlbum(page.selected[page.currentView], failed => {
if (!failed) return
if (failed.length)
page.selected[page.currentView] = page.selected[page.currentView].filter(id => {
return failed.includes(id)
})
else
page.selected[page.currentView] = []
localStorage[lsKeys.selected[page.currentView]] = JSON.stringify(page.selected[page.currentView])
page.getUploads(page.views[page.currentView])
})
}
page.addToAlbum = id => {
page.addUploadsToAlbum([id], failed => {
if (!failed) return
page.getUploads(page.views[page.currentView])
})
}
page.addUploadsToAlbum = (ids, callback) => {
const count = ids.length
const content = document.createElement('div')
content.innerHTML = `
You are about to add ${count} upload${count === 1 ? '' : 's'} to an album.
If an upload is already in an album, it will be moved.
Remove from album
Fetching albums list\u2026
`
swal({
icon: 'warning',
content,
buttons: {
cancel: true,
confirm: {
text: 'OK',
closeModal: false
}
}
}).then(choose => {
if (!choose) return
const albumid = parseInt(document.querySelector('#swalAlbum').value)
if (isNaN(albumid))
return swal('An error occurred!', 'You did not choose an album.', 'error')
axios.post('api/albums/addfiles', {
ids,
albumid
}).then(add => {
if (!add) return
if (add.data.success === false) {
if (add.data.description === 'No token provided')
page.verifyToken(page.token)
else
swal('An error occurred!', add.data.description, 'error')
return
}
let added = ids.length
if (add.data.failed && add.data.failed.length)
added -= add.data.failed.length
const suffix = `upload${ids.length === 1 ? '' : 's'}`
if (!added)
return swal('An error occurred!', `Could not add the ${suffix} to the album.`, 'error')
swal('Woohoo!', `Successfully ${albumid < 0 ? 'removed' : 'added'} ${added} ${suffix} ${albumid < 0 ? 'from' : 'to'} the album.`, 'success', {
buttons: false,
timer: 1500
})
callback(add.data.failed)
}).catch(page.onAxiosError)
})
// Get albums list then update content of swal
axios.get('api/albums').then(list => {
if (list.data.success === false) {
if (list.data.description === 'No token provided')
page.verifyToken(page.token)
else
swal('An error occurred!', list.data.description, 'error')
return
}
// If the prompt was replaced, the container would be missing
const select = document.querySelector('#swalAlbum')
if (!select) return
select.innerHTML += list.data.albums
.map(album => {
return `${album.name} `
})
.join('\n')
select.getElementsByTagName('option')[1].innerHTML = 'Choose an album'
select.removeAttribute('disabled')
}).catch(page.onAxiosError)
}
page.getAlbums = (params = {}) => {
if (params && params.all && !page.permissions.moderator)
return swal('An error occurred!', 'You cannot do this!', 'error')
if (page.isSomethingLoading)
return page.warnSomethingLoading()
page.updateTrigger(params.trigger, 'loading')
if (typeof params.pageNum !== 'number')
params.pageNum = 0
const headers = {}
if (params.all)
headers.all = '1'
const url = `api/albums/${params.pageNum}`
axios.get(url, { headers }).then(response => {
if (!response) return
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
page.updateTrigger(params.trigger)
return swal('An error occurred!', response.data.description, 'error')
}
const pages = Math.ceil(response.data.count / 25)
const albums = response.data.albums
if (params.pageNum && (albums.length === 0)) {
page.updateTrigger(params.trigger)
if (params.autoPage) {
params.pageNum = pages - 1
return page.getAlbums(params)
} else {
return swal('An error occurred!', `There are no more albums to populate page ${params.pageNum + 1}.`, 'error')
}
}
page.currentView = params.all ? 'albumsAll' : 'albums'
page.cache = {}
const users = response.data.users
const homeDomain = response.data.homeDomain
if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum)
const pagination = page.paginate(response.data.count, 25, params.pageNum)
const filter = `
`
const extraControls = `
`
const controls = `
`
// Do some string replacements for bottom controls
const bottomFiltersId = 'bFilters'
const bottomJumpId = 'bJumpToPage'
const bottomExtraControls = extraControls
.replace(/id="filters"/, `id="${bottomFiltersId}"`)
.replace(/(data-action="filter-uploads")/, `$1 data-filtersid="${bottomFiltersId}"`)
.replace(/id="jumpToPage"/, `id="${bottomJumpId}"`)
.replace(/(data-action="jump-to-page")/g, `$1 data-jumpid="${bottomJumpId}"`)
const bottomPagination = pagination
.replace(/(data-action="page-ellipsis")/g, `$1 data-jumpid="${bottomJumpId}"`)
// Whether there are any unselected items
let unselected = false
const createNewAlbum = `
Create new album
Max length is ${page.albumDescMaxLength} characters.
`
page.dom.innerHTML = `
${!params.all ? createNewAlbum : ''}
${pagination}
${extraControls}
${controls}
${controls}
${bottomExtraControls}
${bottomPagination}
`
const table = document.querySelector('#table')
for (let i = 0; i < albums.length; i++) {
const album = albums[i]
const albumUrl = `${homeDomain}/a/${album.identifier}`
const selected = page.selected[page.currentView].includes(album.id)
if (!selected) unselected = true
// Prettify
album.prettyDate = page.getPrettyDate(new Date(album.timestamp * 1000))
// Server-side explicitly expect this value to consider an album as disabled
const enabled = album.enabled !== 0
page.cache[album.id] = {
name: album.name,
download: album.download,
public: album.public,
description: album.description,
enabled
}
const tr = document.createElement('tr')
tr.dataset.id = album.id
tr.innerHTML = `
${album.id}
${album.name}
${params.all ? ` ${album.userid ? (users[album.userid] || '') : ''} ` : ''}
${album.uploads}
${album.prettyDate}
${albumUrl}
`
table.appendChild(tr)
page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
}
const selectAll = document.querySelector('#selectAll')
if (selectAll && !unselected) {
selectAll.checked = true
selectAll.title = 'Unselect all'
}
page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active')
if (page.currentView === 'albumsAll')
page.views[page.currentView].filters = params.filters
page.views[page.currentView].pageNum = albums.length ? params.pageNum : 0
}).catch(error => {
page.updateTrigger(params.trigger)
page.onAxiosError(error)
})
}
page.editAlbum = id => {
const album = page.cache[id]
if (!album) return
const div = document.createElement('div')
div.innerHTML = `
${(album.description || '').substring(0, page.albumDescMaxLength)}
Max length is ${page.albumDescMaxLength} characters.
${page.currentView === 'albumsAll' && page.permissions.moderator
? ``
: ''}
`
swal({
title: 'Edit album',
icon: 'info',
content: div,
buttons: {
cancel: true,
confirm: {
closeModal: false
}
}
}).then(value => {
if (!value) return
const post = {
id,
name: document.querySelector('#swalName').value.trim(),
description: document.querySelector('#swalDescription').value.trim(),
download: document.querySelector('#swalDownload').checked,
public: document.querySelector('#swalPublic').checked,
requestLink: document.querySelector('#swalRequestLink').checked
}
if (page.currentView === 'albumsAll' && page.permissions.moderator)
post.enabled = document.querySelector('#swalEnabled').checked
axios.post('api/albums/edit', post).then(response => {
if (!response) return
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', response.data.description, 'error')
}
if (response.data.identifier)
swal('Success!', `The album's new identifier is: ${response.data.identifier}.`, 'success')
else if (response.data.name !== album.name)
swal('Success!', `The album was renamed to: ${response.data.name}.`, 'success')
else
swal('Success!', 'The album was edited.', 'success', {
buttons: false,
timer: 1500
})
page.getAlbumsSidebar()
// Reload albums list
// eslint-disable-next-line compat/compat
page.getAlbums(Object.assign(page.views[page.currentView], {
autoPage: true
}))
}).catch(page.onAxiosError)
})
}
page.disableAlbum = id => {
swal({
title: 'Are you sure?',
text: 'This won\'t delete the uploads associated with the album!',
icon: 'warning',
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: 'Yes, disable it!',
closeModal: false
},
purge: {
text: 'Umm, delete the uploads too, please?',
value: 'purge',
className: 'swal-button--danger',
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('api/albums/disable', {
id,
purge: proceed === 'purge'
}).then(response => {
if (response.data.success === false) {
const failed = Array.isArray(response.data.failed)
? response.data.failed
: []
if (response.data.description === 'No token provided')
return page.verifyToken(page.token)
else if (failed.length)
return swal('An error occurred!', `Unable to delete ${failed.length} of the album's upload${failed.length === 1 ? '' : 's'}.`, 'error')
else
return swal('An error occurred!', response.data.description, 'error')
}
swal('Deleted!', 'Your album has been disabled.', 'success', {
buttons: false,
timer: 1500
})
page.getAlbumsSidebar()
// Reload albums list
// eslint-disable-next-line compat/compat
page.getAlbums(Object.assign(page.views[page.currentView], {
autoPage: true
}))
}).catch(page.onAxiosError)
})
}
page.submitAlbum = element => {
page.updateTrigger(element, 'loading')
axios.post('api/albums', {
name: document.querySelector('#albumName').value.trim(),
description: document.querySelector('#albumDescription').value.trim()
}).then(response => {
if (!response) return
page.updateTrigger(element)
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', response.data.description, 'error')
}
swal('Woohoo!', 'Album was created successfully.', 'success', {
buttons: false,
timer: 1500
})
page.getAlbumsSidebar()
page.getAlbums({
pageNum: -1
})
}).catch(error => {
page.updateTrigger(element)
page.onAxiosError(error)
})
}
page.getAlbumsSidebar = () => {
axios.get('api/albums', { headers: { sidebar: '1' } }).then(response => {
if (!response) return
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', response.data.description, 'error')
}
const albums = response.data.albums
const count = response.data.count
const albumsContainer = document.querySelector('#albumsContainer')
// Clear albums sidebar if necessary
const oldAlbums = albumsContainer.querySelectorAll('li > a')
if (oldAlbums.length) {
for (let i = 0; i < oldAlbums.length; i++)
page.menus.splice(page.menus.indexOf(oldAlbums[i]), 1)
albumsContainer.innerHTML = ''
}
if (albums === undefined)
return
for (let i = 0; i < albums.length; i++) {
const album = albums[i]
const li = document.createElement('li')
const a = document.createElement('a')
a.id = album.id
a.className = 'is-relative'
a.innerHTML = album.name
a.addEventListener('click', event => {
page.getUploads({
album: parseInt(event.currentTarget.id),
trigger: event.currentTarget
})
})
page.menus.push(a)
li.appendChild(a)
albumsContainer.appendChild(li)
}
if (count > albums.length) {
const li = document.createElement('li')
const a = document.createElement('a')
a.className = 'is-relative'
a.innerHTML = '...'
a.title = `You have ${count} albums, but the sidebar can only list your first ${albums.length} albums.`
a.addEventListener('click', event => {
page.getAlbums({
trigger: document.querySelector('#itemManageYourAlbums')
})
})
li.appendChild(a)
albumsContainer.appendChild(li)
}
}).catch(page.onAxiosError)
}
page.changeToken = (params = {}) => {
page.dom.innerHTML = `
`
page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active')
document.querySelector('#getNewToken').addEventListener('click', event => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
const trigger = event.currentTarget
page.updateTrigger(trigger, 'loading')
axios.post('api/tokens/change').then(response => {
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
page.updateTrigger(trigger)
return swal('An error occurred!', response.data.description, 'error')
}
page.updateTrigger(trigger)
swal({
title: 'Woohoo!',
text: 'Your token was successfully changed.',
icon: 'success',
buttons: false,
timer: 1500
}).then(() => {
axios.defaults.headers.common.token = response.data.token
localStorage[lsKeys.token] = response.data.token
page.token = response.data.token
page.changeToken()
})
}).catch(error => {
page.updateTrigger(trigger)
page.onAxiosError(error)
})
})
}
page.changePassword = (params = {}) => {
page.dom.innerHTML = `
`
page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active')
document.querySelector('#sendChangePassword').addEventListener('click', event => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
if (!page.dom.querySelector('form').checkValidity())
return
if (document.querySelector('#password').value === document.querySelector('#passwordConfirm').value)
page.sendNewPassword(document.querySelector('#password').value, event.currentTarget)
else
swal({
title: 'Password mismatch!',
text: 'Your passwords do not match, please try again.',
icon: 'error'
})
})
}
page.sendNewPassword = (pass, element) => {
page.updateTrigger(element, 'loading')
axios.post('api/password/change', { password: pass }).then(response => {
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
page.updateTrigger(element)
return swal('An error occurred!', response.data.description, 'error')
}
page.updateTrigger(element)
swal({
title: 'Woohoo!',
text: 'Your password was successfully changed.',
icon: 'success',
buttons: false,
timer: 1500
}).then(() => {
page.changePassword()
})
}).catch(error => {
page.updateTrigger(element)
page.onAxiosError(error)
})
}
page.getUsers = (params = {}) => {
if (page.isSomethingLoading)
return page.warnSomethingLoading()
if (!page.permissions.admin)
return swal('An error occurred!', 'You cannot do this!', 'error')
page.updateTrigger(params.trigger, 'loading')
if (typeof params.pageNum !== 'number')
params.pageNum = 0
const url = `api/users/${params.pageNum}`
axios.get(url).then(response => {
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
page.updateTrigger(params.trigger)
return swal('An error occurred!', response.data.description, 'error')
}
const pages = Math.ceil(response.data.count / 25)
const users = response.data.users
if (params.pageNum && (users.length === 0)) {
page.updateTrigger(params.trigger)
if (params.autoPage) {
params.pageNum = pages - 1
return page.getUsers(params)
} else {
return swal('An error occurred!', `There are no more users to populate page ${params.pageNum + 1}.`, 'error')
}
}
page.currentView = 'users'
page.cache = {}
if (params.pageNum < 0) params.pageNum = Math.max(0, pages + params.pageNum)
const pagination = page.paginate(response.data.count, 25, params.pageNum)
const filter = `
`
const extraControls = `
`
const controls = `
`
// Do some string replacements for bottom controls
const bottomFiltersId = 'bFilters'
const bottomJumpId = 'bJumpToPage'
const bottomExtraControls = extraControls
.replace(/id="filters"/, `id="${bottomFiltersId}"`)
.replace(/(data-action="filter-uploads")/, `$1 data-filtersid="${bottomFiltersId}"`)
.replace(/id="jumpToPage"/, `id="${bottomJumpId}"`)
.replace(/(data-action="jump-to-page")/g, `$1 data-jumpid="${bottomJumpId}"`)
const bottomPagination = pagination
.replace(/(data-action="page-ellipsis")/g, `$1 data-jumpid="${bottomJumpId}"`)
// Whether there are any unselected items
let unselected = false
page.dom.innerHTML = `
${pagination}
${extraControls}
${controls}
${controls}
${bottomExtraControls}
${bottomPagination}
`
const table = document.querySelector('#table')
for (let i = 0; i < users.length; i++) {
const user = users[i]
const selected = page.selected[page.currentView].includes(user.id)
if (!selected) unselected = true
let displayGroup = null
const groups = Object.keys(user.groups)
for (let i = 0; i < groups.length; i++) {
if (!user.groups[groups[i]]) break
displayGroup = groups[i]
}
// Server-side explicitly expects either of these two values to consider a user as disabled
const enabled = user.enabled !== false && user.enabled !== 0
page.cache[user.id] = {
username: user.username,
groups: user.groups,
enabled,
displayGroup
}
const prettyDate = user.registration
? page.getPrettyDate(new Date(user.registration * 1000))
: '-'
const prettyTokenUpdate = user.timestamp
? page.getPrettyDate(new Date(user.timestamp * 1000))
: '-'
const tr = document.createElement('tr')
tr.dataset.id = user.id
tr.innerHTML = `
${user.username}
${user.uploads}
${page.getPrettyBytes(user.usage)}
${displayGroup}
${prettyDate}
${prettyTokenUpdate}
`
table.appendChild(tr)
page.checkboxes = table.querySelectorAll('.checkbox[data-action="select"]')
}
const selectAll = document.querySelector('#selectAll')
if (selectAll && !unselected) {
selectAll.checked = true
selectAll.title = 'Unselect all'
}
page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active')
page.views[page.currentView].pageNum = users.length ? params.pageNum : 0
}).catch(error => {
page.updateTrigger(params.trigger)
page.onAxiosError(error)
})
}
page.createUser = () => {
const groupOptions = Object.keys(page.permissions).map((g, i, a) => {
const disabled = !(a[i + 1] && page.permissions[a[i + 1]])
return `${g} `
}).join('\n')
const div = document.createElement('div')
div.innerHTML = `
`
swal({
title: 'Create new user',
icon: 'info',
content: div,
buttons: {
cancel: true,
confirm: {
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('api/users/create', {
username: document.querySelector('#swalUsername').value,
password: document.querySelector('#swalPassword').value,
group: document.querySelector('#swalGroup').value
}).then(response => {
if (!response) return
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', response.data.description, 'error')
}
const div = document.createElement('div')
div.innerHTML = `
Username: ${response.data.username}
Password: ${response.data.password}
User group: ${response.data.group}
`
swal({
title: 'Created a new user!',
icon: 'success',
content: div
})
// Load last page of users list
// eslint-disable-next-line compat/compat
page.getUsers(Object.assign(page.views.users, {
pageNum: -1
}))
}).catch(page.onAxiosError)
})
}
page.editUser = id => {
const user = page.cache[id]
if (!user) return
const groupOptions = Object.keys(page.permissions).map((g, i, a) => {
const selected = g === user.displayGroup
const disabled = !(a[i + 1] && page.permissions[a[i + 1]])
return `${g} `
}).join('\n')
const div = document.createElement('div')
div.innerHTML = `
`
swal({
title: 'Edit user',
icon: 'info',
content: div,
buttons: {
cancel: true,
confirm: {
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('api/users/edit', {
id,
username: document.querySelector('#swalUsername').value,
group: document.querySelector('#swalGroup').value,
enabled: document.querySelector('#swalEnabled').checked,
resetPassword: document.querySelector('#swalResetPassword').checked
}).then(response => {
if (!response) return
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
return swal('An error occurred!', response.data.description, 'error')
}
let autoClose = true
const div = document.createElement('div')
let displayName = user.username
if (response.data.update.username !== user.username) {
div.innerHTML += `${user.username} was renamed into: ${response.data.update.username} .
`
autoClose = false
displayName = response.data.update.username
}
if (response.data.update.password) {
div.innerHTML += `
${displayName}'s new password is:
${response.data.update.password}
`
autoClose = false
}
if (response.data.update.enabled !== user.enabled)
div.innerHTML += `${displayName} has been ${response.data.update.enabled ? 'enabled' : 'disabled'}!
`
if (!div.innerHTML)
div.innerHTML = `${displayName} was edited!
`
swal({
title: 'Success!',
icon: 'success',
content: div,
buttons: !autoClose,
timer: autoClose ? 1500 : null
})
page.getUsers(page.views.users)
}).catch(page.onAxiosError)
})
}
page.disableUser = id => {
const user = page.cache[id]
if (!user || !user.enabled) return
const content = document.createElement('div')
content.innerHTML = `
You will be disabling a user named ${page.cache[id].username} .
Their files will remain.
`
swal({
title: 'Are you sure?',
icon: 'warning',
content,
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: 'Yes, disable them!',
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('api/users/disable', { id }).then(response => {
if (!response) return
if (response.data.success === false)
if (response.data.description === 'No token provided')
return page.verifyToken(page.token)
else
return swal('An error occurred!', response.data.description, 'error')
swal('Success!', `${page.cache[id].username} has been disabled.`, 'success', {
buttons: false,
timer: 1500
})
page.getUsers(page.views.users)
}).catch(page.onAxiosError)
})
}
page.deleteUser = id => {
const user = page.cache[id]
if (!user) return
const content = document.createElement('div')
content.innerHTML = `
You will be deleting a user named ${page.cache[id].username} .
Their files will remain, unless you choose otherwise.
`
swal({
title: 'Are you sure?',
icon: 'warning',
content,
dangerMode: true,
buttons: {
cancel: true,
confirm: {
text: 'Yes, delete it!',
closeModal: false
},
purge: {
text: 'Yes, and the uploads too!',
value: 'purge',
className: 'swal-button--danger',
closeModal: false
}
}
}).then(proceed => {
if (!proceed) return
axios.post('api/users/delete', {
id,
purge: proceed === 'purge'
}).then(response => {
if (!response) return
if (response.data.success === false) {
const failed = Array.isArray(response.data.failed)
? response.data.failed
: []
if (response.data.description === 'No token provided')
return page.verifyToken(page.token)
else if (failed.length)
return swal('An error occurred!', `Unable to delete ${failed.length} of the user's upload${failed.length === 1 ? '' : 's'}.`, 'error')
else
return swal('An error occurred!', response.data.description, 'error')
}
swal('Success!', `${page.cache[id].username} has been deleted.`, 'success', {
buttons: false,
timer: 1500
})
// Reload users list
// eslint-disable-next-line compat/compat
page.getUsers(Object.assign(page.views.users, {
autoPage: true
}))
}).catch(page.onAxiosError)
})
}
// Roughly based on https://github.com/mayuska/pagination/blob/master/index.js
page.paginate = (totalItems, itemsPerPage, currentPage) => {
currentPage = currentPage + 1
const step = 3
const numPages = Math.ceil(totalItems / itemsPerPage)
let template = ''
const elementsToShow = step * 2
const add = {
pageNum (start, end) {
for (let i = start; i <= end; ++i)
template += ` `
},
startDots () {
template += `
`
},
endDots () {
template += `
`
}
}
if (elementsToShow + 1 >= numPages) {
add.pageNum(1, numPages)
} else if (currentPage < elementsToShow) {
add.pageNum(1, elementsToShow)
add.endDots()
} else if (currentPage > numPages - elementsToShow + 1) {
add.startDots()
add.pageNum(numPages - elementsToShow + 1, numPages)
} else {
add.startDots()
add.pageNum(currentPage - step + 1, currentPage + step - 1)
add.endDots()
}
return `
`
}
page.getStatistics = (params = {}) => {
if (!page.permissions.admin)
return swal('An error occurred!', 'You cannot do this!', 'error')
if (page.isSomethingLoading)
return page.warnSomethingLoading()
page.updateTrigger(params.trigger, 'loading')
const url = 'api/stats'
axios.get(url).then(response => {
if (response.data.success === false)
if (response.data.description === 'No token provided') {
return page.verifyToken(page.token)
} else {
page.updateTrigger(params.trigger)
return swal('An error occurred!', response.data.description, 'error')
}
let content = ''
const keys = Object.keys(response.data.stats)
for (let i = 0; i < keys.length; i++) {
let rows = ''
if (!response.data.stats[keys[i]])
rows += `
Generating, please try again later\u2026
`
else
try {
const types = response.data.stats[keys[i]]._types || {}
const valKeys = Object.keys(response.data.stats[keys[i]])
for (let j = 0; j < valKeys.length; j++) {
// Skip keys that starts with an underscore
if (/^_/.test(valKeys[j]))
continue
const value = response.data.stats[keys[i]][valKeys[j]]
let parsed = value
// Parse values with some preset formatting
if ((types.number || []).includes(valKeys[j]))
parsed = value.toLocaleString()
if ((types.byte || []).includes(valKeys[j]))
parsed = page.getPrettyBytes(value)
if ((types.byteUsage || []).includes(valKeys[j]))
parsed = `${page.getPrettyBytes(value.used)} / ${page.getPrettyBytes(value.total)} (${Math.floor(value.used / value.total * 100)}%)`
if ((types.uptime || []).includes(valKeys[j]))
parsed = page.getPrettyUptime(value)
const string = valKeys[j]
.replace(/([A-Z])/g, ' $1')
.replace(/(^|\s)(cpu|db|zip)/gi, s => s.toUpperCase())
rows += `
${string}
${parsed}
`
}
} catch (error) {
rows = `
Error parsing response. Try again?
`
page.onError(error)
}
content += `
`
}
if (Array.isArray(response.data.hrtime))
content += `
Time taken: ${response.data.hrtime[0]}s ${Math.ceil(response.data.hrtime[1] / 1000000)}ms.
`
page.dom.innerHTML = content
page.fadeAndScroll()
page.updateTrigger(params.trigger, 'active')
}).catch(error => {
page.updateTrigger(params.trigger)
page.onAxiosError(error)
})
}
window.addEventListener('DOMContentLoaded', () => {
// Polyfill Object.assign()
// eslint-disable-next-line compat/compat
if (typeof Object.assign !== 'function')
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, 'assign', {
value: function assign (target, varArgs) { // .length of function is 2
'use strict'
if (target === null || target === undefined)
throw new TypeError('Cannot convert undefined or null to object')
const to = Object(target)
for (let i = 1; i < arguments.length; i++) {
const nextSource = arguments[i]
if (nextSource !== null && nextSource !== undefined)
for (const nextKey in nextSource)
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey))
to[nextKey] = nextSource[nextKey]
}
return to
},
writable: true,
configurable: true
})
// Add 'no-touch' class to non-touch devices
if (!('ontouchstart' in document.documentElement))
document.documentElement.classList.add('no-touch')
const selectedKeys = ['uploads', 'uploadsAll', 'albums', 'albumsAll', 'users']
for (let i = 0; i < selectedKeys.length; i++) {
const ls = localStorage[lsKeys.selected[selectedKeys[i]]]
if (ls) page.selected[selectedKeys[i]] = JSON.parse(ls)
}
page.preparePage()
page.clipboardJS = new ClipboardJS('.clipboard-js')
page.clipboardJS.on('success', () => {
return swal('', 'The link has been copied to clipboard.', 'success', {
buttons: false,
timer: 1500
})
})
page.clipboardJS.on('error', page.onError)
page.lazyLoad = new LazyLoad()
})