mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			28 Commits
		
	
	
		
			v0.8.0-bet
			...
			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 | ||
|  | 31d5ac05ff | ||
|  | 72d91d1571 | ||
|  | f4b57f4c57 | ||
|  | ee0833390a | 
							
								
								
									
										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", |   "name": "trilium", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.8.0-beta", |   "version": "0.9.0-beta", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "electron.js", |   "main": "electron.js", | ||||||
|   "repository": { |   "repository": { | ||||||
|   | |||||||
| @@ -73,6 +73,8 @@ require('./services/backup'); | |||||||
| // trigger consistency checks timer | // trigger consistency checks timer | ||||||
| require('./services/consistency_checks'); | require('./services/consistency_checks'); | ||||||
|  |  | ||||||
|  | require('./services/scheduler'); | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     app, |     app, | ||||||
|     sessionParser |     sessionParser | ||||||
|   | |||||||
| @@ -24,13 +24,52 @@ class Note extends Entity { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     isJavaScript() { |     isJavaScript() { | ||||||
|         return this.type === "code" && this.mime === "application/javascript"; |         return (this.type === "code" || this.type === "file") | ||||||
|  |             && (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() { |     async getAttributes() { | ||||||
|         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); |         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) { |     async getAttribute(name) { | ||||||
|         return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]); |         return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]); | ||||||
|     } |     } | ||||||
| @@ -61,7 +100,8 @@ class Note extends Entity { | |||||||
|             JOIN notes USING(noteId)  |             JOIN notes USING(noteId)  | ||||||
|           WHERE notes.isDeleted = 0 |           WHERE notes.isDeleted = 0 | ||||||
|                 AND note_tree.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() { |     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"); |     const $pluginButtons = $("#plugin-buttons"); | ||||||
|  |  | ||||||
|     async function activateNote(notePath) { |     async function activateNote(notePath) { | ||||||
| @@ -13,9 +21,42 @@ const api = (function() { | |||||||
|         $pluginButtons.append(button); |         $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 { |     return { | ||||||
|  |         startNote: startNote, | ||||||
|  |         currentNote: currentNote, | ||||||
|         addButtonToToolbar, |         addButtonToToolbar, | ||||||
|         activateNote, |         activateNote, | ||||||
|         getInstanceName: noteTree.getInstanceName |         getInstanceName: noteTree.getInstanceName, | ||||||
|  |         runOnServer | ||||||
|     } |     } | ||||||
| })(); | } | ||||||
| @@ -29,6 +29,9 @@ const sqlConsole = (function() { | |||||||
|             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; |             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||||
|             CodeMirror.keyMap.default["Tab"] = "indentMore"; |             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'; |             CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||||
|  |  | ||||||
|             codeEditor = CodeMirror($query[0], { |             codeEditor = CodeMirror($query[0], { | ||||||
| @@ -45,7 +48,11 @@ const sqlConsole = (function() { | |||||||
|         codeEditor.focus(); |         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 sqlQuery = codeEditor.getValue(); | ||||||
|  |  | ||||||
|         const result = await server.post("sql/execute", { |         const result = await server.post("sql/execute", { | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| function exportSubTree(noteId) { | function exportSubTree(noteId) { | ||||||
|     const url = getHost() + "/api/export/" + noteId; |     const url = getHost() + "/api/export/" + noteId + "?protectedSessionId=" | ||||||
|  |         + encodeURIComponent(protected_session.getProtectedSessionId()); | ||||||
|  |  | ||||||
|     download(url); |     download(url); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -201,9 +201,9 @@ window.onerror = function (msg, url, lineNo, columnNo, error) { | |||||||
| $("#logout-button").toggle(!isElectron()); | $("#logout-button").toggle(!isElectron()); | ||||||
|  |  | ||||||
| $(document).ready(() => { | $(document).ready(() => { | ||||||
|     server.get("script/startup").then(scripts => { |     server.get("script/startup").then(scriptBundles => { | ||||||
|         for (const script of scripts) { |         for (const bundle of scriptBundles) { | ||||||
|             executeScript(script); |             executeBundle(bundle); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -77,13 +77,15 @@ const noteEditor = (function() { | |||||||
|  |  | ||||||
|     function updateNoteFromInputs(note) { |     function updateNoteFromInputs(note) { | ||||||
|         if (note.detail.type === 'text') { |         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 |             // if content is only tags/whitespace (typically <p> </p>), then just make it empty | ||||||
|             // this is important when setting new note to code |             // this is important when setting new note to code | ||||||
|             if (jQuery(note.detail.content).text().trim() === '') { |             if (jQuery(content).text().trim() === '' && !content.includes("<img")) { | ||||||
|                 note.detail.content = '' |                 content = ''; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             note.detail.content = content; | ||||||
|         } |         } | ||||||
|         else if (note.detail.type === 'code') { |         else if (note.detail.type === 'code') { | ||||||
|             note.detail.content = codeEditor.getValue(); |             note.detail.content = codeEditor.getValue(); | ||||||
| @@ -217,9 +219,11 @@ const noteEditor = (function() { | |||||||
|         if (currentNote.detail.type === 'render') { |         if (currentNote.detail.type === 'render') { | ||||||
|             $noteDetailRender.show(); |             $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') { |         else if (currentNote.detail.type === 'file') { | ||||||
|             $noteDetailAttachment.show(); |             $noteDetailAttachment.show(); | ||||||
| @@ -298,9 +302,17 @@ const noteEditor = (function() { | |||||||
|             // make sure note is saved so we load latest changes |             // make sure note is saved so we load latest changes | ||||||
|             await saveNoteIfChanged(); |             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') { |         if (note.type === 'code') { | ||||||
|             extraClasses.push("code"); |             extraClasses.push("code"); | ||||||
|         } |         } | ||||||
|  |         else if (note.type === 'render') { | ||||||
|  |             extraClasses.push('render'); | ||||||
|  |         } | ||||||
|         else if (note.type === 'file') { |         else if (note.type === 'file') { | ||||||
|             extraClasses.push('attachment'); |             extraClasses.push('attachment'); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -25,7 +25,8 @@ const noteType = (function() { | |||||||
|             { mime: 'text/html', title: 'HTML' }, |             { mime: 'text/html', title: 'HTML' }, | ||||||
|             { mime: 'message/http', title: 'HTTP' }, |             { mime: 'message/http', title: 'HTTP' }, | ||||||
|             { mime: 'text/x-java', title: 'Java' }, |             { 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: 'application/json', title: 'JSON' }, | ||||||
|             { mime: 'text/x-kotlin', title: 'Kotlin' }, |             { mime: 'text/x-kotlin', title: 'Kotlin' }, | ||||||
|             { mime: 'text/x-lua', title: 'Lua' }, |             { mime: 'text/x-lua', title: 'Lua' }, | ||||||
| @@ -121,7 +122,7 @@ const noteType = (function() { | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         this.updateExecuteScriptButtonVisibility = 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); |         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; |     let i = 1; | ||||||
|     const reqResolves = {}; |     const reqResolves = {}; | ||||||
|  |  | ||||||
| @@ -126,8 +94,6 @@ const server = (function() { | |||||||
|         post, |         post, | ||||||
|         put, |         put, | ||||||
|         remove, |         remove, | ||||||
|         exec, |  | ||||||
|         setJob, |  | ||||||
|         ajax, |         ajax, | ||||||
|         // don't remove, used from CKEditor image upload! |         // don't remove, used from CKEditor image upload! | ||||||
|         getHeaders |         getHeaders | ||||||
|   | |||||||
| @@ -115,8 +115,10 @@ async function stopWatch(what, func) { | |||||||
|     return ret; |     return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
| function executeScript(script) { | async function executeBundle(bundle) { | ||||||
|     eval(script); |     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) { | function formatValueWithWhitespace(val) { | ||||||
| @@ -206,3 +208,15 @@ function download(url) { | |||||||
|         window.location.href = 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; | ||||||
|  | } | ||||||
| @@ -28,6 +28,11 @@ | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function validatorJavaScript(text, options) { |     async function validatorJavaScript(text, options) { | ||||||
|  |         if (noteEditor.getCurrentNote().detail.mime === 'application/json') { | ||||||
|  |             // eslint doesn't seem to validate pure JSON well | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         await requireLibrary(ESLINT); |         await requireLibrary(ESLINT); | ||||||
|  |  | ||||||
|         if (text.length > 20000) { |         if (text.length > 20000) { | ||||||
|   | |||||||
| @@ -846,6 +846,8 @@ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); | |||||||
| CodeMirror.defineMIME("text/javascript", "javascript"); | CodeMirror.defineMIME("text/javascript", "javascript"); | ||||||
| CodeMirror.defineMIME("text/ecmascript", "javascript"); | CodeMirror.defineMIME("text/ecmascript", "javascript"); | ||||||
| CodeMirror.defineMIME("application/javascript", "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/x-javascript", "javascript"); | ||||||
| CodeMirror.defineMIME("application/ecmascript", "javascript"); | CodeMirror.defineMIME("application/ecmascript", "javascript"); | ||||||
| CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); | 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: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]}, | ||||||
|     {name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]}, |     {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: "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"]}, |      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", 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"]}, |     {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"); |     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 { | span.fancytree-node.protected > span.fancytree-icon { | ||||||
|     filter: drop-shadow(2px 2px 2px black); |     filter: drop-shadow(2px 2px 2px black); | ||||||
| } | } | ||||||
| @@ -108,6 +113,9 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title { | |||||||
|  |  | ||||||
| .icon-action { | .icon-action { | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |     display: block; | ||||||
|  |     height: 24px; | ||||||
|  |     width: 24px; | ||||||
| } | } | ||||||
|  |  | ||||||
| #protect-button, #unprotect-button { | #protect-button, #unprotect-button { | ||||||
|   | |||||||
| @@ -3,21 +3,22 @@ | |||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const attributes = require('../../services/attributes'); |  | ||||||
| const html = require('html'); | const html = require('html'); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
| const tar = require('tar-stream'); | const tar = require('tar-stream'); | ||||||
| const sanitize = require("sanitize-filename"); | const sanitize = require("sanitize-filename"); | ||||||
|  | const Repository = require("../../services/repository"); | ||||||
|  |  | ||||||
| router.get('/:noteId/', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|  |     const repo = new Repository(req); | ||||||
|  |  | ||||||
|     const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]); |     const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]); | ||||||
|  |  | ||||||
|     const pack = tar.pack(); |     const pack = tar.pack(); | ||||||
|  |  | ||||||
|     const name = await exportNote(noteTreeId, '', pack); |     const name = await exportNote(noteTreeId, '', pack, repo); | ||||||
|  |  | ||||||
|     pack.finalize(); |     pack.finalize(); | ||||||
|  |  | ||||||
| @@ -27,9 +28,9 @@ router.get('/:noteId/', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
|     pack.pipe(res); |     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 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) { |     if (note.isProtected) { | ||||||
|         return; |         return; | ||||||
| @@ -54,7 +55,7 @@ async function exportNote(noteTreeId, directory, pack) { | |||||||
|  |  | ||||||
|     if (children.length > 0) { |     if (children.length > 0) { | ||||||
|         for (const child of children) { |         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) { | async function getMetadata(note) { | ||||||
|     return { |     return { | ||||||
|  |         version: 1, | ||||||
|         title: note.title, |         title: note.title, | ||||||
|         type: note.type, |         type: note.type, | ||||||
|         mime: note.mime, |         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 router = express.Router(); | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
|  | const attributes = require('../../services/attributes'); | ||||||
| const notes = require('../../services/notes'); | const notes = require('../../services/notes'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
| const tar = require('tar-stream'); | const tar = require('tar-stream'); | ||||||
| @@ -111,6 +112,10 @@ router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload | |||||||
|  |  | ||||||
| async function importNotes(files, parentNoteId, sourceId) { | async function importNotes(files, parentNoteId, sourceId) { | ||||||
|     for (const file of files) { |     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') { |         if (file.meta.type !== 'file') { | ||||||
|             file.data = file.data.toString("UTF-8"); |             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, { |         const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, { | ||||||
|             type: file.meta.type, |             type: file.meta.type, | ||||||
|             mime: file.meta.mime, |             mime: file.meta.mime, | ||||||
|             attributes: file.meta.attributes, |  | ||||||
|             sourceId: sourceId |             sourceId: sourceId | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|  |         for (const attr of file.meta.attributes) { | ||||||
|  |             await attributes.createAttribute(noteId, attr.name, attr.value); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (file.children.length > 0) { |         if (file.children.length > 0) { | ||||||
|             await importNotes(file.children, noteId, sourceId); |             await importNotes(file.children, noteId, sourceId); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -4,87 +4,50 @@ const express = require('express'); | |||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
| const notes = require('../../services/notes'); |  | ||||||
| const attributes = require('../../services/attributes'); | const attributes = require('../../services/attributes'); | ||||||
| const script = require('../../services/script'); | const script = require('../../services/script'); | ||||||
| const Repository = require('../../services/repository'); | const Repository = require('../../services/repository'); | ||||||
|  |  | ||||||
| router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => { | 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({ |     res.send({ | ||||||
|         executionResult: ret |         executionResult: ret | ||||||
|     }); |     }); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| router.post('/job', auth.checkApiAuth, wrap(async (req, res, next) => { | router.post('/run/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     await script.setJob(req.body); |     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) => { | router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup"); |  | ||||||
|     const repository = new Repository(req); |     const repository = new Repository(req); | ||||||
|  |     const notes = await attributes.getNotesWithAttribute(repository, "run", "frontend_startup"); | ||||||
|  |  | ||||||
|     const scripts = []; |     const scripts = []; | ||||||
|  |  | ||||||
|     for (const noteId of noteIds) { |     for (const note of notes) { | ||||||
|         scripts.push(await getNoteWithSubtreeScript(noteId, repository)); |         const bundle = await script.getScriptBundle(note); | ||||||
|  |  | ||||||
|  |         scripts.push(bundle); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     res.send(scripts); |     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 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' |  | ||||||
|                                            AND (notes.mime = 'application/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.mime === 'application/javascript') { |  | ||||||
|             child.content = '<script>' + child.content + '</script>'; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         script += child.content + "\r\n"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return script; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -5,13 +5,32 @@ const router = express.Router(); | |||||||
| const auth = require('../services/auth'); | const auth = require('../services/auth'); | ||||||
| const source_id = require('../services/source_id'); | const source_id = require('../services/source_id'); | ||||||
| const sql = require('../services/sql'); | const sql = require('../services/sql'); | ||||||
|  | const Repository = require('../services/repository'); | ||||||
|  | const attributes = require('../services/attributes'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
|  |  | ||||||
| router.get('', auth.checkAuth, wrap(async (req, res, next) => { | router.get('', auth.checkAuth, wrap(async (req, res, next) => { | ||||||
|  |     const repository = new Repository(req); | ||||||
|  |  | ||||||
|     res.render('index', { |     res.render('index', { | ||||||
|         sourceId: await source_id.generateSourceId(), |         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; | module.exports = router; | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| const build = require('./build'); | const build = require('./build'); | ||||||
| const packageJson = require('../../package'); | const packageJson = require('../../package'); | ||||||
|  |  | ||||||
| const APP_DB_VERSION = 77; | const APP_DB_VERSION = 78; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     app_version: packageJson.version, |     app_version: packageJson.version, | ||||||
|   | |||||||
| @@ -3,14 +3,18 @@ | |||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
| const sync_table = require('./sync_table'); | const sync_table = require('./sync_table'); | ||||||
| const Repository = require('./repository'); |  | ||||||
|  |  | ||||||
| const BUILTIN_ATTRIBUTES = [ | const BUILTIN_ATTRIBUTES = [ | ||||||
|     'run_on_startup', |     'frontend_startup', | ||||||
|  |     'backend_startup', | ||||||
|     'disable_versioning', |     'disable_versioning', | ||||||
|     'calendar_root', |     'calendar_root', | ||||||
|     'hide_in_autocomplete', |     'hide_in_autocomplete', | ||||||
|     'exclude_from_export' |     'exclude_from_export', | ||||||
|  |     'run', | ||||||
|  |     'manual_transaction_handling', | ||||||
|  |     'disable_inclusion', | ||||||
|  |     'app_css' | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| async function getNoteAttributeMap(noteId) { | async function getNoteAttributeMap(noteId) { | ||||||
| @@ -25,9 +29,7 @@ async function getNoteIdWithAttribute(name, value) { | |||||||
|                 AND attributes.value = ?`, [name, value]); |                 AND attributes.value = ?`, [name, value]); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getNotesWithAttribute(dataKey, name, value) { | async function getNotesWithAttribute(repository, name, value) { | ||||||
|     const repository = new Repository(dataKey); |  | ||||||
|  |  | ||||||
|     let notes; |     let notes; | ||||||
|  |  | ||||||
|     if (value !== undefined) { |     if (value !== undefined) { | ||||||
| @@ -42,8 +44,8 @@ async function getNotesWithAttribute(dataKey, name, value) { | |||||||
|     return notes; |     return notes; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getNoteWithAttribute(dataKey, name, value) { | async function getNoteWithAttribute(repository, name, value) { | ||||||
|     const notes = getNotesWithAttribute(dataKey, name, value); |     const notes = getNotesWithAttribute(repository, name, value); | ||||||
|  |  | ||||||
|     return notes.length > 0 ? notes[0] : null; |     return notes.length > 0 ? notes[0] : null; | ||||||
| } | } | ||||||
| @@ -60,12 +62,14 @@ async function createAttribute(noteId, name, value = "", sourceId = null) { | |||||||
|  |  | ||||||
|     const now = utils.nowDate(); |     const now = utils.nowDate(); | ||||||
|     const attributeId = utils.newAttributeId(); |     const attributeId = utils.newAttributeId(); | ||||||
|  |     const position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [noteId]); | ||||||
|  |  | ||||||
|     await sql.insert("attributes", { |     await sql.insert("attributes", { | ||||||
|         attributeId: attributeId, |         attributeId: attributeId, | ||||||
|         noteId: noteId, |         noteId: noteId, | ||||||
|         name: name, |         name: name, | ||||||
|         value: value, |         value: value, | ||||||
|  |         position: position, | ||||||
|         dateModified: now, |         dateModified: now, | ||||||
|         dateCreated: now, |         dateCreated: now, | ||||||
|         isDeleted: false |         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 sql = require('./sql'); | ||||||
| const ScriptContext = require('./script_context'); | const ScriptContext = require('./script_context'); | ||||||
|  | const Repository = require('./repository'); | ||||||
|  |  | ||||||
| async function executeScript(dataKey, script, params) { | async function executeNote(dataKey, note) { | ||||||
|     const ctx = new ScriptContext(dataKey); |     if (!note.isJavaScript()) { | ||||||
|     const paramsStr = getParams(params); |         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) { | async function execute(ctx, script, paramsStr) { | ||||||
|     return await (function() { return eval(`const api = this; (${script})(${paramsStr})`); }.call(ctx)); |     return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${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); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function getParams(params) { | function getParams(params) { | ||||||
| @@ -72,7 +68,72 @@ function getParams(params) { | |||||||
|     }).join(","); |     }).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 = { | module.exports = { | ||||||
|  |     executeNote, | ||||||
|     executeScript, |     executeScript, | ||||||
|     setJob |     getScriptBundle | ||||||
| }; | }; | ||||||
| @@ -9,9 +9,18 @@ const config = require('./config'); | |||||||
| const Repository = require('./repository'); | const Repository = require('./repository'); | ||||||
| const axios = require('axios'); | const axios = require('axios'); | ||||||
|  |  | ||||||
| function ScriptContext(dataKey) { | function ScriptContext(dataKey, startNote, allNotes) { | ||||||
|     dataKey = protected_session.getDataKey(dataKey); |     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); |     const repository = new Repository(dataKey); | ||||||
|  |     this.startNote = startNote; | ||||||
|  |     this.currentNote = currentNote; | ||||||
|  |  | ||||||
|     this.axios = axios; |     this.axios = axios; | ||||||
|  |  | ||||||
| @@ -27,7 +36,7 @@ function ScriptContext(dataKey) { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.getNotesWithAttribute = async function (attrName, attrValue) { |     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) { |     this.getNoteWithAttribute = async function (attrName, attrValue) { | ||||||
| @@ -46,7 +55,7 @@ function ScriptContext(dataKey) { | |||||||
|  |  | ||||||
|     this.updateEntity = repository.updateEntity; |     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.getRootCalendarNoteId = date_notes.getRootCalendarNoteId; | ||||||
|     this.getDateNoteId = date_notes.getDateNoteId; |     this.getDateNoteId = date_notes.getDateNoteId; | ||||||
|   | |||||||
| @@ -134,6 +134,18 @@ function unescapeHtml(str) { | |||||||
|     return unescape(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 = { | module.exports = { | ||||||
|     randomSecureToken, |     randomSecureToken, | ||||||
|     randomString, |     randomString, | ||||||
| @@ -159,5 +171,6 @@ module.exports = { | |||||||
|     sanitizeSql, |     sanitizeSql, | ||||||
|     assertArguments, |     assertArguments, | ||||||
|     stopWatch, |     stopWatch, | ||||||
|     unescapeHtml |     unescapeHtml, | ||||||
|  |     toObject | ||||||
| }; | }; | ||||||
| @@ -40,21 +40,17 @@ | |||||||
|  |  | ||||||
|       <div class="hide-toggle" style="grid-area: tree-actions;"> |       <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;"> |         <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"> |           <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"/> |              style="background: url('images/icons/file-plus.png')"></a> | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action"> |           <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action" | ||||||
|             <img src="images/icons/list.png" alt="Collapse note tree"/> |              style="background: url('images/icons/list.png')"></a> | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action"> |           <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"/> |              style="background: url('images/icons/crosshair.png')"></a> | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action"> |           <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action" | ||||||
|             <img src="images/icons/search.png" alt="Search in notes"/> |              style="background: url('images/icons/search.png')"></a> | ||||||
|           </a> |  | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <input type="file" id="import-upload" style="display: none" /> |         <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" |              title="Protect the note so that password will be required to view the note" | ||||||
|              class="icon-action" |              class="icon-action" | ||||||
|              id="protect-button" |              id="protect-button" | ||||||
|              style="display: none;"> |              style="display: none; background: url('images/icons/lock.png')"></a> | ||||||
|             <img src="images/icons/lock.png" alt="Protect note"/> |  | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="protected_session.unprotectNoteAndSendToServer()" |           <a onclick="protected_session.unprotectNoteAndSendToServer()" | ||||||
|              title="Unprotect note so that password will not be required to access this note in the future" |              title="Unprotect note so that password will not be required to access this note in the future" | ||||||
|              class="icon-action" |              class="icon-action" | ||||||
|              id="unprotect-button" |              id="unprotect-button" | ||||||
|              style="display: none;"> |              style="display: none; background: url('images/icons/unlock.png')"></a> | ||||||
|             <img src="images/icons/unlock.png" alt="Unprotect note"/> |  | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|             |             | ||||||
|  |  | ||||||
| @@ -529,5 +521,9 @@ | |||||||
|       // final form which is pretty ugly. |       // final form which is pretty ugly. | ||||||
|       $("#container").show(); |       $("#container").show(); | ||||||
|     </script> |     </script> | ||||||
|  |  | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <%= appCss %> | ||||||
|  |     </style> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
		Reference in New Issue
	
	Block a user