mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			v0.7.0-bet
			...
			v0.8.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2acff07368 | ||
|  | bea1d24f07 | ||
|  | adc270c59f | ||
|  | 66064f7a94 | ||
|  | 1501fa8dbf | ||
|  | 60bba46d80 | ||
|  | 12c06ae97e | ||
|  | f0bea9cf71 | ||
|  | a555b6319c | ||
|  | 5dd93e4cdc | ||
|  | 3b4509d833 | ||
|  | 19308bbfbd | ||
|  | 4acc5432c3 | ||
|  | 08b8141fdf | ||
|  | e1200aa308 | ||
|  | 89666eb078 | ||
|  | d5605aa64d | ||
|  | 2582b016f9 | 
| @@ -1,3 +1,7 @@ | |||||||
|  | [General] | ||||||
|  | # Instance name can be used to distinguish between different instances | ||||||
|  | instanceName= | ||||||
|  |  | ||||||
| [Network] | [Network] | ||||||
| port=8080 | port=8080 | ||||||
| # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). | # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "version": "0.6.2", |   "version": "0.7.0-beta", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.7.0-beta", |   "version": "0.8.0-beta", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "electron.js", |   "main": "electron.js", | ||||||
|   "repository": { |   "repository": { | ||||||
| @@ -57,6 +57,7 @@ | |||||||
|     "session-file-store": "^1.1.2", |     "session-file-store": "^1.1.2", | ||||||
|     "simple-node-logger": "^0.93.30", |     "simple-node-logger": "^0.93.30", | ||||||
|     "sqlite": "^2.9.0", |     "sqlite": "^2.9.0", | ||||||
|  |     "tar-stream": "^1.5.5", | ||||||
|     "unescape": "^1.0.1", |     "unescape": "^1.0.1", | ||||||
|     "ws": "^3.3.2" |     "ws": "^3.3.2" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -73,8 +73,6 @@ require('./services/backup'); | |||||||
| // trigger consistency checks timer | // trigger consistency checks timer | ||||||
| require('./services/consistency_checks'); | require('./services/consistency_checks'); | ||||||
|  |  | ||||||
| require('./plugins/reddit'); |  | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     app, |     app, | ||||||
|     sessionParser |     sessionParser | ||||||
|   | |||||||
| @@ -43,6 +43,48 @@ class Note extends Entity { | |||||||
|         return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); |         return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async getChild(name) { | ||||||
|  |         return this.repository.getEntity(` | ||||||
|  |           SELECT notes.*  | ||||||
|  |           FROM note_tree  | ||||||
|  |             JOIN notes USING(noteId)  | ||||||
|  |           WHERE notes.isDeleted = 0 | ||||||
|  |                 AND note_tree.isDeleted = 0 | ||||||
|  |                 AND note_tree.parentNoteId = ? | ||||||
|  |                 AND notes.title = ?`, [this.noteId, name]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getChildren() { | ||||||
|  |         return this.repository.getEntities(` | ||||||
|  |           SELECT notes.*  | ||||||
|  |           FROM note_tree  | ||||||
|  |             JOIN notes USING(noteId)  | ||||||
|  |           WHERE notes.isDeleted = 0 | ||||||
|  |                 AND note_tree.isDeleted = 0 | ||||||
|  |                 AND note_tree.parentNoteId = ?`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getParents() { | ||||||
|  |         return this.repository.getEntities(` | ||||||
|  |           SELECT parent_notes.*  | ||||||
|  |           FROM  | ||||||
|  |             note_tree AS child_tree  | ||||||
|  |             JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId  | ||||||
|  |           WHERE child_tree.noteId = ? | ||||||
|  |                 AND child_tree.isDeleted = 0 | ||||||
|  |                 AND parent_notes.isDeleted = 0`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getNoteTree() { | ||||||
|  |         return this.repository.getEntities(` | ||||||
|  |           SELECT note_tree.*  | ||||||
|  |           FROM note_tree  | ||||||
|  |             JOIN notes USING(noteId)  | ||||||
|  |           WHERE notes.isDeleted = 0 | ||||||
|  |                 AND note_tree.isDeleted = 0 | ||||||
|  |                 AND note_tree.noteId = ?`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     beforeSaving() { |     beforeSaving() { | ||||||
|         this.content = JSON.stringify(this.jsonContent, null, '\t'); |         this.content = JSON.stringify(this.jsonContent, null, '\t'); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,144 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const sql = require('../services/sql'); |  | ||||||
| const notes = require('../services/notes'); |  | ||||||
| const axios = require('axios'); |  | ||||||
| const log = require('../services/log'); |  | ||||||
| const utils = require('../services/utils'); |  | ||||||
| const unescape = require('unescape'); |  | ||||||
| const attributes = require('../services/attributes'); |  | ||||||
| const sync_mutex = require('../services/sync_mutex'); |  | ||||||
| const config = require('../services/config'); |  | ||||||
| const date_notes = require('../services/date_notes'); |  | ||||||
|  |  | ||||||
| // "reddit" date note is subnote of date note which contains all reddit comments from that date |  | ||||||
| const REDDIT_DATE_ATTRIBUTE = 'reddit_date_note'; |  | ||||||
|  |  | ||||||
| async function createNote(parentNoteId, noteTitle, noteText) { |  | ||||||
|     return (await notes.createNewNote(parentNoteId, { |  | ||||||
|         title: noteTitle, |  | ||||||
|         content: noteText, |  | ||||||
|         target: 'into', |  | ||||||
|         isProtected: false |  | ||||||
|     })).noteId; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function redditId(kind, id) { |  | ||||||
|     return kind + "_" + id; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) { |  | ||||||
|     const dateStr = dateTimeStr.substr(0, 10); |  | ||||||
|  |  | ||||||
|     let redditDateNoteId = await attributes.getNoteIdWithAttribute(REDDIT_DATE_ATTRIBUTE, dateStr); |  | ||||||
|  |  | ||||||
|     if (!redditDateNoteId) { |  | ||||||
|         const dateNoteId = await date_notes.getDateNoteId(dateTimeStr, rootNoteId); |  | ||||||
|  |  | ||||||
|         redditDateNoteId = await createNote(dateNoteId, "Reddit"); |  | ||||||
|  |  | ||||||
|         await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr); |  | ||||||
|         await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return redditDateNoteId; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function importComments(rootNoteId, accountName, afterId = null) { |  | ||||||
|     let url = `https://www.reddit.com/user/${accountName}.json`; |  | ||||||
|  |  | ||||||
|     if (afterId) { |  | ||||||
|         url += "?after=" + afterId; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const response = await axios.get(url); |  | ||||||
|     const listing = response.data; |  | ||||||
|  |  | ||||||
|     if (listing.kind !== 'Listing') { |  | ||||||
|         log.info(`Reddit: Unknown object kind ${listing.kind}`); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const children = listing.data.children; |  | ||||||
|  |  | ||||||
|     let importedComments = 0; |  | ||||||
|  |  | ||||||
|     for (const child of children) { |  | ||||||
|         const comment = child.data; |  | ||||||
|  |  | ||||||
|         let commentNoteId = await attributes.getNoteIdWithAttribute('reddit_id', redditId(child.kind, comment.id)); |  | ||||||
|  |  | ||||||
|         if (commentNoteId) { |  | ||||||
|             continue; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const dateTimeStr = utils.dateStr(new Date(comment.created_utc * 1000)); |  | ||||||
|  |  | ||||||
|         const permaLink = 'https://reddit.com' + comment.permalink; |  | ||||||
|  |  | ||||||
|         const noteText = |  | ||||||
| `<p><a href="${permaLink}">${permaLink}</a></p> |  | ||||||
| <p>author: <a href="https://reddit.com/u/${comment.author}">${comment.author}</a>,  |  | ||||||
| subreddit: <a href="https://reddit.com/r/${comment.subreddit}">${comment.subreddit}</a>,  |  | ||||||
| karma: ${comment.score}, created at ${dateTimeStr}</p><p></p>` |  | ||||||
|             + unescape(comment.body_html); |  | ||||||
|  |  | ||||||
|         let parentNoteId = await getDateNoteIdForReddit(dateTimeStr, rootNoteId); |  | ||||||
|  |  | ||||||
|         await sql.doInTransaction(async () => { |  | ||||||
|             commentNoteId = await createNote(parentNoteId, comment.link_title, noteText); |  | ||||||
|  |  | ||||||
|             log.info("Reddit: Imported comment to note " + commentNoteId); |  | ||||||
|             importedComments++; |  | ||||||
|  |  | ||||||
|             await attributes.createAttribute(commentNoteId, "reddit_kind", child.kind); |  | ||||||
|             await attributes.createAttribute(commentNoteId, "reddit_id", redditId(child.kind, comment.id)); |  | ||||||
|             await attributes.createAttribute(commentNoteId, "reddit_created_utc", comment.created_utc); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // if there have been no imported comments on this page, there shouldn't be any to import |  | ||||||
|     // on the next page since those are older |  | ||||||
|     if (listing.data.after && importedComments > 0) { |  | ||||||
|         importedComments += await importComments(rootNoteId, accountName, listing.data.after); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return importedComments; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| let redditAccounts = []; |  | ||||||
|  |  | ||||||
| async function runImport() { |  | ||||||
|     const rootNoteId = await date_notes.getRootNoteId(); |  | ||||||
|  |  | ||||||
|     // technically mutex shouldn't be necessary but we want to avoid doing potentially expensive import |  | ||||||
|     // concurrently with sync |  | ||||||
|     await sync_mutex.doExclusively(async () => { |  | ||||||
|         let importedComments = 0; |  | ||||||
|  |  | ||||||
|         for (const account of redditAccounts) { |  | ||||||
|             importedComments += await importComments(rootNoteId, account); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         log.info(`Reddit: Imported ${importedComments} comments.`); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| sql.dbReady.then(async () => { |  | ||||||
|     if (!config['Reddit'] || config['Reddit']['enabled'] !== true) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const redditAccountsStr = config['Reddit']['accounts']; |  | ||||||
|  |  | ||||||
|     if (!redditAccountsStr) { |  | ||||||
|         log.info("Reddit: No reddit accounts defined in option 'reddit_accounts'"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     redditAccounts = redditAccountsStr.split(",").map(s => s.trim()); |  | ||||||
|  |  | ||||||
|     const pollingIntervalInSeconds = config['Reddit']['pollingIntervalInSeconds'] || (4 * 3600); |  | ||||||
|  |  | ||||||
|     setInterval(runImport, pollingIntervalInSeconds * 1000); |  | ||||||
|     setTimeout(runImport, 10000); // 10 seconds after startup - intentionally after initial sync |  | ||||||
| }); |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 358 B | 
| @@ -13,9 +13,9 @@ const api = (function() { | |||||||
|         $pluginButtons.append(button); |         $pluginButtons.append(button); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         addButtonToToolbar, |         addButtonToToolbar, | ||||||
|         activateNote |         activateNote, | ||||||
|  |         getInstanceName: noteTree.getInstanceName | ||||||
|     } |     } | ||||||
| })(); | })(); | ||||||
| @@ -85,9 +85,12 @@ const contextMenu = (function() { | |||||||
|             {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, |             {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, | ||||||
|             {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, |             {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, | ||||||
|             {title: "----"}, |             {title: "----"}, | ||||||
|             {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"}, |             {title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"}, | ||||||
|             {title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"}, |             {title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"}, | ||||||
|             {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} |             {title: "----"}, | ||||||
|  |             {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapseSubTree", uiIcon: "ui-icon-minus"}, | ||||||
|  |             {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, | ||||||
|  |             {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} | ||||||
|  |  | ||||||
|         ], |         ], | ||||||
|         beforeOpen: (event, ui) => { |         beforeOpen: (event, ui) => { | ||||||
| @@ -139,13 +142,19 @@ const contextMenu = (function() { | |||||||
|             else if (ui.cmd === "delete") { |             else if (ui.cmd === "delete") { | ||||||
|                 treeChanges.deleteNodes(noteTree.getSelectedNodes(true)); |                 treeChanges.deleteNodes(noteTree.getSelectedNodes(true)); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "collapse-sub-tree") { |             else if (ui.cmd === "exportSubTree") { | ||||||
|  |                 exportSubTree(node.data.noteId); | ||||||
|  |             } | ||||||
|  |             else if (ui.cmd === "importSubTree") { | ||||||
|  |                 importSubTree(node.data.noteId); | ||||||
|  |             } | ||||||
|  |             else if (ui.cmd === "collapseSubTree") { | ||||||
|                 noteTree.collapseTree(node); |                 noteTree.collapseTree(node); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "force-note-sync") { |             else if (ui.cmd === "forceNoteSync") { | ||||||
|                 forceNoteSync(node.data.noteId); |                 forceNoteSync(node.data.noteId); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "sort-alphabetically") { |             else if (ui.cmd === "sortAlphabetically") { | ||||||
|                 noteTree.sortAlphabetically(node.data.noteId); |                 noteTree.sortAlphabetically(node.data.noteId); | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | function exportSubTree(noteId) { | ||||||
|  |     const url = getHost() + "/api/export/" + noteId; | ||||||
|  |  | ||||||
|  |     download(url); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let importNoteId; | ||||||
|  |  | ||||||
|  | function importSubTree(noteId) { | ||||||
|  |     importNoteId = noteId; | ||||||
|  |  | ||||||
|  |     $("#import-upload").trigger('click'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $("#import-upload").change(async function() { | ||||||
|  |     const formData = new FormData(); | ||||||
|  |     formData.append('upload', this.files[0]); | ||||||
|  |  | ||||||
|  |     await $.ajax({ | ||||||
|  |         url: baseApiUrl + 'import/' + importNoteId, | ||||||
|  |         headers: server.getHeaders(), | ||||||
|  |         data: formData, | ||||||
|  |         type: 'POST', | ||||||
|  |         contentType: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |         processData: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await noteTree.reload(); | ||||||
|  | }); | ||||||
| @@ -114,22 +114,32 @@ $.ui.autocomplete.filter = (array, terms) => { | |||||||
|     const tokens = terms.toLowerCase().split(" "); |     const tokens = terms.toLowerCase().split(" "); | ||||||
|  |  | ||||||
|     for (const item of array) { |     for (const item of array) { | ||||||
|         let found = true; |  | ||||||
|         const lcLabel = item.label.toLowerCase(); |         const lcLabel = item.label.toLowerCase(); | ||||||
|  |  | ||||||
|         for (const token of tokens) { |         const found = tokens.every(token => lcLabel.indexOf(token) !== -1); | ||||||
|             if (lcLabel.indexOf(token) === -1) { |         if (!found) { | ||||||
|                 found = false; |             continue; | ||||||
|                 break; |         } | ||||||
|  |  | ||||||
|  |         // this is not completely correct and might cause minor problems with note with names containing this " / " | ||||||
|  |         const lastSegmentIndex = lcLabel.lastIndexOf(" / "); | ||||||
|  |  | ||||||
|  |         if (lastSegmentIndex !== -1) { | ||||||
|  |             const lastSegment = lcLabel.substr(lastSegmentIndex + 3); | ||||||
|  |  | ||||||
|  |             // at least some token needs to be in the last segment (leaf note), otherwise this | ||||||
|  |             // particular note is not that interesting (query is satisfied by parent note) | ||||||
|  |             const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1); | ||||||
|  |  | ||||||
|  |             if (!foundInLastSegment) { | ||||||
|  |                 continue; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (found) { |         results.push(item); | ||||||
|             results.push(item); |  | ||||||
|  |  | ||||||
|             if (results.length > 100) { |         if (results.length > 100) { | ||||||
|                 break; |             break; | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -216,10 +226,10 @@ if (isElectron()) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function uploadAttachment() { | function uploadAttachment() { | ||||||
|     $("#file-upload").trigger('click'); |     $("#attachment-upload").trigger('click'); | ||||||
| } | } | ||||||
|  |  | ||||||
| $("#file-upload").change(async function() { | $("#attachment-upload").change(async function() { | ||||||
|     const formData = new FormData(); |     const formData = new FormData(); | ||||||
|     formData.append('upload', this.files[0]); |     formData.append('upload', this.files[0]); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -84,7 +84,7 @@ const messaging = (function() { | |||||||
|     let connectionBrokenNotification = null; |     let connectionBrokenNotification = null; | ||||||
|  |  | ||||||
|     setInterval(async () => { |     setInterval(async () => { | ||||||
|         if (new Date().getTime() - lastPingTs > 5000) { |         if (new Date().getTime() - lastPingTs > 30000) { | ||||||
|             if (!connectionBrokenNotification) { |             if (!connectionBrokenNotification) { | ||||||
|                 connectionBrokenNotification = $.notify({ |                 connectionBrokenNotification = $.notify({ | ||||||
|                     // options |                     // options | ||||||
|   | |||||||
| @@ -154,7 +154,10 @@ const noteEditor = (function() { | |||||||
|                     indentUnit: 4, |                     indentUnit: 4, | ||||||
|                     matchBrackets: true, |                     matchBrackets: true, | ||||||
|                     matchTags: { bothTags: true }, |                     matchTags: { bothTags: true }, | ||||||
|                     highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false } |                     highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }, | ||||||
|  |                     lint: true, | ||||||
|  |                     gutters: ["CodeMirror-lint-markers"], | ||||||
|  |                     lineNumbers: true | ||||||
|                 }); |                 }); | ||||||
|  |  | ||||||
|                 codeEditor.on('change', noteChanged); |                 codeEditor.on('change', noteChanged); | ||||||
| @@ -171,6 +174,8 @@ const noteEditor = (function() { | |||||||
|                 codeEditor.setOption("mode", info.mime); |                 codeEditor.setOption("mode", info.mime); | ||||||
|                 CodeMirror.autoLoadMode(codeEditor, info.mode); |                 CodeMirror.autoLoadMode(codeEditor, info.mode); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             codeEditor.refresh(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -299,16 +304,7 @@ const noteEditor = (function() { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $attachmentDownload.click(() => { |     $attachmentDownload.click(() => download(getAttachmentUrl())); | ||||||
|         if (isElectron()) { |  | ||||||
|             const remote = require('electron').remote; |  | ||||||
|  |  | ||||||
|             remote.getCurrentWebContents().downloadURL(getAttachmentUrl()); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             window.location.href = getAttachmentUrl(); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     $attachmentOpen.click(() => { |     $attachmentOpen.click(() => { | ||||||
|         if (isElectron()) { |         if (isElectron()) { | ||||||
| @@ -323,12 +319,8 @@ const noteEditor = (function() { | |||||||
|  |  | ||||||
|     function getAttachmentUrl() { |     function getAttachmentUrl() { | ||||||
|         // electron needs absolute URL so we extract current host, port, protocol |         // electron needs absolute URL so we extract current host, port, protocol | ||||||
|         const url = new URL(window.location.href); |         return getHost() + "/api/attachments/download/" + getCurrentNoteId() | ||||||
|         const host = url.protocol + "//" + url.hostname + ":" + url.port; |             + "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId()); | ||||||
|  |  | ||||||
|         const downloadUrl = "/api/attachments/download/" + getCurrentNoteId(); |  | ||||||
|  |  | ||||||
|         return host + downloadUrl; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $(document).ready(() => { |     $(document).ready(() => { | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ const noteTree = (function() { | |||||||
|     const $parentList = $("#parent-list"); |     const $parentList = $("#parent-list"); | ||||||
|     const $parentListList = $("#parent-list-inner"); |     const $parentListList = $("#parent-list-inner"); | ||||||
|  |  | ||||||
|  |     let instanceName = null; // should have better place | ||||||
|  |  | ||||||
|     let startNotePath = null; |     let startNotePath = null; | ||||||
|     let notesTreeMap = {}; |     let notesTreeMap = {}; | ||||||
|  |  | ||||||
| @@ -155,6 +157,9 @@ const noteTree = (function() { | |||||||
|         if (note.type === 'code') { |         if (note.type === 'code') { | ||||||
|             extraClasses.push("code"); |             extraClasses.push("code"); | ||||||
|         } |         } | ||||||
|  |         else if (note.type === 'file') { | ||||||
|  |             extraClasses.push('attachment'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return extraClasses.join(" "); |         return extraClasses.join(" "); | ||||||
|     } |     } | ||||||
| @@ -645,6 +650,7 @@ const noteTree = (function() { | |||||||
|     async function loadTree() { |     async function loadTree() { | ||||||
|         const resp = await server.get('tree'); |         const resp = await server.get('tree'); | ||||||
|         startNotePath = resp.start_note_path; |         startNotePath = resp.start_note_path; | ||||||
|  |         instanceName = resp.instanceName; | ||||||
|  |  | ||||||
|         if (document.location.hash) { |         if (document.location.hash) { | ||||||
|             startNotePath = getNotePathFromAddress(); |             startNotePath = getNotePathFromAddress(); | ||||||
| @@ -710,6 +716,9 @@ const noteTree = (function() { | |||||||
|             titlePath = ''; |             titlePath = ''; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // https://github.com/zadam/trilium/issues/46 | ||||||
|  |         // unfortunately not easy to implement because we don't have an easy access to note's isProtected property | ||||||
|  |  | ||||||
|         const autocompleteItems = []; |         const autocompleteItems = []; | ||||||
|  |  | ||||||
|         for (const childNoteId of parentToChildren[parentNoteId]) { |         for (const childNoteId of parentToChildren[parentNoteId]) { | ||||||
| @@ -820,6 +829,10 @@ const noteTree = (function() { | |||||||
|         return !!childToParents[noteId]; |         return !!childToParents[noteId]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     function getInstanceName() { | ||||||
|  |         return instanceName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+o', e => { |     $(document).bind('keydown', 'ctrl+o', e => { | ||||||
|         const node = getCurrentNode(); |         const node = getCurrentNode(); | ||||||
|         const parentNoteId = node.data.parentNoteId; |         const parentNoteId = node.data.parentNoteId; | ||||||
| @@ -894,6 +907,7 @@ const noteTree = (function() { | |||||||
|         setParentChildRelation, |         setParentChildRelation, | ||||||
|         getSelectedNodes, |         getSelectedNodes, | ||||||
|         sortAlphabetically, |         sortAlphabetically, | ||||||
|         noteExists |         noteExists, | ||||||
|  |         getInstanceName | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
| @@ -25,7 +25,9 @@ const protected_session = (function() { | |||||||
|         if (requireProtectedSession && !isProtectedSessionAvailable()) { |         if (requireProtectedSession && !isProtectedSessionAvailable()) { | ||||||
|             protectedSessionDeferred = dfd; |             protectedSessionDeferred = dfd; | ||||||
|  |  | ||||||
|             $noteDetailWrapper.hide(); |             if (noteTree.getCurrentNode().data.isProtected) { | ||||||
|  |                 $noteDetailWrapper.hide(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             $dialog.dialog({ |             $dialog.dialog({ | ||||||
|                 modal: modal, |                 modal: modal, | ||||||
|   | |||||||
| @@ -32,6 +32,10 @@ const server = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     function prepareParams(params) { |     function prepareParams(params) { | ||||||
|  |         if (!params) { | ||||||
|  |             return params; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return params.map(p => { |         return params.map(p => { | ||||||
|             if (typeof p === "function") { |             if (typeof p === "function") { | ||||||
|                 return "!@#Function: " + p.toString(); |                 return "!@#Function: " + p.toString(); | ||||||
| @@ -52,6 +56,13 @@ const server = (function() { | |||||||
|         return ret.executionResult; |         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 = {}; | ||||||
|  |  | ||||||
| @@ -116,6 +127,7 @@ const server = (function() { | |||||||
|         put, |         put, | ||||||
|         remove, |         remove, | ||||||
|         exec, |         exec, | ||||||
|  |         setJob, | ||||||
|         ajax, |         ajax, | ||||||
|         // don't remove, used from CKEditor image upload! |         // don't remove, used from CKEditor image upload! | ||||||
|         getHeaders |         getHeaders | ||||||
|   | |||||||
| @@ -143,13 +143,18 @@ const CODE_MIRROR = { | |||||||
|         "libraries/codemirror/addon/edit/matchbrackets.js", |         "libraries/codemirror/addon/edit/matchbrackets.js", | ||||||
|         "libraries/codemirror/addon/edit/matchtags.js", |         "libraries/codemirror/addon/edit/matchtags.js", | ||||||
|         "libraries/codemirror/addon/search/match-highlighter.js", |         "libraries/codemirror/addon/search/match-highlighter.js", | ||||||
|         "libraries/codemirror/mode/meta.js" |         "libraries/codemirror/mode/meta.js", | ||||||
|  |         "libraries/codemirror/addon/lint/lint.js", | ||||||
|  |         "libraries/codemirror/addon/lint/eslint.js" | ||||||
|     ], |     ], | ||||||
|     css: [ |     css: [ | ||||||
|         "libraries/codemirror/codemirror.css" |         "libraries/codemirror/codemirror.css", | ||||||
|  |         "libraries/codemirror/addon/lint/lint.css" | ||||||
|     ] |     ] | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const ESLINT = { js: [ "libraries/eslint.js" ] }; | ||||||
|  |  | ||||||
| async function requireLibrary(library) { | async function requireLibrary(library) { | ||||||
|     if (library.css) { |     if (library.css) { | ||||||
|         library.css.map(cssUrl => requireCss(cssUrl)); |         library.css.map(cssUrl => requireCss(cssUrl)); | ||||||
| @@ -162,13 +167,13 @@ async function requireLibrary(library) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| async function requireScript(url) { | const dynamicallyLoadedScripts = []; | ||||||
|     const scripts = Array |  | ||||||
|         .from(document.querySelectorAll('script')) |  | ||||||
|         .map(scr => scr.src); |  | ||||||
|  |  | ||||||
|     if (!scripts.includes(url)) { | async function requireScript(url) { | ||||||
|         return $.ajax({ |     if (!dynamicallyLoadedScripts.includes(url)) { | ||||||
|  |         dynamicallyLoadedScripts.push(url); | ||||||
|  |  | ||||||
|  |         return await $.ajax({ | ||||||
|             url: url, |             url: url, | ||||||
|             dataType: "script", |             dataType: "script", | ||||||
|             cache: true |             cache: true | ||||||
| @@ -185,3 +190,19 @@ async function requireCss(url) { | |||||||
|         $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); |         $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function getHost() { | ||||||
|  |     const url = new URL(window.location.href); | ||||||
|  |     return url.protocol + "//" + url.hostname + ":" + url.port; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function download(url) { | ||||||
|  |     if (isElectron()) { | ||||||
|  |         const remote = require('electron').remote; | ||||||
|  |  | ||||||
|  |         remote.getCurrentWebContents().downloadURL(url); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         window.location.href = url; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -102,18 +102,23 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   var currentlyHighlighted = null; |  | ||||||
|   function doMatchBrackets(cm) { |   function doMatchBrackets(cm) { | ||||||
|     cm.operation(function() { |     cm.operation(function() { | ||||||
|       if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} |       if (cm.state.matchBrackets.currentlyHighlighted) { | ||||||
|       currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); |         cm.state.matchBrackets.currentlyHighlighted(); | ||||||
|  |         cm.state.matchBrackets.currentlyHighlighted = null; | ||||||
|  |       } | ||||||
|  |       cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { |   CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { | ||||||
|     if (old && old != CodeMirror.Init) { |     if (old && old != CodeMirror.Init) { | ||||||
|       cm.off("cursorActivity", doMatchBrackets); |       cm.off("cursorActivity", doMatchBrackets); | ||||||
|       if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} |       if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { | ||||||
|  |         cm.state.matchBrackets.currentlyHighlighted(); | ||||||
|  |         cm.state.matchBrackets.currentlyHighlighted = null; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     if (val) { |     if (val) { | ||||||
|       cm.state.matchBrackets = typeof val == "object" ? val : {}; |       cm.state.matchBrackets = typeof val == "object" ? val : {}; | ||||||
|   | |||||||
| @@ -138,7 +138,7 @@ | |||||||
|     var iter = new Iter(cm, start.line, 0); |     var iter = new Iter(cm, start.line, 0); | ||||||
|     for (;;) { |     for (;;) { | ||||||
|       var openTag = toNextTag(iter), end; |       var openTag = toNextTag(iter), end; | ||||||
|       if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return; |       if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return; | ||||||
|       if (!openTag[1] && end != "selfClose") { |       if (!openTag[1] && end != "selfClose") { | ||||||
|         var startPos = Pos(iter.line, iter.ch); |         var startPos = Pos(iter.line, iter.ch); | ||||||
|         var endPos = findMatchingClose(iter, openTag[2]); |         var endPos = findMatchingClose(iter, openTag[2]); | ||||||
|   | |||||||
							
								
								
									
										87
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | // CodeMirror, copyright (c) by Marijn Haverbeke and others | ||||||
|  | // Distributed under an MIT license: http://codemirror.net/LICENSE | ||||||
|  |  | ||||||
|  | (function(mod) { | ||||||
|  |   if (typeof exports == "object" && typeof module == "object") // CommonJS | ||||||
|  |     mod(require("../../lib/codemirror")); | ||||||
|  |   else if (typeof define == "function" && define.amd) // AMD | ||||||
|  |     define(["../../lib/codemirror"], mod); | ||||||
|  |   else // Plain browser env | ||||||
|  |     mod(CodeMirror); | ||||||
|  | })(function(CodeMirror) { | ||||||
|  |     "use strict"; | ||||||
|  |  | ||||||
|  |     async function validatorHtml(text, options) { | ||||||
|  |         const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text); | ||||||
|  |  | ||||||
|  |         if (result !== null) { | ||||||
|  |             // preceding code is copied over but any (non-newline) character is replaced with space | ||||||
|  |             // this will preserve line numbers etc. | ||||||
|  |             const prefix = text.substr(0, result.index).replace(/./g, " "); | ||||||
|  |  | ||||||
|  |             const js = prefix + result[1]; | ||||||
|  |  | ||||||
|  |             return await validatorJavaScript(js, options); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function validatorJavaScript(text, options) { | ||||||
|  |         await requireLibrary(ESLINT); | ||||||
|  |  | ||||||
|  |         if (text.length > 20000) { | ||||||
|  |             console.log("Skipping linting because of large size: ", text.length); | ||||||
|  |  | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const errors = new eslint().verify(text, { | ||||||
|  |             root: true, | ||||||
|  |             parserOptions: { | ||||||
|  |                 ecmaVersion: 2017 | ||||||
|  |             }, | ||||||
|  |             extends: ['eslint:recommended', 'airbnb-base'], | ||||||
|  |             env: { | ||||||
|  |                 'node': true | ||||||
|  |             }, | ||||||
|  |             rules: { | ||||||
|  |                 'import/no-unresolved': 'off', | ||||||
|  |                 'func-names': 'off', | ||||||
|  |                 'comma-dangle': ['warn'], | ||||||
|  |                 'padded-blocks': 'off', | ||||||
|  |                 'linebreak-style': 'off', | ||||||
|  |                 'class-methods-use-this': 'off', | ||||||
|  |                 'no-unused-vars': ['warn', { vars: 'local', args: 'after-used' }], | ||||||
|  |                 'no-nested-ternary': 'off', | ||||||
|  |                 'no-underscore-dangle': ['error', {'allow': ['_super', '_lookupFactory']}] | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         const result = []; | ||||||
|  |         if (errors) { | ||||||
|  |             parseErrors(errors, result); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     CodeMirror.registerHelper("lint", "javascript", validatorJavaScript); | ||||||
|  |     CodeMirror.registerHelper("lint", "html", validatorHtml); | ||||||
|  |  | ||||||
|  |     function parseErrors(errors, output) { | ||||||
|  |         for (const error of errors) { | ||||||
|  |             const startLine = error.line - 1; | ||||||
|  |             const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine; | ||||||
|  |             const startCol = error.column - 1; | ||||||
|  |             const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1; | ||||||
|  |  | ||||||
|  |             output.push({ | ||||||
|  |                 message: error.message, | ||||||
|  |                 severity: error.severity === 1 ? "warning" : "error", | ||||||
|  |                 from: CodeMirror.Pos(startLine, startCol), | ||||||
|  |                 to: CodeMirror.Pos(endLine, endCol) | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }); | ||||||
							
								
								
									
										73
									
								
								src/public/libraries/codemirror/addon/lint/lint.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/public/libraries/codemirror/addon/lint/lint.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | /* The lint marker gutter */ | ||||||
|  | .CodeMirror-lint-markers { | ||||||
|  |   width: 16px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-tooltip { | ||||||
|  |   background-color: #ffd; | ||||||
|  |   border: 1px solid black; | ||||||
|  |   border-radius: 4px 4px 4px 4px; | ||||||
|  |   color: black; | ||||||
|  |   font-family: monospace; | ||||||
|  |   font-size: 10pt; | ||||||
|  |   overflow: hidden; | ||||||
|  |   padding: 2px 5px; | ||||||
|  |   position: fixed; | ||||||
|  |   white-space: pre; | ||||||
|  |   white-space: pre-wrap; | ||||||
|  |   z-index: 100; | ||||||
|  |   max-width: 600px; | ||||||
|  |   opacity: 0; | ||||||
|  |   transition: opacity .4s; | ||||||
|  |   -moz-transition: opacity .4s; | ||||||
|  |   -webkit-transition: opacity .4s; | ||||||
|  |   -o-transition: opacity .4s; | ||||||
|  |   -ms-transition: opacity .4s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { | ||||||
|  |   background-position: left bottom; | ||||||
|  |   background-repeat: repeat-x; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-mark-error { | ||||||
|  |   background-image: | ||||||
|  |   url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") | ||||||
|  |   ; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-mark-warning { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { | ||||||
|  |   background-position: center center; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   cursor: pointer; | ||||||
|  |   display: inline-block; | ||||||
|  |   height: 16px; | ||||||
|  |   width: 16px; | ||||||
|  |   vertical-align: middle; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { | ||||||
|  |   padding-left: 18px; | ||||||
|  |   background-position: top left; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-multiple { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   background-position: right bottom; | ||||||
|  |   width: 100%; height: 100%; | ||||||
|  | } | ||||||
							
								
								
									
										252
									
								
								src/public/libraries/codemirror/addon/lint/lint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								src/public/libraries/codemirror/addon/lint/lint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,252 @@ | |||||||
|  | // CodeMirror, copyright (c) by Marijn Haverbeke and others | ||||||
|  | // Distributed under an MIT license: http://codemirror.net/LICENSE | ||||||
|  |  | ||||||
|  | (function(mod) { | ||||||
|  |   if (typeof exports == "object" && typeof module == "object") // CommonJS | ||||||
|  |     mod(require("../../lib/codemirror")); | ||||||
|  |   else if (typeof define == "function" && define.amd) // AMD | ||||||
|  |     define(["../../lib/codemirror"], mod); | ||||||
|  |   else // Plain browser env | ||||||
|  |     mod(CodeMirror); | ||||||
|  | })(function(CodeMirror) { | ||||||
|  |   "use strict"; | ||||||
|  |   var GUTTER_ID = "CodeMirror-lint-markers"; | ||||||
|  |  | ||||||
|  |   function showTooltip(e, content) { | ||||||
|  |     var tt = document.createElement("div"); | ||||||
|  |     tt.className = "CodeMirror-lint-tooltip"; | ||||||
|  |     tt.appendChild(content.cloneNode(true)); | ||||||
|  |     document.body.appendChild(tt); | ||||||
|  |  | ||||||
|  |     function position(e) { | ||||||
|  |       if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position); | ||||||
|  |       tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px"; | ||||||
|  |       tt.style.left = (e.clientX + 5) + "px"; | ||||||
|  |     } | ||||||
|  |     CodeMirror.on(document, "mousemove", position); | ||||||
|  |     position(e); | ||||||
|  |     if (tt.style.opacity != null) tt.style.opacity = 1; | ||||||
|  |     return tt; | ||||||
|  |   } | ||||||
|  |   function rm(elt) { | ||||||
|  |     if (elt.parentNode) elt.parentNode.removeChild(elt); | ||||||
|  |   } | ||||||
|  |   function hideTooltip(tt) { | ||||||
|  |     if (!tt.parentNode) return; | ||||||
|  |     if (tt.style.opacity == null) rm(tt); | ||||||
|  |     tt.style.opacity = 0; | ||||||
|  |     setTimeout(function() { rm(tt); }, 600); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function showTooltipFor(e, content, node) { | ||||||
|  |     var tooltip = showTooltip(e, content); | ||||||
|  |     function hide() { | ||||||
|  |       CodeMirror.off(node, "mouseout", hide); | ||||||
|  |       if (tooltip) { hideTooltip(tooltip); tooltip = null; } | ||||||
|  |     } | ||||||
|  |     var poll = setInterval(function() { | ||||||
|  |       if (tooltip) for (var n = node;; n = n.parentNode) { | ||||||
|  |         if (n && n.nodeType == 11) n = n.host; | ||||||
|  |         if (n == document.body) return; | ||||||
|  |         if (!n) { hide(); break; } | ||||||
|  |       } | ||||||
|  |       if (!tooltip) return clearInterval(poll); | ||||||
|  |     }, 400); | ||||||
|  |     CodeMirror.on(node, "mouseout", hide); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function LintState(cm, options, hasGutter) { | ||||||
|  |     this.marked = []; | ||||||
|  |     this.options = options; | ||||||
|  |     this.timeout = null; | ||||||
|  |     this.hasGutter = hasGutter; | ||||||
|  |     this.onMouseOver = function(e) { onMouseOver(cm, e); }; | ||||||
|  |     this.waitingFor = 0 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function parseOptions(_cm, options) { | ||||||
|  |     if (options instanceof Function) return {getAnnotations: options}; | ||||||
|  |     if (!options || options === true) options = {}; | ||||||
|  |     return options; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function clearMarks(cm) { | ||||||
|  |     var state = cm.state.lint; | ||||||
|  |     if (state.hasGutter) cm.clearGutter(GUTTER_ID); | ||||||
|  |     for (var i = 0; i < state.marked.length; ++i) | ||||||
|  |       state.marked[i].clear(); | ||||||
|  |     state.marked.length = 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function makeMarker(labels, severity, multiple, tooltips) { | ||||||
|  |     var marker = document.createElement("div"), inner = marker; | ||||||
|  |     marker.className = "CodeMirror-lint-marker-" + severity; | ||||||
|  |     if (multiple) { | ||||||
|  |       inner = marker.appendChild(document.createElement("div")); | ||||||
|  |       inner.className = "CodeMirror-lint-marker-multiple"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) { | ||||||
|  |       showTooltipFor(e, labels, inner); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return marker; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getMaxSeverity(a, b) { | ||||||
|  |     if (a == "error") return a; | ||||||
|  |     else return b; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function groupByLine(annotations) { | ||||||
|  |     var lines = []; | ||||||
|  |     for (var i = 0; i < annotations.length; ++i) { | ||||||
|  |       var ann = annotations[i], line = ann.from.line; | ||||||
|  |       (lines[line] || (lines[line] = [])).push(ann); | ||||||
|  |     } | ||||||
|  |     return lines; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function annotationTooltip(ann) { | ||||||
|  |     var severity = ann.severity; | ||||||
|  |     if (!severity) severity = "error"; | ||||||
|  |     var tip = document.createElement("div"); | ||||||
|  |     tip.className = "CodeMirror-lint-message-" + severity; | ||||||
|  |     if (typeof ann.messageHTML != 'undefined') { | ||||||
|  |         tip.innerHTML = ann.messageHTML; | ||||||
|  |     } else { | ||||||
|  |         tip.appendChild(document.createTextNode(ann.message)); | ||||||
|  |     } | ||||||
|  |     return tip; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function lintAsync(cm, getAnnotations, passOptions) { | ||||||
|  |     var state = cm.state.lint | ||||||
|  |     var id = ++state.waitingFor | ||||||
|  |     function abort() { | ||||||
|  |       id = -1 | ||||||
|  |       cm.off("change", abort) | ||||||
|  |     } | ||||||
|  |     cm.on("change", abort) | ||||||
|  |     getAnnotations(cm.getValue(), function(annotations, arg2) { | ||||||
|  |       cm.off("change", abort) | ||||||
|  |       if (state.waitingFor != id) return | ||||||
|  |       if (arg2 && annotations instanceof CodeMirror) annotations = arg2 | ||||||
|  |       cm.operation(function() {updateLinting(cm, annotations)}) | ||||||
|  |     }, passOptions, cm); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function startLinting(cm) { | ||||||
|  |     var state = cm.state.lint, options = state.options; | ||||||
|  |     /* | ||||||
|  |      * Passing rules in `options` property prevents JSHint (and other linters) from complaining | ||||||
|  |      * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc. | ||||||
|  |      */ | ||||||
|  |     var passOptions = options.options || options; | ||||||
|  |     var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint"); | ||||||
|  |     if (!getAnnotations) return; | ||||||
|  |     if (options.async || getAnnotations.async) { | ||||||
|  |       lintAsync(cm, getAnnotations, passOptions) | ||||||
|  |     } else { | ||||||
|  |       var annotations = getAnnotations(cm.getValue(), passOptions, cm); | ||||||
|  |       if (!annotations) return; | ||||||
|  |       if (annotations.then) annotations.then(function(issues) { | ||||||
|  |         cm.operation(function() {updateLinting(cm, issues)}) | ||||||
|  |       }); | ||||||
|  |       else cm.operation(function() {updateLinting(cm, annotations)}) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function updateLinting(cm, annotationsNotSorted) { | ||||||
|  |     clearMarks(cm); | ||||||
|  |     var state = cm.state.lint, options = state.options; | ||||||
|  |  | ||||||
|  |     var annotations = groupByLine(annotationsNotSorted); | ||||||
|  |  | ||||||
|  |     for (var line = 0; line < annotations.length; ++line) { | ||||||
|  |       var anns = annotations[line]; | ||||||
|  |       if (!anns) continue; | ||||||
|  |  | ||||||
|  |       var maxSeverity = null; | ||||||
|  |       var tipLabel = state.hasGutter && document.createDocumentFragment(); | ||||||
|  |  | ||||||
|  |       for (var i = 0; i < anns.length; ++i) { | ||||||
|  |         var ann = anns[i]; | ||||||
|  |         var severity = ann.severity; | ||||||
|  |         if (!severity) severity = "error"; | ||||||
|  |         maxSeverity = getMaxSeverity(maxSeverity, severity); | ||||||
|  |  | ||||||
|  |         if (options.formatAnnotation) ann = options.formatAnnotation(ann); | ||||||
|  |         if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann)); | ||||||
|  |  | ||||||
|  |         if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, { | ||||||
|  |           className: "CodeMirror-lint-mark-" + severity, | ||||||
|  |           __annotation: ann | ||||||
|  |         })); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (state.hasGutter) | ||||||
|  |         cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1, | ||||||
|  |                                                        state.options.tooltips)); | ||||||
|  |     } | ||||||
|  |     if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function onChange(cm) { | ||||||
|  |     var state = cm.state.lint; | ||||||
|  |     if (!state) return; | ||||||
|  |     clearTimeout(state.timeout); | ||||||
|  |     state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function popupTooltips(annotations, e) { | ||||||
|  |     var target = e.target || e.srcElement; | ||||||
|  |     var tooltip = document.createDocumentFragment(); | ||||||
|  |     for (var i = 0; i < annotations.length; i++) { | ||||||
|  |       var ann = annotations[i]; | ||||||
|  |       tooltip.appendChild(annotationTooltip(ann)); | ||||||
|  |     } | ||||||
|  |     showTooltipFor(e, tooltip, target); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function onMouseOver(cm, e) { | ||||||
|  |     var target = e.target || e.srcElement; | ||||||
|  |     if (!/\bCodeMirror-lint-mark-/.test(target.className)) return; | ||||||
|  |     var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2; | ||||||
|  |     var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client")); | ||||||
|  |  | ||||||
|  |     var annotations = []; | ||||||
|  |     for (var i = 0; i < spans.length; ++i) { | ||||||
|  |       var ann = spans[i].__annotation; | ||||||
|  |       if (ann) annotations.push(ann); | ||||||
|  |     } | ||||||
|  |     if (annotations.length) popupTooltips(annotations, e); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   CodeMirror.defineOption("lint", false, function(cm, val, old) { | ||||||
|  |     if (old && old != CodeMirror.Init) { | ||||||
|  |       clearMarks(cm); | ||||||
|  |       if (cm.state.lint.options.lintOnChange !== false) | ||||||
|  |         cm.off("change", onChange); | ||||||
|  |       CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver); | ||||||
|  |       clearTimeout(cm.state.lint.timeout); | ||||||
|  |       delete cm.state.lint; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (val) { | ||||||
|  |       var gutters = cm.getOption("gutters"), hasLintGutter = false; | ||||||
|  |       for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true; | ||||||
|  |       var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter); | ||||||
|  |       if (state.options.lintOnChange !== false) | ||||||
|  |         cm.on("change", onChange); | ||||||
|  |       if (state.options.tooltips != false && state.options.tooltips != "gutter") | ||||||
|  |         CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver); | ||||||
|  |  | ||||||
|  |       startLinting(cm); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   CodeMirror.defineExtension("performLint", function() { | ||||||
|  |     if (this.state.lint) startLinting(this); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -90,7 +90,7 @@ | |||||||
|     var state = cm.state.matchHighlighter; |     var state = cm.state.matchHighlighter; | ||||||
|     cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); |     cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); | ||||||
|     if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { |     if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { | ||||||
|       var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; |       var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[+*?(){|^$]/g, "\\$&") + "\\b") : query; | ||||||
|       state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, |       state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, | ||||||
|         {className: "CodeMirror-selection-highlight-scrollbar"}); |         {className: "CodeMirror-selection-highlight-scrollbar"}); | ||||||
|     } |     } | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										101349
									
								
								src/public/libraries/eslint.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101349
									
								
								src/public/libraries/eslint.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -72,6 +72,11 @@ span.fancytree-node.fancytree-folder.code > span.fancytree-icon { | |||||||
|     background-image: url("../images/icons/code-folder.png"); |     background-image: url("../images/icons/code-folder.png"); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | span.fancytree-node.attachment > span.fancytree-icon { | ||||||
|  |     background-position: 0 0; | ||||||
|  |     background-image: url("../images/icons/paperclip.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); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ const sql = require('../../services/sql'); | |||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const notes = require('../../services/notes'); | const notes = require('../../services/notes'); | ||||||
| const attributes = require('../../services/attributes'); | const attributes = require('../../services/attributes'); | ||||||
|  | const protected_session = require('../../services/protected_session'); | ||||||
| const multer = require('multer')(); | const multer = require('multer')(); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
|  |  | ||||||
| @@ -44,9 +45,21 @@ router.post('/upload/:parentNoteId', auth.checkApiAuthOrElectron, multer.single( | |||||||
| router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => { | router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |     const protectedSessionId = req.query.protectedSessionId; | ||||||
|  |  | ||||||
|     if (!note) { |     if (!note) { | ||||||
|         return res.status(404).send(`Note ${parentNoteId} doesn't exist.`); |         return res.status(404).send(`Note ${noteId} doesn't exist.`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (note.isProtected) { | ||||||
|  |         const dataKey = protected_session.getDataKeyForProtectedSessionId(protectedSessionId); | ||||||
|  |  | ||||||
|  |         if (!dataKey) { | ||||||
|  |             res.status(401).send("Protected session not available"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         protected_session.decryptNote(dataKey, note); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const attributeMap = await attributes.getNoteAttributeMap(noteId); |     const attributeMap = await attributes.getNoteAttributeMap(noteId); | ||||||
|   | |||||||
| @@ -2,56 +2,72 @@ | |||||||
|  |  | ||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const rimraf = require('rimraf'); |  | ||||||
| const fs = require('fs'); |  | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const data_dir = require('../../services/data_dir'); | 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 sanitize = require("sanitize-filename"); | ||||||
|  |  | ||||||
| router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/:noteId/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); |  | ||||||
|  |  | ||||||
|     if (!fs.existsSync(data_dir.EXPORT_DIR)) { |  | ||||||
|         fs.mkdirSync(data_dir.EXPORT_DIR); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const completeExportDir = data_dir.EXPORT_DIR + '/' + directory; |  | ||||||
|  |  | ||||||
|     if (fs.existsSync(completeExportDir)) { |  | ||||||
|         rimraf.sync(completeExportDir); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fs.mkdirSync(completeExportDir); |  | ||||||
|  |  | ||||||
|     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]); | ||||||
|  |  | ||||||
|     await exportNote(noteTreeId, completeExportDir); |     const pack = tar.pack(); | ||||||
|  |  | ||||||
|     res.send({}); |     const name = await exportNote(noteTreeId, '', pack); | ||||||
|  |  | ||||||
|  |     pack.finalize(); | ||||||
|  |  | ||||||
|  |     res.setHeader('Content-Disposition', 'attachment; filename="' + name + '.tar"'); | ||||||
|  |     res.setHeader('Content-Type', 'application/tar'); | ||||||
|  |  | ||||||
|  |     pack.pipe(res); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| async function exportNote(noteTreeId, dir) { | async function exportNote(noteTreeId, directory, pack) { | ||||||
|     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 sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]); | ||||||
|  |  | ||||||
|     const pos = (noteTree.notePosition + '').padStart(4, '0'); |     if (note.isProtected) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2})); |     const metadata = await getMetadata(note); | ||||||
|  |  | ||||||
|  |     if ('exclude_from_export' in metadata.attributes) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const metadataJson = JSON.stringify(metadata, null, '\t'); | ||||||
|  |     const childFileName = directory + sanitize(note.title); | ||||||
|  |  | ||||||
|  |     pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson); | ||||||
|  |  | ||||||
|  |     const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; | ||||||
|  |  | ||||||
|  |     pack.entry({ name: childFileName + ".dat", size: content.length }, content); | ||||||
|  |  | ||||||
|     const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]); |     const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]); | ||||||
|  |  | ||||||
|     if (children.length > 0) { |     if (children.length > 0) { | ||||||
|         const childrenDir = dir + '/' + pos + '-' + note.title; |  | ||||||
|  |  | ||||||
|         fs.mkdirSync(childrenDir); |  | ||||||
|  |  | ||||||
|         for (const child of children) { |         for (const child of children) { | ||||||
|             await exportNote(child.noteTreeId, childrenDir); |             await exportNote(child.noteTreeId, childFileName + "/", pack); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return childFileName; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getMetadata(note) { | ||||||
|  |     return { | ||||||
|  |         title: note.title, | ||||||
|  |         type: note.type, | ||||||
|  |         mime: note.mime, | ||||||
|  |         attributes: await attributes.getNoteAttributeMap(note.noteId) | ||||||
|  |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -2,104 +2,128 @@ | |||||||
|  |  | ||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const fs = require('fs'); |  | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const data_dir = require('../../services/data_dir'); |  | ||||||
| const utils = require('../../services/utils'); |  | ||||||
| const sync_table = require('../../services/sync_table'); |  | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
|  | 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 multer = require('multer')(); | ||||||
|  | const stream = require('stream'); | ||||||
|  | const path = require('path'); | ||||||
|  |  | ||||||
| router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { | function getFileName(name) { | ||||||
|     const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); |     let key; | ||||||
|  |  | ||||||
|  |     if (name.endsWith(".dat")) { | ||||||
|  |         key = "data"; | ||||||
|  |         name = name.substr(0, name.length - 4); | ||||||
|  |     } | ||||||
|  |     else if (name.endsWith((".meta"))) { | ||||||
|  |         key = "meta"; | ||||||
|  |         name = name.substr(0, name.length - 5); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         throw new Error("Unknown file type in import archive: " + name); | ||||||
|  |     } | ||||||
|  |     return {name, key}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function parseImportFile(file) { | ||||||
|  |     const fileMap = {}; | ||||||
|  |     const files = []; | ||||||
|  |  | ||||||
|  |     const extract = tar.extract(); | ||||||
|  |  | ||||||
|  |     extract.on('entry', function(header, stream, next) { | ||||||
|  |         let {name, key} = getFileName(header.name); | ||||||
|  |  | ||||||
|  |         let file = fileMap[name]; | ||||||
|  |  | ||||||
|  |         if (!file) { | ||||||
|  |             file = fileMap[name] = { | ||||||
|  |                 children: [] | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             let parentFileName = path.dirname(header.name); | ||||||
|  |  | ||||||
|  |             if (parentFileName && parentFileName !== '.') { | ||||||
|  |                 fileMap[parentFileName].children.push(file); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 files.push(file); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const chunks = []; | ||||||
|  |  | ||||||
|  |         stream.on("data", function (chunk) { | ||||||
|  |             chunks.push(chunk); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // header is the tar header | ||||||
|  |         // stream is the content body (might be an empty stream) | ||||||
|  |         // call next when you are done with this entry | ||||||
|  |  | ||||||
|  |         stream.on('end', function() { | ||||||
|  |             file[key] = Buffer.concat(chunks); | ||||||
|  |  | ||||||
|  |             if (key === "meta") { | ||||||
|  |                 file[key] = JSON.parse(file[key].toString("UTF-8")); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             next(); // ready for next entry | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         stream.resume(); // just auto drain the stream | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return new Promise(resolve => { | ||||||
|  |         extract.on('finish', function() { | ||||||
|  |             resolve(files); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         const bufferStream = new stream.PassThrough(); | ||||||
|  |         bufferStream.end(file.buffer); | ||||||
|  |  | ||||||
|  |         bufferStream.pipe(extract); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => { | ||||||
|  |     const sourceId = req.headers.source_id; | ||||||
|     const parentNoteId = req.params.parentNoteId; |     const parentNoteId = req.params.parentNoteId; | ||||||
|  |     const file = req.file; | ||||||
|  |  | ||||||
|     const dir = data_dir.EXPORT_DIR + '/' + directory; |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); | ||||||
|  |  | ||||||
|     await sql.doInTransaction(async () => await importNotes(dir, parentNoteId)); |     if (!note) { | ||||||
|  |         return res.status(404).send(`Note ${parentNoteId} doesn't exist.`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const files = await parseImportFile(file); | ||||||
|  |  | ||||||
|  |     await sql.doInTransaction(async () => { | ||||||
|  |         await importNotes(files, parentNoteId, sourceId); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     res.send({}); |     res.send({}); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| async function importNotes(dir, parentNoteId) { | async function importNotes(files, parentNoteId, sourceId) { | ||||||
|     const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); |     for (const file of files) { | ||||||
|  |         if (file.meta.type !== 'file') { | ||||||
|     if (!parent) { |             file.data = file.data.toString("UTF-8"); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const fileList = fs.readdirSync(dir); |  | ||||||
|  |  | ||||||
|     for (const file of fileList) { |  | ||||||
|         const path = dir + '/' + file; |  | ||||||
|  |  | ||||||
|         if (fs.lstatSync(path).isDirectory()) { |  | ||||||
|             continue; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!file.endsWith('.html')) { |         const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, { | ||||||
|             continue; |             type: file.meta.type, | ||||||
|         } |             mime: file.meta.mime, | ||||||
|  |             attributes: file.meta.attributes, | ||||||
|         const fileNameWithoutExt = file.substr(0, file.length - 5); |             sourceId: sourceId | ||||||
|  |  | ||||||
|         let noteTitle; |  | ||||||
|         let notePos; |  | ||||||
|  |  | ||||||
|         const match = fileNameWithoutExt.match(/^([0-9]{4})-(.*)$/); |  | ||||||
|         if (match) { |  | ||||||
|             notePos = parseInt(match[1]); |  | ||||||
|             noteTitle = match[2]; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             let maxPos = await sql.getValue("SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]); |  | ||||||
|             if (maxPos) { |  | ||||||
|                 notePos = maxPos + 1; |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 notePos = 0; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             noteTitle = fileNameWithoutExt; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const noteText = fs.readFileSync(path, "utf8"); |  | ||||||
|  |  | ||||||
|         const noteId = utils.newNoteId(); |  | ||||||
|         const noteTreeId = utils.newNoteRevisionId(); |  | ||||||
|  |  | ||||||
|         const now = utils.nowDate(); |  | ||||||
|  |  | ||||||
|         await sql.insert('note_tree', { |  | ||||||
|             noteTreeId: noteTreeId, |  | ||||||
|             noteId: noteId, |  | ||||||
|             parentNoteId: parentNoteId, |  | ||||||
|             notePosition: notePos, |  | ||||||
|             isExpanded: 0, |  | ||||||
|             isDeleted: 0, |  | ||||||
|             dateModified: now |  | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         await sync_table.addNoteTreeSync(noteTreeId); |         if (file.children.length > 0) { | ||||||
|  |             await importNotes(file.children, noteId, sourceId); | ||||||
|         await sql.insert('notes', { |  | ||||||
|             noteId: noteId, |  | ||||||
|             title: noteTitle, |  | ||||||
|             content: noteText, |  | ||||||
|             isDeleted: 0, |  | ||||||
|             isProtected: 0, |  | ||||||
|             type: 'text', |  | ||||||
|             mime: 'text/html', |  | ||||||
|             dateCreated: now, |  | ||||||
|             dateModified: now |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         await sync_table.addNoteSync(noteId); |  | ||||||
|  |  | ||||||
|         const noteDir = dir + '/' + fileNameWithoutExt; |  | ||||||
|  |  | ||||||
|         if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) { |  | ||||||
|             await importNotes(noteDir, noteId); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,6 +17,12 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
|     }); |     }); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | router.post('/job', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     await script.setJob(req.body); | ||||||
|  |  | ||||||
|  |     res.send({}); | ||||||
|  | })); | ||||||
|  |  | ||||||
| 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 noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup"); | ||||||
|     const repository = new Repository(req); |     const repository = new Repository(req); | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ const sql = require('../../services/sql'); | |||||||
| const options = require('../../services/options'); | const options = require('../../services/options'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
|  | const config = require('../../services/config'); | ||||||
| const protected_session = require('../../services/protected_session'); | const protected_session = require('../../services/protected_session'); | ||||||
| const sync_table = require('../../services/sync_table'); | const sync_table = require('../../services/sync_table'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
| @@ -41,6 +42,7 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
|         AND notes.isDeleted = 0`); |         AND notes.isDeleted = 0`); | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ | ||||||
|  |         instanceName: config.General ? config.General.instanceName : null, | ||||||
|         notes: notes, |         notes: notes, | ||||||
|         hiddenInAutocomplete: hiddenInAutocomplete, |         hiddenInAutocomplete: hiddenInAutocomplete, | ||||||
|         start_note_path: await options.getOption('start_note_path') |         start_note_path: await options.getOption('start_note_path') | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -9,7 +9,8 @@ const BUILTIN_ATTRIBUTES = [ | |||||||
|     'run_on_startup', |     'run_on_startup', | ||||||
|     'disable_versioning', |     'disable_versioning', | ||||||
|     'calendar_root', |     'calendar_root', | ||||||
|     'hide_in_autocomplete' |     'hide_in_autocomplete', | ||||||
|  |     'exclude_from_export' | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| async function getNoteAttributeMap(noteId) { | async function getNoteAttributeMap(noteId) { | ||||||
|   | |||||||
| @@ -20,6 +20,5 @@ module.exports = { | |||||||
|     DOCUMENT_PATH, |     DOCUMENT_PATH, | ||||||
|     BACKUP_DIR, |     BACKUP_DIR, | ||||||
|     LOG_DIR, |     LOG_DIR, | ||||||
|     EXPORT_DIR, |  | ||||||
|     ANONYMIZED_DB_DIR |     ANONYMIZED_DB_DIR | ||||||
| }; | }; | ||||||
| @@ -88,7 +88,7 @@ function noteTitleIv(iv) { | |||||||
|     return "0" + iv; |     return "0" + iv; | ||||||
| } | } | ||||||
|  |  | ||||||
| function noteTextIv(iv) { | function noteContentIv(iv) { | ||||||
|     return "1" + iv; |     return "1" + iv; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -97,5 +97,5 @@ module.exports = { | |||||||
|     decrypt, |     decrypt, | ||||||
|     decryptString, |     decryptString, | ||||||
|     noteTitleIv, |     noteTitleIv, | ||||||
|     noteTextIv |     noteContentIv | ||||||
| }; | }; | ||||||
| @@ -29,7 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) { | |||||||
|                                     AND note_tree.isDeleted = 0`, [parentNoteId]); |                                     AND note_tree.isDeleted = 0`, [parentNoteId]); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getRootNoteId() { | async function getRootCalendarNoteId() { | ||||||
|     let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)  |     let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)  | ||||||
|               WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`); |               WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`); | ||||||
|  |  | ||||||
| @@ -91,7 +91,7 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) { | |||||||
|  |  | ||||||
| async function getDateNoteId(dateTimeStr, rootNoteId = null) { | async function getDateNoteId(dateTimeStr, rootNoteId = null) { | ||||||
|     if (!rootNoteId) { |     if (!rootNoteId) { | ||||||
|         rootNoteId = await getRootNoteId(); |         rootNoteId = await getRootCalendarNoteId(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const dateStr = dateTimeStr.substr(0, 10); |     const dateStr = dateTimeStr.substr(0, 10); | ||||||
| @@ -119,7 +119,7 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) { | |||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     getRootNoteId, |     getRootCalendarNoteId, | ||||||
|     getYearNoteId, |     getYearNoteId, | ||||||
|     getMonthNoteId, |     getMonthNoteId, | ||||||
|     getDateNoteId |     getDateNoteId | ||||||
|   | |||||||
| @@ -83,6 +83,40 @@ async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function createNote(parentNoteId, title, content = "", extraOptions = {}) { | ||||||
|  |     if (!parentNoteId) throw new Error("Empty parentNoteId"); | ||||||
|  |     if (!title) throw new Error("Empty title"); | ||||||
|  |  | ||||||
|  |     const note = { | ||||||
|  |         title: title, | ||||||
|  |         content: extraOptions.json ? JSON.stringify(content, null, '\t') : content, | ||||||
|  |         target: 'into', | ||||||
|  |         isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false, | ||||||
|  |         type: extraOptions.type, | ||||||
|  |         mime: extraOptions.mime | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (extraOptions.json) { | ||||||
|  |         note.type = "code"; | ||||||
|  |         note.mime = "application/json"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!note.type) { | ||||||
|  |         note.type = "text"; | ||||||
|  |         note.mime = "text/html"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const {noteId} = await createNewNote(parentNoteId, note, extraOptions.dataKey, extraOptions.sourceId); | ||||||
|  |  | ||||||
|  |     if (extraOptions.attributes) { | ||||||
|  |         for (const attrName in extraOptions.attributes) { | ||||||
|  |             await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return noteId; | ||||||
|  | } | ||||||
|  |  | ||||||
| async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { | async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { | ||||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
| @@ -148,10 +182,14 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) { | |||||||
| async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { | async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { | ||||||
|     const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |     const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
|  |     if (oldNote.type === 'file') { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (oldNote.isProtected) { |     if (oldNote.isProtected) { | ||||||
|         protected_session.decryptNote(dataKey, oldNote); |         protected_session.decryptNote(dataKey, oldNote); | ||||||
|  |  | ||||||
|         note.isProtected = false; |         oldNote.isProtected = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const newNoteRevisionId = utils.newNoteRevisionId(); |     const newNoteRevisionId = utils.newNoteRevisionId(); | ||||||
| @@ -217,7 +255,21 @@ async function saveNoteImages(noteId, noteText, sourceId) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function loadFile(noteId, newNote, dataKey) { | ||||||
|  |     const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
|  |     if (oldNote.isProtected) { | ||||||
|  |         await protected_session.decryptNote(dataKey, oldNote); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     newNote.detail.content = oldNote.content; | ||||||
|  | } | ||||||
|  |  | ||||||
| async function updateNote(noteId, newNote, dataKey, sourceId) { | async function updateNote(noteId, newNote, dataKey, sourceId) { | ||||||
|  |     if (newNote.detail.type === 'file') { | ||||||
|  |         await loadFile(noteId, newNote, dataKey); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (newNote.detail.isProtected) { |     if (newNote.detail.isProtected) { | ||||||
|         await protected_session.encryptNote(dataKey, newNote.detail); |         await protected_session.encryptNote(dataKey, newNote.detail); | ||||||
|     } |     } | ||||||
| @@ -289,6 +341,7 @@ async function deleteNote(noteTreeId, sourceId) { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     createNewNote, |     createNewNote, | ||||||
|  |     createNote, | ||||||
|     updateNote, |     updateNote, | ||||||
|     deleteNote, |     deleteNote, | ||||||
|     protectNoteRecursively |     protectNoteRecursively | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ function getDataKey(obj) { | |||||||
|  |  | ||||||
|     const protectedSessionId = getProtectedSessionId(obj); |     const protectedSessionId = getProtectedSessionId(obj); | ||||||
|  |  | ||||||
|  |     return getDataKeyForProtectedSessionId(protectedSessionId); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getDataKeyForProtectedSessionId(protectedSessionId) { | ||||||
|     if (protectedSessionId && session.protectedSessionId === protectedSessionId) { |     if (protectedSessionId && session.protectedSessionId === protectedSessionId) { | ||||||
|         return session.decryptedDataKey; |         return session.decryptedDataKey; | ||||||
|     } |     } | ||||||
| @@ -52,7 +56,14 @@ function decryptNote(dataKey, note) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (note.content) { |     if (note.content) { | ||||||
|         note.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(note.noteId), note.content); |         const contentIv = data_encryption.noteContentIv(note.noteId); | ||||||
|  |  | ||||||
|  |         if (note.type === 'file') { | ||||||
|  |             note.content = data_encryption.decrypt(dataKey, contentIv, note.content); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             note.content = data_encryption.decryptString(dataKey, contentIv, note.content); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -76,7 +87,7 @@ function decryptNoteHistoryRow(dataKey, hist) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (hist.content) { |     if (hist.content) { | ||||||
|         hist.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(hist.noteRevisionId), hist.content); |         hist.content = data_encryption.decryptString(dataKey, data_encryption.noteContentIv(hist.noteRevisionId), hist.content); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -92,19 +103,20 @@ function encryptNote(dataKey, note) { | |||||||
|     dataKey = getDataKey(dataKey); |     dataKey = getDataKey(dataKey); | ||||||
|  |  | ||||||
|     note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title); |     note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title); | ||||||
|     note.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(note.noteId), note.content); |     note.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(note.noteId), note.content); | ||||||
| } | } | ||||||
|  |  | ||||||
| function encryptNoteHistoryRow(dataKey, history) { | function encryptNoteHistoryRow(dataKey, history) { | ||||||
|     dataKey = getDataKey(dataKey); |     dataKey = getDataKey(dataKey); | ||||||
|  |  | ||||||
|     history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title); |     history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title); | ||||||
|     history.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(history.noteRevisionId), history.content); |     history.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(history.noteRevisionId), history.content); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     setDataKey, |     setDataKey, | ||||||
|     getDataKey, |     getDataKey, | ||||||
|  |     getDataKeyForProtectedSessionId, | ||||||
|     isProtectedSessionAvailable, |     isProtectedSessionAvailable, | ||||||
|     decryptNote, |     decryptNote, | ||||||
|     decryptNotes, |     decryptNotes, | ||||||
|   | |||||||
| @@ -1,24 +1,67 @@ | |||||||
| const log = require('./log'); |  | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const ScriptContext = require('./script_context'); | const ScriptContext = require('./script_context'); | ||||||
|  |  | ||||||
| async function executeScript(dataKey, script, params) { | async function executeScript(dataKey, script, params) { | ||||||
|     log.info('Executing script: ' + script); |  | ||||||
|  |  | ||||||
|     const ctx = new ScriptContext(dataKey); |     const ctx = new ScriptContext(dataKey); | ||||||
|  |  | ||||||
|     const paramsStr = getParams(params); |     const paramsStr = getParams(params); | ||||||
|  |  | ||||||
|     let ret; |     return await sql.doInTransaction(async () => execute(ctx, script, paramsStr)); | ||||||
|  | } | ||||||
|  |  | ||||||
|     await sql.doInTransaction(async () => { | async function execute(ctx, script, paramsStr) { | ||||||
|         ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx)); |     return await (function() { return eval(`const api = this; (${script})(${paramsStr})`); }.call(ctx)); | ||||||
|     }); | } | ||||||
|  |  | ||||||
|     return ret; | 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) { | ||||||
|  |     if (!params) { | ||||||
|  |         return params; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return params.map(p => { |     return params.map(p => { | ||||||
|         if (typeof p === "string" && p.startsWith("!@#Function: ")) { |         if (typeof p === "string" && p.startsWith("!@#Function: ")) { | ||||||
|             return p.substr(13); |             return p.substr(13); | ||||||
| @@ -30,5 +73,6 @@ function getParams(params) { | |||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     executeScript |     executeScript, | ||||||
|  |     setJob | ||||||
| }; | }; | ||||||
| @@ -1,20 +1,33 @@ | |||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
| const protected_session = require('./protected_session'); | const protected_session = require('./protected_session'); | ||||||
| const notes = require('./notes'); | const notes = require('./notes'); | ||||||
|  | const sql = require('./sql'); | ||||||
|  | const utils = require('./utils'); | ||||||
| const attributes = require('./attributes'); | const attributes = require('./attributes'); | ||||||
| const date_notes = require('./date_notes'); | const date_notes = require('./date_notes'); | ||||||
|  | const config = require('./config'); | ||||||
| const Repository = require('./repository'); | const Repository = require('./repository'); | ||||||
|  | const axios = require('axios'); | ||||||
|  |  | ||||||
| function ScriptContext(noteId, dataKey) { | function ScriptContext(dataKey) { | ||||||
|     this.dataKey = protected_session.getDataKey(dataKey); |     dataKey = protected_session.getDataKey(dataKey); | ||||||
|     this.repository = new Repository(dataKey); |     const repository = new Repository(dataKey); | ||||||
|  |  | ||||||
|  |     this.axios = axios; | ||||||
|  |  | ||||||
|  |     this.utils = { | ||||||
|  |         unescapeHtml: utils.unescapeHtml, | ||||||
|  |         isoDateTimeStr: utils.dateStr | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.getInstanceName = () => config.General ? config.General.instanceName : null; | ||||||
|  |  | ||||||
|     this.getNoteById = async function(noteId) { |     this.getNoteById = async function(noteId) { | ||||||
|         return this.repository.getNote(noteId); |         return repository.getNote(noteId); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.getNotesWithAttribute = async function (attrName, attrValue) { |     this.getNotesWithAttribute = async function (attrName, attrValue) { | ||||||
|         return await attributes.getNotesWithAttribute(this.dataKey, attrName, attrValue); |         return await attributes.getNotesWithAttribute(dataKey, attrName, attrValue); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.getNoteWithAttribute = async function (attrName, attrValue) { |     this.getNoteWithAttribute = async function (attrName, attrValue) { | ||||||
| @@ -23,46 +36,22 @@ function ScriptContext(noteId, dataKey) { | |||||||
|         return notes.length > 0 ? notes[0] : null; |         return notes.length > 0 ? notes[0] : null; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.createNote = async function (parentNoteId, name, jsonContent, extraOptions = {}) { |     this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) { | ||||||
|         const note = { |         extraOptions.dataKey = dataKey; | ||||||
|             title: name, |  | ||||||
|             content: extraOptions.json ? JSON.stringify(jsonContent, null, '\t') : jsonContent, |  | ||||||
|             target: 'into', |  | ||||||
|             isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false, |  | ||||||
|             type: extraOptions.type, |  | ||||||
|             mime: extraOptions.mime |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (extraOptions.json) { |         return await notes.createNote(parentNoteId, title, content, extraOptions); | ||||||
|             note.type = "code"; |  | ||||||
|             note.mime = "application/json"; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!note.type) { |  | ||||||
|             note.type = "text"; |  | ||||||
|             note.mime = "text/html"; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const noteId = (await notes.createNewNote(parentNoteId, note, this.dataKey)).noteId; |  | ||||||
|  |  | ||||||
|         if (extraOptions.attributes) { |  | ||||||
|             for (const attrName in extraOptions.attributes) { |  | ||||||
|                 await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return noteId; |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.createAttribute = attributes.createAttribute; |     this.createAttribute = attributes.createAttribute; | ||||||
|  |  | ||||||
|     this.updateEntity = this.repository.updateEntity; |     this.updateEntity = repository.updateEntity; | ||||||
|  |  | ||||||
|     this.log = function(message) { |     this.log = message => log.info(`Script: ${message}`); | ||||||
|         log.info(`Script: ${message}`); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|  |     this.getRootCalendarNoteId = date_notes.getRootCalendarNoteId; | ||||||
|     this.getDateNoteId = date_notes.getDateNoteId; |     this.getDateNoteId = date_notes.getDateNoteId; | ||||||
|  |  | ||||||
|  |     this.transaction = sql.doInTransaction; | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = ScriptContext; | module.exports = ScriptContext; | ||||||
| @@ -195,6 +195,7 @@ async function doInTransaction(func) { | |||||||
|         await transactionPromise; |         await transactionPromise; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     let ret = null; | ||||||
|     const error = new Error(); // to capture correct stack trace in case of exception |     const error = new Error(); // to capture correct stack trace in case of exception | ||||||
|  |  | ||||||
|     transactionActive = true; |     transactionActive = true; | ||||||
| @@ -202,7 +203,7 @@ async function doInTransaction(func) { | |||||||
|         try { |         try { | ||||||
|             await beginTransaction(); |             await beginTransaction(); | ||||||
|  |  | ||||||
|             await func(); |             ret = await func(); | ||||||
|  |  | ||||||
|             await commit(); |             await commit(); | ||||||
|  |  | ||||||
| @@ -223,6 +224,8 @@ async function doInTransaction(func) { | |||||||
|     if (transactionActive) { |     if (transactionActive) { | ||||||
|         await transactionPromise; |         await transactionPromise; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function isDbUpToDate() { | async function isDbUpToDate() { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| const crypto = require('crypto'); | const crypto = require('crypto'); | ||||||
| const randtoken = require('rand-token').generator({source: 'crypto'}); | const randtoken = require('rand-token').generator({source: 'crypto'}); | ||||||
|  | const unescape = require('unescape'); | ||||||
|  |  | ||||||
| function newNoteId() { | function newNoteId() { | ||||||
|     return randomString(12); |     return randomString(12); | ||||||
| @@ -129,6 +130,10 @@ async function stopWatch(what, func) { | |||||||
|     return ret; |     return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function unescapeHtml(str) { | ||||||
|  |     return unescape(str); | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     randomSecureToken, |     randomSecureToken, | ||||||
|     randomString, |     randomString, | ||||||
| @@ -153,5 +158,6 @@ module.exports = { | |||||||
|     getDateTimeForFile, |     getDateTimeForFile, | ||||||
|     sanitizeSql, |     sanitizeSql, | ||||||
|     assertArguments, |     assertArguments, | ||||||
|     stopWatch |     stopWatch, | ||||||
|  |     unescapeHtml | ||||||
| }; | }; | ||||||
| @@ -56,6 +56,8 @@ | |||||||
|             <img src="images/icons/search.png" alt="Search in notes"/> |             <img src="images/icons/search.png" alt="Search in notes"/> | ||||||
|           </a> |           </a> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|  |         <input type="file" id="import-upload" style="display: none" /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> |       <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> | ||||||
| @@ -167,7 +169,7 @@ | |||||||
|           </table> |           </table> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <input type="file" id="file-upload" style="display: none" /> |         <input type="file" id="attachment-upload" style="display: none" /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div id="attribute-list"> |       <div id="attribute-list"> | ||||||
| @@ -497,6 +499,7 @@ | |||||||
|     <script src="javascripts/drag_and_drop.js"></script> |     <script src="javascripts/drag_and_drop.js"></script> | ||||||
|     <script src="javascripts/context_menu.js"></script> |     <script src="javascripts/context_menu.js"></script> | ||||||
|     <script src="javascripts/search_tree.js"></script> |     <script src="javascripts/search_tree.js"></script> | ||||||
|  |     <script src="javascripts/export.js"></script> | ||||||
|  |  | ||||||
|     <!-- Note detail --> |     <!-- Note detail --> | ||||||
|     <script src="javascripts/note_editor.js"></script> |     <script src="javascripts/note_editor.js"></script> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user