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
CHANGELOG.md Normal file
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

281
agent.js
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
cacheJobs.push( return fileStatus
fs.statAsync(cachePath).then((st) => { }).then((fileStatus) => {
return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active'; // -> Update cache and search index
}).catch((err) => {
return (err.code !== 'EEXIST') ? err : 'new';
}).then((fileStatus) => {
//-> Delete expired cache file if (fileStatus !== 'active') {
return entries.updateCache(entryPath)
}
if(fileStatus === 'expired') { return true
return fs.unlinkAsync(cachePath).return(fileStatus); })
} )
}
}).on('end', () => {
jobCbStreamDocsResolve(Promise.all(cacheJobs))
})
return fileStatus; return jobCbStreamDocs
})
}))
}).then((fileStatus) => { //* ****************************************
// -> Clear failed temporary upload files
//* ****************************************
//-> Update cache and search index jobs.push(
fs.readdirAsync(uploadsTempPath).then((ls) => {
let fifteenAgo = moment().subtract(15, 'minutes')
if(fileStatus !== 'active') { return Promise.map(ls, (f) => {
return entries.updateCache(entryPath); 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
}
})
})
})
)
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(() => {
}).on('end', () => { job.start()
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()
}); })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,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

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)
_.delay(() => {
self.mdl.children.splice(nAlertIdx, 1)
}, 500)
}
}
if(nAlertIdx >= 0 && nAlert) { }
nAlert.class += ' exit';
Vue.set(self.mdl.children, nAlertIdx, nAlert);
_.delay(() => {
self.mdl.children.splice(nAlertIdx, 1);
}, 500);
}
}
}

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();
}
}
});

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: () => {
let moveFolders = _.map(vueFile.folders, (f) => { attachContextMenus: () => {
return { let moveFolders = _.map(vueFile.folders, (f) => {
name: (f !== '') ? f : '/ (root)', return {
icon: 'fa-folder', name: (f !== '') ? f : '/ (root)',
callback: (key, opt) => { icon: 'fa-folder',
let moveFileId = _.toString($(opt.$trigger).data('uid')); callback: (key, opt) => {
let moveFileDestFolder = _.nth(vueFile.folders, key); let moveFileId = _.toString($(opt.$trigger).data('uid'))
vueFile.moveFile(moveFileId, moveFileDestFolder); let moveFileDestFolder = _.nth(vueFile.folders, key)
} vueFile.moveFile(moveFileId, moveFileDestFolder)
}; }
}); }
})
$.contextMenu('destroy', '.editor-modal-file-choices > figure'); $.contextMenu('destroy', '.editor-modal-file-choices > figure')
$.contextMenu({ $.contextMenu({
selector: '.editor-modal-file-choices > figure', selector: '.editor-modal-file-choices > figure',
appendTo: '.editor-modal-file-choices', appendTo: '.editor-modal-file-choices',
position: (opt, x, y) => { position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen'); $(opt.$trigger).addClass('is-contextopen')
let trigPos = $(opt.$trigger).position(); let trigPos = $(opt.$trigger).position()
let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 }; 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 }); opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w })
}, },
events: { events: {
hide: (opt) => { hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen'); $(opt.$trigger).removeClass('is-contextopen')
} }
}, },
items: { items: {
rename: { rename: {
name: "Rename", name: 'Rename',
icon: "fa-edit", icon: 'fa-edit',
callback: (key, opt) => { callback: (key, opt) => {
vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid); vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid)
vueFile.renameFile(); vueFile.renameFile()
} }
}, },
move: { move: {
name: "Move to...", name: 'Move to...',
icon: "fa-folder-open-o", icon: 'fa-folder-open-o',
items: moveFolders items: moveFolders
}, },
delete: { delete: {
name: "Delete", name: 'Delete',
icon: "fa-trash", icon: 'fa-trash',
callback: (key, opt) => { callback: (key, opt) => {
vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid); vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid)
vueFile.deleteFileWarn(true); 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;
}
}
});
});

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.fetchFromUrlDiscard(); vueImage.isLoadingText = 'Fetching image...'
vueImage.isLoadingText = 'Fetching image...'; vueImage.isLoading = true
vueImage.isLoading = true;
Vue.nextTick(() => { Vue.nextTick(() => {
socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => { socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => {
if(data.ok) { if (data.ok) {
vueImage.waitChangeComplete(vueImage.images.length, true); vueImage.waitChangeComplete(vueImage.images.length, true)
} else { } else {
vueImage.isLoading = false; vueImage.isLoading = false
alerts.pushError('Upload error', data.msg); 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: () => {
let moveFolders = _.map(vueImage.folders, (f) => { attachContextMenus: () => {
return { let moveFolders = _.map(vueImage.folders, (f) => {
name: (f !== '') ? f : '/ (root)', return {
icon: 'fa-folder', name: (f !== '') ? f : '/ (root)',
callback: (key, opt) => { icon: 'fa-folder',
let moveImageId = _.toString($(opt.$trigger).data('uid')); callback: (key, opt) => {
let moveImageDestFolder = _.nth(vueImage.folders, key); let moveImageId = _.toString($(opt.$trigger).data('uid'))
vueImage.moveImage(moveImageId, moveImageDestFolder); let moveImageDestFolder = _.nth(vueImage.folders, key)
} vueImage.moveImage(moveImageId, moveImageDestFolder)
}; }
}); }
})
$.contextMenu('destroy', '.editor-modal-image-choices > figure'); $.contextMenu('destroy', '.editor-modal-image-choices > figure')
$.contextMenu({ $.contextMenu({
selector: '.editor-modal-image-choices > figure', selector: '.editor-modal-image-choices > figure',
appendTo: '.editor-modal-image-choices', appendTo: '.editor-modal-image-choices',
position: (opt, x, y) => { position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen'); $(opt.$trigger).addClass('is-contextopen')
let trigPos = $(opt.$trigger).position(); let trigPos = $(opt.$trigger).position()
let trigDim = { w: $(opt.$trigger).width() / 2, h: $(opt.$trigger).height() / 2 }; 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 }); opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w })
}, },
events: { events: {
hide: (opt) => { hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen'); $(opt.$trigger).removeClass('is-contextopen')
} }
}, },
items: { items: {
rename: { rename: {
name: "Rename", name: 'Rename',
icon: "fa-edit", icon: 'fa-edit',
callback: (key, opt) => { callback: (key, opt) => {
vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid); vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid)
vueImage.renameImage(); vueImage.renameImage()
} }
}, },
move: { move: {
name: "Move to...", name: 'Move to...',
icon: "fa-folder-open-o", icon: 'fa-folder-open-o',
items: moveFolders items: moveFolders
}, },
delete: { delete: {
name: "Delete", name: 'Delete',
icon: "fa-trash", icon: 'fa-trash',
callback: (key, opt) => { callback: (key, opt) => {
vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid); vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid)
vueImage.deleteImageWarn(true); 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;
}
}
});
});

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()
}
} }
} })
});

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 $(window).bind('keydown', (ev) => {
if (ev.ctrlKey || ev.metaKey) {
let saveCurrentDocument = (ev) => { switch (String.fromCharCode(ev.which).toLowerCase()) {
$.ajax(window.location.href, { case 's':
data: { ev.preventDefault()
markdown: mde.value() saveCurrentDocument(ev)
}, break
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;
}
}
});
}

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: () => {
if (vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
vueHeader.searchmoveidx++
}
},
moveUpSearch: () => {
if (vueHeader.searchmoveidx > 0) {
vueHeader.searchmoveidx--
}
}
}
})
}, $('main').on('click', vueHeader.closeSearch)
moveDownSearch: () => { }
if(vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
vueHeader.searchmoveidx++;
}
},
moveUpSearch: () => {
if(vueHeader.searchmoveidx > 0) {
vueHeader.searchmoveidx--;
}
}
}
});
$('main').on('click', vueHeader.closeSearch);
}

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()
} }
} }

