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
// ===========================================
global.PROCNAME = 'AGENT';
global.ROOTPATH = __dirname;
global.IS_DEBUG = process.env.NODE_ENV === 'development';
if(IS_DEBUG) {
global.CORE_PATH = ROOTPATH + '/../core/';
global.PROCNAME = 'AGENT'
global.ROOTPATH = __dirname
global.IS_DEBUG = process.env.NODE_ENV === 'development'
if (IS_DEBUG) {
global.CORE_PATH = ROOTPATH + '/../core/'
} else {
global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/';
global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/'
}
// ----------------------------------------
// 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
// ----------------------------------------
winston.info('[AGENT] Background Agent is initializing...');
winston.info('[AGENT] Background Agent is initializing...')
let appconf = require(CORE_PATH + 'core-libs/config')();
global.appconfig = appconf.config;
global.appdata = appconf.data;
global.db = require(CORE_PATH + 'core-libs/mongodb').init();
global.upl = require('./libs/uploads-agent').init();
global.git = require('./libs/git').init();
global.entries = require('./libs/entries').init();
global.mark = require('./libs/markdown');
let appconf = require(CORE_PATH + 'core-libs/config')()
global.appconfig = appconf.config
global.appdata = appconf.data
global.db = require(CORE_PATH + 'core-libs/mongodb').init()
global.upl = require('./libs/uploads-agent').init()
global.git = require('./libs/git').init()
global.entries = require('./libs/entries').init()
global.mark = require('./libs/markdown')
// ----------------------------------------
// Load modules
// ----------------------------------------
var _ = require('lodash');
var moment = require('moment');
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require("fs-extra"));
var klaw = require('klaw');
var path = require('path');
var cron = require('cron').CronJob;
var moment = require('moment')
var Promise = require('bluebird')
var fs = Promise.promisifyAll(require('fs-extra'))
var klaw = require('klaw')
var path = require('path')
var Cron = require('cron').CronJob
// ----------------------------------------
// Start Cron
// ----------------------------------------
var jobIsBusy = false;
var jobUplWatchStarted = false;
var jobIsBusy = false
var jobUplWatchStarted = false
var job = new cron({
cronTime: '0 */5 * * * *',
onTick: () => {
var job = new Cron({
cronTime: '0 */5 * * * *',
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) {
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
// 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);
let dataPath = path.resolve(ROOTPATH, appconfig.paths.data);
let uploadsPath = path.join(repoPath, 'uploads');
let uploadsTempPath = path.join(dataPath, 'temp-upload');
// ----------------------------------------
// REGULAR JOBS
// ----------------------------------------
// ----------------------------------------
// REGULAR JOBS
// ----------------------------------------
//* ****************************************
// -> Sync with Git remote
//* ****************************************
//*****************************************
//-> Sync with Git remote
//*****************************************
jobs.push(git.onReady.then(() => {
return git.resync().then(() => {
// -> Stream all documents
jobs.push(git.onReady.then(() => {
return git.resync().then(() => {
let cacheJobs = []
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 = [];
let jobCbStreamDocs_resolve = null,
jobCbStreamDocs = new Promise((resolve, reject) => {
jobCbStreamDocs_resolve = resolve;
});
// -> Purge outdated cache
klaw(repoPath).on('data', function (item) {
if(path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') {
cacheJobs.push(
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));
let cachePath = entries.getCachePath(entryPath);
//-> Purge outdated cache
if (fileStatus === 'expired') {
return fs.unlinkAsync(cachePath).return(fileStatus)
}
cacheJobs.push(
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) => {
return fileStatus
}).then((fileStatus) => {
// -> Update cache and search index
//-> Delete expired cache file
if (fileStatus !== 'active') {
return entries.updateCache(entryPath)
}
if(fileStatus === 'expired') {
return fs.unlinkAsync(cachePath).return(fileStatus);
}
return true
})
)
}
}).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 entries.updateCache(entryPath);
}
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
}
})
})
})
)
return true;
// ----------------------------------------
// Run
// ----------------------------------------
})
Promise.all(jobs).then(() => {
winston.info('[AGENT] All jobs completed successfully! Going to sleep for now.')
);
}
}).on('end', () => {
jobCbStreamDocs_resolve(Promise.all(cacheJobs));
});
return jobCbStreamDocs;
});
}));
//*****************************************
//-> Clear failed temporary upload files
//*****************************************
jobs.push(
fs.readdirAsync(uploadsTempPath).then((ls) => {
let fifteenAgo = moment().subtract(15, 'minutes');
return Promise.map(ls, (f) => {
return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s }; });
}).filter((s) => { return s.stat.isFile(); }).then((arrFiles) => {
return Promise.map(arrFiles, (f) => {
if(moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
return fs.unlinkAsync(path.join(uploadsTempPath, f.filename));
} else {
return true;
}
});
});
})
);
// ----------------------------------------
// Run
// ----------------------------------------
Promise.all(jobs).then(() => {
winston.info('[AGENT] All jobs completed successfully! Going to sleep for now.');
if(!jobUplWatchStarted) {
jobUplWatchStarted = true;
upl.initialScan().then(() => {
job.start();
});
}
return true;
}).catch((err) => {
winston.error('[AGENT] One or more jobs have failed: ', err);
}).finally(() => {
jobIsBusy = false;
});
},
start: false,
timeZone: 'UTC',
runOnInit: true
});
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
})
// ----------------------------------------
// Shutdown gracefully
// ----------------------------------------
process.on('disconnect', () => {
winston.warn('[AGENT] Lost connection to main server. Exiting...');
job.stop();
process.exit();
});
winston.warn('[AGENT] Lost connection to main server. Exiting...')
job.stop()
process.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";
jQuery( document ).ready(function( $ ) {
'use strict'
jQuery(document).ready(function ($) {
// ====================================
// Scroll
// ====================================
$('a').smoothScroll({
speed: 400,
offset: -70
});
$('a').smoothScroll({
speed: 400,
offset: -70
})
var sticky = new Sticky('.stickyscroll');
var sticky = new Sticky('.stickyscroll')
// ====================================
// Notifications
// ====================================
$(window).bind('beforeunload', () => {
$('#notifload').addClass('active');
});
$(document).ajaxSend(() => {
$('#notifload').addClass('active');
}).ajaxComplete(() => {
$('#notifload').removeClass('active');
});
$(window).bind('beforeunload', () => {
$('#notifload').addClass('active')
})
$(document).ajaxSend(() => {
$('#notifload').addClass('active')
}).ajaxComplete(() => {
$('#notifload').removeClass('active')
})
var alerts = new Alerts();
if(alertsData) {
_.forEach(alertsData, (alertRow) => {
alerts.push(alertRow);
});
}
var alerts = new Alerts()
if (alertsData) {
_.forEach(alertsData, (alertRow) => {
alerts.push(alertRow)
})
}
// ====================================
// 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
// ====================================
//=include pages/view.js
//=include pages/create.js
//=include pages/edit.js
//=include pages/source.js
//=include pages/admin.js
// =include pages/view.js
// =include pages/create.js
// =include pages/edit.js
// =include pages/source.js
// =include pages/admin.js
})
});
// =include helpers/form.js
// =include helpers/pages.js
//=include helpers/form.js
//=include helpers/pages.js
//=include components/alerts.js
// =include components/alerts.js

View File

