Sfoglia il codice sorgente

Standard JS code conversion + fixes

NGPixel 8 anni fa
parent
commit
414dc386d6
54 ha cambiato i file con 4215 aggiunte e 4513 eliminazioni
  1. 24 0
      CHANGELOG.md
  2. 154 173
      agent.js
  3. 0 0
      assets/css/app.css
  4. 0 0
      assets/css/error.css
  5. 0 0
      assets/css/libs.css
  6. 0 0
      assets/css/login.css
  7. 0 0
      assets/js/libs.js
  8. 32 34
      client/js/app.js
  9. 67 73
      client/js/components/alerts.js
  10. 61 65
      client/js/components/editor-codeblock.js
  11. 310 324
      client/js/components/editor-file.js
  12. 353 369
      client/js/components/editor-image.js
  13. 34 36
      client/js/components/editor-video.js
  14. 205 210
      client/js/components/editor.js
  15. 75 78
      client/js/components/search.js
  16. 14 14
      client/js/helpers/form.js
  17. 8 10
      client/js/helpers/pages.js
  18. 4 6
      client/js/login.js
  19. 23 25
      client/js/modals/admin-users-create.js
  20. 16 16
      client/js/modals/admin-users-delete.js
  21. 19 21
      client/js/modals/create.js
  22. 36 39
      client/js/modals/move.js
  23. 91 97
      client/js/pages/admin.js
  24. 8 10
      client/js/pages/create.js
  25. 8 10
      client/js/pages/edit.js
  26. 13 15
      client/js/pages/source.js
  27. 5 7
      client/js/pages/view.js
  28. 123 139
      controllers/admin.js
  29. 57 57
      controllers/auth.js
  30. 192 209
      controllers/pages.js
  31. 145 169
      controllers/uploads.js
  32. 58 58
      controllers/ws.js
  33. 154 159
      gulpfile.js
  34. 445 493
      libs/entries.js
  35. 224 251
      libs/git.js
  36. 14 20
      libs/internalAuth.js
  37. 169 180
      libs/local.js
  38. 273 279
      libs/markdown.js
  39. 249 286
      libs/uploads-agent.js
  40. 272 300
      libs/uploads.js
  41. 18 21
      middlewares/auth.js
  42. 4 6
      middlewares/flash.js
  43. 15 15
      middlewares/security.js
  44. 10 10
      models/bruteforce.js
  45. 7 10
      models/entry.js
  46. 4 10
      models/upl-file.js
  47. 4 10
      models/upl-folder.js
  48. 61 66
      models/user.js
  49. 30 0
      package.json
  50. 109 107
      server.js
  51. 3 0
      test/index.js
  52. 0 11
      tests/index.js
  53. 10 15
      views/error.pug
  54. 5 0
      views/pages/view.pug

+ 24 - 0
CHANGELOG.md

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

+ 154 - 173
agent.js

@@ -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 job = new cron({
-	cronTime: '0 */5 * * * *',
-	onTick: () => {
-
-		// 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;
-
-		// Prepare async job collector
-
-		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
-		// ----------------------------------------
-
-		//*****************************************
-		//-> Sync with Git remote
-		//*****************************************
-
-		jobs.push(git.onReady.then(() => {
-			return git.resync().then(() => {
-
-				//-> Stream all documents
-
-				let cacheJobs = [];
-				let jobCbStreamDocs_resolve = null,
-						jobCbStreamDocs = new Promise((resolve, reject) => {
-							jobCbStreamDocs_resolve = resolve;
-						});
-
-				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);
-						
-						//-> Purge outdated cache
-
-						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
-
-								if(fileStatus === 'expired') {
-									return fs.unlinkAsync(cachePath).return(fileStatus);
-								}
-
-								return fileStatus;
-
-							}).then((fileStatus) => {
-
-								//-> Update cache and search index
-
-								if(fileStatus !== 'active') {
-									return entries.updateCache(entryPath);
-								}
-
-								return true;
-
-							})
-
-						);
-
-					}
-				}).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
-});
-
+var jobIsBusy = false
+var jobUplWatchStarted = false
+
+var job = new Cron({
+  cronTime: '0 */5 * * * *',
+  onTick: () => {
+    // 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
+
+    // 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')
+
+    // ----------------------------------------
+    // REGULAR JOBS
+    // ----------------------------------------
+
+    //* ****************************************
+    // -> Sync with Git remote
+    //* ****************************************
+
+    jobs.push(git.onReady.then(() => {
+      return git.resync().then(() => {
+        // -> Stream all documents
+
+        let cacheJobs = []
+        let jobCbStreamDocsResolve = null
+        let jobCbStreamDocs = new Promise((resolve, reject) => {
+          jobCbStreamDocsResolve = resolve
+        })
+
+        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)
+
+            // -> Purge outdated cache
+
+            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
+
+                if (fileStatus === 'expired') {
+                  return fs.unlinkAsync(cachePath).return(fileStatus)
+                }
+
+                return fileStatus
+              }).then((fileStatus) => {
+                // -> Update cache and search index
+
+                if (fileStatus !== 'active') {
+                  return entries.updateCache(entryPath)
+                }
+
+                return true
+              })
+            )
+          }
+        }).on('end', () => {
+          jobCbStreamDocsResolve(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
+})
 
 // ----------------------------------------
 // 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 it is too large
+ 0 - 0
assets/css/app.css


File diff suppressed because it is too large
+ 0 - 0
assets/css/error.css


File diff suppressed because it is too large
+ 0 - 0
assets/css/libs.css


File diff suppressed because it is too large
+ 0 - 0
assets/css/login.css


File diff suppressed because it is too large
+ 0 - 0
assets/js/libs.js


+ 32 - 34
client/js/app.js

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

+ 67 - 73
client/js/components/alerts.js

@@ -1,4 +1,4 @@
-"use strict";
+'use strict'
 
 /**
  * Alerts
@@ -10,25 +10,23 @@ class Alerts {
 	 *
 	 * @class
 	 */
-	constructor() {
-
-		let self = this;
-
-		self.mdl = new Vue({
-			el: '#alerts',
-			data: {
-				children: []
-			},
-			methods: {
-				acknowledge: (uid) => {
-					self.close(uid);
-				}
-			}
-		});
-
-		self.uidNext = 1;
-
-	}
+  constructor () {
+    let self = this
+
+    self.mdl = new Vue({
+      el: '#alerts',
+      data: {
+        children: []
+      },
+      methods: {
+        acknowledge: (uid) => {
+          self.close(uid)
+        }
+      }
+    })
+
+    self.uidNext = 1
+  }
 
 	/**
 	 * Show a new Alert
@@ -36,29 +34,27 @@ class Alerts {
 	 * @param      {Object}  options  Alert properties
 	 * @return     {null}  Void
 	 */
-	push(options) {
-
-		let self = this;
-
-		let nAlert = _.defaults(options, {
-			_uid: self.uidNext,
-			class: 'info',
-			message: '---',
-			sticky: false,
-			title: '---'
-		});
+  push (options) {
+    let self = this
 
-		self.mdl.children.push(nAlert);
+    let nAlert = _.defaults(options, {
+      _uid: self.uidNext,
+      class: 'info',
+      message: '---',
+      sticky: false,
+      title: '---'
+    })
 
-		if(!nAlert.sticky) {
-			_.delay(() => {
-				self.close(nAlert._uid);
-			}, 5000);
-		}
+    self.mdl.children.push(nAlert)
 
-		self.uidNext++;
+    if (!nAlert.sticky) {
+      _.delay(() => {
+        self.close(nAlert._uid)
+      }, 5000)
+    }
 
-	}
+    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) {
-
-		let self = this;
-
-		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);
-		}
-
-	}
-
-}
+  close (uid) {
+    let self = this
+
+    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)
+    }
+  }
+
+}

