Compare commits

...

34 Commits

Author SHA1 Message Date
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
azivner
e8c52e25f0 release 0.7.0-beta 2018-02-19 23:03:30 -05:00
azivner
a149c6a105 lazy / dynamic loading of CKEditor and Code mirror 2018-02-19 22:02:03 -05:00
azivner
131af9ab12 fix attachment sync 2018-02-18 22:55:36 -05:00
azivner
aa2bbc6575 attachment download now works also in electron, added option to open the attachment 2018-02-18 22:19:07 -05:00
azivner
78e8c15786 attachment upload and download now works for browser 2018-02-18 21:28:24 -05:00
azivner
fda4146150 correct handling of inclusion of dependencies 2018-02-18 10:47:02 -05:00
azivner
ddc885066e support passing functions to the backend as parameters 2018-02-18 09:53:36 -05:00
azivner
08bc2afb49 now it's possible to add comment to the weight, closes #54 2018-02-17 11:47:22 -05:00
azivner
1d0220b03d add weight causes updating old chart instead of creating new chart, closes #53 2018-02-17 10:45:00 -05:00
azivner
3033f7cc08 attribute value is now non-null, fixes #52 2018-02-16 19:07:59 -05:00
azivner
6b9ff47c88 Merge branch 'stable' 2018-02-15 23:24:02 -05:00
azivner
cdde6a4d8e file/attachment upload, wiP 2018-02-14 23:31:20 -05:00
58 changed files with 106685 additions and 4331 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,23 @@
UPDATE attributes SET value = '' WHERE value IS NULL;
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL DEFAULT '',
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted)
SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);

View File