@ -1,4 +1,4 @@
"use strict";
'use strict'
/**
* Alerts
@ -10,25 +10,23 @@ class Alerts {
*
* @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({
el: '#alerts',
data: {
children: []
},
methods: {
acknowledge: (uid) => {
self.close(uid);
}
}
});
self.uidNext = 1;
}
self.uidNext = 1
}
/**
* Show a new Alert
@ -36,29 +34,27 @@ class Alerts {
* @param {Object} options Alert properties
* @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, {
_uid: self.uidNext,
class: 'info',
message: '---',
sticky: false,
title: '---'
});
self.mdl.children.push(nAlert)
self.mdl.children.push(nAlert);
if (!nAlert.sticky) {
_.delay(() => {
self.close(nAlert._uid)
}, 5000)
}
if(!nAlert.sticky) {
_.delay(() => {
self.close(nAlert._uid);
}, 5000);
}
self.uidNext++;
}
self.uidNext++
}
/**
* Shorthand method for pushing errors
@ -66,14 +62,14 @@ class Alerts {
* @param {String} title The title
* @param {String} message The message
*/
pushError(title, message) {
this.push({
class: 'error',
message,
sticky: false,
title
});
}
pushError (title, message) {
this.push({
class: 'error',
message,
sticky: false,
title
})
}
/**
* Shorthand method for pushing success messages
@ -81,35 +77,33 @@ class Alerts {
* @param {String} title The title
* @param {String} message The message
*/
pushSuccess(title, message) {
this.push({
class: 'success',
message,
sticky: false,
title
});
}
pushSuccess (title, message) {
this.push({
class: 'success',
message,
sticky: false,
title
})
}
/**
* Close an 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]);
let nAlert = _.nth(self.mdl.children, nAlertIdx);
if (nAlertIdx >= 0 && nAlert) {
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 codeEditor = null;
let modelist = ace.require('ace/ext/modelist')
let codeEditor = null
// ACE - Mode Loader
let modelistLoaded = [];
let modelistLoaded = []
let loadAceMode = (m) => {
return $.ajax({
url: '/js/ace/mode-' + m + '.js',
dataType: "script",
cache: true,
beforeSend: () => {
if(_.includes(modelistLoaded, m)) {
return false;
}
},
success: () => {
modelistLoaded.push(m);
}
});
};
return $.ajax({
url: '/js/ace/mode-' + m + '.js',
dataType: 'script',
cache: true,
beforeSend: () => {
if (_.includes(modelistLoaded, m)) {
return false
}
},
success: () => {
modelistLoaded.push(m)
}
})
}
// Vue Code Block instance
let vueCodeBlock = new Vue({
el: '#modal-editor-codeblock',
data: {
modes: modelist.modesByName,
modeSelected: 'text',
initContent: ''
},
watch: {
modeSelected: (val, oldVal) => {
loadAceMode(val).done(() => {
ace.require("ace/mode/" + val);
codeEditor.getSession().setMode("ace/mode/" + val);
});
}
},
methods: {
open: (ev) => {
el: '#modal-editor-codeblock',
data: {
modes: modelist.modesByName,
modeSelected: 'text',
initContent: ''
},
watch: {
modeSelected: (val, oldVal) => {
loadAceMode(val).done(() => {
ace.require('ace/mode/' + val)
codeEditor.getSession().setMode('ace/mode/' + val)
})
}
},
methods: {
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 = 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.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();
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';
mde.codemirror.doc.replaceSelection(codeBlockText);
vueCodeBlock.cancel();
}
}
});
mde.codemirror.doc.replaceSelection(codeBlockText)
vueCodeBlock.cancel()
}
}
})

View File

@ -1,365 +1,351 @@
let vueFile = new Vue({
el: '#modal-editor-file',
data: {
isLoading: false,
isLoadingText: '',
newFolderName: '',
newFolderShow: false,
newFolderError: false,
folders: [],
currentFolder: '',
currentFile: '',
files: [],
uploadSucceeded: false,
postUploadChecks: 0,
renameFileShow: false,
renameFileId: '',
renameFileFilename: '',
deleteFileShow: false,
deleteFileId: '',
deleteFileFilename: ''
},
methods: {
el: '#modal-editor-file',
data: {
isLoading: false,
isLoadingText: '',
newFolderName: '',
newFolderShow: false,
newFolderError: false,
folders: [],
currentFolder: '',
currentFile: '',
files: [],
uploadSucceeded: false,
postUploadChecks: 0,
renameFileShow: false,
renameFileId: '',
renameFileFilename: '',
deleteFileShow: false,
deleteFileId: '',
deleteFileFilename: ''
},
methods: {
open: () => {
mdeModalOpenState = true;
$('#modal-editor-file').addClass('is-active');
vueFile.refreshFolders();
},
cancel: (ev) => {
mdeModalOpenState = false;
$('#modal-editor-file').removeClass('is-active');
},
open: () => {
mdeModalOpenState = true
$('#modal-editor-file').addClass('is-active')
vueFile.refreshFolders()
},
cancel: (ev) => {
mdeModalOpenState = false
$('#modal-editor-file').removeClass('is-active')
},
// -------------------------------------------
// INSERT LINK TO FILE
// -------------------------------------------
selectFile: (fileId) => {
vueFile.currentFile = fileId;
},
insertFileLink: (ev) => {
selectFile: (fileId) => {
vueFile.currentFile = fileId
},
insertFileLink: (ev) => {
if (mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection')
}
if(mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection');
}
let selFile = _.find(vueFile.files, ['_id', vueFile.currentFile])
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]);
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 + '")'
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
// -------------------------------------------
newFolder: (ev) => {
vueFile.newFolderName = '';
vueFile.newFolderError = false;
vueFile.newFolderShow = true;
_.delay(() => { $('#txt-editor-file-newfoldername').focus(); }, 400);
},
newFolderDiscard: (ev) => {
vueFile.newFolderShow = false;
},
newFolderCreate: (ev) => {
newFolder: (ev) => {
vueFile.newFolderName = ''
vueFile.newFolderError = false
vueFile.newFolderShow = true
_.delay(() => { $('#txt-editor-file-newfoldername').focus() }, 400)
},
newFolderDiscard: (ev) => {
vueFile.newFolderShow = false
},
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]$");
vueFile.newFolderName = _.kebabCase(_.trim(vueFile.newFolderName));
if (_.isEmpty(vueFile.newFolderName) || !regFolderName.test(vueFile.newFolderName)) {
vueFile.newFolderError = true
return
}
if(_.isEmpty(vueFile.newFolderName) || !regFolderName.test(vueFile.newFolderName)) {
vueFile.newFolderError = true;
return;
}
vueFile.newFolderDiscard()
vueFile.isLoadingText = 'Creating new folder...'
vueFile.isLoading = true
vueFile.newFolderDiscard();
vueFile.isLoadingText = 'Creating new folder...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsCreateFolder', { foldername: vueFile.newFolderName }, (data) => {
vueFile.folders = data;
vueFile.currentFolder = vueFile.newFolderName;
vueFile.files = [];
vueFile.isLoading = false;
});
});
},
Vue.nextTick(() => {
socket.emit('uploadsCreateFolder', { foldername: vueFile.newFolderName }, (data) => {
vueFile.folders = data
vueFile.currentFolder = vueFile.newFolderName
vueFile.files = []
vueFile.isLoading = false
})
})
},
// -------------------------------------------
// 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 ]);
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;
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);
}
});
});
},
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
// -------------------------------------------
moveFile: (uid, fld) => {
vueFile.isLoadingText = 'Moving file...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
if(data.ok) {
vueFile.loadFiles();
} else {
vueFile.isLoading = false;
alerts.pushError('Rename error', data.msg);
}
});
});
},
moveFile: (uid, fld) => {
vueFile.isLoadingText = 'Moving file...'
vueFile.isLoading = true
Vue.nextTick(() => {
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
if (data.ok) {
vueFile.loadFiles()
} else {
vueFile.isLoading = false
alerts.pushError('Rename error', data.msg)
}
})
})
},
// -------------------------------------------
// DELETE FILE
// -------------------------------------------
deleteFileWarn: (show) => {
if(show) {
let c = _.find(vueFile.files, ['_id', vueFile.deleteFileId ]);
vueFile.deleteFileFilename = c.filename || 'this file';
}
vueFile.deleteFileShow = show;
},
deleteFileGo: () => {
vueFile.deleteFileWarn(false);
vueFile.isLoadingText = 'Deleting file...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsDeleteFile', { uid: vueFile.deleteFileId }, (data) => {
vueFile.loadFiles();
});
});
},
deleteFileWarn: (show) => {
if (show) {
let c = _.find(vueFile.files, ['_id', vueFile.deleteFileId ])
vueFile.deleteFileFilename = c.filename || 'this file'
}
vueFile.deleteFileShow = show
},
deleteFileGo: () => {
vueFile.deleteFileWarn(false)
vueFile.isLoadingText = 'Deleting file...'
vueFile.isLoading = true
Vue.nextTick(() => {
socket.emit('uploadsDeleteFile', { uid: vueFile.deleteFileId }, (data) => {
vueFile.loadFiles()
})
})
},
// -------------------------------------------
// LOAD FROM REMOTE
// -------------------------------------------
selectFolder: (fldName) => {
vueFile.currentFolder = fldName;
vueFile.loadFiles();
},
selectFolder: (fldName) => {
vueFile.currentFolder = fldName
vueFile.loadFiles()
},
refreshFolders: () => {
vueFile.isLoadingText = 'Fetching folders list...';
vueFile.isLoading = true;
vueFile.currentFolder = '';
vueFile.currentImage = '';
Vue.nextTick(() => {
socket.emit('uploadsGetFolders', { }, (data) => {
vueFile.folders = data;
vueFile.loadFiles();
});
});
},
refreshFolders: () => {
vueFile.isLoadingText = 'Fetching folders list...'
vueFile.isLoading = true
vueFile.currentFolder = ''
vueFile.currentImage = ''
Vue.nextTick(() => {
socket.emit('uploadsGetFolders', { }, (data) => {
vueFile.folders = data
vueFile.loadFiles()
})
})
},
loadFiles: (silent) => {
if(!silent) {
vueFile.isLoadingText = 'Fetching files...';
vueFile.isLoading = true;
}
return new Promise((resolve, reject) => {
Vue.nextTick(() => {
socket.emit('uploadsGetFiles', { folder: vueFile.currentFolder }, (data) => {
vueFile.files = data;
if(!silent) {
vueFile.isLoading = false;
}
vueFile.attachContextMenus();
resolve(true);
});
});
});
},
loadFiles: (silent) => {
if (!silent) {
vueFile.isLoadingText = 'Fetching files...'
vueFile.isLoading = true
}
return new Promise((resolve, reject) => {
Vue.nextTick(() => {
socket.emit('uploadsGetFiles', { folder: vueFile.currentFolder }, (data) => {
vueFile.files = data
if (!silent) {
vueFile.isLoading = false
}
vueFile.attachContextMenus()
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++;
vueFile.isLoadingText = 'Processing...';
Vue.nextTick(() => {
vueFile.loadFiles(true).then(() => {
if((vueFile.files.length !== oldAmount) === expectChange) {
vueFile.postUploadChecks = 0;
vueFile.isLoading = false;
} else if(vueFile.postUploadChecks > 5) {
vueFile.postUploadChecks = 0;
vueFile.isLoading = false;
alerts.pushError('Unable to fetch updated listing', 'Try again later');
} else {
_.delay(() => {
vueFile.waitChangeComplete(oldAmount, expectChange);
}, 1500);
}
});
});
},
Vue.nextTick(() => {
vueFile.loadFiles(true).then(() => {
if ((vueFile.files.length !== oldAmount) === expectChange) {
vueFile.postUploadChecks = 0
vueFile.isLoading = false
} else if (vueFile.postUploadChecks > 5) {
vueFile.postUploadChecks = 0
vueFile.isLoading = false
alerts.pushError('Unable to fetch updated listing', 'Try again later')
} else {
_.delay(() => {
vueFile.waitChangeComplete(oldAmount, expectChange)
}, 1500)
}
})
})
},
// -------------------------------------------
// IMAGE CONTEXT MENU
// -------------------------------------------
attachContextMenus: () => {
let moveFolders = _.map(vueFile.folders, (f) => {
return {
name: (f !== '') ? f : '/ (root)',
icon: 'fa-folder',
callback: (key, opt) => {
let moveFileId = _.toString($(opt.$trigger).data('uid'));
let moveFileDestFolder = _.nth(vueFile.folders, key);
vueFile.moveFile(moveFileId, moveFileDestFolder);
}
};
});
attachContextMenus: () => {
let moveFolders = _.map(vueFile.folders, (f) => {
return {
name: (f !== '') ? f : '/ (root)',
icon: 'fa-folder',
callback: (key, opt) => {
let moveFileId = _.toString($(opt.$trigger).data('uid'))
let moveFileDestFolder = _.nth(vueFile.folders, key)
vueFile.moveFile(moveFileId, moveFileDestFolder)
}
}
})
$.contextMenu('destroy', '.editor-modal-file-choices > figure');
$.contextMenu({
selector: '.editor-modal-file-choices > figure',
appendTo: '.editor-modal-file-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen');
let trigPos = $(opt.$trigger).position();
let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 };
opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w });
},
events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen');
}
},
items: {
rename: {
name: "Rename",
icon: "fa-edit",
callback: (key, opt) => {
vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid);
vueFile.renameFile();
}
},
move: {
name: "Move to...",
icon: "fa-folder-open-o",
items: moveFolders
},
delete: {
name: "Delete",
icon: "fa-trash",
callback: (key, opt) => {
vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid);
vueFile.deleteFileWarn(true);
}
}
}
});
}
$.contextMenu('destroy', '.editor-modal-file-choices > figure')
$.contextMenu({
selector: '.editor-modal-file-choices > figure',
appendTo: '.editor-modal-file-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen')
let trigPos = $(opt.$trigger).position()
let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 }
opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w })
},
events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen')
}
},
items: {
rename: {
name: 'Rename',
icon: 'fa-edit',
callback: (key, opt) => {
vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid)
vueFile.renameFile()
}
},
move: {
name: 'Move to...',
icon: 'fa-folder-open-o',
items: moveFolders
},
delete: {
name: 'Delete',
icon: 'fa-trash',
callback: (key, opt) => {
vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid)
vueFile.deleteFileWarn(true)
}
}
}
})
}
}
});
}
})
$('#btn-editor-file-upload input').on('change', (ev) => {
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',
data: {
folder: vueFile.currentFolder
},
limit: 20,
expect: 'json',
maxFileSize: 0,
init: (totalUploads) => {
vueFile.uploadSucceeded = false
vueFile.isLoadingText = 'Preparing to upload...'
vueFile.isLoading = true
},
init: (totalUploads) => {
vueFile.uploadSucceeded = false;
vueFile.isLoadingText = 'Preparing to upload...';
vueFile.isLoading = true;
},
progress: (progress) => {
vueFile.isLoadingText = 'Uploading...' + Math.round(progress) + '%'
},
progress: (progress) => {
vueFile.isLoadingText = 'Uploading...' + Math.round(progress) + '%';
},
success: (data) => {
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) => {
if(data.ok) {
error: (error) => {
alerts.pushError(error.message, this.upload.file.name)
},
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;
}
finish: () => {
if (vueFile.uploadSucceeded) {
vueFile.waitChangeComplete(curFileAmount, true)
} else {
vueFile.isLoading = false
}
}
} 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({
el: '#modal-editor-image',
data: {
isLoading: false,
isLoadingText: '',
newFolderName: '',
newFolderShow: false,
newFolderError: false,
fetchFromUrlURL: '',
fetchFromUrlShow: false,
folders: [],
currentFolder: '',
currentImage: '',
currentAlign: 'left',
images: [],
uploadSucceeded: false,
postUploadChecks: 0,
renameImageShow: false,
renameImageId: '',
renameImageFilename: '',
deleteImageShow: false,
deleteImageId: '',
deleteImageFilename: ''
},
methods: {
el: '#modal-editor-image',
data: {
isLoading: false,
isLoadingText: '',
newFolderName: '',
newFolderShow: false,
newFolderError: false,
fetchFromUrlURL: '',
fetchFromUrlShow: false,
folders: [],
currentFolder: '',
currentImage: '',
currentAlign: 'left',
images: [],
uploadSucceeded: false,
postUploadChecks: 0,
renameImageShow: false,
renameImageId: '',
renameImageFilename: '',
deleteImageShow: false,
deleteImageId: '',
deleteImageFilename: ''
},
methods: {
open: () => {
mdeModalOpenState = true;
$('#modal-editor-image').addClass('is-active');
vueImage.refreshFolders();
},
cancel: (ev) => {
mdeModalOpenState = false;
$('#modal-editor-image').removeClass('is-active');
},
open: () => {
mdeModalOpenState = true
$('#modal-editor-image').addClass('is-active')
vueImage.refreshFolders()
},
cancel: (ev) => {
mdeModalOpenState = false
$('#modal-editor-image').removeClass('is-active')
},
// -------------------------------------------
// INSERT IMAGE
// -------------------------------------------
selectImage: (imageId) => {
vueImage.currentImage = imageId;
},
insertImage: (ev) => {
selectImage: (imageId) => {
vueImage.currentImage = imageId
},
insertImage: (ev) => {
if (mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection')
}
if(mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection');
}
let selImage = _.find(vueImage.images, ['_id', vueImage.currentImage])
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]);
selImage.normalizedPath = (selImage.folder === 'f:') ? selImage.filename : selImage.folder.slice(2) + '/' + selImage.filename;
selImage.titleGuess = _.startCase(selImage.basename);
let imageText = '![' + selImage.titleGuess + '](/uploads/' + selImage.normalizedPath + ' "' + selImage.titleGuess + '")'
switch (vueImage.currentAlign) {
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 + '")';
switch(vueImage.currentAlign) {
case 'center':
imageText += '{.align-center}';
break;
case 'right':
imageText += '{.align-right}';
break;
case 'logo':
imageText += '{.pagelogo}';
break;
}
mde.codemirror.doc.replaceSelection(imageText);
vueImage.cancel();
},
mde.codemirror.doc.replaceSelection(imageText)
vueImage.cancel()
},
// -------------------------------------------
// NEW FOLDER
// -------------------------------------------
newFolder: (ev) => {
vueImage.newFolderName = '';
vueImage.newFolderError = false;
vueImage.newFolderShow = true;
_.delay(() => { $('#txt-editor-image-newfoldername').focus(); }, 400);
},
newFolderDiscard: (ev) => {
vueImage.newFolderShow = false;
},
newFolderCreate: (ev) => {
newFolder: (ev) => {
vueImage.newFolderName = ''
vueImage.newFolderError = false
vueImage.newFolderShow = true
_.delay(() => { $('#txt-editor-image-newfoldername').focus() }, 400)
},
newFolderDiscard: (ev) => {
vueImage.newFolderShow = false
},
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]$");
vueImage.newFolderName = _.kebabCase(_.trim(vueImage.newFolderName));
if (_.isEmpty(vueImage.newFolderName) || !regFolderName.test(vueImage.newFolderName)) {
vueImage.newFolderError = true
return
}
if(_.isEmpty(vueImage.newFolderName) || !regFolderName.test(vueImage.newFolderName)) {
vueImage.newFolderError = true;
return;
}
vueImage.newFolderDiscard()
vueImage.isLoadingText = 'Creating new folder...'
vueImage.isLoading = true
vueImage.newFolderDiscard();
vueImage.isLoadingText = 'Creating new folder...';
vueImage.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsCreateFolder', { foldername: vueImage.newFolderName }, (data) => {
vueImage.folders = data;
vueImage.currentFolder = vueImage.newFolderName;
vueImage.images = [];
vueImage.isLoading = false;
});
});
},
Vue.nextTick(() => {
socket.emit('uploadsCreateFolder', { foldername: vueImage.newFolderName }, (data) => {
vueImage.folders = data
vueImage.currentFolder = vueImage.newFolderName
vueImage.images = []
vueImage.isLoading = false
})
})
},
// -------------------------------------------
// FETCH FROM URL
// -------------------------------------------
fetchFromUrl: (ev) => {
vueImage.fetchFromUrlURL = '';
vueImage.fetchFromUrlShow = true;
_.delay(() => { $('#txt-editor-image-fetchurl').focus(); }, 400);
},
fetchFromUrlDiscard: (ev) => {
vueImage.fetchFromUrlShow = false;
},
fetchFromUrlGo: (ev) => {
vueImage.fetchFromUrlDiscard();
vueImage.isLoadingText = 'Fetching image...';
vueImage.isLoading = true;
fetchFromUrl: (ev) => {
vueImage.fetchFromUrlURL = ''
vueImage.fetchFromUrlShow = true
_.delay(() => { $('#txt-editor-image-fetchurl').focus() }, 400)
},
fetchFromUrlDiscard: (ev) => {
vueImage.fetchFromUrlShow = false
},
fetchFromUrlGo: (ev) => {
vueImage.fetchFromUrlDiscard()
vueImage.isLoadingText = 'Fetching image...'
vueImage.isLoading = true
Vue.nextTick(() => {
socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => {
if(data.ok) {
vueImage.waitChangeComplete(vueImage.images.length, true);
} else {
vueImage.isLoading = false;
alerts.pushError('Upload error', data.msg);
}
});
});
},
Vue.nextTick(() => {
socket.emit('uploadsFetchFileFromURL', { folder: vueImage.currentFolder, fetchUrl: vueImage.fetchFromUrlURL }, (data) => {
if (data.ok) {
vueImage.waitChangeComplete(vueImage.images.length, true)
} else {
vueImage.isLoading = false
alerts.pushError('Upload error', data.msg)
}
})
})
},
// -------------------------------------------
// 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 ]);
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;
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);
}
});
});
},
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
// -------------------------------------------
moveImage: (uid, fld) => {
vueImage.isLoadingText = 'Moving image...';
vueImage.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
if(data.ok) {
vueImage.loadImages();
} else {
vueImage.isLoading = false;
alerts.pushError('Rename error', data.msg);
}
});
});
},
moveImage: (uid, fld) => {
vueImage.isLoadingText = 'Moving image...'
vueImage.isLoading = true
Vue.nextTick(() => {
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
if (data.ok) {
vueImage.loadImages()
} else {
vueImage.isLoading = false
alerts.pushError('Rename error', data.msg)
}
})
})
},
// -------------------------------------------
// DELETE IMAGE
// -------------------------------------------
deleteImageWarn: (show) => {
if(show) {
let c = _.find(vueImage.images, ['_id', vueImage.deleteImageId ]);
vueImage.deleteImageFilename = c.filename || 'this image';
}
vueImage.deleteImageShow = show;
},
deleteImageGo: () => {
vueImage.deleteImageWarn(false);
vueImage.isLoadingText = 'Deleting image...';
vueImage.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsDeleteFile', { uid: vueImage.deleteImageId }, (data) => {
vueImage.loadImages();
});
});
},
deleteImageWarn: (show) => {
if (show) {
let c = _.find(vueImage.images, ['_id', vueImage.deleteImageId ])
vueImage.deleteImageFilename = c.filename || 'this image'
}
vueImage.deleteImageShow = show
},
deleteImageGo: () => {
vueImage.deleteImageWarn(false)
vueImage.isLoadingText = 'Deleting image...'
vueImage.isLoading = true
Vue.nextTick(() => {
socket.emit('uploadsDeleteFile', { uid: vueImage.deleteImageId }, (data) => {
vueImage.loadImages()
})
})
},
// -------------------------------------------
// LOAD FROM REMOTE
// -------------------------------------------
selectFolder: (fldName) => {
vueImage.currentFolder = fldName;
vueImage.loadImages();
},
selectFolder: (fldName) => {
vueImage.currentFolder = fldName
vueImage.loadImages()
},
refreshFolders: () => {
vueImage.isLoadingText = 'Fetching folders list...';
vueImage.isLoading = true;
vueImage.currentFolder = '';
vueImage.currentImage = '';
Vue.nextTick(() => {
socket.emit('uploadsGetFolders', { }, (data) => {
vueImage.folders = data;
vueImage.loadImages();
});
});
},
refreshFolders: () => {
vueImage.isLoadingText = 'Fetching folders list...'
vueImage.isLoading = true
vueImage.currentFolder = ''
vueImage.currentImage = ''
Vue.nextTick(() => {
socket.emit('uploadsGetFolders', { }, (data) => {
vueImage.folders = data
vueImage.loadImages()
})
})
},
loadImages: (silent) => {
if(!silent) {
vueImage.isLoadingText = 'Fetching images...';
vueImage.isLoading = true;
}
return new Promise((resolve, reject) => {
Vue.nextTick(() => {
socket.emit('uploadsGetImages', { folder: vueImage.currentFolder }, (data) => {
vueImage.images = data;
if(!silent) {
vueImage.isLoading = false;
}
vueImage.attachContextMenus();
resolve(true);
});
});
});
},
loadImages: (silent) => {
if (!silent) {
vueImage.isLoadingText = 'Fetching images...'
vueImage.isLoading = true
}
return new Promise((resolve, reject) => {
Vue.nextTick(() => {
socket.emit('uploadsGetImages', { folder: vueImage.currentFolder }, (data) => {
vueImage.images = data
if (!silent) {
vueImage.isLoading = false
}
vueImage.attachContextMenus()
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++;
vueImage.isLoadingText = 'Processing...';
Vue.nextTick(() => {
vueImage.loadImages(true).then(() => {
if((vueImage.images.length !== oldAmount) === expectChange) {
vueImage.postUploadChecks = 0;
vueImage.isLoading = false;
} else if(vueImage.postUploadChecks > 5) {
vueImage.postUploadChecks = 0;
vueImage.isLoading = false;
alerts.pushError('Unable to fetch updated listing', 'Try again later');
} else {
_.delay(() => {
vueImage.waitChangeComplete(oldAmount, expectChange);
}, 1500);
}
});
});
},
Vue.nextTick(() => {
vueImage.loadImages(true).then(() => {
if ((vueImage.images.length !== oldAmount) === expectChange) {
vueImage.postUploadChecks = 0
vueImage.isLoading = false
} else if (vueImage.postUploadChecks > 5) {
vueImage.postUploadChecks = 0
vueImage.isLoading = false
alerts.pushError('Unable to fetch updated listing', 'Try again later')
} else {
_.delay(() => {
vueImage.waitChangeComplete(oldAmount, expectChange)
}, 1500)
}
})
})
},
// -------------------------------------------
// IMAGE CONTEXT MENU
// -------------------------------------------
attachContextMenus: () => {
let moveFolders = _.map(vueImage.folders, (f) => {
return {
name: (f !== '') ? f : '/ (root)',
icon: 'fa-folder',
callback: (key, opt) => {
let moveImageId = _.toString($(opt.$trigger).data('uid'));
let moveImageDestFolder = _.nth(vueImage.folders, key);
vueImage.moveImage(moveImageId, moveImageDestFolder);
}
};
});
attachContextMenus: () => {
let moveFolders = _.map(vueImage.folders, (f) => {
return {
name: (f !== '') ? f : '/ (root)',
icon: 'fa-folder',
callback: (key, opt) => {
let moveImageId = _.toString($(opt.$trigger).data('uid'))
let moveImageDestFolder = _.nth(vueImage.folders, key)
vueImage.moveImage(moveImageId, moveImageDestFolder)
}
}
})
$.contextMenu('destroy', '.editor-modal-image-choices > figure');
$.contextMenu({
selector: '.editor-modal-image-choices > figure',
appendTo: '.editor-modal-image-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen');
let trigPos = $(opt.$trigger).position();
let trigDim = { w: $(opt.$trigger).width() / 2, h: $(opt.$trigger).height() / 2 };
opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w });
},
events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen');
}
},
items: {
rename: {
name: "Rename",
icon: "fa-edit",
callback: (key, opt) => {
vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid);
vueImage.renameImage();
}
},
move: {
name: "Move to...",
icon: "fa-folder-open-o",
items: moveFolders
},
delete: {
name: "Delete",
icon: "fa-trash",
callback: (key, opt) => {
vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid);
vueImage.deleteImageWarn(true);
}
}
}
});
}
$.contextMenu('destroy', '.editor-modal-image-choices > figure')
$.contextMenu({
selector: '.editor-modal-image-choices > figure',
appendTo: '.editor-modal-image-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen')
let trigPos = $(opt.$trigger).position()
let trigDim = { w: $(opt.$trigger).width() / 2, h: $(opt.$trigger).height() / 2 }
opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w })
},
events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen')
}
},
items: {
rename: {
name: 'Rename',
icon: 'fa-edit',
callback: (key, opt) => {
vueImage.renameImageId = _.toString(opt.$trigger[0].dataset.uid)
vueImage.renameImage()
}
},
move: {
name: 'Move to...',
icon: 'fa-folder-open-o',
items: moveFolders
},
delete: {
name: 'Delete',
icon: 'fa-trash',
callback: (key, opt) => {
vueImage.deleteImageId = _.toString(opt.$trigger[0].dataset.uid)
vueImage.deleteImageWarn(true)
}
}
}
})
}
}
});
}
})
$('#btn-editor-image-upload input').on('change', (ev) => {
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',
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
init: (totalUploads) => {
vueImage.uploadSucceeded = false
vueImage.isLoadingText = 'Preparing to upload...'
vueImage.isLoading = true
},
init: (totalUploads) => {
vueImage.uploadSucceeded = false;
vueImage.isLoadingText = 'Preparing to upload...';
vueImage.isLoading = true;
},
progress: (progress) => {
vueImage.isLoadingText = 'Uploading...' + Math.round(progress) + '%'
},
progress: (progress) => {
vueImage.isLoadingText = 'Uploading...' + Math.round(progress) + '%';
},
success: (data) => {
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) => {
if(data.ok) {
error: (error) => {
alerts.pushError(error.message, this.upload.file.name)
},
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;
}
finish: () => {
if (vueImage.uploadSucceeded) {
vueImage.waitChangeComplete(curImageAmount, true)
} else {
vueImage.isLoading = false
}
}
} 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 = {
'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'),
'dailymotion': new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, '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'),
'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
let vueVideo = new Vue({
el: '#modal-editor-video',
data: {
link: ''
},
methods: {
open: (ev) => {
$('#modal-editor-video').addClass('is-active');
$('#modal-editor-video input').focus();
},
cancel: (ev) => {
mdeModalOpenState = false;
$('#modal-editor-video').removeClass('is-active');
vueVideo.link = '';
},
insertVideo: (ev) => {
if(mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection');
}
el: '#modal-editor-video',
data: {
link: ''
},
methods: {
open: (ev) => {
$('#modal-editor-video').addClass('is-active')
$('#modal-editor-video input').focus()
},
cancel: (ev) => {
mdeModalOpenState = false
$('#modal-editor-video').removeClass('is-active')
vueVideo.link = ''
},
insertVideo: (ev) => {
if (mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection')
}
// Guess video type
let videoType = _.findKey(videoRules, (vr) => {
return vr.test(vueVideo.link);
});
if(_.isNil(videoType)) {
videoType = 'video';
}
let videoType = _.findKey(videoRules, (vr) => {
return vr.test(vueVideo.link)
})
if (_.isNil(videoType)) {
videoType = 'video'
}
// Insert video tag
let videoText = '[video](' + vueVideo.link + '){.' + videoType + '}\n';
let videoText = '[video](' + vueVideo.link + '){.' + videoType + '}\n'
mde.codemirror.doc.replaceSelection(videoText);
vueVideo.cancel();
}
}
});
mde.codemirror.doc.replaceSelection(videoText)
vueVideo.cancel()
}
}
})

View File

@ -3,215 +3,210 @@
// Markdown Editor
// ====================================
if($('#mk-editor').length === 1) {
if ($('#mk-editor').length === 1) {
let mdeModalOpenState = false
let mdeCurrentEditor = null
let mdeModalOpenState = false;
let mdeCurrentEditor = null;
Vue.filter('filesize', (v) => {
return _.toUpper(filesize(v))
})
Vue.filter('filesize', (v) => {
return _.toUpper(filesize(v));
});
// =include editor-image.js
// =include editor-file.js
// =include editor-video.js
// =include editor-codeblock.js
//=include editor-image.js
//=include editor-file.js
//=include editor-video.js
//=include editor-codeblock.js
var mde = new SimpleMDE({
autofocus: true,
autoDownloadFontAwesome: false,
element: $("#mk-editor").get(0),
placeholder: 'Enter Markdown formatted content here...',
spellChecker: false,
status: false,
toolbar: [{
name: "bold",
action: SimpleMDE.toggleBold,
className: "icon-bold",
title: "Bold",
},
{
name: "italic",
action: SimpleMDE.toggleItalic,
className: "icon-italic",
title: "Italic",
},
{
name: "strikethrough",
action: SimpleMDE.toggleStrikethrough,
className: "icon-strikethrough",
title: "Strikethrough",
},
'|',
{
name: "heading-1",
action: SimpleMDE.toggleHeading1,
className: "icon-header fa-header-x fa-header-1",
title: "Big Heading",
},
{
name: "heading-2",
action: SimpleMDE.toggleHeading2,
className: "icon-header fa-header-x fa-header-2",
title: "Medium Heading",
},
{
name: "heading-3",
action: SimpleMDE.toggleHeading3,
className: "icon-header fa-header-x fa-header-3",
title: "Small Heading",
},
{
name: "quote",
action: SimpleMDE.toggleBlockquote,
className: "icon-quote-left",
title: "Quote",
},
'|',
{
name: "unordered-list",
action: SimpleMDE.toggleUnorderedList,
className: "icon-th-list",
title: "Bullet List",
},
{
name: "ordered-list",
action: SimpleMDE.toggleOrderedList,
className: "icon-list-ol",
title: "Numbered List",
},
'|',
{
name: "link",
action: (editor) => {
/*if(!mdeModalOpenState) {
var mde = new SimpleMDE({
autofocus: true,
autoDownloadFontAwesome: false,
element: $('#mk-editor').get(0),
placeholder: 'Enter Markdown formatted content here...',
spellChecker: false,
status: false,
toolbar: [{
name: 'bold',
action: SimpleMDE.toggleBold,
className: 'icon-bold',
title: 'Bold'
},
{
name: 'italic',
action: SimpleMDE.toggleItalic,
className: 'icon-italic',
title: 'Italic'
},
{
name: 'strikethrough',
action: SimpleMDE.toggleStrikethrough,
className: 'icon-strikethrough',
title: 'Strikethrough'
},
'|',
{
name: 'heading-1',
action: SimpleMDE.toggleHeading1,
className: 'icon-header fa-header-x fa-header-1',
title: 'Big Heading'
},
{
name: 'heading-2',
action: SimpleMDE.toggleHeading2,
className: 'icon-header fa-header-x fa-header-2',
title: 'Medium Heading'
},
{
name: 'heading-3',
action: SimpleMDE.toggleHeading3,
className: 'icon-header fa-header-x fa-header-3',
title: 'Small Heading'
},
{
name: 'quote',
action: SimpleMDE.toggleBlockquote,
className: 'icon-quote-left',
title: 'Quote'
},
'|',
{
name: 'unordered-list',
action: SimpleMDE.toggleUnorderedList,
className: 'icon-th-list',
title: 'Bullet List'
},
{
name: 'ordered-list',
action: SimpleMDE.toggleOrderedList,
className: 'icon-list-ol',
title: 'Numbered List'
},
'|',
{
name: 'link',
action: (editor) => {
/* if(!mdeModalOpenState) {
mdeModalOpenState = true;
$('#modal-editor-link').slideToggle();
}*/
},
className: "icon-link2",
title: "Insert Link",
},
{
name: "image",
action: (editor) => {
if(!mdeModalOpenState) {
vueImage.open();
}
},
className: "icon-image",
title: "Insert Image",
},
{
name: "file",
action: (editor) => {
if(!mdeModalOpenState) {
vueFile.open();
}
},
className: "icon-paper",
title: "Insert File",
},
{
name: "video",
action: (editor) => {
if(!mdeModalOpenState) {
vueVideo.open();
}
},
className: "icon-video-camera2",
title: "Insert Video Player",
},
'|',
{
name: "inline-code",
action: (editor) => {
} */
},
className: 'icon-link2',
title: 'Insert Link'
},
{
name: 'image',
action: (editor) => {
if (!mdeModalOpenState) {
vueImage.open()
}
},
className: 'icon-image',
title: 'Insert Image'
},
{
name: 'file',
action: (editor) => {
if (!mdeModalOpenState) {
vueFile.open()
}
},
className: 'icon-paper',
title: 'Insert File'
},
{
name: 'video',
action: (editor) => {
if (!mdeModalOpenState) {
vueVideo.open()
}
},
className: 'icon-video-camera2',
title: 'Insert Video Player'
},
'|',
{
name: 'inline-code',
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()) {
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);
if (mde.codemirror.doc.somethingSelected()) {
vueCodeBlock.initContent = mde.codemirror.doc.getSelection()
}
},
className: "icon-terminal",
title: "Inline Code",
},
{
name: "code-block",
action: (editor) => {
if(!mdeModalOpenState) {
mdeModalOpenState = true;
vueCodeBlock.open()
}
},
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
}
})
if(mde.codemirror.doc.somethingSelected()) {
vueCodeBlock.initContent = mde.codemirror.doc.getSelection();
}
// -> Save
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.')
})
}
}
},
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
}
});
$('.btn-edit-save, .btn-create-save').on('click', (ev) => {
saveCurrentDocument(ev)
})
//-> Save
let saveCurrentDocument = (ev) => {
$.ajax(window.location.href, {
data: {
markdown: mde.value()
},
dataType: 'json',
method: 'PUT'
}).then((rData, rStatus, rXHR) => {
if(rData.ok) {
window.location.assign('/' + pageEntryPath);
} else {
alerts.pushError('Something went wrong', rData.error);
}
}, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.');
});
};
$('.btn-edit-save, .btn-create-save').on('click', (ev) => {
saveCurrentDocument(ev);
});
$(window).bind('keydown', (ev) => {
if (ev.ctrlKey || ev.metaKey) {
switch (String.fromCharCode(ev.which).toLowerCase()) {
case 's':
ev.preventDefault();
saveCurrentDocument(ev);
break;
}
}
});
}
$(window).bind('keydown', (ev) => {
if (ev.ctrlKey || ev.metaKey) {
switch (String.fromCharCode(ev.which).toLowerCase()) {
case 's':
ev.preventDefault()
saveCurrentDocument(ev)
break
}
}
})
}

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',
data: {
searchq: '',
searchres: [],
searchsuggest: [],
searchload: 0,
searchactive: false,
searchmoveidx: 0,
searchmovekey: '',
searchmovearr: []
},
watch: {
searchq: (val, oldVal) => {
vueHeader.searchmoveidx = 0;
if(val.length >= 3) {
vueHeader.searchactive = true;
vueHeader.searchload++;
socket.emit('search', { terms: val }, (data) => {
vueHeader.searchres = data.match;
vueHeader.searchsuggest = data.suggest;
vueHeader.searchmovearr = _.concat([], vueHeader.searchres, vueHeader.searchsuggest);
if(vueHeader.searchload > 0) { vueHeader.searchload--; }
});
} else {
vueHeader.searchactive = false;
vueHeader.searchres = [];
vueHeader.searchsuggest = [];
vueHeader.searchmovearr = [];
vueHeader.searchload = 0;
}
},
searchmoveidx: (val, oldVal) => {
if(val > 0) {
vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1]) ?
var vueHeader = new Vue({
el: '#header-container',
data: {
searchq: '',
searchres: [],
searchsuggest: [],
searchload: 0,
searchactive: false,
searchmoveidx: 0,
searchmovekey: '',
searchmovearr: []
},
watch: {
searchq: (val, oldVal) => {
vueHeader.searchmoveidx = 0
if (val.length >= 3) {
vueHeader.searchactive = true
vueHeader.searchload++
socket.emit('search', { terms: val }, (data) => {
vueHeader.searchres = data.match
vueHeader.searchsuggest = data.suggest
vueHeader.searchmovearr = _.concat([], vueHeader.searchres, vueHeader.searchsuggest)
if (vueHeader.searchload > 0) { vueHeader.searchload-- }
})
} else {
vueHeader.searchactive = false
vueHeader.searchres = []
vueHeader.searchsuggest = []
vueHeader.searchmovearr = []
vueHeader.searchload = 0
}
},
searchmoveidx: (val, oldVal) => {
if (val > 0) {
vueHeader.searchmovekey = (vueHeader.searchmovearr[val - 1]) ?
'res.' + vueHeader.searchmovearr[val - 1]._id :
'sug.' + vueHeader.searchmovearr[val - 1];
} else {
vueHeader.searchmovekey = '';
}
}
},
methods: {
useSuggestion: (sug) => {
vueHeader.searchq = sug;
},
closeSearch: () => {
vueHeader.searchq = '';
},
moveSelectSearch: () => {
if(vueHeader.searchmoveidx < 1) { return; }
let i = vueHeader.searchmoveidx - 1;
'sug.' + vueHeader.searchmovearr[val - 1]
} else {
vueHeader.searchmovekey = ''
}
}
},
methods: {
useSuggestion: (sug) => {
vueHeader.searchq = sug
},
closeSearch: () => {
vueHeader.searchq = ''
},
moveSelectSearch: () => {
if (vueHeader.searchmoveidx < 1) { return }
let i = vueHeader.searchmoveidx - 1
if(vueHeader.searchmovearr[i]) {
window.location.assign('/' + vueHeader.searchmovearr[i]._id);
} else {
vueHeader.searchq = vueHeader.searchmovearr[i];
}
if (vueHeader.searchmovearr[i]) {
window.location.assign('/' + vueHeader.searchmovearr[i]._id)
} else {
vueHeader.searchq = vueHeader.searchmovearr[i]
}
},
moveDownSearch: () => {
if (vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
vueHeader.searchmoveidx++
}
},
moveUpSearch: () => {
if (vueHeader.searchmoveidx > 0) {
vueHeader.searchmoveidx--
}
}
}
})
},
moveDownSearch: () => {
if(vueHeader.searchmoveidx < vueHeader.searchmovearr.length) {
vueHeader.searchmoveidx++;
}
},
moveUpSearch: () => {
if(vueHeader.searchmoveidx > 0) {
vueHeader.searchmoveidx--;
}
}
}
});
$('main').on('click', vueHeader.closeSearch);
}
$('main').on('click', vueHeader.closeSearch)
}

