/* global swal, axios, ClipboardJS, LazyLoad */ const lsKeys = { token: 'token', viewType: { uploads: 'viewTypeUploads', uploadsAll: 'viewTypeUploadsAll' }, selected: { uploads: 'selectedUploads', uploadsAll: 'selectedUploadsAll', users: 'selectedUsers' } } const page = { // #page dom: null, // user token token: localStorage[lsKeys.token], // from api/tokens/verify username: null, permissions: null, // sidebar menus menus: [], currentView: null, views: { // config of uploads view uploads: { type: localStorage[lsKeys.viewType.uploads], album: null, // album's id pageNum: null // page num }, // config of uploads view (all) uploadsAll: { type: localStorage[lsKeys.viewType.uploadsAll], filters: null, // uploads' filters pageNum: null, // page num all: true }, // config of users view users: { pageNum: null } }, // id of selected items (shared across pages and will be synced with localStorage) selected: { uploads: [], uploadsAll: [], users: [] }, checkboxes: { uploads: [], uploadsAll: [], users: [] }, lastSelected: { upload: null, uploadsAll: null, user: null }, // select album dom for dialogs/modals selectAlbumContainer: null, // cache for dialogs/modals cache: { uploads: {}, albums: {}, users: {} }, clipboardJS: null, lazyLoad: null, imageExts: ['.webp', '.jpg', '.jpeg', '.gif', '.png', '.tiff', '.tif', '.svg'], videoExts: ['.webm', '.mp4', '.wmv', '.avi', '.mov', '.mkv'], fadingIn: null } page.preparePage = function () { if (!page.token) { window.location = 'auth' return } page.verifyToken(page.token, true) } page.verifyToken = function (token, reloadOnError) { axios.post('api/tokens/verify', { token }).then(function (response) { if (response.data.success === false) return swal({ title: 'An error occurred!', text: response.data.description, icon: 'error' }).then(function () { if (!reloadOnError) return localStorage.removeItem(lsKeys.token) location.location = 'auth' }) axios.defaults.headers.common.token = token localStorage[lsKeys.token] = token page.token = token page.username = response.data.username page.permissions = response.data.permissions page.prepareDashboard() }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.prepareDashboard = function () { page.dom = document.querySelector('#page') // Capture all click events page.dom.addEventListener('click', page.domClick, true) // Capture all submit events page.dom.addEventListener('submit', function (event) { // Prevent default if necessary if (event.target && event.target.classList.contains('prevent-default')) return event.preventDefault() }, true) // All item menus in the sidebar const itemMenus = [ { selector: '#itemUploads', onclick: page.getUploads }, { selector: '#itemDeleteUploadsByNames', onclick: page.deleteUploadsByNames }, { selector: '#itemManageAlbums', onclick: page.getAlbums }, { selector: '#itemManageToken', onclick: page.changeToken }, { selector: '#itemChangePassword', onclick: page.changePassword }, { selector: '#itemLogout', onclick: page.logout, inactive: true }, { selector: '#itemManageUploads', onclick: page.getUploads, 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', function () { itemMenus[i].onclick.apply(null, itemMenus[i].params) if (!itemMenus[i].inactive) page.setActiveMenu(this) }) 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 document.querySelector('#dashboard').classList.remove('is-hidden') // Load albums sidebar page.getAlbumsSidebar() if (typeof page.prepareShareX === 'function') page.prepareShareX() } page.logout = function () { localStorage.removeItem(lsKeys.token) location.reload('.') } page.getItemID = function (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 = function (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) { case 'view-list': return page.setUploadsView('list', element) case 'view-thumbs': return page.setUploadsView('thumbs', element) case 'clear-selection': return page.clearSelection() case 'add-selected-uploads-to-album': return page.addSelectedUploadsToAlbum() case 'select': return page.select(element, event) case 'select-all': return page.selectAll(element) case 'add-to-album': return page.addToAlbum(id) case 'delete-upload': return page.deleteUpload(id) case 'bulk-delete-uploads': return page.bulkDeleteUploads() case 'display-thumbnail': return page.displayThumbnail(id) case 'submit-album': return page.submitAlbum(element) case 'edit-album': return page.editAlbum(id) case 'delete-album': return page.deleteAlbum(id) case 'get-new-token': return page.getNewToken(element) case 'edit-user': return page.editUser(id) case 'disable-user': return page.disableUser(id) case 'filters-help': return page.filtersHelp(element) case 'filter-uploads': return page.filterUploads(element) case 'view-user-uploads': return page.viewUserUploads(id) case 'page-ellipsis': return page.focusJumpToPage() case 'page-prev': case 'page-next': case 'page-goto': case 'jump-to-page': return page.switchPage(action, element) } } page.isLoading = function (element, state) { if (!element) return if (state) return element.classList.add('is-loading') element.classList.remove('is-loading') } page.fadeAndScroll = function (content) { if (page.fadingIn) { clearTimeout(page.fadingIn) page.dom.classList.remove('fade-in') } page.dom.classList.add('fade-in') page.fadingIn = setTimeout(function () { page.dom.classList.remove('fade-in') }, 500) page.dom.scrollIntoView(true) } page.switchPage = function (action, element) { const views = {} let func = null if (page.currentView === 'users') { func = page.getUsers } else { func = page.getUploads views.album = page.views[page.currentView].album views.all = page.views[page.currentView].all views.filters = page.views[page.currentView].filters } switch (action) { case 'page-prev': views.pageNum = page.views[page.currentView].pageNum - 1 if (views.pageNum < 0) return swal('An error occurred!', 'This is already the first page.', 'error') return func(views, element) case 'page-next': views.pageNum = page.views[page.currentView].pageNum + 1 return func(views, element) case 'page-goto': views.pageNum = parseInt(element.dataset.goto) return func(views, element) case 'jump-to-page': { const jumpToPage = document.querySelector('#jumpToPage') if (!jumpToPage.checkValidity()) return const parsed = parseInt(jumpToPage.value) views.pageNum = isNaN(parsed) ? 0 : (parsed - 1) if (views.pageNum < 0) views.pageNum = 0 return func(views, element) } } } page.focusJumpToPage = function () { const element = document.querySelector('#jumpToPage') if (!element) return element.focus() element.select() } page.getUploads = function ({ pageNum, album, all, filters, autoPage } = {}, element) { if (element) page.isLoading(element, true) if ((all || filters) && !page.permissions.moderator) return swal('An error occurred!', 'You can not do this!', 'error') if (typeof pageNum !== 'number' || pageNum < 0) pageNum = 0 let url = `api/uploads/${pageNum}` if (typeof album === 'string') url = `api/album/${album}/${pageNum}` const headers = {} if (all) headers.all = '1' if (filters) headers.filters = filters axios.get(url, { headers }).then(function (response) { if (response.data.success === false) if (response.data.description === 'No token provided') { return page.verifyToken(page.token) } else { if (element) page.isLoading(element, false) return swal('An error occurred!', response.data.description, 'error') } const files = response.data.files if (pageNum && (files.length === 0)) { if (element) page.isLoading(element, false) if (autoPage) return page.getUploads({ pageNum: Math.ceil(response.data.count / 25) - 1, album, all, filters }, element) else return swal('An error occurred!', `There are no more uploads to populate page ${pageNum + 1}.`, 'error') } page.currentView = all ? 'uploadsAll' : 'uploads' page.cache.uploads = {} const albums = response.data.albums const users = response.data.users const basedomain = response.data.basedomain const pagination = page.paginate(response.data.count, 25, pageNum) let filter = '
' if (all) filter = `
` const extraControls = `
${filter}
` const controls = `
${all ? '' : ` `} Bulk delete
` // Whether there are any unselected items let unselected = false const hasExpiryDateColumn = files.some(file => 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}` // Cache bare minimum data for thumbnails viewer page.cache.uploads[files[i].id] = { name: files[i].name, thumb: files[i].thumb, original: files[i].file } // 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)) : '-' // 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 (all) files[i].appendix = files[i].userid ? users[files[i].userid] || '' : '' else files[i].appendix = files[i].albumid ? albums[files[i].albumid] || '' : '' } if (page.views[page.currentView].type === 'thumbs') { page.dom.innerHTML = ` ${pagination} ${extraControls} ${controls}

${pagination} ` 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 is-narrow' div.dataset.id = upload.id if (upload.thumb !== undefined) div.innerHTML = `${upload.name}` else div.innerHTML = `

${upload.extname || 'N/A'}

` div.innerHTML += `

${upload.name}

${upload.appendix ? `${upload.appendix} – ` : ''}${upload.prettyBytes}

` table.appendChild(div) page.checkboxes[page.currentView] = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) page.lazyLoad.update() } } else { page.dom.innerHTML = ` ${pagination} ${extraControls} ${controls}
${all ? '' : ''} ${hasExpiryDateColumn ? '' : ''}
File ${all ? 'User' : 'Album'} SizeIPDateExpiry date

${pagination} ` 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} ${upload.appendix} ${upload.prettyBytes} ${all ? `${upload.ip || ''}` : ''} ${upload.prettyDate} ${hasExpiryDateColumn ? `${upload.prettyExpiryDate}` : ''} ${all ? '' : ` `} ` table.appendChild(tr) page.checkboxes[page.currentView] = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) } } const selectAll = document.querySelector('#selectAll') if (selectAll && !unselected) { selectAll.checked = true selectAll.title = 'Unselect all' } page.fadeAndScroll() if (page.currentView === 'uploads') page.views.uploads.album = album if (page.currentView === 'uploadsAll') page.views.uploadsAll.filters = filters page.views[page.currentView].pageNum = files.length ? pageNum : 0 }).catch(function (error) { if (element) page.isLoading(element, false) console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.setUploadsView = function (view, element) { localStorage[lsKeys.viewType[page.currentView]] = view page.views[page.currentView].type = view page.getUploads(page.views[page.currentView], element) } page.displayThumbnail = function (id) { const file = page.cache.uploads[id] if (!file.thumb) return const div = document.createElement('div') div.innerHTML = `
` if (file.original) { div.innerHTML += `
Load original
` div.querySelector('#swalOriginal').addEventListener('click', function () { const button = this const original = button.dataset.original button.classList.add('is-loading') const thumb = div.querySelector('#swalThumb') const exec = /.[\w]+(\?|$)/.exec(original) if (!exec || !exec[0]) return const extname = exec[0].toLowerCase() if (page.imageExts.includes(extname)) { thumb.src = file.original thumb.onload = function () { button.classList.add('is-hidden') document.body.querySelector('.swal-overlay .swal-modal:not(.is-expanded)').classList.add('is-expanded') } thumb.onerror = function () { button.className = 'button is-danger' button.innerHTML = 'Unable to load original' } } else if (page.videoExts.includes(extname)) { thumb.classList.add('is-hidden') const video = document.createElement('video') video.id = 'swalVideo' video.controls = true video.src = file.original thumb.insertAdjacentElement('afterend', video) button.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(function () { const video = div.querySelector('#swalVideo') if (video) video.remove() // Restore modal size document.body.querySelector('.swal-overlay .swal-modal.is-expanded').classList.remove('is-expanded') }) } page.selectAll = function (element) { for (let i = 0; i < page.checkboxes[page.currentView].length; i++) { const id = page.getItemID(page.checkboxes[page.currentView][i]) if (isNaN(id)) continue if (page.checkboxes[page.currentView][i].checked !== element.checked) { page.checkboxes[page.currentView][i].checked = element.checked if (page.checkboxes[page.currentView][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 = function (element, lastElement) { if (!element || !lastElement || element === lastElement) return if (!Array.isArray(page.checkboxes[page.currentView]) || !page.checkboxes[page.currentView].length) return const thisIndex = page.checkboxes[page.currentView].indexOf(element) const lastIndex = page.checkboxes[page.currentView].indexOf(lastElement) const distance = thisIndex - lastIndex if (distance >= -1 && distance <= 1) return for (let i = 0; i < page.checkboxes[page.currentView].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 page.checkboxes[page.currentView][i].checked = lastElement.checked const id = page.getItemID(page.checkboxes[page.currentView][i]) if (!page.selected[page.currentView].includes(id) && page.checkboxes[page.currentView][i].checked) page.selected[page.currentView].push(id) else if (page.selected[page.currentView].includes(id) && !page.checkboxes[page.currentView][i].checked) page.selected[page.currentView].splice(page.selected[page.currentView].indexOf(id), 1) } } page.select = function (element, event) { const id = page.getItemID(element) if (isNaN(id)) return const lastSelected = page.lastSelected[page.currentView] if (event.shiftKey && lastSelected) { page.selectInBetween(element, lastSelected) // Check or uncheck depending on the state of the initial checkbox element.checked = lastSelected.checked } else { page.lastSelected[page.currentView] = 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 = function () { const selected = page.selected[page.currentView] const type = page.currentView === 'users' ? 'users' : 'uploads' 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(function (proceed) { if (!proceed) return const checkboxes = page.checkboxes[page.currentView] 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.filtersHelp = function (element) { const content = document.createElement('div') content.style = 'text-align: left' content.innerHTML = ` This supports 3 filter keys, namely user (username), ip and name (upload name). Each key can be specified more than once. Backlashes should be used if the usernames have spaces. There are also 2 additional flags, namely -user and -ip, which will match uploads by non-registered users and have no IPs respectively. How does it work? First, it will filter uploads matching ANY of the supplied user or ip keys. Then, it will refine the matches using the supplied name keys. Examples: Uploads from user with username "demo": user:demo Uploads from users with username either "John Doe" OR "demo": user:John\\ Doe user:demo Uploads from IP "127.0.0.1" AND which upload names match "*.rar" OR "*.zip": ip:127.0.0.1 name:*.rar name:*.zip Uploads from user with username "test" OR from non-registered users: user:test -user `.trim().replace(/^ {6}/gm, '').replace(/\n/g, '
') swal({ content }) } page.filterUploads = function (element) { const filters = document.querySelector('#filters').value page.getUploads({ all: true, filters }, element) } page.viewUserUploads = function (id) { const user = page.cache.users[id] if (!user) return page.getUploads({ all: true, filters: `user:${user.username.replace(/ /g, '\\ ')}` }) page.setActiveMenu(document.querySelector('#itemManageUploads')) } page.deleteUpload = function (id) { page.postBulkDeleteUploads({ 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 const views = Object.assign({}, page.views[page.currentView]) views.autoPage = true page.getUploads(views) } }) } page.bulkDeleteUploads = function () { const count = page.selected[page.currentView].length if (!count) return swal('An error occurred!', 'You have not selected any uploads.', 'error') page.postBulkDeleteUploads({ 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(function (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 const views = Object.assign({}, page.views[page.currentView]) views.autoPage = true page.getUploads(views) } }) } page.deleteUploadsByNames = function () { let appendix = '' if (page.permissions.moderator) appendix = '
As a staff, you can use this feature to delete uploads from other users.' page.dom.innerHTML = `

Separate each entry with a new line.${appendix}

` page.fadeAndScroll() document.querySelector('#submitBulkDelete').addEventListener('click', function () { const textArea = document.querySelector('#bulkDeleteNames') // Clean up const seen = {} const names = textArea.value .split(/\r?\n/) .map(function (name) { const trimmed = name.trim() return /^[^\s]+$/.test(trimmed) ? trimmed : '' }) .filter(function (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({ field: 'name', values: names, cb (failed) { textArea.value = failed.join('\n') } }) }) } page.postBulkDeleteUploads = function ({ field, values, cb } = {}) { const count = values.length const objective = `${values.length} upload${count === 1 ? '' : 's'}` let text = `You won't be able to recover ${objective}!` if (page.currentView === 'uploadsAll') text += '\nBe aware, you may be nuking uploads by other users!' swal({ title: 'Are you sure?', text, icon: 'warning', dangerMode: true, buttons: { cancel: true, confirm: { text: `Yes, nuke ${values.length === 1 ? 'it' : 'them'}!`, closeModal: false } } }).then(function (proceed) { if (!proceed) return axios.post('api/upload/bulkdelete', { field, values }).then(function (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 === values.length) swal('An error occurred!', `Unable to delete any of the ${objective}.`, 'error') else if (failed.length && failed.length < 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') if (typeof cb === 'function') cb(failed) }).catch(function (error) { console.error(error) swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) }) } page.addSelectedUploadsToAlbum = function () { 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], function (failed) { if (!failed) return if (failed.length) page.selected[page.currentView] = page.selected[page.currentView].filter(function (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 = function (id) { page.addUploadsToAlbum([id], function (failed) { if (!failed) return page.getUploads(page.views[page.currentView]) }) } page.addUploadsToAlbum = function (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.

` swal({ icon: 'warning', content, buttons: { cancel: true, confirm: { text: 'OK', closeModal: false } } }).then(function (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(function (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') callback(add.data.failed) }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) // Get albums list then update content of swal axios.get('api/albums').then(function (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 } const select = document.querySelector('#swalAlbum') // If the prompt was replaced, the container would be missing if (!select) return select.innerHTML += list.data.albums .map(function (album) { return `` }) .join('\n') select.getElementsByTagName('option')[1].innerHTML = 'Choose an album' select.removeAttribute('disabled') }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.getAlbums = function () { axios.get('api/albums').then(function (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') } page.cache.albums = {} page.dom.innerHTML = `

Create new album


List of albums

ID Name Files Created at Public link
` const homeDomain = response.data.homeDomain const table = document.querySelector('#table') for (let i = 0; i < response.data.albums.length; i++) { const album = response.data.albums[i] const albumUrl = `${homeDomain}/a/${album.identifier}` // Prettify album.prettyDate = page.getPrettyDate(new Date(album.timestamp * 1000)) page.cache.albums[album.id] = { name: album.name, download: album.download, public: album.public, description: album.description } const tr = document.createElement('tr') tr.innerHTML = ` ${album.id} ${album.name} ${album.files} ${album.prettyDate} ${albumUrl} ` table.appendChild(tr) } page.fadeAndScroll() }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.editAlbum = function (id) { const album = page.cache.albums[id] if (!album) return const div = document.createElement('div') div.innerHTML = `
` swal({ title: 'Edit album', icon: 'info', content: div, buttons: { cancel: true, confirm: { closeModal: false } } }).then(function (value) { if (!value) return axios.post('api/albums/edit', { 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 }).then(function (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!', `Your album's new identifier is: ${response.data.identifier}.`, 'success') else if (response.data.name !== album.name) swal('Success!', `Your album was renamed to: ${response.data.name}.`, 'success') else swal('Success!', 'Your album was edited!', 'success') page.getAlbumsSidebar() page.getAlbums() }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) }) } page.deleteAlbum = function (id) { swal({ title: 'Are you sure?', text: 'This won\'t delete your uploads, only the album!', icon: 'warning', dangerMode: true, buttons: { cancel: true, confirm: { text: 'Yes, delete it!', closeModal: false }, purge: { text: 'Umm, delete the uploads too please?', value: 'purge', className: 'swal-button--danger', closeModal: false } } }).then(function (proceed) { if (!proceed) return axios.post('api/albums/delete', { id, purge: proceed === 'purge' }).then(function (response) { if (response.data.success === false) if (response.data.description === 'No token provided') { return page.verifyToken(page.token) } else if (Array.isArray(response.data.failed) && response.data.failed.length) { return swal('An error occurred!', 'Unable to delete ', 'error') } else { return swal('An error occurred!', response.data.description, 'error') } swal('Deleted!', 'Your album has been deleted.', 'success') page.getAlbumsSidebar() page.getAlbums() }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) }) } page.submitAlbum = function (element) { page.isLoading(element, true) axios.post('api/albums', { name: document.querySelector('#albumName').value, description: document.querySelector('#albumDescription').value }).then(function (response) { if (!response) return page.isLoading(element, false) 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') page.getAlbumsSidebar() page.getAlbums() }).catch(function (error) { console.error(error) page.isLoading(element, false) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.getAlbumsSidebar = function () { axios.get('api/albums/sidebar').then(function (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 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 (response.data.albums === undefined) return for (let i = 0; i < response.data.albums.length; i++) { const album = response.data.albums[i] const li = document.createElement('li') const a = document.createElement('a') a.id = album.id a.innerHTML = album.name a.addEventListener('click', function () { page.getUploads({ album: this.id }) page.setActiveMenu(this) }) page.menus.push(a) li.appendChild(a) albumsContainer.appendChild(li) } }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.changeToken = function () { axios.get('api/tokens').then(function (response) { 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') } page.dom.innerHTML = `
Request new token
` page.fadeAndScroll() }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.getNewToken = function (element) { page.isLoading(element, true) axios.post('api/tokens/change').then(function (response) { page.isLoading(element, false) 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({ title: 'Woohoo!', text: 'Your token was successfully changed.', icon: 'success' }).then(function () { axios.defaults.headers.common.token = response.data.token localStorage[lsKeys.token] = response.data.token page.token = response.data.token page.changeToken() }) }).catch(function (error) { console.error(error) page.isLoading(element, false) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.changePassword = function () { page.dom.innerHTML = `
` page.fadeAndScroll() document.querySelector('#sendChangePassword').addEventListener('click', function () { if (document.querySelector('#password').value === document.querySelector('#passwordConfirm').value) page.sendNewPassword(document.querySelector('#password').value, this) else swal({ title: 'Password mismatch!', text: 'Your passwords do not match, please try again.', icon: 'error' }) }) } page.sendNewPassword = function (pass, element) { page.isLoading(element, true) axios.post('api/password/change', { password: pass }).then(function (response) { page.isLoading(element, false) 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({ title: 'Woohoo!', text: 'Your password was successfully changed.', icon: 'success' }).then(function () { page.changePassword() }) }).catch(function (error) { console.error(error) page.isLoading(element, false) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.setActiveMenu = function (element) { for (let i = 0; i < page.menus.length; i++) page.menus[i].classList.remove('is-active') element.classList.add('is-active') } page.getUsers = function ({ pageNum } = {}, element) { if (element) page.isLoading(element, true) if (pageNum === undefined) pageNum = 0 if (!page.permissions.admin) return swal('An error occurred!', 'You can not do this!', 'error') const url = `api/users/${pageNum}` axios.get(url).then(function (response) { 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 (pageNum && (response.data.users.length === 0)) { // Only remove loading class here, since beyond this the entire page will be replaced anyways if (element) page.isLoading(element, false) return swal('An error occurred!', `There are no more users to populate page ${pageNum + 1}.`, 'error') } page.currentView = 'users' page.cache.users = {} const pagination = page.paginate(response.data.count, 25, pageNum) const extraControls = `
` const controls = `
Bulk disable Bulk delete
` // Whether there are any unselected items let unselected = false page.dom.innerHTML = ` ${pagination} ${extraControls} ${controls}
ID Username Uploads Usage Group

${pagination} ` const table = document.querySelector('#table') for (let i = 0; i < response.data.users.length; i++) { const user = response.data.users[i] const selected = page.selected.users.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.users[user.id] = { username: user.username, groups: user.groups, enabled, displayGroup } const tr = document.createElement('tr') tr.dataset.id = user.id tr.innerHTML = ` ${user.id} ${user.username} ${user.uploads} ${page.getPrettyBytes(user.usage)} ${displayGroup} ` table.appendChild(tr) page.checkboxes.users = Array.from(table.querySelectorAll('.checkbox[data-action="select"]')) } const selectAll = document.querySelector('#selectAll') if (selectAll && !unselected) { selectAll.checked = true selectAll.title = 'Unselect all' } page.fadeAndScroll() page.views.users.pageNum = response.data.users.length ? pageNum : 0 }).catch(function (error) { if (element) page.isLoading(element, false) console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) } page.editUser = function (id) { const user = page.cache.users[id] if (!user) return const groupOptions = Object.keys(page.permissions).map(function (g, i, a) { const selected = g === user.displayGroup const disabled = !(a[i + 1] && page.permissions[a[i + 1]]) return `` }).join('\n') const div = document.createElement('div') div.innerHTML = `
` swal({ title: 'Edit user', icon: 'info', content: div, buttons: { cancel: true, confirm: { closeModal: false } } }).then(function (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(function (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.password) { const div = document.createElement('div') div.innerHTML = `

${user.username}'s new password is:

${response.data.password}

` swal({ title: 'Success!', icon: 'success', content: div }) } else if (response.data.update && response.data.update.username !== user.username) { swal('Success!', `${user.username} was renamed into: ${response.data.update.name}.`, 'success') } else { swal('Success!', 'The user was edited!', 'success') } page.getUsers(page.views.users) }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) }) } page.disableUser = function (id) { const user = page.cache.users[id] if (!user || !user.enabled) return const content = document.createElement('div') content.innerHTML = `You will be disabling a user with the username ${page.cache.users[id].username}!` swal({ title: 'Are you sure?', icon: 'warning', content, dangerMode: true, buttons: { cancel: true, confirm: { text: 'Yes, disable them!', closeModal: false } } }).then(function (proceed) { if (!proceed) return axios.post('api/users/disable', { id }).then(function (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!', 'The user has been disabled.', 'success') page.getUsers(page.views.users) }).catch(function (error) { console.error(error) return swal('An error occurred!', 'There was an error with the request, please check the console for more information.', 'error') }) }) } page.paginate = function (totalItems, itemsPerPage, currentPage) { // Roughly based on https://github.com/mayuska/pagination/blob/master/index.js 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 += `
  • ${i}
  • ` }, startDots () { template += `
  • 1
  • ` }, endDots () { template += `
  • ${numPages}
  • ` } } 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 = function (element) { if (!page.permissions.admin) return swal('An error occurred!', 'You can not do this!', 'error') page.dom.innerHTML = ` Please wait, this may take a while\u2026 ` const url = 'api/stats' axios.get(url).then(function (response) { 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 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.round(value.used / value.total * 100)}%)` const string = valKeys[j] .replace(/([A-Z])/g, ' $1') .toUpperCase() rows += ` ${string} ${parsed} ` } } catch (error) { console.error(error) rows = ` Error parsing response. Try again? ` } content += `
    ${rows}
    ${keys[i].toUpperCase()}
    ` } page.dom.innerHTML = content page.fadeAndScroll() }).catch(function (error) { console.error(error) const description = error.response.data && error.response.data.description ? error.response.data && error.response.data.description : 'There was an error with the request, please check the console for more information.' page.dom.innerHTML = `

    ${description}

    ` page.fadeAndScroll() return swal('An error occurred!', description, 'error') }) } window.onload = function () { // Add 'no-touch' class to non-touch devices if (!('ontouchstart' in document.documentElement)) document.documentElement.classList.add('no-touch') const selectedKeys = ['uploads', 'uploadsAll', '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', function () { return swal('Copied!', 'The link has been copied to clipboard.', 'success') }) page.clipboardJS.on('error', function (event) { console.error(event) return swal('An error occurred!', 'There was an error when trying to copy the link to clipboard, please check the console for more information.', 'error') }) page.lazyLoad = new LazyLoad() }