+ 61 - 65
client/js/components/editor-codeblock.js

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

+ 310 - 324
client/js/components/editor-file.js

@@ -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: {
-
-		open: () => {
-			mdeModalOpenState = true;
-			$('#modal-editor-file').addClass('is-active');
-			vueFile.refreshFolders();
-		},
-		cancel: (ev) => {
-			mdeModalOpenState = false;
-			$('#modal-editor-file').removeClass('is-active');
-		},
+  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')
+    },
 
 		// -------------------------------------------
 		// 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) => {
-
-			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;
-			}
-
-			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;
-				});
-			});
-
-		},
+    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))
+
+      if (_.isEmpty(vueFile.newFolderName) || !regFolderName.test(vueFile.newFolderName)) {
+        vueFile.newFolderError = true
+        return
+      }
+
+      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
+        })
+      })
+    },
 
 		// -------------------------------------------
 		// RENAME FILE
 		// -------------------------------------------
 
-		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;
-
-			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);
-					}
-				});
-			});
-
-		},
+    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
+
+      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();
-		},
-
-		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);
-					});
-				});
-			});
-		},
-
-		waitChangeComplete: (oldAmount, expectChange) => {
-
-			expectChange = (_.isBoolean(expectChange)) ? expectChange : true;
-
-			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);
-					}
-				});
-			});
-
-		},
+    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()
+        })
+      })
+    },
+
+    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) => {
+      expectChange = (_.isBoolean(expectChange)) ? expectChange : true
+
+      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)
+          }
+        })
+      })
+    },
 
 		// -------------------------------------------
 		// 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);
-					}
-				};
-			});
-
-			$.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) => {
+    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)
+            }
+          }
+        }
+      })
+    }
+
+  }
+})
 
-	let curFileAmount = vueFile.files.length;
-
-	$(ev.currentTarget).simpleUpload("/uploads/file", {
-
-		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;
-		},
-
-		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);
-			}
-		},
-
-		error: (error) => {
-			alerts.pushError(error.message, this.upload.file.name);
-		},
-
-		finish: () => {
-			if(vueFile.uploadSucceeded) {
-				vueFile.waitChangeComplete(curFileAmount, true);
-			} else {
-				vueFile.isLoading = false;
-			}
-		}
-
-	});
-
-});
+$('#btn-editor-file-upload input').on('change', (ev) => {
+  let curFileAmount = vueFile.files.length
+
+  $(ev.currentTarget).simpleUpload('/uploads/file', {
+
+    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
+    },
+
+    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)
+      }
+    },
+
+    error: (error) => {
+      alerts.pushError(error.message, this.upload.file.name)
+    },
+
+    finish: () => {
+      if (vueFile.uploadSucceeded) {
+        vueFile.waitChangeComplete(curFileAmount, true)
+      } else {
+        vueFile.isLoading = false
+      }
+    }
+
+  })
+})

+ 353 - 369
client/js/components/editor-image.js

@@ -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: {
-
-		open: () => {
-			mdeModalOpenState = true;
-			$('#modal-editor-image').addClass('is-active');
-			vueImage.refreshFolders();
-		},
-		cancel: (ev) => {
-			mdeModalOpenState = false;
-			$('#modal-editor-image').removeClass('is-active');
-		},
+  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')
+    },
 
 		// -------------------------------------------
 		// INSERT IMAGE
 		// -------------------------------------------
 
-		selectImage: (imageId) => {
-			vueImage.currentImage = imageId;
-		},
-		insertImage: (ev) => {
-
-			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 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();
-
-		},
+    selectImage: (imageId) => {
+      vueImage.currentImage = imageId
+    },
+    insertImage: (ev) => {
+      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 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()
+    },
 
 		// -------------------------------------------
 		// 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) => {
-
-			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;
-			}
-
-			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;
-				});
-			});
-
-		},
+    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))
+
+      if (_.isEmpty(vueImage.newFolderName) || !regFolderName.test(vueImage.newFolderName)) {
+        vueImage.newFolderError = true
+        return
+      }
+
+      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
+        })
+      })
+    },
 
 		// -------------------------------------------
 		// 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;
-
-			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);
-					}
-				});
-			});
-
-		},
+    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)
+          }
+        })
+      })
+    },
 
 		// -------------------------------------------
 		// RENAME IMAGE
 		// -------------------------------------------
 
-		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;
-
-			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);
-					}
-				});
-			});
-
-		},
+    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
+
+      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();
-		},
-
-		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);
-					});
-				});
-			});
-		},
-
-		waitChangeComplete: (oldAmount, expectChange) => {
-
-			expectChange = (_.isBoolean(expectChange)) ? expectChange : true;
-
-			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);
-					}
-				});
-			});
-
-		},
+    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()
+        })
+      })
+    },
+
+    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) => {
+      expectChange = (_.isBoolean(expectChange)) ? expectChange : true
+
+      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)
+          }
+        })
+      })
+    },
 
 		// -------------------------------------------
 		// 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);
-					}
-				};
-			});
-
-			$.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) => {
+    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)
+            }
+          }
+        }
+      })
+    }
+
+  }
+})
 