View File

@ -1,16 +1,16 @@
function setInputSelection(input, startPos, endPos) {
input.focus();
if (typeof input.selectionStart != "undefined") {
input.selectionStart = startPos;
input.selectionEnd = endPos;
} else if (document.selection && document.selection.createRange) {
function setInputSelection (input, startPos, endPos) {
input.focus()
if (typeof input.selectionStart !== 'undefined') {
input.selectionStart = startPos
input.selectionEnd = endPos
} else if (document.selection && document.selection.createRange) {
// IE branch
input.select();
var range = document.selection.createRange();
range.collapse(true);
range.moveEnd("character", endPos);
range.moveStart("character", startPos);
range.select();
}
}
input.select()
var range = document.selection.createRange()
range.collapse(true)
range.moveEnd('character', endPos)
range.moveStart('character', startPos)
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), '/');
rawParts = _.map(rawParts, (r) => {
return _.kebabCase(_.deburr(_.trim(r)));
});
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r); }), '/');
}
return _.join(_.filter(rawParts, (r) => { return !_.isEmpty(r) }), '/')
}

View File

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

View File

@ -2,29 +2,27 @@
// Vue Create User instance
let vueCreateUser = new Vue({
el: '#modal-admin-users-create',
data: {
email: '',
provider: 'local',
password: '',
name: ''
},
methods: {
open: (ev) => {
$('#modal-admin-users-create').addClass('is-active');
$('#modal-admin-users-create input').first().focus();
},
cancel: (ev) => {
$('#modal-admin-users-create').removeClass('is-active');
vueCreateUser.email = '';
vueCreateUser.provider = 'local';
},
create: (ev) => {
el: '#modal-admin-users-create',
data: {
email: '',
provider: 'local',
password: '',
name: ''
},
methods: {
open: (ev) => {
$('#modal-admin-users-create').addClass('is-active')
$('#modal-admin-users-create input').first().focus()
},
cancel: (ev) => {
$('#modal-admin-users-create').removeClass('is-active')
vueCreateUser.email = ''
vueCreateUser.provider = 'local'
},
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
let vueDeleteUser = new Vue({
el: '#modal-admin-users-delete',
data: {
el: '#modal-admin-users-delete',
data: {
},
methods: {
open: (ev) => {
$('#modal-admin-users-delete').addClass('is-active');
},
cancel: (ev) => {
$('#modal-admin-users-delete').removeClass('is-active');
},
deleteUser: (ev) => {
vueDeleteUser.cancel();
}
}
});
},
methods: {
open: (ev) => {
$('#modal-admin-users-delete').addClass('is-active')
},
cancel: (ev) => {
$('#modal-admin-users-delete').removeClass('is-active')
},
deleteUser: (ev) => {
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) => {
$('#txt-create-prompt').val(suggestedCreatePath);
$('#modal-create-prompt').toggleClass('is-active');
setInputSelection($('#txt-create-prompt').get(0), currentBasePath.length + 1, suggestedCreatePath.length);
$('#txt-create-prompt').removeClass('is-danger').next().addClass('is-hidden');
});
$('#txt-create-prompt').val(suggestedCreatePath)
$('#modal-create-prompt').toggleClass('is-active')
setInputSelection($('#txt-create-prompt').get(0), currentBasePath.length + 1, suggestedCreatePath.length)
$('#txt-create-prompt').removeClass('is-danger').next().addClass('is-hidden')
})
$('#txt-create-prompt').on('keypress', (ev) => {
if(ev.which === 13) {
$('.btn-create-go').trigger('click');
}
});
if (ev.which === 13) {
$('.btn-create-go').trigger('click')
}
})
$('.btn-create-go').on('click', (ev) => {
let newDocPath = makeSafePath($('#txt-create-prompt').val());
if(_.isEmpty(newDocPath)) {
$('#txt-create-prompt').addClass('is-danger').next().removeClass('is-hidden');
} else {
$('#txt-create-prompt').parent().addClass('is-loading');
window.location.assign('/create/' + newDocPath);
}
});
let newDocPath = makeSafePath($('#txt-create-prompt').val())
if (_.isEmpty(newDocPath)) {
$('#txt-create-prompt').addClass('is-danger').next().removeClass('is-hidden')
} else {
$('#txt-create-prompt').parent().addClass('is-loading')
window.location.assign('/create/' + newDocPath)
}
})

View File