@@ -15,6 +15,8 @@ require('electron-debug')();
// Prevent window being garbage collected
let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() {
// Dereference the window
// For multiple windows store them in an array

72
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.6.1",
"version": "0.7.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3206,6 +3206,16 @@
"electron-localshortcut": "3.1.0"
}
},
"electron-dl": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.11.0.tgz",
"integrity": "sha512-iL9qHzzWOuL9bus+UT+P72SwrDQcFTV6QHqcbhwgqjCC9/K5jhdRzG0dIMB3TzYlk6rmApanPqh9DvWykwIH1Q==",
"requires": {
"ext-name": "5.0.0",
"pupa": "1.0.0",
"unused-filename": "1.0.0"
}
},
"electron-download": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
@@ -4374,6 +4384,23 @@
}
}
},
"ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
"integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
"requires": {
"mime-db": "1.30.0"
}
},
"ext-name": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
"integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
"requires": {
"ext-list": "2.2.2",
"sort-keys-length": "1.0.1"
}
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -5922,8 +5949,7 @@
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
"dev": true
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"is-png": {
"version": "1.1.0",
@@ -7152,6 +7178,11 @@
}
}
},
"modify-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"moment": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
@@ -7564,6 +7595,11 @@
"mimic-fn": "1.1.0"
}
},
"open": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
"integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw="
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
@@ -8391,6 +8427,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"pupa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz",
"integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y="
},
"q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -9186,11 +9227,18 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"dev": true,
"requires": {
"is-plain-obj": "1.1.0"
}
},
"sort-keys-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
"requires": {
"sort-keys": "1.1.2"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -10963,6 +11011,22 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unused-filename": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz",
"integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=",
"requires": {
"modify-filename": "1.1.0",
"path-exists": "3.0.0"
},
"dependencies": {
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
}
}
},
"unzip-response": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.6.2",
"version": "0.8.1",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -29,6 +29,7 @@
"ejs": "~2.5.7",
"electron": "^1.8.2",
"electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4",
"express": "~4.16.2",
"express-promise-wrap": "^0.2.2",
@@ -45,6 +46,7 @@
"jimp": "^0.2.28",
"moment": "^2.20.1",
"multer": "^1.3.0",
"open": "0.0.5",
"rand-token": "^0.4.0",
"request": "^2.83.0",
"request-promise": "^4.2.2",
@@ -55,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,8 +73,6 @@ require('./services/backup');
// trigger consistency checks timer
require('./services/consistency_checks');
require('./plugins/reddit');
module.exports = {
app,
sessionParser

View File

@@ -23,6 +23,11 @@ class Note extends Entity {
return this.type === "code" && this.mime === "application/json";
}
isJavaScript() {
return (this.type === "code" || this.type === "file")
&& (this.mime === "application/javascript" || this.mime === "application/x-javascript");
}
async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
@@ -39,6 +44,48 @@ 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 = ?`, [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

View File

@@ -1,5 +1,5 @@
const api = (function() {
const pluginButtonsEl = $("#plugin-buttons");
const $pluginButtons = $("#plugin-buttons");
async function activateNote(notePath) {
await noteTree.activateNode(notePath);
@@ -10,12 +10,12 @@ const api = (function() {
button.attr('id', buttonId);
pluginButtonsEl.append(button);
$pluginButtons.append(button);
}
return {
addButtonToToolbar,
activateNote
activateNote,
getInstanceName: noteTree.getInstanceName
}
})();

View File

@@ -1,7 +1,7 @@
"use strict";
const contextMenu = (function() {
const treeEl = $("#tree");
const $tree = $("#tree");
let clipboardIds = [];
let clipboardMode = null;
@@ -85,16 +85,19 @@ 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) => {
const node = $.ui.fancytree.getNode(ui.target);
// Modify menu entries depending on node status
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
// Activate node on right-click
node.setActive();
@@ -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

@@ -17,26 +17,34 @@ const sqlConsole = (function() {
width: $(window).width(),
height: $(window).height(),
open: function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.focus();
initEditor();
}
});
}
async function initEditor() {
if (!codeEditor) {
await requireLibrary(CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
}
codeEditor.focus();
}
async function execute() {
const sqlQuery = codeEditor.getValue();

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,22 +114,32 @@ $.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);
results.push(item);
if (results.length > 100) {
break;
}
if (results.length > 100) {
break;
}
}
@@ -213,4 +223,26 @@ if (isElectron()) {
noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
}
}
function uploadAttachment() {
$("#attachment-upload").trigger('click');
}
$("#attachment-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
const resp = await $.ajax({
url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await noteTree.reload();
await noteTree.activateNode(resp.noteId);
});

View File

@@ -41,11 +41,11 @@ const link = (function() {
function goToLink(e) {
e.preventDefault();
const linkEl = $(e.target);
let notePath = linkEl.attr("note-path");
const $link = $(e.target);
let notePath = $link.attr("note-path");
if (!notePath) {
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) {
return;

View File

@@ -1,7 +1,7 @@
"use strict";
const messaging = (function() {
const changesToPushCountEl = $("#changes-to-push-count");
const $changesToPushCount = $("#changes-to-push-count");
function logError(message) {
console.log(now(), message); // needs to be separate from .trace()
@@ -52,7 +52,7 @@ const messaging = (function() {
// we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well
changesToPushCountEl.html(message.changesToPushCount);
$changesToPushCount.html(message.changesToPushCount);
}
else if (message.type === 'sync-hash-check-failed') {
showError("Sync check failed!", 60000);
@@ -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

@@ -1,16 +1,24 @@
"use strict";
const noteEditor = (function() {
const noteTitleEl = $("#note-title");
const noteDetailEl = $('#note-detail');
const noteDetailCodeEl = $('#note-detail-code');
const noteDetailRenderEl = $('#note-detail-render');
const protectButton = $("#protect-button");
const unprotectButton = $("#unprotect-button");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const noteIdDisplayEl = $("#note-id-display");
const attributeListEl = $("#attribute-list");
const attributeListInnerEl = $("#attribute-list-inner");
const $noteTitle = $("#note-title");
const $noteDetail = $('#note-detail');
const $noteDetailCode = $('#note-detail-code');
const $noteDetailRender = $('#note-detail-render');
const $noteDetailAttachment = $('#note-detail-attachment');
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $attributeList = $("#attribute-list");
const $attributeListInner = $("#attribute-list-inner");
const $attachmentFileName = $("#attachment-filename");
const $attachmentFileType = $("#attachment-filetype");
const $attachmentFileSize = $("#attachment-filesize");
const $attachmentDownload = $("#attachment-download");
const $attachmentOpen = $("#attachment-open");
let editor = null;
let codeEditor = null;
@@ -80,14 +88,14 @@ const noteEditor = (function() {
else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue();
}
else if (note.detail.type === 'render') {
else if (note.detail.type === 'render' || note.detail.type === 'file') {
// nothing
}
else {
throwError("Unrecognized type: " + note.detail.type);
}
const title = noteTitleEl.val();
const title = $noteTitle.val();
note.detail.title = title;
@@ -105,9 +113,9 @@ const noteEditor = (function() {
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.detail.isProtected;
noteDetailWrapperEl.toggleClass("protected", isProtected);
protectButton.toggle(!isProtected);
unprotectButton.toggle(isProtected);
$noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected);
}
let isNewNoteCreated = false;
@@ -116,19 +124,46 @@ const noteEditor = (function() {
isNewNoteCreated = true;
}
function setContent(content) {
async function setContent(content) {
if (currentNote.detail.type === 'text') {
if (!editor) {
await requireLibrary(CKEDITOR);
editor = await BalloonEditor.create($noteDetail[0], {});
editor.document.on('change', noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>");
noteDetailEl.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
$noteDetail.show();
}
else if (currentNote.detail.type === 'code') {
noteDetailEl.hide();
noteDetailCodeEl.show();
noteDetailRenderEl.html('').hide();
if (!codeEditor) {
await requireLibrary(CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false },
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true
});
codeEditor.on('change', noteChanged);
}
$noteDetailCode.show();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content);
@@ -139,6 +174,8 @@ const noteEditor = (function() {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
codeEditor.refresh();
}
}
@@ -148,10 +185,10 @@ const noteEditor = (function() {
if (isNewNoteCreated) {
isNewNoteCreated = false;
noteTitleEl.focus().select();
$noteTitle.focus().select();
}
noteIdDisplayEl.html(noteId);
$noteIdDisplay.html(noteId);
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
@@ -163,26 +200,36 @@ const noteEditor = (function() {
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protected_session.ensureDialogIsClosed();
noteDetailWrapperEl.show();
$noteDetailWrapper.show();
noteChangeDisabled = true;
noteTitleEl.val(currentNote.detail.title);
$noteTitle.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime);
$noteDetail.hide();
$noteDetailCode.hide();
$noteDetailRender.html('').hide();
$noteDetailAttachment.hide();
if (currentNote.detail.type === 'render') {
noteDetailEl.hide();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').show();
$noteDetailRender.show();
const subTree = await server.get('script/subtree/' + getCurrentNoteId());
noteDetailRenderEl.html(subTree);
$noteDetailRender.html(subTree);
}
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
$attachmentFileName.text(currentNote.attributes.original_file_name);
$attachmentFileSize.text(currentNote.attributes.file_size + " bytes");
$attachmentFileType.text(currentNote.detail.mime);
}
else {
setContent(currentNote.detail.content);
await setContent(currentNote.detail.content);
}
noteChangeDisabled = false;
@@ -191,7 +238,7 @@ const noteEditor = (function() {
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0);
$noteDetailWrapper.scrollTop(0);
loadAttributeList();
}
@@ -201,17 +248,17 @@ const noteEditor = (function() {
const attributes = await server.get('notes/' + noteId + '/attributes');
attributeListInnerEl.html('');
$attributeListInner.html('');
if (attributes.length > 0) {
for (const attr of attributes) {
attributeListInnerEl.append(formatAttribute(attr) + " ");
$attributeListInner.append(formatAttribute(attr) + " ");
}
attributeListEl.show();
$attributeList.show();
}
else {
attributeListEl.hide();
$attributeList.hide();
}
}
@@ -227,12 +274,12 @@ const noteEditor = (function() {
const note = getCurrentNote();
if (note.detail.type === 'text') {
noteDetailEl.focus();
$noteDetail.focus();
}
else if (note.detail.type === 'code') {
codeEditor.focus();
}
else if (note.detail.type === 'render') {
else if (note.detail.type === 'render' || note.detail.type === 'file') {
// do nothing
}
else {
@@ -257,45 +304,36 @@ const noteEditor = (function() {
}
}
$attachmentDownload.click(() => download(getAttachmentUrl()));
$attachmentOpen.click(() => {
if (isElectron()) {
const open = require("open");
open(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
function getAttachmentUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return getHost() + "/api/attachments/download/" + getCurrentNoteId()
+ "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
}
$(document).ready(() => {
noteTitleEl.on('input', () => {
$noteTitle.on('input', () => {
noteChanged();
const title = noteTitleEl.val();
const title = $noteTitle.val();
noteTree.setNoteTitle(getCurrentNoteId(), title);
});
BalloonEditor
.create(document.querySelector('#note-detail'), {
})
.then(edit => {
editor = edit;
editor.document.on('change', noteChanged);
})
.catch(error => {
console.error(error);
});
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.on('change', noteChanged);
// so that tab jumps from note title (which has tabindex 1)
noteDetailEl.attr("tabindex", 2);
$noteDetail.attr("tabindex", 2);
});
$(document).bind('keydown', "ctrl+return", executeCurrentNote);

View File

@@ -1,9 +1,11 @@
"use strict";
const noteTree = (function() {
const treeEl = $("#tree");
const parentListEl = $("#parent-list");
const parentListListEl = $("#parent-list-inner");
const $tree = $("#tree");
const $parentList = $("#parent-list");
const $parentListList = $("#parent-list-inner");
let instanceName = null; // should have better place
let startNotePath = null;
let notesTreeMap = {};
@@ -52,7 +54,7 @@ const noteTree = (function() {
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
function getCurrentNode() {
return treeEl.fancytree("getActiveNode");
return $tree.fancytree("getActiveNode");
}
function getCurrentNotePath() {
@@ -155,6 +157,9 @@ const noteTree = (function() {
if (note.type === 'code') {
extraClasses.push("code");
}
else if (note.type === 'file') {
extraClasses.push('attachment');
}
return extraClasses.join(" ");
}
@@ -314,11 +319,11 @@ const noteTree = (function() {
}
if (parents.length <= 1) {
parentListEl.hide();
$parentList.hide();
}
else {
parentListEl.show();
parentListListEl.empty();
$parentList.show();
$parentListList.empty();
for (const parentNoteId of parents) {
const parentNotePath = getSomeNotePath(parentNoteId);
@@ -335,7 +340,7 @@ const noteTree = (function() {
item = link.createNoteLink(notePath, title);
}
parentListListEl.append($("<li/>").append(item));
$parentListList.append($("<li/>").append(item));
}
}
}
@@ -543,7 +548,7 @@ const noteTree = (function() {
}
};
treeEl.fancytree({
$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"],
@@ -624,11 +629,11 @@ const noteTree = (function() {
}
});
treeEl.contextmenu(contextMenu.contextMenuSettings);
$tree.contextmenu(contextMenu.contextMenuSettings);
}
function getTree() {
return treeEl.fancytree('getTree');
return $tree.fancytree('getTree');
}
async function reload() {
@@ -645,6 +650,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();
@@ -663,7 +669,7 @@ const noteTree = (function() {
function collapseTree(node = null) {
if (!node) {
node = treeEl.fancytree("getRootNode");
node = $tree.fancytree("getRootNode");
}
node.setExpanded(false);
@@ -710,6 +716,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]) {
@@ -744,7 +753,7 @@ const noteTree = (function() {
}
async function createNewTopLevelNote() {
const rootNode = treeEl.fancytree("getRootNode");
const rootNode = $tree.fancytree("getRootNode");
await createNote(rootNode, "root", "into");
}
@@ -820,6 +829,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 +907,7 @@ const noteTree = (function() {
setParentChildRelation,
getSelectedNodes,
sortAlphabetically,
noteExists
noteExists,
getInstanceName
};
})();

View File

@@ -1,7 +1,7 @@
"use strict";
const noteType = (function() {
const executeScriptButton = $("#execute-script-button");
const $executeScriptButton = $("#execute-script-button");
const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() {
@@ -65,11 +65,18 @@ const noteType = (function() {
else if (type === 'render') {
return 'Render HTML note';
}
else if (type === 'file') {
return 'Attachment';
}
else {
throwError('Unrecognized type: ' + type);
}
};
this.isDisabled = function() {
return self.type() === "file";
};
async function save() {
const note = noteEditor.getCurrentNote();
@@ -114,7 +121,7 @@ const noteType = (function() {
};
this.updateExecuteScriptButtonVisibility = function() {
executeScriptButton.toggle(self.mime() === 'application/javascript');
$executeScriptButton.toggle(self.mime() === 'application/javascript');
}
}

View File

@@ -1,10 +1,10 @@
"use strict";
const protected_session = (function() {
const dialogEl = $("#protected-session-password-dialog");
const passwordFormEl = $("#protected-session-password-form");
const passwordEl = $("#protected-session-password");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const $dialog = $("#protected-session-password-dialog");
const $passwordForm = $("#protected-session-password-form");
const $password = $("#protected-session-password");
const $noteDetailWrapper = $("#note-detail-wrapper");
let protectedSessionDeferred = null;
let lastProtectedSessionOperationDate = null;
@@ -25,9 +25,11 @@ const protected_session = (function() {
if (requireProtectedSession && !isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd;
noteDetailWrapperEl.hide();
if (noteTree.getCurrentNode().data.isProtected) {
$noteDetailWrapper.hide();
}
dialogEl.dialog({
$dialog.dialog({
modal: modal,
width: 400,
open: () => {
@@ -46,8 +48,8 @@ const protected_session = (function() {
}
async function setupProtectedSession() {
const password = passwordEl.val();
passwordEl.val("");
const password = $password.val();
$password.val("");
const response = await enterProtectedSession(password);
@@ -58,15 +60,15 @@ const protected_session = (function() {
protectedSessionId = response.protectedSessionId;
dialogEl.dialog("close");
$dialog.dialog("close");
noteEditor.reload();
noteTree.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed(dialogEl, passwordEl);
ensureDialogIsClosed($dialog, $password);
noteDetailWrapperEl.show();
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
@@ -77,11 +79,11 @@ const protected_session = (function() {
function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened
try {
dialogEl.dialog('close');
$dialog.dialog('close');
}
catch (e) {}
passwordEl.val('');
$password.val('');
}
async function enterProtectedSession(password) {
@@ -155,7 +157,7 @@ const protected_session = (function() {
noteEditor.reload();
}
passwordFormEl.submit(() => {
$passwordForm.submit(() => {
setupProtectedSession();
return false;

View File

@@ -1,40 +1,40 @@
"use strict";
const searchTree = (function() {
const treeEl = $("#tree");
const searchInputEl = $("input[name='search-text']");
const resetSearchButton = $("button#reset-search-button");
const searchBoxEl = $("#search-box");
const $tree = $("#tree");
const $searchInput = $("input[name='search-text']");
const $resetSearchButton = $("button#reset-search-button");
const $searchBox = $("#search-box");
resetSearchButton.click(resetSearch);
$resetSearchButton.click(resetSearch);
function toggleSearch() {
if (searchBoxEl.is(":hidden")) {
searchBoxEl.show();
searchInputEl.focus();
if ($searchBox.is(":hidden")) {
$searchBox.show();
$searchInput.focus();
}
else {
resetSearch();
searchBoxEl.hide();
$searchBox.hide();
}
}
function resetSearch() {
searchInputEl.val("");
$searchInput.val("");
getTree().clearFilter();
}
function getTree() {
return treeEl.fancytree('getTree');
return $tree.fancytree('getTree');
}
searchInputEl.keyup(async e => {
const searchText = searchInputEl.val();
$searchInput.keyup(async e => {
const searchText = $searchInput.val();
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
resetSearchButton.click();
$resetSearchButton.click();
return;
}

View File

@@ -31,16 +31,38 @@ const server = (function() {
return await call('DELETE', url);
}
function prepareParams(params) {
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "function") {
return "!@#Function: " + p.toString();
}
else {
return p;
}
});
}
async function exec(params, script) {
if (typeof script === "function") {
script = script.toString();
}
const ret = await post('script/exec', { script: script, params: params });
const ret = await post('script/exec', { script: script, params: prepareParams(params) });
return ret.executionResult;
}
async function setJob(opts) {
opts.job = opts.job.toString();
opts.params = prepareParams(opts.params);
await post('script/job', opts);
}
let i = 1;
const reqResolves = {};
@@ -105,6 +127,8 @@ const server = (function() {
put,
remove,
exec,
setJob,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders
}

View File

@@ -1,14 +1,14 @@
"use strict";
const treeUtils = (function() {
const treeEl = $("#tree");
const $tree = $("#tree");
function getParentProtectedStatus(node) {
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
}
function getNodeByKey(key) {
return treeEl.fancytree('getNodeByKey', key);
return $tree.fancytree('getNodeByKey', key);
}
function getNoteIdFromNotePath(notePath) {

View File

@@ -116,8 +116,7 @@ async function stopWatch(what, func) {
}
function executeScript(script) {
// last \r\n is necessary if script contains line comment on its last line
eval("(async function() {" + script + "\r\n})()");
eval(script);
}
function formatValueWithWhitespace(val) {
@@ -132,4 +131,78 @@ function formatAttribute(attr) {
}
return str;
}
const CKEDITOR = { "js": ["libraries/ckeditor/ckeditor.js"] };
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"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/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],
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));
}
if (library.js) {
for (const scriptUrl of library.js) {
await requireScript(scriptUrl);
}
}
}
const dynamicallyLoadedScripts = [];
async function requireScript(url) {
if (!dynamicallyLoadedScripts.includes(url)) {
dynamicallyLoadedScripts.push(url);
return await $.ajax({
url: url,
dataType: "script",
cache: true
})
}
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(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;
}
}

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

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

File diff suppressed because one or more lines are too long

View File

@@ -72,6 +72,11 @@ 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.protected > span.fancytree-icon {
filter: drop-shadow(2px 2px 2px black);
}
@@ -268,4 +273,9 @@ div.ui-tooltip {
#attribute-list button {
padding: 2px;
margin-right: 5px;
}
#attachment-table th, #attachment-table td {
padding: 10px;
font-size: large;
}

View File

@@ -0,0 +1,74 @@
"use strict";
const express = require('express');
const router = express.Router();
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;
router.post('/upload/: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 originalName = file.originalname;
const size = file.size;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
await sql.doInTransaction(async () => {
const noteId = (await notes.createNewNote(parentNoteId, {
title: originalName,
content: file.buffer,
target: 'into',
isProtected: false,
type: 'file',
mime: file.mimetype
}, req, sourceId)).noteId;
await attributes.createAttribute(noteId, "original_file_name", originalName, sourceId);
await attributes.createAttribute(noteId, "file_size", size, sourceId);
res.send({
noteId: noteId
});
});
}));
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 ${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 fileName = attributeMap.original_file_name ? attributeMap.original_file_name : note.title;
res.setHeader('Content-Disposition', 'attachment; filename=' + fileName);
res.setHeader('Content-Type', note.mime);
res.send(note.content);
}));
module.exports = router;

View File

@@ -2,56 +2,72 @@
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 attributes = require('../../services/attributes');
const html = require('html');
const auth = require('../../services/auth');
const wrap = require('express-promise-wrap').wrap;
const tar = require('tar-stream');
const sanitize = require("sanitize-filename");
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 noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]);
await exportNote(noteTreeId, completeExportDir);
const pack = tar.pack();
res.send({});
const name = await exportNote(noteTreeId, '', pack);
pack.finalize();
res.setHeader('Content-Disposition', 'attachment; filename="' + name + '.tar"');
res.setHeader('Content-Type', 'application/tar');
pack.pipe(res);
}));
async function exportNote(noteTreeId, dir) {
async function exportNote(noteTreeId, directory, pack) {
const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]);
const pos = (noteTree.notePosition + '').padStart(4, '0');
if (note.isProtected) {
return;
}
fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2}));
const metadata = await getMetadata(note);
if ('exclude_from_export' in metadata.attributes) {
return;
}
const metadataJson = JSON.stringify(metadata, null, '\t');
const childFileName = directory + sanitize(note.title);
pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({ name: childFileName + ".dat", size: content.length }, content);
const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]);
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);
}
}
return childFileName;
}
async function getMetadata(note) {
return {
title: note.title,
type: note.type,
mime: note.mime,
attributes: await attributes.getNoteAttributeMap(note.noteId)
};
}
module.exports = router;

View File

@@ -2,104 +2,128 @@
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 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;
}
const fileList = fs.readdirSync(dir);
for (const file of fileList) {
const path = dir + '/' + file;
if (fs.lstatSync(path).isDirectory()) {
continue;
async function importNotes(files, parentNoteId, sourceId) {
for (const file of files) {
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,
attributes: file.meta.attributes,
sourceId: sourceId
});
await sync_table.addNoteTreeSync(noteTreeId);
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

@@ -5,6 +5,7 @@ const router = express.Router();
const auth = require('../../services/auth');
const sql = require('../../services/sql');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const log = require('../../services/log');
const utils = require('../../services/utils');
const protected_session = require('../../services/protected_session');
@@ -25,8 +26,19 @@ router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
protected_session.decryptNote(req, detail);
let attributeMap = null;
if (detail.type === 'file') {
// no need to transfer attachment payload for this request
detail.content = null;
// attributes contain important attachment metadata - filename and size
attributeMap = await attributes.getNoteAttributeMap(noteId);
}
res.send({
detail: detail
detail: detail,
attributes: attributeMap
});
}));

View File

@@ -17,6 +17,12 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
});
}));
router.post('/job', auth.checkApiAuth, wrap(async (req, res, next) => {
await script.setJob(req.body);
res.send({});
}));
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
const repository = new Repository(req);
@@ -31,32 +37,36 @@ router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
}));
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
const noteId = req.params.noteId;
const repository = new Repository(req);
const noteScript = (await repository.getNote(noteId)).content;
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
res.send(subTreeScripts + noteScript);
res.send(await getNoteWithSubtreeScript(noteId, repository));
}));
async function getNoteWithSubtreeScript(noteId, repository) {
const noteScript = (await repository.getNote(noteId)).content;
const note = await repository.getNote(noteId);
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
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) {
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]);
AND note_tree.parentNoteId = ? AND (notes.type = 'code' OR notes.type = 'file')
AND (notes.mime = 'application/javascript'
OR notes.mime = 'application/x-javascript'
OR notes.mime = 'text/html')`, [parentId]);
let script = "\r\n";
@@ -69,7 +79,7 @@ async function getSubTreeScripts(parentId, includedNoteIds, repository) {
script += await getSubTreeScripts(child.noteId, includedNoteIds, repository);
if (child.mime === 'application/javascript') {
if (!isJavaScript && child.isJavaScript()) {
child.content = '<script>' + child.content + '</script>';
}

View File

@@ -79,9 +79,12 @@ router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
sync.serializeNoteContentBuffer(entity);
res.send({
entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId])
entity: entity
});
}));

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

