mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	Compare commits
	
		
			24 Commits
		
	
	
		
			v0.8.1
			...
			v0.9.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a5c9180533 | ||
|  | e86f1e0d05 | ||
|  | b6277049f3 | ||
|  | c831221cc4 | ||
|  | 577a168714 | ||
|  | b0bd27321a | ||
|  | 90c5348ca7 | ||
|  | 8e95b080da | ||
|  | 766a567a32 | ||
|  | 6d0218cb36 | ||
|  | d26170762b | ||
|  | b3209a9bbf | ||
|  | 61c2456cf6 | ||
|  | 1c6fc9029f | ||
|  | 5c91e38dfe | ||
|  | 07bf075894 | ||
|  | ddce5c959e | ||
|  | 3b9d1df05c | ||
|  | d239ef2956 | ||
|  | 7a865a9081 | ||
|  | 83d6c2970f | ||
|  | 8c7d159012 | ||
|  | d169f67901 | ||
|  | 982b723647 | 
							
								
								
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript'; | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.8.1", | ||||
|   "version": "0.9.0-beta", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "repository": { | ||||
|   | ||||
| @@ -73,6 +73,8 @@ require('./services/backup'); | ||||
| // trigger consistency checks timer | ||||
| require('./services/consistency_checks'); | ||||
|  | ||||
| require('./services/scheduler'); | ||||
|  | ||||
| module.exports = { | ||||
|     app, | ||||
|     sessionParser | ||||
|   | ||||
| @@ -25,13 +25,51 @@ class Note extends Entity { | ||||
|  | ||||
|     isJavaScript() { | ||||
|         return (this.type === "code" || this.type === "file") | ||||
|             && (this.mime === "application/javascript" || this.mime === "application/x-javascript"); | ||||
|             && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript"); | ||||
|     } | ||||
|  | ||||
|     isHtml() { | ||||
|         return (this.type === "code" || this.type === "file") && this.mime === "text/html"; | ||||
|     } | ||||
|  | ||||
|     getScriptEnv() { | ||||
|         if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { | ||||
|             return "frontend"; | ||||
|         } | ||||
|  | ||||
|         if (this.type === 'render') { | ||||
|             return "frontend"; | ||||
|         } | ||||
|  | ||||
|         if (this.isJavaScript() && this.mime.endsWith('env=backend')) { | ||||
|             return "backend"; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     async getAttributes() { | ||||
|         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); | ||||
|     } | ||||
|  | ||||
|     // WARNING: this doesn't take into account the possibility to have multi-valued attributes! | ||||
|     async getAttributeMap() { | ||||
|         const map = {}; | ||||
|  | ||||
|         for (const attr of await this.getAttributes()) { | ||||
|             map[attr.name] = attr.value; | ||||
|         } | ||||
|  | ||||
|         return map; | ||||
|     } | ||||
|  | ||||
|     async hasAttribute(name) { | ||||
|         const map = await this.getAttributeMap(); | ||||
|  | ||||
|         return map.hasOwnProperty(name); | ||||
|     } | ||||
|  | ||||
|     // WARNING: this doesn't take into account the possibility to have multi-valued attributes! | ||||
|     async getAttribute(name) { | ||||
|         return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]); | ||||
|     } | ||||
| @@ -62,7 +100,8 @@ class Note extends Entity { | ||||
|             JOIN notes USING(noteId)  | ||||
|           WHERE notes.isDeleted = 0 | ||||
|                 AND note_tree.isDeleted = 0 | ||||
|                 AND note_tree.parentNoteId = ?`, [this.noteId]); | ||||
|                 AND note_tree.parentNoteId = ? | ||||
|           ORDER BY note_tree.notePosition`, [this.noteId]); | ||||
|     } | ||||
|  | ||||
|     async getParents() { | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 252 B | 
| @@ -1,4 +1,12 @@ | ||||
| const api = (function() { | ||||
| function ScriptContext(startNote, allNotes) { | ||||
|     return { | ||||
|         modules: {}, | ||||
|         notes: toObject(allNotes, note => [note.noteId, note]), | ||||
|         apis: toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]), | ||||
|     }; | ||||
| } | ||||
|  | ||||
| function ScriptApi(startNote, currentNote) { | ||||
|     const $pluginButtons = $("#plugin-buttons"); | ||||
|  | ||||
|     async function activateNote(notePath) { | ||||
| @@ -13,9 +21,42 @@ const api = (function() { | ||||
|         $pluginButtons.append(button); | ||||
|     } | ||||
|  | ||||
|     function prepareParams(params) { | ||||
|         if (!params) { | ||||
|             return params; | ||||
|         } | ||||
|  | ||||
|         return params.map(p => { | ||||
|             if (typeof p === "function") { | ||||
|                 return "!@#Function: " + p.toString(); | ||||
|             } | ||||
|             else { | ||||
|                 return p; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async function runOnServer(script, params = []) { | ||||
|         if (typeof script === "function") { | ||||
|             script = script.toString(); | ||||
|         } | ||||
|  | ||||
|         const ret = await server.post('script/exec', { | ||||
|             script: script, | ||||
|             params: prepareParams(params), | ||||
|             startNoteId: startNote.noteId, | ||||
|             currentNoteId: currentNote.noteId | ||||
|         }); | ||||
|  | ||||
|         return ret.executionResult; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         startNote: startNote, | ||||
|         currentNote: currentNote, | ||||
|         addButtonToToolbar, | ||||
|         activateNote, | ||||
|         getInstanceName: noteTree.getInstanceName | ||||
|         getInstanceName: noteTree.getInstanceName, | ||||
|         runOnServer | ||||
|     } | ||||
| })(); | ||||
| } | ||||
| @@ -29,6 +29,9 @@ const sqlConsole = (function() { | ||||
|             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||
|             CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||
|  | ||||
|             // removing Escape binding so that Escape will propagate to the dialog (which will close on escape) | ||||
|             delete CodeMirror.keyMap.basic["Esc"]; | ||||
|  | ||||
|             CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||
|  | ||||
|             codeEditor = CodeMirror($query[0], { | ||||
| @@ -45,7 +48,11 @@ const sqlConsole = (function() { | ||||
|         codeEditor.focus(); | ||||
|     } | ||||
|  | ||||
|     async function execute() { | ||||
|     async function execute(e) { | ||||
|         // stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes) | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
|  | ||||
|         const sqlQuery = codeEditor.getValue(); | ||||
|  | ||||
|         const result = await server.post("sql/execute", { | ||||
|   | ||||
| @@ -201,9 +201,9 @@ window.onerror = function (msg, url, lineNo, columnNo, error) { | ||||
| $("#logout-button").toggle(!isElectron()); | ||||
|  | ||||
| $(document).ready(() => { | ||||
|     server.get("script/startup").then(scripts => { | ||||
|         for (const script of scripts) { | ||||
|             executeScript(script); | ||||
|     server.get("script/startup").then(scriptBundles => { | ||||
|         for (const bundle of scriptBundles) { | ||||
|             executeBundle(bundle); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -77,13 +77,15 @@ const noteEditor = (function() { | ||||
|  | ||||
|     function updateNoteFromInputs(note) { | ||||
|         if (note.detail.type === 'text') { | ||||
|             note.detail.content = editor.getData(); | ||||
|             let content = editor.getData(); | ||||
|  | ||||
|             // if content is only tags/whitespace (typically <p> </p>), then just make it empty | ||||
|             // this is important when setting new note to code | ||||
|             if (jQuery(note.detail.content).text().trim() === '') { | ||||
|                 note.detail.content = '' | ||||
|             if (jQuery(content).text().trim() === '' && !content.includes("<img")) { | ||||
|                 content = ''; | ||||
|             } | ||||
|  | ||||
|             note.detail.content = content; | ||||
|         } | ||||
|         else if (note.detail.type === 'code') { | ||||
|             note.detail.content = codeEditor.getValue(); | ||||
| @@ -217,9 +219,11 @@ const noteEditor = (function() { | ||||
|         if (currentNote.detail.type === 'render') { | ||||
|             $noteDetailRender.show(); | ||||
|  | ||||
|             const subTree = await server.get('script/subtree/' + getCurrentNoteId()); | ||||
|             const bundle = await server.get('script/bundle/' + getCurrentNoteId()); | ||||
|  | ||||
|             $noteDetailRender.html(subTree); | ||||
|             $noteDetailRender.html(bundle.html); | ||||
|  | ||||
|             executeBundle(bundle); | ||||
|         } | ||||
|         else if (currentNote.detail.type === 'file') { | ||||
|             $noteDetailAttachment.show(); | ||||
| @@ -298,9 +302,17 @@ const noteEditor = (function() { | ||||
|             // make sure note is saved so we load latest changes | ||||
|             await saveNoteIfChanged(); | ||||
|  | ||||
|             const script = await server.get('script/subtree/' + getCurrentNoteId()); | ||||
|             if (currentNote.detail.mime.endsWith("env=frontend")) { | ||||
|                 const bundle = await server.get('script/bundle/' + getCurrentNoteId()); | ||||
|  | ||||
|             executeScript(script); | ||||
|                 executeBundle(bundle); | ||||
|             } | ||||
|  | ||||
|             if (currentNote.detail.mime.endsWith("env=backend")) { | ||||
|                 await server.post('script/run/' + getCurrentNoteId()); | ||||
|             } | ||||
|  | ||||
|             showMessage("Note executed"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -157,6 +157,9 @@ const noteTree = (function() { | ||||
|         if (note.type === 'code') { | ||||
|             extraClasses.push("code"); | ||||
|         } | ||||
|         else if (note.type === 'render') { | ||||
|             extraClasses.push('render'); | ||||
|         } | ||||
|         else if (note.type === 'file') { | ||||
|             extraClasses.push('attachment'); | ||||
|         } | ||||
|   | ||||
| @@ -25,7 +25,8 @@ const noteType = (function() { | ||||
|             { mime: 'text/html', title: 'HTML' }, | ||||
|             { mime: 'message/http', title: 'HTTP' }, | ||||
|             { mime: 'text/x-java', title: 'Java' }, | ||||
|             { mime: 'application/javascript', title: 'JavaScript' }, | ||||
|             { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' }, | ||||
|             { mime: 'application/javascript;env=backend', title: 'JavaScript backend' }, | ||||
|             { mime: 'application/json', title: 'JSON' }, | ||||
|             { mime: 'text/x-kotlin', title: 'Kotlin' }, | ||||
|             { mime: 'text/x-lua', title: 'Lua' }, | ||||
| @@ -121,7 +122,7 @@ const noteType = (function() { | ||||
|         }; | ||||
|  | ||||
|         this.updateExecuteScriptButtonVisibility = function() { | ||||
|             $executeScriptButton.toggle(self.mime() === 'application/javascript'); | ||||
|             $executeScriptButton.toggle(self.mime().startsWith('application/javascript')); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -31,38 +31,6 @@ const server = (function() { | ||||
|         return await call('DELETE', url); | ||||
|     } | ||||
|  | ||||
|     function prepareParams(params) { | ||||
|         if (!params) { | ||||
|             return params; | ||||
|         } | ||||
|  | ||||
|         return params.map(p => { | ||||
|             if (typeof p === "function") { | ||||
|                 return "!@#Function: " + p.toString(); | ||||
|             } | ||||
|             else { | ||||
|                 return p; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async function exec(params, script) { | ||||
|         if (typeof script === "function") { | ||||
|             script = script.toString(); | ||||
|         } | ||||
|  | ||||
|         const ret = await post('script/exec', { script: script, params: prepareParams(params) }); | ||||
|  | ||||
|         return ret.executionResult; | ||||
|     } | ||||
|  | ||||
|     async function setJob(opts) { | ||||
|         opts.job = opts.job.toString(); | ||||
|         opts.params = prepareParams(opts.params); | ||||
|  | ||||
|         await post('script/job', opts); | ||||
|     } | ||||
|  | ||||
|     let i = 1; | ||||
|     const reqResolves = {}; | ||||
|  | ||||
| @@ -126,8 +94,6 @@ const server = (function() { | ||||
|         post, | ||||
|         put, | ||||
|         remove, | ||||
|         exec, | ||||
|         setJob, | ||||
|         ajax, | ||||
|         // don't remove, used from CKEditor image upload! | ||||
|         getHeaders | ||||
|   | ||||
| @@ -115,8 +115,10 @@ async function stopWatch(what, func) { | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function executeScript(script) { | ||||
|     eval(script); | ||||
| async function executeBundle(bundle) { | ||||
|     const apiContext = ScriptContext(bundle.note, bundle.allNotes); | ||||
|  | ||||
|     return await (function() { return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }.call(apiContext)); | ||||
| } | ||||
|  | ||||
| function formatValueWithWhitespace(val) { | ||||
| @@ -206,3 +208,15 @@ function download(url) { | ||||
|         window.location.href = url; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function toObject(array, fn) { | ||||
|     const obj = {}; | ||||
|  | ||||
|     for (const item of array) { | ||||
|         const ret = fn(item); | ||||
|  | ||||
|         obj[ret[0]] = ret[1]; | ||||
|     } | ||||
|  | ||||
|     return obj; | ||||
| } | ||||
| @@ -846,6 +846,8 @@ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); | ||||
| CodeMirror.defineMIME("text/javascript", "javascript"); | ||||
| CodeMirror.defineMIME("text/ecmascript", "javascript"); | ||||
| CodeMirror.defineMIME("application/javascript", "javascript"); | ||||
| CodeMirror.defineMIME("application/javascript;env=frontend", "javascript"); | ||||
| CodeMirror.defineMIME("application/javascript;env=backend", "javascript"); | ||||
| CodeMirror.defineMIME("application/x-javascript", "javascript"); | ||||
| CodeMirror.defineMIME("application/ecmascript", "javascript"); | ||||
| CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); | ||||
|   | ||||
							
								
								
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							| @@ -70,7 +70,7 @@ | ||||
|     {name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]}, | ||||
|     {name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]}, | ||||
|     {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]}, | ||||
|     {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"], | ||||
|     {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/javascript;env=frontend", "application/javascript;env=backend", "application/x-javascript", "application/ecmascript"], | ||||
|      mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]}, | ||||
|     {name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]}, | ||||
|     {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]}, | ||||
|   | ||||
| @@ -77,6 +77,11 @@ span.fancytree-node.attachment > span.fancytree-icon { | ||||
|     background-image: url("../images/icons/paperclip.png"); | ||||
| } | ||||
|  | ||||
| span.fancytree-node.render > span.fancytree-icon { | ||||
|     background-position: 0 0; | ||||
|     background-image: url("../images/icons/play.png"); | ||||
| } | ||||
|  | ||||
| span.fancytree-node.protected > span.fancytree-icon { | ||||
|     filter: drop-shadow(2px 2px 2px black); | ||||
| } | ||||
| @@ -108,6 +113,9 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title { | ||||
|  | ||||
| .icon-action { | ||||
|     cursor: pointer; | ||||
|     display: block; | ||||
|     height: 24px; | ||||
|     width: 24px; | ||||
| } | ||||
|  | ||||
| #protect-button, #unprotect-button { | ||||
|   | ||||
| @@ -3,21 +3,22 @@ | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const sql = require('../../services/sql'); | ||||
| const attributes = require('../../services/attributes'); | ||||
| const html = require('html'); | ||||
| const auth = require('../../services/auth'); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
| const tar = require('tar-stream'); | ||||
| const sanitize = require("sanitize-filename"); | ||||
| const Repository = require("../../services/repository"); | ||||
|  | ||||
| router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => { | ||||
|     const noteId = req.params.noteId; | ||||
|     const repo = new Repository(req); | ||||
|  | ||||
|     const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]); | ||||
|  | ||||
|     const pack = tar.pack(); | ||||
|  | ||||
|     const name = await exportNote(noteTreeId, '', pack); | ||||
|     const name = await exportNote(noteTreeId, '', pack, repo); | ||||
|  | ||||
|     pack.finalize(); | ||||
|  | ||||
| @@ -27,9 +28,9 @@ router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) | ||||
|     pack.pipe(res); | ||||
| })); | ||||
|  | ||||
| async function exportNote(noteTreeId, directory, pack) { | ||||
| async function exportNote(noteTreeId, directory, pack, repo) { | ||||
|     const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]); | ||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]); | ||||
|     const note = await repo.getEntity("SELECT notes.* FROM notes WHERE noteId = ?", [noteTree.noteId]); | ||||
|  | ||||
|     if (note.isProtected) { | ||||
|         return; | ||||
| @@ -54,7 +55,7 @@ async function exportNote(noteTreeId, directory, pack) { | ||||
|  | ||||
|     if (children.length > 0) { | ||||
|         for (const child of children) { | ||||
|             await exportNote(child.noteTreeId, childFileName + "/", pack); | ||||
|             await exportNote(child.noteTreeId, childFileName + "/", pack, repo); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -63,10 +64,16 @@ async function exportNote(noteTreeId, directory, pack) { | ||||
|  | ||||
| async function getMetadata(note) { | ||||
|     return { | ||||
|         version: 1, | ||||
|         title: note.title, | ||||
|         type: note.type, | ||||
|         mime: note.mime, | ||||
|         attributes: await attributes.getNoteAttributeMap(note.noteId) | ||||
|         attributes: (await note.getAttributes()).map(attr => { | ||||
|             return { | ||||
|                 name: attr.name, | ||||
|                 value: attr.value | ||||
|             }; | ||||
|         }) | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const sql = require('../../services/sql'); | ||||
| const auth = require('../../services/auth'); | ||||
| const attributes = require('../../services/attributes'); | ||||
| const notes = require('../../services/notes'); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
| const tar = require('tar-stream'); | ||||
| @@ -111,6 +112,10 @@ router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload | ||||
|  | ||||
| async function importNotes(files, parentNoteId, sourceId) { | ||||
|     for (const file of files) { | ||||
|         if (file.meta.version !== 1) { | ||||
|             throw new Error("Can't read meta data version " + file.meta.version); | ||||
|         } | ||||
|  | ||||
|         if (file.meta.type !== 'file') { | ||||
|             file.data = file.data.toString("UTF-8"); | ||||
|         } | ||||
| @@ -118,10 +123,13 @@ async function importNotes(files, parentNoteId, sourceId) { | ||||
|         const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, { | ||||
|             type: file.meta.type, | ||||
|             mime: file.meta.mime, | ||||
|             attributes: file.meta.attributes, | ||||
|             sourceId: sourceId | ||||
|         }); | ||||
|  | ||||
|         for (const attr of file.meta.attributes) { | ||||
|             await attributes.createAttribute(noteId, attr.name, attr.value); | ||||
|         } | ||||
|  | ||||
|         if (file.children.length > 0) { | ||||
|             await importNotes(file.children, noteId, sourceId); | ||||
|         } | ||||
|   | ||||
| @@ -4,89 +4,50 @@ const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const auth = require('../../services/auth'); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
| const notes = require('../../services/notes'); | ||||
| const attributes = require('../../services/attributes'); | ||||
| const script = require('../../services/script'); | ||||
| const Repository = require('../../services/repository'); | ||||
|  | ||||
| router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const ret = await script.executeScript(req, req.body.script, req.body.params); | ||||
|     const ret = await script.executeScript(req, req.body.script, req.body.params, req.body.startNoteId, req.body.currentNoteId); | ||||
|  | ||||
|     res.send({ | ||||
|         executionResult: ret | ||||
|     }); | ||||
| })); | ||||
|  | ||||
| router.post('/job', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     await script.setJob(req.body); | ||||
| router.post('/run/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const repository = new Repository(req); | ||||
|     const note = await repository.getNote(req.params.noteId); | ||||
|  | ||||
|     res.send({}); | ||||
|     const ret = await script.executeNote(req, note); | ||||
|  | ||||
|     res.send({ | ||||
|         executionResult: ret | ||||
|     }); | ||||
| })); | ||||
|  | ||||
| router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup"); | ||||
|     const repository = new Repository(req); | ||||
|     const notes = await attributes.getNotesWithAttribute(repository, "run", "frontend_startup"); | ||||
|  | ||||
|     const scripts = []; | ||||
|  | ||||
|     for (const noteId of noteIds) { | ||||
|         scripts.push(await getNoteWithSubtreeScript(noteId, repository)); | ||||
|     for (const note of notes) { | ||||
|         const bundle = await script.getScriptBundle(note); | ||||
|  | ||||
|         scripts.push(bundle); | ||||
|     } | ||||
|  | ||||
|     res.send(scripts); | ||||
| })); | ||||
|  | ||||
| router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
| router.get('/bundle/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const repository = new Repository(req); | ||||
|     const noteId = req.params.noteId; | ||||
|     const note = await repository.getNote(req.params.noteId); | ||||
|     const bundle = await script.getScriptBundle(note); | ||||
|  | ||||
|     res.send(await getNoteWithSubtreeScript(noteId, repository)); | ||||
|     res.send(bundle); | ||||
| })); | ||||
|  | ||||
| async function getNoteWithSubtreeScript(noteId, repository) { | ||||
|     const note = await repository.getNote(noteId); | ||||
|  | ||||
|     let noteScript = note.content; | ||||
|  | ||||
|     if (note.isJavaScript()) { | ||||
|         // last \r\n is necessary if script contains line comment on its last line | ||||
|         noteScript = "(async function() {" + noteScript + "\r\n})()"; | ||||
|     } | ||||
|  | ||||
|     const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository, note.isJavaScript()); | ||||
|  | ||||
|     return subTreeScripts + noteScript; | ||||
| } | ||||
|  | ||||
| async function getSubTreeScripts(parentId, includedNoteIds, repository, isJavaScript) { | ||||
|     const children = await repository.getEntities(` | ||||
|                                       SELECT notes.*  | ||||
|                                       FROM notes JOIN note_tree USING(noteId) | ||||
|                                       WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0 | ||||
|                                            AND note_tree.parentNoteId = ? AND (notes.type = 'code' OR notes.type = 'file') | ||||
|                                            AND (notes.mime = 'application/javascript'  | ||||
|                                                 OR notes.mime = 'application/x-javascript'  | ||||
|                                                 OR notes.mime = 'text/html')`, [parentId]); | ||||
|  | ||||
|     let script = "\r\n"; | ||||
|  | ||||
|     for (const child of children) { | ||||
|         if (includedNoteIds.includes(child.noteId)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         includedNoteIds.push(child.noteId); | ||||
|  | ||||
|         script += await getSubTreeScripts(child.noteId, includedNoteIds, repository); | ||||
|  | ||||
|         if (!isJavaScript && child.isJavaScript()) { | ||||
|             child.content = '<script>' + child.content + '</script>'; | ||||
|         } | ||||
|  | ||||
|         script += child.content + "\r\n"; | ||||
|     } | ||||
|  | ||||
|     return script; | ||||
| } | ||||
|  | ||||
| module.exports = router; | ||||
| @@ -5,13 +5,32 @@ const router = express.Router(); | ||||
| const auth = require('../services/auth'); | ||||
| const source_id = require('../services/source_id'); | ||||
| const sql = require('../services/sql'); | ||||
| const Repository = require('../services/repository'); | ||||
| const attributes = require('../services/attributes'); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
|  | ||||
| router.get('', auth.checkAuth, wrap(async (req, res, next) => { | ||||
|     const repository = new Repository(req); | ||||
|  | ||||
|     res.render('index', { | ||||
|         sourceId: await source_id.generateSourceId(), | ||||
|         maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync") | ||||
|         maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"), | ||||
|         appCss: await getAppCss(repository) | ||||
|     }); | ||||
| })); | ||||
|  | ||||
| async function getAppCss(repository) { | ||||
|     let css = ''; | ||||
|     const notes = attributes.getNotesWithAttribute(repository, 'app_css'); | ||||
|  | ||||
|     for (const note of await notes) { | ||||
|         css += `/* ${note.noteId} */ | ||||
| ${note.content} | ||||
|  | ||||
| `; | ||||
|     } | ||||
|  | ||||
|     return css; | ||||
| } | ||||
|  | ||||
| module.exports = router; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
|  | ||||
| const APP_DB_VERSION = 77; | ||||
| const APP_DB_VERSION = 78; | ||||
|  | ||||
| module.exports = { | ||||
|     app_version: packageJson.version, | ||||
|   | ||||
| @@ -3,14 +3,18 @@ | ||||
| const sql = require('./sql'); | ||||
| const utils = require('./utils'); | ||||
| const sync_table = require('./sync_table'); | ||||
| const Repository = require('./repository'); | ||||
|  | ||||
| const BUILTIN_ATTRIBUTES = [ | ||||
|     'run_on_startup', | ||||
|     'frontend_startup', | ||||
|     'backend_startup', | ||||
|     'disable_versioning', | ||||
|     'calendar_root', | ||||
|     'hide_in_autocomplete', | ||||
|     'exclude_from_export' | ||||
|     'exclude_from_export', | ||||
|     'run', | ||||
|     'manual_transaction_handling', | ||||
|     'disable_inclusion', | ||||
|     'app_css' | ||||
| ]; | ||||
|  | ||||
| async function getNoteAttributeMap(noteId) { | ||||
| @@ -25,9 +29,7 @@ async function getNoteIdWithAttribute(name, value) { | ||||
|                 AND attributes.value = ?`, [name, value]); | ||||
| } | ||||
|  | ||||
| async function getNotesWithAttribute(dataKey, name, value) { | ||||
|     const repository = new Repository(dataKey); | ||||
|  | ||||
| async function getNotesWithAttribute(repository, name, value) { | ||||
|     let notes; | ||||
|  | ||||
|     if (value !== undefined) { | ||||
| @@ -42,8 +44,8 @@ async function getNotesWithAttribute(dataKey, name, value) { | ||||
|     return notes; | ||||
| } | ||||
|  | ||||
| async function getNoteWithAttribute(dataKey, name, value) { | ||||
|     const notes = getNotesWithAttribute(dataKey, name, value); | ||||
| async function getNoteWithAttribute(repository, name, value) { | ||||
|     const notes = getNotesWithAttribute(repository, name, value); | ||||
|  | ||||
|     return notes.length > 0 ? notes[0] : null; | ||||
| } | ||||
| @@ -60,12 +62,14 @@ async function createAttribute(noteId, name, value = "", sourceId = null) { | ||||
|  | ||||
|     const now = utils.nowDate(); | ||||
|     const attributeId = utils.newAttributeId(); | ||||
|     const position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [noteId]); | ||||
|  | ||||
|     await sql.insert("attributes", { | ||||
|         attributeId: attributeId, | ||||
|         noteId: noteId, | ||||
|         name: name, | ||||
|         value: value, | ||||
|         position: position, | ||||
|         dateModified: now, | ||||
|         dateCreated: now, | ||||
|         isDeleted: false | ||||
|   | ||||
							
								
								
									
										27
									
								
								src/services/scheduler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/services/scheduler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| const script = require('./script'); | ||||
| const Repository = require('./repository'); | ||||
|  | ||||
| const repo = new Repository(); | ||||
|  | ||||
| async function runNotesWithAttribute(runAttrValue) { | ||||
|     const notes = await repo.getEntities(` | ||||
|         SELECT notes.*  | ||||
|         FROM notes  | ||||
|           JOIN attributes ON attributes.noteId = notes.noteId | ||||
|                            AND attributes.isDeleted = 0 | ||||
|                            AND attributes.name = 'run'  | ||||
|                            AND attributes.value = ?  | ||||
|         WHERE | ||||
|           notes.type = 'code' | ||||
|           AND notes.isDeleted = 0`, [runAttrValue]); | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         script.executeNote(null, note); | ||||
|     } | ||||
| } | ||||
|  | ||||
| setTimeout(() => runNotesWithAttribute('backend_startup'), 10 * 1000); | ||||
|  | ||||
| setInterval(() => runNotesWithAttribute('hourly'), 3600 * 1000); | ||||
|  | ||||
| setInterval(() => runNotesWithAttribute('daily'), 24 * 3600 * 1000); | ||||
| @@ -1,60 +1,56 @@ | ||||
| const sql = require('./sql'); | ||||
| const ScriptContext = require('./script_context'); | ||||
| const Repository = require('./repository'); | ||||
|  | ||||
| async function executeScript(dataKey, script, params) { | ||||
|     const ctx = new ScriptContext(dataKey); | ||||
|     const paramsStr = getParams(params); | ||||
| async function executeNote(dataKey, note) { | ||||
|     if (!note.isJavaScript()) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     return await sql.doInTransaction(async () => execute(ctx, script, paramsStr)); | ||||
|     const bundle = await getScriptBundle(note); | ||||
|  | ||||
|     await executeBundle(dataKey, bundle); | ||||
| } | ||||
|  | ||||
| async function executeBundle(dataKey, bundle, startNote) { | ||||
|     if (!startNote) { | ||||
|         // this is the default case, the only exception is when we want to preserve frontend startNote | ||||
|         startNote = bundle.note; | ||||
|     } | ||||
|  | ||||
|     // last \r\n is necessary if script contains line comment on its last line | ||||
|     const script = "async function() {\r\n" + bundle.script + "\r\n}"; | ||||
|  | ||||
|     const ctx = new ScriptContext(dataKey, startNote, bundle.allNotes); | ||||
|  | ||||
|     if (await bundle.note.hasAttribute('manual_transaction_handling')) { | ||||
|         return await execute(ctx, script, ''); | ||||
|     } | ||||
|     else { | ||||
|         return await sql.doInTransaction(async () => execute(ctx, script, '')); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This method preserves frontend startNode - that's why we start execution from currentNote and override | ||||
|  * bundle's startNote. | ||||
|  */ | ||||
| async function executeScript(dataKey, script, params, startNoteId, currentNoteId) { | ||||
|     const repository = new Repository(dataKey); | ||||
|     const startNote = await repository.getNote(startNoteId); | ||||
|     const currentNote = await repository.getNote(currentNoteId); | ||||
|  | ||||
|     currentNote.content = `return await (${script}\r\n)(${getParams(params)})`; | ||||
|     currentNote.type = 'code'; | ||||
|     currentNote.mime = 'application/javascript;env=backend'; | ||||
|  | ||||
|     const bundle = await getScriptBundle(currentNote); | ||||
|  | ||||
|     return await executeBundle(dataKey, bundle, startNote); | ||||
| } | ||||
|  | ||||
| async function execute(ctx, script, paramsStr) { | ||||
|     return await (function() { return eval(`const api = this; (${script})(${paramsStr})`); }.call(ctx)); | ||||
| } | ||||
|  | ||||
| const timeouts = {}; | ||||
| const intervals = {}; | ||||
|  | ||||
| function clearExistingJob(name) { | ||||
|     if (timeouts[name]) { | ||||
|         clearTimeout(timeouts[name]); | ||||
|  | ||||
|         delete timeouts[name]; | ||||
|     } | ||||
|  | ||||
|     if (intervals[name]) { | ||||
|         clearInterval(intervals[name]); | ||||
|  | ||||
|         delete intervals[name]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function executeJob(script, params, manualTransactionHandling) { | ||||
|     const ctx = new ScriptContext(); | ||||
|     const paramsStr = getParams(params); | ||||
|  | ||||
|     if (manualTransactionHandling) { | ||||
|         return await execute(ctx, script, paramsStr); | ||||
|     } | ||||
|     else { | ||||
|         return await sql.doInTransaction(async () => execute(ctx, script, paramsStr)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function setJob(opts) { | ||||
|     const { name, runEveryMs, initialRunAfterMs } = opts; | ||||
|  | ||||
|     clearExistingJob(name); | ||||
|  | ||||
|     const jobFunc = () => executeJob(opts.job, opts.params, opts.manualTransactionHandling); | ||||
|  | ||||
|     if (runEveryMs && runEveryMs > 0) { | ||||
|         intervals[name] = setInterval(jobFunc, runEveryMs); | ||||
|     } | ||||
|  | ||||
|     if (initialRunAfterMs && initialRunAfterMs > 0) { | ||||
|         timeouts[name] = setTimeout(jobFunc, initialRunAfterMs); | ||||
|     } | ||||
|     return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx)); | ||||
| } | ||||
|  | ||||
| function getParams(params) { | ||||
| @@ -72,7 +68,72 @@ function getParams(params) { | ||||
|     }).join(","); | ||||
| } | ||||
|  | ||||
| async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) { | ||||
|     if (!note.isJavaScript() && !note.isHtml() && note.type !== 'render') { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (await note.hasAttribute('disable_inclusion')) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (root) { | ||||
|         scriptEnv = note.getScriptEnv(); | ||||
|     } | ||||
|  | ||||
|     if (note.type !== 'file' && scriptEnv !== note.getScriptEnv()) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const bundle = { | ||||
|         note: note, | ||||
|         script: '', | ||||
|         html: '', | ||||
|         allNotes: [note] | ||||
|     }; | ||||
|  | ||||
|     if (includedNoteIds.includes(note.noteId)) { | ||||
|         return bundle; | ||||
|     } | ||||
|  | ||||
|     includedNoteIds.push(note.noteId); | ||||
|  | ||||
|     const modules = []; | ||||
|  | ||||
|     for (const child of await note.getChildren()) { | ||||
|         const childBundle = await getScriptBundle(child, false, scriptEnv, includedNoteIds); | ||||
|  | ||||
|         if (childBundle) { | ||||
|             modules.push(childBundle.note); | ||||
|             bundle.script += childBundle.script; | ||||
|             bundle.html += childBundle.html; | ||||
|             bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (note.isJavaScript()) { | ||||
|         bundle.script += ` | ||||
| apiContext.modules['${note.noteId}'] = {}; | ||||
| ${root ? 'return ' : ''}await (async function(exports, module, api` + (modules.length > 0 ? ', ' : '') + | ||||
|             modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) { | ||||
| ${note.content} | ||||
| })({}, apiContext.modules['${note.noteId}'], apiContext.apis['${note.noteId}']` + (modules.length > 0 ? ', ' : '') + | ||||
|             modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ') + `); | ||||
| `; | ||||
|     } | ||||
|     else if (note.isHtml()) { | ||||
|         bundle.html += note.content; | ||||
|     } | ||||
|  | ||||
|     return bundle; | ||||
| } | ||||
|  | ||||
| function sanitizeVariableName(str) { | ||||
|     return str.replace(/[^a-z0-9_]/gim, ""); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     executeNote, | ||||
|     executeScript, | ||||
|     setJob | ||||
|     getScriptBundle | ||||
| }; | ||||
| @@ -9,9 +9,18 @@ const config = require('./config'); | ||||
| const Repository = require('./repository'); | ||||
| const axios = require('axios'); | ||||
|  | ||||
| function ScriptContext(dataKey) { | ||||
| function ScriptContext(dataKey, startNote, allNotes) { | ||||
|     dataKey = protected_session.getDataKey(dataKey); | ||||
|  | ||||
|     this.modules = {}; | ||||
|     this.notes = utils.toObject(allNotes, note => [note.noteId, note]); | ||||
|     this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(dataKey, startNote, note)]); | ||||
| } | ||||
|  | ||||
| function ScriptApi(dataKey, startNote, currentNote) { | ||||
|     const repository = new Repository(dataKey); | ||||
|     this.startNote = startNote; | ||||
|     this.currentNote = currentNote; | ||||
|  | ||||
|     this.axios = axios; | ||||
|  | ||||
| @@ -27,7 +36,7 @@ function ScriptContext(dataKey) { | ||||
|     }; | ||||
|  | ||||
|     this.getNotesWithAttribute = async function (attrName, attrValue) { | ||||
|         return await attributes.getNotesWithAttribute(dataKey, attrName, attrValue); | ||||
|         return await attributes.getNotesWithAttribute(repository, attrName, attrValue); | ||||
|     }; | ||||
|  | ||||
|     this.getNoteWithAttribute = async function (attrName, attrValue) { | ||||
| @@ -46,7 +55,7 @@ function ScriptContext(dataKey) { | ||||
|  | ||||
|     this.updateEntity = repository.updateEntity; | ||||
|  | ||||
|     this.log = message => log.info(`Script: ${message}`); | ||||
|     this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`); | ||||
|  | ||||
|     this.getRootCalendarNoteId = date_notes.getRootCalendarNoteId; | ||||
|     this.getDateNoteId = date_notes.getDateNoteId; | ||||
|   | ||||
| @@ -134,6 +134,18 @@ function unescapeHtml(str) { | ||||
|     return unescape(str); | ||||
| } | ||||
|  | ||||
| function toObject(array, fn) { | ||||
|     const obj = {}; | ||||
|  | ||||
|     for (const item of array) { | ||||
|         const ret = fn(item); | ||||
|  | ||||
|         obj[ret[0]] = ret[1]; | ||||
|     } | ||||
|  | ||||
|     return obj; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     randomSecureToken, | ||||
|     randomString, | ||||
| @@ -159,5 +171,6 @@ module.exports = { | ||||
|     sanitizeSql, | ||||
|     assertArguments, | ||||
|     stopWatch, | ||||
|     unescapeHtml | ||||
|     unescapeHtml, | ||||
|     toObject | ||||
| }; | ||||
| @@ -40,21 +40,17 @@ | ||||
|  | ||||
|       <div class="hide-toggle" style="grid-area: tree-actions;"> | ||||
|         <div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;"> | ||||
|           <a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action"> | ||||
|             <img src="images/icons/file-plus.png" alt="Create new top level note"/> | ||||
|           </a> | ||||
|           <a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action" | ||||
|              style="background: url('images/icons/file-plus.png')"></a> | ||||
|  | ||||
|           <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action"> | ||||
|             <img src="images/icons/list.png" alt="Collapse note tree"/> | ||||
|           </a> | ||||
|           <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action" | ||||
|              style="background: url('images/icons/list.png')"></a> | ||||
|  | ||||
|           <a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action"> | ||||
|             <img src="images/icons/crosshair.png" alt="Scroll to current note"/> | ||||
|           </a> | ||||
|           <a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action" | ||||
|              style="background: url('images/icons/crosshair.png')"></a> | ||||
|  | ||||
|           <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action"> | ||||
|             <img src="images/icons/search.png" alt="Search in notes"/> | ||||
|           </a> | ||||
|           <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action" | ||||
|              style="background: url('images/icons/search.png')"></a> | ||||
|         </div> | ||||
|  | ||||
|         <input type="file" id="import-upload" style="display: none" /> | ||||
| @@ -83,17 +79,13 @@ | ||||
|              title="Protect the note so that password will be required to view the note" | ||||
|              class="icon-action" | ||||
|              id="protect-button" | ||||
|              style="display: none;"> | ||||
|             <img src="images/icons/lock.png" alt="Protect note"/> | ||||
|           </a> | ||||
|              style="display: none; background: url('images/icons/lock.png')"></a> | ||||
|  | ||||
|           <a onclick="protected_session.unprotectNoteAndSendToServer()" | ||||
|              title="Unprotect note so that password will not be required to access this note in the future" | ||||
|              class="icon-action" | ||||
|              id="unprotect-button" | ||||
|              style="display: none;"> | ||||
|             <img src="images/icons/unlock.png" alt="Unprotect note"/> | ||||
|           </a> | ||||
|              style="display: none; background: url('images/icons/unlock.png')"></a> | ||||
|  | ||||
|             | ||||
|  | ||||
| @@ -529,5 +521,9 @@ | ||||
|       // final form which is pretty ugly. | ||||
|       $("#container").show(); | ||||
|     </script> | ||||
|  | ||||
|     <style type="text/css"> | ||||
|       <%= appCss %> | ||||
|     </style> | ||||
|   </body> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user