@ -1,49 +1,46 @@
//-> Move Existing Document
// -> Move Existing Document
if(currentBasePath !== '') {
$('.btn-move-prompt').removeClass('is-hidden');
if (currentBasePath !== '') {
$('.btn-move-prompt').removeClass('is-hidden')
}
let moveInitialDocument = _.lastIndexOf(currentBasePath, '/') + 1;
let moveInitialDocument = _.lastIndexOf(currentBasePath, '/') + 1
$('.btn-move-prompt').on('click', (ev) => {
$('#txt-move-prompt').val(currentBasePath);
$('#modal-move-prompt').toggleClass('is-active');
setInputSelection($('#txt-move-prompt').get(0), moveInitialDocument, currentBasePath.length);
$('#txt-move-prompt').removeClass('is-danger').next().addClass('is-hidden');
});
$('#txt-move-prompt').val(currentBasePath)
$('#modal-move-prompt').toggleClass('is-active')
setInputSelection($('#txt-move-prompt').get(0), moveInitialDocument, currentBasePath.length)
$('#txt-move-prompt').removeClass('is-danger').next().addClass('is-hidden')
})
$('#txt-move-prompt').on('keypress', (ev) => {
if(ev.which === 13) {
$('.btn-move-go').trigger('click');
}
});
if (ev.which === 13) {
$('.btn-move-go').trigger('click')
}
})
$('.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());
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');
$.ajax(window.location.href, {
data: {
move: newDocPath
},
dataType: 'json',
method: 'PUT'
}).then((rData, rStatus, rXHR) => {
if(rData.ok) {
window.location.assign('/' + newDocPath);
} else {
alerts.pushError('Something went wrong', rData.error);
}
}, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.');
});
}
});
$.ajax(window.location.href, {
data: {
move: newDocPath
},
dataType: 'json',
method: 'PUT'
}).then((rData, rStatus, rXHR) => {
if (rData.ok) {
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({
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;
}
});
// =include ../modals/admin-users-create.js
} 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) {
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
}
// =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) => {
$('#modal-create-discard').toggleClass('is-active');
});
//=include ../components/editor.js
}
// =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) => {
$('#modal-edit-discard').toggleClass('is-active');
});
//=include ../components/editor.js
}
// =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");
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') : ''
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 router = express.Router();
const Promise = require('bluebird');
const validator = require('validator');
const _ = require('lodash');
var express = require('express')
var router = express.Router()
const Promise = require('bluebird')
const validator = require('validator')
const _ = require('lodash')
/**
* Admin
*/
router.get('/', (req, res) => {
res.redirect('/admin/profile');
});
res.redirect('/admin/profile')
})
router.get('/profile', (req, res) => {
if (res.locals.isGuest) {
return res.render('error-forbidden')
}
if(res.locals.isGuest) {
return res.render('error-forbidden');
}
res.render('pages/admin/profile', { adminTab: 'profile' });
});
res.render('pages/admin/profile', { adminTab: 'profile' })
})
router.post('/profile', (req, res) => {
if (res.locals.isGuest) {
return res.render('error-forbidden')
}
if(res.locals.isGuest) {
return res.render('error-forbidden');
}
return db.User.findById(req.user.id).then((usr) => {
usr.name = _.trim(req.body.name);
if(usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password);
if(nPwd.length < 6) {
return Promise.reject(new Error('New Password too short!'))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd;
return usr.save();
});
}
} else {
return usr.save();
}
}).then(() => {
return res.json({ msg: 'OK' });
}).catch((err) => {
res.status(400).json({ msg: err.message });
})
});
return db.User.findById(req.user.id).then((usr) => {
usr.name = _.trim(req.body.name)
if (usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password)
if (nPwd.length < 6) {
return Promise.reject(new Error('New Password too short!'))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd
return usr.save()
})
}
} else {
return usr.save()
}
}).then(() => {
return res.json({ msg: 'OK' })
}).catch((err) => {
res.status(400).json({ msg: err.message })
})
})
router.get('/stats', (req, res) => {
if (res.locals.isGuest) {
return res.render('error-forbidden')
}
if(res.locals.isGuest) {
return res.render('error-forbidden');
}
Promise.all([
db.Entry.count(),
db.UplFile.count(),
db.User.count()
]).spread((totalEntries, totalUploads, totalUsers) => {
return res.render('pages/admin/stats', {
totalEntries, totalUploads, totalUsers,
adminTab: 'stats'
}) || true;
}).catch((err) => {
throw err;
});
});
Promise.all([
db.Entry.count(),
db.UplFile.count(),
db.User.count()
]).spread((totalEntries, totalUploads, totalUsers) => {
return res.render('pages/admin/stats', {
totalEntries, totalUploads, totalUsers, adminTab: 'stats'
}) || true
}).catch((err) => {
throw err
})
})
router.get('/users', (req, res) => {
if (!res.locals.rights.manage) {
return res.render('error-forbidden')
}
if(!res.locals.rights.manage) {
return res.render('error-forbidden');
}
db.User.find({})
.select('-password -rights')
.sort('name email')
.exec().then((usrs) => {
res.render('pages/admin/users', { adminTab: 'users', usrs });
});
});
db.User.find({})
.select('-password -rights')
.sort('name email')
.exec().then((usrs) => {
res.render('pages/admin/users', { adminTab: 'users', usrs })
})
})
router.get('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.render('error-forbidden')
}
if(!res.locals.rights.manage) {
return res.render('error-forbidden');
}
if (!validator.isMongoId(req.params.id)) {
return res.render('error-forbidden')
}
if(!validator.isMongoId(req.params.id)) {
return res.render('error-forbidden');
}
db.User.findById(req.params.id)
.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)
.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 });
});
});
res.render('pages/admin/users-edit', { adminTab: 'users', usr, usrOpts })
})
})
router.post('/users/:id', (req, res) => {
if (!res.locals.rights.manage) {
return res.status(401).json({ msg: 'Unauthorized' })
}
if(!res.locals.rights.manage) {
return res.status(401).json({ msg: 'Unauthorized' });
}
if (!validator.isMongoId(req.params.id)) {
return res.status(400).json({ msg: 'Invalid User ID' })
}
if(!validator.isMongoId(req.params.id)) {
return res.status(400).json({ msg: 'Invalid User ID' });
}
return db.User.findById(req.params.id).then((usr) => {
usr.name = _.trim(req.body.name);
usr.rights = JSON.parse(req.body.rights);
if(usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password);
if(nPwd.length < 6) {
return Promise.reject(new Error('New Password too short!'))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd;
return usr.save();
});
}
} else {
return usr.save();
}
}).then(() => {
return res.json({ msg: 'OK' });
}).catch((err) => {
res.status(400).json({ msg: err.message });
})
});
return db.User.findById(req.params.id).then((usr) => {
usr.name = _.trim(req.body.name)
usr.rights = JSON.parse(req.body.rights)
if (usr.provider === 'local' && req.body.password !== '********') {
let nPwd = _.trim(req.body.password)
if (nPwd.length < 6) {
return Promise.reject(new Error('New Password too short!'))
} else {
return db.User.hashPassword(nPwd).then((pwd) => {
usr.password = pwd
return usr.save()
})
}
} else {
return usr.save()
}
}).then(() => {
return res.json({ msg: 'OK' })
}).catch((err) => {
res.status(400).json({ msg: err.message })
})
})
router.get('/settings', (req, res) => {
if (!res.locals.rights.manage) {
return res.render('error-forbidden')
}
if(!res.locals.rights.manage) {
return res.render('error-forbidden');
}
res.render('pages/admin/settings', { adminTab: 'settings' })
})
res.render('pages/admin/settings', { adminTab: 'settings' });
});
module.exports = router;
module.exports = router

View File

@ -1,80 +1,80 @@
var express = require('express');
var router = express.Router();
var passport = require('passport');
var ExpressBrute = require('express-brute');
var ExpressBruteMongooseStore = require('express-brute-mongoose');
var moment = require('moment');
'use strict'
const express = require('express')
const router = express.Router()
const passport = require('passport')
const ExpressBrute = require('express-brute')
const ExpressBruteMongooseStore = require('express-brute-mongoose')
const moment = require('moment')
/**
* Setup Express-Brute
*/
var EBstore = new ExpressBruteMongooseStore(db.Bruteforce);
var bruteforce = new ExpressBrute(EBstore, {
freeRetries: 5,
minWait: 60 * 1000,
maxWait: 5 * 60 * 1000,
refreshTimeoutOnRequest: false,
failCallback(req, res, next, nextValidRequestDate) {
req.flash('alert', {
class: 'error',
title: 'Too many attempts!',
message: "You've made too many failed attempts in a short period of time, please try again " + moment(nextValidRequestDate).fromNow() + '.',
iconClass: 'fa-times'
});
res.redirect('/login');
}
});
const EBstore = new ExpressBruteMongooseStore(db.Bruteforce)
const bruteforce = new ExpressBrute(EBstore, {
freeRetries: 5,
minWait: 60 * 1000,
maxWait: 5 * 60 * 1000,
refreshTimeoutOnRequest: false,
failCallback (req, res, next, nextValidRequestDate) {
req.flash('alert', {
class: 'error',
title: 'Too many attempts!',
message: "You've made too many failed attempts in a short period of time, please try again " + moment(nextValidRequestDate).fromNow() + '.',
iconClass: 'fa-times'
})
res.redirect('/login')
}
})
/**
* Login form
*/
router.get('/login', function(req, res, next) {
res.render('auth/login', {
usr: res.locals.usr
});
});
router.get('/login', function (req, res, next) {
res.render('auth/login', {
usr: res.locals.usr
})
})
router.post('/login', bruteforce.prevent, function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
router.post('/login', bruteforce.prevent, function (req, res, next) {
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.flash('alert', {
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.logIn(user, function (err) {
if (err) { return next(err) }
req.brute.reset(function () {
return res.redirect('/');
});
});
})(req, res, next);
});
return res.redirect('/')
})
})
})(req, res, next)
})
/**
* Social Login
*/
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/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }));
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/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
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/facebook/callback', passport.authenticate('facebook', { 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/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
/**
* Logout
*/
router.get('/logout', function(req, res) {
req.logout();
res.redirect('/');
});
router.get('/logout', function (req, res) {
req.logout()
res.redirect('/')
})
module.exports = router;
module.exports = router

View File

@ -1,8 +1,8 @@
"use strict";
'use strict'
var express = require('express');
var router = express.Router();
var _ = require('lodash');
const express = require('express')
const router = express.Router()
const _ = require('lodash')
// ==========================================
// EDIT MODE
@ -12,132 +12,123 @@ var _ = require('lodash');
* Edit document in Markdown
*/
router.get('/edit/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.render('error-forbidden')
}
if(!res.locals.rights.write) {
return res.render('error-forbidden');
}
let safePath = entries.parsePath(_.replace(req.path, '/edit', ''))
let safePath = entries.parsePath(_.replace(req.path, '/edit', ''));
entries.fetchOriginal(safePath, {
parseMarkdown: false,
parseMeta: true,
parseTree: false,
includeMarkdown: true,
includeParentInfo: false,
cache: false
}).then((pageData) => {
if(pageData) {
res.render('pages/edit', { pageData });
} else {
throw new Error('Invalid page path.');
}
return true;
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
});
});
});
entries.fetchOriginal(safePath, {
parseMarkdown: false,
parseMeta: true,
parseTree: false,
includeMarkdown: true,
includeParentInfo: false,
cache: false
}).then((pageData) => {
if (pageData) {
res.render('pages/edit', { pageData })
} else {
throw new Error('Invalid page path.')
}
return true
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
})
})
})
router.put('/edit/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
})
}
if(!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
});
}
let safePath = entries.parsePath(_.replace(req.path, '/edit', ''))
let safePath = entries.parsePath(_.replace(req.path, '/edit', ''));
entries.update(safePath, req.body.markdown).then(() => {
return res.json({
ok: true
}) || true;
}).catch((err) => {
res.json({
ok: false,
error: err.message
});
});
});
entries.update(safePath, req.body.markdown).then(() => {
return res.json({
ok: true
}) || true
}).catch((err) => {
res.json({
ok: false,
error: err.message
})
})
})
// ==========================================
// CREATE MODE
// ==========================================
router.get('/create/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.render('error-forbidden')
}
if(!res.locals.rights.write) {
return res.render('error-forbidden');
}
if (_.some(['create', 'edit', 'account', 'source', 'history', 'mk'], (e) => { return _.startsWith(req.path, '/create/' + e) })) {
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); })) {
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', ''));
let safePath = entries.parsePath(_.replace(req.path, '/create', ''))
entries.exists(safePath).then((docExists) => {
if(!docExists) {
return entries.getStarter(safePath).then((contents) => {
entries.exists(safePath).then((docExists) => {
if (!docExists) {
return entries.getStarter(safePath).then((contents) => {
let pageData = {
markdown: contents,
meta: {
title: _.startCase(safePath),
path: safePath
}
}
res.render('pages/create', { pageData })
let pageData = {
markdown: contents,
meta: {
title: _.startCase(safePath),
path: safePath
}
};
res.render('pages/create', { pageData });
return true;
}).catch((err) => {
throw new Error('Could not load starter content!');
});
} else {
throw new Error('This entry already exists!');
}
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
});
});
});
return true
}).catch((err) => {
winston.warn(err)
throw new Error('Could not load starter content!')
})
} else {
throw new Error('This entry already exists!')
}
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
})
})
})
router.put('/create/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
})
}
if(!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
});
}
let safePath = entries.parsePath(_.replace(req.path, '/create', ''))
let safePath = entries.parsePath(_.replace(req.path, '/create', ''));
entries.create(safePath, req.body.markdown).then(() => {
return res.json({
ok: true
}) || true;
}).catch((err) => {
return res.json({
ok: false,
error: err.message
});
});
});
entries.create(safePath, req.body.markdown).then(() => {
return res.json({
ok: true
}) || true
}).catch((err) => {
return res.json({
ok: false,
error: err.message
})
})
})
// ==========================================
// VIEW MODE
@ -147,102 +138,94 @@ router.put('/create/*', (req, res, next) => {
* View source of a document
*/
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,
parseMeta: true,
parseTree: false,
includeMarkdown: true,
includeParentInfo: false,
cache: false
}).then((pageData) => {
if(pageData) {
res.render('pages/source', { pageData });
} else {
throw new Error('Invalid page path.');
}
return true;
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
});
});
});
entries.fetchOriginal(safePath, {
parseMarkdown: false,
parseMeta: true,
parseTree: false,
includeMarkdown: true,
includeParentInfo: false,
cache: false
}).then((pageData) => {
if (pageData) {
res.render('pages/source', { pageData })
} else {
throw new Error('Invalid page path.')
}
return true
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
})
})
})
/**
* View document
*/
router.get('/*', (req, res, next) => {
let safePath = entries.parsePath(req.path)
let safePath = entries.parsePath(req.path);
entries.fetch(safePath).then((pageData) => {
if(pageData) {
res.render('pages/view', { pageData });
} else {
res.render('error-notexist', {
newpath: safePath
});
}
return true;
}).error((err) => {
if(safePath === 'home') {
res.render('pages/welcome');
} else {
res.render('error-notexist', {
message: err.message,
newpath: safePath
});
}
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
});
});
});
entries.fetch(safePath).then((pageData) => {
if (pageData) {
res.render('pages/view', { pageData })
} else {
res.render('error-notexist', {
newpath: safePath
})
}
return true
}).error((err) => {
if (safePath === 'home') {
res.render('pages/welcome')
} else {
res.render('error-notexist', {
message: err.message,
newpath: safePath
})
}
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
})
})
})
/**
* Move document
*/
router.put('/*', (req, res, next) => {
if (!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
})
}
if(!res.locals.rights.write) {
return res.json({
ok: false,
error: 'Forbidden'
});
}
let safePath = entries.parsePath(req.path)
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)) {
return res.json({
ok: false,
error: 'Invalid document action call.'
});
}
let safeNewPath = entries.parsePath(req.body.move)
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(() => {
res.json({
ok: true
});
}).catch((err) => {
res.json({
ok: false,
error: err.message
});
});
});
module.exports = router;
module.exports = router

View File

@ -1,184 +1,160 @@
"use strict";
'use strict'
var express = require('express');
var router = express.Router();
const express = require('express')
const router = express.Router()
var readChunk = require('read-chunk'),
fileType = require('file-type'),
Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra')),
path = require('path'),
_ = require('lodash');
const readChunk = require('read-chunk')
const fileType = require('file-type')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const path = require('path')
const _ = require('lodash')
var validPathRe = new RegExp("^([a-z0-9\\/-]+\\.[a-z0-9]+)$");
var validPathThumbsRe = new RegExp("^([0-9]+\\.png)$");
const validPathRe = new RegExp('^([a-z0-9\\/-]+\\.[a-z0-9]+)$')
const validPathThumbsRe = new RegExp('^([0-9]+\\.png)$')
// ==========================================
// SERVE UPLOADS FILES
// ==========================================
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];
if(!validPathThumbsRe.test(fileName)) {
return res.sendStatus(404).end();
}
// todo: Authentication-based access
//todo: Authentication-based access
res.sendFile(fileName, {
root: lcdata.getThumbsPath(),
dotfiles: 'deny'
}, (err) => {
if (err) {
res.status(err.status).end();
}
});
});
res.sendFile(fileName, {
root: lcdata.getThumbsPath(),
dotfiles: 'deny'
}, (err) => {
if (err) {
res.status(err.status).end()
}
})
})
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) => {
if(!destFolderPath) {
res.json({ ok: false, msg: 'Invalid Folder' });
return true;
}
Promise.map(req.files, (f) => {
let destFilename = ''
let destFilePath = ''
Promise.map(req.files, (f) => {
return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
destFilename = fname
destFilePath = path.resolve(destFolderPath, destFilename)
let destFilename = '';
let destFilePath = '';
return readChunk(f.path, 0, 262)
}).then((buf) => {
// -> Check MIME type by magic number
return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
destFilename = fname;
destFilePath = path.resolve(destFolderPath, destFilename);
let mimeInfo = fileType(buf)
if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
return Promise.reject(new Error('Invalid file type.'))
}
return true
}).then(() => {
// -> Move file to final destination
return readChunk(f.path, 0, 262);
}).then((buf) => {
//-> Check MIME type by magic number
let mimeInfo = fileType(buf);
if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
return Promise.reject(new Error('Invalid file type.'));
}
return true;
}).then(() => {
//-> Move file to final destination
return fs.moveAsync(f.path, destFilePath, { clobber: false });
}).then(() => {
return {
ok: true,
filename: destFilename,
filesize: f.size
};
}).reflect();
}, {concurrency: 3}).then((results) => {
let uplResults = _.map(results, (r) => {
if(r.isFulfilled()) {
return r.value();
} else {
return {
ok: false,
msg: r.reason().message
};
}
});
res.json({ ok: true, results: uplResults });
return true;
}).catch((err) => {
res.json({ ok: false, msg: err.message });
return true;
});
});
});
return fs.moveAsync(f.path, destFilePath, { clobber: false })
}).then(() => {
return {
ok: true,
filename: destFilename,
filesize: f.size
}
}).reflect()
}, {concurrency: 3}).then((results) => {
let uplResults = _.map(results, (r) => {
if (r.isFulfilled()) {
return r.value()
} else {
return {
ok: false,
msg: r.reason().message
}
}
})
res.json({ ok: true, results: uplResults })
return true
}).catch((err) => {
res.json({ ok: false, msg: err.message })
return true
})
})
})
router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
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) => {
if(!destFolderPath) {
res.json({ ok: false, msg: 'Invalid Folder' });
return true;
}
Promise.map(req.files, (f) => {
let destFilename = ''
let destFilePath = ''
Promise.map(req.files, (f) => {
return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
destFilename = fname
destFilePath = path.resolve(destFolderPath, destFilename)
let destFilename = '';
let destFilePath = '';
// -> Move file to final destination
return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
destFilename = fname;
destFilePath = path.resolve(destFolderPath, destFilename);
//-> Move file to final destination
return fs.moveAsync(f.path, destFilePath, { clobber: false });
}).then(() => {
return {
ok: true,
filename: destFilename,
filesize: f.size
};
}).reflect();
}, {concurrency: 3}).then((results) => {
let uplResults = _.map(results, (r) => {
if(r.isFulfilled()) {
return r.value();
} else {
return {
ok: false,
msg: r.reason().message
};
}
});
res.json({ ok: true, results: uplResults });
return true;
}).catch((err) => {
res.json({ ok: false, msg: err.message });
return true;
});
});
});
return fs.moveAsync(f.path, destFilePath, { clobber: false })
}).then(() => {
return {
ok: true,
filename: destFilename,
filesize: f.size
}
}).reflect()
}, {concurrency: 3}).then((results) => {
let uplResults = _.map(results, (r) => {
if (r.isFulfilled()) {
return r.value()
} else {
return {
ok: false,
msg: r.reason().message
}
}
})
res.json({ ok: true, results: uplResults })
return true
}).catch((err) => {
res.json({ ok: false, msg: err.message })
return true
})
})
})
router.get('/*', (req, res, next) => {
let fileName = req.params[0]
if (!validPathRe.test(fileName)) {
return res.sendStatus(404).end()
}
let fileName = req.params[0];
if(!validPathRe.test(fileName)) {
return res.sendStatus(404).end();
}
// todo: Authentication-based access
//todo: Authentication-based access
res.sendFile(fileName, {
root: git.getRepoPath() + '/uploads/',
dotfiles: 'deny'
}, (err) => {
if (err) {
res.status(err.status).end()
}
})
})
res.sendFile(fileName, {
root: git.getRepoPath() + '/uploads/',
dotfiles: 'deny'
}, (err) => {
if (err) {
res.status(err.status).end();
}
});
});
module.exports = router;
module.exports = router

