mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 04:16:17 +01:00 
			
		
		
		
	Compare commits
	
		
			56 Commits
		
	
	
		
			v0.7.0-bet
			...
			v0.9.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					840af15dae | ||
| 
						 | 
					0fdb6af98a | ||
| 
						 | 
					f6c7f6a0f2 | ||
| 
						 | 
					354999f37a | ||
| 
						 | 
					348c622845 | ||
| 
						 | 
					44bcdedaba | ||
| 
						 | 
					755c0f3ce2 | ||
| 
						 | 
					895bda41b5 | ||
| 
						 | 
					b2df622cb6 | ||
| 
						 | 
					9ba6e6d0f5 | ||
| 
						 | 
					a5c9180533 | ||
| 
						 | 
					e86f1e0d05 | ||
| 
						 | 
					b6277049f3 | ||
| 
						 | 
					c831221cc4 | ||
| 
						 | 
					577a168714 | ||
| 
						 | 
					b0bd27321a | ||
| 
						 | 
					90c5348ca7 | ||
| 
						 | 
					8e95b080da | ||
| 
						 | 
					766a567a32 | ||
| 
						 | 
					6d0218cb36 | ||
| 
						 | 
					d26170762b | ||
| 
						 | 
					b3209a9bbf | ||
| 
						 | 
					61c2456cf6 | ||
| 
						 | 
					1c6fc9029f | ||
| 
						 | 
					5c91e38dfe | ||
| 
						 | 
					07bf075894 | ||
| 
						 | 
					ddce5c959e | ||
| 
						 | 
					3b9d1df05c | ||
| 
						 | 
					d239ef2956 | ||
| 
						 | 
					7a865a9081 | ||
| 
						 | 
					83d6c2970f | ||
| 
						 | 
					8c7d159012 | ||
| 
						 | 
					d169f67901 | ||
| 
						 | 
					982b723647 | ||
| 
						 | 
					31d5ac05ff | ||
| 
						 | 
					72d91d1571 | ||
| 
						 | 
					f4b57f4c57 | ||
| 
						 | 
					ee0833390a | ||
| 
						 | 
					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).
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript';
 | 
				
			||||||
							
								
								
									
										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.9.2",
 | 
				
			||||||
  "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,7 +73,7 @@ require('./services/backup');
 | 
				
			|||||||
// trigger consistency checks timer
 | 
					// trigger consistency checks timer
 | 
				
			||||||