-	let curImageAmount = vueImage.images.length;
-
-	$(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
-
-		init: (totalUploads) => {
-			vueImage.uploadSucceeded = false;
-			vueImage.isLoadingText = 'Preparing to upload...';
-			vueImage.isLoading = true;
-		},
-
-		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);
-			}
-		},
-
-		error: (error) => {
-			alerts.pushError(error.message, this.upload.file.name);
-		},
-
-		finish: () => {
-			if(vueImage.uploadSucceeded) {
-				vueImage.waitChangeComplete(curImageAmount, true);
-			} else {
-				vueImage.isLoading = false;
-			}
-		}
-
-	});
-
-});
+$('#btn-editor-image-upload input').on('change', (ev) => {
+  let curImageAmount = vueImage.images.length
+
+  $(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
+
+    init: (totalUploads) => {
+      vueImage.uploadSucceeded = false
+      vueImage.isLoadingText = 'Preparing to upload...'
+      vueImage.isLoading = true
+    },
+
+    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)
+      }
+    },
+
+    error: (error) => {
+      alerts.pushError(error.message, this.upload.file.name)
+    },
+
+    finish: () => {
+      if (vueImage.uploadSucceeded) {
+        vueImage.waitChangeComplete(curImageAmount, true)
+      } else {
+        vueImage.isLoading = false
+      }
+    }
+
+  })
+})

+ 34 - 36
client/js/components/editor-video.js

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

+ 205 - 210
client/js/components/editor.js

@@ -3,215 +3,210 @@
 // Markdown Editor
 // ====================================
 
-if($('#mk-editor').length === 1) {
-
-	let mdeModalOpenState = false;
-	let mdeCurrentEditor = null;
-
-	Vue.filter('filesize', (v) => {
-		return _.toUpper(filesize(v));
-	});
-
-	//=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) {
+if ($('#mk-editor').length === 1) {
+  let mdeModalOpenState = false
+  let mdeCurrentEditor = null
+
+  Vue.filter('filesize', (v) => {
+    return _.toUpper(filesize(v))
+  })
+
+	// =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) {
 						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) => {
-
-					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(mde.codemirror.doc.somethingSelected()) {
-							vueCodeBlock.initContent = mde.codemirror.doc.getSelection();
-						}
-
-						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
-		}
-	});
-
-	//-> 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;
-			}
-		}
-	});
-
-}
+					} */
+        },
+        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 (mde.codemirror.doc.somethingSelected()) {
+              vueCodeBlock.initContent = mde.codemirror.doc.getSelection()
+            }
+
+            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
+    }
+  })
+
+	// -> 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
+      }
+    }
+  })
+}

+ 75 - 78
client/js/components/search.js

@@ -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;
-
-				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--;
-				}
-			}
-		}
-	});
+																			'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
 
-	$('main').on('click', vueHeader.closeSearch);
+        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--
+        }
+      }
+    }
+  })
 
-}
+  $('main').on('click', vueHeader.closeSearch)
+}

+ 14 - 14
client/js/helpers/form.js

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

+ 8 - 10
client/js/helpers/pages.js

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

+ 4 - 6
client/js/login.js

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

+ 23 - 25
client/js/modals/admin-users-create.js

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

+ 16 - 16
client/js/modals/admin-users-delete.js

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

+ 19 - 21
client/js/modals/create.js

@@ -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)
+  }
+})

+ 36 - 39
client/js/modals/move.js

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

+ 91 - 97
client/js/pages/admin.js

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

+ 8 - 10
client/js/pages/create.js

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

+ 8 - 10
client/js/pages/edit.js

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

+ 13 - 15
client/js/pages/source.js

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

+ 5 - 7
client/js/pages/view.js

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

+ 123 - 139
controllers/admin.js

@@ -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');
-	}
-
-	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 });
-	})
-
-});
+  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 })
+  })
+})
 
 router.get('/stats', (req, res) => {
-
-	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;
-	});
-
-});
+  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
+  })
+})
 
 router.get('/users', (req, res) => {
-
-	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 });
-		});
-
-});
+  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 })
+    })
+})
 
 router.get('/users/:id', (req, res) => {
-
-	if(!res.locals.rights.manage) {
-		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))
-			};
-
-			res.render('pages/admin/users-edit', { adminTab: 'users', usr, usrOpts });
-		});
-
-});
+  if (!res.locals.rights.manage) {
+    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))
+      }
+
+      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(!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 });
-	})
-
-});
+  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' })
+  }
+
+  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

+ 57 - 57
controllers/auth.js

@@ -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.post('/login', bruteforce.prevent, function(req, res, next) {
-	passport.authenticate('local', function(err, user, info) {
+router.get('/login', function (req, res, next) {
+  res.render('auth/login', {
+    usr: res.locals.usr
+  })
+})
 
-		if (err) { return next(err); }
+router.post('/login', bruteforce.prevent, function (req, res, next) {
+  passport.authenticate('local', function (err, user, info) {
+    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

+ 192 - 209
controllers/pages.js

@@ -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');
-	}
-
-	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: {}
-		});
-	});
-
-});
+  if (!res.locals.rights.write) {
+    return res.render('error-forbidden')
+  }
+
+  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: {}
+    })
+  })
+})
 
 router.put('/edit/*', (req, res, next) => {
-
-	if(!res.locals.rights.write) {
-		return res.json({
-			ok: false,
-			error: 'Forbidden'
-		});
-	}
-
-	let safePath = entries.parsePath(_.replace(req.path, '/edit', ''));
-
-	entries.update(safePath, req.body.markdown).then(() => {
-		return res.json({
-			ok: true
-		}) || true;
-	}).catch((err) => {
-		res.json({
-			ok: false,
-			error: err.message
-		});
-	});
-
-});
+  if (!res.locals.rights.write) {
+    return res.json({
+      ok: false,
+      error: 'Forbidden'
+    })
+  }
+
+  let safePath = entries.parsePath(_.replace(req.path, '/edit', ''))
+
+  entries.update(safePath, req.body.markdown).then(() => {
+    return res.json({
+      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(_.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', ''));
-
-	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 });
-
-				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: {}
-		});
-	});
-
-});
+  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: {}
+    })
+  }
+
+  let safePath = entries.parsePath(_.replace(req.path, '/create', ''))
+
+  entries.exists(safePath).then((docExists) => {
+    if (!docExists) {
+      return entries.getStarter(safePath).then((contents) => {
+        let pageData = {
+          markdown: contents,
+          meta: {
+            title: _.startCase(safePath),
+            path: safePath
+          }
+        }
+        res.render('pages/create', { pageData })
+
+        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'
-		});
-	}
-
-	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
-		});
-	});
-
-});
+  if (!res.locals.rights.write) {
+    return res.json({
+      ok: false,
+      error: 'Forbidden'
+    })
+  }
+
+  let safePath = entries.parsePath(_.replace(req.path, '/create', ''))
+
+  entries.create(safePath, req.body.markdown).then(() => {
+    return res.json({
+      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', ''));
-
-	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: {}
-		});
-	});
-
-});
+  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: {}
+    })
+  })
+})
 
 /**
  * View document
  */
 router.get('/*', (req, res, next) => {
-
-	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: {}
-		});
-	});
-
-});
+  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: {}
+    })
+  })
+})
 
 /**
  * Move document
  */
 router.put('/*', (req, res, next) => {
-
-	if(!res.locals.rights.write) {
-		return res.json({
-			ok: false,
-			error: 'Forbidden'
-		});
-	}
-
-	let safePath = entries.parsePath(req.path);
-
-	if(_.isEmpty(req.body.move)) {
-		return res.json({
-			ok: false,
-			error: 'Invalid document action call.'
-		});
-	}
-
-	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
-		});
-	});
-
-});
-
-module.exports = router;
+  if (!res.locals.rights.write) {
+    return res.json({
+      ok: false,
+      error: 'Forbidden'
+    })
+  }
+
+  let safePath = entries.parsePath(req.path)
+
+  if (_.isEmpty(req.body.move)) {
+    return res.json({
+      ok: false,
+      error: 'Invalid document action call.'
+    })
+  }
+
+  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
+    })
+  })
+})
+
+module.exports = router