View File

@ -1,95 +1,95 @@
"use strict";
'use strict'
const _ = require('lodash')
module.exports = (socket) => {
if(!socket.request.user.logged_in) {
return;
if (!socket.request.user.logged_in) {
return
}
//-----------------------------------------
// -----------------------------------------
// SEARCH
//-----------------------------------------
// -----------------------------------------
socket.on('search', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
entries.search(data.terms).then((results) => {
return cb(results) || true;
});
});
return cb(results) || true
})
})
//-----------------------------------------
// -----------------------------------------
// UPLOADS
//-----------------------------------------
// -----------------------------------------
socket.on('uploadsGetFolders', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.getUploadsFolders().then((f) => {
return cb(f) || true;
});
});
return cb(f) || true
})
})
socket.on('uploadsCreateFolder', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.createUploadsFolder(data.foldername).then((f) => {
return cb(f) || true;
});
});
return cb(f) || true
})
})
socket.on('uploadsGetImages', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.getUploadsFiles('image', data.folder).then((f) => {
return cb(f) || true;
});
});
return cb(f) || true
})
})
socket.on('uploadsGetFiles', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.getUploadsFiles('binary', data.folder).then((f) => {
return cb(f) || true;
});
});
return cb(f) || true
})
})
socket.on('uploadsDeleteFile', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.deleteUploadsFile(data.uid).then((f) => {
return cb(f) || true;
});
});
return cb(f) || true
})
})
socket.on('uploadsFetchFileFromURL', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.downloadFromUrl(data.folder, data.fetchUrl).then((f) => {
return cb({ ok: true }) || true;
return cb({ ok: true }) || true
}).catch((err) => {
return cb({
ok: false,
msg: err.message
}) || true;
});
});
return cb({
ok: false,
msg: err.message
}) || true
})
})
socket.on('uploadsRenameFile', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.moveUploadsFile(data.uid, data.folder, data.filename).then((f) => {
return cb({ ok: true }) || true;
return cb({ ok: true }) || true
}).catch((err) => {
return cb({
ok: false,
msg: err.message
}) || true;
});
});
return cb({
ok: false,
msg: err.message
}) || true
})
})
socket.on('uploadsMoveFile', (data, cb) => {
cb = cb || _.noop;
cb = cb || _.noop
upl.moveUploadsFile(data.uid, data.folder).then((f) => {
return cb({ ok: true }) || true;
return cb({ ok: true }) || true
}).catch((err) => {
return cb({
ok: false,
msg: err.message
}) || true;
});
});
};
return cb({
ok: false,
msg: err.message
}) || true
})
})
}

View File