@@ -29,6 +29,7 @@ const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender');
const attachmentsRoute = require('./api/attachments');
function register(app) {
app.use('/', indexRoute);
@@ -61,6 +62,7 @@ function register(app) {
app.use('/api/images', imageRoute);
app.use('/api/script', scriptRoute);
app.use('/api/sender', senderRoute);
app.use('/api/attachments', attachmentsRoute);
}
module.exports = {

Binary file not shown.

View File

@@ -1,4 +1,4 @@
<form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;">
<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;" />
@@ -7,6 +7,10 @@
<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>
@@ -16,84 +20,127 @@
<canvas id="canvas"></canvas>
<script>
(async function() {
const dateEl = $("#weight-date");
const weightEl = $("#weight");
(async function() {
const $form = $("#weight-form");
const $date = $("#weight-date");
const $weight = $("#weight");
const $comment = $("#comment");
let chart;
dateEl.datepicker();
dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd');
dateEl.datepicker('setDate', new Date());
$date.datepicker();
$date.datepicker('option', 'dateFormat', 'yy-mm-dd');
$date.datepicker('setDate', new Date());
async function saveWeight() {
await server.exec([dateEl.val(), weightEl.val()], async (date, weight) => {
const dataNote = await this.getNoteWithAttribute('date_data', 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 (dataNote) {
dataNote.jsonContent.weight = weight;
await this.updateEntity(dataNote);
}
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
if (comment) {
dataNote.jsonContent.weight_comment = comment;
}
});
}
});
showMessage("Weight has been saved");
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();
}
async function drawChart() {
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
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(row => row.date),
datasets: [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}]
}
});
}
$("#weight-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 = 76;
const APP_DB_VERSION = 77;
module.exports = {
app_version: packageJson.version,

View File

@@ -9,11 +9,12 @@ const BUILTIN_ATTRIBUTES = [
'run_on_startup',
'disable_versioning',
'calendar_root',
'hide_in_autocomplete'
'hide_in_autocomplete',
'exclude_from_export'
];
async function getNoteAttributeMap(noteId) {
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]);
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ? AND isDeleted = 0`, [noteId]);
}
async function getNoteIdWithAttribute(name, value) {
@@ -52,7 +53,11 @@ async function getNoteIdsWithAttribute(name) {
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]);
}
async function createAttribute(noteId, name, value = null, sourceId = null) {
async function createAttribute(noteId, name, value = "", sourceId = null) {
if (value === null || value === undefined) {
value = "";
}
const now = utils.nowDate();
const attributeId = utils.newAttributeId();

View File

@@ -214,7 +214,7 @@ async function runAllChecks() {
FROM
notes
WHERE
type != 'text' AND type != 'code' AND type != 'render'`,
type != 'text' AND type != 'code' AND type != 'render' AND type != 'file'`,
"Note has invalid type", errorList);
await runSyncRowChecks("notes", "noteId", errorList);

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,

View File

@@ -1,27 +1,78 @@
const log = require('./log');
const sql = require('./sql');
const ScriptContext = require('./script_context');
async function executeScript(dataKey, script, params) {
log.info('Executing script: ' + script);
const ctx = new ScriptContext(dataKey);
const paramsStr = getParams(params);
let ret;
return await sql.doInTransaction(async () => execute(ctx, script, paramsStr));
}
await sql.doInTransaction(async () => {
ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx));
});
async function execute(ctx, script, paramsStr) {
return await (function() { return eval(`const api = this; (${script})(${paramsStr})`); }.call(ctx));
}
return ret;
const timeouts = {};
const intervals = {};
function clearExistingJob(name) {
if (timeouts[name]) {
clearTimeout(timeouts[name]);
delete timeouts[name];
}
if (intervals[name]) {
clearInterval(intervals[name]);
delete intervals[name];
}
}
async function executeJob(script, params, manualTransactionHandling) {
const ctx = new ScriptContext();
const paramsStr = getParams(params);
if (manualTransactionHandling) {
return await execute(ctx, script, paramsStr);
}
else {
return await sql.doInTransaction(async () => execute(ctx, script, paramsStr));
}
}
async function setJob(opts) {
const { name, runEveryMs, initialRunAfterMs } = opts;
clearExistingJob(name);
const jobFunc = () => executeJob(opts.job, opts.params, opts.manualTransactionHandling);
if (runEveryMs && runEveryMs > 0) {
intervals[name] = setInterval(jobFunc, runEveryMs);
}
if (initialRunAfterMs && initialRunAfterMs > 0) {
timeouts[name] = setTimeout(jobFunc, initialRunAfterMs);
}
}
function getParams(params) {
return params.map(p => JSON.stringify(p)).join(",");
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
}
else {
return JSON.stringify(p);
}
}).join(",");
}
module.exports = {
executeScript
executeScript,
setJob
};

View File

@@ -1,20 +1,33 @@
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) {
dataKey = protected_session.getDataKey(dataKey);
const repository = new Repository(dataKey);
this.axios = axios;
this.utils = {
unescapeHtml: utils.unescapeHtml,
isoDateTimeStr: utils.dateStr
};
this.getInstanceName = () => config.General ? config.General.instanceName : null;
this.getNoteById = async function(noteId) {
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(dataKey, attrName, attrValue);
};
this.getNoteWithAttribute = async function (attrName, attrValue) {
@@ -23,46 +36,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: ${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

@@ -204,6 +204,8 @@ async function pushEntity(sync, syncContext) {
if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
}
else if (sync.entityName === 'note_tree') {
entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]);
@@ -258,6 +260,12 @@ async function pushEntity(sync, syncContext) {
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
}
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
@@ -350,5 +358,6 @@ sql.dbReady.then(() => {
});
module.exports = {
sync
sync,
serializeNoteContentBuffer
};

View File

@@ -1,10 +1,17 @@
const sql = require('./sql');
const log = require('./log');
const eventLog = require('./event_log');
const notes = require('./notes');
const sync_table = require('./sync_table');
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary');
}
}
async function updateNote(entity, sourceId) {
deserializeNoteContentBuffer(entity);
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
if (!origNote || origNote.dateModified <= entity.dateModified) {

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,10 @@ async function stopWatch(what, func) {
return ret;
}
function unescapeHtml(str) {
return unescape(str);
}
module.exports = {
randomSecureToken,
randomString,
@@ -153,5 +158,6 @@ module.exports = {
getDateTimeForFile,
sanitizeSql,
assertArguments,
stopWatch
stopWatch,
unescapeHtml
};

View File

@@ -56,6 +56,8 @@
<img src="images/icons/search.png" alt="Search in notes"/>
</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;">
@@ -105,7 +107,7 @@
onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button>
<div class="dropdown" id="note-type">
<button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
<button data-bind="disable: isDisabled()" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
</button>
@@ -130,6 +132,7 @@
<li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li>
<li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li>
<li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li>
<li><a onclick="uploadAttachment();">Upload attachment</a></li>
</ul>
</div>
</div>
@@ -141,6 +144,32 @@
<div id="note-detail-code"></div>
<div id="note-detail-render"></div>
<div id="note-detail-attachment">
<table id="attachment-table">
<tr>
<th>File name:</th>
<td id="attachment-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="attachment-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="attachment-filesize"></td>
</tr>
<tr>
<td>
<button id="attachment-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="attachment-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="attachment-upload" style="display: none" />
</div>
<div id="attribute-list">
@@ -449,8 +478,6 @@
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all.min.js"></script>
<script src="libraries/ckeditor/ckeditor.js"></script>
<script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
@@ -458,15 +485,6 @@
<script src="libraries/knockout.min.js"></script>
<script src="libraries/codemirror/codemirror.js"></script>
<link rel="stylesheet" href="libraries/codemirror/codemirror.css">
<script src="libraries/codemirror/addon/mode/loadmode.js"></script>
<script src="libraries/codemirror/addon/fold/xml-fold.js"></script>
<script src="libraries/codemirror/addon/edit/matchbrackets.js"></script>
<script src="libraries/codemirror/addon/edit/matchtags.js"></script>
<script src="libraries/codemirror/addon/search/match-highlighter.js"></script>
<script src="libraries/codemirror/mode/meta.js"></script>
<link href="stylesheets/style.css" rel="stylesheet">
<script src="javascripts/utils.js"></script>
@@ -481,6 +499,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>