+ 145 - 169
controllers/uploads.js

@@ -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();
-	}
-
-	//todo: Authentication-based access
-
-	res.sendFile(fileName, {
-		root: lcdata.getThumbsPath(),
-		dotfiles: 'deny'
-	}, (err) => {
-		if (err) {
-			res.status(err.status).end();
-		}
-	});
-
-});
+  let fileName = req.params[0]
+  if (!validPathThumbsRe.test(fileName)) {
+    return res.sendStatus(404).end()
+  }
+
+  // todo: Authentication-based access
+
+  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();
-
-	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 = '';
-
-			return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
-				
-				destFilename = fname;
-				destFilePath = path.resolve(destFolderPath, destFilename);
-
-				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;
-		});
-
-	});
-
-});
+  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
+    }
+
+    Promise.map(req.files, (f) => {
+      let destFilename = ''
+      let destFilePath = ''
+
+      return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
+        destFilename = fname
+        destFilePath = path.resolve(destFolderPath, destFilename)
+
+        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
+    })
+  })
+})
 
 router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
-
-	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;
-		}
-
-		Promise.map(req.files, (f) => {
-
-			let destFilename = '';
-			let destFilePath = '';
-
-			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;
-		});
-
-	});
-
-});
+  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
+    }
+
+    Promise.map(req.files, (f) => {
+      let destFilename = ''
+      let destFilePath = ''
+
+      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
+    })
+  })
+})
 
 router.get('/*', (req, res, next) => {
-
-	let fileName = req.params[0];
-	if(!validPathRe.test(fileName)) {
-		return res.sendStatus(404).end();
-	}
-
-	//todo: Authentication-based access
-
-	res.sendFile(fileName, {
-		root: git.getRepoPath() + '/uploads/',
-		dotfiles: 'deny'
-	}, (err) => {
-		if (err) {
-			res.status(err.status).end();
-		}
-	});
-
-});
-
-module.exports = router;
+  let fileName = req.params[0]
+  if (!validPathRe.test(fileName)) {
+    return res.sendStatus(404).end()
+  }
+
+  // todo: Authentication-based access
+
+  res.sendFile(fileName, {
+    root: git.getRepoPath() + '/uploads/',
+    dotfiles: 'deny'
+  }, (err) => {
+    if (err) {
+      res.status(err.status).end()
+    }
+  })
+})
+
+module.exports = router

+ 58 - 58
controllers/ws.js

@@ -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
+    })
+  })
+}

+ 154 - 159
gulpfile.js

@@ -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('dev', function() {
-
-	paths.css.includes.pop();
-	paths.css.includes.push('../core');
+gulp.task('default', ['watch', 'server'])
 
-	paths.fonts.pop();
-	paths.fonts.push('../core/core-client/fonts/**/*');
+gulp.task('dev', function () {
+  paths.css.includes.pop()
+  paths.css.includes.push('../core')
 
-	return run('default');
+  paths.fonts.pop()
+  paths.fonts.push('../core/core-client/fonts/**/*')
 
-});
+  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'));
-
-	var targzStream = gulp.src(paths.deploy)
-		.pipe(tar('wiki-js.tar'))
-		.pipe(gzip())
-		.pipe(gulp.dest('dist'));
-
-	return merge(zipStream, targzStream);
-});
+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'))
+
+  return merge(zipStream, targzStream)
+})

+ 445 - 493
libs/entries.js

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

+ 224 - 251
libs/git.js