@ -1,215 +1,210 @@
var gulp = require("gulp");
var watch = require('gulp-watch');
var merge = require('merge-stream');
var babel = require("gulp-babel");
var uglify = require('gulp-uglify');
var concat = require('gulp-concat');
var nodemon = require('gulp-nodemon');
var plumber = require('gulp-plumber');
var zip = require('gulp-zip');
var tar = require('gulp-tar');
var gzip = require('gulp-gzip');
var sass = require('gulp-sass');
var cleanCSS = require('gulp-clean-css');
var include = require("gulp-include");
var run = require('run-sequence');
var _ = require('lodash');
'use strict'
const gulp = require('gulp')
const watch = require('gulp-watch')
const merge = require('merge-stream')
const babel = require('gulp-babel')
const uglify = require('gulp-uglify')
const concat = require('gulp-concat')
const nodemon = require('gulp-nodemon')
const plumber = require('gulp-plumber')
const zip = require('gulp-zip')
const tar = require('gulp-tar')
const gzip = require('gulp-gzip')
const sass = require('gulp-sass')
const cleanCSS = require('gulp-clean-css')
const include = require('gulp-include')
const run = require('run-sequence')
/**
* Paths
*
* @type {Object}
*/
var paths = {
scripts: {
combine: [
'./node_modules/socket.io-client/dist/socket.io.min.js',
'./node_modules/jquery/dist/jquery.min.js',
'./node_modules/vue/dist/vue.min.js',
'./node_modules/jquery-smooth-scroll/jquery.smooth-scroll.min.js',
'./node_modules/jquery-simple-upload/simpleUpload.min.js',
'./node_modules/jquery-contextmenu/dist/jquery.contextMenu.min.js',
'./node_modules/sticky-js/dist/sticky.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/ext-modelist.js',
'./node_modules/ace-builds/src-min-noconflict/mode-markdown.js',
'./node_modules/ace-builds/src-min-noconflict/theme-tomorrow_night.js',
'./node_modules/filesize.js/dist/filesize.min.js',
'./node_modules/lodash/lodash.min.js'
],
ace: [
'./node_modules/ace-builds/src-min-noconflict/mode-*.js',
'!./node_modules/ace-builds/src-min-noconflict/mode-markdown.js'
],
compile: [
'./client/js/*.js'
],
watch: [
'./client/js/**/*.js'
]
},
css: {
combine: [
'./node_modules/highlight.js/styles/tomorrow.css',
'./node_modules/simplemde/dist/simplemde.min.css'
],
compile: [
'./client/scss/*.scss'
],
includes: [
'./node_modules/requarks-core' //! MUST BE LAST
],
watch: [
'./client/scss/**/*.scss',
'../core/core-client/scss/**/*.scss'
]
},
fonts: [
'../node_modules/requarks-core/core-client/fonts/**/*' //! MUST BE LAST
],
deploy: [
'./**/*',
'!node_modules', '!node_modules/**',
'!coverage', '!coverage/**',
'!client/js', '!client/js/**',
'!client/scss', '!client/scss/**',
'!dist', '!dist/**',
'!tests', '!tests/**',
'!data', '!data/**',
'!repo', '!repo/**',
'!.babelrc', '!.gitattributes', '!.gitignore', '!.snyk', '!.travis.yml',
'!gulpfile.js', '!inch.json', '!config.yml', '!wiki.sublime-project'
]
};
const paths = {
scripts: {
combine: [
'./node_modules/socket.io-client/dist/socket.io.min.js',
'./node_modules/jquery/dist/jquery.min.js',
'./node_modules/vue/dist/vue.min.js',
'./node_modules/jquery-smooth-scroll/jquery.smooth-scroll.min.js',
'./node_modules/jquery-simple-upload/simpleUpload.min.js',
'./node_modules/jquery-contextmenu/dist/jquery.contextMenu.min.js',
'./node_modules/sticky-js/dist/sticky.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/ext-modelist.js',
'./node_modules/ace-builds/src-min-noconflict/mode-markdown.js',
'./node_modules/ace-builds/src-min-noconflict/theme-tomorrow_night.js',
'./node_modules/filesize.js/dist/filesize.min.js',
'./node_modules/lodash/lodash.min.js'
],
ace: [
'./node_modules/ace-builds/src-min-noconflict/mode-*.js',
'!./node_modules/ace-builds/src-min-noconflict/mode-markdown.js'
],
compile: [
'./client/js/*.js'
],
watch: [
'./client/js/**/*.js'
]
},
css: {
combine: [
'./node_modules/highlight.js/styles/tomorrow.css',
'./node_modules/simplemde/dist/simplemde.min.css'
],
compile: [
'./client/scss/*.scss'
],
includes: [
'./node_modules/requarks-core' //! MUST BE LAST
],
watch: [
'./client/scss/**/*.scss',
'../core/core-client/scss/**/*.scss'
]
},
fonts: [
'../node_modules/requarks-core/core-client/fonts/**/*' //! MUST BE LAST
],
deploy: [
'./**/*',
'!node_modules', '!node_modules/**',
'!coverage', '!coverage/**',
'!client/js', '!client/js/**',
'!client/scss', '!client/scss/**',
'!dist', '!dist/**',
'!tests', '!tests/**',
'!data', '!data/**',
'!repo', '!repo/**',
'!.babelrc', '!.gitattributes', '!.gitignore', '!.snyk', '!.travis.yml',
'!gulpfile.js', '!inch.json', '!config.yml', '!wiki.sublime-project'
]
}
/**
* TASK - Starts server in development mode
*/
gulp.task('server', ['scripts', 'css', 'fonts'], function() {
nodemon({
script: './server',
ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'],
ext: 'js json',
env: { 'NODE_ENV': 'development' }
});
});
gulp.task('server', ['scripts', 'css', 'fonts'], function () {
nodemon({
script: './server',
ignore: ['assets/', 'client/', 'data/', 'repo/', 'tests/'],
ext: 'js json',
env: { 'NODE_ENV': 'development' }
})
})
/**
* TASK - Process all scripts processes
*/
gulp.task("scripts", ['scripts-libs', 'scripts-app']);
gulp.task('scripts', ['scripts-libs', 'scripts-app'])
/**
* 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)
.pipe(concat('libs.js', {newLine: ';\n'}))
.pipe(uglify({ mangle: false }))
.pipe(gulp.dest("./assets/js")),
gulp.src(paths.scripts.ace)
.pipe(gulp.dest('./assets/js/ace'))
gulp.src(paths.scripts.ace)
.pipe(gulp.dest("./assets/js/ace"))
);
});
)
})
/**
* TASK - Combine, make compatible and compress js app scripts
*/
gulp.task("scripts-app", function () {
return gulp.src(paths.scripts.compile)
.pipe(plumber())
.pipe(include({ extensions: "js" }))
.pipe(babel())
.pipe(uglify())
.pipe(plumber.stop())
.pipe(gulp.dest("./assets/js"));
});
gulp.task('scripts-app', function () {
return gulp.src(paths.scripts.compile)
.pipe(plumber())
.pipe(include({ extensions: 'js' }))
.pipe(babel())
.pipe(uglify())
.pipe(plumber.stop())
.pipe(gulp.dest('./assets/js'))
})
/**
* TASK - Process all css processes
*/
gulp.task("css", ['css-libs', 'css-app']);
gulp.task('css', ['css-libs', 'css-app'])
/**
* TASK - Combine css libraries
*/
gulp.task("css-libs", function () {
return gulp.src(paths.css.combine)
.pipe(plumber())
.pipe(concat('libs.css'))
.pipe(cleanCSS({ keepSpecialComments: 0 }))
.pipe(plumber.stop())
.pipe(gulp.dest("./assets/css"));
});
gulp.task('css-libs', function () {
return gulp.src(paths.css.combine)
.pipe(plumber())
.pipe(concat('libs.css'))
.pipe(cleanCSS({ keepSpecialComments: 0 }))
.pipe(plumber.stop())
.pipe(gulp.dest('./assets/css'))
})
/**
* TASK - Combine app css
*/
gulp.task("css-app", function () {
return gulp.src(paths.css.compile)
.pipe(plumber())
.pipe(sass.sync({ includePaths: paths.css.includes }))
.pipe(cleanCSS({ keepSpecialComments: 0 }))
.pipe(plumber.stop())
.pipe(gulp.dest("./assets/css"));
});
gulp.task('css-app', function () {
return gulp.src(paths.css.compile)
.pipe(plumber())
.pipe(sass.sync({ includePaths: paths.css.includes }))
.pipe(cleanCSS({ keepSpecialComments: 0 }))
.pipe(plumber.stop())
.pipe(gulp.dest('./assets/css'))
})
/**
* TASK - Copy web fonts
*/
gulp.task("fonts", function () {
return gulp.src(paths.fonts)
.pipe(gulp.dest("./assets/fonts"));
});
gulp.task('fonts', function () {
return gulp.src(paths.fonts)
.pipe(gulp.dest('./assets/fonts'))
})
/**
* TASK - Start dev watchers
*/
gulp.task('watch', function() {
return merge(
watch(paths.scripts.watch, {base: './'}, function() { return gulp.start('scripts-app'); }),
watch(paths.css.watch, {base: './'}, function() { return gulp.start('css-app'); })
);
});
gulp.task('watch', function () {
return merge(
watch(paths.scripts.watch, {base: './'}, function () { return gulp.start('scripts-app') }),
watch(paths.css.watch, {base: './'}, function () { return gulp.start('css-app') })
)
})
/**
* 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.css.includes.push('../core');
paths.fonts.pop()
paths.fonts.push('../core/core-client/fonts/**/*')
paths.fonts.pop();
paths.fonts.push('../core/core-client/fonts/**/*');
return run('default');
});
return run('default')
})
/**
* TASK - Creates deployment packages
*/
gulp.task('deploy', ['scripts', 'css', 'fonts'], function() {
var zipStream = gulp.src(paths.deploy)
.pipe(zip('wiki-js.zip'))
.pipe(gulp.dest('dist'));
gulp.task('deploy', ['scripts', 'css', 'fonts'], function () {
var zipStream = gulp.src(paths.deploy)
.pipe(zip('wiki-js.zip'))
.pipe(gulp.dest('dist'))
var targzStream = gulp.src(paths.deploy)
.pipe(tar('wiki-js.tar'))
.pipe(gzip())
.pipe(gulp.dest('dist'));
var targzStream = gulp.src(paths.deploy)
.pipe(tar('wiki-js.tar'))
.pipe(gzip())
.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'),
path = require('path'),
fs = Promise.promisifyAll(require("fs-extra")),
_ = require('lodash'),
farmhash = require('farmhash'),
moment = require('moment');
const Promise = require('bluebird')
const path = require('path')
const fs = Promise.promisifyAll(require('fs-extra'))
const _ = require('lodash')
const farmhash = require('farmhash')
/**
* Entries Model
*/
module.exports = {
_repoPath: 'repo',
_cachePath: 'data/cache',
/**
* Initialize Entries model
*
* @return {Object} Entries model instance
*/
init() {
let self = this;
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo);
self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache');
return self;
},
/**
* Check if a document already exists
*
* @param {String} entryPath The entry path
* @return {Promise<Boolean>} True if exists, false otherwise
*/
exists(entryPath) {
let self = this;
return self.fetchOriginal(entryPath, {
parseMarkdown: false,
parseMeta: false,
parseTree: false,
includeMarkdown: false,
includeParentInfo: false,
cache: false
}).then(() => {
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(entryPath) {
let self = this;
let cpath = self.getCachePath(entryPath);
return fs.statAsync(cpath).then((st) => {
return st.isFile();
}).catch((err) => {
return false;
}).then((isCache) => {
if(isCache) {
// Load from cache
return fs.readFileAsync(cpath).then((contents) => {
return JSON.parse(contents);
}).catch((err) => {
winston.error('Corrupted cache file. Deleting it...');
fs.unlinkSync(cpath);
return false;
});
} else {
// Load original
return self.fetchOriginal(entryPath);
}
});
},
/**
* Fetches the original document entry
*
* @param {String} entryPath The entry path
* @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);
options = _.defaults(options, {
parseMarkdown: true,
parseMeta: true,
parseTree: true,
includeMarkdown: false,
includeParentInfo: true,
cache: true
});
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
// Parse contents
let pageData = {
markdown: (options.includeMarkdown) ? contents : '',
html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
tree: (options.parseTree) ? mark.parseTree(contents) : []
};
if(!pageData.meta.title) {
pageData.meta.title = _.startCase(entryPath);
}
pageData.meta.path = entryPath;
// Get parent
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
return (pageData.parent = parentData);
}).catch((err) => {
return (pageData.parent = false);
}) : Promise.resolve(true);
return parentPromise.then(() => {
// Cache to disk
if(options.cache) {
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false);
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
winston.error('Unable to write to cache! Performance may be affected.');
return true;
});
} else {
return true;
}
}).return(pageData);
});
} else {
return false;
}
}).catch((err) => {
return Promise.reject(new Promise.OperationalError('Entry ' + entryPath + ' does not exist!'));
});
},
/**
* Parse raw url path and make it safe
*
* @param {String} urlPath The url path
* @return {String} Safe entry path
*/
parsePath(urlPath) {
let wlist = new RegExp('[^a-z0-9/\-]','g');
urlPath = _.toLower(urlPath).replace(wlist, '');
if(urlPath === '/') {
urlPath = 'home';
}
let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p); });
return _.join(urlParts, '/');
},
/**
* Gets the parent information.
*
* @param {String} entryPath The entry path
* @return {Promise<Object|False>} The parent information.
*/
getParentInfo(entryPath) {
let self = this;
if(_.includes(entryPath, '/')) {
let parentParts = _.initial(_.split(entryPath, '/'));
let parentPath = _.join(parentParts,'/');
let parentFile = _.last(parentParts);
let fpath = self.getFullPath(parentPath);
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let pageMeta = mark.parseMeta(contents);
return {
path: parentPath,
title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
};
});
} else {
return Promise.reject(new Error('Parent entry is not a valid file.'));
}
});
} else {
return Promise.reject(new Error('Parent entry is root.'));
}
},
/**
* Gets the full original path of a document.
*
* @param {String} entryPath The entry path
* @return {String} The full path.
*/
getFullPath(entryPath) {
return path.join(this._repoPath, entryPath + '.md');
},
/**
* Gets the full cache path of a document.
*
* @param {String} entryPath The entry path
* @return {String} The full cache path.
*/
getCachePath(entryPath) {
return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.json');
},
/**
* Gets the entry path from full path.
*
* @param {String} fullPath The full path
* @return {String} The entry path
*/
getEntryPathFromFullPath(fullPath) {
let absRepoPath = path.resolve(ROOTPATH, this._repoPath);
return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'),'/').value();
},
/**
* Update an existing document
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @return {Promise<Boolean>} True on success, false on failure
*/
update(entryPath, contents) {
let self = this;
let fpath = self.getFullPath(entryPath);
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
return self.makePersistent(entryPath, contents).then(() => {
return self.updateCache(entryPath);
});
} else {
return Promise.reject(new Error('Entry does not exist!'));
}
}).catch((err) => {
winston.error(err);
return Promise.reject(new Error('Failed to save document.'));
});
},
/**
* Update local cache and search index
*
* @param {String} entryPath The entry path
* @return {Promise} Promise of the operation
*/
updateCache(entryPath) {
let self = this;
return self.fetchOriginal(entryPath, {
parseMarkdown: true,
parseMeta: true,
parseTree: true,
includeMarkdown: true,
includeParentInfo: true,
cache: true
}).then((pageData) => {
return {
entryPath,
meta: pageData.meta,
parent: pageData.parent || {},
text: mark.removeMarkdown(pageData.markdown)
};
}).then((content) => {
return db.Entry.findOneAndUpdate({
_id: content.entryPath
}, {
_id: content.entryPath,
title: content.meta.title || content.entryPath,
subtitle: content.meta.subtitle || '',
parent: content.parent.title || '',
content: content.text || ''
}, {
new: true,
upsert: true
});
});
},
/**
* Create a new document
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @return {Promise<Boolean>} True on success, false on failure
*/
create(entryPath, contents) {
let self = this;
return self.exists(entryPath).then((docExists) => {
if(!docExists) {
return self.makePersistent(entryPath, contents).then(() => {
return self.updateCache(entryPath);
});
} else {
return Promise.reject(new Error('Entry already exists!'));
}
}).catch((err) => {
winston.error(err);
return Promise.reject(new Error('Something went wrong.'));
});
},
/**
* 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
*/
makePersistent(entryPath, contents) {
let self = this;
let fpath = self.getFullPath(entryPath);
return fs.outputFileAsync(fpath, contents).then(() => {
return git.commitDocument(entryPath);
});
},
/**
* Move a document
*
* @param {String} entryPath The current entry path
* @param {String} newEntryPath The new entry path
* @return {Promise} Promise of the operation
*/
move(entryPath, newEntryPath) {
let self = this;
if(_.isEmpty(entryPath) || entryPath === 'home') {
return Promise.reject(new Error('Invalid path!'));
}
return git.moveDocument(entryPath, newEntryPath).then(() => {
return git.commitDocument(newEntryPath).then(() => {
// Delete old cache version
let oldEntryCachePath = self.getCachePath(entryPath);
fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true; });
// Delete old index entry
ws.emit('searchDel', {
auth: WSInternalKey,
entryPath
});
// Create cache for new entry
return self.updateCache(newEntryPath);
});
});
},
/**
* Generate a starter page content based on the entry path
*
* @param {String} entryPath The entry path
* @return {Promise<String>} Starter content
*/
getStarter(entryPath) {
let self = this;
let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')));
return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {
return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle);
});
},
/**
* Searches entries based on terms.
*
* @param {String} terms The terms to search for
* @return {Promise<Object>} Promise of the search results
*/
search(terms) {
let self = this;
terms = _.chain(terms)
.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: []
};
});
}
};
_repoPath: 'repo',
_cachePath: 'data/cache',
/**
* Initialize Entries model
*
* @return {Object} Entries model instance
*/
init () {
let self = this
self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache')
return self
},
/**
* Check if a document already exists
*
* @param {String} entryPath The entry path
* @return {Promise<Boolean>} True if exists, false otherwise
*/
exists (entryPath) {
let self = this
return self.fetchOriginal(entryPath, {
parseMarkdown: false,
parseMeta: false,
parseTree: false,
includeMarkdown: false,
includeParentInfo: false,
cache: false
}).then(() => {
return true
}).catch((err) => { // eslint-disable-line handle-callback-err
return false
})
},
/**
* Fetch a document from cache, otherwise the original
*
* @param {String} entryPath The entry path
* @return {Promise<Object>} Page Data
*/
fetch (entryPath) {
let self = this
let cpath = self.getCachePath(entryPath)
return fs.statAsync(cpath).then((st) => {
return st.isFile()
}).catch((err) => { // eslint-disable-line handle-callback-err
return false
}).then((isCache) => {
if (isCache) {
// Load from cache
return fs.readFileAsync(cpath).then((contents) => {
return JSON.parse(contents)
}).catch((err) => { // eslint-disable-line handle-callback-err
winston.error('Corrupted cache file. Deleting it...')
fs.unlinkSync(cpath)
return false
})
} else {
// Load original
return self.fetchOriginal(entryPath)
}
})
},
/**
* Fetches the original document entry
*
* @param {String} entryPath The entry path
* @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)
options = _.defaults(options, {
parseMarkdown: true,
parseMeta: true,
parseTree: true,
includeMarkdown: false,
includeParentInfo: true,
cache: true
})
return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
// Parse contents
let pageData = {
markdown: (options.includeMarkdown) ? contents : '',
html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
tree: (options.parseTree) ? mark.parseTree(contents) : []
}
if (!pageData.meta.title) {
pageData.meta.title = _.startCase(entryPath)
}
pageData.meta.path = entryPath
// Get parent
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
return (pageData.parent = parentData)
}).catch((err) => { // eslint-disable-line handle-callback-err
return (pageData.parent = false)
}) : Promise.resolve(true)
return parentPromise.then(() => {
// Cache to disk
if (options.cache) {
let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
winston.error('Unable to write to cache! Performance may be affected.')
winston.error(err)
return true
})
} else {
return true
}
}).return(pageData)
})
} else {
return false
}
}).catch((err) => { // eslint-disable-line handle-callback-err
return Promise.reject(new Promise.OperationalError('Entry ' + entryPath + ' does not exist!'))
})
},
/**
* Parse raw url path and make it safe
*
* @param {String} urlPath The url path
* @return {String} Safe entry path
*/
parsePath (urlPath) {
let wlist = new RegExp('[^a-z0-9/-]', 'g')
urlPath = _.toLower(urlPath).replace(wlist, '')
if (urlPath === '/') {
urlPath = 'home'
}
let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p) })
return _.join(urlParts, '/')
},
/**
* Gets the parent information.
*
* @param {String} entryPath The entry path
* @return {Promise<Object|False>} The parent information.
*/
getParentInfo (entryPath) {
let self = this
if (_.includes(entryPath, '/')) {
let parentParts = _.initial(_.split(entryPath, '/'))
let parentPath = _.join(parentParts, '/')
let parentFile = _.last(parentParts)
let fpath = self.getFullPath(parentPath)
return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let pageMeta = mark.parseMeta(contents)
return {
path: parentPath,
title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
}
})
} else {
return Promise.reject(new Error('Parent entry is not a valid file.'))
}
})
} else {
return Promise.reject(new Error('Parent entry is root.'))
}
},
/**
* Gets the full original path of a document.
*
* @param {String} entryPath The entry path
* @return {String} The full path.
*/
getFullPath (entryPath) {
return path.join(this._repoPath, entryPath + '.md')
},
/**
* Gets the full cache path of a document.
*
* @param {String} entryPath The entry path
* @return {String} The full cache path.
*/
getCachePath (entryPath) {
return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.json')
},
/**
* Gets the entry path from full path.
*
* @param {String} fullPath The full path
* @return {String} The entry path
*/
getEntryPathFromFullPath (fullPath) {
let absRepoPath = path.resolve(ROOTPATH, this._repoPath)
return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'), '/').value()
},
/**
* Update an existing document
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @return {Promise<Boolean>} True on success, false on failure
*/
update (entryPath, contents) {
let self = this
let fpath = self.getFullPath(entryPath)
return fs.statAsync(fpath).then((st) => {
if (st.isFile()) {
return self.makePersistent(entryPath, contents).then(() => {
return self.updateCache(entryPath)
})
} else {
return Promise.reject(new Error('Entry does not exist!'))
}
}).catch((err) => {
winston.error(err)
return Promise.reject(new Error('Failed to save document.'))
})
},
/**
* Update local cache and search index
*
* @param {String} entryPath The entry path
* @return {Promise} Promise of the operation
*/
updateCache (entryPath) {
let self = this
return self.fetchOriginal(entryPath, {
parseMarkdown: true,
parseMeta: true,
parseTree: true,
includeMarkdown: true,
includeParentInfo: true,
cache: true
}).then((pageData) => {
return {
entryPath,
meta: pageData.meta,
parent: pageData.parent || {},
text: mark.removeMarkdown(pageData.markdown)
}
}).then((content) => {
return db.Entry.findOneAndUpdate({
_id: content.entryPath
}, {
_id: content.entryPath,
title: content.meta.title || content.entryPath,
subtitle: content.meta.subtitle || '',
parent: content.parent.title || '',
content: content.text || ''
}, {
new: true,
upsert: true
})
})
},
/**
* Create a new document
*
* @param {String} entryPath The entry path
* @param {String} contents The markdown-formatted contents
* @return {Promise<Boolean>} True on success, false on failure
*/
create (entryPath, contents) {
let self = this
return self.exists(entryPath).then((docExists) => {
if (!docExists) {
return self.makePersistent(entryPath, contents).then(() => {
return self.updateCache(entryPath)
})
} else {
return Promise.reject(new Error('Entry already exists!'))
}
}).catch((err) => {
winston.error(err)
return Promise.reject(new Error('Something went wrong.'))
})
},
/**
* 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
*/
makePersistent (entryPath, contents) {
let self = this
let fpath = self.getFullPath(entryPath)
return fs.outputFileAsync(fpath, contents).then(() => {
return git.commitDocument(entryPath)
})
},
/**
* Move a document
*
* @param {String} entryPath The current entry path
* @param {String} newEntryPath The new entry path
* @return {Promise} Promise of the operation
*/
move (entryPath, newEntryPath) {
let self = this
if (_.isEmpty(entryPath) || entryPath === 'home') {
return Promise.reject(new Error('Invalid path!'))
}
return git.moveDocument(entryPath, newEntryPath).then(() => {
return git.commitDocument(newEntryPath).then(() => {
// Delete old cache version
let oldEntryCachePath = self.getCachePath(entryPath)
fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
// Delete old index entry
ws.emit('searchDel', {
auth: WSInternalKey,
entryPath
})
// Create cache for new entry
return self.updateCache(newEntryPath)
})
})
},
/**
* Generate a starter page content based on the entry path
*
* @param {String} entryPath The entry path
* @return {Promise<String>} Starter content
*/
getStarter (entryPath) {
let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))
return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {
return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle)
})
},
/**
* Searches entries based on terms.
*
* @param {String} terms The terms to search for
* @return {Promise<Object>} Promise of the search results
*/
search (terms) {
terms = _.chain(terms)
.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"),
Promise = require('bluebird'),
path = require('path'),
os = require('os'),
fs = Promise.promisifyAll(require("fs")),
moment = require('moment'),
_ = require('lodash'),
URL = require('url');
const Git = require('git-wrapper2-promise')
const Promise = require('bluebird')
const path = require('path')
const fs = Promise.promisifyAll(require('fs'))
const _ = require('lodash')
const URL = require('url')
/**
* Git Model
*/
module.exports = {
_git: null,
_url: '',
_repo: {
path: '',
branch: 'master',
exists: false
},
_signature: {
name: 'Wiki',
email: 'user@example.com'
},
_opts: {
clone: {},
push: {}
},
onReady: null,
_git: null,
_url: '',
_repo: {
path: '',
branch: 'master',
exists: false
},
_signature: {
name: 'Wiki',
email: 'user@example.com'
},
_opts: {
clone: {},
push: {}
},
onReady: null,
/**
* Initialize Git model
*
* @return {Object} Git model instance
*/
init() {
/**
* Initialize Git model
*
* @return {Object} Git model instance
*/
init () {
let self = this
let self = this;
// -> Build repository path
//-> Build repository path
if(_.isEmpty(appconfig.paths.repo)) {
self._repo.path = path.join(ROOTPATH, 'repo');
} else {
self._repo.path = appconfig.paths.repo;
}
if (_.isEmpty(appconfig.paths.repo)) {
self._repo.path = path.join(ROOTPATH, '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.email = appconfig.git.signature.email || 'user@example.com';
self._signature.name = appconfig.git.signature.name || 'Wiki'
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
/**
* Initialize Git repository
*
* @param {Object} appconfig The application config
* @return {Object} Promise
*/
_initRepo(appconfig) {
winston.info('[' + PROCNAME + '][GIT] Checking Git repository...')
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) => {
if(err.code !== 'EEXIST') {
winston.error('[' + PROCNAME + '][GIT] Invalid Git repository path or missing permissions.');
}
}).then(() => {
return self._git.isRepo().then((isRepo) => {
self._repo.exists = isRepo
return (!isRepo) ? self._git.exec('init') : true
}).catch((err) => { // eslint-disable-line handle-callback-err
self._repo.exists = false
})
}).then(() => {
// Initialize remote
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;
return (!isRepo) ? self._git.exec('init') : true;
}).catch((err) => {
self._repo.exists = false;
});
/**
* Gets the repo path.
*
* @return {String} The repo path.
*/
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);
urlObj.auth = appconfig.git.auth.username + ((appconfig.git.auth.type !== 'ssh') ? ':' + appconfig.git.auth.password : '');
self._url = URL.format(urlObj);
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('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]);
});
}
});
return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
let out = cProc.stdout.toString()
}).catch((err) => {
winston.error('[' + PROCNAME + '][GIT] Git remote error!');
throw err;
}).then(() => {
winston.info('[' + PROCNAME + '][GIT] Git repository is OK.');
return true;
});
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
})
},
/**
* Gets the repo path.
*
* @return {String} The repo path.
*/
getRepoPath() {
/**
* 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 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'
/**
* Sync with the remote repository
*
* @return {Promise} Resolve on sync success
*/
resync() {
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
})
},
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
*/
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'),
Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra')),
multer = require('multer'),
os = require('os'),
_ = require('lodash');
const path = require('path')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const multer = require('multer')
const os = require('os')
const _ = require('lodash')
/**
* Local Data Storage
*/
module.exports = {
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
uploadImgHandler: null,
uploadImgHandler: null,
/**
* Initialize Local Data Storage model
*
* @return {Object} Local Data Storage model instance
*/
init() {
/**
* Initialize Local Data Storage model
*
* @return {Object} Local Data Storage model instance
*/
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._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
this.createBaseDirectories(appconfig)
this.initMulter(appconfig)
this.createBaseDirectories(appconfig);
this.initMulter(appconfig);
return this
},
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
/**
* Init Multer upload handlers
*
* @param {Object} appconfig The application config
* @return {boolean} Void
*/
initMulter(appconfig) {
this.uploadImgHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
}
}),
fileFilter: (req, f, cb) => {
// -> Check filesize
let maxFileSizes = {
img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
};
if (f.size > maxFileSizes.img) {
return cb(null, false)
}
//-> IMAGES
// -> Check MIME type (quick check only)
this.uploadImgHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'));
}
}),
fileFilter: (req, f, cb) => {
if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) {
return cb(null, false)
}
//-> Check filesize
cb(null, true)
}
}).array('imgfile', 20)
if(f.size > maxFileSizes.img) {
return cb(null, false);
}
// -> FILES
//-> 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)) {
return cb(null, false);
}
if (f.size > maxFileSizes.file) {
return cb(null, false)
}
cb(null, true);
}
}).array('imgfile', 20);
cb(null, true)
}
}).array('binfile', 20)
//-> FILES
return true
},
this.uploadFileHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'));
}
}),
fileFilter: (req, f, cb) => {
/**
* Creates a base directories (Synchronous).
*
* @param {Object} appconfig The application config
* @return {Void} Void
*/
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) {
return cb(null, false);
}
if (os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '644')
}
cb(null, true);
}
}).array('binfile', 20);
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo))
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'))
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.')
/**
* Creates a base directories (Synchronous).
*
* @param {Object} appconfig The application config
* @return {Void} Void
*/
createBaseDirectories(appconfig) {
return
},
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));
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'));
/**
* Gets the thumbnails folder path.
*
* @return {String} The thumbs path.
*/
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));
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'));
if (isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
fext = '.png'
}
if(os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './upload'), '644');
}
f = fname + fext
let fpath = path.resolve(this._uploadsPath, fld, f)
} catch (err) {
winston.error(err);
}
return fs.statAsync(fpath).then((s) => {
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'),
md = require('markdown-it'),
mdEmoji = require('markdown-it-emoji'),
mdTaskLists = require('markdown-it-task-lists'),
mdAbbr = require('markdown-it-abbr'),
mdAnchor = require('markdown-it-anchor'),
mdFootnote = require('markdown-it-footnote'),
mdExternalLinks = require('markdown-it-external-links'),
mdExpandTabs = require('markdown-it-expand-tabs'),
mdAttrs = require('markdown-it-attrs'),
hljs = require('highlight.js'),
cheerio = require('cheerio'),
_ = require('lodash'),
mdRemove = require('remove-markdown');
const md = require('markdown-it')
const mdEmoji = require('markdown-it-emoji')
const mdTaskLists = require('markdown-it-task-lists')
const mdAbbr = require('markdown-it-abbr')
const mdAnchor = require('markdown-it-anchor')
const mdFootnote = require('markdown-it-footnote')
const mdExternalLinks = require('markdown-it-external-links')
const mdExpandTabs = require('markdown-it-expand-tabs')
const mdAttrs = require('markdown-it-attrs')
const hljs = require('highlight.js')
const cheerio = require('cheerio')
const _ = require('lodash')
const mdRemove = require('remove-markdown')
// Load plugins
var mkdown = md({
html: true,
linkify: true,
typography: true,
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>';
} catch (err) {
return '<pre><code>' + str + '</code></pre>';
}
}
return '<pre><code>' + str + '</code></pre>';
}
})
.use(mdEmoji)
.use(mdTaskLists)
.use(mdAbbr)
.use(mdAnchor, {
slugify: _.kebabCase,
permalink: true,
permalinkClass: 'toc-anchor',
permalinkSymbol: '#',
permalinkBefore: true
})
.use(mdFootnote)
.use(mdExternalLinks, {
externalClassName: 'external-link',
internalClassName: 'internal-link'
})
.use(mdExpandTabs, {
tabWidth: 4
})
.use(mdAttrs);
html: true,
linkify: true,
typography: true,
highlight (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'
} catch (err) {
return '<pre><code>' + str + '</code></pre>'
}
}
return '<pre><code>' + str + '</code></pre>'
}
})
.use(mdEmoji)
.use(mdTaskLists)
.use(mdAbbr)
.use(mdAnchor, {
slugify: _.kebabCase,
permalink: true,
permalinkClass: 'toc-anchor',
permalinkSymbol: '#',
permalinkBefore: true
})
.use(mdFootnote)
.use(mdExternalLinks, {
externalClassName: 'external-link',
internalClassName: 'internal-link'
})
.use(mdExpandTabs, {
tabWidth: 4
})
.use(mdAttrs)
// Rendering rules
mkdown.renderer.rules.emoji = function(token, idx) {
return '<i class="twa twa-' + _.replace(token[idx].markup, /_/g, '-') + '"></i>';
};
mkdown.renderer.rules.emoji = function (token, idx) {
return '<i class="twa twa-' + _.replace(token[idx].markup, /_/g, '-') + '"></i>'
}
// Video rules
const videoRules = [
{
selector: 'a.youtube',
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>'
},
{
selector: 'a.vimeo',
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>'
},
{
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'),
output: '<iframe width="640" height="360" src="//www.dailymotion.com/embed/video/{0}?endscreen-enable=false" frameborder="0" allowfullscreen></iframe>'
},
{
selector: 'a.video',
regexp: false,
output: '<video width="640" height="360" controls preload="metadata"><source src="{0}" type="video/mp4"></video>'
}
{
selector: 'a.youtube',
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>'
},
{
selector: 'a.vimeo',
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>'
},
{
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'),
output: '<iframe width="640" height="360" src="//www.dailymotion.com/embed/video/{0}?endscreen-enable=false" frameborder="0" allowfullscreen></iframe>'
},
{
selector: 'a.video',
regexp: false,
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
*/
const parseTree = (content) => {
let tokens = md().parse(content, {})
let tocArray = []
let tokens = md().parse(content, {});
let tocArray = [];
// -> Extract headings and their respective levels
//-> 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++) {
if (tokens[i].type !== "heading_close") {
continue;
}
const heading = tokens[i - 1]
const headingclose = tokens[i]
const heading = tokens[i - 1];
const heading_close = tokens[i];
if (heading.type === 'inline') {
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") {
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, ""));
}
tocArray.push({
content,
anchor,
level: +headingclose.tag.substr(1, 1)
})
}
}
tocArray.push({
content,
anchor,
level: +heading_close.tag.substr(1, 1)
});
}
}
// -> Exclude levels deeper than 2
//-> 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;
if(v.level < 2) {
tree.push({
content: v.content,
anchor: v.anchor,
nodes: []
});
} else {
let lastNodeLevel = 1;
let GetNodePath = (startPos) => {
lastNodeLevel++;
if(_.isEmpty(startPos)) {
startPos = 'nodes';
}
if(lastNodeLevel === v.level) {
return startPos;
} else {
return GetNodePath(startPos + '[' + (_.at(tree[treeLength], startPos).length - 1) + '].nodes');
}
};
let lastNodePath = GetNodePath();
let lastNode = _.get(tree[treeLength], lastNodePath);
if(lastNode) {
lastNode.push({
content: v.content,
anchor: v.anchor,
nodes: []
});
_.set(tree[treeLength], lastNodePath, lastNode);
}
}
return tree;
}, []);
};
return _.reduce(tocArray, (tree, v) => {
let treeLength = tree.length - 1
if (v.level < 2) {
tree.push({
content: v.content,
anchor: v.anchor,
nodes: []
})
} else {
let lastNodeLevel = 1
let GetNodePath = (startPos) => {
lastNodeLevel++
if (_.isEmpty(startPos)) {
startPos = 'nodes'
}
if (lastNodeLevel === v.level) {
return startPos
} else {
return GetNodePath(startPos + '[' + (_.at(tree[treeLength], startPos).length - 1) + '].nodes')
}
}
let lastNodePath = GetNodePath()
let lastNode = _.get(tree[treeLength], lastNodePath)
if (lastNode) {
lastNode.push({
content: v.content,
anchor: v.anchor,
nodes: []
})
_.set(tree[treeLength], lastNodePath, lastNode)
}
}
return tree
}, [])
}
/**
* Parse markdown content to HTML
@ -172,87 +169,85 @@ const parseTree = (content) => {
* @param {String} content Markdown 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);
let cr = cheerio.load(output);
// -> Check for empty first element
//-> 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];
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
//-> 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) => {
let txtLink = cr(elm).text();
cr(elm).replaceWith(txtLink);
});
// -> Re-attach blockquote styling classes to their parents
//-> Re-attach blockquote styling classes to their parents
cr.root().children('blockquote').each((i, elm) => {
if(cr(elm).children().length > 0) {
let bqLastChild = cr(elm).children().last()[0];
let bqLastChildClasses = cr(bqLastChild).attr('class');
if(bqLastChildClasses && bqLastChildClasses.length > 0) {
cr(bqLastChild).removeAttr('class');
cr(elm).addClass(bqLastChildClasses);
}
}
});
cr.root().children('blockquote').each((i, elm) => {
if (cr(elm).children().length > 0) {
let bqLastChild = cr(elm).children().last()[0]
let bqLastChildClasses = cr(bqLastChild).attr('class')
if (bqLastChildClasses && bqLastChildClasses.length > 0) {
cr(bqLastChild).removeAttr('class')
cr(elm).addClass(bqLastChildClasses)
}
}
})
//-> Enclose content below headers
// -> Enclose content below headers
cr('h2').each((i, elm) => {
let subH2Content = cr(elm).nextUntil('h1, h2');
cr(elm).after('<div class="indent-h2"></div>');
let subH2Container = cr(elm).next('.indent-h2');
_.forEach(subH2Content, (ch) => {
cr(subH2Container).append(ch);
});
});
cr('h2').each((i, elm) => {
let subH2Content = cr(elm).nextUntil('h1, h2')
cr(elm).after('<div class="indent-h2"></div>')
let subH2Container = cr(elm).next('.indent-h2')
_.forEach(subH2Content, (ch) => {
cr(subH2Container).append(ch)
})
})
cr('h3').each((i, elm) => {
let subH3Content = cr(elm).nextUntil('h1, h2, h3');
cr(elm).after('<div class="indent-h3"></div>');
let subH3Container = cr(elm).next('.indent-h3');
_.forEach(subH3Content, (ch) => {
cr(subH3Container).append(ch);
});
});
cr('h3').each((i, elm) => {
let subH3Content = cr(elm).nextUntil('h1, h2, h3')
cr(elm).after('<div class="indent-h3"></div>')
let subH3Container = cr(elm).next('.indent-h3')
_.forEach(subH3Content, (ch) => {
cr(subH3Container).append(ch)
})
})
// Replace video links with embeds
// Replace video links with embeds
_.forEach(videoRules, (vrule) => {
cr(vrule.selector).each((i, elm) => {
let originLink = cr(elm).attr('href');
if(vrule.regexp) {
let vidMatches = originLink.match(vrule.regexp);
if((vidMatches && _.isArray(vidMatches))) {
vidMatches = _.filter(vidMatches, (f) => {
return f && _.isString(f);
});
originLink = _.last(vidMatches);
}
}
let processedLink = _.replace(vrule.output, '{0}', originLink);
cr(elm).replaceWith(processedLink);
});
});
_.forEach(videoRules, (vrule) => {
cr(vrule.selector).each((i, elm) => {
let originLink = cr(elm).attr('href')
if (vrule.regexp) {
let vidMatches = originLink.match(vrule.regexp)
if ((vidMatches && _.isArray(vidMatches))) {
vidMatches = _.filter(vidMatches, (f) => {
return f && _.isString(f)
})
originLink = _.last(vidMatches)
}
}
let processedLink = _.replace(vrule.output, '{0}', originLink)
cr(elm).replaceWith(processedLink)
})
})
output = cr.html();
output = cr.html()
return output;
};
return output
}
/**
* Parse meta-data tags from content
@ -261,58 +256,57 @@ const parseContent = (content) => {
* @return {Object} Properties found in the content and their values
*/
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');
let results = {}, match;
while(match = commentMeta.exec(content)) {
results[_.toLower(match[1])] = _.trim(match[2]);
}
return results;
};
return results
}
module.exports = {
/**
* Parse content and return all data
*
* @param {String} content Markdown-formatted content
* @return {Object} Object containing meta, html and tree data
*/
parse(content) {
return {
meta: parseMeta(content),
html: parseContent(content),
tree: parseTree(content)
};
},
/**
* Parse content and return all data
*
* @param {String} content Markdown-formatted content
* @return {Object} Object containing meta, html and tree data
*/
parse (content) {
return {
meta: parseMeta(content),
html: parseContent(content),
tree: parseTree(content)
}
},
parseContent,
parseMeta,
parseTree,
parseContent,
parseMeta,
parseTree,
/**
* Strips non-text elements from Markdown content
*
* @param {String} content Markdown-formatted content
* @return {String} Text-only version
*/
removeMarkdown(content) {
return mdRemove(_.chain(content)
.replace(/<!-- ?([a-zA-Z]+):(.*)-->/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(/\r?\n|\r/g, ' ')
.deburr()
.toLower()
.replace(/(\b([^a-z]+)\b)/g, ' ')
.replace(/[^a-z]+/g, ' ')
.replace(/(\b(\w{1,2})\b(\W|$))/g, '')
.replace(/\s\s+/g, ' ')
.value()
);
}
/**
* Strips non-text elements from Markdown content
*
* @param {String} content Markdown-formatted content
* @return {String} Text-only version
*/
removeMarkdown (content) {
return mdRemove(_.chain(content)
.replace(/<!-- ?([a-zA-Z]+):(.*)-->/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(/\r?\n|\r/g, ' ')
.deburr()
.toLower()
.replace(/(\b([^a-z]+)\b)/g, ' ')
.replace(/[^a-z]+/g, ' ')
.replace(/(\b(\w{1,2})\b(\W|$))/g, '')
.replace(/\s\s+/g, ' ')
.value()
)
}
};
}

View File

@ -1,292 +1,255 @@
"use strict";
'use strict'
var path = require('path'),
Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra')),
readChunk = require('read-chunk'),
fileType = require('file-type'),
mime = require('mime-types'),
farmhash = require('farmhash'),
moment = require('moment'),
chokidar = require('chokidar'),
sharp = require('sharp'),
_ = require('lodash');
const path = require('path')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const readChunk = require('read-chunk')
const fileType = require('file-type')
const mime = require('mime-types')
const farmhash = require('farmhash')
const chokidar = require('chokidar')
const sharp = require('sharp')
const _ = require('lodash')
/**
* Uploads - Agent
*/
module.exports = {
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
_watcher: null,
/**
* Initialize Uploads model
*
* @return {Object} Uploads model instance
*/
init() {
let self = this;
self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads');
self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
// Disable Sharp cache, as it cause file locks issues when deleting uploads.
sharp.cache(false);
return self;
},
/**
* Watch the uploads folder for changes
*
* @return {Void} Void
*/
watch() {
let self = this;
self._watcher = chokidar.watch(self._uploadsPath, {
persistent: true,
ignoreInitial: true,
cwd: self._uploadsPath,
depth: 1,
awaitWriteFinish: true
});
//-> Add new upload file
self._watcher.on('add', (p) => {
let pInfo = self.parseUploadsRelPath(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) => {
let pInfo = self.parseUploadsRelPath(p);
return git.commitUploads('Deleted/Renamed ' + p);
});
},
/**
* Initial Uploads scan
*
* @return {Promise<Void>} Promise of the scan operation
*/
initialScan() {
let self = this;
return fs.readdirAsync(self._uploadsPath).then((ls) => {
// Get all folders
return Promise.map(ls, (f) => {
return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s }; });
}).filter((s) => { return s.stat.isDirectory(); }).then((arrDirs) => {
let folderNames = _.map(arrDirs, 'filename');
folderNames.unshift('');
// Add folders to DB
return db.UplFolder.remove({}).then(() => {
return db.UplFolder.insertMany(_.map(folderNames, (f) => {
return {
_id: 'f:' + f,
name: f
};
}));
}).then(() => {
// Travel each directory and scan files
let allFiles = [];
return Promise.map(folderNames, (fldName) => {
let fldPath = path.join(self._uploadsPath, fldName);
return fs.readdirAsync(fldPath).then((fList) => {
return Promise.map(fList, (f) => {
return upl.processFile(fldName, f).then((mData) => {
if(mData) {
allFiles.push(mData);
}
return true;
});
}, {concurrency: 3});
});
}, {concurrency: 1}).finally(() => {
// Add files to DB
return db.UplFile.remove({}).then(() => {
if(_.isArray(allFiles) && allFiles.length > 0) {
return db.UplFile.insertMany(allFiles);
} else {
return true;
}
});
});
});
});
}).then(() => {
// Watch for new changes
return upl.watch();
});
},
/**
* Parse relative Uploads path
*
* @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
};
},
/**
* Get metadata from file and generate thumbnails if necessary
*
* @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;
let fldPath = path.join(self._uploadsPath, fldName);
let fPath = path.join(fldPath, f);
let fPathObj = path.parse(fPath);
let fUid = farmhash.fingerprint32(fldName + '/' + f);
return fs.statAsync(fPath).then((s) => {
if(!s.isFile()) { return false; }
// Get MIME info
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262));
if(_.isNil(mimeInfo)) {
mimeInfo = {
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
};
}
// Images
if(s.size < 3145728) { // ignore files larger than 3MB
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);
let mData = {
_id: fUid,
category: 'image',
mime: mimeInfo.mime,
extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
folder: 'f:' + fldName,
filename: f,
basename: fPathObj.name,
filesize: s.size
};
// Generate thumbnail
return fs.statAsync(cacheThumbnailPathStr).then((st) => {
return st.isFile();
}).catch((err) => {
return false;
}).then((thumbExists) => {
return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => {
return self.generateThumbnail(fPath, cacheThumbnailPathStr);
}).return(mData);
});
});
}
}
// Other Files
return {
_id: fUid,
category: 'binary',
mime: mimeInfo.mime,
folder: 'f:' + fldName,
filename: f,
basename: fPathObj.name,
filesize: s.size
};
});
},
/**
* 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();
}
};
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
_watcher: null,
/**
* Initialize Uploads model
*
* @return {Object} Uploads model instance
*/
init () {
let self = this
self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
// Disable Sharp cache, as it cause file locks issues when deleting uploads.
sharp.cache(false)
return self
},
/**
* Watch the uploads folder for changes
*
* @return {Void} Void
*/
watch () {
let self = this
self._watcher = chokidar.watch(self._uploadsPath, {
persistent: true,
ignoreInitial: true,
cwd: self._uploadsPath,
depth: 1,
awaitWriteFinish: true
})
// -> Add new upload file
self._watcher.on('add', (p) => {
let pInfo = self.parseUploadsRelPath(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)
})
},
/**
* Initial Uploads scan
*
* @return {Promise<Void>} Promise of the scan operation
*/
initialScan () {
let self = this
return fs.readdirAsync(self._uploadsPath).then((ls) => {
// Get all folders
return Promise.map(ls, (f) => {
return fs.statAsync(path.join(self._uploadsPath, f)).then((s) => { return { filename: f, stat: s } })
}).filter((s) => { return s.stat.isDirectory() }).then((arrDirs) => {
let folderNames = _.map(arrDirs, 'filename')
folderNames.unshift('')
// Add folders to DB
return db.UplFolder.remove({}).then(() => {
return db.UplFolder.insertMany(_.map(folderNames, (f) => {
return {
_id: 'f:' + f,
name: f
}
}))
}).then(() => {
// Travel each directory and scan files
let allFiles = []
return Promise.map(folderNames, (fldName) => {
let fldPath = path.join(self._uploadsPath, fldName)
return fs.readdirAsync(fldPath).then((fList) => {
return Promise.map(fList, (f) => {
return upl.processFile(fldName, f).then((mData) => {
if (mData) {
allFiles.push(mData)
}
return true
})
}, {concurrency: 3})
})
}, {concurrency: 1}).finally(() => {
// Add files to DB
return db.UplFile.remove({}).then(() => {
if (_.isArray(allFiles) && allFiles.length > 0) {
return db.UplFile.insertMany(allFiles)
} else {
return true
}
})
})
})
})
}).then(() => {
// Watch for new changes
return upl.watch()
})
},
/**
* Parse relative Uploads path
*
* @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
}
},
/**
* Get metadata from file and generate thumbnails if necessary
*
* @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
let fldPath = path.join(self._uploadsPath, fldName)
let fPath = path.join(fldPath, f)
let fPathObj = path.parse(fPath)
let fUid = farmhash.fingerprint32(fldName + '/' + f)
return fs.statAsync(fPath).then((s) => {
if (!s.isFile()) { return false }
// Get MIME info
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262))
if (_.isNil(mimeInfo)) {
mimeInfo = {
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
}
}
// Images
if (s.size < 3145728) { // ignore files larger than 3MB
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)
let mData = {
_id: fUid,
category: 'image',
mime: mimeInfo.mime,
extra: _.pick(mImgData, ['format', 'width', 'height', 'density', 'hasAlpha', 'orientation']),
folder: 'f:' + fldName,
filename: f,
basename: fPathObj.name,
filesize: s.size
}
// Generate thumbnail
return fs.statAsync(cacheThumbnailPathStr).then((st) => {
return st.isFile()
}).catch((err) => { // eslint-disable-line handle-callback-err
return false
}).then((thumbExists) => {
return (thumbExists) ? mData : fs.ensureDirAsync(cacheThumbnailPath.dir).then(() => {
return self.generateThumbnail(fPath, cacheThumbnailPathStr)
}).return(mData)
})
})
}
}
// Other Files
return {
_id: fUid,
category: 'binary',
mime: mimeInfo.mime,
folder: 'f:' + fldName,
filename: f,
basename: fPathObj.name,
filesize: s.size
}
})
},
/**
* 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'),
Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra')),
multer = require('multer'),
request = require('request'),
url = require('url'),
farmhash = require('farmhash'),
_ = require('lodash');
const path = require('path')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs-extra'))
const request = require('request')
const url = require('url')
const farmhash = require('farmhash')
const _ = require('lodash')
var regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
const maxDownloadFileSize = 3145728; // 3 MB
var regFolderName = new RegExp('^[a-z0-9][a-z0-9-]*[a-z0-9]$')
const maxDownloadFileSize = 3145728 // 3 MB
/**
* Uploads
*/
module.exports = {
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
_uploadsPath: './repo/uploads',
_uploadsThumbsPath: './data/thumbs',
/**
* Initialize Local Data Storage model
*
* @return {Object} Uploads model instance
*/
init() {
/**
* Initialize Local Data Storage model
*
* @return {Object} Uploads model instance
*/
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._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs');
return this
},
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.
*
* @return {String} The thumbs path.
*/
getThumbsPath() {
return this._uploadsThumbsPath;
},
/**
* Creates an uploads folder.
*
* @param {String} folderName The folder name
* @return {Promise} Promise of the operation
*/
createUploadsFolder (folderName) {
let self = this
/**
* 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: '' }];
});
},
folderName = _.kebabCase(_.trim(folderName))
/**
* Creates an uploads folder.
*
* @param {String} folderName The folder name
* @return {Promise} Promise of the operation
*/
createUploadsFolder(folderName) {
if (_.isEmpty(folderName) || !regFolderName.test(folderName)) {
return Promise.resolve(self.getUploadsFolders())
}
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({
_id: 'f:' + folderName
}, {
name: folderName
}, {
upsert: true
});
}).then(() => {
return self.getUploadsFolders();
});
/**
* Gets the uploads files.
*
* @param {String} cat Category type
* @param {String} fld Folder
* @return {Array<Object>} The files matching the query
*/
getUploadsFiles (cat, fld) {
return db.UplFile.find({
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
/**
* Check if folder is valid and exists
*
* @param {String} folderName The folder name
* @return {Boolean} True if valid
*/
validateUploadsFolder(folderName) {
return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
if (f) {
return self.deleteUploadsFileTry(f, 0)
}
return true
})
},
return db.UplFolder.findOne({ name: folderName }).then((f) => {
return (f) ? path.resolve(this._uploadsPath, folderName) : false;
});
deleteUploadsFileTry (f, attempt) {
let self = this
},
let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'
/**
* 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 Promise.join(
fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
).catch((err) => {
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
}
})
},
/**
* Gets the uploads files.
*
* @param {String} cat Category type
* @param {String} fld Folder
* @return {Array<Object>} The files matching the query
*/
getUploadsFiles(cat, fld) {
/**
* Downloads a file from url.
*
* @param {String} fFolder The folder
* @param {String} fUrl The full URL
* @return {Promise} Promise of the operation
*/
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({
category: cat,
folder: 'f:' + fld
}).sort('filename').exec();
return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if (!destFolderPath) {
return Promise.reject(new Error('Invalid Folder'))
}
},
return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
let destFilePath = path.resolve(destFolderPath, destFilename)
/**
* Deletes an uploads file.
*
* @param {string} uid The file unique ID
* @return {Promise} Promise of the operation
*/
deleteUploadsFile(uid) {
return new Promise((resolve, reject) => {
let rq = request({
url: fUrl,
method: 'GET',
followRedirect: true,
maxRedirects: 5,
timeout: 10000
})
let self = this;
let destFileStream = fs.createWriteStream(destFilePath)
let curFileSize = 0
return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
if(f) {
return self.deleteUploadsFileTry(f, 0);
}
return true;
});
},
rq.on('data', (data) => {
curFileSize += data.length
if (curFileSize > maxDownloadFileSize) {
rq.abort()
destFileStream.destroy()
fs.remove(destFilePath)
reject(new Error('Remote file is too large!'))
}
}).on('error', (err) => {
destFileStream.destroy()
fs.remove(destFilePath)
reject(err)
})
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(
fs.removeAsync(path.join(self._uploadsThumbsPath, f._id + '.png')),
fs.removeAsync(path.resolve(self._uploadsPath, fFolder, f.filename))
).catch((err) => {
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;
}
});
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)
}
/**
* Downloads a file from url.
*
* @param {String} fFolder The folder
* @param {String} fUrl The full URL
* @return {Promise} Promise of the operation
*/
downloadFromUrl(fFolder, fUrl) {
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 = []
let self = this;
// -> Check for invalid operations
let fUrlObj = url.parse(fUrl);
let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'));
let destFolder = _.chain(fFolder).trim().toLower().value();
if (sourceFilePath === destFilePath) {
return Promise.reject(new Error('Invalid Operation!'))
}
return upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if(!destFolderPath) {
return Promise.reject(new Error('Invalid Folder'));
}
// -> Delete DB entry
return lcdata.validateUploadsFilename(fUrlFilename, destFolder).then((destFilename) => {
let destFilePath = path.resolve(destFolderPath, destFilename);
preMoveOps.push(db.UplFile.findByIdAndRemove(uid))
return new Promise((resolve, reject) => {
// -> Move thumbnail ahead to avoid re-generation
let rq = request({
url: fUrl,
method: 'GET',
followRedirect: true,
maxRedirects: 5,
timeout: 10000
});
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))
}
let destFileStream = fs.createWriteStream(destFilePath);
let curFileSize = 0;
// -> Proceed to move actual file
rq.on('data', (data) => {
curFileSize += data.length;
if(curFileSize > maxDownloadFileSize) {
rq.abort();
destFileStream.destroy();
fs.remove(destFilePath);
reject(new Error('Remote file is too large!'));
}
}).on('error', (err) => {
destFileStream.destroy();
fs.remove(destFilePath);
reject(err);
});
return Promise.all(preMoveOps).then(() => {
return fs.moveAsync(sourceFilePath, destFilePath, {
clobber: false
})
})
})
})
} else {
return Promise.reject(new Error('Invalid Destination Folder'))
}
})
}
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'),
moment = require('moment-timezone');
const moment = require('moment-timezone')
/**
* Authentication middleware
@ -12,29 +11,27 @@ var Promise = require('bluebird'),
* @return {any} void
*/
module.exports = (req, res, next) => {
// Is user authenticated ?
// Is user authenticated ?
if (!req.isAuthenticated()) {
return res.redirect('/login')
}
if (!req.isAuthenticated()) {
return res.redirect('/login');
}
// Check permissions
// Check permissions
if (!rights.check(req, 'read')) {
return res.render('error-forbidden')
}
if(!rights.check(req, 'read')) {
return res.render('error-forbidden');
}
// Set i18n locale
// 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);
res.locals.userMoment = moment;
res.locals.userMoment.locale(req.user.lang);
// Expose user data
// 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
@ -9,9 +9,7 @@
* @return {any} void
*/
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
*
@ -6,23 +8,21 @@
* @param {Function} next next callback function
* @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
app.disable('x-powered-by');
// -> Disable Frame Embedding
res.set('X-Frame-Options', 'deny')
//-> Disable Frame Embedding
res.set('X-Frame-Options', 'deny');
// -> Re-enable XSS Fitler if disabled
res.set('X-XSS-Protection', '1; mode=block')
//-> Re-enable XSS Fitler if disabled
res.set('X-XSS-Protection', '1; mode=block');
// -> Disable MIME-sniffing
res.set('X-Content-Type-Options', 'nosniff')
//-> Disable MIME-sniffing
res.set('X-Content-Type-Options', 'nosniff');
// -> Disable IE Compatibility Mode
res.set('X-UA-Compatible', 'IE=edge')
//-> Disable IE Compatibility Mode
res.set('X-UA-Compatible', 'IE=edge');
return next();
};
return next()
}

