Compare commits

...

52 Commits

Author SHA1 Message Date
azivner
348c622845 release 0.9.1-beta 2018-03-09 00:12:22 -05:00
azivner
44bcdedaba Updated all scripts to current versions working with current script API 2018-03-09 00:10:43 -05:00
azivner
755c0f3ce2 fix exclude from export 2018-03-09 00:10:02 -05:00
azivner
895bda41b5 return only startup bundles for executale notes 2018-03-08 23:36:08 -05:00
azivner
b2df622cb6 Merge branch 'stable' 2018-03-08 23:35:17 -05:00
azivner
9ba6e6d0f5 disable inclusion should work only on non-root notes 2018-03-08 23:35:08 -05:00
azivner
a5c9180533 release 0.9.0-beta 2018-03-08 20:18:37 -05:00
azivner
e86f1e0d05 changes to the HTML template to allow more complete styling 2018-03-07 23:58:34 -05:00
azivner
b6277049f3 added support for app_css attribute, which allows custom styling 2018-03-07 23:24:23 -05:00
azivner
c831221cc4 add "play" icon for "render" note types 2018-03-07 20:52:34 -05:00
azivner
577a168714 stop propagation of ctrl+enter from SQL console, fixes #73 2018-03-07 20:46:01 -05:00
azivner
b0bd27321a escape will close SQL console, closes #72 2018-03-07 20:33:41 -05:00
azivner
90c5348ca7 fix saving only image in a note, fixes #77 2018-03-07 20:19:53 -05:00
azivner
8e95b080da fixed render notes 2018-03-07 00:17:18 -05:00
azivner
766a567a32 changes in access to startNote and currentNote 2018-03-06 23:04:35 -05:00
azivner
6d0218cb36 execute note (ctrl+enter) now works for both frontend and backend scripts 2018-03-05 23:19:46 -05:00
azivner
d26170762b inclusion of scripts based on script environment 2018-03-05 23:09:36 -05:00
azivner
b3209a9bbf split javascript mime type into frontend and backend 2018-03-05 22:08:45 -05:00
azivner
61c2456cf6 startNote/currentNote is now accessible on frontend as well 2018-03-04 23:28:26 -05:00
azivner
1c6fc9029f new "disable_inclusion" attribute 2018-03-04 22:09:51 -05:00
azivner
5c91e38dfe server.exec() refactored into api 2018-03-04 21:43:14 -05:00
azivner
07bf075894 cleaned up unused jobs implementation 2018-03-04 21:33:06 -05:00
azivner
ddce5c959e fix render 2018-03-04 21:05:14 -05:00
azivner
3b9d1df05c fixed frontend script execution 2018-03-04 14:21:11 -05:00
azivner
d239ef2956 refactoring of getModules function 2018-03-04 12:06:35 -05:00
azivner
7a865a9081 common JS module system prototype 2018-03-04 10:32:53 -05:00
azivner
83d6c2970f added versioning to the metadata files in export tars 2018-03-03 09:32:21 -05:00
azivner
8c7d159012 fix export/import for multi-valued attributes 2018-03-03 09:30:18 -05:00
azivner
d169f67901 changes in backend script running 2018-03-03 09:11:41 -05:00
azivner
982b723647 basic scheduling of backend scripts using attributes 2018-03-02 20:56:58 -05:00
azivner
31d5ac05ff release 0.8.1 2018-03-01 23:08:53 -05:00
azivner
72d91d1571 don't use eslint on JSON notes, closes #70 2018-03-01 22:42:51 -05:00
azivner
f4b57f4c57 Allow attachments to be included in the scripts, closes #66 2018-03-01 22:30:06 -05:00
azivner
ee0833390a fix export in electron (auth problem) 2018-02-27 09:47:05 -05:00
azivner
2acff07368 release 0.8.0-beta 2018-02-26 22:57:15 -05:00
azivner
bea1d24f07 tweaks to eslint 2018-02-26 22:55:58 -05:00
azivner
adc270c59f removed reference to reddit plugin 2018-02-26 22:31:35 -05:00
azivner
66064f7a94 Script API changes, finished porting reddit plugin, reddit importer tar file 2018-02-26 20:47:34 -05:00
azivner
1501fa8dbf import notes from tar archive, closes #63 2018-02-26 00:07:43 -05:00
azivner
60bba46d80 export subtree to tar file 2018-02-25 10:55:21 -05:00
azivner
12c06ae97e manual transaction handling for jobs 2018-02-24 22:44:45 -05:00
azivner
f0bea9cf71 API changes necessary to port reddit plugin, closes #58 2018-02-24 21:23:04 -05:00
azivner
a555b6319c support for backend jobs and other script API changes 2018-02-24 14:42:52 -05:00
azivner
5dd93e4cdc eslint for javascript inside HTML (htmlmixed mode), closes #62 2018-02-24 00:58:11 -05:00
azivner
3b4509d833 support encryption for files, closes #60 2018-02-23 22:58:24 -05:00
azivner
19308bbfbd small changes to linting and protected session 2018-02-23 20:10:29 -05:00
azivner
4acc5432c3 autocomplete returns items which have at least one of the tokens in the leaf note title, closes #59 2018-02-22 19:52:08 -05:00
azivner
08b8141fdf upgrade to codemirror 5.35 2018-02-21 23:09:52 -05:00
azivner
e1200aa308 lazy loading of eslint only for JS code 2018-02-21 20:30:15 -05:00
azivner
89666eb078 paperclip icon for attachment, closes #61 2018-02-21 19:53:46 -05:00
azivner
d5605aa64d initial support for eslint backed JS linting 2018-02-20 23:24:55 -05:00
azivner
2582b016f9 increased "connection lost" timeout from 5 seconds to 30, it was way to common and mostly false positive 2018-02-20 07:52:39 -05:00
56 changed files with 106364 additions and 4383 deletions