View File

@ -1,11 +1,9 @@
function makeSafePath(rawPath) { function makeSafePath (rawPath) {
let rawParts = _.split(_.trim(rawPath), '/')
rawParts = _.map(rawParts, (r) => {
return _.kebabCase(_.deburr(_.trim(r)))
})
let rawParts = _.split(_.trim(rawPath), '/'); return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/')
rawParts = _.map(rawParts, (r) => { }
return _.kebabCase(_.deburr(_.trim(r)));
});
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r); }), '/');
}

View File

@ -1,7 +1,5 @@
"use strict"; 'use strict'
jQuery( document ).ready(function( $ ) { jQuery(document).ready(function ($) {
$('#login-user').focus()
$('#login-user').focus(); })
});

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);

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)

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); }
} })
});

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.');
});
}
});

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'])
this.roleoverride = 'admin'
} else {
this.rights = usrData.rights
}
}
})
} else if($('#page-type-admin-users-edit').length) { // =include ../modals/admin-users-delete.js
}
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;
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
}

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) => { // =include ../components/editor.js
$('#modal-create-discard').toggleClass('is-active'); }
});
//=include ../components/editor.js
}

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) => { // =include ../components/editor.js
$('#modal-edit-discard').toggleClass('is-active'); }
});
//=include ../components/editor.js
}

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
}

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
}

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;

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

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) => { entries.exists(safePath).then((docExists) => {
if(!docExists) { if (!docExists) {
return entries.getStarter(safePath).then((contents) => { return entries.getStarter(safePath).then((contents) => {
let pageData = {
markdown: contents,
meta: {
title: _.startCase(safePath),
path: safePath
}
}
res.render('pages/create', { pageData })
let pageData = { return true
markdown: contents, }).catch((err) => {
meta: { winston.warn(err)
title: _.startCase(safePath), throw new Error('Could not load starter content!')
path: safePath })
} } else {
}; throw new Error('This entry already exists!')
res.render('pages/create', { pageData }); }
}).catch((err) => {
return true; res.render('error', {
message: err.message,
}).catch((err) => { error: {}
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;

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 = ''
if(!destFolderPath) { let destFilePath = ''
res.json({ ok: false, msg: 'Invalid Folder' });
return true;
}
Promise.map(req.files, (f) => { return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
destFilename = fname
destFilePath = path.resolve(destFolderPath, destFilename)
let destFilename = ''; return readChunk(f.path, 0, 262)
let destFilePath = ''; }).then((buf) => {
// -> Check MIME type by magic number
return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => { let mimeInfo = fileType(buf)
if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
destFilename = fname; return Promise.reject(new Error('Invalid file type.'))
destFilePath = path.resolve(destFolderPath, destFilename); }
return true
}).then(() => {
// -> Move file to final destination
return readChunk(f.path, 0, 262); return fs.moveAsync(f.path, destFilePath, { clobber: false })
}).then(() => {
}).then((buf) => { return {
ok: true,
//-> Check MIME type by magic number filename: destFilename,
filesize: f.size
let mimeInfo = fileType(buf); }
if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) { }).reflect()
return Promise.reject(new Error('Invalid file type.')); }, {concurrency: 3}).then((results) => {
} let uplResults = _.map(results, (r) => {
return true; if (r.isFulfilled()) {
return r.value()
}).then(() => { } else {
return {
//-> Move file to final destination ok: false,
msg: r.reason().message
return fs.moveAsync(f.path, destFilePath, { clobber: false }); }
}
}).then(() => { })
return { res.json({ ok: true, results: uplResults })
ok: true, return true
filename: destFilename, }).catch((err) => {
filesize: f.size res.json({ ok: false, msg: err.message })
}; return true
}).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 = ''
if(!destFolderPath) { let destFilePath = ''
res.json({ ok: false, msg: 'Invalid Folder' });
return true;
}
Promise.map(req.files, (f) => { return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
destFilename = fname
destFilePath = path.resolve(destFolderPath, destFilename)
let destFilename = ''; // -> Move file to final destination
let destFilePath = '';
return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => { return fs.moveAsync(f.path, destFilePath, { clobber: false })
}).then(() => {
destFilename = fname; return {
destFilePath = path.resolve(destFolderPath, destFilename); ok: true,
filename: destFilename,
//-> Move file to final destination filesize: f.size
}
return fs.moveAsync(f.path, destFilePath, { clobber: false }); }).reflect()
}, {concurrency: 3}).then((results) => {
}).then(() => { let uplResults = _.map(results, (r) => {
return { if (r.isFulfilled()) {
ok: true, return r.value()
filename: destFilename, } else {
filesize: f.size return {
}; ok: false,
}).reflect(); msg: r.reason().message
}
}, {concurrency: 3}).then((results) => { }
let uplResults = _.map(results, (r) => { })
if(r.isFulfilled()) { res.json({ ok: true, results: uplResults })
return r.value(); return true
} else { }).catch((err) => {
return { res.json({ ok: false, msg: err.message })
ok: false, return true
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;

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
}); })
}); })
}
};

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)
}); })

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: []
};
});
}
};

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')
if(_.isEmpty(appconfig.paths.repo)) { } else {
self._repo.path = path.join(ROOTPATH, 'repo'); self._repo.path = appconfig.paths.repo
} else { }
self._repo.path = appconfig.paths.repo;
}
//-> Initialize repository // -> 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.name = appconfig.git.signature.name || 'Wiki'
self._signature.email = appconfig.git.signature.email || 'user@example.com'; self._signature.email = appconfig.git.signature.email || 'user@example.com'
return self; 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...')
* Initialize Git repository
*
* @param {Object} appconfig The application config
* @return {Object} Promise
*/
_initRepo(appconfig) {
let self = this; // -> Check if path is accessible
winston.info('[' + PROCNAME + '][GIT] Checking Git repository...'); 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 })
//-> Check if path is accessible // -> Check if path already contains a git working folder
return fs.mkdirAsync(self._repo.path).catch((err) => { return self._git.isRepo().then((isRepo) => {
if(err.code !== 'EEXIST') { self._repo.exists = isRepo
winston.error('[' + PROCNAME + '][GIT] Invalid Git repository path or missing permissions.'); return (!isRepo) ? self._git.exec('init') : true
} }).catch((err) => { // eslint-disable-line handle-callback-err
}).then(() => { self._repo.exists = false
})
}).then(() => {
// Initialize remote
self._git = new Git({ 'git-dir': self._repo.path }); let urlObj = URL.parse(appconfig.git.url)
urlObj.auth = appconfig.git.auth.username + ((appconfig.git.auth.type !== 'ssh') ? ':' + appconfig.git.auth.password : '')
self._url = URL.format(urlObj)
//-> Check if path already contains a git working folder 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
})
},
return self._git.isRepo().then((isRepo) => { /**
self._repo.exists = isRepo; * Gets the repo path.
return (!isRepo) ? self._git.exec('init') : true; *
}).catch((err) => { * @return {String} The repo path.
self._repo.exists = false; */
}); getRepoPath () {
return this._repo.path || path.join(ROOTPATH, 'repo')
},
}).then(() => { /**
* Sync with the remote repository
*
* @return {Promise} Resolve on sync success
*/
resync () {
let self = this
// Initialize remote // Fetch
let urlObj = URL.parse(appconfig.git.url); winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...')
urlObj.auth = appconfig.git.auth.username + ((appconfig.git.auth.type !== 'ssh') ? ':' + appconfig.git.auth.password : ''); return self._git.pull('origin', self._repo.branch).then((cProc) => {
self._url = URL.format(urlObj); 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('remote', 'show').then((cProc) => { return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
let out = cProc.stdout.toString(); 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) => { if (_.includes(out, 'commit')) {
winston.error('[' + PROCNAME + '][GIT] Git remote error!'); winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...')
throw err; return self._git.push('origin', self._repo.branch).then(() => {
}).then(() => { return winston.info('[' + PROCNAME + '][GIT] Push completed.')
winston.info('[' + PROCNAME + '][GIT] Git repository is OK.'); })
return true; } 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
})
},
/** /**
* Gets the repo path. * Commits a document.
* *
* @return {String} The repo path. * @param {String} entryPath The entry path
*/ * @return {Promise} Resolve on commit success
getRepoPath() { */
commitDocument (entryPath) {
let self = this
let gitFilePath = entryPath + '.md'
let commitMsg = ''
return this._repo.path || path.join(ROOTPATH, 'repo'); 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) => {
* Sync with the remote repository let out = cProc.stdout.toString()
* if (_.includes(out, 'fatal')) {
* @return {Promise} Resolve on sync success let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
*/ throw new Error(errorMsg)
resync() { }
return true
})
},
let self = this; /**
* 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'
// Fetch return self._git.add('uploads').then(() => {
return self._git.commit(msg).catch((err) => {
if (_.includes(err.stdout, 'nothing to commit')) { return true }
})
})
}
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; }
});
});
}
};

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;
}
};

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;
});
},
};

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) {
cr.root().children('blockquote').each((i, elm) => { let bqLastChild = cr(elm).children().last()[0]
if(cr(elm).children().length > 0) { let bqLastChildClasses = cr(bqLastChild).attr('class')
let bqLastChild = cr(elm).children().last()[0]; if (bqLastChildClasses && bqLastChildClasses.length > 0) {
let bqLastChildClasses = cr(bqLastChild).attr('class'); cr(bqLastChild).removeAttr('class')
if(bqLastChildClasses && bqLastChildClasses.length > 0) { cr(elm).addClass(bqLastChildClasses)
cr(bqLastChild).removeAttr('class'); }
cr(elm).addClass(bqLastChildClasses); }
} })
}
});
//-> Enclose content below headers // -> Enclose content below headers
cr('h2').each((i, elm) => { cr('h2').each((i, elm) => {
let subH2Content = cr(elm).nextUntil('h1, h2'); let subH2Content = cr(elm).nextUntil('h1, h2')
cr(elm).after('<div class="indent-h2"></div>'); cr(elm).after('<div class="indent-h2"></div>')
let subH2Container = cr(elm).next('.indent-h2'); let subH2Container = cr(elm).next('.indent-h2')
_.forEach(subH2Content, (ch) => { _.forEach(subH2Content, (ch) => {
cr(subH2Container).append(ch); cr(subH2Container).append(ch)
}); })
}); })
cr('h3').each((i, elm) => { cr('h3').each((i, elm) => {
let subH3Content = cr(elm).nextUntil('h1, h2, h3'); let subH3Content = cr(elm).nextUntil('h1, h2, h3')
cr(elm).after('<div class="indent-h3"></div>'); cr(elm).after('<div class="indent-h3"></div>')
let subH3Container = cr(elm).next('.indent-h3'); let subH3Container = cr(elm).next('.indent-h3')
_.forEach(subH3Content, (ch) => { _.forEach(subH3Content, (ch) => {
cr(subH3Container).append(ch); cr(subH3Container).append(ch)
}); })
}); })
// Replace video links with embeds // Replace video links with embeds
_.forEach(videoRules, (vrule) => { _.forEach(videoRules, (vrule) => {
cr(vrule.selector).each((i, elm) => { cr(vrule.selector).each((i, elm) => {
let originLink = cr(elm).attr('href'); let originLink = cr(elm).attr('href')
if(vrule.regexp) { if (vrule.regexp) {
let vidMatches = originLink.match(vrule.regexp); let vidMatches = originLink.match(vrule.regexp)
if((vidMatches && _.isArray(vidMatches))) { if ((vidMatches && _.isArray(vidMatches))) {
vidMatches = _.filter(vidMatches, (f) => { vidMatches = _.filter(vidMatches, (f) => {
return f && _.isString(f); return f && _.isString(f)
}); })
originLink = _.last(vidMatches); originLink = _.last(vidMatches)
} }
} }
let processedLink = _.replace(vrule.output, '{0}', originLink); let processedLink = _.replace(vrule.output, '{0}', originLink)
cr(elm).replaceWith(processedLink); cr(elm).replaceWith(processedLink)
}); })
}); })
output = cr.html(); 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()
); )
} }
}; }

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();
}
};

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) {
return Promise.reject(new Error('Invalid Folder'));
}
return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => { preMoveOps.push(db.UplFile.findByIdAndRemove(uid))
let destFilePath = path.resolve(destFolderPath, destFilename);
return new Promise((resolve, reject) => { // -> Move thumbnail ahead to avoid re-generation
let rq = request({ if (originFile.category === 'image') {
url: fUrl, let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename)
method: 'GET', let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png')
followRedirect: true, let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png')
maxRedirects: 5, preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath))
timeout: 10000 } else {
}); preMoveOps.push(Promise.resolve(true))
}
let destFileStream = fs.createWriteStream(destFilePath); // -> Proceed to move actual file
let curFileSize = 0;
rq.on('data', (data) => { return Promise.all(preMoveOps).then(() => {
curFileSize += data.length; return fs.moveAsync(sourceFilePath, destFilePath, {
if(curFileSize > maxDownloadFileSize) { clobber: false
rq.abort(); })
destFileStream.destroy(); })
fs.remove(destFilePath); })
reject(new Error('Remote file is too large!')); })
} } else {
}).on('error', (err) => { return Promise.reject(new Error('Invalid Destination Folder'))
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'));
}
});
}
};

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();
};

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();
};

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();
};

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)

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)

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)

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)

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)

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
} }

212
server.js
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
test/index.js Normal file
View File

@ -0,0 +1,3 @@
'use strict'
// TODO

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');

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}

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