View File

@ -1,4 +1,4 @@
"use strict";
'use strict'
/**
* BruteForce schema
@ -6,13 +6,13 @@
* @type {<Mongoose.Schema>}
*/
var bruteForceSchema = Mongoose.Schema({
_id: { type: String, index: 1 },
data: {
count: Number,
lastRequest: Date,
firstRequest: Date
},
expires: { type: Date, index: { expires: '1d' } }
});
_id: { type: String, index: 1 },
data: {
count: Number,
lastRequest: Date,
firstRequest: Date
},
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";
const Promise = require('bluebird'),
_ = require('lodash');
'use strict'
/**
* Entry schema
@ -10,7 +7,7 @@ const Promise = require('bluebird'),
*/
var entrySchema = Mongoose.Schema({
_id: String,
_id: String,
title: {
type: String,
@ -31,9 +28,9 @@ var entrySchema = Mongoose.Schema({
}
},
{
timestamps: {}
});
{
timestamps: {}
})
entrySchema.index({
_id: 'text',
@ -48,6 +45,6 @@ entrySchema.index({
content: 1
},
name: 'EntriesTextIndex'
});
})
module.exports = Mongoose.model('Entry', entrySchema);
module.exports = Mongoose.model('Entry', entrySchema)

View File

@ -1,7 +1,4 @@
"use strict";
const Promise = require('bluebird'),
_ = require('lodash');
'use strict'
/**
* Upload File schema
@ -10,7 +7,7 @@ const Promise = require('bluebird'),
*/
var uplFileSchema = Mongoose.Schema({
_id: String,
_id: String,
category: {
type: String,
@ -42,9 +39,6 @@ var uplFileSchema = Mongoose.Schema({
required: true
}
},
{
timestamps: {}
});
}, { timestamps: {} })
module.exports = Mongoose.model('UplFile', uplFileSchema);
module.exports = Mongoose.model('UplFile', uplFileSchema)