@@ -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,
-
-	/**
-	 * Initialize Git model
-	 *
-	 * @return     {Object}  Git model instance
-	 */
-	init() {
-
-		let self = this;
-
-		//-> Build repository path
-		
-		if(_.isEmpty(appconfig.paths.repo)) {
-			self._repo.path = path.join(ROOTPATH, 'repo');
-		} else {
-			self._repo.path = appconfig.paths.repo;
-		}
-
-		//-> Initialize repository
-
-		self.onReady = self._initRepo(appconfig);
-
-		// Define signature
-
-		self._signature.name = appconfig.git.signature.name || 'Wiki';
-		self._signature.email = appconfig.git.signature.email || 'user@example.com';
-
-		return self;
-
-	},
-
-	/**
-	 * Initialize Git repository
-	 *
-	 * @param      {Object}  appconfig  The application config
-	 * @return     {Object}  Promise
-	 */
-	_initRepo(appconfig) {
-
-		let self = this;
-
-		winston.info('[' + PROCNAME + '][GIT] Checking Git repository...');
-
-		//-> Check if path is accessible
-
-		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 already contains a git working folder
-
-			return self._git.isRepo().then((isRepo) => {
-				self._repo.exists = isRepo;
-				return (!isRepo) ? self._git.exec('init') : true;
-			}).catch((err) => {
-				self._repo.exists = false;
-			});
-
-		}).then(() => {
-
-			// Initialize remote
-
-			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);
-
-			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;
-		});
-
-	},
-
-	/**
-	 * Gets the repo path.
-	 *
-	 * @return     {String}  The repo path.
-	 */
-	getRepoPath() {
-
-		return this._repo.path || path.join(ROOTPATH, 'repo');
-
-	},
-
-	/**
-	 * Sync with the remote repository
-	 *
-	 * @return     {Promise}  Resolve on sync success
-	 */
-	resync() {
-
-		let self = this;
-
-		// Fetch
-
-		winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...');
-		return self._git.pull('origin', self._repo.branch).then((cProc) => {
-			winston.info('[' + PROCNAME + '][GIT] Pull completed.');
-		})
-		.catch((err) => {
-			winston.error('[' + PROCNAME + '][GIT] Unable to fetch from git origin!');
-			throw err;
-		})
-		.then(() => {
-
-			// Check for changes
-
-			return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
-				let out = cProc.stdout.toString();
-
-				if(_.includes(out, 'commit')) {
-
-					winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...');
-					return self._git.push('origin', self._repo.branch).then(() => {
-						return winston.info('[' + PROCNAME + '][GIT] Push completed.');
-					});
-
-				} else {
-
-					winston.info('[' + PROCNAME + '][GIT] Push skipped. Repository is already in sync.');
-
-				}
-
-				return true;
-
-			});
-
-		})
-		.catch((err) => {
-			winston.error('[' + PROCNAME + '][GIT] Unable to push changes to remote!');
-			throw err;
-		});
-
-	},
-
-	/**
-	 * Commits a document.
-	 *
-	 * @param      {String}   entryPath  The entry path
-	 * @return     {Promise}  Resolve on commit success
-	 */
-	commitDocument(entryPath) {
-
-		let self = this;
-		let gitFilePath = entryPath + '.md';
-		let commitMsg = '';
-
-		return self._git.exec('ls-files', gitFilePath).then((cProc) => {
-			let out = cProc.stdout.toString();
-			return _.includes(out, gitFilePath);
-		}).then((isTracked) => {
-			commitMsg = (isTracked) ? 'Updated ' + gitFilePath : 'Added ' + gitFilePath;
-			return self._git.add(gitFilePath);
-		}).then(() => {
-			return self._git.commit(commitMsg).catch((err) => {
-			  if(_.includes(err.stdout, 'nothing to commit')) { return true; }
-			});
-		});
-
-	},
-
-	/**
-	 * Move a document.
-	 *
-	 * @param      {String}            entryPath     The current entry path
-	 * @param      {String}            newEntryPath  The new entry path
-	 * @return     {Promise<Boolean>}  Resolve on success
-	 */
-	moveDocument(entryPath, newEntryPath) {
-
-		let self = this;
-		let gitFilePath = entryPath + '.md';
-		let gitNewFilePath = newEntryPath + '.md';
-
-		return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
-			let out = cProc.stdout.toString();
-			if(_.includes(out, 'fatal')) {
-				let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')));
-				throw new Error(errorMsg);
-			}
-			return true;
-		});
-
-	},
-
-	/**
-	 * Commits uploads changes.
-	 *
-	 * @param      {String}   msg     The commit message
-	 * @return     {Promise}  Resolve on commit success
-	 */
-	commitUploads(msg) {
-
-		let self = this;
-		msg = msg || "Uploads repository sync";
-
-		return self._git.add('uploads').then(() => {
-			return self._git.commit(msg).catch((err) => {
-			  if(_.includes(err.stdout, 'nothing to commit')) { return true; }
-			});
-		});
-
-	}
-
-};
+  _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 () {
+    let self = this
+
+    // -> Build repository path
+
+    if (_.isEmpty(appconfig.paths.repo)) {
+      self._repo.path = path.join(ROOTPATH, 'repo')
+    } else {
+      self._repo.path = appconfig.paths.repo
+    }
+
+    // -> Initialize repository
+
+    self.onReady = self._initRepo(appconfig)
+
+    // Define signature
+
+    self._signature.name = appconfig.git.signature.name || 'Wiki'
+    self._signature.email = appconfig.git.signature.email || 'user@example.com'
+
+    return self
+  },
+
+  /**
+   * Initialize Git repository
+   *
+   * @param      {Object}  appconfig  The application config
+   * @return     {Object}  Promise
+   */
+  _initRepo (appconfig) {
+    let self = this
+
+    winston.info('[' + PROCNAME + '][GIT] Checking Git repository...')
+
+    // -> Check if path is accessible
+
+    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 already contains a git working folder
+
+      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
+
+      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)
+
+      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
+    })
+  },
+
+  /**
+   * Gets the repo path.
+   *
+   * @return     {String}  The repo path.
+   */
+  getRepoPath () {
+    return this._repo.path || path.join(ROOTPATH, 'repo')
+  },
+
+  /**
+   * Sync with the remote repository
+   *
+   * @return     {Promise}  Resolve on sync success
+   */
+  resync () {
+    let self = this
+
+    // Fetch
+
+    winston.info('[' + PROCNAME + '][GIT] Performing pull from remote repository...')
+    return self._git.pull('origin', self._repo.branch).then((cProc) => {
+      winston.info('[' + PROCNAME + '][GIT] Pull completed.')
+    })
+    .catch((err) => {
+      winston.error('[' + PROCNAME + '][GIT] Unable to fetch from git origin!')
+      throw err
+    })
+    .then(() => {
+      // Check for changes
+
+      return self._git.exec('log', 'origin/' + self._repo.branch + '..HEAD').then((cProc) => {
+        let out = cProc.stdout.toString()
+
+        if (_.includes(out, 'commit')) {
+          winston.info('[' + PROCNAME + '][GIT] Performing push to remote repository...')
+          return self._git.push('origin', self._repo.branch).then(() => {
+            return winston.info('[' + PROCNAME + '][GIT] Push completed.')
+          })
+        } else {
+          winston.info('[' + PROCNAME + '][GIT] Push skipped. Repository is already in sync.')
+        }
+
+        return true
+      })
+    })
+    .catch((err) => {
+      winston.error('[' + PROCNAME + '][GIT] Unable to push changes to remote!')
+      throw err
+    })
+  },
+
+  /**
+   * Commits a document.
+   *
+   * @param      {String}   entryPath  The entry path
+   * @return     {Promise}  Resolve on commit success
+   */
+  commitDocument (entryPath) {
+    let self = this
+    let gitFilePath = entryPath + '.md'
+    let commitMsg = ''
+
+    return self._git.exec('ls-files', gitFilePath).then((cProc) => {
+      let out = cProc.stdout.toString()
+      return _.includes(out, gitFilePath)
+    }).then((isTracked) => {
+      commitMsg = (isTracked) ? 'Updated ' + gitFilePath : 'Added ' + gitFilePath
+      return self._git.add(gitFilePath)
+    }).then(() => {
+      return self._git.commit(commitMsg).catch((err) => {
+        if (_.includes(err.stdout, 'nothing to commit')) { return true }
+      })
+    })
+  },
+
+  /**
+   * Move a document.
+   *
+   * @param      {String}            entryPath     The current entry path
+   * @param      {String}            newEntryPath  The new entry path
+   * @return     {Promise<Boolean>}  Resolve on success
+   */
+  moveDocument (entryPath, newEntryPath) {
+    let self = this
+    let gitFilePath = entryPath + '.md'
+    let gitNewFilePath = newEntryPath + '.md'
+
+    return self._git.exec('mv', [gitFilePath, gitNewFilePath]).then((cProc) => {
+      let out = cProc.stdout.toString()
+      if (_.includes(out, 'fatal')) {
+        let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
+        throw new Error(errorMsg)
+      }
+      return true
+    })
+  },
+
+  /**
+   * Commits uploads changes.
+   *
+   * @param      {String}   msg     The commit message
+   * @return     {Promise}  Resolve on commit success
+   */
+  commitUploads (msg) {
+    let self = this
+    msg = msg || 'Uploads repository sync'
+
+    return self._git.add('uploads').then(() => {
+      return self._git.commit(msg).catch((err) => {
+        if (_.includes(err.stdout, 'nothing to commit')) { return true }
+      })
+    })
+  }
+
+}