require('./services/consistency_checks');
 | 
					require('./services/consistency_checks');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require('./plugins/reddit');
 | 
					require('./services/scheduler');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    app,
 | 
					    app,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,13 +24,52 @@ class Note extends Entity {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isJavaScript() {
 | 
					    isJavaScript() {
 | 
				
			||||||
        return this.type === "code" && this.mime === "application/javascript";
 | 
					        return (this.type === "code" || this.type === "file")
 | 
				
			||||||
 | 
					            && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isHtml() {
 | 
				
			||||||
 | 
					        return (this.type === "code" || this.type === "file") && this.mime === "text/html";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getScriptEnv() {
 | 
				
			||||||
 | 
					        if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
 | 
				
			||||||
 | 
					            return "frontend";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.type === 'render') {
 | 
				
			||||||
 | 
					            return "frontend";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
 | 
				
			||||||
 | 
					            return "backend";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getAttributes() {
 | 
					    async getAttributes() {
 | 
				
			||||||
        return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
 | 
					        return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // WARNING: this doesn't take into account the possibility to have multi-valued attributes!
 | 
				
			||||||
 | 
					    async getAttributeMap() {
 | 
				
			||||||
 | 
					        const map = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const attr of await this.getAttributes()) {
 | 
				
			||||||
 | 
					            map[attr.name] = attr.value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return map;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async hasAttribute(name) {
 | 
				
			||||||
 | 
					        const map = await this.getAttributeMap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return map.hasOwnProperty(name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // WARNING: this doesn't take into account the possibility to have multi-valued attributes!
 | 
				
			||||||
    async getAttribute(name) {
 | 
					    async getAttribute(name) {
 | 
				
			||||||
        return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
 | 
					        return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -43,6 +82,49 @@ 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 = ?
 | 
				
			||||||
 | 
					          ORDER BY note_tree.notePosition`, [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  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 252 B  | 
@@ -1,4 +1,26 @@
 | 
				
			|||||||
const api = (function() {
 | 
					function ScriptContext(startNote, allNotes) {
 | 
				
			||||||
 | 
					    const modules = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        modules: modules,
 | 
				
			||||||
 | 
					        notes: toObject(allNotes, note => [note.noteId, note]),
 | 
				
			||||||
 | 
					        apis: toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
 | 
				
			||||||
 | 
					        require: moduleNoteIds => {
 | 
				
			||||||
 | 
					            return moduleName => {
 | 
				
			||||||
 | 
					                const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
 | 
				
			||||||
 | 
					                const note = candidates.find(c => c.title === moduleName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!note) {
 | 
				
			||||||
 | 
					                    throw new Error("Could not find module note " + moduleName);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return modules[note.noteId].exports;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ScriptApi(startNote, currentNote) {
 | 
				
			||||||
    const $pluginButtons = $("#plugin-buttons");
 | 
					    const $pluginButtons = $("#plugin-buttons");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function activateNote(notePath) {
 | 
					    async function activateNote(notePath) {
 | 
				
			||||||
@@ -13,9 +35,42 @@ const api = (function() {
 | 
				
			|||||||
        $pluginButtons.append(button);
 | 
					        $pluginButtons.append(button);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function prepareParams(params) {
 | 
				
			||||||
 | 
					        if (!params) {
 | 
				
			||||||
 | 
					            return params;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return params.map(p => {
 | 
				
			||||||
 | 
					            if (typeof p === "function") {
 | 
				
			||||||
 | 
					                return "!@#Function: " + p.toString();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else {
 | 
				
			||||||
 | 
					                return p;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function runOnServer(script, params = []) {
 | 
				
			||||||
 | 
					        if (typeof script === "function") {
 | 
				
			||||||
 | 
					            script = script.toString();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const ret = await server.post('script/exec', {
 | 
				
			||||||
 | 
					            script: script,
 | 
				
			||||||
 | 
					            params: prepareParams(params),
 | 
				
			||||||
 | 
					            startNoteId: startNote.noteId,
 | 
				
			||||||
 | 
					            currentNoteId: currentNote.noteId
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ret.executionResult;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					        startNote: startNote,
 | 
				
			||||||
 | 
					        currentNote: currentNote,
 | 
				
			||||||
        addButtonToToolbar,
 | 
					        addButtonToToolbar,
 | 
				
			||||||
        activateNote
 | 
					        activateNote,
 | 
				
			||||||
 | 
					        getInstanceName: noteTree.getInstanceName,
 | 
				
			||||||
 | 
					        runOnServer
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
})();
 | 
					}
 | 
				
			||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,9 @@ const sqlConsole = (function() {
 | 
				
			|||||||
            CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
 | 
					            CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
 | 
				
			||||||
            CodeMirror.keyMap.default["Tab"] = "indentMore";
 | 
					            CodeMirror.keyMap.default["Tab"] = "indentMore";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
 | 
				
			||||||
 | 
					            delete CodeMirror.keyMap.basic["Esc"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
 | 
					            CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            codeEditor = CodeMirror($query[0], {
 | 
					            codeEditor = CodeMirror($query[0], {
 | 
				
			||||||
@@ -45,7 +48,11 @@ const sqlConsole = (function() {
 | 
				
			|||||||
        codeEditor.focus();
 | 
					        codeEditor.focus();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function execute() {
 | 
					    async function execute(e) {
 | 
				
			||||||
 | 
					        // stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const sqlQuery = codeEditor.getValue();
 | 
					        const sqlQuery = codeEditor.getValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const result = await server.post("sql/execute", {
 | 
					        const result = await server.post("sql/execute", {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function exportSubTree(noteId) {
 | 
				
			||||||
 | 
					    const url = getHost() + "/api/export/" + noteId + "?protectedSessionId="
 | 
				
			||||||
 | 
					        + encodeURIComponent(protected_session.getProtectedSessionId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,24 +114,34 @@ $.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;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
 | 
					    console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -191,9 +201,9 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
				
			|||||||
$("#logout-button").toggle(!isElectron());
 | 
					$("#logout-button").toggle(!isElectron());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$(document).ready(() => {
 | 
					$(document).ready(() => {
 | 
				
			||||||
    server.get("script/startup").then(scripts => {
 | 
					    server.get("script/startup").then(scriptBundles => {
 | 
				
			||||||
        for (const script of scripts) {
 | 
					        for (const bundle of scriptBundles) {
 | 
				
			||||||
            executeScript(script);
 | 
					            executeBundle(bundle);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,13 +77,15 @@ const noteEditor = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    function updateNoteFromInputs(note) {
 | 
					    function updateNoteFromInputs(note) {
 | 
				
			||||||
        if (note.detail.type === 'text') {
 | 
					        if (note.detail.type === 'text') {
 | 
				
			||||||
            note.detail.content = editor.getData();
 | 
					            let content = editor.getData();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // if content is only tags/whitespace (typically <p> </p>), then just make it empty
 | 
					            // if content is only tags/whitespace (typically <p> </p>), then just make it empty
 | 
				
			||||||
            // this is important when setting new note to code
 | 
					            // this is important when setting new note to code
 | 
				
			||||||
            if (jQuery(note.detail.content).text().trim() === '') {
 | 
					            if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
 | 
				
			||||||
                note.detail.content = ''
 | 
					                content = '';
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            note.detail.content = content;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (note.detail.type === 'code') {
 | 
					        else if (note.detail.type === 'code') {
 | 
				
			||||||
            note.detail.content = codeEditor.getValue();
 | 
					            note.detail.content = codeEditor.getValue();
 | 
				
			||||||
@@ -154,7 +156,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 +176,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();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -212,9 +219,11 @@ const noteEditor = (function() {
 | 
				
			|||||||
        if (currentNote.detail.type === 'render') {
 | 
					        if (currentNote.detail.type === 'render') {
 | 
				
			||||||
            $noteDetailRender.show();
 | 
					            $noteDetailRender.show();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const subTree = await server.get('script/subtree/' + getCurrentNoteId());
 | 
					            const bundle = await server.get('script/bundle/' + getCurrentNoteId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $noteDetailRender.html(subTree);
 | 
					            $noteDetailRender.html(bundle.html);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            executeBundle(bundle);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (currentNote.detail.type === 'file') {
 | 
					        else if (currentNote.detail.type === 'file') {
 | 
				
			||||||
            $noteDetailAttachment.show();
 | 
					            $noteDetailAttachment.show();
 | 
				
			||||||
@@ -293,22 +302,21 @@ const noteEditor = (function() {
 | 
				
			|||||||
            // make sure note is saved so we load latest changes
 | 
					            // make sure note is saved so we load latest changes
 | 
				
			||||||
            await saveNoteIfChanged();
 | 
					            await saveNoteIfChanged();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const script = await server.get('script/subtree/' + getCurrentNoteId());
 | 
					            if (currentNote.detail.mime.endsWith("env=frontend")) {
 | 
				
			||||||
 | 
					                const bundle = await server.get('script/bundle/' + getCurrentNoteId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            executeScript(script);
 | 
					                executeBundle(bundle);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (currentNote.detail.mime.endsWith("env=backend")) {
 | 
				
			||||||
 | 
					                await server.post('script/run/' + getCurrentNoteId());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            showMessage("Note executed");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $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 +331,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,12 @@ const noteTree = (function() {
 | 
				
			|||||||
        if (note.type === 'code') {
 | 
					        if (note.type === 'code') {
 | 
				
			||||||
            extraClasses.push("code");
 | 
					            extraClasses.push("code");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        else if (note.type === 'render') {
 | 
				
			||||||
 | 
					            extraClasses.push('render');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (note.type === 'file') {
 | 
				
			||||||
 | 
					            extraClasses.push('attachment');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return extraClasses.join(" ");
 | 
					        return extraClasses.join(" ");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -645,6 +653,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 +719,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 +832,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 +910,7 @@ const noteTree = (function() {
 | 
				
			|||||||
        setParentChildRelation,
 | 
					        setParentChildRelation,
 | 
				
			||||||
        getSelectedNodes,
 | 
					        getSelectedNodes,
 | 
				
			||||||
        sortAlphabetically,
 | 
					        sortAlphabetically,
 | 
				
			||||||
        noteExists
 | 
					        noteExists,
 | 
				
			||||||
 | 
					        getInstanceName
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
@@ -25,7 +25,8 @@ const noteType = (function() {
 | 
				
			|||||||
            { mime: 'text/html', title: 'HTML' },
 | 
					            { mime: 'text/html', title: 'HTML' },
 | 
				
			||||||
            { mime: 'message/http', title: 'HTTP' },
 | 
					            { mime: 'message/http', title: 'HTTP' },
 | 
				
			||||||
            { mime: 'text/x-java', title: 'Java' },
 | 
					            { mime: 'text/x-java', title: 'Java' },
 | 
				
			||||||
            { mime: 'application/javascript', title: 'JavaScript' },
 | 
					            { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
 | 
				
			||||||
 | 
					            { mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
 | 
				
			||||||
            { mime: 'application/json', title: 'JSON' },
 | 
					            { mime: 'application/json', title: 'JSON' },
 | 
				
			||||||
            { mime: 'text/x-kotlin', title: 'Kotlin' },
 | 
					            { mime: 'text/x-kotlin', title: 'Kotlin' },
 | 
				
			||||||
            { mime: 'text/x-lua', title: 'Lua' },
 | 
					            { mime: 'text/x-lua', title: 'Lua' },
 | 
				
			||||||
@@ -121,7 +122,7 @@ const noteType = (function() {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.updateExecuteScriptButtonVisibility = function() {
 | 
					        this.updateExecuteScriptButtonVisibility = function() {
 | 
				
			||||||
            $executeScriptButton.toggle(self.mime() === 'application/javascript');
 | 
					            $executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,9 @@ const protected_session = (function() {
 | 
				
			|||||||
        if (requireProtectedSession && !isProtectedSessionAvailable()) {
 | 
					        if (requireProtectedSession && !isProtectedSessionAvailable()) {
 | 
				
			||||||
            protectedSessionDeferred = dfd;
 | 
					            protectedSessionDeferred = dfd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (noteTree.getCurrentNode().data.isProtected) {
 | 
				
			||||||
                $noteDetailWrapper.hide();
 | 
					                $noteDetailWrapper.hide();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $dialog.dialog({
 | 
					            $dialog.dialog({
 | 
				
			||||||
                modal: modal,
 | 
					                modal: modal,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,27 +31,6 @@ const server = (function() {
 | 
				
			|||||||
        return await call('DELETE', url);
 | 
					        return await call('DELETE', url);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function prepareParams(params) {
 | 
					 | 
				
			||||||
        return params.map(p => {
 | 
					 | 
				
			||||||
            if (typeof p === "function") {
 | 
					 | 
				
			||||||
                return "!@#Function: " + p.toString();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                return p;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function exec(params, script) {
 | 
					 | 
				
			||||||
        if (typeof script === "function") {
 | 
					 | 
				
			||||||
            script = script.toString();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const ret = await post('script/exec', { script: script, params: prepareParams(params) });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return ret.executionResult;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let i = 1;
 | 
					    let i = 1;
 | 
				
			||||||
    const reqResolves = {};
 | 
					    const reqResolves = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -115,7 +94,6 @@ const server = (function() {
 | 
				
			|||||||
        post,
 | 
					        post,
 | 
				
			||||||
        put,
 | 
					        put,
 | 
				
			||||||
        remove,
 | 
					        remove,
 | 
				
			||||||
        exec,
 | 
					 | 
				
			||||||
        ajax,
 | 
					        ajax,
 | 
				
			||||||
        // don't remove, used from CKEditor image upload!
 | 
					        // don't remove, used from CKEditor image upload!
 | 
				
			||||||
        getHeaders
 | 
					        getHeaders
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -115,8 +115,10 @@ async function stopWatch(what, func) {
 | 
				
			|||||||
    return ret;
 | 
					    return ret;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function executeScript(script) {
 | 
					async function executeBundle(bundle) {
 | 
				
			||||||
    eval(script);
 | 
					    const apiContext = ScriptContext(bundle.note, bundle.allNotes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return await (function() { return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }.call(apiContext));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function formatValueWithWhitespace(val) {
 | 
					function formatValueWithWhitespace(val) {
 | 
				
			||||||
@@ -143,13 +145,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 +169,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 +192,31 @@ 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toObject(array, fn) {
 | 
				
			||||||
 | 
					    const obj = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const item of array) {
 | 
				
			||||||
 | 
					        const ret = fn(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        obj[ret[0]] = ret[1];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return obj;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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]);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										92
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					// 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) {
 | 
				
			||||||
 | 
					        if (noteEditor.getCurrentNote().detail.mime === 'application/json') {
 | 
				
			||||||
 | 
					            // eslint doesn't seem to validate pure JSON well
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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
											
										
									
								
							@@ -846,6 +846,8 @@ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
 | 
				
			|||||||
CodeMirror.defineMIME("text/javascript", "javascript");
 | 
					CodeMirror.defineMIME("text/javascript", "javascript");
 | 
				
			||||||
CodeMirror.defineMIME("text/ecmascript", "javascript");
 | 
					CodeMirror.defineMIME("text/ecmascript", "javascript");
 | 
				
			||||||
CodeMirror.defineMIME("application/javascript", "javascript");
 | 
					CodeMirror.defineMIME("application/javascript", "javascript");
 | 
				
			||||||
 | 
					CodeMirror.defineMIME("application/javascript;env=frontend", "javascript");
 | 
				
			||||||
 | 
					CodeMirror.defineMIME("application/javascript;env=backend", "javascript");
 | 
				
			||||||
CodeMirror.defineMIME("application/x-javascript", "javascript");
 | 
					CodeMirror.defineMIME("application/x-javascript", "javascript");
 | 
				
			||||||
CodeMirror.defineMIME("application/ecmascript", "javascript");
 | 
					CodeMirror.defineMIME("application/ecmascript", "javascript");
 | 
				
			||||||
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
 | 
					CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							@@ -70,7 +70,7 @@
 | 
				
			|||||||
    {name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]},
 | 
					    {name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]},
 | 
				
			||||||
    {name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
 | 
					    {name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
 | 
				
			||||||
    {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
 | 
					    {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
 | 
				
			||||||
    {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"],
 | 
					    {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/javascript;env=frontend", "application/javascript;env=backend", "application/x-javascript", "application/ecmascript"],
 | 
				
			||||||
     mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]},
 | 
					     mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]},
 | 
				
			||||||
    {name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]},
 | 
					    {name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]},
 | 
				
			||||||
    {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]},
 | 
					    {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]},
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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,16 @@ 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.render > span.fancytree-icon {
 | 
				
			||||||
 | 
					    background-position: 0 0;
 | 
				
			||||||
 | 
					    background-image: url("../images/icons/play.png");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
span.fancytree-node.protected > span.fancytree-icon {
 | 
					span.fancytree-node.protected > span.fancytree-icon {
 | 
				
			||||||
    filter: drop-shadow(2px 2px 2px black);
 | 
					    filter: drop-shadow(2px 2px 2px black);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -103,6 +113,9 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.icon-action {
 | 
					.icon-action {
 | 
				
			||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    height: 24px;
 | 
				
			||||||
 | 
					    width: 24px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#protect-button, #unprotect-button {
 | 
					#protect-button, #unprotect-button {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,79 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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 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");
 | 
				
			||||||
 | 
					const Repository = require("../../services/repository");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					    const noteId = req.params.noteId;
 | 
				
			||||||
    const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
 | 
					    const repo = new Repository(req);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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, repo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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, repo) {
 | 
				
			||||||
    const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
 | 
					    const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
 | 
				
			||||||
    const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]);
 | 
					    const note = await repo.getEntity("SELECT notes.* FROM notes WHERE noteId = ?", [noteTree.noteId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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 (metadata.attributes.find(attr => attr.name === 'exclude_from_export')) {
 | 
				
			||||||
 | 
					        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, repo);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return childFileName;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getMetadata(note) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        version: 1,
 | 
				
			||||||
 | 
					        title: note.title,
 | 
				
			||||||
 | 
					        type: note.type,
 | 
				
			||||||
 | 
					        mime: note.mime,
 | 
				
			||||||
 | 
					        attributes: (await note.getAttributes()).map(attr => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                name: attr.name,
 | 
				
			||||||
 | 
					                value: attr.value
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
@@ -2,104 +2,136 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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 attributes = require('../../services/attributes');
 | 
				
			||||||
 | 
					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.version !== 1) {
 | 
				
			||||||
    if (!parent) {
 | 
					            throw new Error("Can't read meta data version " + file.meta.version);
 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fileList = fs.readdirSync(dir);
 | 
					        if (file.meta.type !== 'file') {
 | 
				
			||||||
 | 
					            file.data = file.data.toString("UTF-8");
 | 
				
			||||||
    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,
 | 
				
			||||||
 | 
					            sourceId: sourceId
 | 
				
			||||||
        const fileNameWithoutExt = file.substr(0, file.length - 5);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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);
 | 
					        for (const attr of file.meta.attributes) {
 | 
				
			||||||
 | 
					            await attributes.createAttribute(noteId, attr.name, attr.value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await sql.insert('notes', {
 | 
					        if (file.children.length > 0) {
 | 
				
			||||||
            noteId: noteId,
 | 
					            await importNotes(file.children, noteId, sourceId);
 | 
				
			||||||
            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);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,13 +4,23 @@ const express = require('express');
 | 
				
			|||||||
const router = express.Router();
 | 
					const router = express.Router();
 | 
				
			||||||
const auth = require('../../services/auth');
 | 
					const auth = require('../../services/auth');
 | 
				
			||||||
const wrap = require('express-promise-wrap').wrap;
 | 
					const wrap = require('express-promise-wrap').wrap;
 | 
				
			||||||
const notes = require('../../services/notes');
 | 
					 | 
				
			||||||
const attributes = require('../../services/attributes');
 | 
					const attributes = require('../../services/attributes');
 | 
				
			||||||
const script = require('../../services/script');
 | 
					const script = require('../../services/script');
 | 
				
			||||||
const Repository = require('../../services/repository');
 | 
					const Repository = require('../../services/repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const ret = await script.executeScript(req, req.body.script, req.body.params);
 | 
					    const ret = await script.executeScript(req, req.body.script, req.body.params, req.body.startNoteId, req.body.currentNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send({
 | 
				
			||||||
 | 
					        executionResult: ret
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.post('/run/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
 | 
					    const repository = new Repository(req);
 | 
				
			||||||
 | 
					    const note = await repository.getNote(req.params.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const ret = await script.executeNote(req, note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send({
 | 
					    res.send({
 | 
				
			||||||
        executionResult: ret
 | 
					        executionResult: ret
 | 
				
			||||||
@@ -18,67 +28,28 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			|||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
 | 
					 | 
				
			||||||
    const repository = new Repository(req);
 | 
					    const repository = new Repository(req);
 | 
				
			||||||
 | 
					    const notes = await attributes.getNotesWithAttribute(repository, "run", "frontend_startup");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const scripts = [];
 | 
					    const scripts = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const noteId of noteIds) {
 | 
					    for (const note of notes) {
 | 
				
			||||||
        scripts.push(await getNoteWithSubtreeScript(noteId, repository));
 | 
					        const bundle = await script.getScriptBundle(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (bundle) {
 | 
				
			||||||
 | 
					            scripts.push(bundle);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send(scripts);
 | 
					    res.send(scripts);
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.get('/bundle/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const repository = new Repository(req);
 | 
					    const repository = new Repository(req);
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					    const note = await repository.getNote(req.params.noteId);
 | 
				
			||||||
 | 
					    const bundle = await script.getScriptBundle(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send(await getNoteWithSubtreeScript(noteId, repository));
 | 
					    res.send(bundle);
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteWithSubtreeScript(noteId, repository) {
 | 
					 | 
				
			||||||
    const note = await repository.getNote(noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let noteScript = note.content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (note.isJavaScript()) {
 | 
					 | 
				
			||||||
        // last \r\n is necessary if script contains line comment on its last line
 | 
					 | 
				
			||||||
        noteScript = "(async function() {" + noteScript + "\r\n})()";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository, note.isJavaScript());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return subTreeScripts + noteScript;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getSubTreeScripts(parentId, includedNoteIds, repository, isJavaScript) {
 | 
					 | 
				
			||||||
    const children = await repository.getEntities(`
 | 
					 | 
				
			||||||
                                      SELECT notes.* 
 | 
					 | 
				
			||||||
                                      FROM notes JOIN note_tree USING(noteId)
 | 
					 | 
				
			||||||
                                      WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0
 | 
					 | 
				
			||||||
                                           AND note_tree.parentNoteId = ? AND notes.type = 'code'
 | 
					 | 
				
			||||||
                                           AND (notes.mime = 'application/javascript' OR notes.mime = 'text/html')`, [parentId]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let script = "\r\n";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const child of children) {
 | 
					 | 
				
			||||||
        if (includedNoteIds.includes(child.noteId)) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        includedNoteIds.push(child.noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        script += await getSubTreeScripts(child.noteId, includedNoteIds, repository);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!isJavaScript && child.mime === 'application/javascript') {
 | 
					 | 
				
			||||||
            child.content = '<script>' + child.content + '</script>';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        script += child.content + "\r\n";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return script;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
@@ -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')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,32 @@ const router = express.Router();
 | 
				
			|||||||
const auth = require('../services/auth');
 | 
					const auth = require('../services/auth');
 | 
				
			||||||
const source_id = require('../services/source_id');
 | 
					const source_id = require('../services/source_id');
 | 
				
			||||||
const sql = require('../services/sql');
 | 
					const sql = require('../services/sql');
 | 
				
			||||||
 | 
					const Repository = require('../services/repository');
 | 
				
			||||||
 | 
					const attributes = require('../services/attributes');
 | 
				
			||||||
const wrap = require('express-promise-wrap').wrap;
 | 
					const wrap = require('express-promise-wrap').wrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('', auth.checkAuth, wrap(async (req, res, next) => {
 | 
					router.get('', auth.checkAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
 | 
					    const repository = new Repository(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.render('index', {
 | 
					    res.render('index', {
 | 
				
			||||||
        sourceId: await source_id.generateSourceId(),
 | 
					        sourceId: await source_id.generateSourceId(),
 | 
				
			||||||
        maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync")
 | 
					        maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
 | 
				
			||||||
 | 
					        appCss: await getAppCss(repository)
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getAppCss(repository) {
 | 
				
			||||||
 | 
					    let css = '';
 | 
				
			||||||
 | 
					    const notes = attributes.getNotesWithAttribute(repository, 'app_css');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const note of await notes) {
 | 
				
			||||||
 | 
					        css += `/* ${note.noteId} */
 | 
				
			||||||
 | 
					${note.content}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return css;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/scripts/Today.tar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/scripts/Today.tar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/scripts/Weight Tracker.tar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/scripts/Weight Tracker.tar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,13 +0,0 @@
 | 
				
			|||||||
api.addButtonToToolbar('go-today', $('<button class="btn btn-xs" onclick="goToday();"><span class="ui-icon ui-icon-calendar"></span> Today</button>'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
window.goToday = async function() {
 | 
					 | 
				
			||||||
    const todayDateStr = formatDateISO(new Date());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const todayNoteId = await server.exec([todayDateStr], async todayDateStr => {
 | 
					 | 
				
			||||||
        return await this.getDateNoteId(todayDateStr);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    api.activateNote(todayNoteId);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(document).bind('keydown', "alt+t", window.goToday);
 | 
					 | 
				
			||||||
@@ -1,146 +0,0 @@
 | 
				
			|||||||
<form id="weight-form" style="display: flex; width: 700px; justify-content: space-around; align-items: flex-end;">
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
        <label for="weight-date">Date</label>
 | 
					 | 
				
			||||||
        <input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
        <label for="weight">Weight</label>
 | 
					 | 
				
			||||||
        <input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
        <label for="comment">Comment</label>
 | 
					 | 
				
			||||||
        <input type="text" id="comment" class="form-control" style="width: 200px;" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <button type="submit" class="btn btn-primary">Add</button>
 | 
					 | 
				
			||||||
</form>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<br/><br/>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<canvas id="canvas"></canvas>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
    (async function() {
 | 
					 | 
				
			||||||
        const $form = $("#weight-form");
 | 
					 | 
				
			||||||
        const $date = $("#weight-date");
 | 
					 | 
				
			||||||
        const $weight = $("#weight");
 | 
					 | 
				
			||||||
        const $comment = $("#comment");
 | 
					 | 
				
			||||||
        let chart;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $date.datepicker();
 | 
					 | 
				
			||||||
        $date.datepicker('option', 'dateFormat', 'yy-mm-dd');
 | 
					 | 
				
			||||||
        $date.datepicker('setDate', new Date());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async function saveWeight() {
 | 
					 | 
				
			||||||
            await server.exec([$date.val(), parseFloat($weight.val()), $comment.val()], async (date, weight, comment) => {
 | 
					 | 
				
			||||||
                const dataNote = await this.getNoteWithAttribute('date_data', date);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (dataNote) {
 | 
					 | 
				
			||||||
                    dataNote.jsonContent.weight = weight;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (comment) {
 | 
					 | 
				
			||||||
                        dataNote.jsonContent.weight_comment = comment;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    await this.updateEntity(dataNote);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                else {
 | 
					 | 
				
			||||||
                    const parentNoteId = await this.getDateNoteId(date);
 | 
					 | 
				
			||||||
                    const jsonContent = { weight: weight };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (comment) {
 | 
					 | 
				
			||||||
                        jsonContent.weight_comment = comment;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    await this.createNote(parentNoteId, 'data', jsonContent, {
 | 
					 | 
				
			||||||
                        json: true,
 | 
					 | 
				
			||||||
                        attributes: {
 | 
					 | 
				
			||||||
                            date_data: date,
 | 
					 | 
				
			||||||
                            hide_in_autocomplete: null
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            showMessage("Weight has been saved");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            chart.data = await getData();
 | 
					 | 
				
			||||||
            chart.update();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async function drawChart() {
 | 
					 | 
				
			||||||
            const data = await getData();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const ctx = $("#canvas")[0].getContext("2d");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            chart = new Chart(ctx, {
 | 
					 | 
				
			||||||
                type: 'line',
 | 
					 | 
				
			||||||
                data: data,
 | 
					 | 
				
			||||||
                options: {
 | 
					 | 
				
			||||||
                    tooltips: {
 | 
					 | 
				
			||||||
                        enabled: true,
 | 
					 | 
				
			||||||
                        mode: 'single',
 | 
					 | 
				
			||||||
                        callbacks: {
 | 
					 | 
				
			||||||
                            label: function (tooltipItem, data) {
 | 
					 | 
				
			||||||
                                const multistringText = [tooltipItem.yLabel];
 | 
					 | 
				
			||||||
                                const comment = data.comments[tooltipItem['index']];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                if (comment) {
 | 
					 | 
				
			||||||
                                    multistringText.push(comment);
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                return multistringText;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        async function getData() {
 | 
					 | 
				
			||||||
            const data = await server.exec([], async () => {
 | 
					 | 
				
			||||||
                const notes = await this.getNotesWithAttribute('date_data');
 | 
					 | 
				
			||||||
                const data = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                for (const note of notes) {
 | 
					 | 
				
			||||||
                    const dateAttr = await note.getAttribute('date_data');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    data.push({
 | 
					 | 
				
			||||||
                        date: dateAttr.value,
 | 
					 | 
				
			||||||
                        weight: note.jsonContent.weight,
 | 
					 | 
				
			||||||
                        comment: note.jsonContent.weight_comment
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                data.sort((a, b) => a.date < b.date ? -1 : +1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return data;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const datasets = [{
 | 
					 | 
				
			||||||
                label: "Weight",
 | 
					 | 
				
			||||||
                backgroundColor: 'red',
 | 
					 | 
				
			||||||
                borderColor: 'red',
 | 
					 | 
				
			||||||
                data: data.map(row => row.weight),
 | 
					 | 
				
			||||||
                fill: false
 | 
					 | 
				
			||||||
            }];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const labels = data.map(row => row.date);
 | 
					 | 
				
			||||||
            const comments = data.map(row => row.comment);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return {
 | 
					 | 
				
			||||||
                labels: labels,
 | 
					 | 
				
			||||||
                datasets: datasets,
 | 
					 | 
				
			||||||
                comments: comments
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $form.submit(event => {
 | 
					 | 
				
			||||||
            saveWeight();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            event.preventDefault();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        drawChart();
 | 
					 | 
				
			||||||
    })();
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
const build = require('./build');
 | 
					const build = require('./build');
 | 
				
			||||||
const packageJson = require('../../package');
 | 
					const packageJson = require('../../package');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const APP_DB_VERSION = 77;
 | 
					const APP_DB_VERSION = 78;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    app_version: packageJson.version,
 | 
					    app_version: packageJson.version,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,13 +3,18 @@
 | 
				
			|||||||
const sql = require('./sql');
 | 
					const sql = require('./sql');
 | 
				
			||||||
const utils = require('./utils');
 | 
					const utils = require('./utils');
 | 
				
			||||||
const sync_table = require('./sync_table');
 | 
					const sync_table = require('./sync_table');
 | 
				
			||||||
const Repository = require('./repository');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const BUILTIN_ATTRIBUTES = [
 | 
					const BUILTIN_ATTRIBUTES = [
 | 
				
			||||||
    'run_on_startup',
 | 
					    'frontend_startup',
 | 
				
			||||||
 | 
					    'backend_startup',
 | 
				
			||||||
    'disable_versioning',
 | 
					    'disable_versioning',
 | 
				
			||||||
    'calendar_root',
 | 
					    'calendar_root',
 | 
				
			||||||
    'hide_in_autocomplete'
 | 
					    'hide_in_autocomplete',
 | 
				
			||||||
 | 
					    'exclude_from_export',
 | 
				
			||||||
 | 
					    'run',
 | 
				
			||||||
 | 
					    'manual_transaction_handling',
 | 
				
			||||||
 | 
					    'disable_inclusion',
 | 
				
			||||||
 | 
					    'app_css'
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteAttributeMap(noteId) {
 | 
					async function getNoteAttributeMap(noteId) {
 | 
				
			||||||
@@ -24,9 +29,7 @@ async function getNoteIdWithAttribute(name, value) {
 | 
				
			|||||||
                AND attributes.value = ?`, [name, value]);
 | 
					                AND attributes.value = ?`, [name, value]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNotesWithAttribute(dataKey, name, value) {
 | 
					async function getNotesWithAttribute(repository, name, value) {
 | 
				
			||||||
    const repository = new Repository(dataKey);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let notes;
 | 
					    let notes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (value !== undefined) {
 | 
					    if (value !== undefined) {
 | 
				
			||||||
@@ -41,8 +44,8 @@ async function getNotesWithAttribute(dataKey, name, value) {
 | 
				
			|||||||
    return notes;
 | 
					    return notes;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteWithAttribute(dataKey, name, value) {
 | 
					async function getNoteWithAttribute(repository, name, value) {
 | 
				
			||||||
    const notes = getNotesWithAttribute(dataKey, name, value);
 | 
					    const notes = getNotesWithAttribute(repository, name, value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return notes.length > 0 ? notes[0] : null;
 | 
					    return notes.length > 0 ? notes[0] : null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -59,12 +62,14 @@ async function createAttribute(noteId, name, value = "", sourceId = null) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const now = utils.nowDate();
 | 
					    const now = utils.nowDate();
 | 
				
			||||||
    const attributeId = utils.newAttributeId();
 | 
					    const attributeId = utils.newAttributeId();
 | 
				
			||||||
 | 
					    const position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [noteId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sql.insert("attributes", {
 | 
					    await sql.insert("attributes", {
 | 
				
			||||||
        attributeId: attributeId,
 | 
					        attributeId: attributeId,
 | 
				
			||||||
        noteId: noteId,
 | 
					        noteId: noteId,
 | 
				
			||||||
        name: name,
 | 
					        name: name,
 | 
				
			||||||
        value: value,
 | 
					        value: value,
 | 
				
			||||||
 | 
					        position: position,
 | 
				
			||||||
        dateModified: now,
 | 
					        dateModified: now,
 | 
				
			||||||
        dateCreated: now,
 | 
					        dateCreated: now,
 | 
				
			||||||
        isDeleted: false
 | 
					        isDeleted: false
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										27
									
								
								src/services/scheduler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/services/scheduler.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					const script = require('./script');
 | 
				
			||||||
 | 
					const Repository = require('./repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const repo = new Repository();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function runNotesWithAttribute(runAttrValue) {
 | 
				
			||||||
 | 
					    const notes = await repo.getEntities(`
 | 
				
			||||||
 | 
					        SELECT notes.* 
 | 
				
			||||||
 | 
					        FROM notes 
 | 
				
			||||||
 | 
					          JOIN attributes ON attributes.noteId = notes.noteId
 | 
				
			||||||
 | 
					                           AND attributes.isDeleted = 0
 | 
				
			||||||
 | 
					                           AND attributes.name = 'run' 
 | 
				
			||||||
 | 
					                           AND attributes.value = ? 
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					          notes.type = 'code'
 | 
				
			||||||
 | 
					          AND notes.isDeleted = 0`, [runAttrValue]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const note of notes) {
 | 
				
			||||||
 | 
					        script.executeNote(null, note);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					setTimeout(() => runNotesWithAttribute('backend_startup'), 10 * 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					setInterval(() => runNotesWithAttribute('hourly'), 3600 * 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					setInterval(() => runNotesWithAttribute('daily'), 24 * 3600 * 1000);
 | 
				
			||||||
@@ -1,24 +1,63 @@
 | 
				
			|||||||
const log = require('./log');
 | 
					 | 
				
			||||||
const sql = require('./sql');
 | 
					const sql = require('./sql');
 | 
				
			||||||
const ScriptContext = require('./script_context');
 | 
					const ScriptContext = require('./script_context');
 | 
				
			||||||
 | 
					const Repository = require('./repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function executeScript(dataKey, script, params) {
 | 
					async function executeNote(dataKey, note) {
 | 
				
			||||||
    log.info('Executing script: ' + script);
 | 
					    if (!note.isJavaScript()) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ctx = new ScriptContext(dataKey);
 | 
					    const bundle = await getScriptBundle(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const paramsStr = getParams(params);
 | 
					    await executeBundle(dataKey, bundle);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let ret;
 | 
					async function executeBundle(dataKey, bundle, startNote) {
 | 
				
			||||||
 | 
					    if (!startNote) {
 | 
				
			||||||
 | 
					        // this is the default case, the only exception is when we want to preserve frontend startNote
 | 
				
			||||||
 | 
					        startNote = bundle.note;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sql.doInTransaction(async () => {
 | 
					    // last \r\n is necessary if script contains line comment on its last line
 | 
				
			||||||
        ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx));
 | 
					    const script = "async function() {\r\n" + bundle.script + "\r\n}";
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ret;
 | 
					    const ctx = new ScriptContext(dataKey, startNote, bundle.allNotes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (await bundle.note.hasAttribute('manual_transaction_handling')) {
 | 
				
			||||||
 | 
					        return await execute(ctx, script, '');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        return await sql.doInTransaction(async () => execute(ctx, script, ''));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This method preserves frontend startNode - that's why we start execution from currentNote and override
 | 
				
			||||||
 | 
					 * bundle's startNote.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function executeScript(dataKey, script, params, startNoteId, currentNoteId) {
 | 
				
			||||||
 | 
					    const repository = new Repository(dataKey);
 | 
				
			||||||
 | 
					    const startNote = await repository.getNote(startNoteId);
 | 
				
			||||||
 | 
					    const currentNote = await repository.getNote(currentNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    currentNote.content = `return await (${script}\r\n)(${getParams(params)})`;
 | 
				
			||||||
 | 
					    currentNote.type = 'code';
 | 
				
			||||||
 | 
					    currentNote.mime = 'application/javascript;env=backend';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const bundle = await getScriptBundle(currentNote);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return await executeBundle(dataKey, bundle, startNote);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function execute(ctx, script, paramsStr) {
 | 
				
			||||||
 | 
					    return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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);
 | 
				
			||||||
@@ -29,6 +68,74 @@ function getParams(params) {
 | 
				
			|||||||
    }).join(",");
 | 
					    }).join(",");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
 | 
				
			||||||
 | 
					    if (!note.isJavaScript() && !note.isHtml() && note.type !== 'render') {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!root && await note.hasAttribute('disable_inclusion')) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (root) {
 | 
				
			||||||
 | 
					        scriptEnv = note.getScriptEnv();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (note.type !== 'file' && scriptEnv !== note.getScriptEnv()) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const bundle = {
 | 
				
			||||||
 | 
					        note: note,
 | 
				
			||||||
 | 
					        script: '',
 | 
				
			||||||
 | 
					        html: '',
 | 
				
			||||||
 | 
					        allNotes: [note]
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (includedNoteIds.includes(note.noteId)) {
 | 
				
			||||||
 | 
					        return bundle;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    includedNoteIds.push(note.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modules = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const child of await note.getChildren()) {
 | 
				
			||||||
 | 
					        const childBundle = await getScriptBundle(child, false, scriptEnv, includedNoteIds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (childBundle) {
 | 
				
			||||||
 | 
					            modules.push(childBundle.note);
 | 
				
			||||||
 | 
					            bundle.script += childBundle.script;
 | 
				
			||||||
 | 
					            bundle.html += childBundle.html;
 | 
				
			||||||
 | 
					            bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const moduleNoteIds = modules.map(mod => mod.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (note.isJavaScript()) {
 | 
				
			||||||
 | 
					        bundle.script += `
 | 
				
			||||||
 | 
					apiContext.modules['${note.noteId}'] = {};
 | 
				
			||||||
 | 
					${root ? 'return ' : ''}await (async function(exports, module, require, api` + (modules.length > 0 ? ', ' : '') +
 | 
				
			||||||
 | 
					            modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) {
 | 
				
			||||||
 | 
					${note.content}
 | 
				
			||||||
 | 
					})({}, apiContext.modules['${note.noteId}'], apiContext.require(${JSON.stringify(moduleNoteIds)}), apiContext.apis['${note.noteId}']` + (modules.length > 0 ? ', ' : '') +
 | 
				
			||||||
 | 
					            modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ') + `);
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else if (note.isHtml()) {
 | 
				
			||||||
 | 
					        bundle.html += note.content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return bundle;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sanitizeVariableName(str) {
 | 
				
			||||||
 | 
					    return str.replace(/[^a-z0-9_]/gim, "");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    executeScript
 | 
					    executeNote,
 | 
				
			||||||
 | 
					    executeScript,
 | 
				
			||||||
 | 
					    getScriptBundle
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -1,20 +1,55 @@
 | 
				
			|||||||
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, startNote, allNotes) {
 | 
				
			||||||
    this.dataKey = protected_session.getDataKey(dataKey);
 | 
					    dataKey = protected_session.getDataKey(dataKey);
 | 
				
			||||||
    this.repository = new Repository(dataKey);
 | 
					
 | 
				
			||||||
 | 
					    this.modules = {};
 | 
				
			||||||
 | 
					    this.notes = utils.toObject(allNotes, note => [note.noteId, note]);
 | 
				
			||||||
 | 
					    this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(dataKey, startNote, note)]);
 | 
				
			||||||
 | 
					    this.require = moduleNoteIds => {
 | 
				
			||||||
 | 
					        return moduleName => {
 | 
				
			||||||
 | 
					            const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
 | 
				
			||||||
 | 
					            const note = candidates.find(c => c.title === moduleName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!note) {
 | 
				
			||||||
 | 
					                throw new Error("Could not find module note " + moduleName);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return this.modules[note.noteId].exports;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ScriptApi(dataKey, startNote, currentNote) {
 | 
				
			||||||
 | 
					    const repository = new Repository(dataKey);
 | 
				
			||||||
 | 
					    this.startNote = startNote;
 | 
				
			||||||
 | 
					    this.currentNote = currentNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.axios = axios;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.utils = {
 | 
				
			||||||
 | 
					        unescapeHtml: utils.unescapeHtml,
 | 
				
			||||||
 | 
					        isoDateTimeStr: utils.dateStr,
 | 
				
			||||||
 | 
					        isoDateStr: date => utils.dateStr(date).substr(0, 10)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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(repository, attrName, attrValue);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.getNoteWithAttribute = async function (attrName, attrValue) {
 | 
					    this.getNoteWithAttribute = async function (attrName, attrValue) {
 | 
				
			||||||
@@ -23,46 +58,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 ${currentNote.noteId}: ${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,22 @@ async function stopWatch(what, func) {
 | 
				
			|||||||
    return ret;
 | 
					    return ret;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function unescapeHtml(str) {
 | 
				
			||||||
 | 
					    return unescape(str);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toObject(array, fn) {
 | 
				
			||||||
 | 
					    const obj = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const item of array) {
 | 
				
			||||||
 | 
					        const ret = fn(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        obj[ret[0]] = ret[1];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return obj;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    randomSecureToken,
 | 
					    randomSecureToken,
 | 
				
			||||||
    randomString,
 | 
					    randomString,
 | 
				
			||||||
@@ -153,5 +170,7 @@ module.exports = {
 | 
				
			|||||||
    getDateTimeForFile,
 | 
					    getDateTimeForFile,
 | 
				
			||||||
    sanitizeSql,
 | 
					    sanitizeSql,
 | 
				
			||||||
    assertArguments,
 | 
					    assertArguments,
 | 
				
			||||||
    stopWatch
 | 
					    stopWatch,
 | 
				
			||||||
 | 
					    unescapeHtml,
 | 
				
			||||||
 | 
					    toObject
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -40,22 +40,20 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      <div class="hide-toggle" style="grid-area: tree-actions;">
 | 
					      <div class="hide-toggle" style="grid-area: tree-actions;">
 | 
				
			||||||
        <div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
 | 
					        <div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
 | 
				
			||||||
          <a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action">
 | 
					          <a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action"
 | 
				
			||||||
            <img src="images/icons/file-plus.png" alt="Create new top level note"/>
 | 
					             style="background: url('images/icons/file-plus.png')"></a>
 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action">
 | 
					          <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action"
 | 
				
			||||||
            <img src="images/icons/list.png" alt="Collapse note tree"/>
 | 
					             style="background: url('images/icons/list.png')"></a>
 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action">
 | 
					          <a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action"
 | 
				
			||||||
            <img src="images/icons/crosshair.png" alt="Scroll to current note"/>
 | 
					             style="background: url('images/icons/crosshair.png')"></a>
 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action">
 | 
					          <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action"
 | 
				
			||||||
            <img src="images/icons/search.png" alt="Search in notes"/>
 | 
					             style="background: url('images/icons/search.png')"></a>
 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <input type="file" id="import-upload" style="display: none" />
 | 
				
			||||||
      </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;">
 | 
				
			||||||
@@ -81,17 +79,13 @@
 | 
				
			|||||||
             title="Protect the note so that password will be required to view the note"
 | 
					             title="Protect the note so that password will be required to view the note"
 | 
				
			||||||
             class="icon-action"
 | 
					             class="icon-action"
 | 
				
			||||||
             id="protect-button"
 | 
					             id="protect-button"
 | 
				
			||||||
             style="display: none;">
 | 
					             style="display: none; background: url('images/icons/lock.png')"></a>
 | 
				
			||||||
            <img src="images/icons/lock.png" alt="Protect note"/>
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <a onclick="protected_session.unprotectNoteAndSendToServer()"
 | 
					          <a onclick="protected_session.unprotectNoteAndSendToServer()"
 | 
				
			||||||
             title="Unprotect note so that password will not be required to access this note in the future"
 | 
					             title="Unprotect note so that password will not be required to access this note in the future"
 | 
				
			||||||
             class="icon-action"
 | 
					             class="icon-action"
 | 
				
			||||||
             id="unprotect-button"
 | 
					             id="unprotect-button"
 | 
				
			||||||
             style="display: none;">
 | 
					             style="display: none; background: url('images/icons/unlock.png')"></a>
 | 
				
			||||||
            <img src="images/icons/unlock.png" alt="Unprotect note"/>
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
           
 | 
					           
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -167,7 +161,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 +491,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>
 | 
				
			||||||
@@ -526,5 +521,9 @@
 | 
				
			|||||||
      // final form which is pretty ugly.
 | 
					      // final form which is pretty ugly.
 | 
				
			||||||
      $("#container").show();
 | 
					      $("#container").show();
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <style type="text/css">
 | 
				
			||||||
 | 
					      <%= appCss %>
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user