View File

@ -1,7 +1,4 @@
"use strict";
const Promise = require('bluebird'),
_ = require('lodash');
'use strict'
/**
* Upload Folder schema
@ -10,16 +7,13 @@ const Promise = require('bluebird'),
*/
var uplFolderSchema = Mongoose.Schema({
_id: String,
_id: String,
name: {
type: String,
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'),
bcrypt = require('bcryptjs-then'),
_ = require('lodash');
const Promise = require('bluebird')
const bcrypt = require('bcryptjs-then')
const _ = require('lodash')
/**
* Region schema
@ -11,78 +11,73 @@ const Promise = require('bluebird'),
*/
var userSchema = Mongoose.Schema({
email: {
type: String,
required: true,
index: true
},
email: {
type: String,
required: true,
index: true
},
provider: {
type: String,
required: true
},
provider: {
type: String,
required: true
},
providerId: {
type: String
},
providerId: {
type: String
},
password: {
type: String
},
password: {
type: String
},
name: {
type: String
},
name: {
type: String
},
rights: [{
role: String,
path: String,
exact: Boolean,
deny: Boolean
}]
rights: [{
role: String,
path: String,
exact: Boolean,
deny: Boolean
}]
},
{
timestamps: {}
});
}, { timestamps: {} })
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 = '';
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'));
}
return db.User.findOneAndUpdate({
email: primaryEmail,
provider: profile.provider
}, {
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.'));
});
};
return db.User.findOneAndUpdate({
email: primaryEmail,
provider: profile.provider
}, {
email: primaryEmail,
provider: profile.provider,
providerId: profile.id,
name: profile.displayName || _.split(primaryEmail, '@')[0]
}, {
new: true,
upsert: true
}).then((user) => {
return user || Promise.reject(new Error('User Upsert failed.'))
})
}
userSchema.statics.hashPassword = (rawPwd) => {
return bcrypt.hash(rawPwd);
};
return bcrypt.hash(rawPwd)
}
userSchema.methods.validatePassword = function(rawPwd) {
return bcrypt.compare(rawPwd, this.password).then((isValid) => {
return (isValid) ? true : Promise.reject(new Error('Invalid Login'));
});
};
userSchema.methods.validatePassword = function (rawPwd) {
return bcrypt.compare(rawPwd, this.password).then((isValid) => {
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",
"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
}

212
server.js
View File

@ -1,122 +1,124 @@
"use strict";
'use strict'
// ===========================================
// Wiki.js
// 1.0.0
// Licensed under AGPLv3
// ===========================================
global.PROCNAME = 'SERVER';
global.ROOTPATH = __dirname;
global.IS_DEBUG = process.env.NODE_ENV === 'development';
if(IS_DEBUG) {
global.CORE_PATH = ROOTPATH + '/../core/';
global.PROCNAME = 'SERVER'
global.ROOTPATH = __dirname
global.IS_DEBUG = process.env.NODE_ENV === 'development'
if (IS_DEBUG) {
global.CORE_PATH = ROOTPATH + '/../core/'
} else {
global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/';
global.CORE_PATH = ROOTPATH + '/node_modules/requarks-core/'
}
process.env.VIPS_WARNING = false
// ----------------------------------------
// Load Winston
// ----------------------------------------
global.winston = require(CORE_PATH + 'core-libs/winston')(IS_DEBUG);
winston.info('[SERVER] Wiki.js is initializing...');
global.winston = require(CORE_PATH + 'core-libs/winston')(IS_DEBUG)
winston.info('[SERVER] Wiki.js is initializing...')
// ----------------------------------------
// Load global modules
// ----------------------------------------
let appconf = require(CORE_PATH + 'core-libs/config')();
global.appconfig = appconf.config;
global.appdata = appconf.data;
global.lcdata = require('./libs/local').init();
global.db = require(CORE_PATH + 'core-libs/mongodb').init();
global.entries = require('./libs/entries').init();
global.git = require('./libs/git').init(false);
global.lang = require('i18next');
global.mark = require('./libs/markdown');
global.upl = require('./libs/uploads').init();
let appconf = require(CORE_PATH + 'core-libs/config')()
global.appconfig = appconf.config
global.appdata = appconf.data
global.lcdata = require('./libs/local').init()
global.db = require(CORE_PATH + 'core-libs/mongodb').init()
global.entries = require('./libs/entries').init()
global.git = require('./libs/git').init(false)
global.lang = require('i18next')
global.mark = require('./libs/markdown')
global.upl = require('./libs/uploads').init()
// ----------------------------------------
// Load modules
// ----------------------------------------
const _ = require('lodash');
const autoload = require('auto-load');
const bodyParser = require('body-parser');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const express = require('express');
const favicon = require('serve-favicon');
const flash = require('connect-flash');
const fork = require('child_process').fork;
const http = require('http');
const i18next_backend = require('i18next-node-fs-backend');
const i18next_mw = require('i18next-express-middleware');
const passport = require('passport');
const passportSocketIo = require('passport.socketio');
const path = require('path');
const session = require('express-session');
const sessionMongoStore = require('connect-mongo')(session);
const socketio = require('socket.io');
const autoload = require('auto-load')
const bodyParser = require('body-parser')
const compression = require('compression')
const cookieParser = require('cookie-parser')
const express = require('express')
const favicon = require('serve-favicon')
const flash = require('connect-flash')
const fork = require('child_process').fork
const http = require('http')
const i18nextBackend = require('i18next-node-fs-backend')
const i18nextMw = require('i18next-express-middleware')
const passport = require('passport')
const passportSocketIo = require('passport.socketio')
const path = require('path')
const session = require('express-session')
const SessionMongoStore = require('connect-mongo')(session)
const socketio = require('socket.io')
var mw = autoload(CORE_PATH + '/core-middlewares');
var ctrl = autoload(path.join(ROOTPATH, '/controllers'));
var libInternalAuth = require('./libs/internalAuth');
var mw = autoload(CORE_PATH + '/core-middlewares')
var ctrl = autoload(path.join(ROOTPATH, '/controllers'))
var libInternalAuth = require('./libs/internalAuth')
global.WSInternalKey = libInternalAuth.generateKey();
global.WSInternalKey = libInternalAuth.generateKey()
// ----------------------------------------
// Define Express App
// ----------------------------------------
global.app = express();
app.use(compression());
global.app = express()
app.use(compression())
// ----------------------------------------
// Security
// ----------------------------------------
app.use(mw.security);
app.use(mw.security)
// ----------------------------------------
// Public Assets
// ----------------------------------------
app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')));
app.use(express.static(path.join(ROOTPATH, 'assets')));
app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')))
app.use(express.static(path.join(ROOTPATH, 'assets')))
// ----------------------------------------
// Passport Authentication
// ----------------------------------------
var strategy = require(CORE_PATH + 'core-libs/auth')(passport);
global.rights = require(CORE_PATH + 'core-libs/rights');
rights.init();
require(CORE_PATH + 'core-libs/auth')(passport)
global.rights = require(CORE_PATH + 'core-libs/rights')
rights.init()
var sessionStore = new sessionMongoStore({
var sessionStore = new SessionMongoStore({
mongooseConnection: db.connection,
touchAfter: 15
});
})
app.use(cookieParser());
app.use(cookieParser())
app.use(session({
name: 'requarkswiki.sid',
store: sessionStore,
secret: appconfig.sessionSecret,
resave: false,
saveUninitialized: false
}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());
}))
app.use(flash())
app.use(passport.initialize())
app.use(passport.session())
// ----------------------------------------
// Localization Engine
// ----------------------------------------
lang
.use(i18next_backend)
.use(i18next_mw.LanguageDetector)
.use(i18nextBackend)
.use(i18nextMw.LanguageDetector)
.init({
load: 'languageOnly',
ns: ['common', 'auth'],
@ -124,94 +126,94 @@ lang
saveMissing: false,
supportedLngs: ['en', 'fr'],
preload: ['en', 'fr'],
fallbackLng : 'en',
fallbackLng: 'en',
backend: {
loadPath: './locales/{{lng}}/{{ns}}.json'
}
});
})
// ----------------------------------------
// View Engine Setup
// ----------------------------------------
app.use(i18next_mw.handle(lang));
app.set('views', path.join(ROOTPATH, 'views'));
app.set('view engine', 'pug');
app.use(i18nextMw.handle(lang))
app.set('views', path.join(ROOTPATH, 'views'))
app.set('view engine', 'pug')
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
// ----------------------------------------
// View accessible data
// ----------------------------------------
app.locals._ = require('lodash');
app.locals.moment = require('moment');
app.locals.appconfig = appconfig;
app.use(mw.flash);
app.locals._ = require('lodash')
app.locals.moment = require('moment')
app.locals.appconfig = appconfig
app.use(mw.flash)
// ----------------------------------------
// Controllers
// ----------------------------------------
app.use('/', ctrl.auth);
app.use('/', ctrl.auth)
app.use('/uploads', mw.auth, ctrl.uploads);
app.use('/admin', mw.auth, ctrl.admin);
app.use('/', mw.auth, ctrl.pages);
app.use('/uploads', mw.auth, ctrl.uploads)
app.use('/admin', mw.auth, ctrl.admin)
app.use('/', mw.auth, ctrl.pages)
// ----------------------------------------
// Error handling
// ----------------------------------------
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
app.use(function (req, res, next) {
var err = new Error('Not Found')
err.status = 404
next(err)
})
app.use(function(err, req, res, next) {
res.status(err.status || 500);
app.use(function (err, req, res, next) {
res.status(err.status || 500)
res.render('error', {
message: err.message,
error: IS_DEBUG ? err : {}
});
});
})
})
// ----------------------------------------
// 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);
var server = http.createServer(app);
var io = socketio(server);
app.set('port', appconfig.port)
var server = http.createServer(app)
var io = socketio(server)
server.listen(appconfig.port);
server.listen(appconfig.port)
server.on('error', (error) => {
if (error.syscall !== 'listen') {
throw error;
throw error
}
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error('Listening on port ' + appconfig.port + ' requires elevated privileges!');
process.exit(1);
break;
console.error('Listening on port ' + appconfig.port + ' requires elevated privileges!')
process.exit(1)
break
case 'EADDRINUSE':
console.error('Port ' + appconfig.port + ' is already in use!');
process.exit(1);
break;
console.error('Port ' + appconfig.port + ' is already in use!')
process.exit(1)
break
default:
throw error;
throw error
}
});
})
server.on('listening', () => {
winston.info('[SERVER] HTTP/WS server started successfully! [RUNNING]');
});
winston.info('[SERVER] HTTP/WS server started successfully! [RUNNING]')
})
// ----------------------------------------
// WebSocket
@ -224,21 +226,21 @@ io.use(passportSocketIo.authorize({
passport,
cookieParser,
success: (data, accept) => {
accept();
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
// ----------------------------------------
global.bgAgent = fork('agent.js');
global.bgAgent = fork('agent.js')
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
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')
section.hero.is-warning.is-fullheight
.hero-body
.container
a(href='/'): img(src='/favicons/android-icon-96x96.png')
h1.title(style={ 'margin-top': '30px'})= message
h2.subtitle(style={ 'margin-bottom': '50px'}) Oops, something went wrong
a.button.is-warning.is-inverted(href='/') Go Home
body(class='is-error')
.container
a(href='/'): img(src='/favicons/android-icon-96x96.png')
h1= message
h2 Oops, something went wrong
a.button.is-amber.is-inverted.is-featured(href='/') Go Home
if error.stack
section.section
.container.is-fluid
.content
h3 Detailed debug trail:
pre: code #{error.stack}
if error.stack
h3 Detailed debug trail:
pre: code #{error.stack}

View File

@ -53,6 +53,11 @@ block content
a(href='/admin')
i.icon-head
span Account
else
li
a(href='/login')
i.icon-unlock
span Login
aside.stickyscroll(data-margin-top=40)
.sidebar-label
i.icon-th-list