+ 14 - 20
libs/internalAuth.js

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

+ 169 - 180
libs/local.js

@@ -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',
-
-	uploadImgHandler: null,
-
-	/**
-	 * 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.createBaseDirectories(appconfig);
-		this.initMulter(appconfig);
-
-		return this;
-
-	},
-
-	/**
-	 * Init Multer upload handlers
-	 *
-	 * @param      {Object}   appconfig  The application config
-	 * @return     {boolean}  Void
-	 */
-	initMulter(appconfig) {
-
-		let maxFileSizes = {
-			img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
-			file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
-		};
-
-		//-> IMAGES
-
-		this.uploadImgHandler = multer({
-			storage: multer.diskStorage({
-				destination: (req, f, cb) => {
-					cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'));
-				}
-			}),
-			fileFilter: (req, f, cb) => {
-
-				//-> Check filesize
-
-				if(f.size > maxFileSizes.img) {
-					return cb(null, false);
-				}
-
-				//-> Check MIME type (quick check only)
-
-				if(!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) {
-					return cb(null, false);
-				}
-
-				cb(null, true);
-			}
-		}).array('imgfile', 20);
-
-		//-> FILES
-
-		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(f.size > maxFileSizes.file) {
-					return cb(null, false);
-				}
-
-				cb(null, true);
-			}
-		}).array('binfile', 20);
-
-		return true;
-
-	},
-
-	/**
-	 * Creates a base directories (Synchronous).
-	 *
-	 * @param      {Object}  appconfig  The application config
-	 * @return     {Void}  Void
-	 */
-	createBaseDirectories(appconfig) {
-
-		winston.info('[SERVER] Checking data directories...');
-
-		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(os.type() !== 'Windows_NT') {
-				fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '644');
-			}
-
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo));
-			fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'));
-
-			if(os.type() !== 'Windows_NT') {
-				fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './upload'), '644');
-			}
-
-		} catch (err) {
-			winston.error(err);
-		}
-
-		winston.info('[SERVER] Data and Repository directories are OK.');
-
-		return;
-
-	},
-
-	/**
-	 * 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;
-		});
-
-	},
-
-};
+  _uploadsPath: './repo/uploads',
+  _uploadsThumbsPath: './data/thumbs',
+
+  uploadImgHandler: null,
+
+  /**
+   * 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.createBaseDirectories(appconfig)
+    this.initMulter(appconfig)
+
+    return this
+  },
+
+  /**
+   * Init Multer upload handlers
+   *
+   * @param      {Object}   appconfig  The application config
+   * @return     {boolean}  Void
+   */
+  initMulter (appconfig) {
+    let maxFileSizes = {
+      img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
+      file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
+    }
+
+    // -> IMAGES
+
+    this.uploadImgHandler = multer({
+      storage: multer.diskStorage({
+        destination: (req, f, cb) => {
+          cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
+        }
+      }),
+      fileFilter: (req, f, cb) => {
+        // -> Check filesize
+
+        if (f.size > maxFileSizes.img) {
+          return cb(null, false)
+        }
+
+        // -> Check MIME type (quick check only)
+
+        if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], f.mimetype)) {
+          return cb(null, false)
+        }
+
+        cb(null, true)
+      }
+    }).array('imgfile', 20)
+
+    // -> FILES
+
+    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 (f.size > maxFileSizes.file) {
+          return cb(null, false)
+        }
+
+        cb(null, true)
+      }
+    }).array('binfile', 20)
+
+    return true
+  },
+
+  /**
+   * Creates a base directories (Synchronous).
+   *
+   * @param      {Object}  appconfig  The application config
+   * @return     {Void}  Void
+   */
+  createBaseDirectories (appconfig) {
+    winston.info('[SERVER] Checking data directories...')
+
+    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 (os.type() !== 'Windows_NT') {
+        fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '644')
+      }
+
+      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo))
+      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'))
+
+      if (os.type() !== 'Windows_NT') {
+        fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './upload'), '644')
+      }
+    } catch (err) {
+      winston.error(err)
+    }
+
+    winston.info('[SERVER] Data and Repository directories are OK.')
+
+    return
+  },
+
+  /**
+   * 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
+    })
+  }
+
+}

+ 273 - 279
libs/markdown.js

@@ -1,86 +1,85 @@
-"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');
+'use strict'
+
+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 = [];
-
-	//-> Extract headings and their respective levels
-
-	for (let i = 0; i < tokens.length; i++) {
-		if (tokens[i].type !== "heading_close") {
-			continue;
-		}
-
-		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, ""));
-			}
-
-			tocArray.push({
-			 content,
-			 anchor,
-			 level: +heading_close.tag.substr(1, 1)
-			});
-		}
-	 }
-
-	 //-> Exclude levels deeper than 2
-
-	 _.remove(tocArray, (n) => { return n.level > 2; });
-
-	 //-> 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;
-	}, []);
-
-};
+  let tokens = md().parse(content, {})
+  let tocArray = []
+
+  // -> Extract headings and their respective levels
+
+  for (let i = 0; i < tokens.length; i++) {
+    if (tokens[i].type !== 'heading_close') {
+      continue
+    }
+
+    const heading = tokens[i - 1]
+    const headingclose = 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, ''))
+      }
+
+      tocArray.push({
+        content,
+        anchor,
+        level: +headingclose.tag.substr(1, 1)
+      })
+    }
+  }
+
+  // -> Exclude levels deeper than 2
+
+  _.remove(tocArray, (n) => { return n.level > 2 })
+
+  // -> 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
+  }, [])
+}
 
 /**
  * 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)  => {
-
-	let output = mkdown.render(content);
-	let cr = cheerio.load(output);
-
-	//-> Check for empty first element
-
-	let firstElm = cr.root().children().first()[0];
-	if(firstElm.type === 'tag' && firstElm.name === 'p') {
-		let firstElmChildren = firstElm.children;
-		if(firstElmChildren.length < 1) {
-			firstElm.remove();
-		} else if(firstElmChildren.length === 1 && firstElmChildren[0].type === 'tag' && firstElmChildren[0].name === 'img') {
-			cr(firstElm).addClass('is-gapless');
-		}
-	}
-
-	//-> 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);
-	});
-
-	//-> 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);
-			}
-		}
-	});
-
-	//-> 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('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
-
-	_.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();
-
-	return output;
-
-};
+const parseContent = (content) => {
+  let output = mkdown.render(content)
+  let cr = cheerio.load(output)
+
+  // -> Check for empty first element
+
+  let firstElm = cr.root().children().first()[0]
+  if (firstElm.type === 'tag' && firstElm.name === 'p') {
+    let firstElmChildren = firstElm.children
+    if (firstElmChildren.length < 1) {
+      firstElm.remove()
+    } else if (firstElmChildren.length === 1 && firstElmChildren[0].type === 'tag' && firstElmChildren[0].name === 'img') {
+      cr(firstElm).addClass('is-gapless')
+    }
+  }
+
+  // -> 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)
+  })
+
+  // -> 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)
+      }
+    }
+  })
+
+  // -> 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('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
+
+  _.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()
+
+  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)
-		};
-	},
-
-	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()
-		);
-	}
-
-};
+  /**
+   * 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,
+
+  /**
+   * 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()
+    )
+  }
+
+}

+ 249 - 286
libs/uploads-agent.js

@@ -1,292 +1,255 @@
-"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');
+'use strict'
+
+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()
+  }
+
+}

+ 272 - 300
libs/uploads.js

@@ -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',
-
-	/**
-	 * 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');
-
-		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: '' }];
-		});
-	},
-
-	/**
-	 * Creates an uploads folder.
-	 *
-	 * @param      {String}  folderName  The folder name
-	 * @return     {Promise}  Promise of the operation
-	 */
-	createUploadsFolder(folderName) {
-
-		let self = this;
-
-		folderName = _.kebabCase(_.trim(folderName));
-
-		if(_.isEmpty(folderName) || !regFolderName.test(folderName)) {
-			return Promise.resolve(self.getUploadsFolders());
-		}
-
-		return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
-			return db.UplFolder.findOneAndUpdate({
-				_id: 'f:' + folderName
-			}, {
-				name: folderName
-			}, {
-				upsert: true
-			});
-		}).then(() => {
-			return self.getUploadsFolders();
-		});
-
-	},
-
-	/**
-	 * 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;
-		});
-
-	},
-
-	/**
-	 * 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;
-	},
-
-	/**
-	 * 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;
-
-		return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
-			if(f) {
-				return self.deleteUploadsFileTry(f, 0);
-			}
-			return true;
-		});
-	},
-
-	deleteUploadsFileTry(f, attempt) {
-
-		let self = this;
-
-		let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './';
-
-		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;
-			}
-		});
-
-	},
-
-	/**
-	 * 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 self = this;
-
-		let fUrlObj = url.parse(fUrl);
-		let fUrlFilename = _.last(_.split(fUrlObj.pathname, '/'));
-		let destFolder = _.chain(fFolder).trim().toLower().value();
-
-		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);
-
-				return new Promise((resolve, reject) => {
-
-					let rq = request({
-						url: fUrl,
-						method: 'GET',
-						followRedirect: true,
-						maxRedirects: 5,
-						timeout: 10000
-					});
-
-					let destFileStream = fs.createWriteStream(destFilePath);
-					let curFileSize = 0;
-
-					rq.on('data', (data) => {
-						curFileSize += data.length;
-						if(curFileSize > maxDownloadFileSize) {
-							rq.abort();
-							destFileStream.destroy();
-							fs.remove(destFilePath);
-							reject(new Error('Remote file is too large!'));
-						}
-					}).on('error', (err) => {
-						destFileStream.destroy();
-						fs.remove(destFilePath);
-						reject(err);
-					});
-
-					destFileStream.on('finish', () => {
-						resolve(true);
-					});
-
-					rq.pipe(destFileStream);
-
-				});
-
-			});
-
-		});
-
-	},
-
-	/**
-	 * Move/Rename a file
-	 *
-	 * @param      {String}   uid        The file ID
-	 * @param      {String}   fld        The destination folder
-	 * @param      {String}   nFilename  The new filename (optional)
-	 * @return     {Promise}  Promise of the operation
-	 */
-	moveUploadsFile(uid, fld, nFilename) {
-
-		let self = this;
-
-		return db.UplFolder.findById('f:' + fld).then((folder) => {
-			if(folder) {
-				return db.UplFile.findById(uid).then((originFile) => {
-
-					//-> Check if rename is valid
-
-					let nameCheck = null;
-					if(nFilename) {
-						let originFileObj = path.parse(originFile.filename);
-						nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name);
-					} else {
-						nameCheck = Promise.resolve(originFile.filename);
-					}
-
-					return nameCheck.then((destFilename) => {
-
-						let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './';
-						let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename);
-						let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename);
-						let preMoveOps = [];
-
-						//-> Check for invalid operations
-
-						if(sourceFilePath === destFilePath) {
-							return Promise.reject(new Error('Invalid Operation!'));
-						}
-
-						//-> Delete DB entry
-
-						preMoveOps.push(db.UplFile.findByIdAndRemove(uid));
-
-						//-> Move thumbnail ahead to avoid re-generation
-
-						if(originFile.category === 'image') {
-							let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename);
-							let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png');
-							let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png');
-							preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath));
-						} else {
-							preMoveOps.push(Promise.resolve(true));
-						}
-
-						//-> Proceed to move actual file
-
-						return Promise.all(preMoveOps).then(() => {
-							return fs.moveAsync(sourceFilePath, destFilePath, {
-								clobber: false
-							});
-						});
-
-					})
-
-				});
-			} else {
-				return Promise.reject(new Error('Invalid Destination Folder'));
-			}
-		});
-
-	}
-
-};
+  _uploadsPath: './repo/uploads',
+  _uploadsThumbsPath: './data/thumbs',
+
+  /**
+   * 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')
+
+    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: '' }]
+    })
+  },
+
+  /**
+   * Creates an uploads folder.
+   *
+   * @param      {String}  folderName  The folder name
+   * @return     {Promise}  Promise of the operation
+   */
+  createUploadsFolder (folderName) {
+    let self = this
+
+    folderName = _.kebabCase(_.trim(folderName))
+
+    if (_.isEmpty(folderName) || !regFolderName.test(folderName)) {
+      return Promise.resolve(self.getUploadsFolders())
+    }
+
+    return fs.ensureDirAsync(path.join(self._uploadsPath, folderName)).then(() => {
+      return db.UplFolder.findOneAndUpdate({
+        _id: 'f:' + folderName
+      }, {
+        name: folderName
+      }, {
+        upsert: true
+      })
+    }).then(() => {
+      return self.getUploadsFolders()
+    })
+  },
+
+  /**
+   * 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
+    })
+  },
+
+  /**
+   * 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
+  },
+
+  /**
+   * 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
+
+    return db.UplFile.findOneAndRemove({ _id: uid }).then((f) => {
+      if (f) {
+        return self.deleteUploadsFileTry(f, 0)
+      }
+      return true
+    })
+  },
+
+  deleteUploadsFileTry (f, attempt) {
+    let self = this
+
+    let fFolder = (f.folder && f.folder !== 'f:') ? f.folder.slice(2) : './'
+
+    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
+      }
+    })
+  },
+
+  /**
+   * 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 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)
+
+        return new Promise((resolve, reject) => {
+          let rq = request({
+            url: fUrl,
+            method: 'GET',
+            followRedirect: true,
+            maxRedirects: 5,
+            timeout: 10000
+          })
+
+          let destFileStream = fs.createWriteStream(destFilePath)
+          let curFileSize = 0
+
+          rq.on('data', (data) => {
+            curFileSize += data.length
+            if (curFileSize > maxDownloadFileSize) {
+              rq.abort()
+              destFileStream.destroy()
+              fs.remove(destFilePath)
+              reject(new Error('Remote file is too large!'))
+            }
+          }).on('error', (err) => {
+            destFileStream.destroy()
+            fs.remove(destFilePath)
+            reject(err)
+          })
+
+          destFileStream.on('finish', () => {
+            resolve(true)
+          })
+
+          rq.pipe(destFileStream)
+        })
+      })
+    })
+  },
+
+  /**
+   * Move/Rename a file
+   *
+   * @param      {String}   uid        The file ID
+   * @param      {String}   fld        The destination folder
+   * @param      {String}   nFilename  The new filename (optional)
+   * @return     {Promise}  Promise of the operation
+   */
+  moveUploadsFile (uid, fld, nFilename) {
+    let self = this
+
+    return db.UplFolder.findById('f:' + fld).then((folder) => {
+      if (folder) {
+        return db.UplFile.findById(uid).then((originFile) => {
+          // -> Check if rename is valid
+
+          let nameCheck = null
+          if (nFilename) {
+            let originFileObj = path.parse(originFile.filename)
+            nameCheck = lcdata.validateUploadsFilename(nFilename + originFileObj.ext, folder.name)
+          } else {
+            nameCheck = Promise.resolve(originFile.filename)
+          }
+
+          return nameCheck.then((destFilename) => {
+            let originFolder = (originFile.folder && originFile.folder !== 'f:') ? originFile.folder.slice(2) : './'
+            let sourceFilePath = path.resolve(self._uploadsPath, originFolder, originFile.filename)
+            let destFilePath = path.resolve(self._uploadsPath, folder.name, destFilename)
+            let preMoveOps = []
+
+            // -> Check for invalid operations
+
+            if (sourceFilePath === destFilePath) {
+              return Promise.reject(new Error('Invalid Operation!'))
+            }
+
+            // -> Delete DB entry
+
+            preMoveOps.push(db.UplFile.findByIdAndRemove(uid))
+
+            // -> Move thumbnail ahead to avoid re-generation
+
+            if (originFile.category === 'image') {
+              let fUid = farmhash.fingerprint32(folder.name + '/' + destFilename)
+              let sourceThumbPath = path.resolve(self._uploadsThumbsPath, originFile._id + '.png')
+              let destThumbPath = path.resolve(self._uploadsThumbsPath, fUid + '.png')
+              preMoveOps.push(fs.moveAsync(sourceThumbPath, destThumbPath))
+            } else {
+              preMoveOps.push(Promise.resolve(true))
+            }
+
+            // -> Proceed to move actual file
+
+            return Promise.all(preMoveOps).then(() => {
+              return fs.moveAsync(sourceFilePath, destFilePath, {
+                clobber: false
+              })
+            })
+          })
+        })
+      } else {
+        return Promise.reject(new Error('Invalid Destination Folder'))
+      }
+    })
+  }
+
+}