View File

@@ -1,3 +1,7 @@
[General]
# Instance name can be used to distinguish between different instances
instanceName=
[Network]
port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).

View File

@@ -0,0 +1 @@
UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript';

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.6.2",
"version": "0.7.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.7.0-beta",
"version": "0.9.1-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -57,6 +57,7 @@
"session-file-store": "^1.1.2",
"simple-node-logger": "^0.93.30",
"sqlite": "^2.9.0",
"tar-stream": "^1.5.5",
"unescape": "^1.0.1",
"ws": "^3.3.2"
},

View File

@@ -73,7 +73,7 @@ require('./services/backup');
// trigger consistency checks timer
require('./services/consistency_checks');
require('./plugins/reddit');
require('./services/scheduler');
module.exports = {
app,

View File

@@ -24,13 +24,52 @@ class Note extends Entity {
}
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() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
// WARNING: this doesn't take into account the possibility to have multi-valued attributes!
async getAttributeMap() {
const map = {};
for (const attr of await this.getAttributes()) {
map[attr.name] = attr.value;
}
return map;
}
async hasAttribute(name) {
const map = await this.getAttributeMap();
return map.hasOwnProperty(name);
}
// WARNING: this doesn't take into account the possibility to have multi-valued attributes!
async getAttribute(name) {
return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
}
@@ -43,6 +82,49 @@ class Note extends Entity {
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() {
this.content = JSON.stringify(this.jsonContent, null, '\t');

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View File

@@ -1,4 +1,12 @@
const api = (function() {
function ScriptContext(startNote, allNotes) {
return {
modules: {},
notes: toObject(allNotes, note => [note.noteId, note]),
apis: toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
};
}
function ScriptApi(startNote, currentNote) {
const $pluginButtons = $("#plugin-buttons");
async function activateNote(notePath) {
@@ -13,9 +21,42 @@ const api = (function() {
$pluginButtons.append(button);
}
function prepareParams(params) {
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "function") {
return "!@#Function: " + p.toString();
}
else {
return p;
}
});
}
async function runOnServer(script, params = []) {
if (typeof script === "function") {
script = script.toString();
}
const ret = await server.post('script/exec', {
script: script,
params: prepareParams(params),
startNoteId: startNote.noteId,
currentNoteId: currentNote.noteId
});
return ret.executionResult;
}
return {
startNote: startNote,
currentNote: currentNote,
addButtonToToolbar,
activateNote
activateNote,
getInstanceName: noteTree.getInstanceName,
runOnServer
}
})();
}

View File

@@ -85,9 +85,12 @@ const contextMenu = (function() {
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"},
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
{title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"},
{title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"},
{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) => {
@@ -139,13 +142,19 @@ const contextMenu = (function() {
else if (ui.cmd === "delete") {
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);
}
else if (ui.cmd === "force-note-sync") {
else if (ui.cmd === "forceNoteSync") {
forceNoteSync(node.data.noteId);
}
else if (ui.cmd === "sort-alphabetically") {
else if (ui.cmd === "sortAlphabetically") {
noteTree.sortAlphabetically(node.data.noteId);
}
else {

View File

@@ -29,6 +29,9 @@ const sqlConsole = (function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
// removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
delete CodeMirror.keyMap.basic["Esc"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
@@ -45,7 +48,11 @@ const sqlConsole = (function() {
codeEditor.focus();
}
async function execute() {
async function execute(e) {
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
e.preventDefault();
e.stopPropagation();
const sqlQuery = codeEditor.getValue();
const result = await server.post("sql/execute", {

View 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();
});

View File

@@ -114,24 +114,34 @@ $.ui.autocomplete.filter = (array, terms) => {
const tokens = terms.toLowerCase().split(" ");
for (const item of array) {
let found = true;
const lcLabel = item.label.toLowerCase();
for (const token of tokens) {
if (lcLabel.indexOf(token) === -1) {
found = false;
break;
const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
if (!found) {
continue;
}
// 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);
if (results.length > 100) {
break;
}
}
}
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());
$(document).ready(() => {
server.get("script/startup").then(scripts => {
for (const script of scripts) {
executeScript(script);
server.get("script/startup").then(scriptBundles => {
for (const bundle of scriptBundles) {
executeBundle(bundle);
}
});
});
@@ -216,10 +226,10 @@ if (isElectron()) {
}
function uploadAttachment() {
$("#file-upload").trigger('click');
$("#attachment-upload").trigger('click');
}
$("#file-upload").change(async function() {
$("#attachment-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);

View File

@@ -84,7 +84,7 @@ const messaging = (function() {
let connectionBrokenNotification = null;
setInterval(async () => {
if (new Date().getTime() - lastPingTs > 5000) {
if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({
// options

View File

@@ -77,13 +77,15 @@ const noteEditor = (function() {
function updateNoteFromInputs(note) {
if (note.detail.type === 'text') {
note.detail.content = editor.getData();
let content = editor.getData();
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty
// this is important when setting new note to code
if (jQuery(note.detail.content).text().trim() === '') {
note.detail.content = ''
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
content = '';
}
note.detail.content = content;
}
else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue();
@@ -154,7 +156,10 @@ const noteEditor = (function() {
indentUnit: 4,
matchBrackets: 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);
@@ -171,6 +176,8 @@ const noteEditor = (function() {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
codeEditor.refresh();
}
}
@@ -212,9 +219,11 @@ const noteEditor = (function() {
if (currentNote.detail.type === 'render') {
$noteDetailRender.show();
const subTree = await server.get('script/subtree/' + getCurrentNoteId());
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
$noteDetailRender.html(subTree);
$noteDetailRender.html(bundle.html);
executeBundle(bundle);
}
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
@@ -293,22 +302,21 @@ const noteEditor = (function() {
// make sure note is saved so we load latest changes
await saveNoteIfChanged();
const script = await server.get('script/subtree/' + getCurrentNoteId());
if (currentNote.detail.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
executeScript(script);
executeBundle(bundle);
}
if (currentNote.detail.mime.endsWith("env=backend")) {
await server.post('script/run/' + getCurrentNoteId());
}
showMessage("Note executed");
}
}
$attachmentDownload.click(() => {
if (isElectron()) {
const remote = require('electron').remote;
remote.getCurrentWebContents().downloadURL(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
$attachmentDownload.click(() => download(getAttachmentUrl()));
$attachmentOpen.click(() => {
if (isElectron()) {
@@ -323,12 +331,8 @@ const noteEditor = (function() {
function getAttachmentUrl() {
// electron needs absolute URL so we extract current host, port, protocol
const url = new URL(window.location.href);
const host = url.protocol + "//" + url.hostname + ":" + url.port;
const downloadUrl = "/api/attachments/download/" + getCurrentNoteId();
return host + downloadUrl;
return getHost() + "/api/attachments/download/" + getCurrentNoteId()
+ "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
}
$(document).ready(() => {

View File

@@ -5,6 +5,8 @@ const noteTree = (function() {
const $parentList = $("#parent-list");
const $parentListList = $("#parent-list-inner");
let instanceName = null; // should have better place
let startNotePath = null;
let notesTreeMap = {};
@@ -155,6 +157,12 @@ const noteTree = (function() {
if (note.type === 'code') {
extraClasses.push("code");
}
else if (note.type === 'render') {
extraClasses.push('render');
}
else if (note.type === 'file') {
extraClasses.push('attachment');
}
return extraClasses.join(" ");
}
@@ -645,6 +653,7 @@ const noteTree = (function() {
async function loadTree() {
const resp = await server.get('tree');
startNotePath = resp.start_note_path;
instanceName = resp.instanceName;
if (document.location.hash) {
startNotePath = getNotePathFromAddress();
@@ -710,6 +719,9 @@ const noteTree = (function() {
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 = [];
for (const childNoteId of parentToChildren[parentNoteId]) {
@@ -820,6 +832,10 @@ const noteTree = (function() {
return !!childToParents[noteId];
}
function getInstanceName() {
return instanceName;
}
$(document).bind('keydown', 'ctrl+o', e => {
const node = getCurrentNode();
const parentNoteId = node.data.parentNoteId;
@@ -894,6 +910,7 @@ const noteTree = (function() {
setParentChildRelation,
getSelectedNodes,
sortAlphabetically,
noteExists
noteExists,
getInstanceName
};
})();

View File

@@ -25,7 +25,8 @@ const noteType = (function() {
{ mime: 'text/html', title: 'HTML' },
{ mime: 'message/http', title: 'HTTP' },
{ mime: 'text/x-java', title: 'Java' },
{ mime: 'application/javascript', title: 'JavaScript' },
{ mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
{ mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
{ mime: 'application/json', title: 'JSON' },
{ mime: 'text/x-kotlin', title: 'Kotlin' },
{ mime: 'text/x-lua', title: 'Lua' },
@@ -121,7 +122,7 @@ const noteType = (function() {
};
this.updateExecuteScriptButtonVisibility = function() {
$executeScriptButton.toggle(self.mime() === 'application/javascript');
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
}
}

View File

@@ -25,7 +25,9 @@ const protected_session = (function() {
if (requireProtectedSession && !isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd;
if (noteTree.getCurrentNode().data.isProtected) {
$noteDetailWrapper.hide();
}
$dialog.dialog({
modal: modal,

View File

@@ -31,27 +31,6 @@ const server = (function() {
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;
const reqResolves = {};
@@ -115,7 +94,6 @@ const server = (function() {
post,
put,
remove,
exec,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders

View File

@@ -115,8 +115,10 @@ async function stopWatch(what, func) {
return ret;
}
function executeScript(script) {
eval(script);
async function executeBundle(bundle) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
return await (function() { return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }.call(apiContext));
}
function formatValueWithWhitespace(val) {
@@ -143,13 +145,18 @@ const CODE_MIRROR = {
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.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: [
"libraries/codemirror/codemirror.css"
"libraries/codemirror/codemirror.css",
"libraries/codemirror/addon/lint/lint.css"
]
};
const ESLINT = { js: [ "libraries/eslint.js" ] };
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
@@ -162,13 +169,13 @@ async function requireLibrary(library) {
}
}
async function requireScript(url) {
const scripts = Array
.from(document.querySelectorAll('script'))
.map(scr => scr.src);
const dynamicallyLoadedScripts = [];
if (!scripts.includes(url)) {
return $.ajax({
async function requireScript(url) {
if (!dynamicallyLoadedScripts.includes(url)) {
dynamicallyLoadedScripts.push(url);
return await $.ajax({
url: url,
dataType: "script",
cache: true
@@ -185,3 +192,31 @@ async function requireCss(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;
}

View File

@@ -102,18 +102,23 @@
}
}
var currentlyHighlighted = null;
function doMatchBrackets(cm) {
cm.operation(function() {
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
if (cm.state.matchBrackets.currentlyHighlighted) {
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) {
if (old && old != CodeMirror.Init) {
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) {
cm.state.matchBrackets = typeof val == "object" ? val : {};

View File

@@ -138,7 +138,7 @@
var iter = new Iter(cm, start.line, 0);
for (;;) {
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") {
var startPos = Pos(iter.line, iter.ch);
var endPos = findMatchingClose(iter, openTag[2]);

View 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)
});
}
}
});

View 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%;
}

View 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);
});
});

View File

@@ -90,7 +90,7 @@
var state = cm.state.matchHighlighter;
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
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,
{className: "CodeMirror-selection-highlight-scrollbar"});
}

File diff suppressed because it is too large Load Diff

View File

@@ -846,6 +846,8 @@ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/javascript;env=frontend", "javascript");
CodeMirror.defineMIME("application/javascript;env=backend", "javascript");
CodeMirror.defineMIME("application/x-javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});

View File

@@ -70,7 +70,7 @@
{name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]},
{name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
{name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"],
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/javascript;env=frontend", "application/javascript;env=backend", "application/x-javascript", "application/ecmascript"],
mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]},
{name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]},
{name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]},

101349
src/public/libraries/eslint.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -72,6 +72,16 @@ span.fancytree-node.fancytree-folder.code > span.fancytree-icon {
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 {
filter: drop-shadow(2px 2px 2px black);
}
@@ -103,6 +113,9 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
.icon-action {
cursor: pointer;
display: block;
height: 24px;
width: 24px;
}
#protect-button, #unprotect-button {

View File

@@ -6,6 +6,7 @@ const sql = require('../../services/sql');
const auth = require('../../services/auth');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const protected_session = require('../../services/protected_session');
const multer = require('multer')();
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) => {
const noteId = req.params.noteId;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
const protectedSessionId = req.query.protectedSessionId;
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);

View File

@@ -2,56 +2,79 @@
const express = require('express');
const router = express.Router();
const rimraf = require('rimraf');
const fs = require('fs');
const sql = require('../../services/sql');
const data_dir = require('../../services/data_dir');
const html = require('html');
const auth = require('../../services/auth');
const wrap = require('express-promise-wrap').wrap;
const tar = require('tar-stream');
const sanitize = require("sanitize-filename");
const Repository = require("../../services/repository");
router.get('/:noteId/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 directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
if (!fs.existsSync(data_dir.EXPORT_DIR)) {
fs.mkdirSync(data_dir.EXPORT_DIR);
}
const completeExportDir = data_dir.EXPORT_DIR + '/' + directory;
if (fs.existsSync(completeExportDir)) {
rimraf.sync(completeExportDir);
}
fs.mkdirSync(completeExportDir);
const repo = new Repository(req);
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 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]);
if (children.length > 0) {
const childrenDir = dir + '/' + pos + '-' + note.title;
fs.mkdirSync(childrenDir);
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;

View File

@@ -2,104 +2,136 @@
const express = require('express');
const router = express.Router();
const fs = require('fs');
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 attributes = require('../../services/attributes');
const notes = require('../../services/notes');
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) => {
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
function getFileName(name) {
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 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({});
}));
async function importNotes(dir, parentNoteId) {
const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
if (!parent) {
return;
async function importNotes(files, parentNoteId, sourceId) {
for (const file of files) {
if (file.meta.version !== 1) {
throw new Error("Can't read meta data version " + file.meta.version);
}
const fileList = fs.readdirSync(dir);
for (const file of fileList) {
const path = dir + '/' + file;
if (fs.lstatSync(path).isDirectory()) {
continue;
if (file.meta.type !== 'file') {
file.data = file.data.toString("UTF-8");
}
if (!file.endsWith('.html')) {
continue;
}
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
const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, {
type: file.meta.type,
mime: file.meta.mime,
sourceId: sourceId
});
await sync_table.addNoteTreeSync(noteTreeId);
for (const attr of file.meta.attributes) {
await attributes.createAttribute(noteId, attr.name, attr.value);
}
await sql.insert('notes', {
noteId: noteId,
title: noteTitle,
content: noteText,
isDeleted: 0,
isProtected: 0,
type: 'text',
mime: 'text/html',
dateCreated: now,
dateModified: now
});
await sync_table.addNoteSync(noteId);
const noteDir = dir + '/' + fileNameWithoutExt;
if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) {
await importNotes(noteDir, noteId);
if (file.children.length > 0) {
await importNotes(file.children, noteId, sourceId);
}
}
}

View File

@@ -4,13 +4,23 @@ const express = require('express');
const router = express.Router();
const auth = require('../../services/auth');
const wrap = require('express-promise-wrap').wrap;
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const script = require('../../services/script');
const Repository = require('../../services/repository');
router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
const ret = await script.executeScript(req, req.body.script, req.body.params);
const ret = await script.executeScript(req, req.body.script, req.body.params, req.body.startNoteId, req.body.currentNoteId);
res.send({
executionResult: ret
});
}));
router.post('/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({
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) => {
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
const repository = new Repository(req);
const notes = await attributes.getNotesWithAttribute(repository, "run", "frontend_startup");
const scripts = [];
for (const noteId of noteIds) {
scripts.push(await getNoteWithSubtreeScript(noteId, repository));
for (const note of notes) {
const bundle = await script.getScriptBundle(note);
if (bundle) {
scripts.push(bundle);
}
}
res.send(scripts);
}));
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/bundle/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
const noteId = req.params.noteId;
const note = await repository.getNote(req.params.noteId);
const bundle = await script.getScriptBundle(note);
res.send(await getNoteWithSubtreeScript(noteId, repository));
res.send(bundle);
}));
async function getNoteWithSubtreeScript(noteId, repository) {
const note = await repository.getNote(noteId);
let noteScript = note.content;
if (note.isJavaScript()) {
// last \r\n is necessary if script contains line comment on its last line
noteScript = "(async function() {" + noteScript + "\r\n})()";
}
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository, note.isJavaScript());
return subTreeScripts + noteScript;
}
async function getSubTreeScripts(parentId, includedNoteIds, repository, isJavaScript) {
const children = await repository.getEntities(`
SELECT notes.*
FROM notes JOIN note_tree USING(noteId)
WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0
AND note_tree.parentNoteId = ? AND notes.type = 'code'
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;

View File

@@ -6,6 +6,7 @@ const sql = require('../../services/sql');
const options = require('../../services/options');
const utils = require('../../services/utils');
const auth = require('../../services/auth');
const config = require('../../services/config');
const protected_session = require('../../services/protected_session');
const sync_table = require('../../services/sync_table');
const wrap = require('express-promise-wrap').wrap;
@@ -41,6 +42,7 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
AND notes.isDeleted = 0`);
res.send({
instanceName: config.General ? config.General.instanceName : null,
notes: notes,
hiddenInAutocomplete: hiddenInAutocomplete,
start_note_path: await options.getOption('start_note_path')

View File

@@ -5,13 +5,32 @@ const router = express.Router();
const auth = require('../services/auth');
const source_id = require('../services/source_id');
const sql = require('../services/sql');
const Repository = require('../services/repository');
const attributes = require('../services/attributes');
const wrap = require('express-promise-wrap').wrap;
router.get('', auth.checkAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
res.render('index', {
sourceId: await source_id.generateSourceId(),
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync")
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
appCss: await getAppCss(repository)
});
}));
async function getAppCss(repository) {
let css = '';
const notes = attributes.getNotesWithAttribute(repository, 'app_css');
for (const note of await notes) {
css += `/* ${note.noteId} */
${note.content}
`;
}
return css;
}
module.exports = router;

Binary file not shown.

BIN
src/scripts/Today.tar Normal file

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 77;
const APP_DB_VERSION = 78;
module.exports = {
app_version: packageJson.version,

View File

@@ -3,13 +3,18 @@
const sql = require('./sql');
const utils = require('./utils');
const sync_table = require('./sync_table');
const Repository = require('./repository');
const BUILTIN_ATTRIBUTES = [
'run_on_startup',
'frontend_startup',
'backend_startup',
'disable_versioning',
'calendar_root',
'hide_in_autocomplete'
'hide_in_autocomplete',
'exclude_from_export',
'run',
'manual_transaction_handling',
'disable_inclusion',
'app_css'
];
async function getNoteAttributeMap(noteId) {
@@ -24,9 +29,7 @@ async function getNoteIdWithAttribute(name, value) {
AND attributes.value = ?`, [name, value]);
}
async function getNotesWithAttribute(dataKey, name, value) {
const repository = new Repository(dataKey);
async function getNotesWithAttribute(repository, name, value) {
let notes;
if (value !== undefined) {
@@ -41,8 +44,8 @@ async function getNotesWithAttribute(dataKey, name, value) {
return notes;
}
async function getNoteWithAttribute(dataKey, name, value) {
const notes = getNotesWithAttribute(dataKey, name, value);
async function getNoteWithAttribute(repository, name, value) {
const notes = getNotesWithAttribute(repository, name, value);
return notes.length > 0 ? notes[0] : null;
}
@@ -59,12 +62,14 @@ async function createAttribute(noteId, name, value = "", sourceId = null) {
const now = utils.nowDate();
const attributeId = utils.newAttributeId();
const position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [noteId]);
await sql.insert("attributes", {
attributeId: attributeId,
noteId: noteId,
name: name,
value: value,
position: position,
dateModified: now,
dateCreated: now,
isDeleted: false

View File

@@ -20,6 +20,5 @@ module.exports = {
DOCUMENT_PATH,
BACKUP_DIR,
LOG_DIR,
EXPORT_DIR,
ANONYMIZED_DB_DIR
};

View File

@@ -88,7 +88,7 @@ function noteTitleIv(iv) {
return "0" + iv;
}
function noteTextIv(iv) {
function noteContentIv(iv) {
return "1" + iv;
}
@@ -97,5 +97,5 @@ module.exports = {
decrypt,
decryptString,
noteTitleIv,
noteTextIv
noteContentIv
};

View File

@@ -29,7 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
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)
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) {
if (!rootNoteId) {
rootNoteId = await getRootNoteId();
rootNoteId = await getRootCalendarNoteId();
}
const dateStr = dateTimeStr.substr(0, 10);
@@ -119,7 +119,7 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) {
}
module.exports = {
getRootNoteId,
getRootCalendarNoteId,
getYearNoteId,
getMonthNoteId,
getDateNoteId

View File

@@ -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) {
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) {
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (oldNote.type === 'file') {
return;
}
if (oldNote.isProtected) {
protected_session.decryptNote(dataKey, oldNote);
note.isProtected = false;
oldNote.isProtected = false;
}
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) {
if (newNote.detail.type === 'file') {
await loadFile(noteId, newNote, dataKey);
}
if (newNote.detail.isProtected) {
await protected_session.encryptNote(dataKey, newNote.detail);
}
@@ -289,6 +341,7 @@ async function deleteNote(noteTreeId, sourceId) {
module.exports = {
createNewNote,
createNote,
updateNote,
deleteNote,
protectNoteRecursively

View File

@@ -26,6 +26,10 @@ function getDataKey(obj) {
const protectedSessionId = getProtectedSessionId(obj);
return getDataKeyForProtectedSessionId(protectedSessionId);
}
function getDataKeyForProtectedSessionId(protectedSessionId) {
if (protectedSessionId && session.protectedSessionId === protectedSessionId) {
return session.decryptedDataKey;
}
@@ -52,7 +56,14 @@ function decryptNote(dataKey, note) {
}
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) {
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);
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) {
dataKey = getDataKey(dataKey);
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 = {
setDataKey,
getDataKey,
getDataKeyForProtectedSessionId,
isProtectedSessionAvailable,
decryptNote,
decryptNotes,

27
src/services/scheduler.js Normal file
View 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);

View File

@@ -1,24 +1,63 @@
const log = require('./log');
const sql = require('./sql');
const ScriptContext = require('./script_context');
const Repository = require('./repository');
async function executeScript(dataKey, script, params) {
log.info('Executing script: ' + script);
async function executeNote(dataKey, note) {
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 () => {
ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx));
});
// last \r\n is necessary if script contains line comment on its last line
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) {
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
@@ -29,6 +68,72 @@ function getParams(params) {
}).join(",");
}
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
if (!note.isJavaScript() && !note.isHtml() && note.type !== 'render') {
return;
}
if (!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);
}
}
if (note.isJavaScript()) {
bundle.script += `
apiContext.modules['${note.noteId}'] = {};
${root ? 'return ' : ''}await (async function(exports, module, api` + (modules.length > 0 ? ', ' : '') +
modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) {
${note.content}
})({}, apiContext.modules['${note.noteId}'], apiContext.apis['${note.noteId}']` + (modules.length > 0 ? ', ' : '') +
modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ') + `);
`;
}
else if (note.isHtml()) {
bundle.html += note.content;
}
return bundle;
}
function sanitizeVariableName(str) {
return str.replace(/[^a-z0-9_]/gim, "");
}
module.exports = {
executeScript
executeNote,
executeScript,
getScriptBundle
};

View File

@@ -1,20 +1,42 @@
const log = require('./log');
const protected_session = require('./protected_session');
const notes = require('./notes');
const sql = require('./sql');
const utils = require('./utils');
const attributes = require('./attributes');
const date_notes = require('./date_notes');
const config = require('./config');
const Repository = require('./repository');
const axios = require('axios');
function ScriptContext(noteId, dataKey) {
this.dataKey = protected_session.getDataKey(dataKey);
this.repository = new Repository(dataKey);
function ScriptContext(dataKey, startNote, allNotes) {
dataKey = protected_session.getDataKey(dataKey);
this.modules = {};
this.notes = utils.toObject(allNotes, note => [note.noteId, note]);
this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(dataKey, startNote, note)]);
}
function ScriptApi(dataKey, startNote, currentNote) {
const repository = new Repository(dataKey);
this.startNote = startNote;
this.currentNote = currentNote;
this.axios = axios;
this.utils = {
unescapeHtml: utils.unescapeHtml,
isoDateTimeStr: utils.dateStr
};
this.getInstanceName = () => config.General ? config.General.instanceName : null;
this.getNoteById = async function(noteId) {
return this.repository.getNote(noteId);
return repository.getNote(noteId);
};
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) {
@@ -23,46 +45,22 @@ function ScriptContext(noteId, dataKey) {
return notes.length > 0 ? notes[0] : null;
};
this.createNote = async function (parentNoteId, name, jsonContent, extraOptions = {}) {
const note = {
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
};
this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) {
extraOptions.dataKey = dataKey;
if (extraOptions.json) {
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;
return await notes.createNote(parentNoteId, title, content, extraOptions);
};
this.createAttribute = attributes.createAttribute;
this.updateEntity = this.repository.updateEntity;
this.updateEntity = repository.updateEntity;
this.log = function(message) {
log.info(`Script: ${message}`);
};
this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`);
this.getRootCalendarNoteId = date_notes.getRootCalendarNoteId;
this.getDateNoteId = date_notes.getDateNoteId;
this.transaction = sql.doInTransaction;
}
module.exports = ScriptContext;

View File

@@ -195,6 +195,7 @@ async function doInTransaction(func) {
await transactionPromise;
}
let ret = null;
const error = new Error(); // to capture correct stack trace in case of exception
transactionActive = true;
@@ -202,7 +203,7 @@ async function doInTransaction(func) {
try {
await beginTransaction();
await func();
ret = await func();
await commit();
@@ -223,6 +224,8 @@ async function doInTransaction(func) {
if (transactionActive) {
await transactionPromise;
}
return ret;
}
async function isDbUpToDate() {

View File

@@ -2,6 +2,7 @@
const crypto = require('crypto');
const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape');
function newNoteId() {
return randomString(12);
@@ -129,6 +130,22 @@ async function stopWatch(what, func) {
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 = {
randomSecureToken,
randomString,
@@ -153,5 +170,7 @@ module.exports = {
getDateTimeForFile,
sanitizeSql,
assertArguments,
stopWatch
stopWatch,
unescapeHtml,
toObject
};

View File

@@ -40,22 +40,20 @@
<div class="hide-toggle" style="grid-area: tree-actions;">
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
<a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action">
<img src="images/icons/file-plus.png" alt="Create new top level note"/>
</a>
<a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action"
style="background: url('images/icons/file-plus.png')"></a>
<a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action">
<img src="images/icons/list.png" alt="Collapse note tree"/>
</a>
<a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action"
style="background: url('images/icons/list.png')"></a>
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action">
<img src="images/icons/crosshair.png" alt="Scroll to current note"/>
</a>
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action"
style="background: url('images/icons/crosshair.png')"></a>
<a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action">
<img src="images/icons/search.png" alt="Search in notes"/>
</a>
<a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action"
style="background: url('images/icons/search.png')"></a>
</div>
<input type="file" id="import-upload" style="display: none" />
</div>
<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"
class="icon-action"
id="protect-button"
style="display: none;">
<img src="images/icons/lock.png" alt="Protect note"/>
</a>
style="display: none; background: url('images/icons/lock.png')"></a>
<a onclick="protected_session.unprotectNoteAndSendToServer()"
title="Unprotect note so that password will not be required to access this note in the future"
class="icon-action"
id="unprotect-button"
style="display: none;">
<img src="images/icons/unlock.png" alt="Unprotect note"/>
</a>
style="display: none; background: url('images/icons/unlock.png')"></a>
&nbsp;
@@ -167,7 +161,7 @@
</table>
</div>
<input type="file" id="file-upload" style="display: none" />
<input type="file" id="attachment-upload" style="display: none" />
</div>
<div id="attribute-list">
@@ -497,6 +491,7 @@
<script src="javascripts/drag_and_drop.js"></script>
<script src="javascripts/context_menu.js"></script>
<script src="javascripts/search_tree.js"></script>
<script src="javascripts/export.js"></script>
<!-- Note detail -->
<script src="javascripts/note_editor.js"></script>
@@ -526,5 +521,9 @@
// final form which is pretty ugly.
$("#container").show();
</script>
<style type="text/css">
<%= appCss %>
</style>
</body>
</html>