Standard JS code conversion + fixes

This commit is contained in:
NGPixel
2017-02-08 20:52:37 -05:00
parent a508b2a7f4
commit 414dc386d6
54 changed files with 4022 additions and 4288 deletions
+24
View File
@@ -0,0 +1,24 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Change log
### Fixed
- Fixed issue with social accounts with empty name
### Changed
- Updated dependencies + snyk policy
- Conversion to Standard JS compliant code
## [v1.0-beta.2] - 2017-01-30
### Added
- Save own profile under My Account
### Changed
- Updated dependencies + snyk policy
[Unreleased]: https://github.com/Requarks/wiki/compare/v1.0-beta.2...HEAD
[v1.0-beta.2]: https://github.com/Requarks/wiki/releases/tag/v1.0-beta.2
+131 -150
View File
@@ -4,207 +4,188 @@
// Licensed under AGPLv3 // Licensed under AGPLv3
// =========================================== // ===========================================
global.PROCNAME = 'AGENT'; global.PROCNAME = 'AGENT'
global.ROOTPATH = __dirname; global.ROOTPATH = __dirname
global.IS_DEBUG = process.env.NODE_ENV === 'development'; global.IS_DEBUG = process.env.NODE_ENV === 'development'
if(IS_DEBUG) { if (IS_DEBUG) {
global.CORE_PATH = ROOTPATH + '/../core/'; global.CORE_PATH = ROOTPATH + '/../core/'
} else { } else {
global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/'; global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/'
} }
// ---------------------------------------- // ----------------------------------------
// Load Winston // Load Winston
// ---------------------------------------- // ----------------------------------------
global.winston = require(CORE_PATH + 'core-libs/winston')(IS_DEBUG); global.winston = require(CORE_PATH + 'core-libs/winston')(IS_DEBUG)
// ---------------------------------------- // ----------------------------------------
// Load global modules // Load global modules
// ---------------------------------------- // ----------------------------------------
winston.info('[AGENT] Background Agent is initializing...'); winston.info('[AGENT] Background Agent is initializing...')
let appconf = require(CORE_PATH + 'core-libs/config')(); let appconf = require(CORE_PATH + 'core-libs/config')()
global.appconfig = appconf.config; global.appconfig = appconf.config
global.appdata = appconf.data; global.appdata = appconf.data
global.db = require(CORE_PATH + 'core-libs/mongodb').init(); global.db = require(CORE_PATH + 'core-libs/mongodb').init()
global.upl = require('./libs/uploads-agent').init(); global.upl = require('./libs/uploads-agent').init()
global.git = require('./libs/git').init(); global.git = require('./libs/git').init()
global.entries = require('./libs/entries').init(); global.entries = require('./libs/entries').init()
global.mark = require('./libs/markdown'); global.mark = require('./libs/markdown')
// ---------------------------------------- // ----------------------------------------
// Load modules // Load modules
// ---------------------------------------- // ----------------------------------------
var _ = require('lodash'); var moment = require('moment')
var moment = require('moment'); var Promise = require('bluebird')
var Promise = require('bluebird'); var fs = Promise.promisifyAll(require('fs-extra'))
var fs = Promise.promisifyAll(require("fs-extra")); var klaw = require('klaw')
var klaw = require('klaw'); var path = require('path')
var path = require('path'); var Cron = require('cron').CronJob
var cron = require('cron').CronJob;
// ---------------------------------------- // ----------------------------------------
// Start Cron // Start Cron
// ---------------------------------------- // ----------------------------------------
var jobIsBusy = false; var jobIsBusy = false
var jobUplWatchStarted = false; var jobUplWatchStarted = false
var job = new cron({ var job = new Cron({
cronTime: '0 */5 * * * *', cronTime: '0 */5 * * * *',
onTick: () => { onTick: () => {
// Make sure we don't start two concurrent jobs
// Make sure we don't start two concurrent jobs if (jobIsBusy) {
winston.warn('[AGENT] Previous job has not completed gracefully or is still running! Skipping for now. (This is not normal, you should investigate)')
return
}
winston.info('[AGENT] Running all jobs...')
jobIsBusy = true
if(jobIsBusy) { // Prepare async job collector
winston.warn('[AGENT] Previous job has not completed gracefully or is still running! Skipping for now. (This is not normal, you should investigate)');
return;
}
winston.info('[AGENT] Running all jobs...');
jobIsBusy = true;
// Prepare async job collector let jobs = []
let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
let dataPath = path.resolve(ROOTPATH, appconfig.paths.data)
let uploadsTempPath = path.join(dataPath, 'temp-upload')
let jobs = []; // ----------------------------------------
let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo); // REGULAR JOBS
let dataPath = path.resolve(ROOTPATH, appconfig.paths.data); // ----------------------------------------
let uploadsPath = path.join(repoPath, 'uploads');
let uploadsTempPath = path.join(dataPath, 'temp-upload');
// ---------------------------------------- //* ****************************************
// REGULAR JOBS // -> Sync with Git remote
// ---------------------------------------- //* ****************************************
//***************************************** jobs.push(git.onReady.then(() => {
//-> Sync with Git remote return git.resync().then(() => {
//***************************************** // -> Stream all documents
jobs.push(git.onReady.then(() => { let cacheJobs = []
return git.resync().then(() => { let jobCbStreamDocsResolve = null
let jobCbStreamDocs = new Promise((resolve, reject) => {
jobCbStreamDocsResolve = resolve
})
//-> Stream all documents klaw(repoPath).on('data', function (item) {
if (path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') {
let entryPath = entries.parsePath(entries.getEntryPathFromFullPath(item.path))
let cachePath = entries.getCachePath(entryPath)
let cacheJobs = []; // -> Purge outdated cache
let jobCbStreamDocs_resolve = null,
jobCbStreamDocs = new Promise((resolve, reject) => {
jobCbStreamDocs_resolve = resolve;
});
klaw(repoPath).on('data', function (item) { cacheJobs.push(
if(path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') { fs.statAsync(cachePath).then((st) => {
return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active'
}).catch((err) => {
return (err.code !== 'EEXIST') ? err : 'new'
}).then((fileStatus) => {
// -> Delete expired cache file
let entryPath = entries.parsePath(entries.getEntryPathFromFullPath(item.path)); if (fileStatus === 'expired') {
let cachePath = entries.getCachePath(entryPath); return fs.unlinkAsync(cachePath).return(fileStatus)
}
//-> Purge outdated cache return fileStatus
}).then((fileStatus) => {
// -> Update cache and search index
cacheJobs.push( if (fileStatus !== 'active') {
fs.statAsync(cachePath).then((st) => { return entries.updateCache(entryPath)
return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active'; }
}).catch((err) => {
return (err.code !== 'EEXIST') ? err : 'new';
}).then((fileStatus) => {
//-> Delete expired cache file return true
})
)
}
}).on('end', () => {
jobCbStreamDocsResolve(Promise.all(cacheJobs))
})
if(fileStatus === 'expired') { return jobCbStreamDocs
return fs.unlinkAsync(cachePath).return(fileStatus); })
} }))
return fileStatus; //* ****************************************
// -> Clear failed temporary upload files
//* ****************************************
}).then((fileStatus) => { jobs.push(
fs.readdirAsync(uploadsTempPath).then((ls) => {
let fifteenAgo = moment().subtract(15, 'minutes')
//-> Update cache and search index return Promise.map(ls, (f) => {
return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s } })
}).filter((s) => { return s.stat.isFile() }).then((arrFiles) => {
return Promise.map(arrFiles, (f) => {
if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
return fs.unlinkAsync(path.join(uploadsTempPath, f.filename))
} else {
return true
}
})
})
})
)
if(fileStatus !== 'active') { // ----------------------------------------
return entries.updateCache(entryPath); // Run
} // ----------------------------------------
return true; Promise.all(jobs).then(() => {
winston.info('[AGENT] All jobs completed successfully! Going to sleep for now.')
}) if (!jobUplWatchStarted) {
jobUplWatchStarted = true
); upl.initialScan().then(() => {
job.start()
} })
}).on('end', () => { }
jobCbStreamDocs_resolve(Promise.all(cacheJobs));
});
return jobCbStreamDocs;
});
}));
//*****************************************
//-> Clear failed temporary upload files
//*****************************************
jobs.push(
fs.readdirAsync(uploadsTempPath).then((ls) => {
let fifteenAgo = moment().subtract(15, 'minutes');
return Promise.map(ls, (f) => {
return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s }; });
}).filter((s) => { return s.stat.isFile(); }).then((arrFiles) => {
return Promise.map(arrFiles, (f) => {
if(moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
return fs.unlinkAsync(path.join(uploadsTempPath, f.filename));
} else {
return true;
}
});
});
})
);
// ----------------------------------------
// Run
// ----------------------------------------
Promise.all(jobs).then(() => {
winston.info('[AGENT] All jobs completed successfully! Going to sleep for now.');
if(!jobUplWatchStarted) {
jobUplWatchStarted = true;
upl.initialScan().then(() => {
job.start();
});
}
return true;
}).catch((err) => {
winston.error('[AGENT] One or more jobs have failed: ', err);
}).finally(() => {
jobIsBusy = false;
});
},
start: false,
timeZone: 'UTC',
runOnInit: true
});
return true
}).catch((err) => {
winston.error('[AGENT] One or more jobs have failed: ', err)
}).finally(() => {
jobIsBusy = false
})
},
start: false,
timeZone: 'UTC',
runOnInit: true
})
// ---------------------------------------- // ----------------------------------------
// Shutdown gracefully // Shutdown gracefully
// ---------------------------------------- // ----------------------------------------
process.on('disconnect', () => { process.on('disconnect', () => {
winston.warn('[AGENT] Lost connection to main server. Exiting...'); winston.warn('[AGENT] Lost connection to main server. Exiting...')
job.stop(); job.stop()
process.exit(); process.exit()
}); })
process.on('exit', () => { process.on('exit', () => {
job.stop(); job.stop()
}); })
+21 -1
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
File diff suppressed because one or more lines are too long
+25 -25
View File
File diff suppressed because one or more lines are too long
+32 -34
View File
@@ -1,59 +1,57 @@
"use strict"; 'use strict'
jQuery( document ).ready(function( $ ) {
jQuery(document).ready(function ($) {
// ==================================== // ====================================
// Scroll // Scroll
// ==================================== // ====================================
$('a').smoothScroll({ $('a').smoothScroll({
speed: 400, speed: 400,
offset: -70 offset: -70
}); })
var sticky = new Sticky('.stickyscroll'); var sticky = new Sticky('.stickyscroll')
// ==================================== // ====================================
// Notifications // Notifications
// ==================================== // ====================================
$(window).bind('beforeunload', () => { $(window).bind('beforeunload', () => {
$('#notifload').addClass('active'); $('#notifload').addClass('active')
}); })
$(document).ajaxSend(() => { $(document).ajaxSend(() => {
$('#notifload').addClass('active'); $('#notifload').addClass('active')
}).ajaxComplete(() => { }).ajaxComplete(() => {
$('#notifload').removeClass('active'); $('#notifload').removeClass('active')
}); })
var alerts = new Alerts(); var alerts = new Alerts()
if(alertsData) { if (alertsData) {
_.forEach(alertsData, (alertRow) => { _.forEach(alertsData, (alertRow) => {
alerts.push(alertRow); alerts.push(alertRow)
}); })
} }
// ==================================== // ====================================
// Establish WebSocket connection // Establish WebSocket connection
// ==================================== // ====================================
var socket = io(window.location.origin); var socket = io(window.location.origin)
//=include components/search.js // =include components/search.js
// ==================================== // ====================================
// Pages logic // Pages logic
// ==================================== // ====================================
//=include pages/view.js // =include pages/view.js
//=include pages/create.js // =include pages/create.js
//=include pages/edit.js // =include pages/edit.js
//=include pages/source.js // =include pages/source.js
//=include pages/admin.js // =include pages/admin.js
})
}); // =include helpers/form.js
// =include helpers/pages.js
//=include helpers/form.js // =include components/alerts.js
//=include helpers/pages.js
//=include components/alerts.js
+61 -67
View File
@@ -1,4 +1,4 @@
"use strict"; 'use strict'
/** /**
* Alerts * Alerts
@@ -10,25 +10,23 @@ class Alerts {
* *
* @class * @class
*/ */
constructor() { constructor () {
let self = this
let self = this; self.mdl = new Vue({
el: '#alerts',
data: {
children: []
},
methods: {
acknowledge: (uid) => {
self.close(uid)
}
}
})
self.mdl = new Vue({ self.uidNext = 1
el: '#alerts', }
data: {
children: []
},
methods: {
acknowledge: (uid) => {
self.close(uid);
}
}
});
self.uidNext = 1;
}
/** /**
* Show a new Alert * Show a new Alert
@@ -36,29 +34,27 @@ class Alerts {
* @param {Object} options Alert properties * @param {Object} options Alert properties
* @return {null} Void * @return {null} Void
*/ */
push(options) { push (options) {
let self = this
let self = this; let nAlert = _.defaults(options, {
_uid: self.uidNext,
class: 'info',
message: '---',
sticky: false,
title: '---'
})
let nAlert = _.defaults(options, { self.mdl.children.push(nAlert)
_uid: self.uidNext,
class: 'info',
message: '---',
sticky: false,
title: '---'
});
self.mdl.children.push(nAlert); if (!nAlert.sticky) {
_.delay(() => {
self.close(nAlert._uid)
}, 5000)
}
if(!nAlert.sticky) { self.uidNext++
_.delay(() => { }
self.close(nAlert._uid);
}, 5000);
}
self.uidNext++;
}
/** /**
* Shorthand method for pushing errors * Shorthand method for pushing errors
@@ -66,14 +62,14 @@ class Alerts {
* @param {String} title The title * @param {String} title The title
* @param {String} message The message * @param {String} message The message
*/ */
pushError(title, message) { pushError (title, message) {
this.push({ this.push({
class: 'error', class: 'error',
message, message,
sticky: false, sticky: false,
title title
}); })
} }
/** /**
* Shorthand method for pushing success messages * Shorthand method for pushing success messages
@@ -81,35 +77,33 @@ class Alerts {
* @param {String} title The title * @param {String} title The title
* @param {String} message The message * @param {String} message The message
*/ */
pushSuccess(title, message) { pushSuccess (title, message) {
this.push({ this.push({
class: 'success', class: 'success',
message, message,
sticky: false, sticky: false,
title title
}); })
} }
/** /**
* Close an alert * Close an alert
* *
* @param {Integer} uid The unique ID of the alert * @param {Integer} uid The unique ID of the alert
*/ */
close(uid) { close (uid) {
let self = this
let self = this; let nAlertIdx = _.findIndex(self.mdl.children, ['_uid', uid])
let nAlert = _.nth(self.mdl.children, nAlertIdx)
let nAlertIdx = _.findIndex(self.mdl.children, ['_uid', uid]); if (nAlertIdx >= 0 && nAlert) {
let nAlert = _.nth(self.mdl.children, nAlertIdx); nAlert.class += ' exit'
Vue.set(self.mdl.children, nAlertIdx, nAlert)
if(nAlertIdx >= 0 && nAlert) { _.delay(() => {
nAlert.class += ' exit'; self.mdl.children.splice(nAlertIdx, 1)
Vue.set(self.mdl.children, nAlertIdx, nAlert); }, 500)
_.delay(() => { }
self.mdl.children.splice(nAlertIdx, 1); }
}, 500);
}
}
} }
+61 -65
View File
@@ -1,78 +1,74 @@
let modelist = ace.require("ace/ext/modelist"); let modelist = ace.require('ace/ext/modelist')
let codeEditor = null; let codeEditor = null
// ACE - Mode Loader // ACE - Mode Loader
let modelistLoaded = []; let modelistLoaded = []
let loadAceMode = (m) => { let loadAceMode = (m) => {
return $.ajax({ return $.ajax({
url: '/js/ace/mode-' + m + '.js', url: '/js/ace/mode-' + m + '.js',
dataType: "script", dataType: 'script',
cache: true, cache: true,
beforeSend: () => { beforeSend: () => {
if(_.includes(modelistLoaded, m)) { if (_.includes(modelistLoaded, m)) {
return false; return false
} }
}, },
success: () => { success: () => {
modelistLoaded.push(m); modelistLoaded.push(m)
} }
}); })
}; }
// Vue Code Block instance // Vue Code Block instance
let vueCodeBlock = new Vue({ let vueCodeBlock = new Vue({
el: '#modal-editor-codeblock', el: '#modal-editor-codeblock',
data: { data: {
modes: modelist.modesByName, modes: modelist.modesByName,
modeSelected: 'text', modeSelected: 'text',
initContent: '' initContent: ''
}, },
watch: { watch: {
modeSelected: (val, oldVal) => { modeSelected: (val, oldVal) => {
loadAceMode(val).done(() => { loadAceMode(val).done(() => {
ace.require("ace/mode/" + val); ace.require('ace/mode/' + val)
codeEditor.getSession().setMode("ace/mode/" + val); codeEditor.getSession().setMode('ace/mode/' + val)
}); })
} }
}, },
methods: { methods: {
open: (ev) => { open: (ev) => {
$('#modal-editor-codeblock').addClass('is-active')
$('#modal-editor-codeblock').addClass('is-active'); _.delay(() => {
codeEditor = ace.edit('codeblock-editor')
codeEditor.setTheme('ace/theme/tomorrow_night')
codeEditor.getSession().setMode('ace/mode/' + vueCodeBlock.modeSelected)
codeEditor.setOption('fontSize', '14px')
codeEditor.setOption('hScrollBarAlwaysVisible', false)
codeEditor.setOption('wrap', true)
_.delay(() => { codeEditor.setValue(vueCodeBlock.initContent)
codeEditor = ace.edit("codeblock-editor");
codeEditor.setTheme("ace/theme/tomorrow_night");
codeEditor.getSession().setMode("ace/mode/" + vueCodeBlock.modeSelected);
codeEditor.setOption('fontSize', '14px');
codeEditor.setOption('hScrollBarAlwaysVisible', false);
codeEditor.setOption('wrap', true);
codeEditor.setValue(vueCodeBlock.initContent); codeEditor.focus()
codeEditor.renderer.updateFull()
}, 300)
},
cancel: (ev) => {
mdeModalOpenState = false
$('#modal-editor-codeblock').removeClass('is-active')
vueCodeBlock.initContent = ''
},
insertCode: (ev) => {
if (mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection')
}
let codeBlockText = '\n```' + vueCodeBlock.modeSelected + '\n' + codeEditor.getValue() + '\n```\n'
codeEditor.focus(); mde.codemirror.doc.replaceSelection(codeBlockText)
codeEditor.renderer.updateFull(); vueCodeBlock.cancel()
}, 300); }
}
}, })
cancel: (ev) => {
mdeModalOpenState = false;
$('#modal-editor-codeblock').removeClass('is-active');
vueCodeBlock.initContent = '';
},
insertCode: (ev) => {
if(mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection');
}
let codeBlockText = '\n```' + vueCodeBlock.modeSelected + '\n' + codeEditor.getValue() + '\n```\n';
mde.codemirror.doc.replaceSelection(codeBlockText);
vueCodeBlock.cancel();
}
}
});
+289 -303
View File
@@ -1,365 +1,351 @@
let vueFile = new Vue({ let vueFile = new Vue({
el: '#modal-editor-file', el: '#modal-editor-file',
data: { data: {
isLoading: false, isLoading: false,
isLoadingText: '', isLoadingText: '',
newFolderName: '', newFolderName: '',
newFolderShow: false, newFolderShow: false,
newFolderError: false, newFolderError: false,
folders: [], folders: [],
currentFolder: '', currentFolder: '',
currentFile: '', currentFile: '',
files: [], files: [],
uploadSucceeded: false, uploadSucceeded: false,
postUploadChecks: 0, postUploadChecks: 0,
renameFileShow: false, renameFileShow: false,
renameFileId: '', renameFileId: '',
renameFileFilename: '', renameFileFilename: '',
deleteFileShow: false, deleteFileShow: false,
deleteFileId: '', deleteFileId: '',
deleteFileFilename: '' deleteFileFilename: ''
}, },
methods: { methods: {
open: () => { open: () => {
mdeModalOpenState = true; mdeModalOpenState = true
$('#modal-editor-file').addClass('is-active'); $('#modal-editor-file').addClass('is-active')
vueFile.refreshFolders(); vueFile.refreshFolders()
}, },
cancel: (ev) => { cancel: (ev) => {
mdeModalOpenState = false; mdeModalOpenState = false
$('#modal-editor-file').removeClass('is-active'); $('#modal-editor-file').removeClass('is-active')
}, },
// ------------------------------------------- // -------------------------------------------
// INSERT LINK TO FILE // INSERT LINK TO FILE
// ------------------------------------------- // -------------------------------------------
selectFile: (fileId) => { selectFile: (fileId) => {
vueFile.currentFile = fileId; vueFile.currentFile = fileId
}, },
insertFileLink: (ev) => { insertFileLink: (ev) => {
if (mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection')
}
if(mde.codemirror.doc.somethingSelected()) { let selFile = _.find(vueFile.files, ['_id', vueFile.currentFile])
mde.codemirror.execCommand('singleSelection'); selFile.normalizedPath = (selFile.folder === 'f:') ? selFile.filename : selFile.folder.slice(2) + '/' + selFile.filename
} selFile.titleGuess = _.startCase(selFile.basename)
let selFile = _.find(vueFile.files, ['_id', vueFile.currentFile]); let fileText = '[' + selFile.titleGuess + '](/uploads/' + selFile.normalizedPath + ' "' + selFile.titleGuess + '")'
selFile.normalizedPath = (selFile.folder === 'f:') ? selFile.filename : selFile.folder.slice(2) + '/' + selFile.filename;
selFile.titleGuess = _.startCase(selFile.basename);
let fileText = '[' + selFile.titleGuess + '](/uploads/' + selFile.normalizedPath + ' "' + selFile.titleGuess + '")'; mde.codemirror.doc.replaceSelection(fileText)
vueFile.cancel()
mde.codemirror.doc.replaceSelection(fileText); },
vueFile.cancel();
},
// ------------------------------------------- // -------------------------------------------
// NEW FOLDER // NEW FOLDER
// ------------------------------------------- // -------------------------------------------
newFolder: (ev) => { newFolder: (ev) => {
vueFile.newFolderName = ''; vueFile.newFolderName = ''
vueFile.newFolderError = false; vueFile.newFolderError = false
vueFile.newFolderShow = true; vueFile.newFolderShow = true
_.delay(() => { $('#txt-editor-file-newfoldername').focus(); }, 400); _.delay(() => { $('#txt-editor-file-newfoldername').focus() }, 400)
}, },
newFolderDiscard: (ev) => { newFolderDiscard: (ev) => {
vueFile.newFolderShow = false; vueFile.newFolderShow = false
}, },
newFolderCreate: (ev) => { newFolderCreate: (ev) => {
let regFolderName = new RegExp('^[a-z0-9][a-z0-9\-]*[a-z0-9]$')
vueFile.newFolderName = _.kebabCase(_.trim(vueFile.newFolderName))
let regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$"); if (_.isEmpty(vueFile.newFolderName) || !regFolderName.test(vueFile.newFolderName)) {
vueFile.newFolderName = _.kebabCase(_.trim(vueFile.newFolderName)); vueFile.newFolderError = true
return
}
if(_.isEmpty(vueFile.newFolderName) || !regFolderName.test(vueFile.newFolderName)) { vueFile.newFolderDiscard()
vueFile.newFolderError = true; vueFile.isLoadingText = 'Creating new folder...'
return; vueFile.isLoading = true
}
vueFile.newFolderDiscard(); Vue.nextTick(() => {
vueFile.isLoadingText = 'Creating new folder...'; socket.emit('uploadsCreateFolder', { foldername: vueFile.newFolderName }, (data) => {
vueFile.isLoading = true; vueFile.folders = data
vueFile.currentFolder = vueFile.newFolderName
Vue.nextTick(() => { vueFile.files = []
socket.emit('uploadsCreateFolder', { foldername: vueFile.newFolderName }, (data) => { vueFile.isLoading = false
vueFile.folders = data; })
vueFile.currentFolder = vueFile.newFolderName; })
vueFile.files = []; },
vueFile.isLoading = false;
});
});
},
// ------------------------------------------- // -------------------------------------------
// RENAME FILE // RENAME FILE
// ------------------------------------------- // -------------------------------------------
renameFile: () => { renameFile: () => {
let c = _.find(vueFile.files, ['_id', vueFile.renameFileId ])
vueFile.renameFileFilename = c.basename || ''
vueFile.renameFileShow = true
_.delay(() => {
$('#txt-editor-renamefile').focus()
_.defer(() => { $('#txt-editor-file-rename').select() })
}, 400)
},
renameFileDiscard: () => {
vueFile.renameFileShow = false
},
renameFileGo: () => {
vueFile.renameFileDiscard()
vueFile.isLoadingText = 'Renaming file...'
vueFile.isLoading = true
let c = _.find(vueFile.files, ['_id', vueFile.renameFileId ]); Vue.nextTick(() => {
vueFile.renameFileFilename = c.basename || ''; socket.emit('uploadsRenameFile', { uid: vueFile.renameFileId, folder: vueFile.currentFolder, filename: vueFile.renameFileFilename }, (data) => {
vueFile.renameFileShow = true; if (data.ok) {
_.delay(() => { vueFile.waitChangeComplete(vueFile.files.length, false)
$('#txt-editor-renamefile').focus(); } else {
_.defer(() => { $('#txt-editor-file-rename').select(); }); vueFile.isLoading = false
}, 400); alerts.pushError('Rename error', data.msg)
}, }
renameFileDiscard: () => { })
vueFile.renameFileShow = false; })
}, },
renameFileGo: () => {
vueFile.renameFileDiscard();
vueFile.isLoadingText = 'Renaming file...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsRenameFile', { uid: vueFile.renameFileId, folder: vueFile.currentFolder, filename: vueFile.renameFileFilename }, (data) => {
if(data.ok) {
vueFile.waitChangeComplete(vueFile.files.length, false);
} else {
vueFile.isLoading = false;
alerts.pushError('Rename error', data.msg);
}
});
});
},
// ------------------------------------------- // -------------------------------------------
// MOVE FILE // MOVE FILE
// ------------------------------------------- // -------------------------------------------
moveFile: (uid, fld) => { moveFile: (uid, fld) => {
vueFile.isLoadingText = 'Moving file...'; vueFile.isLoadingText = 'Moving file...'
vueFile.isLoading = true; vueFile.isLoading = true
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => { socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
if(data.ok) { if (data.ok) {
vueFile.loadFiles(); vueFile.loadFiles()
} else { } else {
vueFile.isLoading = false; vueFile.isLoading = false
alerts.pushError('Rename error', data.msg); alerts.pushError('Rename error', data.msg)
} }
}); })
}); })
}, },
// ------------------------------------------- // -------------------------------------------
// DELETE FILE // DELETE FILE
// ------------------------------------------- // -------------------------------------------
deleteFileWarn: (show) => { deleteFileWarn: (show) => {
if(show) { if (show) {
let c = _.find(vueFile.files, ['_id', vueFile.deleteFileId ]); let c = _.find(vueFile.files, ['_id', vueFile.deleteFileId ])
vueFile.deleteFileFilename = c.filename || 'this file'; vueFile.deleteFileFilename = c.filename || 'this file'
} }
vueFile.deleteFileShow = show; vueFile.deleteFileShow = show
}, },
deleteFileGo: () => { deleteFileGo: () => {
vueFile.deleteFileWarn(false); vueFile.deleteFileWarn(false)
vueFile.isLoadingText = 'Deleting file...'; vueFile.isLoadingText = 'Deleting file...'
vueFile.isLoading = true; vueFile.isLoading = true
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsDeleteFile', { uid: vueFile.deleteFileId }, (data) => { socket.emit('uploadsDeleteFile', { uid: vueFile.deleteFileId }, (data) => {
vueFile.loadFiles(); vueFile.loadFiles()
}); })
}); })
}, },
// ------------------------------------------- // -------------------------------------------
// LOAD FROM REMOTE // LOAD FROM REMOTE
// ------------------------------------------- // -------------------------------------------
selectFolder: (fldName) => { selectFolder: (fldName) => {
vueFile.currentFolder = fldName; vueFile.currentFolder = fldName
vueFile.loadFiles(); vueFile.loadFiles()
}, },
refreshFolders: () => { refreshFolders: () => {
vueFile.isLoadingText = 'Fetching folders list...'; vueFile.isLoadingText = 'Fetching folders list...'
vueFile.isLoading = true; vueFile.isLoading = true
vueFile.currentFolder = ''; vueFile.currentFolder = ''
vueFile.currentImage = ''; vueFile.currentImage = ''
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsGetFolders', { }, (data) => { socket.emit('uploadsGetFolders', { }, (data) => {
vueFile.folders = data; vueFile.folders = data
vueFile.loadFiles(); vueFile.loadFiles()
}); })
}); })
}, },
loadFiles: (silent) => { loadFiles: (silent) => {
if(!silent) { if (!silent) {
vueFile.isLoadingText = 'Fetching files...'; vueFile.isLoadingText = 'Fetching files...'
vueFile.isLoading = true; vueFile.isLoading = true
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsGetFiles', { folder: vueFile.currentFolder }, (data) => { socket.emit('uploadsGetFiles', { folder: vueFile.currentFolder }, (data) => {
vueFile.files = data; vueFile.files = data
if(!silent) { if (!silent) {
vueFile.isLoading = false; vueFile.isLoading = false
} }
vueFile.attachContextMenus(); vueFile.attachContextMenus()
resolve(true); resolve(true)
}); })
}); })
}); })
}, },
waitChangeComplete: (oldAmount, expectChange) => { waitChangeComplete: (oldAmount, expectChange) => {
expectChange = (_.isBoolean(expectChange)) ? expectChange : true
expectChange = (_.isBoolean(expectChange)) ? expectChange : true; vueFile.postUploadChecks++
vueFile.isLoadingText = 'Processing...'
vueFile.postUploadChecks++; Vue.nextTick(() => {
vueFile.isLoadingText = 'Processing...'; vueFile.loadFiles(true).then(() => {
if ((vueFile.files.length !== oldAmount) === expectChange) {
Vue.nextTick(() => { vueFile.postUploadChecks = 0
vueFile.loadFiles(true).then(() => { vueFile.isLoading = false
if((vueFile.files.length !== oldAmount) === expectChange) { } else if (vueFile.postUploadChecks > 5) {
vueFile.postUploadChecks = 0; vueFile.postUploadChecks = 0
vueFile.isLoading = false; vueFile.isLoading = false
} else if(vueFile.postUploadChecks > 5) { alerts.pushError('Unable to fetch updated listing', 'Try again later')
vueFile.postUploadChecks = 0; } else {
vueFile.isLoading = false; _.delay(() => {
alerts.pushError('Unable to fetch updated listing', 'Try again later'); vueFile.waitChangeComplete(oldAmount, expectChange)
} else { }, 1500)
_.delay(() => { }
vueFile.waitChangeComplete(oldAmount, expectChange); })
}, 1500); })
} },
});
});
},
// ------------------------------------------- // -------------------------------------------
// IMAGE CONTEXT MENU // IMAGE CONTEXT MENU
// ------------------------------------------- // -------------------------------------------
attachContextMenus: () => { attachContextMenus: () => {
let moveFolders = _.map(vueFile.folders, (f) => {
return {
name: (f !== '') ? f : '/ (root)',
icon: 'fa-folder',
callback: (key, opt) => {
let moveFileId = _.toString($(opt.$trigger).data('uid'))
let moveFileDestFolder = _.nth(vueFile.folders, key)
vueFile.moveFile(moveFileId, moveFileDestFolder)
}
}
})
let moveFolders = _.map(vueFile.folders, (f) => { $.contextMenu('destroy', '.editor-modal-file-choices > figure')
return { $.contextMenu({
name: (f !== '') ? f : '/ (root)', selector: '.editor-modal-file-choices > figure',
icon: 'fa-folder', appendTo: '.editor-modal-file-choices',
callback: (key, opt) => { position: (opt, x, y) => {
let moveFileId = _.toString($(opt.$trigger).data('uid')); $(opt.$trigger).addClass('is-contextopen')
let moveFileDestFolder = _.nth(vueFile.folders, key); let trigPos = $(opt.$trigger).position()
vueFile.moveFile(moveFileId, moveFileDestFolder); let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 }
} opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w })
}; },
}); events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen')
}
},
items: {
rename: {
name: 'Rename',
icon: 'fa-edit',
callback: (key, opt) => {
vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid)
vueFile.renameFile()
}
},
move: {
name: 'Move to...',
icon: 'fa-folder-open-o',
items: moveFolders
},
delete: {
name: 'Delete',
icon: 'fa-trash',
callback: (key, opt) => {
vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid)
vueFile.deleteFileWarn(true)
}
}
}
})
}
$.contextMenu('destroy', '.editor-modal-file-choices > figure'); }
$.contextMenu({ })
selector: '.editor-modal-file-choices > figure',
appendTo: '.editor-modal-file-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen');
let trigPos = $(opt.$trigger).position();
let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 };
opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w });
},
events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen');
}
},
items: {
rename: {
name: "Rename",
icon: "fa-edit",
callback: (key, opt) => {
vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid);
vueFile.renameFile();
}
},
move: {
name: "Move to...",
icon: "fa-folder-open-o",
items: moveFolders
},
delete: {
name: "Delete",
icon: "fa-trash",
callback: (key, opt) => {
vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid);
vueFile.deleteFileWarn(true);
}
}
}
});
}
}
});
$('#btn-editor-file-upload input').on('change', (ev) => { $('#btn-editor-file-upload input').on('change', (ev) => {
let curFileAmount = vueFile.files.length
let curFileAmount = vueFile.files.length; $(ev.currentTarget).simpleUpload('/uploads/file', {
$(ev.currentTarget).simpleUpload("/uploads/file", { name: 'binfile',
data: {
folder: vueFile.currentFolder
},
limit: 20,
expect: 'json',
maxFileSize: 0,
name: 'binfile', init: (totalUploads) => {
data: { vueFile.uploadSucceeded = false
folder: vueFile.currentFolder vueFile.isLoadingText = 'Preparing to upload...'
}, vueFile.isLoading = true
limit: 20, },
expect: 'json',
maxFileSize: 0,
init: (totalUploads) => { progress: (progress) => {
vueFile.uploadSucceeded = false; vueFile.isLoadingText = 'Uploading...' + Math.round(progress) + '%'
vueFile.isLoadingText = 'Preparing to upload...'; },
vueFile.isLoading = true;
},
progress: (progress) => { success: (data) => {
vueFile.isLoadingText = 'Uploading...' + Math.round(progress) + '%'; if (data.ok) {
}, let failedUpls = _.filter(data.results, ['ok', false])
if (failedUpls.length) {
_.forEach(failedUpls, (u) => {
alerts.pushError('Upload error', u.msg)
})
if (failedUpls.length < data.results.length) {
alerts.push({
title: 'Some uploads succeeded',
message: 'Files that are not mentionned in the errors above were uploaded successfully.'
})
vueFile.uploadSucceeded = true
}
} else {
vueFile.uploadSucceeded = true
}
} else {
alerts.pushError('Upload error', data.msg)
}
},
success: (data) => { error: (error) => {
if(data.ok) { alerts.pushError(error.message, this.upload.file.name)
},
let failedUpls = _.filter(data.results, ['ok', false]); finish: () => {
if(failedUpls.length) { if (vueFile.uploadSucceeded) {
_.forEach(failedUpls, (u) => { vueFile.waitChangeComplete(curFileAmount, true)
alerts.pushError('Upload error', u.msg); } else {
}); vueFile.isLoading = false
if(failedUpls.length < data.results.length) { }
alerts.push({ }
title: 'Some uploads succeeded',
message: 'Files that are not mentionned in the errors above were uploaded successfully.'
});
vueFile.uploadSucceeded = true;
}
} else {
vueFile.uploadSucceeded = true;
}
} else { })
alerts.pushError('Upload error', data.msg); })
}
},
error: (error) => {
alerts.pushError(error.message, this.upload.file.name);
},
finish: () => {
if(vueFile.uploadSucceeded) {
vueFile.waitChangeComplete(curFileAmount, true);
} else {
vueFile.isLoading = false;
}
}
});
});
+328 -344
View File
@@ -1,412 +1,396 @@
let vueImage = new Vue({ let vueImage = new Vue({
el: '#modal-editor-image', el: '#modal-editor-image',
data: { data: {
isLoading: false, isLoading: false,
isLoadingText: '', isLoadingText: '',
newFolderName: '', newFolderName: '',
newFolderShow: false, newFolderShow: false,
newFolderError: false, newFolderError: false,
fetchFromUrlURL: '', fetchFromUrlURL: '',
fetchFromUrlShow: false, fetchFromUrlShow: false,
folders: [], folders: [],
currentFolder: '', currentFolder: '',
currentImage: '', currentImage: '',
currentAlign: 'left', currentAlign: 'left',
images: [], images: [],
uploadSucceeded: false, uploadSucceeded: false,
postUploadChecks: 0, postUploadChecks: 0,
renameImageShow: false, renameImageShow: false,
renameImageId: '', renameImageId: '',
renameImageFilename: '', renameImageFilename: '',
deleteImageShow: false, deleteImageShow: false,
deleteImageId: '', deleteImageId: '',
deleteImageFilename: '' deleteImageFilename: ''
}, },
methods: { methods: {
open: () => { open: () => {
mdeModalOpenState = true; mdeModalOpenState = true
$('#modal-editor-image').addClass('is-active'); $('#modal-editor-image').addClass('is-active')
vueImage.refreshFolders(); vueImage.refreshFolders()
}, },
cancel: (ev) => { cancel: (ev) => {
mdeModalOpenState = false; mdeModalOpenState = false
$('#modal-editor-image').removeClass('is-active'); $('#modal-editor-image').removeClass('is-active')
}, },
// ------------------------------------------- // -------------------------------------------
// INSERT IMAGE // INSERT IMAGE
// ------------------------------------------- // -------------------------------------------
selectImage: (imageId) => { selectImage: (imageId) => {
vueImage.currentImage = imageId; vueImage.currentImage = imageId
}, },
insertImage: (ev) => { insertImage: (ev) => {
if (mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection')
}
if(mde.codemirror.doc.somethingSelected()) { let selImage = _.find(vueImage.images, ['_id', vueImage.currentImage])
mde.codemirror.execCommand('singleSelection'); selImage.normalizedPath = (selImage.folder === 'f:') ? selImage.filename : selImage.folder.slice(2) + '/' + selImage.filename
} selImage.titleGuess = _.startCase(selImage.basename)
let selImage = _.find(vueImage.images, ['_id', vueImage.currentImage]); let imageText = '![' + selImage.titleGuess + '](/uploads/' + selImage.normalizedPath + ' "' + selImage.titleGuess + '")'
selImage.normalizedPath = (selImage.folder === 'f:') ? selImage.filename : selImage.folder.slice(2) + '/' + selImage.filename; switch (vueImage.currentAlign) {
selImage.titleGuess = _.startCase(selImage.basename); case 'center':
imageText += '{.align-center}'
break
case 'right':
imageText += '{.align-right}'
break
case 'logo':
imageText += '{.pagelogo}'
break
}
let imageText = '![' + selImage.titleGuess + '](/uploads/' + selImage.normalizedPath + ' "' + selImage.titleGuess + '")'; mde.codemirror.doc.replaceSelection(imageText)
switch(vueImage.currentAlign) { vueImage.cancel()
case 'center': },
imageText += '{.align-center}';
break;
case 'right':
imageText += '{.align-right}';
break;
case 'logo':
imageText += '{.pagelogo}';
break;
}
mde.codemirror.doc.replaceSelection(imageText);
vueImage.cancel();
},
// ------------------------------------------- // -------------------------------------------
// NEW FOLDER // NEW FOLDER
// ------------------------------------------- // -------------------------------------------
newFolder: (ev) => { newFolder: (ev) => {
vueImage.newFolderName = ''; vueImage.newFolderName = ''
vueImage.newFolderError = false; vueImage.newFolderError = false
vueImage.newFolderShow = true; vueImage.newFolderShow = true
_.delay(() => { $('#txt-editor-image-newfoldername').focus(); }, 400); _.delay(() => { $('#txt-editor-image-newfoldername').focus() }, 400)
}, },
newFolderDiscard: (ev) => { newFolderDiscard: (ev) => {
vueImage.newFolderShow = false; vueImage.newFolderShow = false
}, },
newFolderCreate: (ev) => { newFolderCreate: (ev) => {
let regFolderName = new RegExp('^[a-z0-9][a-z0-9\-]*[a-z0-9]$')
vueImage.newFolderName = _.kebabCase(_.trim(vueImage.newFolderName))
let regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$"); if (_.isEmpty(vueImage.newFolderName) || !regFolderName.test(vueImage.newFolderName)) {
vueImage.newFolderName = _.kebabCase(_.trim(vueImage.newFolderName)); vueImage.newFolderError = true
return
}
if(_.isEmpty(vueImage.newFolderName) || !regFolderName.test(vueImage.newFolderName)) { vueImage.newFolderDiscard()
vueImage.newFolderError = true; vueImage.isLoadingText = 'Creating new folder...'
return; vueImage.isLoading = true
}
vueImage.newFolderDiscard(); Vue.nextTick(() => {
vueImage.isLoadingText = 'Creating new folder...'; socket.emit('uploadsCreateFolder', { foldername: vueImage.newFolderName }, (data) => {
vueImage.isLoading = true; vueImage.folders = data
vueImage.currentFolder = vueImage.newFolderName
Vue.nextTick(() => { vueImage.images = []
socket.emit('uploadsCreateFolder', { foldername: vueImage.newFolderName }, (data) => { vueImage.isLoading = false
vueImage.folders = data; })
vueImage.currentFolder = vueImage.newFolderName; })
vueImage.images = []; },
vueImage.isLoading = false;
});
});
},
// ------------------------------------------- // -------------------------------------------
// FETCH FROM URL // FETCH FROM URL
// ------------------------------------------- // -------------------------------------------
fetchFromUrl: (ev) => { fetchFromUrl: (ev) => {
vueImage.fetchFromUrlURL = ''; vueImage.fetchFromUrlURL = ''
vueImage.fetchFromUrlShow = true; vueImage.fetchFromUrlShow = true
_.delay(() => { $('#txt-editor-image-fetchurl').focus(); }, 400); _.delay(() => { $('#txt-editor-image-fetchurl').focus() }, 400)
}, },
fetchFromUrlDiscard: (ev) => { fetchFromUrlDiscard: (ev) => {
vueImage.fetchFromUrlShow = false; vueImage.fetchFromUrlShow = false
}, },
fetchFromUrlGo: (ev) => { fetchFromUrlGo: (ev) => {
vueImage.fetchFromUrlDiscard()
vueImage.isLoadingText = 'Fetching image...'
vueImage.isLoading = true
vueImage.fetchFromUrlDiscard(); Vue.nextTick(() => {
vueImage.isLoadingText = 'Fetching image...'; socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => {
vueImage.isLoading = true; if (data.ok) {
vueImage.waitChangeComplete(vueImage.images.length, true)
Vue.nextTick(() => { } else {
socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => { vueImage.isLoading = false
if(data.ok) { alerts.pushError('Upload error', data.msg)
vueImage.waitChangeComplete(vueImage.images.length, true); }
} else { })
vueImage.isLoading = false; })
alerts.pushError('Upload error', data.msg); },
}
});
});
},
// ------------------------------------------- // -------------------------------------------
// RENAME IMAGE // RENAME IMAGE
// ------------------------------------------- // -------------------------------------------
renameImage: () => { renameImage: () => {
let c = _.find(vueImage.images, ['_id', vueImage.renameImageId ])
vueImage.renameImageFilename = c.basename || ''
vueImage.renameImageShow = true
_.delay(() => {
$('#txt-editor-image-rename').focus()
_.defer(() => { $('#txt-editor-image-rename').select() })
}, 400)
},
renameImageDiscard: () => {
vueImage.renameImageShow = false
},
renameImageGo: () => {
vueImage.renameImageDiscard()
vueImage.isLoadingText = 'Renaming image...'
vueImage.isLoading = true
let c = _.find(vueImage.images, ['_id', vueImage.renameImageId ]); Vue.nextTick(() => {
vueImage.renameImageFilename = c.basename || ''; socket.emit('uploadsRenameFile', { uid: vueImage.renameImageId, folder: vueImage.currentFolder, filename: vueImage.renameImageFilename }, (data) => {
vueImage.renameImageShow = true; if (data.ok) {
_.delay(() => { vueImage.waitChangeComplete(vueImage.images.length, false)
$('#txt-editor-image-rename').focus(); } else {
_.defer(() => { $('#txt-editor-image-rename').select(); }); vueImage.isLoading = false
}, 400); alerts.pushError('Rename error', data.msg)
}, }
renameImageDiscard: () => { })
vueImage.renameImageShow = false; })
}, },
renameImageGo: () => {
vueImage.renameImageDiscard();
vueImage.isLoadingText = 'Renaming image...';
vueImage.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsRenameFile', { uid: vueImage.renameImageId, folder: vueImage.currentFolder, filename: vueImage.renameImageFilename }, (data) => {
if(data.ok) {
vueImage.waitChangeComplete(vueImage.images.length, false);
} else {
vueImage.isLoading = false;
alerts.pushError('Rename error', data.msg);
}
});
});
},
// ------------------------------------------- // -------------------------------------------
// MOVE IMAGE // MOVE IMAGE
// ------------------------------------------- // -------------------------------------------
moveImage: (uid, fld) => { moveImage: (uid, fld) => {
vueImage.isLoadingText = 'Moving image...'; vueImage.isLoadingText = 'Moving image...'
vueImage.isLoading = true; vueImage.isLoading = true
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => { socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
if(data.ok) { if (data.ok) {
vueImage.loadImages(); vueImage.loadImages()
} else { } else {
vueImage.isLoading = false; vueImage.isLoading = false
alerts.pushError('Rename error', data.msg); alerts.pushError('Rename error', data.msg)
} }
}); })
}); })
}, },
// ------------------------------------------- // -------------------------------------------
// DELETE IMAGE // DELETE IMAGE
// ------------------------------------------- // -------------------------------------------
deleteImageWarn: (show) => { deleteImageWarn: (show) => {
if(show) { if (show) {
let c = _.find(vueImage.images, ['_id', vueImage.deleteImageId ]); let c = _.find(vueImage.images, ['_id', vueImage.deleteImageId ])
vueImage.deleteImageFilename = c.filename || 'this image'; vueImage.deleteImageFilename = c.filename || 'this image'
} }
vueImage.deleteImageShow = show; vueImage.deleteImageShow = show
}, },
deleteImageGo: () => { deleteImageGo: () => {
vueImage.deleteImageWarn(false); vueImage.deleteImageWarn(false)
vueImage.isLoadingText = 'Deleting image...'; vueImage.isLoadingText = 'Deleting image...'
vueImage.isLoading = true; vueImage.isLoading = true
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsDeleteFile', { uid: vueImage.deleteImageId }, (data) => { socket.emit('uploadsDeleteFile', { uid: vueImage.deleteImageId }, (data) => {
vueImage.loadImages(); vueImage.loadImages()
}); })
}); })
}, },
// ------------------------------------------- // -------------------------------------------
// LOAD FROM REMOTE // LOAD FROM REMOTE
// ------------------------------------------- // -------------------------------------------
selectFolder: (fldName) => { selectFolder: (fldName) => {
vueImage.currentFolder = fldName; vueImage.currentFolder = fldName
vueImage.loadImages(); vueImage.loadImages()
}, },
refreshFolders: () => { refreshFolders: () => {
vueImage.isLoadingText = 'Fetching folders list...'; vueImage.isLoadingText = 'Fetching folders list...'
vueImage.isLoading = true; vueImage.isLoading = true
vueImage.currentFolder = ''; vueImage.currentFolder = ''
vueImage.currentImage = ''; vueImage.currentImage = ''
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsGetFolders', { }, (data) => { socket.emit('uploadsGetFolders', { }, (data) => {
vueImage.folders = data; vueImage.folders = data
vueImage.loadImages(); vueImage.loadImages()
}); })
}); })
}, },
loadImages: (silent) => { loadImages: (silent) => {
if(!silent) { if (!silent) {
vueImage.isLoadingText = 'Fetching images...'; vueImage.isLoadingText = 'Fetching images...'
vueImage.isLoading = true; vueImage.isLoading = true
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsGetImages', { folder: vueImage.currentFolder }, (data) => { socket.emit('uploadsGetImages', { folder: vueImage.currentFolder }, (data) => {
vueImage.images = data; vueImage.images = data
if(!silent) { if (!silent) {
vueImage.isLoading = false; vueImage.isLoading = false
} }
vueImage.attachContextMenus(); vueImage.attachContextMenus()
resolve(true); resolve(true)
}); })
}); })
}); })
}, },
waitChangeComplete: (oldAmount, expectChange) => { waitChangeComplete: (oldAmount, expectChange) => {
expectChange = (_.isBoolean(expectChange)) ? expectChange : true
expectChange = (_.isBoolean(expectChange)) ? expectChange : true; vueImage.postUploadChecks++
vueImage.isLoadingText = 'Processing...'
vueImage.postUploadChecks++; Vue.nextTick(() => {
vueImage.isLoadingText = 'Processing...'; vueImage.loadImages(true).then(() => {
if ((vueImage.images.length !== oldAmount) === expectChange) {
Vue.nextTick(() => { vueImage.postUploadChecks = 0
vueImage.loadImages(true).then(() => { vueImage.isLoading = false
if((vueImage.images.length !== oldAmount) === expectChange) { } else if (vueImage.postUploadChecks > 5) {
vueImage.postUploadChecks = 0; vueImage.postUploadChecks = 0
vueImage.isLoading = false; vueImage.isLoading = false
} else if(vueImage.postUploadChecks > 5) { alerts.pushError('Unable to fetch updated listing', 'Try again later')
vueImage.postUploadChecks = 0; } else {
vueImage.isLoading = false; _.delay(() => {
alerts.pushError('Unable to fetch updated listing', 'Try again later'); vueImage.waitChangeComplete(oldAmount, expectChange)
} else { }, 1500)
_.delay(() => { }
vueImage.waitChangeComplete(oldAmount, expectChange); })
}, 1500); })
} },
});
});
},
// ------------------------------------------- // -------------------------------------------
// IMAGE CONTEXT MENU // IMAGE CONTEXT MENU
// ------------------------------------------- // -------------------------------------------
attachContextMenus: () => { attachContextMenus: () => {
let moveFolders = _.map(vueImage.folders, (f) => {
return {
name: (f !== '') ? f : '/ (root)',
icon: 'fa-folder',
callback: (key, opt) => {
let moveImageId = _.toString($(opt.$trigger).data('uid'))
let moveImageDestFolder = _.nth(vueImage.folders, key)
vueImage.moveImage(moveImageId, moveImageDestFolder)
}
}
})
let moveFolders = _.map(vueImage.folders, (f) => { $.contextMenu('destroy', '.editor-modal-image-choices > figure')
return { $.contextMenu({
name: (f !== '') ? f : '/ (root)', selector: '.editor-modal-image-choices > figure',
icon: 'fa-folder', appendTo: '.editor-modal-image-choices',
callback: (key, opt) => { position: (opt, x, y) => {
let moveImageId = _.toString($(opt.$trigger).data('uid')); $(opt.$trigger).addClass('is-contextopen')
let moveImageDestFolder = _.nth(vueImage.folders, key); let trigPos = $(opt.$trigger).position()
vueImage.moveImage(moveImageId, moveImageDestFolder); let trigDim = { w: $(opt.$trigger).width() / 2, h: $(opt.$trigger).height() / 2 }
} opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w })
}; },
}); events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen')
}
},
items: {
rename: {
name: 'Rename',
icon: 'fa-edit',
callback: (key, opt) => {
vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid)
vueImage.renameImage()
}
},
move: {
name: 'Move to...',
icon: 'fa-folder-open-o',
items: moveFolders
},
delete: {
name: 'Delete',
icon: 'fa-trash',
callback: (key, opt) => {
vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid)
vueImage.deleteImageWarn(true)
}
}
}
})
}
$.contextMenu('destroy', '.editor-modal-image-choices > figure'); }
$.contextMenu({ })
selector: '.editor-modal-image-choices > figure',
appendTo: '.editor-modal-image-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen');
let trigPos = $(opt.$trigger).position();
let trigDim = { w: $(opt.$trigger).width() / 2, h: $(opt.$trigger).height() / 2 };
opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w });
},
events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen');
}
},
items: {
rename: {
name: "Rename",
icon: "fa-edit",
callback: (key, opt) => {
vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid);
vueImage.renameImage();
}
},
move: {
name: "Move to...",
icon: "fa-folder-open-o",
items: moveFolders
},
delete: {
name: "Delete",
icon: "fa-trash",
callback: (key, opt) => {
vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid);
vueImage.deleteImageWarn(true);
}
}
}
});
}
}
});
$('#btn-editor-image-upload input').on('change', (ev) => { $('#btn-editor-image-upload input').on('change', (ev) => {
let curImageAmount = vueImage.images.length
let curImageAmount = vueImage.images.length; $(ev.currentTarget).simpleUpload('/uploads/img', {
$(ev.currentTarget).simpleUpload("/uploads/img", { name: 'imgfile',
data: {
folder: vueImage.currentFolder
},
limit: 20,
expect: 'json',
allowedExts: ['jpg', 'jpeg', 'gif', 'png', 'webp'],
allowedTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
maxFileSize: 3145728, // max 3 MB
name: 'imgfile', init: (totalUploads) => {
data: { vueImage.uploadSucceeded = false
folder: vueImage.currentFolder vueImage.isLoadingText = 'Preparing to upload...'
}, vueImage.isLoading = true
limit: 20, },
expect: 'json',
allowedExts: ["jpg", "jpeg", "gif", "png", "webp"],
allowedTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
maxFileSize: 3145728, // max 3 MB
init: (totalUploads) => { progress: (progress) => {
vueImage.uploadSucceeded = false; vueImage.isLoadingText = 'Uploading...' + Math.round(progress) + '%'
vueImage.isLoadingText = 'Preparing to upload...'; },
vueImage.isLoading = true;
},
progress: (progress) => { success: (data) => {
vueImage.isLoadingText = 'Uploading...' + Math.round(progress) + '%'; if (data.ok) {
}, let failedUpls = _.filter(data.results, ['ok', false])
if (failedUpls.length) {
_.forEach(failedUpls, (u) => {
alerts.pushError('Upload error', u.msg)
})
if (failedUpls.length < data.results.length) {
alerts.push({
title: 'Some uploads succeeded',
message: 'Files that are not mentionned in the errors above were uploaded successfully.'
})
vueImage.uploadSucceeded = true
}
} else {
vueImage.uploadSucceeded = true
}
} else {
alerts.pushError('Upload error', data.msg)
}
},
success: (data) => { error: (error) => {
if(data.ok) { alerts.pushError(error.message, this.upload.file.name)
},
let failedUpls = _.filter(data.results, ['ok', false]); finish: () => {
if(failedUpls.length) { if (vueImage.uploadSucceeded) {
_.forEach(failedUpls, (u) => { vueImage.waitChangeComplete(curImageAmount, true)
alerts.pushError('Upload error', u.msg); } else {
}); vueImage.isLoading = false
if(failedUpls.length < data.results.length) { }
alerts.push({ }
title: 'Some uploads succeeded',
message: 'Files that are not mentionned in the errors above were uploaded successfully.'
});
vueImage.uploadSucceeded = true;
}
} else {
vueImage.uploadSucceeded = true;
}
} else { })
alerts.pushError('Upload error', data.msg); })
}
},
error: (error) => {
alerts.pushError(error.message, this.upload.file.name);
},
finish: () => {
if(vueImage.uploadSucceeded) {
vueImage.waitChangeComplete(curImageAmount, true);
} else {
vueImage.isLoading = false;
}
}
});
});
+34 -36
View File
@@ -1,49 +1,47 @@
const videoRules = { const videoRules = {
'youtube': new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/, 'i'), 'youtube': new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/, 'i'),
'vimeo': new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'), 'vimeo': new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'),
'dailymotion': new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i') 'dailymotion': new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i')
}; }
// Vue Video instance // Vue Video instance
let vueVideo = new Vue({ let vueVideo = new Vue({
el: '#modal-editor-video', el: '#modal-editor-video',
data: { data: {
link: '' link: ''
}, },
methods: { methods: {
open: (ev) => { open: (ev) => {
$('#modal-editor-video').addClass('is-active'); $('#modal-editor-video').addClass('is-active')
$('#modal-editor-video input').focus(); $('#modal-editor-video input').focus()
}, },
cancel: (ev) => { cancel: (ev) => {
mdeModalOpenState = false; mdeModalOpenState = false
$('#modal-editor-video').removeClass('is-active'); $('#modal-editor-video').removeClass('is-active')
vueVideo.link = ''; vueVideo.link = ''
}, },
insertVideo: (ev) => { insertVideo: (ev) => {
if (mde.codemirror.doc.somethingSelected()) {
if(mde.codemirror.doc.somethingSelected()) { mde.codemirror.execCommand('singleSelection')
mde.codemirror.execCommand('singleSelection'); }
}
// Guess video type // Guess video type
let videoType = _.findKey(videoRules, (vr) => { let videoType = _.findKey(videoRules, (vr) => {
return vr.test(vueVideo.link); return vr.test(vueVideo.link)
}); })
if(_.isNil(videoType)) { if (_.isNil(videoType)) {
videoType = 'video'; videoType = 'video'
} }
// Insert video tag // Insert video tag
let videoText = '[video](' + vueVideo.link + '){.' + videoType + '}\n'; let videoText = '[video](' + vueVideo.link + '){.' + videoType + '}\n'
mde.codemirror.doc.replaceSelection(videoText); mde.codemirror.doc.replaceSelection(videoText)
vueVideo.cancel(); vueVideo.cancel()
}
} }
} })
});
+195 -200
View File
@@ -3,215 +3,210 @@
// Markdown Editor // Markdown Editor
// ==================================== // ====================================
if($('#mk-editor').length === 1) { if ($('#mk-editor').length === 1) {
let mdeModalOpenState = false
let mdeCurrentEditor = null
let mdeModalOpenState = false; Vue.filter('filesize', (v) => {
let mdeCurrentEditor = null; return _.toUpper(filesize(v))
})
Vue.filter('filesize', (v) => { // =include editor-image.js
return _.toUpper(filesize(v)); // =include editor-file.js
}); // =include editor-video.js
// =include editor-codeblock.js
//=include editor-image.js var mde = new SimpleMDE({
//=include editor-file.js autofocus: true,
//=include editor-video.js autoDownloadFontAwesome: false,
//=include editor-codeblock.js element: $('#mk-editor').get(0),
placeholder: 'Enter Markdown formatted content here...',
var mde = new SimpleMDE({ spellChecker: false,
autofocus: true, status: false,
autoDownloadFontAwesome: false, toolbar: [{
element: $("#mk-editor").get(0), name: 'bold',
placeholder: 'Enter Markdown formatted content here...', action: SimpleMDE.toggleBold,
spellChecker: false, className: 'icon-bold',
status: false, title: 'Bold'
toolbar: [{ },
name: "bold", {
action: SimpleMDE.toggleBold, name: 'italic',
className: "icon-bold", action: SimpleMDE.toggleItalic,
title: "Bold", className: 'icon-italic',
}, title: 'Italic'
{ },
name: "italic", {
action: SimpleMDE.toggleItalic, name: 'strikethrough',
className: "icon-italic", action: SimpleMDE.toggleStrikethrough,
title: "Italic", className: 'icon-strikethrough',
}, title: 'Strikethrough'
{ },
name: "strikethrough", '|',
action: SimpleMDE.toggleStrikethrough, {
className: "icon-strikethrough", name: 'heading-1',
title: "Strikethrough", action: SimpleMDE.toggleHeading1,
}, className: 'icon-header fa-header-x fa-header-1',
'|', title: 'Big Heading'
{ },
name: "heading-1", {
action: SimpleMDE.toggleHeading1, name: 'heading-2',
className: "icon-header fa-header-x fa-header-1", action: SimpleMDE.toggleHeading2,
title: "Big Heading", className: 'icon-header fa-header-x fa-header-2',
}, title: 'Medium Heading'
{ },
name: "heading-2", {
action: SimpleMDE.toggleHeading2, name: 'heading-3',
className: "icon-header fa-header-x fa-header-2", action: SimpleMDE.toggleHeading3,
title: "Medium Heading", className: 'icon-header fa-header-x fa-header-3',
}, title: 'Small Heading'
{ },
name: "heading-3", {
action: SimpleMDE.toggleHeading3, name: 'quote',
className: "icon-header fa-header-x fa-header-3", action: SimpleMDE.toggleBlockquote,
title: "Small Heading", className: 'icon-quote-left',
}, title: 'Quote'
{ },
name: "quote", '|',
action: SimpleMDE.toggleBlockquote, {
className: "icon-quote-left", name: 'unordered-list',
title: "Quote", action: SimpleMDE.toggleUnorderedList,
}, className: 'icon-th-list',
'|', title: 'Bullet List'
{ },
name: "unordered-list", {
action: SimpleMDE.toggleUnorderedList, name: 'ordered-list',
className: "icon-th-list", action: SimpleMDE.toggleOrderedList,
title: "Bullet List", className: 'icon-list-ol',
}, title: 'Numbered List'
{ },
name: "ordered-list", '|',
action: SimpleMDE.toggleOrderedList, {
className: "icon-list-ol", name: 'link',
title: "Numbered List", action: (editor) => {
}, /* if(!mdeModalOpenState) {
'|',
{
name: "link",
action: (editor) => {
/*if(!mdeModalOpenState) {
mdeModalOpenState = true; mdeModalOpenState = true;
$('#modal-editor-link').slideToggle(); $('#modal-editor-link').slideToggle();
}*/ } */
}, },
className: "icon-link2", className: 'icon-link2',
title: "Insert Link", title: 'Insert Link'
}, },
{ {
name: "image", name: 'image',
action: (editor) => { action: (editor) => {
if(!mdeModalOpenState) { if (!mdeModalOpenState) {
vueImage.open(); vueImage.open()
} }
}, },
className: "icon-image", className: 'icon-image',
title: "Insert Image", title: 'Insert Image'
}, },
{ {
name: "file", name: 'file',
action: (editor) => { action: (editor) => {
if(!mdeModalOpenState) { if (!mdeModalOpenState) {
vueFile.open(); vueFile.open()
} }
}, },
className: "icon-paper", className: 'icon-paper',
title: "Insert File", title: 'Insert File'
}, },
{ {
name: "video", name: 'video',
action: (editor) => { action: (editor) => {
if(!mdeModalOpenState) { if (!mdeModalOpenState) {
vueVideo.open(); vueVideo.open()
} }
}, },
className: "icon-video-camera2", className: 'icon-video-camera2',
title: "Insert Video Player", title: 'Insert Video Player'
}, },
'|', '|',
{ {
name: "inline-code", name: 'inline-code',
action: (editor) => { action: (editor) => {
if (!editor.codemirror.doc.somethingSelected()) {
return alerts.pushError('Invalid selection', 'You must select at least 1 character first.')
}
let curSel = editor.codemirror.doc.getSelections()
curSel = _.map(curSel, (s) => {
return '`' + s + '`'
})
editor.codemirror.doc.replaceSelections(curSel)
},
className: 'icon-terminal',
title: 'Inline Code'
},
{
name: 'code-block',
action: (editor) => {
if (!mdeModalOpenState) {
mdeModalOpenState = true
if(!editor.codemirror.doc.somethingSelected()) { if (mde.codemirror.doc.somethingSelected()) {
return alerts.pushError('Invalid selection','You must select at least 1 character first.'); vueCodeBlock.initContent = mde.codemirror.doc.getSelection()
} }
let curSel = editor.codemirror.doc.getSelections();
curSel = _.map(curSel, (s) => {
return '`' + s + '`';
});
editor.codemirror.doc.replaceSelections(curSel);
}, vueCodeBlock.open()
className: "icon-terminal", }
title: "Inline Code", },
}, className: 'icon-code',
{ title: 'Code Block'
name: "code-block", },
action: (editor) => { '|',
if(!mdeModalOpenState) { {
mdeModalOpenState = true; name: 'table',
action: (editor) => {
// todo
},
className: 'icon-table',
title: 'Insert Table'
},
{
name: 'horizontal-rule',
action: SimpleMDE.drawHorizontalRule,
className: 'icon-minus2',
title: 'Horizontal Rule'
}
],
shortcuts: {
'toggleBlockquote': null,
'toggleFullScreen': null
}
})
if(mde.codemirror.doc.somethingSelected()) { // -> Save
vueCodeBlock.initContent = mde.codemirror.doc.getSelection();
}
vueCodeBlock.open(); let saveCurrentDocument = (ev) => {
$.ajax(window.location.href, {
data: {
markdown: mde.value()
},
dataType: 'json',
method: 'PUT'
}).then((rData, rStatus, rXHR) => {
if (rData.ok) {
window.location.assign('/' + pageEntryPath)
} else {
alerts.pushError('Something went wrong', rData.error)
}
}, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.')
})
}
} $('.btn-edit-save, .btn-create-save').on('click', (ev) => {
}, saveCurrentDocument(ev)
className: "icon-code", })
title: "Code Block",
},
'|',
{
name: "table",
action: (editor) => {
//todo
},
className: "icon-table",
title: "Insert Table",
},
{
name: "horizontal-rule",
action: SimpleMDE.drawHorizontalRule,
className: "icon-minus2",
title: "Horizontal Rule",
}
],
shortcuts: {
"toggleBlockquote": null,
"toggleFullScreen": null
}
});
//-> Save
let saveCurrentDocument = (ev) => {
$.ajax(window.location.href, {
data: {
markdown: mde.value()
},
dataType: 'json',
method: 'PUT'
}).then((rData, rStatus, rXHR) => {
if(rData.ok) {
window.location.assign('/' + pageEntryPath);
} else {
alerts.pushError('Something went wrong', rData.error);
}
}, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.');
});
};
$('.btn-edit-save, .btn-create-save').on('click', (ev) => {
saveCurrentDocument(ev);
});
$(window).bind('keydown', (ev) => {
if (ev.ctrlKey || ev.metaKey) {
switch (String.fromCharCode(ev.which).toLowerCase()) {
case 's':
ev.preventDefault();
saveCurrentDocument(ev);
break;
}
}
});
$(window).bind('keydown', (ev) => {
if (ev.ctrlKey || ev.metaKey) {
switch (String.fromCharCode(ev.which).toLowerCase()) {
case 's':
ev.preventDefault()
saveCurrentDocument(ev)
break
}
}
})
} }
+74 -77
View File
@@ -1,84 +1,81 @@
"use strict"; 'use strict'
if($('#search-input').length) { if ($('#search-input').length) {
$('#search-input').focus()
$('#search-input').focus(); $('.searchresults').css('display', 'block')
$('.searchresults').css('display', 'block'); var vueHeader = new Vue({
el: '#header-container',
var vueHeader = new Vue({ data: {
el: '#header-container', searchq: '',
data: { searchres: [],
searchq: '', searchsuggest: [],
searchres: [], searchload: 0,
searchsuggest: [], searchactive: false,
searchload: 0, searchmoveidx: 0,
searchactive: false, searchmovekey: '',
searchmoveidx: 0, searchmovearr: []
searchmovekey: '', },
searchmovearr: [] watch: {
}, searchq: (val, oldVal) => {
watch: { vueHeader.searchmoveidx = 0
searchq: (val, oldVal) => { if (val.length >= 3) {
vueHeader.searchmoveidx = 0; vueHeader.searchactive = true
if(val.length >= 3) { vueHeader.searchload++
vueHeader.searchactive = true; socket.emit('search', { terms: val }, (data) => {
vueHeader.searchload++; vueHeader.searchres = data.match
socket.emit('search', { terms: val }, (data) => { vueHeader.searchsuggest = data.suggest
vueHeader.searchres = data.match; vueHeader.searchmovearr = _.concat([], vueHeader.searchres, vueHeader.searchsuggest)
vueHeader.searchsuggest = data.suggest; if (vueHeader.searchload > 0) { vueHeader.searchload-- }
vueHeader.searchmovearr = _.concat([], vueHeader.searchres, vueHeader.searchsuggest); })
if(vueHeader.searchload > 0) { vueHeader.searchload--; } } else {
}); vueHeader.searchactive = false
} else { vueHeader.searchres = []
vueHeader.searchactive = false; vueHeader.searchsuggest = []
vueHeader.searchres = []; vueHeader.searchmovearr = []
vueHeader.searchsuggest = []; vueHeader.searchload = 0
vueHeader.searchmovearr = []; }
vueHeader.searchload = 0; },
} searchmoveidx: (val, oldVal) => {
}, if (val > 0) {
searchmoveidx: (val, oldVal) => { vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1]) ?
if(val > 0) {
vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1]) ?
'res.' + vueHeader.searchmovearr[val - 1]._id : 'res.' + vueHeader.searchmovearr[val - 1]._id :
'sug.' + vueHeader.searchmovearr[val - 1]; 'sug.' + vueHeader.searchmovearr[val - 1]
} else { } else {
vueHeader.searchmovekey = ''; vueHeader.searchmovekey = ''
} }
} }
}, },
methods: { methods: {
useSuggestion: (sug) => { useSuggestion: (sug) => {
vueHeader.searchq = sug; vueHeader.searchq = sug
}, },
closeSearch: () => { closeSearch: () => {
vueHeader.searchq = ''; vueHeader.searchq = ''
}, },
moveSelectSearch: () => { moveSelectSearch: () => {
if(vueHeader.searchmoveidx < 1) { return; } if (vueHeader.searchmoveidx < 1) { return }
let i = vueHeader.searchmoveidx - 1; let i = vueHeader.searchmoveidx - 1
if(vueHeader.searchmovearr[i]) { if (vueHeader.searchmovearr[i]) {
window.location.assign('/' + vueHeader.searchmovearr[i]._id); window.location.assign('/' + vueHeader.searchmovearr[i]._id)
} else { } else {
vueHeader.searchq = vueHeader.searchmovearr[i]; vueHeader.searchq = vueHeader.searchmovearr[i]
} }
},
}, moveDownSearch: () => {
moveDownSearch: () => { if (vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
if(vueHeader.searchmoveidx < vueHeader.searchmovearr.length) { vueHeader.searchmoveidx++
vueHeader.searchmoveidx++; }
} },
}, moveUpSearch: () => {
moveUpSearch: () => { if (vueHeader.searchmoveidx > 0) {
if(vueHeader.searchmoveidx > 0) { vueHeader.searchmoveidx--
vueHeader.searchmoveidx--; }
} }
} }
} })
});
$('main').on('click', vueHeader.closeSearch);
$('main').on('click', vueHeader.closeSearch)
} }
+13 -13
View File
@@ -1,16 +1,16 @@
function setInputSelection(input, startPos, endPos) { function setInputSelection (input, startPos, endPos) {
input.focus(); input.focus()
if (typeof input.selectionStart != "undefined") { if (typeof input.selectionStart !== 'undefined') {
input.selectionStart = startPos; input.selectionStart = startPos
input.selectionEnd = endPos; input.selectionEnd = endPos
} else if (document.selection && document.selection.createRange) { } else if (document.selection && document.selection.createRange) {
// IE branch // IE branch
input.select(); input.select()
var range = document.selection.createRange(); var range = document.selection.createRange()
range.collapse(true); range.collapse(true)
range.moveEnd("character", endPos); range.moveEnd('character', endPos)
range.moveStart("character", startPos); range.moveStart('character', startPos)
range.select(); range.select()
} }
} }
+6 -8
View File
@@ -1,11 +1,9 @@
function makeSafePath(rawPath) { function makeSafePath (rawPath) {
let rawParts = _.split(_.trim(rawPath), '/')
let rawParts = _.split(_.trim(rawPath), '/'); rawParts = _.map(rawParts, (r) => {
rawParts = _.map(rawParts, (r) => { return _.kebabCase(_.deburr(_.trim(r)))
return _.kebabCase(_.deburr(_.trim(r))); })
});
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r); }), '/');
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/')
} }
+4 -6
View File
@@ -1,7 +1,5 @@
"use strict"; 'use strict'
jQuery( document ).ready(function( $ ) { jQuery(document).ready(function ($) {
$('#login-user').focus()
$('#login-user').focus(); })
});
+23 -25
View File
@@ -2,29 +2,27 @@
// Vue Create User instance // Vue Create User instance
let vueCreateUser = new Vue({ let vueCreateUser = new Vue({
el: '#modal-admin-users-create', el: '#modal-admin-users-create',
data: { data: {
email: '', email: '',
provider: 'local', provider: 'local',
password: '', password: '',
name: '' name: ''
}, },
methods: { methods: {
open: (ev) => { open: (ev) => {
$('#modal-admin-users-create').addClass('is-active'); $('#modal-admin-users-create').addClass('is-active')
$('#modal-admin-users-create input').first().focus(); $('#modal-admin-users-create input').first().focus()
}, },
cancel: (ev) => { cancel: (ev) => {
$('#modal-admin-users-create').removeClass('is-active'); $('#modal-admin-users-create').removeClass('is-active')
vueCreateUser.email = ''; vueCreateUser.email = ''
vueCreateUser.provider = 'local'; vueCreateUser.provider = 'local'
}, },
create: (ev) => { create: (ev) => {
vueCreateUser.cancel()
}
}
})
vueCreateUser.cancel(); $('.btn-create-prompt').on('click', vueCreateUser.open)
}
}
});
$('.btn-create-prompt').on('click', vueCreateUser.open);
+16 -16
View File
@@ -2,21 +2,21 @@
// Vue Delete User instance // Vue Delete User instance
let vueDeleteUser = new Vue({ let vueDeleteUser = new Vue({
el: '#modal-admin-users-delete', el: '#modal-admin-users-delete',
data: { data: {
}, },
methods: { methods: {
open: (ev) => { open: (ev) => {
$('#modal-admin-users-delete').addClass('is-active'); $('#modal-admin-users-delete').addClass('is-active')
}, },
cancel: (ev) => { cancel: (ev) => {
$('#modal-admin-users-delete').removeClass('is-active'); $('#modal-admin-users-delete').removeClass('is-active')
}, },
deleteUser: (ev) => { deleteUser: (ev) => {
vueDeleteUser.cancel(); vueDeleteUser.cancel()
} }
} }
}); })
$('.btn-deluser-prompt').on('click', vueDeleteUser.open); $('.btn-deluser-prompt').on('click', vueDeleteUser.open)
+19 -21
View File
@@ -1,29 +1,27 @@
//-> Create New Document // -> Create New Document
let suggestedCreatePath = currentBasePath + '/new-page'; let suggestedCreatePath = currentBasePath + '/new-page'
$('.btn-create-prompt').on('click', (ev) => { $('.btn-create-prompt').on('click', (ev) => {
$('#txt-create-prompt').val(suggestedCreatePath); $('#txt-create-prompt').val(suggestedCreatePath)
$('#modal-create-prompt').toggleClass('is-active'); $('#modal-create-prompt').toggleClass('is-active')
setInputSelection($('#txt-create-prompt').get(0), currentBasePath.length + 1, suggestedCreatePath.length); setInputSelection($('#txt-create-prompt').get(0), currentBasePath.length + 1, suggestedCreatePath.length)
$('#txt-create-prompt').removeClass('is-danger').next().addClass('is-hidden'); $('#txt-create-prompt').removeClass('is-danger').next().addClass('is-hidden')
}); })
$('#txt-create-prompt').on('keypress', (ev) => { $('#txt-create-prompt').on('keypress', (ev) => {
if(ev.which === 13) { if (ev.which === 13) {
$('.btn-create-go').trigger('click'); $('.btn-create-go').trigger('click')
} }
}); })
$('.btn-create-go').on('click', (ev) => { $('.btn-create-go').on('click', (ev) => {
let newDocPath = makeSafePath($('#txt-create-prompt').val())
let newDocPath = makeSafePath($('#txt-create-prompt').val()); if (_.isEmpty(newDocPath)) {
if(_.isEmpty(newDocPath)) { $('#txt-create-prompt').addClass('is-danger').next().removeClass('is-hidden')
$('#txt-create-prompt').addClass('is-danger').next().removeClass('is-hidden'); } else {
} else { $('#txt-create-prompt').parent().addClass('is-loading')
$('#txt-create-prompt').parent().addClass('is-loading'); window.location.assign('/create/' + newDocPath)
window.location.assign('/create/' + newDocPath); }
} })
});
+35 -38
View File
@@ -1,49 +1,46 @@
//-> Move Existing Document // -> Move Existing Document
if(currentBasePath !== '') { if (currentBasePath !== '') {
$('.btn-move-prompt').removeClass('is-hidden'); $('.btn-move-prompt').removeClass('is-hidden')
} }
let moveInitialDocument = _.lastIndexOf(currentBasePath, '/') + 1; let moveInitialDocument = _.lastIndexOf(currentBasePath, '/') + 1
$('.btn-move-prompt').on('click', (ev) => { $('.btn-move-prompt').on('click', (ev) => {
$('#txt-move-prompt').val(currentBasePath); $('#txt-move-prompt').val(currentBasePath)
$('#modal-move-prompt').toggleClass('is-active'); $('#modal-move-prompt').toggleClass('is-active')
setInputSelection($('#txt-move-prompt').get(0), moveInitialDocument, currentBasePath.length); setInputSelection($('#txt-move-prompt').get(0), moveInitialDocument, currentBasePath.length)
$('#txt-move-prompt').removeClass('is-danger').next().addClass('is-hidden'); $('#txt-move-prompt').removeClass('is-danger').next().addClass('is-hidden')
}); })
$('#txt-move-prompt').on('keypress', (ev) => { $('#txt-move-prompt').on('keypress', (ev) => {
if(ev.which === 13) { if (ev.which === 13) {
$('.btn-move-go').trigger('click'); $('.btn-move-go').trigger('click')
} }
}); })
$('.btn-move-go').on('click', (ev) => { $('.btn-move-go').on('click', (ev) => {
let newDocPath = makeSafePath($('#txt-move-prompt').val())
if (_.isEmpty(newDocPath) || newDocPath === currentBasePath || newDocPath === 'home') {
$('#txt-move-prompt').addClass('is-danger').next().removeClass('is-hidden')
} else {
$('#txt-move-prompt').parent().addClass('is-loading')
let newDocPath = makeSafePath($('#txt-move-prompt').val()); $.ajax(window.location.href, {
if(_.isEmpty(newDocPath) || newDocPath === currentBasePath || newDocPath === 'home') { data: {
$('#txt-move-prompt').addClass('is-danger').next().removeClass('is-hidden'); move: newDocPath
} else { },
$('#txt-move-prompt').parent().addClass('is-loading'); dataType: 'json',
method: 'PUT'
$.ajax(window.location.href, { }).then((rData, rStatus, rXHR) => {
data: { if (rData.ok) {
move: newDocPath window.location.assign('/' + newDocPath)
}, } else {
dataType: 'json', alerts.pushError('Something went wrong', rData.error)
method: 'PUT' }
}).then((rData, rStatus, rXHR) => { }, (rXHR, rStatus, err) => {
if(rData.ok) { alerts.pushError('Something went wrong', 'Save operation failed.')
window.location.assign('/' + newDocPath); })
} else { }
alerts.pushError('Something went wrong', rData.error); })
}
}, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.');
});
}
});
+90 -96
View File
@@ -1,102 +1,96 @@
if($('#page-type-admin-profile').length) { if ($('#page-type-admin-profile').length) {
let vueProfile = new Vue({
el: '#page-type-admin-profile',
data: {
password: '********',
passwordVerify: '********',
name: ''
},
methods: {
saveUser: (ev) => {
if (vueProfile.password !== vueProfile.passwordVerify) {
alerts.pushError('Error', "Passwords don't match!")
return
}
$.post(window.location.href, {
password: vueProfile.password,
name: vueProfile.name
}).done((resp) => {
alerts.pushSuccess('Saved successfully', 'Changes have been applied.')
}).fail((jqXHR, txtStatus, resp) => {
alerts.pushError('Error', resp)
})
}
},
created: function () {
this.name = usrDataName
}
})
} else if ($('#page-type-admin-users').length) {
let vueProfile = new Vue({ // =include ../modals/admin-users-create.js
el: '#page-type-admin-profile',
data: {
password: '********',
passwordVerify: '********',
name: ''
},
methods: {
saveUser: (ev) => {
if(vueProfile.password !== vueProfile.passwordVerify) {
alerts.pushError('Error', "Passwords don't match!");
return;
}
$.post(window.location.href, {
password: vueProfile.password,
name: vueProfile.name
}).done((resp) => {
alerts.pushSuccess('Saved successfully', 'Changes have been applied.');
}).fail((jqXHR, txtStatus, resp) => {
alerts.pushError('Error', resp);
})
}
},
created: function() {
this.name = usrDataName;
}
});
} else if($('#page-type-admin-users').length) { } else if ($('#page-type-admin-users-edit').length) {
let vueEditUser = new Vue({
el: '#page-type-admin-users-edit',
data: {
id: '',
email: '',
password: '********',
name: '',
rights: [],
roleoverride: 'none'
},
methods: {
addRightsRow: (ev) => {
vueEditUser.rights.push({
role: 'write',
path: '/',
exact: false,
deny: false
})
},
removeRightsRow: (idx) => {
_.pullAt(vueEditUser.rights, idx)
vueEditUser.$forceUpdate()
},
saveUser: (ev) => {
let formattedRights = _.cloneDeep(vueEditUser.rights)
switch (vueEditUser.roleoverride) {
case 'admin':
formattedRights.push({
role: 'admin',
path: '/',
exact: false,
deny: false
})
break
}
$.post(window.location.href, {
password: vueEditUser.password,
name: vueEditUser.name,
rights: JSON.stringify(formattedRights)
}).done((resp) => {
alerts.pushSuccess('Saved successfully', 'Changes have been applied.')
}).fail((jqXHR, txtStatus, resp) => {
alerts.pushError('Error', resp)
})
}
},
created: function () {
this.id = usrData._id
this.email = usrData.email
this.name = usrData.name
//=include ../modals/admin-users-create.js if (_.find(usrData.rights, { role: 'admin' })) {
this.rights = _.reject(usrData.rights, ['role', 'admin'])
} else if($('#page-type-admin-users-edit').length) { this.roleoverride = 'admin'
} else {
let vueEditUser = new Vue({ this.rights = usrData.rights
el: '#page-type-admin-users-edit', }
data: { }
id: '', })
email: '',
password: '********',
name: '',
rights: [],
roleoverride: 'none'
},
methods: {
addRightsRow: (ev) => {
vueEditUser.rights.push({
role: 'write',
path: '/',
exact: false,
deny: false
});
},
removeRightsRow: (idx) => {
_.pullAt(vueEditUser.rights, idx)
vueEditUser.$forceUpdate()
},
saveUser: (ev) => {
let formattedRights = _.cloneDeep(vueEditUser.rights)
switch(vueEditUser.roleoverride) {
case 'admin':
formattedRights.push({
role: 'admin',
path: '/',
exact: false,
deny: false
})
break;
}
$.post(window.location.href, {
password: vueEditUser.password,
name: vueEditUser.name,
rights: JSON.stringify(formattedRights)
}).done((resp) => {
alerts.pushSuccess('Saved successfully', 'Changes have been applied.');
}).fail((jqXHR, txtStatus, resp) => {
alerts.pushError('Error', resp);
})
}
},
created: function() {
this.id = usrData._id;
this.email = usrData.email;
this.name = usrData.name;
if(_.find(usrData.rights, { role: 'admin' })) {
this.rights = _.reject(usrData.rights, ['role', 'admin']);
this.roleoverride = 'admin';
} else {
this.rights = usrData.rights;
}
}
});
//=include ../modals/admin-users-delete.js
// =include ../modals/admin-users-delete.js
} }
+7 -9
View File
@@ -1,14 +1,12 @@
if($('#page-type-create').length) { if ($('#page-type-create').length) {
let pageEntryPath = $('#page-type-create').data('entrypath')
let pageEntryPath = $('#page-type-create').data('entrypath'); // -> Discard
//-> Discard $('.btn-create-discard').on('click', (ev) => {
$('#modal-create-discard').toggleClass('is-active')
$('.btn-create-discard').on('click', (ev) => { })
$('#modal-create-discard').toggleClass('is-active');
});
//=include ../components/editor.js
// =include ../components/editor.js
} }
+7 -9
View File
@@ -1,14 +1,12 @@
if($('#page-type-edit').length) { if ($('#page-type-edit').length) {
let pageEntryPath = $('#page-type-edit').data('entrypath')
let pageEntryPath = $('#page-type-edit').data('entrypath'); // -> Discard
//-> Discard $('.btn-edit-discard').on('click', (ev) => {
$('#modal-edit-discard').toggleClass('is-active')
$('.btn-edit-discard').on('click', (ev) => { })
$('#modal-edit-discard').toggleClass('is-active');
});
//=include ../components/editor.js
// =include ../components/editor.js
} }
+12 -14
View File
@@ -1,18 +1,16 @@
if($('#page-type-source').length) { if ($('#page-type-source').length) {
var scEditor = ace.edit('source-display')
scEditor.setTheme('ace/theme/tomorrow_night')
scEditor.getSession().setMode('ace/mode/markdown')
scEditor.setOption('fontSize', '14px')
scEditor.setOption('hScrollBarAlwaysVisible', false)
scEditor.setOption('wrap', true)
scEditor.setReadOnly(true)
scEditor.renderer.updateFull()
var scEditor = ace.edit("source-display"); let currentBasePath = ($('#page-type-source').data('entrypath') !== 'home') ? $('#page-type-source').data('entrypath') : ''
scEditor.setTheme("ace/theme/tomorrow_night");
scEditor.getSession().setMode("ace/mode/markdown");
scEditor.setOption('fontSize', '14px');
scEditor.setOption('hScrollBarAlwaysVisible', false);
scEditor.setOption('wrap', true);
scEditor.setReadOnly(true);
scEditor.renderer.updateFull();
let currentBasePath = ($('#page-type-source').data('entrypath') !== 'home') ? $('#page-type-source').data('entrypath') : '';
//=include ../modals/create.js
//=include ../modals/move.js
// =include ../modals/create.js
// =include ../modals/move.js
} }
+4 -6
View File
@@ -1,9 +1,7 @@
if($('#page-type-view').length) { if ($('#page-type-view').length) {
let currentBasePath = ($('#page-type-view').data('entrypath') !== 'home') ? $('#page-type-view').data('entrypath') : ''
let currentBasePath = ($('#page-type-view').data('entrypath') !== 'home') ? $('#page-type-view').data('entrypath') : '';
//=include ../modals/create.js
//=include ../modals/move.js
// =include ../modals/create.js
// =include ../modals/move.js
} }
+115 -131
View File
@@ -1,162 +1,146 @@
"use strict"; 'use strict'
var express = require('express'); var express = require('express')
var router = express.Router(); var router = express.Router()
const Promise = require('bluebird'); const Promise = require('bluebird')
const validator = require('validator'); const validator = require('validator')
const _ = require('lodash'); const _ = require('lodash')
/** /**
* Admin * Admin
*/ */
router.get('/', (req, res) => { router.get('/', (req, res) => {
res.redirect('/admin/profile'); res.redirect('/admin/profile')
}); })
router.get('/profile', (req, res) => { router.get('/profile', (req, res) => {
if (res.locals.isGuest) {
return res.render('error-forbidden')
}
if(res.locals.isGuest) { res.render('pages/admin/profile', { adminTab: 'profile' })
return res.render('error-forbidden'); })
}
res.render('pages/admin/profile', { adminTab: 'profile' });
});
router.post('/profile', (req, res) => { router.post('/profile', (req, res) => {
if (res.locals.isGuest) {
return res.render('error-forbidden')
}
if(res.locals.isGuest) { return db.User.findById(req.user.id).then((usr) => {
return res.render('error-forbidden'); usr.name = _.trim(req.body.name)
} if (usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password)
return db.User.findById(req.user.id).then((usr) => { if (nPwd.length < 6) {
usr.name = _.trim(req.body.name); return Promise.reject(new Error('New Password too short!'))
if(usr.provider === 'local' && req.body.password !== '********') { } else {
let nPwd = _.trim(req.body.password); return db.User.hashPassword(nPwd).then((pwd) => {
if(nPwd.length < 6) { usr.password = pwd
return Promise.reject(new Error('New Password too short!')) return usr.save()
} else { })
return db.User.hashPassword(nPwd).then((pwd) => { }
usr.password = pwd; } else {
return usr.save(); return usr.save()
}); }
} }).then(() => {
} else { return res.json({ msg: 'OK' })
return usr.save(); }).catch((err) => {
} res.status(400).json({ msg: err.message })
}).then(() => { })
return res.json({ msg: 'OK' }); })
}).catch((err) => {
res.status(400).json({ msg: err.message });
})
});
router.get('/stats', (req, res) => { router.get('/stats', (req, res) => {
if (res.locals.isGuest) {
return res.render('error-forbidden')
}
if(res.locals.isGuest) { Promise.all([
return res.render('error-forbidden'); db.Entry.count(),
} db.UplFile.count(),
db.User.count()
Promise.all([ ]).spread((totalEntries, totalUploads, totalUsers) => {
db.Entry.count(), return res.render('pages/admin/stats', {
db.UplFile.count(), totalEntries, totalUploads, totalUsers, adminTab: 'stats'
db.User.count() }) || true
]).spread((totalEntries, totalUploads, totalUsers) => { }).catch((err) => {
return res.render('pages/admin/stats', { throw err
totalEntries, totalUploads, totalUsers, })
adminTab: 'stats' })
}) || true;
}).catch((err) => {
throw err;
});
});
router.get('/users', (req, res) => { router.get('/users', (req, res) => {
if (!res.locals.rights.manage) {
return res.render('error-forbidden')
}
if(!res.locals.rights.manage) { db.User.find({})
return res.render('error-forbidden'); .select('-password -rights')
} .sort('name email')
.exec().then((usrs) => {
db.User.find({}) res.render('pages/admin/users', { adminTab: 'users', usrs })
.select('-password -rights') })
.sort('name email') })
.exec().then((usrs) => {
res.render('pages/admin/users', { adminTab: 'users', usrs });
});
});
router.get('/users/:id', (req, res) => { router.get('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.render('error-forbidden')
}
if(!res.locals.rights.manage) { if (!validator.isMongoId(req.params.id)) {
return res.render('error-forbidden'); return res.render('error-forbidden')
} }
if(!validator.isMongoId(req.params.id)) { db.User.findById(req.params.id)
return res.render('error-forbidden'); .select('-password -providerId')
} .exec().then((usr) => {
let usrOpts = {
canChangeEmail: (usr.email !== 'guest' && usr.provider === 'local' && usr.email !== req.app.locals.appconfig.admin),
canChangeName: (usr.email !== 'guest'),
canChangePassword: (usr.email !== 'guest' && usr.provider === 'local'),
canChangeRole: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin)),
canBeDeleted: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin))
}
db.User.findById(req.params.id) res.render('pages/admin/users-edit', { adminTab: 'users', usr, usrOpts })
.select('-password -providerId') })
.exec().then((usr) => { })
let usrOpts = {
canChangeEmail: (usr.email !== 'guest' && usr.provider === 'local' && usr.email !== req.app.locals.appconfig.admin),
canChangeName: (usr.email !== 'guest'),
canChangePassword: (usr.email !== 'guest' && usr.provider === 'local'),
canChangeRole: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin)),
canBeDeleted: (usr.email !== 'guest' && !(usr.provider === 'local' && usr.email === req.app.locals.appconfig.admin))
};
res.render('pages/admin/users-edit', { adminTab: 'users', usr, usrOpts });
});
});
router.post('/users/:id', (req, res) => { router.post('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.status(401).json({ msg: 'Unauthorized' })
}
if(!res.locals.rights.manage) { if (!validator.isMongoId(req.params.id)) {
return res.status(401).json({ msg: 'Unauthorized' }); return res.status(400).json({ msg: 'Invalid User ID' })
} }
if(!validator.isMongoId(req.params.id)) { return db.User.findById(req.params.id).then((usr) => {
return res.status(400).json({ msg: 'Invalid User ID' }); usr.name = _.trim(req.body.name)
} usr.rights = JSON.parse(req.body.rights)
if (usr.provider === 'local' && req.body.password !== '********') {
return db.User.findById(req.params.id).then((usr) => { let nPwd = _.trim(req.body.password)
usr.name = _.trim(req.body.name); if (nPwd.length < 6) {
usr.rights = JSON.parse(req.body.rights); return Promise.reject(new Error('New Password too short!'))
if(usr.provider === 'local' && req.body.password !== '********') { } else {
let nPwd = _.trim(req.body.password); return db.User.hashPassword(nPwd).then((pwd) => {
if(nPwd.length < 6) { usr.password = pwd
return Promise.reject(new Error('New Password too short!')) return usr.save()
} else { })
return db.User.hashPassword(nPwd).then((pwd) => { }
usr.password = pwd; } else {
return usr.save(); return usr.save()
}); }
} }).then(() => {
} else { return res.json({ msg: 'OK' })
return usr.save(); }).catch((err) => {
} res.status(400).json({ msg: err.message })
}).then(() => { })
return res.json({ msg: 'OK' }); })
}).catch((err) => {
res.status(400).json({ msg: err.message });
})
});
router.get('/settings', (req, res) => { router.get('/settings', (req, res) => {
if (!res.locals.rights.manage) {
return res.render('error-forbidden')
}
if(!res.locals.rights.manage) { res.render('pages/admin/settings', { adminTab: 'settings' })
return res.render('error-forbidden'); })
}
res.render('pages/admin/settings', { adminTab: 'settings' }); module.exports = router
});
module.exports = router;
+57 -57
View File
@@ -1,80 +1,80 @@
var express = require('express'); 'use strict'
var router = express.Router();
var passport = require('passport'); const express = require('express')
var ExpressBrute = require('express-brute'); const router = express.Router()
var ExpressBruteMongooseStore = require('express-brute-mongoose'); const passport = require('passport')
var moment = require('moment'); const ExpressBrute = require('express-brute')
const ExpressBruteMongooseStore = require('express-brute-mongoose')
const moment = require('moment')
/** /**
* Setup Express-Brute * Setup Express-Brute
*/ */
var EBstore = new ExpressBruteMongooseStore(db.Bruteforce); const EBstore = new ExpressBruteMongooseStore(db.Bruteforce)
var bruteforce = new ExpressBrute(EBstore, { const bruteforce = new ExpressBrute(EBstore, {
freeRetries: 5, freeRetries: 5,
minWait: 60 * 1000, minWait: 60 * 1000,
maxWait: 5 * 60 * 1000, maxWait: 5 * 60 * 1000,
refreshTimeoutOnRequest: false, refreshTimeoutOnRequest: false,
failCallback(req, res, next, nextValidRequestDate) { failCallback (req, res, next, nextValidRequestDate) {
req.flash('alert', { req.flash('alert', {
class: 'error', class: 'error',
title: 'Too many attempts!', title: 'Too many attempts!',
message: "You've made too many failed attempts in a short period of time, please try again " + moment(nextValidRequestDate).fromNow() + '.', message: "You've made too many failed attempts in a short period of time, please try again " + moment(nextValidRequestDate).fromNow() + '.',
iconClass: 'fa-times' iconClass: 'fa-times'
}); })
res.redirect('/login'); res.redirect('/login')
} }
}); })
/** /**
* Login form * Login form
*/ */
router.get('/login', function(req, res, next) { router.get('/login', function (req, res, next) {
res.render('auth/login', { res.render('auth/login', {
usr: res.locals.usr usr: res.locals.usr
}); })
}); })
router.post('/login', bruteforce.prevent, function(req, res, next) { router.post('/login', bruteforce.prevent, function (req, res, next) {
passport.authenticate('local', function(err, user, info) { passport.authenticate('local', function (err, user, info) {
if (err) { return next(err) }
if (err) { return next(err); } if (!user) {
req.flash('alert', {
title: 'Invalid login',
message: 'The email or password is invalid.'
})
return res.redirect('/login')
}
if (!user) { req.logIn(user, function (err) {
req.flash('alert', { if (err) { return next(err) }
title: 'Invalid login',
message: "The email or password is invalid."
});
return res.redirect('/login');
}
req.logIn(user, function(err) {
if (err) { return next(err); }
req.brute.reset(function () { req.brute.reset(function () {
return res.redirect('/'); return res.redirect('/')
}); })
}); })
})(req, res, next)
})(req, res, next); })
});
/** /**
* Social Login * Social Login
*/ */
router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] })); router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] })); router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] }))
router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] })); router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' })); router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' })); router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' })); router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
/** /**
* Logout * Logout
*/ */
router.get('/logout', function(req, res) { router.get('/logout', function (req, res) {
req.logout(); req.logout()
res.redirect('/'); res.redirect('/')
}); })
module.exports = router; module.exports = router
+175 -192
View File
@@ -1,8 +1,8 @@
"use strict"; 'use strict'
var express = require('express'); const express = require('express')
var router = express.Router(); const router = express.Router()
var _ = require('lodash'); const _ = require('lodash')
// ========================================== // ==========================================
// EDIT MODE // EDIT MODE
@@ -12,132 +12,123 @@ var _ = require('lodash');
* Edit document in Markdown * Edit document in Markdown
*/ */
router.get('/edit/*', (req, res, next) => { router.get('/edit/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.render('error-forbidden')
}
if(!res.locals.rights.write) { let safePath = entries.parsePath(_.replace(req.path, '/edit', ''))
return res.render('error-forbidden');
}
let safePath = entries.parsePath(_.replace(req.path, '/edit', '')); entries.fetchOriginal(safePath, {
parseMarkdown: false,
entries.fetchOriginal(safePath, { parseMeta: true,
parseMarkdown: false, parseTree: false,
parseMeta: true, includeMarkdown: true,
parseTree: false, includeParentInfo: false,
includeMarkdown: true, cache: false
includeParentInfo: false, }).then((pageData) => {
cache: false if (pageData) {
}).then((pageData) => { res.render('pages/edit', { pageData })
if(pageData) { } else {
res.render('pages/edit', { pageData }); throw new Error('Invalid page path.')
} else { }
throw new Error('Invalid page path.'); return true
} }).catch((err) => {
return true; res.render('error', {
}).catch((err) => { message: err.message,
res.render('error', { error: {}
message: err.message, })
error: {} })
}); })
});
});
router.put('/edit/*', (req, res, next) => { router.put('/edit/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
})
}
if(!res.locals.rights.write) { let safePath = entries.parsePath(_.replace(req.path, '/edit', ''))
return res.json({
ok: false,
error: 'Forbidden'
});
}
let safePath = entries.parsePath(_.replace(req.path, '/edit', '')); entries.update(safePath, req.body.markdown).then(() => {
return res.json({
entries.update(safePath, req.body.markdown).then(() => { ok: true
return res.json({ }) || true
ok: true }).catch((err) => {
}) || true; res.json({
}).catch((err) => { ok: false,
res.json({ error: err.message
ok: false, })
error: err.message })
}); })
});
});
// ========================================== // ==========================================
// CREATE MODE // CREATE MODE
// ========================================== // ==========================================
router.get('/create/*', (req, res, next) => { router.get('/create/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.render('error-forbidden')
}
if(!res.locals.rights.write) { if (_.some(['create', 'edit', 'account', 'source', 'history', 'mk'], (e) => { return _.startsWith(req.path, '/create/' + e) })) {
return res.render('error-forbidden'); return res.render('error', {
} message: 'You cannot create a document with this name as it is reserved by the system.',
error: {}
})
}
if(_.some(['create','edit','account','source','history','mk'], (e) => { return _.startsWith(req.path, '/create/' + e); })) { let safePath = entries.parsePath(_.replace(req.path, '/create', ''))
return res.render('error', {
message: 'You cannot create a document with this name as it is reserved by the system.',
error: {}
});
}
let safePath = entries.parsePath(_.replace(req.path, '/create', '')); entries.exists(safePath).then((docExists) => {
if (!docExists) {
return entries.getStarter(safePath).then((contents) => {
let pageData = {
markdown: contents,
meta: {
title: _.startCase(safePath),
path: safePath
}
}
res.render('pages/create', { pageData })
entries.exists(safePath).then((docExists) => { return true
if(!docExists) { }).catch((err) => {
return entries.getStarter(safePath).then((contents) => { winston.warn(err)
throw new Error('Could not load starter content!')
let pageData = { })
markdown: contents, } else {
meta: { throw new Error('This entry already exists!')
title: _.startCase(safePath), }
path: safePath }).catch((err) => {
} res.render('error', {
}; message: err.message,
res.render('pages/create', { pageData }); error: {}
})
return true; })
})
}).catch((err) => {
throw new Error('Could not load starter content!');
});
} else {
throw new Error('This entry already exists!');
}
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
});
});
});
router.put('/create/*', (req, res, next) => { router.put('/create/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
})
}
if(!res.locals.rights.write) { let safePath = entries.parsePath(_.replace(req.path, '/create', ''))
return res.json({
ok: false,
error: 'Forbidden'
});
}
let safePath = entries.parsePath(_.replace(req.path, '/create', '')); entries.create(safePath, req.body.markdown).then(() => {
return res.json({
entries.create(safePath, req.body.markdown).then(() => { ok: true
return res.json({ }) || true
ok: true }).catch((err) => {
}) || true; return res.json({
}).catch((err) => { ok: false,
return res.json({ error: err.message
ok: false, })
error: err.message })
}); })
});
});
// ========================================== // ==========================================
// VIEW MODE // VIEW MODE
@@ -147,102 +138,94 @@ router.put('/create/*', (req, res, next) => {
* View source of a document * View source of a document
*/ */
router.get('/source/*', (req, res, next) => { router.get('/source/*', (req, res, next) => {
let safePath = entries.parsePath(_.replace(req.path, '/source', ''))
let safePath = entries.parsePath(_.replace(req.path, '/source', '')); entries.fetchOriginal(safePath, {
parseMarkdown: false,
entries.fetchOriginal(safePath, { parseMeta: true,
parseMarkdown: false, parseTree: false,
parseMeta: true, includeMarkdown: true,
parseTree: false, includeParentInfo: false,
includeMarkdown: true, cache: false
includeParentInfo: false, }).then((pageData) => {
cache: false if (pageData) {
}).then((pageData) => { res.render('pages/source', { pageData })
if(pageData) { } else {
res.render('pages/source', { pageData }); throw new Error('Invalid page path.')
} else { }
throw new Error('Invalid page path.'); return true
} }).catch((err) => {
return true; res.render('error', {
}).catch((err) => { message: err.message,
res.render('error', { error: {}
message: err.message, })
error: {} })
}); })
});
});
/** /**
* View document * View document
*/ */
router.get('/*', (req, res, next) => { router.get('/*', (req, res, next) => {
let safePath = entries.parsePath(req.path)
let safePath = entries.parsePath(req.path); entries.fetch(safePath).then((pageData) => {
if (pageData) {
entries.fetch(safePath).then((pageData) => { res.render('pages/view', { pageData })
if(pageData) { } else {
res.render('pages/view', { pageData }); res.render('error-notexist', {
} else { newpath: safePath
res.render('error-notexist', { })
newpath: safePath }
}); return true
} }).error((err) => {
return true; if (safePath === 'home') {
}).error((err) => { res.render('pages/welcome')
} else {
if(safePath === 'home') { res.render('error-notexist', {
res.render('pages/welcome'); message: err.message,
} else { newpath: safePath
res.render('error-notexist', { })
message: err.message, }
newpath: safePath }).catch((err) => {
}); res.render('error', {
} message: err.message,
error: {}
}).catch((err) => { })
res.render('error', { })
message: err.message, })
error: {}
});
});
});
/** /**
* Move document * Move document
*/ */
router.put('/*', (req, res, next) => { router.put('/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
})
}
if(!res.locals.rights.write) { let safePath = entries.parsePath(req.path)
return res.json({
ok: false,
error: 'Forbidden'
});
}
let safePath = entries.parsePath(req.path); if (_.isEmpty(req.body.move)) {
return res.json({
ok: false,
error: 'Invalid document action call.'
})
}
if(_.isEmpty(req.body.move)) { let safeNewPath = entries.parsePath(req.body.move)
return res.json({
ok: false,
error: 'Invalid document action call.'
});
}
let safeNewPath = entries.parsePath(req.body.move); entries.move(safePath, safeNewPath).then(() => {
res.json({
ok: true
})
}).catch((err) => {
res.json({
ok: false,
error: err.message
})
})
})
entries.move(safePath, safeNewPath).then(() => { module.exports = router
res.json({
ok: true
});
}).catch((err) => {
res.json({
ok: false,
error: err.message
});
});
});
module.exports = router;
+129 -153
View File
@@ -1,184 +1,160 @@
"use strict"; 'use strict'
var express = require('express'); const express = require('express')
var router = express.Router(); const router = express.Router()
var readChunk = require('read-chunk'), const readChunk = require('read-chunk')
fileType = require('file-type'), const fileType = require('file-type')
Promise = require('bluebird'), const Promise = require('bluebird')
fs = Promise.promisifyAll(require('fs-extra')), const fs = Promise.promisifyAll(require('fs-extra'))
path = require('path'), const path = require('path')
_ = require('lodash'); const _ = require('lodash')
var validPathRe = new RegExp("^([a-z0-9\\/-]+\\.[a-z0-9]+)$"); const validPathRe = new RegExp('^([a-z0-9\\/-]+\\.[a-z0-9]+)$')
var validPathThumbsRe = new RegExp("^([0-9]+\\.png)$"); const validPathThumbsRe = new RegExp('^([0-9]+\\.png)$')
// ========================================== // ==========================================
// SERVE UPLOADS FILES // SERVE UPLOADS FILES
// ========================================== // ==========================================
router.get('/t/*', (req, res, next) => { router.get('/t/*', (req, res, next) => {
let fileName = req.params[0]
if (!validPathThumbsRe.test(fileName)) {
return res.sendStatus(404).end()
}
let fileName = req.params[0]; // todo: Authentication-based access
if(!validPathThumbsRe.test(fileName)) {
return res.sendStatus(404).end();
}
//todo: Authentication-based access res.sendFile(fileName, {
root: lcdata.getThumbsPath(),
res.sendFile(fileName, { dotfiles: 'deny'
root: lcdata.getThumbsPath(), }, (err) => {
dotfiles: 'deny' if (err) {
}, (err) => { res.status(err.status).end()
if (err) { }
res.status(err.status).end(); })
} })
});
});
router.post('/img', lcdata.uploadImgHandler, (req, res, next) => { router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
let destFolder = _.chain(req.body.folder).trim().toLower().value()
let destFolder = _.chain(req.body.folder).trim().toLower().value(); upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if (!destFolderPath) {
res.json({ ok: false, msg: 'Invalid Folder' })
return true
}
upl.validateUploadsFolder(destFolder).then((destFolderPath) => { Promise.map(req.files, (f) => {
let destFilename = ''
let destFilePath = ''
if(!destFolderPath) { return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
res.json({ ok: false, msg: 'Invalid Folder' }); destFilename = fname
return true; destFilePath = path.resolve(destFolderPath, destFilename)
}
Promise.map(req.files, (f) => { return readChunk(f.path, 0, 262)
}).then((buf) => {
// -> Check MIME type by magic number
let destFilename = ''; let mimeInfo = fileType(buf)
let destFilePath = ''; if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
return Promise.reject(new Error('Invalid file type.'))
}
return true
}).then(() => {
// -> Move file to final destination
return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => { return fs.moveAsync(f.path, destFilePath, { clobber: false })
}).then(() => {
destFilename = fname; return {
destFilePath = path.resolve(destFolderPath, destFilename); ok: true,
filename: destFilename,
return readChunk(f.path, 0, 262); filesize: f.size
}
}).then((buf) => { }).reflect()
}, {concurrency: 3}).then((results) => {
//-> Check MIME type by magic number let uplResults = _.map(results, (r) => {
if (r.isFulfilled()) {
let mimeInfo = fileType(buf); return r.value()
if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) { } else {
return Promise.reject(new Error('Invalid file type.')); return {
} ok: false,
return true; msg: r.reason().message
}
}).then(() => { }
})
//-> Move file to final destination res.json({ ok: true, results: uplResults })
return true
return fs.moveAsync(f.path, destFilePath, { clobber: false }); }).catch((err) => {
res.json({ ok: false, msg: err.message })
}).then(() => { return true
return { })
ok: true, })
filename: destFilename, })
filesize: f.size
};
}).reflect();
}, {concurrency: 3}).then((results) => {
let uplResults = _.map(results, (r) => {
if(r.isFulfilled()) {
return r.value();
} else {
return {
ok: false,
msg: r.reason().message
};
}
});
res.json({ ok: true, results: uplResults });
return true;
}).catch((err) => {
res.json({ ok: false, msg: err.message });
return true;
});
});
});
router.post('/file', lcdata.uploadFileHandler, (req, res, next) => { router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
let destFolder = _.chain(req.body.folder).trim().toLower().value()
let destFolder = _.chain(req.body.folder).trim().toLower().value(); upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if (!destFolderPath) {
res.json({ ok: false, msg: 'Invalid Folder' })
return true
}
upl.validateUploadsFolder(destFolder).then((destFolderPath) => { Promise.map(req.files, (f) => {
let destFilename = ''
let destFilePath = ''
if(!destFolderPath) { return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
res.json({ ok: false, msg: 'Invalid Folder' }); destFilename = fname
return true; destFilePath = path.resolve(destFolderPath, destFilename)
}
Promise.map(req.files, (f) => { // -> Move file to final destination
let destFilename = ''; return fs.moveAsync(f.path, destFilePath, { clobber: false })
let destFilePath = ''; }).then(() => {
return {
return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => { ok: true,
filename: destFilename,
destFilename = fname; filesize: f.size
destFilePath = path.resolve(destFolderPath, destFilename); }
}).reflect()
//-> Move file to final destination }, {concurrency: 3}).then((results) => {
let uplResults = _.map(results, (r) => {
return fs.moveAsync(f.path, destFilePath, { clobber: false }); if (r.isFulfilled()) {
return r.value()
}).then(() => { } else {
return { return {
ok: true, ok: false,
filename: destFilename, msg: r.reason().message
filesize: f.size }
}; }
}).reflect(); })
res.json({ ok: true, results: uplResults })
}, {concurrency: 3}).then((results) => { return true
let uplResults = _.map(results, (r) => { }).catch((err) => {
if(r.isFulfilled()) { res.json({ ok: false, msg: err.message })
return r.value(); return true
} else { })
return { })
ok: false, })
msg: r.reason().message
};
}
});
res.json({ ok: true, results: uplResults });
return true;
}).catch((err) => {
res.json({ ok: false, msg: err.message });
return true;
});
});
});
router.get('/*', (req, res, next) => { router.get('/*', (req, res, next) => {
let fileName = req.params[0]
if (!validPathRe.test(fileName)) {
return res.sendStatus(404).end()
}
let fileName = req.params[0]; // todo: Authentication-based access
if(!validPathRe.test(fileName)) {
return res.sendStatus(404).end();
}
//todo: Authentication-based access res.sendFile(fileName, {
root: git.getRepoPath() + '/uploads/',
dotfiles: 'deny'
}, (err) => {
if (err) {
res.status(err.status).end()
}
})
})
res.sendFile(fileName, { module.exports = router
root: git.getRepoPath() + '/uploads/',
dotfiles: 'deny'
}, (err) => {
if (err) {
res.status(err.status).end();
}
});
});
module.exports = router;
+58 -58
View File
@@ -1,95 +1,95 @@
"use strict"; 'use strict'
const _ = require('lodash')
module.exports = (socket) => { module.exports = (socket) => {
if (!socket.request.user.logged_in) {
if(!socket.request.user.logged_in) { return
return;
} }
//----------------------------------------- // -----------------------------------------
// SEARCH // SEARCH
//----------------------------------------- // -----------------------------------------
socket.on('search', (data, cb) => { socket.on('search', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
entries.search(data.terms).then((results) => { entries.search(data.terms).then((results) => {
return cb(results) || true; return cb(results) || true
}); })
}); })
//----------------------------------------- // -----------------------------------------
// UPLOADS // UPLOADS
//----------------------------------------- // -----------------------------------------
socket.on('uploadsGetFolders', (data, cb) => { socket.on('uploadsGetFolders', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.getUploadsFolders().then((f) => { upl.getUploadsFolders().then((f) => {
return cb(f) || true; return cb(f) || true
}); })
}); })
socket.on('uploadsCreateFolder', (data, cb) => { socket.on('uploadsCreateFolder', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.createUploadsFolder(data.foldername).then((f) => { upl.createUploadsFolder(data.foldername).then((f) => {
return cb(f) || true; return cb(f) || true
}); })
}); })
socket.on('uploadsGetImages', (data, cb) => { socket.on('uploadsGetImages', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.getUploadsFiles('image', data.folder).then((f) => { upl.getUploadsFiles('image', data.folder).then((f) => {
return cb(f) || true; return cb(f) || true
}); })
}); })
socket.on('uploadsGetFiles', (data, cb) => { socket.on('uploadsGetFiles', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.getUploadsFiles('binary', data.folder).then((f) => { upl.getUploadsFiles('binary', data.folder).then((f) => {
return cb(f) || true; return cb(f) || true
}); })
}); })
socket.on('uploadsDeleteFile', (data, cb) => { socket.on('uploadsDeleteFile', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.deleteUploadsFile(data.uid).then((f) => { upl.deleteUploadsFile(data.uid).then((f) => {
return cb(f) || true; return cb(f) || true
}); })
}); })
socket.on('uploadsFetchFileFromURL', (data, cb) => { socket.on('uploadsFetchFileFromURL', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.downloadFromUrl(data.folder, data.fetchUrl).then((f) => { upl.downloadFromUrl(data.folder, data.fetchUrl).then((f) => {
return cb({ ok: true }) || true; return cb({ ok: true }) || true
}).catch((err) => { }).catch((err) => {
return cb({ return cb({
ok: false, ok: false,
msg: err.message msg: err.message
}) || true; }) || true
}); })
}); })
socket.on('uploadsRenameFile', (data, cb) => { socket.on('uploadsRenameFile', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.moveUploadsFile(data.uid, data.folder, data.filename).then((f) => { upl.moveUploadsFile(data.uid, data.folder, data.filename).then((f) => {
return cb({ ok: true }) || true; return cb({ ok: true }) || true
}).catch((err) => { }).catch((err) => {
return cb({ return cb({
ok: false, ok: false,
msg: err.message msg: err.message
}) || true; }) || true
}); })
}); })
socket.on('uploadsMoveFile', (data, cb) => { socket.on('uploadsMoveFile', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop
upl.moveUploadsFile(data.uid, data.folder).then((f) => { upl.moveUploadsFile(data.uid, data.folder).then((f) => {
return cb({ ok: true }) || true; return cb({ ok: true }) || true
}).catch((err) => { }).catch((err) => {
return cb({ return cb({
ok: false, ok: false,
msg: err.message msg: err.message
}) || true; }) || true
}); })
}); })
}
};
+152 -157
View File
@@ -1,215 +1,210 @@
var gulp = require("gulp"); 'use strict'
var watch = require('gulp-watch');
var merge = require('merge-stream'); const gulp = require('gulp')
var babel = require("gulp-babel"); const watch = require('gulp-watch')
var uglify = require('gulp-uglify'); const merge = require('merge-stream')
var concat = require('gulp-concat'); const babel = require('gulp-babel')
var nodemon = require('gulp-nodemon'); const uglify = require('gulp-uglify')
var plumber = require('gulp-plumber'); const concat = require('gulp-concat')
var zip = require('gulp-zip'); const nodemon = require('gulp-nodemon')
var tar = require('gulp-tar'); const plumber = require('gulp-plumber')
var gzip = require('gulp-gzip'); const zip = require('gulp-zip')
var sass = require('gulp-sass'); const tar = require('gulp-tar')
var cleanCSS = require('gulp-clean-css'); const gzip = require('gulp-gzip')
var include = require("gulp-include"); const sass = require('gulp-sass')
var run = require('run-sequence'); const cleanCSS = require('gulp-clean-css')
var _ = require('lodash'); const include = require('gulp-include')
const run = require('run-sequence')
/** /**
* Paths * Paths
* *
* @type {Object} * @type {Object}
*/ */
var paths = { const paths = {
scripts: { scripts: {
combine: [ combine: [
'./node_modules/socket.io-client/dist/socket.io.min.js', './node_modules/socket.io-client/dist/socket.io.min.js',
'./node_modules/jquery/dist/jquery.min.js', './node_modules/jquery/dist/jquery.min.js',
'./node_modules/vue/dist/vue.min.js', './node_modules/vue/dist/vue.min.js',
'./node_modules/jquery-smooth-scroll/jquery.smooth-scroll.min.js', './node_modules/jquery-smooth-scroll/jquery.smooth-scroll.min.js',
'./node_modules/jquery-simple-upload/simpleUpload.min.js', './node_modules/jquery-simple-upload/simpleUpload.min.js',
'./node_modules/jquery-contextmenu/dist/jquery.contextMenu.min.js', './node_modules/jquery-contextmenu/dist/jquery.contextMenu.min.js',
'./node_modules/sticky-js/dist/sticky.min.js', './node_modules/sticky-js/dist/sticky.min.js',
'./node_modules/simplemde/dist/simplemde.min.js', './node_modules/simplemde/dist/simplemde.min.js',
'./node_modules/ace-builds/src-min-noconflict/ace.js', './node_modules/ace-builds/src-min-noconflict/ace.js',
'./node_modules/ace-builds/src-min-noconflict/ext-modelist.js', './node_modules/ace-builds/src-min-noconflict/ext-modelist.js',
'./node_modules/ace-builds/src-min-noconflict/mode-markdown.js', './node_modules/ace-builds/src-min-noconflict/mode-markdown.js',
'./node_modules/ace-builds/src-min-noconflict/theme-tomorrow_night.js', './node_modules/ace-builds/src-min-noconflict/theme-tomorrow_night.js',
'./node_modules/filesize.js/dist/filesize.min.js', './node_modules/filesize.js/dist/filesize.min.js',
'./node_modules/lodash/lodash.min.js' './node_modules/lodash/lodash.min.js'
], ],
ace: [ ace: [
'./node_modules/ace-builds/src-min-noconflict/mode-*.js', './node_modules/ace-builds/src-min-noconflict/mode-*.js',
'!./node_modules/ace-builds/src-min-noconflict/mode-markdown.js' '!./node_modules/ace-builds/src-min-noconflict/mode-markdown.js'
], ],
compile: [ compile: [
'./client/js/*.js' './client/js/*.js'
], ],
watch: [ watch: [
'./client/js/**/*.js' './client/js/**/*.js'
] ]
}, },
css: { css: {
combine: [ combine: [
'./node_modules/highlight.js/styles/tomorrow.css', './node_modules/highlight.js/styles/tomorrow.css',
'./node_modules/simplemde/dist/simplemde.min.css' './node_modules/simplemde/dist/simplemde.min.css'
], ],
compile: [ compile: [
'./client/scss/*.scss' './client/scss/*.scss'
], ],
includes: [ includes: [
'./node_modules/requarks-core' //! MUST BE LAST './node_modules/requarks-core' //! MUST BE LAST
], ],
watch: [ watch: [
'./client/scss/**/*.scss', './client/scss/**/*.scss',
'../core/core-client/scss/**/*.scss' '../core/core-client/scss/**/*.scss'
] ]
}, },
fonts: [ fonts: [
'../node_modules/requarks-core/core-client/fonts/**/*' //! MUST BE LAST '../node_modules/requarks-core/core-client/fonts/**/*' //! MUST BE LAST
], ],
deploy: [ deploy: [
'./**/*', './**/*',
'!node_modules', '!node_modules/**', '!node_modules', '!node_modules/**',
'!coverage', '!coverage/**', '!coverage', '!coverage/**',
'!client/js', '!client/js/**', '!client/js', '!client/js/**',
'!client/scss', '!client/scss/**', '!client/scss', '!client/scss/**',
'!dist', '!dist/**', '!dist', '!dist/**',
'!tests', '!tests/**', '!tests', '!tests/**',
'!data', '!data/**', '!data', '!data/**',
'!repo', '!repo/**', '!repo', '!repo/**',
'!.babelrc', '!.gitattributes', '!.gitignore', '!.snyk', '!.travis.yml', '!.babelrc', '!.gitattributes', '!.gitignore', '!.snyk', '!.travis.yml',
'!gulpfile.js', '!inch.json', '!config.yml', '!wiki.sublime-project' '!gulpfile.js', '!inch.json', '!config.yml', '!wiki.sublime-project'
] ]
}; }
/** /**
* TASK - Starts server in development mode * TASK - Starts server in development mode
*/ */
gulp.task('server', ['scripts', 'css', 'fonts'], function() { gulp.task('server', ['scripts', 'css', 'fonts'], function () {
nodemon({ nodemon({
script: './server', script: './server',
ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'], ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'],
ext: 'js json', ext: 'js json',
env: { 'NODE_ENV': 'development' } env: { 'NODE_ENV': 'development' }
}); })
}); })
/** /**
* TASK - Process all scripts processes * TASK - Process all scripts processes
*/ */
gulp.task("scripts", ['scripts-libs', 'scripts-app']); gulp.task('scripts', ['scripts-libs', 'scripts-app'])
/** /**
* TASK - Combine js libraries * TASK - Combine js libraries
*/ */
gulp.task("scripts-libs", function () { gulp.task('scripts-libs', function () {
return merge(
return merge( gulp.src(paths.scripts.combine)
.pipe(concat('libs.js', {newLine: ';\n'}))
.pipe(uglify({ mangle: false }))
.pipe(gulp.dest('./assets/js')),
gulp.src(paths.scripts.combine) gulp.src(paths.scripts.ace)
.pipe(concat('libs.js', {newLine: ';\n'})) .pipe(gulp.dest('./assets/js/ace'))
.pipe(uglify({ mangle: false }))
.pipe(gulp.dest("./assets/js")),
gulp.src(paths.scripts.ace) )
.pipe(gulp.dest("./assets/js/ace")) })
);
});
/** /**
* TASK - Combine, make compatible and compress js app scripts * TASK - Combine, make compatible and compress js app scripts
*/ */
gulp.task("scripts-app", function () { gulp.task('scripts-app', function () {
return gulp.src(paths.scripts.compile)
return gulp.src(paths.scripts.compile) .pipe(plumber())
.pipe(plumber()) .pipe(include({ extensions: 'js' }))
.pipe(include({ extensions: "js" })) .pipe(babel())
.pipe(babel()) .pipe(uglify())
.pipe(uglify()) .pipe(plumber.stop())
.pipe(plumber.stop()) .pipe(gulp.dest('./assets/js'))
.pipe(gulp.dest("./assets/js")); })
});
/** /**
* TASK - Process all css processes * TASK - Process all css processes
*/ */
gulp.task("css", ['css-libs', 'css-app']); gulp.task('css', ['css-libs', 'css-app'])
/** /**
* TASK - Combine css libraries * TASK - Combine css libraries
*/ */
gulp.task("css-libs", function () { gulp.task('css-libs', function () {
return gulp.src(paths.css.combine) return gulp.src(paths.css.combine)
.pipe(plumber()) .pipe(plumber())
.pipe(concat('libs.css')) .pipe(concat('libs.css'))
.pipe(cleanCSS({ keepSpecialComments: 0 })) .pipe(cleanCSS({ keepSpecialComments: 0 }))
.pipe(plumber.stop()) .pipe(plumber.stop())
.pipe(gulp.dest("./assets/css")); .pipe(gulp.dest('./assets/css'))
}); })
/** /**
* TASK - Combine app css * TASK - Combine app css
*/ */
gulp.task("css-app", function () { gulp.task('css-app', function () {
return gulp.src(paths.css.compile) return gulp.src(paths.css.compile)
.pipe(plumber()) .pipe(plumber())
.pipe(sass.sync({ includePaths: paths.css.includes })) .pipe(sass.sync({ includePaths: paths.css.includes }))
.pipe(cleanCSS({ keepSpecialComments: 0 })) .pipe(cleanCSS({ keepSpecialComments: 0 }))
.pipe(plumber.stop()) .pipe(plumber.stop())
.pipe(gulp.dest("./assets/css")); .pipe(gulp.dest('./assets/css'))
}); })
/** /**
* TASK - Copy web fonts * TASK - Copy web fonts
*/ */
gulp.task("fonts", function () { gulp.task('fonts', function () {
return gulp.src(paths.fonts) return gulp.src(paths.fonts)
.pipe(gulp.dest("./assets/fonts")); .pipe(gulp.dest('./assets/fonts'))
}); })
/** /**
* TASK - Start dev watchers * TASK - Start dev watchers
*/ */
gulp.task('watch', function() { gulp.task('watch', function () {
return merge( return merge(
watch(paths.scripts.watch, {base: './'}, function() { return gulp.start('scripts-app'); }), watch(paths.scripts.watch, {base: './'}, function () { return gulp.start('scripts-app') }),
watch(paths.css.watch, {base: './'}, function() { return gulp.start('css-app'); }) watch(paths.css.watch, {base: './'}, function () { return gulp.start('css-app') })
); )
}); })
/** /**
* TASK - Starts development server with watchers * TASK - Starts development server with watchers
*/ */
gulp.task('default', ['watch', 'server']); gulp.task('default', ['watch', 'server'])
gulp.task('dev', function() { gulp.task('dev', function () {
paths.css.includes.pop()
paths.css.includes.push('../core')
paths.css.includes.pop(); paths.fonts.pop()
paths.css.includes.push('../core'); paths.fonts.push('../core/core-client/fonts/**/*')
paths.fonts.pop(); return run('default')
paths.fonts.push('../core/core-client/fonts/**/*'); })
return run('default');
});
/** /**
* TASK - Creates deployment packages * TASK - Creates deployment packages
*/ */
gulp.task('deploy', ['scripts', 'css', 'fonts'], function() { gulp.task('deploy', ['scripts', 'css', 'fonts'], function () {
var zipStream = gulp.src(paths.deploy) var zipStream = gulp.src(paths.deploy)
.pipe(zip('wiki-js.zip')) .pipe(zip('wiki-js.zip'))
.pipe(gulp.dest('dist')); .pipe(gulp.dest('dist'))
var targzStream = gulp.src(paths.deploy) var targzStream = gulp.src(paths.deploy)
.pipe(tar('wiki-js.tar')) .pipe(tar('wiki-js.tar'))
.pipe(gzip()) .pipe(gzip())
.pipe(gulp.dest('dist')); .pipe(gulp.dest('dist'))
return merge(zipStream, targzStream); return merge(zipStream, targzStream)
}); })
+445 -493
View File
@@ -1,500 +1,452 @@
"use strict"; 'use strict'
var Promise = require('bluebird'), const Promise = require('bluebird')
path = require('path'), const path = require('path')
fs = Promise.promisifyAll(require("fs-extra")), const fs = Promise.promisifyAll(require('fs-extra'))
_ = require('lodash'), const _ = require('lodash')
farmhash = require('farmhash'), const farmhash = require('farmhash')
moment = require('moment');
/** /**
* Entries Model * Entries Model
*/ */
module.exports = { module.exports = {
_repoPath: 'repo', _repoPath: 'repo',
_cachePath: 'data/cache', _cachePath: 'data/cache',
/** /**
* Initialize Entries model * Initialize Entries model
* *
* @return {Object} Entries model instance * @return {Object} Entries model instance
*/ */
init() { init () {
let self = this
let self = this;
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo); self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache')
self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache');
return self
return self; },
}, /**
* Check if a document already exists
/** *
* Check if a document already exists * @param {String} entryPath The entry path
* * @return {Promise<Boolean>} True if exists, false otherwise
* @param {String} entryPath The entry path */
* @return {Promise<Boolean>} True if exists, false otherwise exists (entryPath) {
*/ let self = this
exists(entryPath) {
return self.fetchOriginal(entryPath, {
let self = this; parseMarkdown: false,
parseMeta: false,
return self.fetchOriginal(entryPath, { parseTree: false,
parseMarkdown: false, includeMarkdown: false,
parseMeta: false, includeParentInfo: false,
parseTree: false, cache: false
includeMarkdown: false, }).then(() => {
includeParentInfo: false, return true
cache: false }).catch((err) => { // eslint-disable-line handle-callback-err
}).then(() => { return false
return true; })
}).catch((err) => { },
return false;
}); /**
* Fetch a document from cache, otherwise the original
}, *
* @param {String} entryPath The entry path
/** * @return {Promise<Object>} Page Data
* Fetch a document from cache, otherwise the original */
* fetch (entryPath) {
* @param {String} entryPath The entry path let self = this
* @return {Promise<Object>} Page Data
*/ let cpath = self.getCachePath(entryPath)
fetch(entryPath) {
return fs.statAsync(cpath).then((st) => {
let self = this; return st.isFile()
}).catch((err) => { // eslint-disable-line handle-callback-err
let cpath = self.getCachePath(entryPath); return false
}).then((isCache) => {
return fs.statAsync(cpath).then((st) => { if (isCache) {
return st.isFile(); // Load from cache
}).catch((err) => {
return false; return fs.readFileAsync(cpath).then((contents) => {
}).then((isCache) => { return JSON.parse(contents)
}).catch((err) => { // eslint-disable-line handle-callback-err
if(isCache) { winston.error('Corrupted cache file. Deleting it...')
fs.unlinkSync(cpath)
// Load from cache return false
})
return fs.readFileAsync(cpath).then((contents) => { } else {
return JSON.parse(contents); // Load original
}).catch((err) => {
winston.error('Corrupted cache file. Deleting it...'); return self.fetchOriginal(entryPath)
fs.unlinkSync(cpath); }
return false; })
}); },
} else { /**
* Fetches the original document entry
// Load original *
* @param {String} entryPath The entry path
return self.fetchOriginal(entryPath); * @param {Object} options The options
* @return {Promise<Object>} Page data
} */
fetchOriginal (entryPath, options) {
}); let self = this
}, let fpath = self.getFullPath(entryPath)
let cpath = self.getCachePath(entryPath)
/**
* Fetches the original document entry options = _.defaults(options, {
* parseMarkdown: true,
* @param {String} entryPath The entry path parseMeta: true,
* @param {Object} options The options parseTree: true,
* @return {Promise<Object>} Page data includeMarkdown: false,
*/ includeParentInfo: true,
fetchOriginal(entryPath, options) { cache: true
})
let self = this;
return fs.statAsync(fpath).then((st) => {
let fpath = self.getFullPath(entryPath); if (st.isFile()) {
let cpath = self.getCachePath(entryPath); return fs.readFileAsync(fpath, 'utf8').then((contents) => {
// Parse contents
options = _.defaults(options, {
parseMarkdown: true, let pageData = {
parseMeta: true, markdown: (options.includeMarkdown) ? contents : '',
parseTree: true, html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
includeMarkdown: false, meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
includeParentInfo: true, tree: (options.parseTree) ? mark.parseTree(contents) : []
cache: true }
});
if (!pageData.meta.title) {
return fs.statAsync(fpath).then((st) => { pageData.meta.title = _.startCase(entryPath)
if(st.isFile()) { }
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
pageData.meta.path = entryPath
// Parse contents
// Get parent
let pageData = {
markdown: (options.includeMarkdown) ? contents : '', let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
html: (options.parseMarkdown) ? mark.parseContent(contents) : '', return (pageData.parent = parentData)
meta: (options.parseMeta) ? mark.parseMeta(contents) : {}, }).catch((err) => { // eslint-disable-line handle-callback-err
tree: (options.parseTree) ? mark.parseTree(contents) : [] return (pageData.parent = false)
}; }) : Promise.resolve(true)
if(!pageData.meta.title) { return parentPromise.then(() => {
pageData.meta.title = _.startCase(entryPath); // Cache to disk
}
if (options.cache) {
pageData.meta.path = entryPath; let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
// Get parent winston.error('Unable to write to cache! Performance may be affected.')
winston.error(err)
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => { return true
return (pageData.parent = parentData); })
}).catch((err) => { } else {
return (pageData.parent = false); return true
}) : Promise.resolve(true); }
}).return(pageData)
return parentPromise.then(() => { })
} else {
// Cache to disk return false
}
if(options.cache) { }).catch((err) => { // eslint-disable-line handle-callback-err
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false); return Promise.reject(new Promise.OperationalError('Entry ' + entryPath + ' does not exist!'))
return fs.writeFileAsync(cpath, cacheData).catch((err) => { })
winston.error('Unable to write to cache! Performance may be affected.'); },
return true;
}); /**
} else { * Parse raw url path and make it safe
return true; *
} * @param {String} urlPath The url path
* @return {String} Safe entry path
}).return(pageData); */
parsePath (urlPath) {
}); let wlist = new RegExp('[^a-z0-9/-]', 'g')
} else {
return false; urlPath = _.toLower(urlPath).replace(wlist, '')
}
}).catch((err) => { if (urlPath === '/') {
return Promise.reject(new Promise.OperationalError('Entry ' + entryPath + ' does not exist!')); urlPath = 'home'
}); }
}, let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p) })
/** return _.join(urlParts, '/')
* Parse raw url path and make it safe },
*
* @param {String} urlPath The url path /**
* @return {String} Safe entry path * Gets the parent information.
*/ *
parsePath(urlPath) { * @param {String} entryPath The entry path
* @return {Promise<Object|False>} The parent information.
let wlist = new RegExp('[^a-z0-9/\-]','g'); */
getParentInfo (entryPath) {
urlPath = _.toLower(urlPath).replace(wlist, ''); let self = this
if(urlPath === '/') { if (_.includes(entryPath, '/')) {
urlPath = 'home'; let parentParts = _.initial(_.split(entryPath, '/'))
} let parentPath = _.join(parentParts, '/')
let parentFile = _.last(parentParts)
let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p); }); let fpath = self.getFullPath(parentPath)
return _.join(urlParts, '/'); return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
}, return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let pageMeta = mark.parseMeta(contents)
/**
* Gets the parent information. return {
* path: parentPath,
* @param {String} entryPath The entry path title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
* @return {Promise<Object|False>} The parent information. subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
*/ }
getParentInfo(entryPath) { })
} else {
let self = this; return Promise.reject(new Error('Parent entry is not a valid file.'))
}
if(_.includes(entryPath, '/')) { })
} else {
let parentParts = _.initial(_.split(entryPath, '/')); return Promise.reject(new Error('Parent entry is root.'))
let parentPath = _.join(parentParts,'/'); }
let parentFile = _.last(parentParts); },
let fpath = self.getFullPath(parentPath);
/**
return fs.statAsync(fpath).then((st) => { * Gets the full original path of a document.
if(st.isFile()) { *
return fs.readFileAsync(fpath, 'utf8').then((contents) => { * @param {String} entryPath The entry path
* @return {String} The full path.
let pageMeta = mark.parseMeta(contents); */
getFullPath (entryPath) {
return { return path.join(this._repoPath, entryPath + '.md')
path: parentPath, },
title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false /**
}; * Gets the full cache path of a document.
*
}); * @param {String} entryPath The entry path
} else { * @return {String} The full cache path.
return Promise.reject(new Error('Parent entry is not a valid file.')); */
} getCachePath (entryPath) {
}); return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.json')
},
} else {
return Promise.reject(new Error('Parent entry is root.')); /**
} * Gets the entry path from full path.
*
}, * @param {String} fullPath The full path
* @return {String} The entry path
/** */
* Gets the full original path of a document. getEntryPathFromFullPath (fullPath) {
* let absRepoPath = path.resolve(ROOTPATH, this._repoPath)
* @param {String} entryPath The entry path return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'), '/').value()
* @return {String} The full path. },
*/
getFullPath(entryPath) { /**
return path.join(this._repoPath, entryPath + '.md'); * Update an existing document
}, *
* @param {String} entryPath The entry path
/** * @param {String} contents The markdown-formatted contents
* Gets the full cache path of a document. * @return {Promise<Boolean>} True on success, false on failure
* */
* @param {String} entryPath The entry path update (entryPath, contents) {
* @return {String} The full cache path. let self = this
*/ let fpath = self.getFullPath(entryPath)
getCachePath(entryPath) {
return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.json'); return fs.statAsync(fpath).then((st) => {
}, if (st.isFile()) {
return self.makePersistent(entryPath, contents).then(() => {
/** return self.updateCache(entryPath)
* Gets the entry path from full path. })
* } else {
* @param {String} fullPath The full path return Promise.reject(new Error('Entry does not exist!'))
* @return {String} The entry path }
*/ }).catch((err) => {
getEntryPathFromFullPath(fullPath) { winston.error(err)
let absRepoPath = path.resolve(ROOTPATH, this._repoPath); return Promise.reject(new Error('Failed to save document.'))
return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'),'/').value(); })
}, },
/** /**
* Update an existing document * Update local cache and search index
* *
* @param {String} entryPath The entry path * @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents * @return {Promise} Promise of the operation
* @return {Promise<Boolean>} True on success, false on failure */
*/ updateCache (entryPath) {
update(entryPath, contents) { let self = this
let self = this; return self.fetchOriginal(entryPath, {
let fpath = self.getFullPath(entryPath); parseMarkdown: true,
parseMeta: true,
return fs.statAsync(fpath).then((st) => { parseTree: true,
if(st.isFile()) { includeMarkdown: true,
return self.makePersistent(entryPath, contents).then(() => { includeParentInfo: true,
return self.updateCache(entryPath); cache: true
}); }).then((pageData) => {
} else { return {
return Promise.reject(new Error('Entry does not exist!')); entryPath,
} meta: pageData.meta,
}).catch((err) => { parent: pageData.parent || {},
winston.error(err); text: mark.removeMarkdown(pageData.markdown)
return Promise.reject(new Error('Failed to save document.')); }
}); }).then((content) => {
return db.Entry.findOneAndUpdate({
}, _id: content.entryPath
}, {
/** _id: content.entryPath,
* Update local cache and search index title: content.meta.title || content.entryPath,
* subtitle: content.meta.subtitle || '',
* @param {String} entryPath The entry path parent: content.parent.title || '',
* @return {Promise} Promise of the operation content: content.text || ''
*/ }, {
updateCache(entryPath) { new: true,
upsert: true
let self = this; })
})
return self.fetchOriginal(entryPath, { },
parseMarkdown: true,
parseMeta: true, /**
parseTree: true, * Create a new document
includeMarkdown: true, *
includeParentInfo: true, * @param {String} entryPath The entry path
cache: true * @param {String} contents The markdown-formatted contents
}).then((pageData) => { * @return {Promise<Boolean>} True on success, false on failure
return { */
entryPath, create (entryPath, contents) {
meta: pageData.meta, let self = this
parent: pageData.parent || {},
text: mark.removeMarkdown(pageData.markdown) return self.exists(entryPath).then((docExists) => {
}; if (!docExists) {
}).then((content) => { return self.makePersistent(entryPath, contents).then(() => {
return db.Entry.findOneAndUpdate({ return self.updateCache(entryPath)
_id: content.entryPath })
}, { } else {
_id: content.entryPath, return Promise.reject(new Error('Entry already exists!'))
title: content.meta.title || content.entryPath, }
subtitle: content.meta.subtitle || '', }).catch((err) => {
parent: content.parent.title || '', winston.error(err)
content: content.text || '' return Promise.reject(new Error('Something went wrong.'))
}, { })
new: true, },
upsert: true
}); /**
}); * Makes a document persistent to disk and git repository
*
}, * @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
/** * @return {Promise<Boolean>} True on success, false on failure
* Create a new document */
* makePersistent (entryPath, contents) {
* @param {String} entryPath The entry path let self = this
* @param {String} contents The markdown-formatted contents let fpath = self.getFullPath(entryPath)
* @return {Promise<Boolean>} True on success, false on failure
*/ return fs.outputFileAsync(fpath, contents).then(() => {
create(entryPath, contents) { return git.commitDocument(entryPath)
})
let self = this; },
return self.exists(entryPath).then((docExists) => { /**
if(!docExists) { * Move a document
return self.makePersistent(entryPath, contents).then(() => { *
return self.updateCache(entryPath); * @param {String} entryPath The current entry path
}); * @param {String} newEntryPath The new entry path
} else { * @return {Promise} Promise of the operation
return Promise.reject(new Error('Entry already exists!')); */
} move (entryPath, newEntryPath) {
}).catch((err) => { let self = this
winston.error(err);
return Promise.reject(new Error('Something went wrong.')); if (_.isEmpty(entryPath) || entryPath === 'home') {
}); return Promise.reject(new Error('Invalid path!'))
}
},
return git.moveDocument(entryPath, newEntryPath).then(() => {
/** return git.commitDocument(newEntryPath).then(() => {
* Makes a document persistent to disk and git repository // Delete old cache version
*
* @param {String} entryPath The entry path let oldEntryCachePath = self.getCachePath(entryPath)
* @param {String} contents The markdown-formatted contents fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
* @return {Promise<Boolean>} True on success, false on failure
*/ // Delete old index entry
makePersistent(entryPath, contents) {
ws.emit('searchDel', {
let self = this; auth: WSInternalKey,
let fpath = self.getFullPath(entryPath); entryPath
})
return fs.outputFileAsync(fpath, contents).then(() => {
return git.commitDocument(entryPath); // Create cache for new entry
});
return self.updateCache(newEntryPath)
}, })
})
/** },
* Move a document
* /**
* @param {String} entryPath The current entry path * Generate a starter page content based on the entry path
* @param {String} newEntryPath The new entry path *
* @return {Promise} Promise of the operation * @param {String} entryPath The entry path
*/ * @return {Promise<String>} Starter content
move(entryPath, newEntryPath) { */
getStarter (entryPath) {
let self = this; let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))
if(_.isEmpty(entryPath) || entryPath === 'home') { return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {
return Promise.reject(new Error('Invalid path!')); return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle)
} })
},
return git.moveDocument(entryPath, newEntryPath).then(() => {
return git.commitDocument(newEntryPath).then(() => { /**
* Searches entries based on terms.
// Delete old cache version *
* @param {String} terms The terms to search for
let oldEntryCachePath = self.getCachePath(entryPath); * @return {Promise<Object>} Promise of the search results
fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true; }); */
search (terms) {
// Delete old index entry terms = _.chain(terms)
.deburr()
ws.emit('searchDel', { .toLower()
auth: WSInternalKey, .trim()
entryPath .replace(/[^a-z0-9\- ]/g, '')
}); .split(' ')
.filter((f) => { return !_.isEmpty(f) })
// Create cache for new entry .join(' ')
.value()
return self.updateCache(newEntryPath);
return db.Entry.find(
}); { $text: { $search: terms } },
}); { score: { $meta: 'textScore' }, title: 1 }
)
}, .sort({ score: { $meta: 'textScore' } })
.limit(10)
/** .exec()
* Generate a starter page content based on the entry path .then((hits) => {
* if (hits.length < 5) {
* @param {String} entryPath The entry path let regMatch = new RegExp('^' + _.split(terms, ' ')[0])
* @return {Promise<String>} Starter content return db.Entry.find({
*/ _id: { $regex: regMatch }
getStarter(entryPath) { }, '_id')
.sort('_id')
let self = this; .limit(5)
let formattedTitle = _.startCase(_.last(_.split(entryPath, '/'))); .exec()
.then((matches) => {
return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => { return {
return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle); match: hits,
}); suggest: (matches) ? _.map(matches, '_id') : []
}
}, })
} else {
/** return {
* Searches entries based on terms. match: _.filter(hits, (h) => { return h._doc.score >= 1 }),
* suggest: []
* @param {String} terms The terms to search for }
* @return {Promise<Object>} Promise of the search results }
*/ }).catch((err) => {
search(terms) { winston.error(err)
return {
let self = this; match: [],
terms = _.chain(terms) suggest: []
.deburr() }
.toLower() })
.trim() }
.replace(/[^a-z0-9\- ]/g, '')
.split(' ') }
.filter((f) => { return !_.isEmpty(f); })
.join(' ')
.value();
return db.Entry.find(
{ $text: { $search: terms } },
{ score: { $meta: "textScore" }, title: 1 }
)
.sort({ score: { $meta: "textScore" } })
.limit(10)
.exec()
.then((hits) => {
if(hits.length < 5) {
let regMatch = new RegExp('^' + _.split(terms, ' ')[0]);
return db.Entry.find({
_id: { $regex: regMatch }
}, '_id')
.sort('_id')
.limit(5)
.exec()
.then((matches) => {
return {
match: hits,
suggest: (matches) ? _.map(matches, '_id') : []
};
});
} else {
return {
match: _.filter(hits, (h) => { return h._doc.score >= 1; }),
suggest: []
};
}
}).catch((err) => {
winston.error(err);
return {
match: [],
suggest: []
};
});
}
};
+194 -221
View File
@@ -1,258 +1,231 @@
"use strict"; 'use strict'
var Git = require("git-wrapper2-promise"), const Git = require('git-wrapper2-promise')
Promise = require('bluebird'), const Promise = require('bluebird')
path = require('path'), const path = require('path')
os = require('os'), const fs = Promise.promisifyAll(require('fs'))
fs = Promise.promisifyAll(require("fs")), const _ = require('lodash')
moment = require('moment'), const URL = require('url')
_ = require('lodash'),
URL = require('url');
/** /**
* Git Model * Git Model
*/ */
module.exports = { module.exports = {
_git: null, _git: null,
_url: '', _url: '',
_repo: { _repo: {
path: '', path: '',
branch: 'master', branch: 'master',
exists: false exists: false
}, },
_signature: { _signature: {
name: 'Wiki', name: 'Wiki',
email: 'user@example.com' email: 'user@example.com'
}, },
_opts: { _opts: {
clone: {}, clone: {},
push: {} push: {}
}, },
onReady: null, onReady: null,
/** /**
* Initialize Git model * Initialize Git model
* *
* @return {Object} Git model instance * @return {Object} Git model instance
*/ */
init() { init () {
let self = this
let self = this; // -> Build repository path
//-> Build repository path if (_.isEmpty(appconfig.paths.repo)) {
self._repo.path = path.join(ROOTPATH, 'repo')
} else {
self._repo.path = appconfig.paths.repo
}
if(_.isEmpty(appconfig.paths.repo)) { // -> Initialize repository
self._repo.path = path.join(ROOTPATH, 'repo');
} else {
self._repo.path = appconfig.paths.repo;
}
//-> Initialize repository self.onReady = self._initRepo(appconfig)
self.onReady = self._initRepo(appconfig); // Define signature
// Define signature self._signature.name = appconfig.git.signature.name || 'Wiki'
self._signature.email = appconfig.git.signature.email || 'user@example.com'
self._signature.name = appconfig.git.signature.name || 'Wiki'; return self
self._signature.email = appconfig.git.signature.email || 'user@example.com'; },
return self; /**
* Initialize Git repository
*
* @param {Object} appconfig The application config
* @return {Object} Promise
*/
_initRepo (appconfig) {
let self = this
}, winston.info('[' + PROCNAME + '][GIT] Checking Git repository...')
/** // -> Check if path is accessible
* Initialize Git repository
*
* @param {Object} appconfig The application config
* @return {Object} Promise
*/
_initRepo(appconfig) {
let self = this; return fs.mkdirAsync(self._repo.path).catch((err) => {
if (err.code !== 'EEXIST') {
winston.error('[' + PROCNAME + '][GIT] Invalid Git repository path or missing permissions.')
}
}).then(() => {
self._git = new Git({ 'git-dir': self._repo.path })
winston.info('[' + PROCNAME + '][GIT] Checking Git repository...'); // -> Check if path already contains a git working folder
//-> Check if path is accessible return self._git.isRepo().then((isRepo) => {
self._repo.exists = isRepo
return (!isRepo) ? self._git.exec('init') : true
}).catch((err) => { // eslint-disable-line handle-callback-err
self._repo.exists = false
})
}).then(() => {
// Initialize remote
return fs.mkdirAsync(self._repo.path).catch((err) => { let urlObj = URL.parse(appconfig.git.url)
if(err.code !== 'EEXIST') { urlObj.auth = appconfig.git.auth.username + ((appconfig.git.auth.type !== 'ssh') ? ':' + appconfig.git.auth.password : '')
winston.error('[' + PROCNAME + '][GIT] Invalid Git repository path or missing permissions.'); self._url = URL.format(urlObj)
}
}).then(() => {
self._git = new Git({ 'git-dir': self._repo.path }); return self._git.exec('remote', 'show').then((cProc) => {
let out = cProc.stdout.toString()
if (_.includes(out, 'origin')) {
return true
} else {
return Promise.join(
self._git.exec('config', ['--local', 'user.name', self._signature.name]),
self._git.exec('config', ['--local', 'user.email', self._signature.email])
).then(() => {
return self._git.exec('remote', ['add', 'origin', self._url])
})
}
})
}).catch((err) => {
winston.error('[' + PROCNAME + '][GIT] Git remote error!')
throw err
}).then(() => {
winston.info('[' + PROCNAME + '][GIT] Git repository is OK.')
return true
})
},
//-> Check if path already contains a git working folder /**
* Gets the repo path.
*
* @return {String} The repo path.
*/
getRepoPath () {
return this._repo.path || path.join(ROOTPATH, 'repo')
},
return self._git.isRepo().then((isRepo) => { /**
self._repo.exists = isRepo; * Sync with the remote repository
return (!isRepo) ? self._git.exec('init') : true; *
}).catch((err) => { * @return {Promise} Resolve on sync success
self._repo.exists = false; */
}); resync () {
let self = this
}).then(() => { // Fetch
// Initialize remote winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...')
return self._git.pull('origin', self._repo.branch).then((cProc) => {
winston.info('[' + PROCNAME + '][GIT] Pull completed.')
})
.catch((err) => {
winston.error('[' + PROCNAME + '][GIT] Unable to fetch from git origin!')
throw err
})
.then(() => {
// Check for changes
let urlObj = URL.parse(appconfig.git.url); return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
urlObj.auth = appconfig.git.auth.username + ((appconfig.git.auth.type !== 'ssh') ? ':' + appconfig.git.auth.password : ''); let out = cProc.stdout.toString()
self._url = URL.format(urlObj);
return self._git.exec('remote', 'show').then((cProc) => { if (_.includes(out, 'commit')) {
let out = cProc.stdout.toString(); winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...')
if(_.includes(out, 'origin')) { return self._git.push('origin', self._repo.branch).then(() => {
return true; return winston.info('[' + PROCNAME + '][GIT] Push completed.')
} else { })
return Promise.join( } else {
self._git.exec('config', ['--local', 'user.name', self._signature.name]), winston.info('[' + PROCNAME + '][GIT] Push skipped. Repository is already in sync.')
self._git.exec('config', ['--local', 'user.email', self._signature.email]) }
).then(() => {
return self._git.exec('remote', ['add', 'origin', self._url]);
});
}
});
}).catch((err) => { return true
winston.error('[' + PROCNAME + '][GIT] Git remote error!'); })
throw err; })
}).then(() => { .catch((err) => {
winston.info('[' + PROCNAME + '][GIT] Git repository is OK.'); winston.error('[' + PROCNAME + '][GIT] Unable to push changes to remote!')
return true; throw err
}); })
},
}, /**
* Commits a document.
*
* @param {String} entryPath The entry path
* @return {Promise} Resolve on commit success
*/
commitDocument (entryPath) {
let self = this
let gitFilePath = entryPath + '.md'
let commitMsg = ''
/** return self._git.exec('ls-files', gitFilePath).then((cProc) => {
* Gets the repo path. let out = cProc.stdout.toString()
* return _.includes(out, gitFilePath)
* @return {String} The repo path. }).then((isTracked) => {
*/ commitMsg = (isTracked) ? 'Updated ' + gitFilePath : 'Added ' + gitFilePath
getRepoPath() { return self._git.add(gitFilePath)
}).then(() => {
return self._git.commit(commitMsg).catch((err) => {
if (_.includes(err.stdout, 'nothing to commit')) { return true }
})
})
},
return this._repo.path || path.join(ROOTPATH, 'repo'); /**
* Move a document.
*
* @param {String} entryPath The current entry path
* @param {String} newEntryPath The new entry path
* @return {Promise<Boolean>} Resolve on success
*/
moveDocument (entryPath, newEntryPath) {
let self = this
let gitFilePath = entryPath + '.md'
let gitNewFilePath = newEntryPath + '.md'
}, return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
let out = cProc.stdout.toString()
if (_.includes(out, 'fatal')) {
let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
throw new Error(errorMsg)
}
return true
})
},
/** /**
* Sync with the remote repository * Commits uploads changes.
* *
* @return {Promise} Resolve on sync success * @param {String} msg The commit message
*/ * @return {Promise} Resolve on commit success
resync() { */
commitUploads (msg) {
let self = this
msg = msg || 'Uploads repository sync'
let self = this; return self._git.add('uploads').then(() => {
return self._git.commit(msg).catch((err) => {
if (_.includes(err.stdout, 'nothing to commit')) { return true }
})
})
}
// Fetch }
winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...');
return self._git.pull('origin', self._repo.branch).then((cProc) => {
winston.info('[' + PROCNAME + '][GIT] Pull completed.');
})
.catch((err) => {
winston.error('[' + PROCNAME + '][GIT] Unable to fetch from git origin!');
throw err;
})
.then(() => {
// Check for changes
return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
let out = cProc.stdout.toString();
if(_.includes(out, 'commit')) {
winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...');
return self._git.push('origin', self._repo.branch).then(() => {
return winston.info('[' + PROCNAME + '][GIT] Push completed.');
});
} else {
winston.info('[' + PROCNAME + '][GIT] Push skipped. Repository is already in sync.');
}
return true;
});
})
.catch((err) => {
winston.error('[' + PROCNAME + '][GIT] Unable to push changes to remote!');
throw err;
});
},
/**
* Commits a document.
*
* @param {String} entryPath The entry path
* @return {Promise} Resolve on commit success
*/
commitDocument(entryPath) {
let self = this;
let gitFilePath = entryPath + '.md';
let commitMsg = '';
return self._git.exec('ls-files', gitFilePath).then((cProc) => {
let out = cProc.stdout.toString();
return _.includes(out, gitFilePath);
}).then((isTracked) => {
commitMsg = (isTracked) ? 'Updated ' + gitFilePath : 'Added ' + gitFilePath;
return self._git.add(gitFilePath);
}).then(() => {
return self._git.commit(commitMsg).catch((err) => {
if(_.includes(err.stdout, 'nothing to commit')) { return true; }
});
});
},
/**
* Move a document.
*
* @param {String} entryPath The current entry path
* @param {String} newEntryPath The new entry path
* @return {Promise<Boolean>} Resolve on success
*/
moveDocument(entryPath, newEntryPath) {
let self = this;
let gitFilePath = entryPath + '.md';
let gitNewFilePath = newEntryPath + '.md';
return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
let out = cProc.stdout.toString();
if(_.includes(out, 'fatal')) {
let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')));
throw new Error(errorMsg);
}
return true;
});
},
/**
* Commits uploads changes.
*
* @param {String} msg The commit message
* @return {Promise} Resolve on commit success
*/
commitUploads(msg) {
let self = this;
msg = msg || "Uploads repository sync";
return self._git.add('uploads').then(() => {
return self._git.commit(msg).catch((err) => {
if(_.includes(err.stdout, 'nothing to commit')) { return true; }
});
});
}
};
+14 -20
View File
@@ -1,32 +1,26 @@
"use strict"; 'use strict'
const crypto = require('crypto'); const crypto = require('crypto')
/** /**
* Internal Authentication * Internal Authentication
*/ */
module.exports = { module.exports = {
_curKey: false, _curKey: false,
init(inKey) { init (inKey) {
this._curKey = inKey
this._curKey = inKey; return this
},
return this; generateKey () {
return crypto.randomBytes(20).toString('hex')
},
}, validateKey (inKey) {
return inKey === this._curKey
}
generateKey() { }
return crypto.randomBytes(20).toString('hex');
},
validateKey(inKey) {
return inKey === this._curKey;
}
};
+139 -150
View File
@@ -1,187 +1,176 @@
"use strict"; 'use strict'
var path = require('path'), const path = require('path')
Promise = require('bluebird'), const Promise = require('bluebird')
fs = Promise.promisifyAll(require('fs-extra')), const fs = Promise.promisifyAll(require('fs-extra'))
multer = require('multer'), const multer = require('multer')
os = require('os'), const os = require('os')
_ = require('lodash'); const _ = require('lodash')
/** /**
* Local Data Storage * Local Data Storage
*/ */
module.exports = { module.exports = {
_uploadsPath: './repo/uploads', _uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs', _uploadsThumbsPath: './data/thumbs',
uploadImgHandler: null, uploadImgHandler: null,
/** /**
* Initialize Local Data Storage model * Initialize Local Data Storage model
* *
* @return {Object} Local Data Storage model instance * @return {Object} Local Data Storage model instance
*/ */
init() { init () {
this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads'); this.createBaseDirectories(appconfig)
this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs'); this.initMulter(appconfig)
this.createBaseDirectories(appconfig); return this
this.initMulter(appconfig); },
return this; /**
* Init Multer upload handlers
*
* @param {Object} appconfig The application config
* @return {boolean} Void
*/
initMulter (appconfig) {
let maxFileSizes = {
img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
}
}, // -> IMAGES
/** this.uploadImgHandler = multer({
* Init Multer upload handlers storage: multer.diskStorage({
* destination: (req, f, cb) => {
* @param {Object} appconfig The application config cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
* @return {boolean} Void }
*/ }),
initMulter(appconfig) { fileFilter: (req, f, cb) => {
// -> Check filesize
let maxFileSizes = { if (f.size > maxFileSizes.img) {
img: appconfig.uploads.maxImageFileSize * 1024 * 1024, return cb(null, false)
file: appconfig.uploads.maxOtherFileSize * 1024 * 1024 }
};
//-> IMAGES // -> Check MIME type (quick check only)
this.uploadImgHandler = multer({ if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) {
storage: multer.diskStorage({ return cb(null, false)
destination: (req, f, cb) => { }
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'));
}
}),
fileFilter: (req, f, cb) => {
//-> Check filesize cb(null, true)
}
}).array('imgfile', 20)
if(f.size > maxFileSizes.img) { // -> FILES
return cb(null, false);
}
//-> Check MIME type (quick check only) this.uploadFileHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
}
}),
fileFilter: (req, f, cb) => {
// -> Check filesize
if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) { if (f.size > maxFileSizes.file) {
return cb(null, false); return cb(null, false)
} }
cb(null, true); cb(null, true)
} }
}).array('imgfile', 20); }).array('binfile', 20)
//-> FILES return true
},
this.uploadFileHandler = multer({ /**
storage: multer.diskStorage({ * Creates a base directories (Synchronous).
destination: (req, f, cb) => { *
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload')); * @param {Object} appconfig The application config
} * @return {Void} Void
}), */
fileFilter: (req, f, cb) => { createBaseDirectories (appconfig) {
winston.info('[SERVER] Checking data directories...')
//-> Check filesize try {
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache'))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'))
if(f.size > maxFileSizes.file) { if (os.type() !== 'Windows_NT') {
return cb(null, false); fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '644')
} }
cb(null, true); fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo))
} fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'))
}).array('binfile', 20);
return true; if (os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './upload'), '644')
}
} catch (err) {
winston.error(err)
}
}, winston.info('[SERVER] Data and Repository directories are OK.')
/** return
* Creates a base directories (Synchronous). },
*
* @param {Object} appconfig The application config
* @return {Void} Void
*/
createBaseDirectories(appconfig) {
winston.info('[SERVER] Checking data directories...'); /**
* Gets the uploads path.
*
* @return {String} The uploads path.
*/
getUploadsPath () {
return this._uploadsPath
},
try { /**
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data)); * Gets the thumbnails folder path.
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache')); *
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs')); * @return {String} The thumbs path.
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload')); */
getThumbsPath () {
return this._uploadsThumbsPath
},
if(os.type() !== 'Windows_NT') { /**
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '644'); * Check if filename is valid and unique
} *
* @param {String} f The filename
* @param {String} fld The containing folder
* @param {boolean} isImage Indicates if image
* @return {Promise<String>} Promise of the accepted filename
*/
validateUploadsFilename (f, fld, isImage) {
let fObj = path.parse(f)
let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9-]+/g, '')
let fext = _.toLower(fObj.ext)
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo)); if (isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads')); fext = '.png'
}
if(os.type() !== 'Windows_NT') { f = fname + fext
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './upload'), '644'); let fpath = path.resolve(this._uploadsPath, fld, f)
}
} catch (err) { return fs.statAsync(fpath).then((s) => {
winston.error(err); throw new Error('File ' + f + ' already exists.')
} }).catch((err) => {
if (err.code === 'ENOENT') {
return f
}
throw err
})
}
winston.info('[SERVER] Data and Repository directories are OK.'); }
return;
},
/**
* Gets the uploads path.
*
* @return {String} The uploads path.
*/
getUploadsPath() {
return this._uploadsPath;
},
/**
* Gets the thumbnails folder path.
*
* @return {String} The thumbs path.
*/
getThumbsPath() {
return this._uploadsThumbsPath;
},
/**
* Check if filename is valid and unique
*
* @param {String} f The filename
* @param {String} fld The containing folder
* @param {boolean} isImage Indicates if image
* @return {Promise<String>} Promise of the accepted filename
*/
validateUploadsFilename(f, fld, isImage) {
let fObj = path.parse(f);
let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9\-]+/g, '');
let fext = _.toLower(fObj.ext);
if(isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
fext = '.png';
}
f = fname + fext;
let fpath = path.resolve(this._uploadsPath, fld, f);
return fs.statAsync(fpath).then((s) => {
throw new Error('File ' + f + ' already exists.');
}).catch((err) => {
if(err.code === 'ENOENT') {
return f;
}
throw err;
});
},
};
+247 -253
View File
@@ -1,86 +1,85 @@
"use strict"; 'use strict'
var Promise = require('bluebird'), const md = require('markdown-it')
md = require('markdown-it'), const mdEmoji = require('markdown-it-emoji')
mdEmoji = require('markdown-it-emoji'), const mdTaskLists = require('markdown-it-task-lists')
mdTaskLists = require('markdown-it-task-lists'), const mdAbbr = require('markdown-it-abbr')
mdAbbr = require('markdown-it-abbr'), const mdAnchor = require('markdown-it-anchor')
mdAnchor = require('markdown-it-anchor'), const mdFootnote = require('markdown-it-footnote')
mdFootnote = require('markdown-it-footnote'), const mdExternalLinks = require('markdown-it-external-links')
mdExternalLinks = require('markdown-it-external-links'), const mdExpandTabs = require('markdown-it-expand-tabs')
mdExpandTabs = require('markdown-it-expand-tabs'), const mdAttrs = require('markdown-it-attrs')
mdAttrs = require('markdown-it-attrs'), const hljs = require('highlight.js')
hljs = require('highlight.js'), const cheerio = require('cheerio')
cheerio = require('cheerio'), const _ = require('lodash')
_ = require('lodash'), const mdRemove = require('remove-markdown')
mdRemove = require('remove-markdown');
// Load plugins // Load plugins
var mkdown = md({ var mkdown = md({
html: true, html: true,
linkify: true, linkify: true,
typography: true, typography: true,
highlight(str, lang) { highlight (str, lang) {
if (lang && hljs.getLanguage(lang)) { if (lang && hljs.getLanguage(lang)) {
try { try {
return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'; return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'
} catch (err) { } catch (err) {
return '<pre><code>' + str + '</code></pre>'; return '<pre><code>' + str + '</code></pre>'
} }
} }
return '<pre><code>' + str + '</code></pre>'; return '<pre><code>' + str + '</code></pre>'
} }
}) })
.use(mdEmoji) .use(mdEmoji)
.use(mdTaskLists) .use(mdTaskLists)
.use(mdAbbr) .use(mdAbbr)
.use(mdAnchor, { .use(mdAnchor, {
slugify: _.kebabCase, slugify: _.kebabCase,
permalink: true, permalink: true,
permalinkClass: 'toc-anchor', permalinkClass: 'toc-anchor',
permalinkSymbol: '#', permalinkSymbol: '#',
permalinkBefore: true permalinkBefore: true
}) })
.use(mdFootnote) .use(mdFootnote)
.use(mdExternalLinks, { .use(mdExternalLinks, {
externalClassName: 'external-link', externalClassName: 'external-link',
internalClassName: 'internal-link' internalClassName: 'internal-link'
}) })
.use(mdExpandTabs, { .use(mdExpandTabs, {
tabWidth: 4 tabWidth: 4
}) })
.use(mdAttrs); .use(mdAttrs)
// Rendering rules // Rendering rules
mkdown.renderer.rules.emoji = function(token, idx) { mkdown.renderer.rules.emoji = function (token, idx) {
return '<i class="twa twa-' + _.replace(token[idx].markup, /_/g, '-') + '"></i>'; return '<i class="twa twa-' + _.replace(token[idx].markup, /_/g, '-') + '"></i>'
}; }
// Video rules // Video rules
const videoRules = [ const videoRules = [
{ {
selector: 'a.youtube', selector: 'a.youtube',
regexp: new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/, 'i'), regexp: new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/, 'i'),
output: '<iframe width="640" height="360" src="https://www.youtube.com/embed/{0}?rel=0" frameborder="0" allowfullscreen></iframe>' output: '<iframe width="640" height="360" src="https://www.youtube.com/embed/{0}?rel=0" frameborder="0" allowfullscreen></iframe>'
}, },
{ {
selector: 'a.vimeo', selector: 'a.vimeo',
regexp: new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'), regexp: new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'),
output: '<iframe src="https://player.vimeo.com/video/{0}" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>' output: '<iframe src="https://player.vimeo.com/video/{0}" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>'
}, },
{ {
selector: 'a.dailymotion', selector: 'a.dailymotion',
regexp: new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i'), regexp: new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i'),
output: '<iframe width="640" height="360" src="//www.dailymotion.com/embed/video/{0}?endscreen-enable=false" frameborder="0" allowfullscreen></iframe>' output: '<iframe width="640" height="360" src="//www.dailymotion.com/embed/video/{0}?endscreen-enable=false" frameborder="0" allowfullscreen></iframe>'
}, },
{ {
selector: 'a.video', selector: 'a.video',
regexp: false, regexp: false,
output: '<video width="640" height="360" controls preload="metadata"><source src="{0}" type="video/mp4"></video>' output: '<video width="640" height="360" controls preload="metadata"><source src="{0}" type="video/mp4"></video>'
} }
] ]
/** /**
@@ -90,81 +89,79 @@ const videoRules = [
* @return {Array} TOC tree * @return {Array} TOC tree
*/ */
const parseTree = (content) => { const parseTree = (content) => {
let tokens = md().parse(content, {})
let tocArray = []
let tokens = md().parse(content, {}); // -> Extract headings and their respective levels
let tocArray = [];
//-> Extract headings and their respective levels for (let i = 0; i < tokens.length; i++) {
if (tokens[i].type !== 'heading_close') {
continue
}
for (let i = 0; i < tokens.length; i++) { const heading = tokens[i - 1]
if (tokens[i].type !== "heading_close") { const headingclose = tokens[i]
continue;
}
const heading = tokens[i - 1]; if (heading.type === 'inline') {
const heading_close = tokens[i]; let content = ''
let anchor = ''
if (heading.children && heading.children[0].type === 'link_open') {
content = heading.children[1].content
anchor = _.kebabCase(content)
} else {
content = heading.content
anchor = _.kebabCase(heading.children.reduce((acc, t) => acc + t.content, ''))
}
if (heading.type === "inline") { tocArray.push({
let content = ""; content,
let anchor = ""; anchor,
if (heading.children && heading.children[0].type === "link_open") { level: +headingclose.tag.substr(1, 1)
content = heading.children[1].content; })
anchor = _.kebabCase(content); }
} else { }
content = heading.content;
anchor = _.kebabCase(heading.children.reduce((acc, t) => acc + t.content, ""));
}
tocArray.push({ // -> Exclude levels deeper than 2
content,
anchor,
level: +heading_close.tag.substr(1, 1)
});
}
}
//-> Exclude levels deeper than 2 _.remove(tocArray, (n) => { return n.level > 2 })
_.remove(tocArray, (n) => { return n.level > 2; }); // -> Build tree from flat array
//-> Build tree from flat array return _.reduce(tocArray, (tree, v) => {
let treeLength = tree.length - 1
return _.reduce(tocArray, (tree, v) => { if (v.level < 2) {
let treeLength = tree.length - 1; tree.push({
if(v.level < 2) { content: v.content,
tree.push({ anchor: v.anchor,
content: v.content, nodes: []
anchor: v.anchor, })
nodes: [] } else {
}); let lastNodeLevel = 1
} else { let GetNodePath = (startPos) => {
let lastNodeLevel = 1; lastNodeLevel++
let GetNodePath = (startPos) => { if (_.isEmpty(startPos)) {
lastNodeLevel++; startPos = 'nodes'
if(_.isEmpty(startPos)) { }
startPos = 'nodes'; if (lastNodeLevel === v.level) {
} return startPos
if(lastNodeLevel === v.level) { } else {
return startPos; return GetNodePath(startPos + '[' + (_.at(tree[treeLength], startPos).length - 1) + '].nodes')
} else { }
return GetNodePath(startPos + '[' + (_.at(tree[treeLength], startPos).length - 1) + '].nodes'); }
} let lastNodePath = GetNodePath()
}; let lastNode = _.get(tree[treeLength], lastNodePath)
let lastNodePath = GetNodePath(); if (lastNode) {
let lastNode = _.get(tree[treeLength], lastNodePath); lastNode.push({
if(lastNode) { content: v.content,
lastNode.push({ anchor: v.anchor,
content: v.content, nodes: []
anchor: v.anchor, })
nodes: [] _.set(tree[treeLength], lastNodePath, lastNode)
}); }
_.set(tree[treeLength], lastNodePath, lastNode); }
} return tree
} }, [])
return tree; }
}, []);
};
/** /**
* Parse markdown content to HTML * Parse markdown content to HTML
@@ -172,87 +169,85 @@ const parseTree = (content) => {
* @param {String} content Markdown content * @param {String} content Markdown content
* @return {String} HTML formatted content * @return {String} HTML formatted content
*/ */
const parseContent = (content) => { const parseContent = (content) => {
let output = mkdown.render(content)
let cr = cheerio.load(output)
let output = mkdown.render(content); // -> Check for empty first element
let cr = cheerio.load(output);
//-> Check for empty first element let firstElm = cr.root().children().first()[0]
if (firstElm.type === 'tag' && firstElm.name === 'p') {
let firstElmChildren = firstElm.children
if (firstElmChildren.length < 1) {
firstElm.remove()
} else if (firstElmChildren.length === 1 && firstElmChildren[0].type === 'tag' && firstElmChildren[0].name === 'img') {
cr(firstElm).addClass('is-gapless')
}
}
let firstElm = cr.root().children().first()[0]; // -> Remove links in headers
if(firstElm.type === 'tag' && firstElm.name === 'p') {
let firstElmChildren = firstElm.children;
if(firstElmChildren.length < 1) {
firstElm.remove();
} else if(firstElmChildren.length === 1 && firstElmChildren[0].type === 'tag' && firstElmChildren[0].name === 'img') {
cr(firstElm).addClass('is-gapless');
}
}
//-> Remove links in headers cr('h1 > a:not(.toc-anchor), h2 > a:not(.toc-anchor), h3 > a:not(.toc-anchor)').each((i, elm) => {
let txtLink = cr(elm).text()
cr(elm).replaceWith(txtLink)
})
cr('h1 > a:not(.toc-anchor), h2 > a:not(.toc-anchor), h3 > a:not(.toc-anchor)').each((i, elm) => { // -> Re-attach blockquote styling classes to their parents
let txtLink = cr(elm).text();
cr(elm).replaceWith(txtLink);
});
//-> Re-attach blockquote styling classes to their parents cr.root().children('blockquote').each((i, elm) => {
if (cr(elm).children().length > 0) {
let bqLastChild = cr(elm).children().last()[0]
let bqLastChildClasses = cr(bqLastChild).attr('class')
if (bqLastChildClasses && bqLastChildClasses.length > 0) {
cr(bqLastChild).removeAttr('class')
cr(elm).addClass(bqLastChildClasses)
}
}
})
cr.root().children('blockquote').each((i, elm) => { // -> Enclose content below headers
if(cr(elm).children().length > 0) {
let bqLastChild = cr(elm).children().last()[0];
let bqLastChildClasses = cr(bqLastChild).attr('class');
if(bqLastChildClasses && bqLastChildClasses.length > 0) {
cr(bqLastChild).removeAttr('class');
cr(elm).addClass(bqLastChildClasses);
}
}
});
//-> Enclose content below headers cr('h2').each((i, elm) => {
let subH2Content = cr(elm).nextUntil('h1, h2')
cr(elm).after('<div class="indent-h2"></div>')
let subH2Container = cr(elm).next('.indent-h2')
_.forEach(subH2Content, (ch) => {
cr(subH2Container).append(ch)
})
})
cr('h2').each((i, elm) => { cr('h3').each((i, elm) => {
let subH2Content = cr(elm).nextUntil('h1, h2'); let subH3Content = cr(elm).nextUntil('h1, h2, h3')
cr(elm).after('<div class="indent-h2"></div>'); cr(elm).after('<div class="indent-h3"></div>')
let subH2Container = cr(elm).next('.indent-h2'); let subH3Container = cr(elm).next('.indent-h3')
_.forEach(subH2Content, (ch) => { _.forEach(subH3Content, (ch) => {
cr(subH2Container).append(ch); cr(subH3Container).append(ch)
}); })
}); })
cr('h3').each((i, elm) => { // Replace video links with embeds
let subH3Content = cr(elm).nextUntil('h1, h2, h3');
cr(elm).after('<div class="indent-h3"></div>');
let subH3Container = cr(elm).next('.indent-h3');
_.forEach(subH3Content, (ch) => {
cr(subH3Container).append(ch);
});
});
// Replace video links with embeds _.forEach(videoRules, (vrule) => {
cr(vrule.selector).each((i, elm) => {
let originLink = cr(elm).attr('href')
if (vrule.regexp) {
let vidMatches = originLink.match(vrule.regexp)
if ((vidMatches && _.isArray(vidMatches))) {
vidMatches = _.filter(vidMatches, (f) => {
return f && _.isString(f)
})
originLink = _.last(vidMatches)
}
}
let processedLink = _.replace(vrule.output, '{0}', originLink)
cr(elm).replaceWith(processedLink)
})
})
_.forEach(videoRules, (vrule) => { output = cr.html()
cr(vrule.selector).each((i, elm) => {
let originLink = cr(elm).attr('href');
if(vrule.regexp) {
let vidMatches = originLink.match(vrule.regexp);
if((vidMatches && _.isArray(vidMatches))) {
vidMatches = _.filter(vidMatches, (f) => {
return f && _.isString(f);
});
originLink = _.last(vidMatches);
}
}
let processedLink = _.replace(vrule.output, '{0}', originLink);
cr(elm).replaceWith(processedLink);
});
});
output = cr.html(); return output
}
return output;
};
/** /**
* Parse meta-data tags from content * Parse meta-data tags from content
@@ -261,58 +256,57 @@ const parseContent = (content) => {
* @return {Object} Properties found in the content and their values * @return {Object} Properties found in the content and their values
*/ */
const parseMeta = (content) => { const parseMeta = (content) => {
let commentMeta = new RegExp('<!-- ?([a-zA-Z]+):(.*)-->', 'g')
let results = {}
let match
while ((match = commentMeta.exec(content)) !== null) {
results[_.toLower(match[1])] = _.trim(match[2])
}
let commentMeta = new RegExp('<!-- ?([a-zA-Z]+):(.*)-->','g'); return results
let results = {}, match; }
while(match = commentMeta.exec(content)) {
results[_.toLower(match[1])] = _.trim(match[2]);
}
return results;
};
module.exports = { module.exports = {
/** /**
* Parse content and return all data * Parse content and return all data
* *
* @param {String} content Markdown-formatted content * @param {String} content Markdown-formatted content
* @return {Object} Object containing meta, html and tree data * @return {Object} Object containing meta, html and tree data
*/ */
parse(content) { parse (content) {
return { return {
meta: parseMeta(content), meta: parseMeta(content),
html: parseContent(content), html: parseContent(content),
tree: parseTree(content) tree: parseTree(content)
}; }
}, },
parseContent, parseContent,
parseMeta, parseMeta,
parseTree, parseTree,
/** /**
* Strips non-text elements from Markdown content * Strips non-text elements from Markdown content
* *
* @param {String} content Markdown-formatted content * @param {String} content Markdown-formatted content
* @return {String} Text-only version * @return {String} Text-only version
*/ */
removeMarkdown(content) { removeMarkdown (content) {
return mdRemove(_.chain(content) return mdRemove(_.chain(content)
.replace(/<!-- ?([a-zA-Z]+):(.*)-->/g, '') .replace(/<!-- ?([a-zA-Z]+):(.*)-->/g, '')
.replace(/```[^`]+```/g, '') .replace(/```[^`]+```/g, '')
.replace(/`[^`]+`/g, '') .replace(/`[^`]+`/g, '')
.replace(new RegExp('(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?', 'g'), '') .replace(new RegExp('(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?', 'g'), '')
.replace(/\r?\n|\r/g, ' ') .replace(/\r?\n|\r/g, ' ')
.deburr() .deburr()
.toLower() .toLower()
.replace(/(\b([^a-z]+)\b)/g, ' ') .replace(/(\b([^a-z]+)\b)/g, ' ')
.replace(/[^a-z]+/g, ' ') .replace(/[^a-z]+/g, ' ')
.replace(/(\b(\w{1,2})\b(\W|$))/g, '') .replace(/(\b(\w{1,2})\b(\W|$))/g, '')
.replace(/\s\s+/g, ' ') .replace(/\s\s+/g, ' ')
.value() .value()
); )
} }
}; }
+248 -285
View File
@@ -1,292 +1,255 @@
"use strict"; 'use strict'
var path = require('path'), const path = require('path')
Promise = require('bluebird'), const Promise = require('bluebird')
fs = Promise.promisifyAll(require('fs-extra')), const fs = Promise.promisifyAll(require('fs-extra'))
readChunk = require('read-chunk'), const readChunk = require('read-chunk')
fileType = require('file-type'), const fileType = require('file-type')
mime = require('mime-types'), const mime = require('mime-types')
farmhash = require('farmhash'), const farmhash = require('farmhash')
moment = require('moment'), const chokidar = require('chokidar')
chokidar = require('chokidar'), const sharp = require('sharp')
sharp = require('sharp'), const _ = require('lodash')
_ = require('lodash');
/** /**
* Uploads - Agent * Uploads - Agent
*/ */
module.exports = { module.exports = {
_uploadsPath: './repo/uploads', _uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs', _uploadsThumbsPath: './data/thumbs',
_watcher: null, _watcher: null,
/** /**
* Initialize Uploads model * Initialize Uploads model
* *
* @return {Object} Uploads model instance * @return {Object} Uploads model instance
*/ */
init() { init () {
let self = this
let self = this;
self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads'); self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
// Disable Sharp cache, as it cause file locks issues when deleting uploads.
// Disable Sharp cache, as it cause file locks issues when deleting uploads. sharp.cache(false)
sharp.cache(false);
return self
return self; },
}, /**
* Watch the uploads folder for changes
/** *
* Watch the uploads folder for changes * @return {Void} Void
* */
* @return {Void} Void watch () {
*/ let self = this
watch() {
self._watcher = chokidar.watch(self._uploadsPath, {
let self = this; persistent: true,
ignoreInitial: true,
self._watcher = chokidar.watch(self._uploadsPath, { cwd: self._uploadsPath,
persistent: true, depth: 1,
ignoreInitial: true, awaitWriteFinish: true
cwd: self._uploadsPath, })
depth: 1,
awaitWriteFinish: true // -> Add new upload file
});
self._watcher.on('add', (p) => {
//-> Add new upload file let pInfo = self.parseUploadsRelPath(p)
return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
self._watcher.on('add', (p) => { return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
}).then(() => {
let pInfo = self.parseUploadsRelPath(p); return git.commitUploads('Uploaded ' + p)
return self.processFile(pInfo.folder, pInfo.filename).then((mData) => { })
return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true }); })
}).then(() => {
return git.commitUploads('Uploaded ' + p); // -> Remove upload file
});
self._watcher.on('unlink', (p) => {
}); return git.commitUploads('Deleted/Renamed ' + p)
})
//-> Remove upload file },
self._watcher.on('unlink', (p) => { /**
* Initial Uploads scan
let pInfo = self.parseUploadsRelPath(p); *
return git.commitUploads('Deleted/Renamed ' + p); * @return {Promise<Void>} Promise of the scan operation
*/
}); initialScan () {
let self = this
},
return fs.readdirAsync(self._uploadsPath).then((ls) => {
/** // Get all folders
* Initial Uploads scan
* return Promise.map(ls, (f) => {
* @return {Promise<Void>} Promise of the scan operation return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s } })
*/ }).filter((s) => { return s.stat.isDirectory() }).then((arrDirs) => {
initialScan() { let folderNames = _.map(arrDirs, 'filename')
folderNames.unshift('')
let self = this;
// Add folders to DB
return fs.readdirAsync(self._uploadsPath).then((ls) => {
return db.UplFolder.remove({}).then(() => {
// Get all folders return db.UplFolder.insertMany(_.map(folderNames, (f) => {
return {
return Promise.map(ls, (f) => { _id: 'f:' + f,
return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s }; }); name: f
}).filter((s) => { return s.stat.isDirectory(); }).then((arrDirs) => { }
}))
let folderNames = _.map(arrDirs, 'filename'); }).then(() => {
folderNames.unshift(''); // Travel each directory and scan files
// Add folders to DB let allFiles = []
return db.UplFolder.remove({}).then(() => { return Promise.map(folderNames, (fldName) => {
return db.UplFolder.insertMany(_.map(folderNames, (f) => { let fldPath = path.join(self._uploadsPath, fldName)
return { return fs.readdirAsync(fldPath).then((fList) => {
_id: 'f:' + f, return Promise.map(fList, (f) => {
name: f return upl.processFile(fldName, f).then((mData) => {
}; if (mData) {
})); allFiles.push(mData)
}).then(() => { }
return true
// Travel each directory and scan files })
}, {concurrency: 3})
let allFiles = []; })
}, {concurrency: 1}).finally(() => {
return Promise.map(folderNames, (fldName) => { // Add files to DB
let fldPath = path.join(self._uploadsPath, fldName); return db.UplFile.remove({}).then(() => {
return fs.readdirAsync(fldPath).then((fList) => { if (_.isArray(allFiles) && allFiles.length > 0) {
return Promise.map(fList, (f) => { return db.UplFile.insertMany(allFiles)
return upl.processFile(fldName, f).then((mData) => { } else {
if(mData) { return true
allFiles.push(mData); }
} })
return true; })
}); })
}, {concurrency: 3}); })
}); }).then(() => {
}, {concurrency: 1}).finally(() => { // Watch for new changes
// Add files to DB return upl.watch()
})
return db.UplFile.remove({}).then(() => { },
if(_.isArray(allFiles) && allFiles.length > 0) {
return db.UplFile.insertMany(allFiles); /**
} else { * Parse relative Uploads path
return true; *
} * @param {String} f Relative Uploads path
}); * @return {Object} Parsed path (folder and filename)
*/
}); parseUploadsRelPath (f) {
let fObj = path.parse(f)
}); return {
folder: fObj.dir,
}); filename: fObj.base
}
}).then(() => { },
// Watch for new changes /**
* Get metadata from file and generate thumbnails if necessary
return upl.watch(); *
* @param {String} fldName The folder name
}); * @param {String} f The filename
* @return {Promise<Object>} Promise of the file metadata
}, */
processFile (fldName, f) {
/** let self = this
* Parse relative Uploads path
* let fldPath = path.join(self._uploadsPath, fldName)
* @param {String} f Relative Uploads path let fPath = path.join(fldPath, f)
* @return {Object} Parsed path (folder and filename) let fPathObj = path.parse(fPath)
*/ let fUid = farmhash.fingerprint32(fldName + '/' + f)
parseUploadsRelPath(f) {
return fs.statAsync(fPath).then((s) => {
let fObj = path.parse(f); if (!s.isFile()) { return false }
return {
folder: fObj.dir, // Get MIME info
filename: fObj.base
}; let mimeInfo = fileType(readChunk.sync(fPath, 0, 262))
if (_.isNil(mimeInfo)) {
}, mimeInfo = {
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
/** }
* Get metadata from file and generate thumbnails if necessary }
*
* @param {String} fldName The folder name // Images
* @param {String} f The filename
* @return {Promise<Object>} Promise of the file metadata if (s.size < 3145728) { // ignore files larger than 3MB
*/ if (_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
processFile(fldName, f) { return self.getImageMetadata(fPath).then((mImgData) => {
let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png'))
let self = this; let cacheThumbnailPathStr = path.format(cacheThumbnailPath)
let fldPath = path.join(self._uploadsPath, fldName); let mData = {
let fPath = path.join(fldPath, f); _id: fUid,
let fPathObj = path.parse(fPath); category: 'image',
let fUid = farmhash.fingerprint32(fldName + '/' + f); mime: mimeInfo.mime,
extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
return fs.statAsync(fPath).then((s) => { folder: 'f:' + fldName,
filename: f,
if(!s.isFile()) { return false; } basename: fPathObj.name,
filesize: s.size
// Get MIME info }
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262)); // Generate thumbnail
if(_.isNil(mimeInfo)) {
mimeInfo = { return fs.statAsync(cacheThumbnailPathStr).then((st) => {
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream' return st.isFile()
}; }).catch((err) => { // eslint-disable-line handle-callback-err
} return false
}).then((thumbExists) => {
// Images return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => {
return self.generateThumbnail(fPath, cacheThumbnailPathStr)
if(s.size < 3145728) { // ignore files larger than 3MB }).return(mData)
if(_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) { })
return self.getImageMetadata(fPath).then((mImgData) => { })
}
let cacheThumbnailPath = path.parse(path.join(self._uploadsThumbsPath, fUid + '.png')); }
let cacheThumbnailPathStr = path.format(cacheThumbnailPath);
// Other Files
let mData = {
_id: fUid, return {
category: 'image', _id: fUid,
mime: mimeInfo.mime, category: 'binary',
extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']), mime: mimeInfo.mime,
folder: 'f:' + fldName, folder: 'f:' + fldName,
filename: f, filename: f,
basename: fPathObj.name, basename: fPathObj.name,
filesize: s.size filesize: s.size
}; }
})
// Generate thumbnail },
return fs.statAsync(cacheThumbnailPathStr).then((st) => { /**
return st.isFile(); * Generate thumbnail of image
}).catch((err) => { *
return false; * @param {String} sourcePath The source path
}).then((thumbExists) => { * @param {String} destPath The destination path
* @return {Promise<Object>} Promise returning the resized image info
return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => { */
return self.generateThumbnail(fPath, cacheThumbnailPathStr); generateThumbnail (sourcePath, destPath) {
}).return(mData); return sharp(sourcePath)
.withoutEnlargement()
}); .resize(150, 150)
.background('white')
}); .embed()
} .flatten()
} .toFormat('png')
.toFile(destPath)
// Other Files },
return { /**
_id: fUid, * Gets the image metadata.
category: 'binary', *
mime: mimeInfo.mime, * @param {String} sourcePath The source path
folder: 'f:' + fldName, * @return {Object} The image metadata.
filename: f, */
basename: fPathObj.name, getImageMetadata (sourcePath) {
filesize: s.size return sharp(sourcePath).metadata()
}; }
}); }
},
/**
* Generate thumbnail of image
*
* @param {String} sourcePath The source path
* @param {String} destPath The destination path
* @return {Promise<Object>} Promise returning the resized image info
*/
generateThumbnail(sourcePath, destPath) {
return sharp(sourcePath)
.withoutEnlargement()
.resize(150,150)
.background('white')
.embed()
.flatten()
.toFormat('png')
.toFile(destPath);
},
/**
* Gets the image metadata.
*
* @param {String} sourcePath The source path
* @return {Object} The image metadata.
*/
getImageMetadata(sourcePath) {
return sharp(sourcePath).metadata();
}
};
+235 -263
View File
@@ -1,308 +1,280 @@
"use strict"; 'use strict'
const path = require('path'), const path = require('path')
Promise = require('bluebird'), const Promise = require('bluebird')
fs = Promise.promisifyAll(require('fs-extra')), const fs = Promise.promisifyAll(require('fs-extra'))
multer = require('multer'), const request = require('request')
request = require('request'), const url = require('url')
url = require('url'), const farmhash = require('farmhash')
farmhash = require('farmhash'), const _ = require('lodash')
_ = require('lodash');
var regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$"); var regFolderName = new RegExp('^[a-z0-9][a-z0-9-]*[a-z0-9]$')
const maxDownloadFileSize = 3145728; // 3 MB const maxDownloadFileSize = 3145728 // 3 MB
/** /**
* Uploads * Uploads
*/ */
module.exports = { module.exports = {
_uploadsPath: './repo/uploads', _uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs', _uploadsThumbsPath: './data/thumbs',
/** /**
* Initialize Local Data Storage model * Initialize Local Data Storage model
* *
* @return {Object} Uploads model instance * @return {Object} Uploads model instance
*/ */
init() { init () {
this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads'); return this
this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs'); },
return this; /**
* Gets the thumbnails folder path.
*
* @return {String} The thumbs path.
*/
getThumbsPath () {
return this._uploadsThumbsPath
},
}, /**
* Gets the uploads folders.
*
* @return {Array<String>} The uploads folders.
*/
getUploadsFolders () {
return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => {
return (results) ? _.map(results, 'name') : [{ name: '' }]
})
},
/** /**
* Gets the thumbnails folder path. * Creates an uploads folder.
* *
* @return {String} The thumbs path. * @param {String} folderName The folder name
*/ * @return {Promise} Promise of the operation
getThumbsPath() { */
return this._uploadsThumbsPath; createUploadsFolder (folderName) {
}, let self = this
/** folderName = _.kebabCase(_.trim(folderName))
* Gets the uploads folders.
*
* @return {Array<String>} The uploads folders.
*/
getUploadsFolders() {
return db.UplFolder.find({}, 'name').sort('name').exec().then((results) => {
return (results) ? _.map(results, 'name') : [{ name: '' }];
});
},
/** if (_.isEmpty(folderName) || !regFolderName.test(folderName)) {
* Creates an uploads folder. return Promise.resolve(self.getUploadsFolders())
* }
* @param {String} folderName The folder name
* @return {Promise} Promise of the operation
*/
createUploadsFolder(folderName) {
let self = this; return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
return db.UplFolder.findOneAndUpdate({
_id: 'f:' + folderName
}, {
name: folderName
}, {
upsert: true
})
}).then(() => {
return self.getUploadsFolders()
})
},
folderName = _.kebabCase(_.trim(folderName)); /**
* Check if folder is valid and exists
*
* @param {String} folderName The folder name
* @return {Boolean} True if valid
*/
validateUploadsFolder (folderName) {
return db.UplFolder.findOne({ name: folderName }).then((f) => {
return (f) ? path.resolve(this._uploadsPath, folderName) : false
})
},
if(_.isEmpty(folderName) || !regFolderName.test(folderName)) { /**
return Promise.resolve(self.getUploadsFolders()); * Adds one or more uploads files.
} *
* @param {Array<Object>} arrFiles The uploads files
* @return {Void} Void
*/
addUploadsFiles (arrFiles) {
if (_.isArray(arrFiles) || _.isPlainObject(arrFiles)) {
// this._uploadsDb.Files.insert(arrFiles);
}
return
},
return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => { /**
return db.UplFolder.findOneAndUpdate({ * Gets the uploads files.
_id: 'f:' + folderName *
}, { * @param {String} cat Category type
name: folderName * @param {String} fld Folder
}, { * @return {Array<Object>} The files matching the query
upsert: true */
}); getUploadsFiles (cat, fld) {
}).then(() => { return db.UplFile.find({
return self.getUploadsFolders(); category: cat,
}); folder: 'f:' + fld
}).sort('filename').exec()
},
}, /**
* Deletes an uploads file.
*
* @param {string} uid The file unique ID
* @return {Promise} Promise of the operation
*/
deleteUploadsFile (uid) {
let self = this
/** return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
* Check if folder is valid and exists if (f) {
* return self.deleteUploadsFileTry(f, 0)
* @param {String} folderName The folder name }
* @return {Boolean} True if valid return true
*/ })
validateUploadsFolder(folderName) { },
return db.UplFolder.findOne({ name: folderName }).then((f) => { deleteUploadsFileTry (f, attempt) {
return (f) ? path.resolve(this._uploadsPath, folderName) : false; let self = this
});
}, let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'
/** return Promise.join(
* Adds one or more uploads files. fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
* fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
* @param {Array<Object>} arrFiles The uploads files ).catch((err) => {
* @return {Void} Void if (err.code === 'EBUSY' && attempt < 5) {
*/ return Promise.delay(100).then(() => {
addUploadsFiles(arrFiles) { return self.deleteUploadsFileTry(f, attempt + 1)
if(_.isArray(arrFiles) || _.isPlainObject(arrFiles)) { })
//this._uploadsDb.Files.insert(arrFiles); } else {
} winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.')
return; return true
}, }
})
},
/** /**
* Gets the uploads files. * Downloads a file from url.
* *
* @param {String} cat Category type * @param {String} fFolder The folder
* @param {String} fld Folder * @param {String} fUrl The full URL
* @return {Array<Object>} The files matching the query * @return {Promise} Promise of the operation
*/ */
getUploadsFiles(cat, fld) { downloadFromUrl (fFolder, fUrl) {
let fUrlObj = url.parse(fUrl)
let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'))
let destFolder = _.chain(fFolder).trim().toLower().value()
return db.UplFile.find({ return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
category: cat, if (!destFolderPath) {
folder: 'f:' + fld return Promise.reject(new Error('Invalid Folder'))
}).sort('filename').exec(); }
}, return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
let destFilePath = path.resolve(destFolderPath, destFilename)
/** return new Promise((resolve, reject) => {
* Deletes an uploads file. let rq = request({
* url: fUrl,
* @param {string} uid The file unique ID method: 'GET',
* @return {Promise} Promise of the operation followRedirect: true,
*/ maxRedirects: 5,
deleteUploadsFile(uid) { timeout: 10000
})
let self = this; let destFileStream = fs.createWriteStream(destFilePath)
let curFileSize = 0
return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => { rq.on('data', (data) => {
if(f) { curFileSize += data.length
return self.deleteUploadsFileTry(f, 0); if (curFileSize > maxDownloadFileSize) {
} rq.abort()
return true; destFileStream.destroy()
}); fs.remove(destFilePath)
}, reject(new Error('Remote file is too large!'))
}
}).on('error', (err) => {
destFileStream.destroy()
fs.remove(destFilePath)
reject(err)
})
deleteUploadsFileTry(f, attempt) { destFileStream.on('finish', () => {
resolve(true)
})
let self = this; rq.pipe(destFileStream)
})
})
})
},
let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'; /**
* Move/Rename a file
*
* @param {String} uid The file ID
* @param {String} fld The destination folder
* @param {String} nFilename The new filename (optional)
* @return {Promise} Promise of the operation
*/
moveUploadsFile (uid, fld, nFilename) {
let self = this
return Promise.join( return db.UplFolder.findById('f:' + fld).then((folder) => {
fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')), if (folder) {
fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename)) return db.UplFile.findById(uid).then((originFile) => {
).catch((err) => { // -> Check if rename is valid
if(err.code === 'EBUSY' && attempt < 5) {
return Promise.delay(100).then(() => {
return self.deleteUploadsFileTry(f, attempt + 1);
});
} else {
winston.warn('Unable to delete uploads file ' + f.filename + '. File is locked by another process and multiple attempts failed.');
return true;
}
});
}, let nameCheck = null
if (nFilename) {
let originFileObj = path.parse(originFile.filename)
nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name)
} else {
nameCheck = Promise.resolve(originFile.filename)
}
/** return nameCheck.then((destFilename) => {
* Downloads a file from url. let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './'
* let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename)
* @param {String} fFolder The folder let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename)
* @param {String} fUrl The full URL let preMoveOps = []
* @return {Promise} Promise of the operation
*/
downloadFromUrl(fFolder, fUrl) {
let self = this; // -> Check for invalid operations
let fUrlObj = url.parse(fUrl); if (sourceFilePath === destFilePath) {
let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/')); return Promise.reject(new Error('Invalid Operation!'))
let destFolder = _.chain(fFolder).trim().toLower().value(); }
return upl.validateUploadsFolder(destFolder).then((destFolderPath) => { // -> Delete DB entry
if(!destFolderPath) { preMoveOps.push(db.UplFile.findByIdAndRemove(uid))
return Promise.reject(new Error('Invalid Folder'));
}
return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => { // -> Move thumbnail ahead to avoid re-generation
let destFilePath = path.resolve(destFolderPath, destFilename); if (originFile.category === 'image') {
let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename)
let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png')
let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png')
preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath))
} else {
preMoveOps.push(Promise.resolve(true))
}
return new Promise((resolve, reject) => { // -> Proceed to move actual file
let rq = request({ return Promise.all(preMoveOps).then(() => {
url: fUrl, return fs.moveAsync(sourceFilePath, destFilePath, {
method: 'GET', clobber: false
followRedirect: true, })
maxRedirects: 5, })
timeout: 10000 })
}); })
} else {
return Promise.reject(new Error('Invalid Destination Folder'))
}
})
}
let destFileStream = fs.createWriteStream(destFilePath); }
let curFileSize = 0;
rq.on('data', (data) => {
curFileSize += data.length;
if(curFileSize > maxDownloadFileSize) {
rq.abort();
destFileStream.destroy();
fs.remove(destFilePath);
reject(new Error('Remote file is too large!'));
}
}).on('error', (err) => {
destFileStream.destroy();
fs.remove(destFilePath);
reject(err);
});
destFileStream.on('finish', () => {
resolve(true);
});
rq.pipe(destFileStream);
});
});
});
},
/**
* Move/Rename a file
*
* @param {String} uid The file ID
* @param {String} fld The destination folder
* @param {String} nFilename The new filename (optional)
* @return {Promise} Promise of the operation
*/
moveUploadsFile(uid, fld, nFilename) {
let self = this;
return db.UplFolder.findById('f:' + fld).then((folder) => {
if(folder) {
return db.UplFile.findById(uid).then((originFile) => {
//-> Check if rename is valid
let nameCheck = null;
if(nFilename) {
let originFileObj = path.parse(originFile.filename);
nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name);
} else {
nameCheck = Promise.resolve(originFile.filename);
}
return nameCheck.then((destFilename) => {
let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './';
let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename);
let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename);
let preMoveOps = [];
//-> Check for invalid operations
if(sourceFilePath === destFilePath) {
return Promise.reject(new Error('Invalid Operation!'));
}
//-> Delete DB entry
preMoveOps.push(db.UplFile.findByIdAndRemove(uid));
//-> Move thumbnail ahead to avoid re-generation
if(originFile.category === 'image') {
let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename);
let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png');
let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png');
preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath));
} else {
preMoveOps.push(Promise.resolve(true));
}
//-> Proceed to move actual file
return Promise.all(preMoveOps).then(() => {
return fs.moveAsync(sourceFilePath, destFilePath, {
clobber: false
});
});
})
});
} else {
return Promise.reject(new Error('Invalid Destination Folder'));
}
});
}
};
+18 -21
View File
@@ -1,7 +1,6 @@
"use strict"; 'use strict'
var Promise = require('bluebird'), const moment = require('moment-timezone')
moment = require('moment-timezone');
/** /**
* Authentication middleware * Authentication middleware
@@ -12,29 +11,27 @@ var Promise = require('bluebird'),
* @return {any} void * @return {any} void
*/ */
module.exports = (req, res, next) => { module.exports = (req, res, next) => {
// Is user authenticated ?
// Is user authenticated ? if (!req.isAuthenticated()) {
return res.redirect('/login')
}
if (!req.isAuthenticated()) { // Check permissions
return res.redirect('/login');
}
// Check permissions if (!rights.check(req, 'read')) {
return res.render('error-forbidden')
}
if(!rights.check(req, 'read')) { // Set i18n locale
return res.render('error-forbidden');
}
// Set i18n locale req.i18n.changeLanguage(req.user.lang)
res.locals.userMoment = moment
res.locals.userMoment.locale(req.user.lang)
req.i18n.changeLanguage(req.user.lang); // Expose user data
res.locals.userMoment = moment;
res.locals.userMoment.locale(req.user.lang);
// Expose user data res.locals.user = req.user
res.locals.user = req.user; return next()
}
return next();
};
+4 -6
View File
@@ -1,4 +1,4 @@
"use strict"; 'use strict'
/** /**
* Flash middleware * Flash middleware
@@ -9,9 +9,7 @@
* @return {any} void * @return {any} void
*/ */
module.exports = (req, res, next) => { module.exports = (req, res, next) => {
res.locals.appflash = req.flash('alert')
res.locals.appflash = req.flash('alert'); next()
}
next();
};
+15 -15
View File
@@ -1,3 +1,5 @@
'use strict'
/** /**
* Security Middleware * Security Middleware
* *
@@ -6,23 +8,21 @@
* @param {Function} next next callback function * @param {Function} next next callback function
* @return {any} void * @return {any} void
*/ */
module.exports = function(req, res, next) { module.exports = function (req, res, next) {
// -> Disable X-Powered-By
app.disable('x-powered-by')
//-> Disable X-Powered-By // -> Disable Frame Embedding
app.disable('x-powered-by'); res.set('X-Frame-Options', 'deny')
//-> Disable Frame Embedding // -> Re-enable XSS Fitler if disabled
res.set('X-Frame-Options', 'deny'); res.set('X-XSS-Protection', '1; mode=block')
//-> Re-enable XSS Fitler if disabled // -> Disable MIME-sniffing
res.set('X-XSS-Protection', '1; mode=block'); res.set('X-Content-Type-Options', 'nosniff')
//-> Disable MIME-sniffing // -> Disable IE Compatibility Mode
res.set('X-Content-Type-Options', 'nosniff'); res.set('X-UA-Compatible', 'IE=edge')
//-> Disable IE Compatibility Mode return next()
res.set('X-UA-Compatible', 'IE=edge'); }
return next();
};
+10 -10
View File
@@ -1,4 +1,4 @@
"use strict"; 'use strict'
/** /**
* BruteForce schema * BruteForce schema
@@ -6,13 +6,13 @@
* @type {<Mongoose.Schema>} * @type {<Mongoose.Schema>}
*/ */
var bruteForceSchema = Mongoose.Schema({ var bruteForceSchema = Mongoose.Schema({
_id: { type: String, index: 1 }, _id: { type: String, index: 1 },
data: { data: {
count: Number, count: Number,
lastRequest: Date, lastRequest: Date,
firstRequest: Date firstRequest: Date
}, },
expires: { type: Date, index: { expires: '1d' } } expires: { type: Date, index: { expires: '1d' } }
}); })
module.exports = Mongoose.model('Bruteforce', bruteForceSchema); module.exports = Mongoose.model('Bruteforce', bruteForceSchema)
+7 -10
View File
@@ -1,7 +1,4 @@
"use strict"; 'use strict'
const Promise = require('bluebird'),
_ = require('lodash');
/** /**
* Entry schema * Entry schema
@@ -10,7 +7,7 @@ const Promise = require('bluebird'),
*/ */
var entrySchema = Mongoose.Schema({ var entrySchema = Mongoose.Schema({
_id: String, _id: String,
title: { title: {
type: String, type: String,
@@ -31,9 +28,9 @@ var entrySchema = Mongoose.Schema({
} }
}, },
{ {
timestamps: {} timestamps: {}
}); })
entrySchema.index({ entrySchema.index({
_id: 'text', _id: 'text',
@@ -48,6 +45,6 @@ entrySchema.index({
content: 1 content: 1
}, },
name: 'EntriesTextIndex' name: 'EntriesTextIndex'
}); })
module.exports = Mongoose.model('Entry', entrySchema); module.exports = Mongoose.model('Entry', entrySchema)
+4 -10
View File
@@ -1,7 +1,4 @@
"use strict"; 'use strict'
const Promise = require('bluebird'),
_ = require('lodash');
/** /**
* Upload File schema * Upload File schema
@@ -10,7 +7,7 @@ const Promise = require('bluebird'),
*/ */
var uplFileSchema = Mongoose.Schema({ var uplFileSchema = Mongoose.Schema({
_id: String, _id: String,
category: { category: {
type: String, type: String,
@@ -42,9 +39,6 @@ var uplFileSchema = Mongoose.Schema({
required: true required: true
} }
}, }, { timestamps: {} })
{
timestamps: {}
});
module.exports = Mongoose.model('UplFile', uplFileSchema); module.exports = Mongoose.model('UplFile', uplFileSchema)
+4 -10
View File
@@ -1,7 +1,4 @@
"use strict"; 'use strict'
const Promise = require('bluebird'),
_ = require('lodash');
/** /**
* Upload Folder schema * Upload Folder schema
@@ -10,16 +7,13 @@ const Promise = require('bluebird'),
*/ */
var uplFolderSchema = Mongoose.Schema({ var uplFolderSchema = Mongoose.Schema({
_id: String, _id: String,
name: { name: {
type: String, type: String,
index: true index: true
} }
}, }, { timestamps: {} })
{
timestamps: {}
});
module.exports = Mongoose.model('UplFolder', uplFolderSchema); module.exports = Mongoose.model('UplFolder', uplFolderSchema)
+61 -66
View File
@@ -1,8 +1,8 @@
"use strict"; 'use strict'
const Promise = require('bluebird'), const Promise = require('bluebird')
bcrypt = require('bcryptjs-then'), const bcrypt = require('bcryptjs-then')
_ = require('lodash'); const _ = require('lodash')
/** /**
* Region schema * Region schema
@@ -11,78 +11,73 @@ const Promise = require('bluebird'),
*/ */
var userSchema = Mongoose.Schema({ var userSchema = Mongoose.Schema({
email: { email: {
type: String, type: String,
required: true, required: true,
index: true index: true
}, },
provider: { provider: {
type: String, type: String,
required: true required: true
}, },
providerId: { providerId: {
type: String type: String
}, },
password: { password: {
type: String type: String
}, },
name: { name: {
type: String type: String
}, },
rights: [{ rights: [{
role: String, role: String,
path: String, path: String,
exact: Boolean, exact: Boolean,
deny: Boolean deny: Boolean
}] }]
}, }, { timestamps: {} })
{
timestamps: {}
});
userSchema.statics.processProfile = (profile) => { userSchema.statics.processProfile = (profile) => {
let primaryEmail = ''
if (_.isArray(profile.emails)) {
let e = _.find(profile.emails, ['primary', true])
primaryEmail = (e) ? e.value : _.first(profile.emails).value
} else if (_.isString(profile.email) && profile.email.length > 5) {
primaryEmail = profile.email
} else {
return Promise.reject(new Error('Invalid User Email'))
}
let primaryEmail = ''; return db.User.findOneAndUpdate({
if(_.isArray(profile.emails)) { email: primaryEmail,
let e = _.find(profile.emails, ['primary', true]); provider: profile.provider
primaryEmail = (e) ? e.value : _.first(profile.emails).value; }, {
} else if(_.isString(profile.email) && profile.email.length > 5) { email: primaryEmail,
primaryEmail = profile.email; provider: profile.provider,
} else { providerId: profile.id,
return Promise.reject(new Error('Invalid User Email')); name: profile.displayName || _.split(primaryEmail, '@')[0]
} }, {
new: true,
return db.User.findOneAndUpdate({ upsert: true
email: primaryEmail, }).then((user) => {
provider: profile.provider return user || Promise.reject(new Error('User Upsert failed.'))
}, { })
email: primaryEmail, }
provider: profile.provider,
providerId: profile.id,
name: profile.displayName || _.split(primaryEmail, '@')[0]
}, {
new: true,
upsert: true
}).then((user) => {
return (user) ? user : Promise.reject(new Error('User Upsert failed.'));
});
};
userSchema.statics.hashPassword = (rawPwd) => { userSchema.statics.hashPassword = (rawPwd) => {
return bcrypt.hash(rawPwd); return bcrypt.hash(rawPwd)
}; }
userSchema.methods.validatePassword = function(rawPwd) { userSchema.methods.validatePassword = function (rawPwd) {
return bcrypt.compare(rawPwd, this.password).then((isValid) => { return bcrypt.compare(rawPwd, this.password).then((isValid) => {
return (isValid) ? true : Promise.reject(new Error('Invalid Login')); return (isValid) ? true : Promise.reject(new Error('Invalid Login'))
}); })
}; }
module.exports = Mongoose.model('User', userSchema); module.exports = Mongoose.model('User', userSchema)
+30
View File
@@ -129,5 +129,35 @@
"twemoji-awesome": "^1.0.4", "twemoji-awesome": "^1.0.4",
"vue": "^2.1.10" "vue": "^2.1.10"
}, },
"standard": {
"globals": [
"app",
"appconfig",
"appdata",
"bgAgent",
"db",
"entries",
"git",
"mark",
"lang",
"lcdata",
"rights",
"upl",
"winston",
"ws",
"Mongoose",
"CORE_PATH",
"ROOTPATH",
"IS_DEBUG",
"PROCNAME",
"WSInternalKey"
],
"ignore": [
"assets/**/*",
"data/**/*",
"node_modules/**/*",
"repo/**/*"
]
},
"snyk": true "snyk": true
} }
+107 -105
View File
@@ -1,122 +1,124 @@
"use strict"; 'use strict'
// =========================================== // ===========================================
// Wiki.js // Wiki.js
// 1.0.0 // 1.0.0
// Licensed under AGPLv3 // Licensed under AGPLv3
// =========================================== // ===========================================
global.PROCNAME = 'SERVER'; global.PROCNAME = 'SERVER'
global.ROOTPATH = __dirname; global.ROOTPATH = __dirname
global.IS_DEBUG = process.env.NODE_ENV === 'development'; global.IS_DEBUG = process.env.NODE_ENV === 'development'
if(IS_DEBUG) { if (IS_DEBUG) {
global.CORE_PATH = ROOTPATH + '/../core/'; global.CORE_PATH = ROOTPATH + '/../core/'
} else { } else {
global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/'; global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/'
} }
process.env.VIPS_WARNING = false
// ---------------------------------------- // ----------------------------------------
// Load Winston // Load Winston
// ---------------------------------------- // ----------------------------------------
global.winston = require(CORE_PATH + 'core-libs/winston')(IS_DEBUG); global.winston = require(CORE_PATH + 'core-libs/winston')(IS_DEBUG)
winston.info('[SERVER] Wiki.js is initializing...'); winston.info('[SERVER] Wiki.js is initializing...')
// ---------------------------------------- // ----------------------------------------
// Load global modules // Load global modules
// ---------------------------------------- // ----------------------------------------
let appconf = require(CORE_PATH + 'core-libs/config')(); let appconf = require(CORE_PATH + 'core-libs/config')()
global.appconfig = appconf.config; global.appconfig = appconf.config
global.appdata = appconf.data; global.appdata = appconf.data
global.lcdata = require('./libs/local').init(); global.lcdata = require('./libs/local').init()
global.db = require(CORE_PATH + 'core-libs/mongodb').init(); global.db = require(CORE_PATH + 'core-libs/mongodb').init()
global.entries = require('./libs/entries').init(); global.entries = require('./libs/entries').init()
global.git = require('./libs/git').init(false); global.git = require('./libs/git').init(false)
global.lang = require('i18next'); global.lang = require('i18next')
global.mark = require('./libs/markdown'); global.mark = require('./libs/markdown')
global.upl = require('./libs/uploads').init(); global.upl = require('./libs/uploads').init()
// ---------------------------------------- // ----------------------------------------
// Load modules // Load modules
// ---------------------------------------- // ----------------------------------------
const _ = require('lodash'); const autoload = require('auto-load')
const autoload = require('auto-load'); const bodyParser = require('body-parser')
const bodyParser = require('body-parser'); const compression = require('compression')
const compression = require('compression'); const cookieParser = require('cookie-parser')
const cookieParser = require('cookie-parser'); const express = require('express')
const express = require('express'); const favicon = require('serve-favicon')
const favicon = require('serve-favicon'); const flash = require('connect-flash')
const flash = require('connect-flash'); const fork = require('child_process').fork
const fork = require('child_process').fork; const http = require('http')
const http = require('http'); const i18nextBackend = require('i18next-node-fs-backend')
const i18next_backend = require('i18next-node-fs-backend'); const i18nextMw = require('i18next-express-middleware')
const i18next_mw = require('i18next-express-middleware'); const passport = require('passport')
const passport = require('passport'); const passportSocketIo = require('passport.socketio')
const passportSocketIo = require('passport.socketio'); const path = require('path')
const path = require('path'); const session = require('express-session')
const session = require('express-session'); const SessionMongoStore = require('connect-mongo')(session)
const sessionMongoStore = require('connect-mongo')(session); const socketio = require('socket.io')
const socketio = require('socket.io');
var mw = autoload(CORE_PATH + '/core-middlewares'); var mw = autoload(CORE_PATH + '/core-middlewares')
var ctrl = autoload(path.join(ROOTPATH, '/controllers')); var ctrl = autoload(path.join(ROOTPATH, '/controllers'))
var libInternalAuth = require('./libs/internalAuth'); var libInternalAuth = require('./libs/internalAuth')
global.WSInternalKey = libInternalAuth.generateKey(); global.WSInternalKey = libInternalAuth.generateKey()
// ---------------------------------------- // ----------------------------------------
// Define Express App // Define Express App
// ---------------------------------------- // ----------------------------------------
global.app = express(); global.app = express()
app.use(compression()); app.use(compression())
// ---------------------------------------- // ----------------------------------------
// Security // Security
// ---------------------------------------- // ----------------------------------------
app.use(mw.security); app.use(mw.security)
// ---------------------------------------- // ----------------------------------------
// Public Assets // Public Assets
// ---------------------------------------- // ----------------------------------------
app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico'))); app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')))
app.use(express.static(path.join(ROOTPATH, 'assets'))); app.use(express.static(path.join(ROOTPATH, 'assets')))
// ---------------------------------------- // ----------------------------------------
// Passport Authentication // Passport Authentication
// ---------------------------------------- // ----------------------------------------
var strategy = require(CORE_PATH + 'core-libs/auth')(passport); require(CORE_PATH + 'core-libs/auth')(passport)
global.rights = require(CORE_PATH + 'core-libs/rights'); global.rights = require(CORE_PATH + 'core-libs/rights')
rights.init(); rights.init()
var sessionStore = new sessionMongoStore({ var sessionStore = new SessionMongoStore({
mongooseConnection: db.connection, mongooseConnection: db.connection,
touchAfter: 15 touchAfter: 15
}); })
app.use(cookieParser()); app.use(cookieParser())
app.use(session({ app.use(session({
name: 'requarkswiki.sid', name: 'requarkswiki.sid',
store: sessionStore, store: sessionStore,
secret: appconfig.sessionSecret, secret: appconfig.sessionSecret,
resave: false, resave: false,
saveUninitialized: false saveUninitialized: false
})); }))
app.use(flash()); app.use(flash())
app.use(passport.initialize()); app.use(passport.initialize())
app.use(passport.session()); app.use(passport.session())
// ---------------------------------------- // ----------------------------------------
// Localization Engine // Localization Engine
// ---------------------------------------- // ----------------------------------------
lang lang
.use(i18next_backend) .use(i18nextBackend)
.use(i18next_mw.LanguageDetector) .use(i18nextMw.LanguageDetector)
.init({ .init({
load: 'languageOnly', load: 'languageOnly',
ns: ['common', 'auth'], ns: ['common', 'auth'],
@@ -124,94 +126,94 @@ lang
saveMissing: false, saveMissing: false,
supportedLngs: ['en', 'fr'], supportedLngs: ['en', 'fr'],
preload: ['en', 'fr'], preload: ['en', 'fr'],
fallbackLng : 'en', fallbackLng: 'en',
backend: { backend: {
loadPath: './locales/{{lng}}/{{ns}}.json' loadPath: './locales/{{lng}}/{{ns}}.json'
} }
}); })
// ---------------------------------------- // ----------------------------------------
// View Engine Setup // View Engine Setup
// ---------------------------------------- // ----------------------------------------
app.use(i18next_mw.handle(lang)); app.use(i18nextMw.handle(lang))
app.set('views', path.join(ROOTPATH, 'views')); app.set('views', path.join(ROOTPATH, 'views'))
app.set('view engine', 'pug'); app.set('view engine', 'pug')
app.use(bodyParser.json()); app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.urlencoded({ extended: false }))
// ---------------------------------------- // ----------------------------------------
// View accessible data // View accessible data
// ---------------------------------------- // ----------------------------------------
app.locals._ = require('lodash'); app.locals._ = require('lodash')
app.locals.moment = require('moment'); app.locals.moment = require('moment')
app.locals.appconfig = appconfig; app.locals.appconfig = appconfig
app.use(mw.flash); app.use(mw.flash)
// ---------------------------------------- // ----------------------------------------
// Controllers // Controllers
// ---------------------------------------- // ----------------------------------------
app.use('/', ctrl.auth); app.use('/', ctrl.auth)
app.use('/uploads', mw.auth, ctrl.uploads); app.use('/uploads', mw.auth, ctrl.uploads)
app.use('/admin', mw.auth, ctrl.admin); app.use('/admin', mw.auth, ctrl.admin)
app.use('/', mw.auth, ctrl.pages); app.use('/', mw.auth, ctrl.pages)
// ---------------------------------------- // ----------------------------------------
// Error handling // Error handling
// ---------------------------------------- // ----------------------------------------
app.use(function(req, res, next) { app.use(function (req, res, next) {
var err = new Error('Not Found'); var err = new Error('Not Found')
err.status = 404; err.status = 404
next(err); next(err)
}); })
app.use(function(err, req, res, next) { app.use(function (err, req, res, next) {
res.status(err.status || 500); res.status(err.status || 500)
res.render('error', { res.render('error', {
message: err.message, message: err.message,
error: IS_DEBUG ? err : {} error: IS_DEBUG ? err : {}
}); })
}); })
// ---------------------------------------- // ----------------------------------------
// Start HTTP server // Start HTTP server
// ---------------------------------------- // ----------------------------------------
winston.info('[SERVER] Starting HTTP/WS server on port ' + appconfig.port + '...'); winston.info('[SERVER] Starting HTTP/WS server on port ' + appconfig.port + '...')
app.set('port', appconfig.port); app.set('port', appconfig.port)
var server = http.createServer(app); var server = http.createServer(app)
var io = socketio(server); var io = socketio(server)
server.listen(appconfig.port); server.listen(appconfig.port)
server.on('error', (error) => { server.on('error', (error) => {
if (error.syscall !== 'listen') { if (error.syscall !== 'listen') {
throw error; throw error
} }
// handle specific listen errors with friendly messages // handle specific listen errors with friendly messages
switch (error.code) { switch (error.code) {
case 'EACCES': case 'EACCES':
console.error('Listening on port ' + appconfig.port + ' requires elevated privileges!'); console.error('Listening on port ' + appconfig.port + ' requires elevated privileges!')
process.exit(1); process.exit(1)
break; break
case 'EADDRINUSE': case 'EADDRINUSE':
console.error('Port ' + appconfig.port + ' is already in use!'); console.error('Port ' + appconfig.port + ' is already in use!')
process.exit(1); process.exit(1)
break; break
default: default:
throw error; throw error
} }
}); })
server.on('listening', () => { server.on('listening', () => {
winston.info('[SERVER] HTTP/WS server started successfully! [RUNNING]'); winston.info('[SERVER] HTTP/WS server started successfully! [RUNNING]')
}); })
// ---------------------------------------- // ----------------------------------------
// WebSocket // WebSocket
@@ -224,21 +226,21 @@ io.use(passportSocketIo.authorize({
passport, passport,
cookieParser, cookieParser,
success: (data, accept) => { success: (data, accept) => {
accept(); accept()
}, },
fail: (data, message, error, accept) => { fail: (data, message, error, accept) => {
return accept(new Error(message)); return accept(new Error(message))
} }
})); }))
io.on('connection', ctrl.ws); io.on('connection', ctrl.ws)
// ---------------------------------------- // ----------------------------------------
// Start child processes // Start child processes
// ---------------------------------------- // ----------------------------------------
global.bgAgent = fork('agent.js'); global.bgAgent = fork('agent.js')
process.on('exit', (code) => { process.on('exit', (code) => {
bgAgent.disconnect(); bgAgent.disconnect()
}); })
+3
View File
@@ -0,0 +1,3 @@
'use strict'
// TODO
-11
View File
@@ -1,11 +0,0 @@
"use strict";
let path = require('path'),
fs = require('fs');
// ========================================
// Load global modules
// ========================================
global._ = require('lodash');
global.winston = require('winston');
+10 -15
View File
@@ -19,20 +19,15 @@ html
// CSS // CSS
link(type='text/css', rel='stylesheet', href='/css/libs.css') link(type='text/css', rel='stylesheet', href='/css/libs.css')
link(type='text/css', rel='stylesheet', href='/css/app.css') link(type='text/css', rel='stylesheet', href='/css/error.css')
body(class='server-error') body(class='is-error')
section.hero.is-warning.is-fullheight .container
.hero-body a(href='/'): img(src='/favicons/android-icon-96x96.png')
.container h1= message
a(href='/'): img(src='/favicons/android-icon-96x96.png') h2 Oops, something went wrong
h1.title(style={ 'margin-top': '30px'})= message a.button.is-amber.is-inverted.is-featured(href='/') Go Home
h2.subtitle(style={ 'margin-bottom': '50px'}) Oops, something went wrong
a.button.is-warning.is-inverted(href='/') Go Home
if error.stack if error.stack
section.section h3 Detailed debug trail:
.container.is-fluid pre: code #{error.stack}
.content
h3 Detailed debug trail:
pre: code #{error.stack}
+5
View File
@@ -53,6 +53,11 @@ block content
a(href='/admin') a(href='/admin')
i.icon-head i.icon-head
span Account span Account
else
li
a(href='/login')
i.icon-unlock
span Login
aside.stickyscroll(data-margin-top=40) aside.stickyscroll(data-margin-top=40)
.sidebar-label .sidebar-label
i.icon-th-list i.icon-th-list