+ 18 - 21
middlewares/auth.js

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

+ 4 - 6
middlewares/flash.js

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

+ 15 - 15
middlewares/security.js

@@ -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) {
-
-	//-> Disable X-Powered-By
-	app.disable('x-powered-by');
-
-	//-> Disable Frame Embedding
-	res.set('X-Frame-Options', 'deny');
+module.exports = function (req, res, next) {
+  // -> Disable X-Powered-By
+  app.disable('x-powered-by')
 
-	//-> Re-enable XSS Fitler if disabled
-	res.set('X-XSS-Protection', '1; mode=block');
+  // -> Disable Frame Embedding
+  res.set('X-Frame-Options', 'deny')
 
-	//-> Disable MIME-sniffing
-	res.set('X-Content-Type-Options', 'nosniff');
+  // -> Re-enable XSS Fitler if disabled
+  res.set('X-XSS-Protection', '1; mode=block')
 
-	//-> Disable IE Compatibility Mode
-	res.set('X-UA-Compatible', 'IE=edge');
+  // -> Disable MIME-sniffing
+  res.set('X-Content-Type-Options', 'nosniff')
 
-	return next();
+  // -> Disable IE Compatibility Mode
+  res.set('X-UA-Compatible', 'IE=edge')
 
-};
+  return next()
+}

+ 10 - 10
models/bruteforce.js

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

+ 7 - 10
models/entry.js

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

+ 4 - 10
models/upl-file.js

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

+ 4 - 10
models/upl-folder.js

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

+ 61 - 66
models/user.js

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

+ 30 - 0
package.json

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

+ 109 - 107
server.js

@@ -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');
-
-var mw = autoload(CORE_PATH + '/core-middlewares');
-var ctrl = autoload(path.join(ROOTPATH, '/controllers'));
-var libInternalAuth = require('./libs/internalAuth');
-
-global.WSInternalKey = libInternalAuth.generateKey();
+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')
+
+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 - 0
test/index.js

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

+ 0 - 11
tests/index.js

@@ -1,11 +0,0 @@
-"use strict";
-
-let path = require('path'),
-	 fs = require('fs');
-
-// ========================================
-// Load global modules
-// ========================================
-
-global._ = require('lodash');
-global.winston = require('winston');

+ 10 - 15
views/error.pug

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

+ 5 - 0
views/pages/view.pug

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

Some files were not shown because too many files changed in this diff