Compare commits

...

60 Commits

Author SHA1 Message Date
zadam
b0c0c5f56b release 0.34.0-beta 2019-07-21 21:57:35 +02:00
zadam
6d5f8e0562 added web clipper to readme 2019-07-20 14:27:36 +02:00
zadam
04633bdf3a set clip type from the incoming param 2019-07-19 21:03:47 +02:00
zadam
97219aa12e removed unused clipper image API (migrated to clippings) 2019-07-18 22:35:16 +02:00
zadam
e825abf893 Merge remote-tracking branch 'origin/master' 2019-07-18 20:20:45 +02:00
zadam
63b655cff4 package updates 2019-07-18 20:20:38 +02:00
Yevhen Kolomeiko
0524942d11 fix typo (#592) 2019-07-12 07:16:37 +02:00
zadam
b5d75f183a update to ckeditor 12.3.1 2019-07-11 22:30:40 +02:00
zadam
706fc647ff Merge branch 'stable'
# Conflicts:
#	package.json
2019-07-11 22:27:27 +02:00
zadam
13e9f9f9e7 release 0.33.7 2019-07-11 20:55:56 +02:00
zadam
a76dcb44ae improvements in the exported file extensions 2019-07-11 20:54:57 +02:00
zadam
b9373806cf fix doubling of extension 2019-07-11 20:40:40 +02:00
zadam
9de2927304 image import/export related fixes 2019-07-10 23:01:30 +02:00
zadam
c3e1126489 backported image fixes 2019-07-10 20:38:27 +02:00
zadam
3413c9ed64 backport notePath handling fixes from master 2019-07-10 20:35:01 +02:00
Xavier NUNN
dcebcb0e73 Add host option (#588)
* Added option to configure host

* Updated sample config
2019-07-09 22:50:20 +02:00
zadam
3d7a5f20e7 make it again possible to open notes through URL hash 2019-07-09 22:12:05 +02:00
zadam
6a99af64a8 added clipper protocol version 2019-07-07 22:27:06 +02:00
zadam
7d57961ab2 make clipper api authenticated for server and unauthenticated for local electron 2019-07-07 13:12:40 +02:00
zadam
95a773e5c9 clipper doesn't open new tab if the note is already loaded in existing tab 2019-07-07 11:15:55 +02:00
zadam
a912b2f23d choose port dynamically from range based on environment 2019-07-07 10:49:34 +02:00
zadam
36b581489c saving selections adds to the existing date note instead of creating new one each time 2019-07-06 23:54:48 +02:00
zadam
976684a3a8 save note in clipper 2019-07-06 16:48:06 +02:00
zadam
093dfb4a39 Merge branch 'stable' 2019-07-06 13:14:46 +02:00
zadam
ddf381f92d fixed duplicated notes after creating into a folder which wasn't yet loaded 2019-07-06 13:14:33 +02:00
zadam
2b44f3bc76 fixed websocket reconnection 2019-07-06 12:03:51 +02:00
zadam
7b1fdfabf8 upgrade ckeditor to 12.3.0 2019-07-04 19:53:10 +02:00
zadam
070e8d9647 Merge branch 'stable' 2019-07-03 20:51:34 +02:00
zadam
bf3360572a nest code editor instance to avoid visibility issues 2019-07-03 20:37:59 +02:00
zadam
e5036318af fix enter on title to the code editor 2019-07-03 20:29:55 +02:00
zadam
6d2394a9da release 0.33.6 2019-07-02 22:26:05 +02:00
zadam
427a266c57 Merge branch 'stable' 2019-07-02 21:56:23 +02:00
zadam
196264b8c2 use bootstrap modal to confirm note deletion which fixes #582 2019-07-02 21:54:37 +02:00
zadam
afe24866f0 some debug logging for duplicated nodes 2019-07-02 20:35:06 +02:00
zadam
d18a20cc06 fix focus issue from title to the text content with tab/enter 2019-07-02 20:28:57 +02:00
zadam
e94669de03 Merge branch 'stable' 2019-07-01 21:36:02 +02:00
zadam
9c91b0459e release 0.33.5 2019-06-30 21:47:04 +02:00
zadam
b161db064e choose only desired context menu items 2019-06-30 21:34:19 +02:00
zadam
ec4abe0d81 Merge branch 'stable' 2019-06-30 20:15:37 +02:00
zadam
af21dd4463 fix mobile text editor display 2019-06-30 20:14:57 +02:00
zadam
ef46727870 Merge branch 'stable' 2019-06-30 19:58:56 +02:00
zadam
1ea0d283de fix text instance sometimes remaining displayed when switching to e.g. image 2019-06-30 19:41:26 +02:00
zadam
ed380e09c9 context menu WIP 2019-06-30 18:56:46 +02:00
zadam
b5daa83d69 upgrades + removal of unused test 2019-06-30 11:22:10 +02:00
zadam
c4b957427d implement print, closes #581 2019-06-29 22:57:47 +02:00
zadam
2f3b256272 Merge branch 'stable' 2019-06-29 20:37:51 +02:00
zadam
6e3d8472e1 avoid duplicate key error 2019-06-28 21:50:15 +02:00
zadam
2a9f36a027 fix activating parent note after delete 2019-06-27 22:58:04 +02:00
zadam
cf3726289c attempt to fix the duplicate issue 2019-06-27 21:24:25 +02:00
zadam
c8e4a5c9e7 release 0.33.4 2019-06-26 21:20:30 +02:00
zadam
3851bedb57 Merge branch 't34' 2019-06-26 21:11:27 +02:00
zadam
a3951f1cce make sure autocomplete is closed when navigating to note 2019-06-26 21:08:54 +02:00
zadam
7c77ae758b fix frontend reload again 2019-06-26 20:49:17 +02:00
zadam
174128447b token auth to /login 2019-06-23 21:22:08 +02:00
zadam
5d213eea7e basic webp support 2019-06-23 15:22:05 +02:00
zadam
f45e25172b opening links from the clipper 2019-06-23 13:25:00 +02:00
zadam
ec87856ef4 save image 2019-06-23 12:16:26 +02:00
zadam
6feb7ad1d5 selection clipping now supports images 2019-06-23 11:25:15 +02:00
zadam
6833e84d55 fix pngquant for linux-x64 2019-06-23 09:10:06 +02:00
zadam
154a575701 cleanup + context menu clip now works 2019-06-22 19:49:48 +02:00
50 changed files with 1919 additions and 809 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/workspace.xml
# Datasource local storage ignored files
/dataSources.local.xml

View File

@@ -1,4 +1,4 @@
FROM node:12.4.0-alpine FROM node:12.6.0-alpine
# Create app directory # Create app directory
WORKDIR /usr/src/app WORKDIR /usr/src/app

View File

@@ -21,6 +21,7 @@ Trilium Notes is a hierarchical note taking application with focus on building l
* Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets * Touch optimized [mobile frontend](https://github.com/zadam/trilium/wiki/Mobile-frontend) for smartphones and tablets
* [Night theme](https://github.com/zadam/trilium/wiki/Themes) * [Night theme](https://github.com/zadam/trilium/wiki/Themes)
* [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown) * [Evernote](https://github.com/zadam/trilium/wiki/Evernote-import) and [Markdown import & export](https://github.com/zadam/trilium/wiki/Markdown)
* [Web Clipper](https://github.com/zadam/trilium/wiki/Web-clipper) for easy saving of web content
## Builds ## Builds

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=12.4.0 NODE_VERSION=12.6.0
rm -r $PKG_DIR rm -r $PKG_DIR
mkdir $PKG_DIR mkdir $PKG_DIR
@@ -30,11 +30,11 @@ rm -r ./node_modules/sqlite3/lib/binding/*
cp -r ../../bin/deps/linux-x64/sqlite/node* ./node_modules/sqlite3/lib/binding/ cp -r ../../bin/deps/linux-x64/sqlite/node* ./node_modules/sqlite3/lib/binding/
printf "#/bin/sh\n./node/bin/node src/www" > trilium.sh printf "#!/bin/sh\n./node/bin/node src/www" > trilium.sh
chmod 755 trilium.sh chmod 755 trilium.sh
cd .. cd ..
VERSION=`jq -r ".version" ../package.json` VERSION=`jq -r ".version" ../package.json`
tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server tar cJf trilium-linux-x64-server-${VERSION}.tar.xz trilium-linux-x64-server

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export GITHUB_REPO=trilium
if [[ $# -eq 0 ]] ; then if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version" echo "Missing argument of new version"
exit 1 exit 1

View File

@@ -3,6 +3,8 @@
instanceName= instanceName=
[Network] [Network]
# host setting is relevant only for web deployments - set the host on which the server will listen
# host=0.0.0.0
# port setting is relevant only for web deployments, desktop builds run on random free port # port setting is relevant only for web deployments, desktop builds run on random free port
port=8080 port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).

View File

@@ -9,6 +9,7 @@ const url = require("url");
const port = require('./src/services/port'); const port = require('./src/services/port');
const appIconService = require('./src/services/app_icon'); const appIconService = require('./src/services/app_icon');
const windowStateKeeper = require('electron-window-state'); const windowStateKeeper = require('electron-window-state');
const contextMenu = require('electron-context-menu');
const app = electron.app; const app = electron.app;
const globalShortcut = electron.globalShortcut; const globalShortcut = electron.globalShortcut;
@@ -23,6 +24,26 @@ let mainWindow;
require('electron-dl')({ saveAs: true }); require('electron-dl')({ saveAs: true });
contextMenu({
menu: (actions, params, browserWindow) => [
actions.cut(),
actions.copy(),
actions.copyLink(),
actions.paste(),
{
label: 'Search DuckDuckGo for “{selection}”',
// Only show it when right-clicking text
visible: params.selectionText.trim().length > 0,
click: () => {
const {shell} = require('electron');
shell.openExternal(`https://duckduckgo.com?q=${encodeURIComponent(params.selectionText)}`);
}
},
actions.inspect()
]
});
function onClosed() { function onClosed() {
// Dereference the window // Dereference the window
// For multiple windows store them in an array // For multiple windows store them in an array
@@ -74,7 +95,7 @@ async function createMainWindow() {
const parsedUrl = url.parse(targetUrl); const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages // we still need to allow internal redirects from setup and migration pages
if (parsedUrl.hostname !== 'localhost' || parsedUrl.hostname !== '127.0.0.1' || (parsedUrl.path && parsedUrl.path !== '/')) { if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
ev.preventDefault(); ev.preventDefault();
} }
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@
.CodeMirror { .CodeMirror {
/* Set height, width, borders, and global font properties here */ /* Set height, width, borders, and global font properties here */
font-family: monospace; font-family: monospace;
height: 300px; height: auto;
color: black; color: black;
direction: ltr; direction: ltr;
} }

335
libraries/printThis.js Executable file
View File

@@ -0,0 +1,335 @@
/*
* printThis v1.15.0
* @desc Printing plug-in for jQuery
* @author Jason Day
*
* Resources (based on):
* - jPrintArea: http://plugins.jquery.com/project/jPrintArea
* - jqPrint: https://github.com/permanenttourist/jquery.jqprint
* - Ben Nadal: http://www.bennadel.com/blog/1591-Ask-Ben-Print-Part-Of-A-Web-Page-With-jQuery.htm
*
* Licensed under the MIT licence:
* http://www.opensource.org/licenses/mit-license.php
*
* (c) Jason Day 2015-2018
*
* Usage:
*
* $("#mySelector").printThis({
* debug: false, // show the iframe for debugging
* importCSS: true, // import parent page css
* importStyle: false, // import style tags
* printContainer: true, // grab outer container as well as the contents of the selector
* loadCSS: "path/to/my.css", // path to additional css file - use an array [] for multiple
* pageTitle: "", // add title to print page
* removeInline: false, // remove all inline styles from print elements
* removeInlineSelector: "body *", // custom selectors to filter inline styles. removeInline must be true
* printDelay: 333, // variable print delay
* header: null, // prefix to html
* footer: null, // postfix to html
* base: false, // preserve the BASE tag, or accept a string for the URL
* formValues: true, // preserve input/form values
* canvas: false, // copy canvas elements
* doctypeString: '...', // enter a different doctype for older markup
* removeScripts: false, // remove script tags from print content
* copyTagClasses: false // copy classes from the html & body tag
* beforePrintEvent: null, // callback function for printEvent in iframe
* beforePrint: null, // function called before iframe is filled
* afterPrint: null // function called before iframe is removed
* });
*
* Notes:
* - the loadCSS will load additional CSS (with or without @media print) into the iframe, adjusting layout
*/
;
(function($) {
function appendContent($el, content) {
if (!content) return;
// Simple test for a jQuery element
$el.append(content.jquery ? content.clone() : content);
}
function appendBody($body, $element, opt) {
// Clone for safety and convenience
// Calls clone(withDataAndEvents = true) to copy form values.
var $content = $element.clone(opt.formValues);
if (opt.formValues) {
// Copy original select and textarea values to their cloned counterpart
// Makes up for inability to clone select and textarea values with clone(true)
copyValues($element, $content, 'select, textarea');
}
if (opt.removeScripts) {
$content.find('script').remove();
}
if (opt.printContainer) {
// grab $.selector as container
$content.appendTo($body);
} else {
// otherwise just print interior elements of container
$content.each(function() {
$(this).children().appendTo($body)
});
}
}
// Copies values from origin to clone for passed in elementSelector
function copyValues(origin, clone, elementSelector) {
var $originalElements = origin.find(elementSelector);
clone.find(elementSelector).each(function(index, item) {
$(item).val($originalElements.eq(index).val());
});
}
var opt;
$.fn.printThis = function(options) {
opt = $.extend({}, $.fn.printThis.defaults, options);
var $element = this instanceof jQuery ? this : $(this);
var strFrameName = "printThis-" + (new Date()).getTime();
if (window.location.hostname !== document.domain && navigator.userAgent.match(/msie/i)) {
// Ugly IE hacks due to IE not inheriting document.domain from parent
// checks if document.domain is set by comparing the host name against document.domain
var iframeSrc = "javascript:document.write(\"<head><script>document.domain=\\\"" + document.domain + "\\\";</s" + "cript></head><body></body>\")";
var printI = document.createElement('iframe');
printI.name = "printIframe";
printI.id = strFrameName;
printI.className = "MSIE";
document.body.appendChild(printI);
printI.src = iframeSrc;
} else {
// other browsers inherit document.domain, and IE works if document.domain is not explicitly set
var $frame = $("<iframe id='" + strFrameName + "' name='printIframe' />");
$frame.appendTo("body");
}
var $iframe = $("#" + strFrameName);
// show frame if in debug mode
if (!opt.debug) $iframe.css({
position: "absolute",
width: "0px",
height: "0px",
left: "-600px",
top: "-600px"
});
// before print callback
if (typeof opt.beforePrint === "function") {
opt.beforePrint();
}
// $iframe.ready() and $iframe.load were inconsistent between browsers
setTimeout(function() {
// Add doctype to fix the style difference between printing and render
function setDocType($iframe, doctype){
var win, doc;
win = $iframe.get(0);
win = win.contentWindow || win.contentDocument || win;
doc = win.document || win.contentDocument || win;
doc.open();
doc.write(doctype);
doc.close();
}
if (opt.doctypeString){
setDocType($iframe, opt.doctypeString);
}
var $doc = $iframe.contents(),
$head = $doc.find("head"),
$body = $doc.find("body"),
$base = $('base'),
baseURL;
// add base tag to ensure elements use the parent domain
if (opt.base === true && $base.length > 0) {
// take the base tag from the original page
baseURL = $base.attr('href');
} else if (typeof opt.base === 'string') {
// An exact base string is provided
baseURL = opt.base;
} else {
// Use the page URL as the base
baseURL = document.location.protocol + '//' + document.location.host;
}
$head.append('<base href="' + baseURL + '">');
// import page stylesheets
if (opt.importCSS) $("link[rel=stylesheet]").each(function() {
var href = $(this).attr("href");
if (href) {
var media = $(this).attr("media") || "all";
$head.append("<link type='text/css' rel='stylesheet' href='" + href + "' media='" + media + "'>");
}
});
// import style tags
if (opt.importStyle) $("style").each(function() {
$head.append(this.outerHTML);
});
// add title of the page
if (opt.pageTitle) $head.append("<title>" + opt.pageTitle + "</title>");
// import additional stylesheet(s)
if (opt.loadCSS) {
if ($.isArray(opt.loadCSS)) {
jQuery.each(opt.loadCSS, function(index, value) {
$head.append("<link type='text/css' rel='stylesheet' href='" + this + "'>");
});
} else {
$head.append("<link type='text/css' rel='stylesheet' href='" + opt.loadCSS + "'>");
}
}
var pageHtml = $('html')[0];
// CSS VAR in html tag when dynamic apply e.g. document.documentElement.style.setProperty("--foo", bar);
$doc.find('html').prop('style', pageHtml.style.cssText);
// copy 'root' tag classes
var tag = opt.copyTagClasses;
if (tag) {
tag = tag === true ? 'bh' : tag;
if (tag.indexOf('b') !== -1) {
$body.addClass($('body')[0].className);
}
if (tag.indexOf('h') !== -1) {
$doc.find('html').addClass(pageHtml.className);
}
}
// print header
appendContent($body, opt.header);
if (opt.canvas) {
// add canvas data-ids for easy access after cloning.
var canvasId = 0;
// .addBack('canvas') adds the top-level element if it is a canvas.
$element.find('canvas').addBack('canvas').each(function(){
$(this).attr('data-printthis', canvasId++);
});
}
appendBody($body, $element, opt);
if (opt.canvas) {
// Re-draw new canvases by referencing the originals
$body.find('canvas').each(function(){
var cid = $(this).data('printthis'),
$src = $('[data-printthis="' + cid + '"]');
this.getContext('2d').drawImage($src[0], 0, 0);
// Remove the markup from the original
if ($.isFunction($.fn.removeAttr)) {
$src.removeAttr('data-printthis');
} else {
$.each($src, function(i, el) {
el.removeAttribute('data-printthis')
});
}
});
}
// remove inline styles
if (opt.removeInline) {
// Ensure there is a selector, even if it's been mistakenly removed
var selector = opt.removeInlineSelector || '*';
// $.removeAttr available jQuery 1.7+
if ($.isFunction($.removeAttr)) {
$body.find(selector).removeAttr("style");
} else {
$body.find(selector).attr("style", "");
}
}
// print "footer"
appendContent($body, opt.footer);
// attach event handler function to beforePrint event
function attachOnBeforePrintEvent($iframe, beforePrintHandler) {
var win = $iframe.get(0);
win = win.contentWindow || win.contentDocument || win;
if (typeof beforePrintHandler === "function") {
if ('matchMedia' in win) {
win.matchMedia('print').addListener(function(mql) {
if(mql.matches) beforePrintHandler();
});
} else {
win.onbeforeprint = beforePrintHandler;
}
}
}
attachOnBeforePrintEvent($iframe, opt.beforePrint);
setTimeout(function() {
if ($iframe.hasClass("MSIE")) {
// check if the iframe was created with the ugly hack
// and perform another ugly hack out of neccessity
window.frames["printIframe"].focus();
$head.append("<script> window.print(); </s" + "cript>");
} else {
// proper method
if (document.queryCommandSupported("print")) {
$iframe[0].contentWindow.document.execCommand("print", false, null);
} else {
$iframe[0].contentWindow.focus();
$iframe[0].contentWindow.print();
}
}
// remove iframe after print
if (!opt.debug) {
setTimeout(function() {
$iframe.remove();
}, 1000);
}
// after print callback
if (typeof opt.afterPrint === "function") {
opt.afterPrint();
}
}, opt.printDelay);
}, 333);
};
// defaults
$.fn.printThis.defaults = {
debug: false, // show the iframe for debugging
importCSS: true, // import parent page css
importStyle: false, // import style tags
printContainer: true, // print outer container/$.selector
loadCSS: "", // path to additional css file - use an array [] for multiple
pageTitle: "", // add title to print page
removeInline: false, // remove inline styles from print elements
removeInlineSelector: "*", // custom selectors to filter inline styles. removeInline must be true
printDelay: 333, // variable print delay
header: null, // prefix to html
footer: null, // postfix to html
base: false, // preserve the BASE tag or accept a string for the URL
formValues: true, // preserve input/form values
canvas: false, // copy canvas content
doctypeString: '<!DOCTYPE html>', // enter a different doctype for older markup
removeScripts: false, // remove script tags from print content
copyTagClasses: false, // copy classes from the html & body tag
beforePrintEvent: null, // callback function for printEvent in iframe
beforePrint: null, // function called before iframe is filled
afterPrint: null // function called before iframe is removed
};
})(jQuery);

1761
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.33.3", "version": "0.34.0-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@@ -13,8 +13,8 @@
"url": "https://github.com/zadam/trilium.git" "url": "https://github.com/zadam/trilium.git"
}, },
"scripts": { "scripts": {
"start": "node ./src/www", "start-server": "TRILIUM_ENV=dev node ./src/www",
"start-electron": "electron . --disable-gpu", "start-electron": "TRILIUM_ENV=dev electron . --disable-gpu",
"build-backend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js", "build-backend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js", "build-frontend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs", "build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
@@ -28,23 +28,23 @@
"commonmark": "0.29.0", "commonmark": "0.29.0",
"cookie-parser": "1.4.4", "cookie-parser": "1.4.4",
"csurf": "1.10.0", "csurf": "1.10.0",
"dayjs": "1.8.14", "dayjs": "1.8.15",
"debug": "4.1.1", "debug": "4.1.1",
"ejs": "2.6.2", "ejs": "2.6.2",
"electron-debug": "3.0.0", "electron-context-menu": "0.13.0",
"electron-debug": "3.0.1",
"electron-dl": "1.14.0", "electron-dl": "1.14.0",
"electron-find": "1.0.6", "electron-find": "1.0.6",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"express": "4.17.1", "express": "4.17.1",
"express-session": "1.16.2", "express-session": "1.16.2",
"file-type": "12.0.0", "file-type": "12.0.1",
"fs-extra": "8.0.1", "fs-extra": "8.1.0",
"get-port": "5.0.0", "helmet": "3.19.0",
"helmet": "3.18.0",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.2", "html2plaintext": "2.1.2",
"image-type": "4.1.0", "image-type": "4.1.0",
"imagemin": "6.1.0", "imagemin": "7.0.0",
"imagemin-giflossy": "5.1.10", "imagemin-giflossy": "5.1.10",
"imagemin-mozjpeg": "8.0.0", "imagemin-mozjpeg": "8.0.0",
"imagemin-pngquant": "8.0.0", "imagemin-pngquant": "8.0.0",
@@ -52,37 +52,37 @@
"jimp": "0.6.4", "jimp": "0.6.4",
"mime-types": "2.1.24", "mime-types": "2.1.24",
"moment": "2.24.0", "moment": "2.24.0",
"multer": "1.4.1", "multer": "1.4.2",
"node-abi": "2.9.0", "node-abi": "2.9.0",
"open": "6.3.0", "open": "6.4.0",
"pngjs": "3.4.0", "pngjs": "3.4.0",
"portscanner": "2.2.0",
"rand-token": "0.4.0", "rand-token": "0.4.0",
"rcedit": "2.0.0", "rcedit": "2.0.0",
"rimraf": "2.6.3", "rimraf": "2.6.3",
"sanitize-filename": "1.6.1", "sanitize-filename": "1.6.1",
"sax": "1.2.4", "sax": "1.2.4",
"semver": "6.1.1", "semver": "6.2.0",
"serve-favicon": "2.5.0", "serve-favicon": "2.5.0",
"session-file-store": "1.3.0", "session-file-store": "1.3.1",
"simple-node-logger": "18.12.22", "simple-node-logger": "18.12.23",
"sqlite": "3.0.3", "sqlite": "3.0.3",
"sqlite3": "4.0.9", "sqlite3": "4.0.9",
"tar-stream": "2.1.0", "tar-stream": "2.1.0",
"turndown": "5.0.3", "turndown": "5.0.3",
"unescape": "1.0.1", "unescape": "1.0.1",
"ws": "7.0.1", "ws": "7.1.0",
"xml2js": "0.4.19" "xml2js": "0.4.19"
}, },
"devDependencies": { "devDependencies": {
"devtron": "1.4.0", "devtron": "1.4.0",
"electron": "6.0.0-beta.10", "electron": "6.0.0-beta.14",
"electron-builder": "20.44.2", "electron-builder": "21.1.1",
"electron-compile": "6.4.4", "electron-compile": "6.4.4",
"electron-installer-debian": "2.0.0", "electron-installer-debian": "2.0.0",
"electron-packager": "13.1.1", "electron-packager": "14.0.2",
"electron-rebuild": "1.8.5", "electron-rebuild": "1.8.5",
"lorem-ipsum": "2.0.3", "lorem-ipsum": "2.0.3",
"tape": "4.10.2",
"xo": "0.24.0" "xo": "0.24.0"
}, },
"xo": { "xo": {

View File

@@ -147,6 +147,26 @@ $noteTabContainer.on("click", ".export-note-button", function () {
$noteTabContainer.on("click", ".import-files-button", () => importDialog.showDialog(treeService.getActiveNode())); $noteTabContainer.on("click", ".import-files-button", () => importDialog.showDialog(treeService.getActiveNode()));
$noteTabContainer.on("click", ".print-note-button", async function () {
if ($(this).hasClass("disabled")) {
return;
}
const $tabContext = noteDetailService.getActiveTabContext();
if (!$tabContext) {
return;
}
await libraryLoader.requireLibrary(libraryLoader.PRINT_THIS);
$tabContext.$tabContent.find('.note-detail-component:visible').printThis({
header: $("<h2>").text($tabContext.note && $tabContext.note.title).prop('outerHTML') ,
importCSS: false,
loadCSS: "libraries/codemirror/codemirror.css",
debug: true
});
});
$('[data-toggle="tooltip"]').tooltip({ $('[data-toggle="tooltip"]').tooltip({
html: true html: true
}); });

View File

@@ -7,8 +7,11 @@ const $custom = $("#confirm-dialog-custom");
const DELETE_NOTE_BUTTON_ID = "confirm-dialog-delete-note"; const DELETE_NOTE_BUTTON_ID = "confirm-dialog-delete-note";
let resolve; let resolve;
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
function confirm(message) { function confirm(message) {
$originallyFocused = $(':focus');
$custom.hide(); $custom.hide();
glob.activeDialog = $dialog; glob.activeDialog = $dialog;
@@ -55,6 +58,11 @@ $dialog.on("hidden.bs.modal", () => {
if (resolve) { if (resolve) {
resolve(false); resolve(false);
} }
if ($originallyFocused) {
$originallyFocused.focus();
$originallyFocused = null;
}
}); });
function doResolve(ret) { function doResolve(ret) {

View File

@@ -5,8 +5,11 @@ const $infoContent = $("#info-dialog-content");
const $okButton = $("#info-dialog-ok-button"); const $okButton = $("#info-dialog-ok-button");
let resolve; let resolve;
let $originallyFocused; // element focused before the dialog was opened so we can return to it afterwards
function info(message) { function info(message) {
$originallyFocused = $(':focus');
utils.closeActiveDialog(); utils.closeActiveDialog();
glob.activeDialog = $dialog; glob.activeDialog = $dialog;
@@ -24,6 +27,11 @@ $dialog.on("hidden.bs.modal", () => {
if (resolve) { if (resolve) {
resolve(); resolve();
} }
if ($originallyFocused) {
$originallyFocused.focus();
$originallyFocused = null;
}
}); });
$okButton.click(() => $dialog.modal("hide")); $okButton.click(() => $dialog.modal("hide"));

View File

@@ -6,6 +6,7 @@ import treeCache from "./tree_cache.js";
import treeUtils from "./tree_utils.js"; import treeUtils from "./tree_utils.js";
import hoistedNoteService from "./hoisted_note.js"; import hoistedNoteService from "./hoisted_note.js";
import noteDetailService from "./note_detail.js"; import noteDetailService from "./note_detail.js";
import confirmDialog from "../dialogs/confirm.js";
async function moveBeforeNode(nodesToMove, beforeNode) { async function moveBeforeNode(nodesToMove, beforeNode) {
nodesToMove = await filterRootNote(nodesToMove); nodesToMove = await filterRootNote(nodesToMove);
@@ -82,7 +83,7 @@ async function moveToNode(nodesToMove, toNode) {
async function deleteNodes(nodes) { async function deleteNodes(nodes) {
nodes = await filterRootNote(nodes); nodes = await filterRootNote(nodes);
if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) { if (nodes.length === 0 || !await confirmDialog.confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
return false; return false;
} }
@@ -102,7 +103,7 @@ async function deleteNodes(nodes) {
next = nodes[0].getPrevSibling(); next = nodes[0].getPrevSibling();
} }
if (!next && !hoistedNoteService.isTopLevelNode(nodes[0])) { if (!next && !await hoistedNoteService.isTopLevelNode(nodes[0])) {
next = nodes[0].getParent(); next = nodes[0].getParent();
} }

View File

@@ -45,6 +45,8 @@ const LINK_MAP = {
] ]
}; };
const PRINT_THIS = {js: ["libraries/printThis.js"]};
async function requireLibrary(library) { async function requireLibrary(library) {
if (library.css) { if (library.css) {
library.css.map(cssUrl => cssLoader.requireCss(cssUrl)); library.css.map(cssUrl => cssLoader.requireCss(cssUrl));
@@ -79,5 +81,6 @@ export default {
ESLINT, ESLINT,
COMMONMARK, COMMONMARK,
RELATION_MAP, RELATION_MAP,
LINK_MAP LINK_MAP,
PRINT_THIS
} }

View File

@@ -67,12 +67,9 @@ function connectWebSocket() {
// use wss for secure messaging // use wss for secure messaging
const ws = new WebSocket(protocol + "://" + location.host); const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = event => console.debug(utils.now(), "Connected to server with WebSocket"); ws.onopen = () => console.debug(utils.now(), "Connected to server with WebSocket");
ws.onmessage = handleMessage; ws.onmessage = handleMessage;
ws.onclose = function(){ // we're not handling ws.onclose here because reconnection is done in sendPing()
// Try to reconnect in 5 seconds
setTimeout(() => connectWebSocket(), 5000);
};
return ws; return ws;
} }
@@ -88,10 +85,17 @@ setTimeout(() => {
console.log("Lost connection to server"); console.log("Lost connection to server");
} }
ws.send(JSON.stringify({ if (ws.readyState === ws.OPEN) {
type: 'ping', ws.send(JSON.stringify({
lastSyncId: lastSyncId type: 'ping',
})); lastSyncId: lastSyncId
}));
}
else if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
console.log("WS closed or closing, trying to reconnect");
ws = connectWebSocket();
}
}, 1000); }, 1000);
}, 0); }, 0);

View File

@@ -76,6 +76,22 @@ function getActiveEditor() {
} }
} }
async function activateOrOpenNote(noteId) {
for (const tabContext of tabContexts) {
if (tabContext.note && tabContext.note.noteId === noteId) {
await tabContext.activate();
return;
}
}
// if no tab with this note has been found we'll create new tab
await loadNoteDetail(noteId, {
newTab: true,
activate: true
});
}
function getTabContexts() { function getTabContexts() {
return tabContexts; return tabContexts;
} }
@@ -405,7 +421,7 @@ tabRow.addListener('tabRemove', async ({ detail }) => {
if (tabContextToDelete) { if (tabContextToDelete) {
// sometimes there are orphan autocompletes after closing the tab // sometimes there are orphan autocompletes after closing the tab
tabContextToDelete.$tabContent.find('.aa-input').autocomplete('close'); tabContextToDelete.closeAutocomplete();
await tabContextToDelete.saveNoteIfChanged(); await tabContextToDelete.saveNoteIfChanged();
tabContextToDelete.$tabContent.remove(); tabContextToDelete.$tabContent.remove();
@@ -544,6 +560,7 @@ export default {
getTabContexts, getTabContexts,
getActiveTabContext, getActiveTabContext,
getActiveEditor, getActiveEditor,
activateOrOpenNote,
clearOpenTabsTask, clearOpenTabsTask,
filterTabs, filterTabs,
openEmptyTab, openEmptyTab,

View File

@@ -14,6 +14,7 @@ class NoteDetailCode {
this.ctx = ctx; this.ctx = ctx;
this.codeEditor = null; this.codeEditor = null;
this.$component = ctx.$tabContent.find('.note-detail-code'); this.$component = ctx.$tabContent.find('.note-detail-code');
this.$editorEl = this.$component.find('.note-detail-code-editor');
this.$executeScriptButton = ctx.$tabContent.find(".execute-script-button"); this.$executeScriptButton = ctx.$tabContent.find(".execute-script-button");
utils.bindElShortcut(ctx.$tabContent, "ctrl+return", () => this.executeCurrentNote()); utils.bindElShortcut(ctx.$tabContent, "ctrl+return", () => this.executeCurrentNote());
@@ -34,7 +35,7 @@ class NoteDetailCode {
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
this.codeEditor = CodeMirror(this.$component[0], { this.codeEditor = CodeMirror(this.$editorEl[0], {
value: "", value: "",
viewportMargin: Infinity, viewportMargin: Infinity,
indentUnit: 4, indentUnit: 4,

View File

@@ -1,5 +1,4 @@
import utils from "./utils.js"; import utils from "./utils.js";
import noteDetailService from "./note_detail.js";
import infoService from "./info.js"; import infoService from "./info.js";
import server from "./server.js"; import server from "./server.js";

View File

@@ -8,6 +8,7 @@ class NoteDetailText {
constructor(ctx) { constructor(ctx) {
this.ctx = ctx; this.ctx = ctx;
this.$component = ctx.$tabContent.find('.note-detail-text'); this.$component = ctx.$tabContent.find('.note-detail-text');
this.$editorEl = this.$component.find('.note-detail-text-editor');
this.textEditor = null; this.textEditor = null;
this.$component.on("dblclick", "img", e => { this.$component.on("dblclick", "img", e => {
@@ -39,7 +40,7 @@ class NoteDetailText {
// textEditor might have been initialized during previous await so checking again // textEditor might have been initialized during previous await so checking again
// looks like double initialization can freeze CKEditor pretty badly // looks like double initialization can freeze CKEditor pretty badly
if (!this.textEditor) { if (!this.textEditor) {
this.textEditor = await BalloonEditor.create(this.$component[0], { this.textEditor = await BalloonEditor.create(this.$editorEl[0], {
placeholder: "Type the content of your note here ..." placeholder: "Type the content of your note here ..."
}); });
@@ -73,7 +74,7 @@ class NoteDetailText {
} }
focus() { focus() {
this.$component.focus(); this.$editorEl.focus();
} }
getEditor() { getEditor() {

View File

@@ -83,7 +83,11 @@ class TabContext {
if (utils.isDesktop()) { if (utils.isDesktop()) {
// keyboard plugin is not loaded in mobile // keyboard plugin is not loaded in mobile
this.$noteTitle.bind('keydown', 'return', () => this.getComponent().focus()); this.$noteTitle.bind('keydown', 'return', () => {
this.getComponent().focus();
return false; // to not propagate the enter into the editor (causes issues with codemirror)
});
} }
this.$protectButton = this.$tabContent.find(".protect-button"); this.$protectButton = this.$tabContent.find(".protect-button");
@@ -92,7 +96,7 @@ class TabContext {
this.$unprotectButton = this.$tabContent.find(".unprotect-button"); this.$unprotectButton = this.$tabContent.find(".unprotect-button");
this.$unprotectButton.click(protectedSessionService.unprotectNoteAndSendToServer); this.$unprotectButton.click(protectedSessionService.unprotectNoteAndSendToServer);
console.log(`Created note tab ${this.tabId}`); console.debug(`Created note tab ${this.tabId}`);
} }
setNote(note, notePath) { setNote(note, notePath) {
@@ -109,6 +113,8 @@ class TabContext {
this.setTitleBar(); this.setTitleBar();
this.closeAutocomplete(); // esp. on windows autocomplete is not getting closed automatically
setTimeout(async () => { setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds // we include the note into recent list only if the user stayed on the note at least 5 seconds
if (notePath && notePath === this.notePath) { if (notePath && notePath === this.notePath) {
@@ -125,7 +131,7 @@ class TabContext {
this.showPaths(); this.showPaths();
console.log(`Switched tab ${this.tabId} to ${this.noteId}`); console.debug(`Switched tab ${this.tabId} to ${this.noteId}`);
} }
show() { show() {
@@ -334,6 +340,12 @@ class TabContext {
} }
} }
} }
closeAutocomplete() {
if (utils.isDesktop()) {
this.$tabContent.find('.aa-input').autocomplete('close');
}
}
} }
export default TabContext; export default TabContext;

View File

@@ -196,6 +196,8 @@ async function resolveNotePath(notePath) {
async function getRunPath(notePath) { async function getRunPath(notePath) {
utils.assertArguments(notePath); utils.assertArguments(notePath);
notePath = notePath.split("-")[0];
const path = notePath.split("/").reverse(); const path = notePath.split("/").reverse();
if (!path.includes("root")) { if (!path.includes("root")) {
@@ -335,6 +337,31 @@ async function treeInitialized() {
messagingService.logError("Cannot retrieve open tabs: " + e.stack); messagingService.logError("Cannot retrieve open tabs: " + e.stack);
} }
// if there's notePath in the URL, make sure it's open and active
// (useful, among others, for opening clipped notes from clipper)
if (location.hash) {
const notePath = location.hash.substr(1);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (await treeCache.noteExists(noteId)) {
for (const tab of openTabs) {
tab.active = false;
}
const foundTab = openTabs.find(tab => noteId === treeUtils.getNoteIdFromNotePath(tab.notePath));
if (foundTab) {
foundTab.active = true;
}
else {
openTabs.push({
notePath: notePath,
active: true
});
}
}
}
const filteredTabs = []; const filteredTabs = [];
for (const openTab of openTabs) { for (const openTab of openTabs) {
@@ -630,7 +657,8 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
extraClasses: await treeBuilder.getExtraClasses(noteEntity), extraClasses: await treeBuilder.getExtraClasses(noteEntity),
icon: await treeBuilder.getIcon(noteEntity), icon: await treeBuilder.getIcon(noteEntity),
folder: extraOptions.type === 'search', folder: extraOptions.type === 'search',
lazy: true lazy: true,
key: utils.randomString(12) // this should prevent some "duplicate key" errors
}; };
if (target === 'after') { if (target === 'after') {
@@ -638,10 +666,14 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
} }
else if (target === 'into') { else if (target === 'into') {
if (!node.getChildren() && node.isFolder()) { if (!node.getChildren() && node.isFolder()) {
// folder is not loaded - load will bring up the note since it was already put into cache
await node.load(true);
await node.setExpanded(); await node.setExpanded();
} }
else {
node.addChildren(newNode); node.addChildren(newNode);
}
await node.getLastChild().setActive(true); await node.getLastChild().setActive(true);
@@ -707,6 +739,15 @@ messagingService.subscribeToMessages(message => {
if (message.type === 'refresh-tree') { if (message.type === 'refresh-tree') {
reload(); reload();
} }
else if (message.type === 'open-note') {
noteDetailService.activateOrOpenNote(message.noteId);
if (utils.isElectron()) {
const currentWindow = require("electron").remote.getCurrentWindow();
currentWindow.show();
}
}
}); });
messagingService.subscribeToSyncMessages(syncData => { messagingService.subscribeToSyncMessages(syncData => {
@@ -737,10 +778,12 @@ utils.bindShortcut('ctrl+o', async () => {
async function createNoteInto() { async function createNoteInto() {
const node = getActiveNode(); const node = getActiveNode();
await createNote(node, node.data.noteId, 'into', { if (node) {
isProtected: node.data.isProtected, await createNote(node, node.data.noteId, 'into', {
saveSelection: true isProtected: node.data.isProtected,
}); saveSelection: true
});
}
} }
async function checkFolderStatus(node) { async function checkFolderStatus(node) {

View File

@@ -83,7 +83,8 @@ async function prepareNode(branch) {
icon: await getIcon(note), icon: await getIcon(note),
refKey: note.noteId, refKey: note.noteId,
expanded: branch.isExpanded || hoistedNoteId === note.noteId, expanded: branch.isExpanded || hoistedNoteId === note.noteId,
lazy: true lazy: true,
key: utils.randomString(12) // this should prevent some "duplicate key" errors
}; };
if (note.hasChildren() || note.type === 'search') { if (note.hasChildren() || note.type === 'search') {

View File

@@ -19,7 +19,10 @@ function getNoteIdFromNotePath(notePath) {
const path = notePath.split("/"); const path = notePath.split("/");
return path[path.length - 1]; const lastSegment = path[path.length - 1];
// path could have also tabId suffix
return lastSegment.split("-")[0];
} }
async function getNotePath(node) { async function getNotePath(node) {

View File

@@ -135,13 +135,16 @@ ul.fancytree-container {
.note-detail-text h6 { font-size: 1.1em; } .note-detail-text h6 { font-size: 1.1em; }
.note-detail-text { .note-detail-text {
overflow: auto;
font-family: var(--detail-text-font-family);
}
.note-detail-text-editor {
padding-top: 10px;
border: 0 !important; border: 0 !important;
box-shadow: none !important; box-shadow: none !important;
/* This is because with empty content height of editor is 0 and it's impossible to click into it */ /* This is because with empty content height of editor is 0 and it's impossible to click into it */
min-height: 200px; min-height: 200px;
padding-top: 10px;
overflow: auto;
font-family: var(--detail-text-font-family);
} }
.note-detail-text p:first-child, .note-detail-text::before { .note-detail-text p:first-child, .note-detail-text::before {
@@ -354,10 +357,13 @@ div.ui-tooltip {
} }
.note-detail-code { .note-detail-code {
min-height: 200px;
overflow: auto; overflow: auto;
} }
.note-detail-code-editor {
min-height: 200px;
}
.note-detail-render { .note-detail-render {
min-height: 200px; min-height: 200px;
} }
@@ -832,4 +838,8 @@ a.external:after, a[href^="http://"]:after, a[href^="https://"]:after {
.note-detail-empty { .note-detail-empty {
margin: 50px; margin: 50px;
}
.modal-header {
padding: 0.7rem 1rem !important; /* make modal header padding slightly smaller */
} }

132
src/routes/api/clipper.js Normal file
View File

@@ -0,0 +1,132 @@
"use strict";
const noteService = require('../../services/notes');
const dateNoteService = require('../../services/date_notes');
const dateUtils = require('../../services/date_utils');
const imageService = require('../../services/image');
const appInfo = require('../../services/app_info');
const messagingService = require('../../services/messaging');
const log = require('../../services/log');
const utils = require('../../services/utils');
const path = require('path');
const Link = require('../../entities/link');
async function findClippingNote(todayNote, pageUrl) {
const notes = await todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl);
for (const note of notes) {
if (await note.getLabelValue('clipType') === 'clippings') {
return note;
}
}
return null;
}
async function addClipping(req) {
const {title, content, pageUrl, images} = req.body;
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
let clippingNote = await findClippingNote(todayNote, pageUrl);
if (!clippingNote) {
clippingNote = (await noteService.createNote(todayNote.noteId, title, '')).note;
await clippingNote.setLabel('clipType', 'clippings');
await clippingNote.setLabel('pageUrl', pageUrl);
}
const rewrittenContent = await addImagesToNote(images, clippingNote, content);
await clippingNote.setContent(await clippingNote.getContent() + '<p>' + rewrittenContent + '</p>');
return {
noteId: clippingNote.noteId
};
}
async function createNote(req) {
const {title, content, pageUrl, images, clipType} = req.body;
const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate());
const {note} = await noteService.createNote(todayNote.noteId, title, content);
await note.setLabel('clipType', clipType);
if (pageUrl) {
await note.setLabel('pageUrl', pageUrl);
}
const rewrittenContent = await addImagesToNote(images, note, content);
await note.setContent(rewrittenContent);
return {
noteId: note.noteId
};
}
async function addImagesToNote(images, note, content) {
let rewrittenContent = content;
if (images) {
for (const {src, dataUrl, imageId} of images) {
const filename = path.basename(src);
if (!dataUrl.startsWith("data:image")) {
log.info("Image could not be recognized as data URL:", dataUrl.substr(0, Math.min(100, dataUrl.length)));
continue;
}
const buffer = Buffer.from(dataUrl.split(",")[1], 'base64');
const {note: imageNote, url} = await imageService.saveImage(buffer, filename, note.noteId, true);
await new Link({
noteId: note.noteId,
targetNoteId: imageNote.noteId,
type: 'image'
}).save();
console.log(`Replacing ${imageId} with ${url}`);
rewrittenContent = rewrittenContent.replace(imageId, url);
}
}
return rewrittenContent;
}
async function openNote(req) {
if (utils.isElectron()) {
messagingService.sendMessageToAllClients({
type: 'open-note',
noteId: req.params.noteId
});
return {
result: 'ok'
};
}
else {
return {
result: 'open-in-browser'
}
}
}
async function handshake() {
return {
appName: "trilium",
protocolVersion: appInfo.clipperProtocolVersion
}
}
module.exports = {
createNote,
addClipping,
openNote,
handshake
};

View File

@@ -34,7 +34,7 @@ async function uploadImage(req) {
return [404, `Note ${noteId} doesn't exist.`]; return [404, `Note ${noteId} doesn't exist.`];
} }
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) { if (!["image/png", "image/jpeg", "image/gif", "image/webp"].includes(file.mimetype)) {
return [400, "Unknown image type: " + file.mimetype]; return [400, "Unknown image type: " + file.mimetype];
} }

View File

@@ -11,6 +11,8 @@ const eventService = require('../../services/events');
const cls = require('../../services/cls'); const cls = require('../../services/cls');
const sqlInit = require('../../services/sql_init'); const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const optionService = require('../../services/options');
const ApiToken = require('../../entities/api_token');
async function loginSync(req) { async function loginSync(req) {
if (!await sqlInit.schemaExists()) { if (!await sqlInit.schemaExists()) {
@@ -76,7 +78,28 @@ async function loginToProtectedSession(req) {
}; };
} }
async function token(req) {
const username = req.body.username;
const password = req.body.password;
const isUsernameValid = username === await optionService.getOption('username');
const isPasswordValid = await passwordEncryptionService.verifyPassword(password);
if (!isUsernameValid || !isPasswordValid) {
return [401, "Incorrect username/password"];
}
const apiToken = await new ApiToken({
token: utils.randomSecureToken()
}).save();
return {
token: apiToken.token
};
}
module.exports = { module.exports = {
loginSync, loginSync,
loginToProtectedSession loginToProtectedSession,
token
}; };

View File

@@ -1,33 +1,9 @@
"use strict"; "use strict";
const imageType = require('image-type');
const imageService = require('../../services/image'); const imageService = require('../../services/image');
const utils = require('../../services/utils');
const dateNoteService = require('../../services/date_notes'); const dateNoteService = require('../../services/date_notes');
const sql = require('../../services/sql');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const passwordEncryptionService = require('../../services/password_encryption');
const optionService = require('../../services/options');
const ApiToken = require('../../entities/api_token');
async function login(req) {
const username = req.body.username;
const password = req.body.password;
const isUsernameValid = username === await optionService.getOption('username');
const isPasswordValid = await passwordEncryptionService.verifyPassword(password);
if (!isUsernameValid || !isPasswordValid) {
return [401, "Incorrect username/password"];
}
const apiToken = await new ApiToken({
token: utils.randomSecureToken()
}).save();
return {
token: apiToken.token
};
}
async function uploadImage(req) { async function uploadImage(req) {
const file = req.file; const file = req.file;
@@ -36,9 +12,11 @@ async function uploadImage(req) {
return [400, "Unknown image type: " + file.mimetype]; return [400, "Unknown image type: " + file.mimetype];
} }
const originalName = "Sender image." + imageType(file.buffer).ext;
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']); const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']);
const {noteId} = await imageService.saveImage(file.buffer, "Sender image", parentNote.noteId, true); const {noteId} = await imageService.saveImage(file.buffer, originalName, parentNote.noteId, true);
return { return {
noteId: noteId noteId: noteId
@@ -64,7 +42,6 @@ async function saveNote(req) {
} }
module.exports = { module.exports = {
login,
uploadImage, uploadImage,
saveNote saveNote
}; };

View File

@@ -1,6 +1,7 @@
const setupRoute = require('./setup'); const setupRoute = require('./setup');
const loginRoute = require('./login'); const loginRoute = require('./login');
const indexRoute = require('./index'); const indexRoute = require('./index');
const utils = require('../services/utils');
const multer = require('multer')(); const multer = require('multer')();
// API routes // API routes
@@ -32,6 +33,7 @@ const filesRoute = require('./api/file_upload');
const searchRoute = require('./api/search'); const searchRoute = require('./api/search');
const dateNotesRoute = require('./api/date_notes'); const dateNotesRoute = require('./api/date_notes');
const linkMapRoute = require('./api/link_map'); const linkMapRoute = require('./api/link_map');
const clipperRoute = require('./api/clipper');
const log = require('../services/log'); const log = require('../services/log');
const express = require('express'); const express = require('express');
@@ -212,9 +214,9 @@ function register(app) {
apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles);
// no CSRF since this is called from android app // no CSRF since this is called from android app
route(POST, '/api/sender/login', [], senderRoute.login, apiResultHandler); route(POST, '/api/sender/login', [], loginApiRoute.token, apiResultHandler);
route(POST, '/api/sender/image', [auth.checkSenderToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler); route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
route(POST, '/api/sender/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler); route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes); apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote); apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
@@ -222,6 +224,15 @@ function register(app) {
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler); route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username) // this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession);
route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler);
// in case of local electron, local calls are allowed unauthenticated, for server they need auth
const clipperMiddleware = utils.isElectron() ? [] : [auth.checkToken];
route(GET, '/api/clipper/handshake', clipperMiddleware, clipperRoute.handshake, apiResultHandler);
route(POST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
route(POST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler);
route(POST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler);
app.use('', router); app.use('', router);
} }

View File

@@ -6,6 +6,7 @@ const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 136; const APP_DB_VERSION = 136;
const SYNC_VERSION = 9; const SYNC_VERSION = 9;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = { module.exports = {
appVersion: packageJson.version, appVersion: packageJson.version,
@@ -13,5 +14,6 @@ module.exports = {
syncVersion: SYNC_VERSION, syncVersion: SYNC_VERSION,
buildDate: build.buildDate, buildDate: build.buildDate,
buildRevision: build.buildRevision, buildRevision: build.buildRevision,
dataDirectory: TRILIUM_DATA_DIR dataDirectory: TRILIUM_DATA_DIR,
clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION
}; };

View File

@@ -56,7 +56,7 @@ async function checkAppNotInitialized(req, res, next) {
} }
} }
async function checkSenderToken(req, res, next) { async function checkToken(req, res, next) {
const token = req.headers.authorization; const token = req.headers.authorization;
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) { if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
@@ -89,6 +89,6 @@ module.exports = {
checkAppInitialized, checkAppInitialized,
checkAppNotInitialized, checkAppNotInitialized,
checkApiAuthOrElectron, checkApiAuthOrElectron,
checkSenderToken, checkToken,
checkBasicAuth checkBasicAuth
}; };

View File

@@ -1 +1 @@
module.exports = { buildDate:"2019-06-24T20:47:50+02:00", buildRevision: "1b831f94a98dbbb7184048f329ea89175841dec2" }; module.exports = { buildDate:"2019-07-21T21:57:35+02:00", buildRevision: "6d5f8e056263dbaaf3abf2d532a4fd1059745b58" };

View File

@@ -29,6 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
AND branches.isDeleted = 0`, [parentNoteId]); AND branches.isDeleted = 0`, [parentNoteId]);
} }
/** @return {Promise<Note>} */
async function getRootCalendarNote() { async function getRootCalendarNote() {
// some caching here could be useful (e.g. in CLS) // some caching here could be useful (e.g. in CLS)
let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL); let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
@@ -47,6 +48,7 @@ async function getRootCalendarNote() {
return rootNote; return rootNote;
} }
/** @return {Promise<Note>} */
async function getYearNote(dateStr, rootNote) { async function getYearNote(dateStr, rootNote) {
if (!rootNote) { if (!rootNote) {
rootNote = await getRootCalendarNote(); rootNote = await getRootCalendarNote();
@@ -79,6 +81,7 @@ async function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
.replace(/{month}/g, monthName); .replace(/{month}/g, monthName);
} }
/** @return {Promise<Note>} */
async function getMonthNote(dateStr, rootNote) { async function getMonthNote(dateStr, rootNote) {
const monthStr = dateStr.substr(0, 7); const monthStr = dateStr.substr(0, 7);
const monthNumber = dateStr.substr(5, 2); const monthNumber = dateStr.substr(5, 2);
@@ -116,6 +119,7 @@ async function getDateNoteTitle(rootNote, dayNumber, dateObj) {
.replace(/{weekDay2}/g, weekDay.substr(0, 2)); .replace(/{weekDay2}/g, weekDay.substr(0, 2));
} }
/** @return {Promise<Note>} */
async function getDateNote(dateStr) { async function getDateNote(dateStr) {
const rootNote = await getRootCalendarNote(); const rootNote = await getRootCalendarNote();

5
src/services/env.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
isDev: function () {
return process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === 'dev';
}
};

View File

@@ -32,11 +32,11 @@ async function exportToTar(exportContext, branch, format, res) {
do { do {
index = existingFileNames[lcFileName]++; index = existingFileNames[lcFileName]++;
newName = lcFileName + "_" + index; newName = index + "_" + lcFileName;
} }
while (newName in existingFileNames); while (newName in existingFileNames);
return fileName + "_" + index; return index + "_" + fileName;
} }
else { else {
existingFileNames[lcFileName] = 1; existingFileNames[lcFileName] = 1;
@@ -46,24 +46,32 @@ async function exportToTar(exportContext, branch, format, res) {
} }
function getDataFileName(note, baseFileName, existingFileNames) { function getDataFileName(note, baseFileName, existingFileNames) {
let extension; const existingExtension = path.extname(baseFileName).toLowerCase();
let newExtension;
// following two are handled specifically since we always want to have these extensions no matter the automatic detection
// and/or existing detected extensions in the note name
if (note.type === 'text' && format === 'markdown') { if (note.type === 'text' && format === 'markdown') {
extension = 'md'; newExtension = 'md';
}
else if (note.type === 'text' && format === 'html') {
newExtension = 'html';
} }
else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') { else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') {
extension = 'js'; newExtension = 'js';
}
else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it
newExtension = null;
} }
else { else {
extension = mimeTypes.extension(note.mime) || "dat"; newExtension = mimeTypes.extension(note.mime) || "dat";
} }
let fileName = baseFileName; let fileName = baseFileName;
const existingExtension = path.extname(fileName).toLowerCase();
// if the note is already named with extension (e.g. "jquery.js"), then it's silly to append exact same extension again // if the note is already named with extension (e.g. "jquery.js"), then it's silly to append exact same extension again
if (existingExtension !== extension) { if (newExtension && existingExtension !== "." + newExtension.toLowerCase()) {
fileName += "." + extension; fileName += "." + newExtension;
} }
return getUniqueFilename(existingFileNames, fileName); return getUniqueFilename(existingFileNames, fileName);

10
src/services/host.js Normal file
View File

@@ -0,0 +1,10 @@
const config = require('./config');
const env = require('./env');
let environmentHost;
if (process.env.TRILIUM_HOST) {
environmentHost = process.env.TRILIUM_HOST;
}
module.exports = Promise.resolve(environmentHost || config['Network']['host'] || '0.0.0.0');

View File

@@ -13,14 +13,20 @@ const imageType = require('image-type');
const sanitizeFilename = require('sanitize-filename'); const sanitizeFilename = require('sanitize-filename');
async function saveImage(buffer, originalName, parentNoteId, shrinkImageSwitch) { async function saveImage(buffer, originalName, parentNoteId, shrinkImageSwitch) {
const origImageFormat = imageType(buffer);
if (origImageFormat.ext === "webp") {
// JIMP does not support webp at the moment: https://github.com/oliver-moran/jimp/issues/144
shrinkImageSwitch = false;
}
const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(buffer, originalName) : buffer; const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(buffer, originalName) : buffer;
const imageFormat = imageType(finalImageBuffer); const imageFormat = imageType(finalImageBuffer);
const parentNote = await repository.getNote(parentNoteId); const parentNote = await repository.getNote(parentNoteId);
const fileNameWithoutExtension = originalName.replace(/\.[^/.]+$/, ""); const fileName = sanitizeFilename(originalName);
const fileName = sanitizeFilename(fileNameWithoutExtension + "." + imageFormat.ext);
const {note} = await noteService.createNote(parentNoteId, fileName, finalImageBuffer, { const {note} = await noteService.createNote(parentNoteId, fileName, finalImageBuffer, {
target: 'into', target: 'into',
@@ -48,7 +54,7 @@ async function shrinkImage(buffer, originalName) {
try { try {
finalImageBuffer = await optimize(resizedImage); finalImageBuffer = await optimize(resizedImage);
} catch (e) { } catch (e) {
log.error("Failed to optimize image '" + originalName + "\nStack: " + e.stack); log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack);
finalImageBuffer = resizedImage; finalImageBuffer = resizedImage;
} }
@@ -93,7 +99,7 @@ async function optimize(buffer) {
quality: 50 quality: 50
}), }),
imageminPngQuant({ imageminPngQuant({
quality: "0-70" quality: [0, 0.7]
}), }),
imageminGifLossy({ imageminGifLossy({
lossy: 80, lossy: 80,

View File

@@ -251,7 +251,7 @@ async function importEnex(importContext, file, parentNote) {
noteContent = noteContent.replace(mediaRegex, resourceLink); noteContent = noteContent.replace(mediaRegex, resourceLink);
}; };
if (["image/jpeg", "image/png", "image/gif"].includes(resource.mime)) { if (["image/jpeg", "image/png", "image/gif", "image/webp"].includes(resource.mime)) {
try { try {
const originalName = "image." + resource.mime.substr(6); const originalName = "image." + resource.mime.substr(6);

View File

@@ -75,8 +75,6 @@ function getMime(fileName) {
const ext = path.extname(fileName).toLowerCase(); const ext = path.extname(fileName).toLowerCase();
if (ext in EXTENSION_TO_MIME) { if (ext in EXTENSION_TO_MIME) {
console.log(EXTENSION_TO_MIME[ext]);
return EXTENSION_TO_MIME[ext]; return EXTENSION_TO_MIME[ext];
} }
@@ -100,7 +98,7 @@ async function importSingleFile(importContext, file, parentNote) {
return await importCodeNote(importContext, file, parentNote); return await importCodeNote(importContext, file, parentNote);
} }
if (["image/jpeg", "image/gif", "image/png"].includes(mime)) { if (["image/jpeg", "image/gif", "image/png", "image/webp"].includes(mime)) {
return await importImage(file, parentNote, importContext); return await importImage(file, parentNote, importContext);
} }
@@ -108,7 +106,7 @@ async function importSingleFile(importContext, file, parentNote) {
} }
async function importImage(file, parentNote, importContext) { async function importImage(file, parentNote, importContext) {
const {note} = await imageService.saveImage(file.buffer, getFileNameWithoutExtension(file.originalname), parentNote.noteId, importContext.shrinkImages); const {note} = await imageService.saveImage(file.buffer, file.originalname, parentNote.noteId, importContext.shrinkImages);
importContext.increaseProgressCount(); importContext.increaseProgressCount();

View File

@@ -1,10 +1,28 @@
const getPort = require('get-port');
const config = require('./config'); const config = require('./config');
const utils = require('./utils'); const utils = require('./utils');
const env = require('./env');
const portscanner = require('portscanner');
let environmentPort;
if (process.env.TRILIUM_PORT) {
environmentPort = parseInt(process.env.TRILIUM_PORT);
}
if (utils.isElectron()) { if (utils.isElectron()) {
module.exports = getPort(); module.exports = new Promise((resolve, reject) => {
const startingPort = environmentPort || (env.isDev() ? 37740 : 37840);
portscanner.findAPortNotInUse(startingPort, startingPort + 10, '127.0.0.1', function(error, port) {
if (error) {
reject(error);
}
else {
resolve(port);
}
})
});
} }
else { else {
module.exports = Promise.resolve(config['Network']['port'] || '3000'); module.exports = Promise.resolve(environmentPort || config['Network']['port'] || '3000');
} }

View File

@@ -1,14 +0,0 @@
const test = require('tape');
const data_encryption = require('../services/data_encryption');
test('encrypt & decrypt', t => {
const dataKey = [1,2,3];
const iv = [4,5,6];
const plainText = "Hello World!";
const cipherText = data_encryption.encrypt(dataKey, iv, plainText);
const decodedPlainText = data_encryption.decrypt(dataKey, iv, cipherText);
t.equal(decodedPlainText, plainText);
t.end();
});

View File

@@ -72,7 +72,7 @@
document.cookie = name + "=" + (value || "") + expires + "; path=/"; document.cookie = name + "=" + (value || "") + expires + "; path=/";
} }
</script> </script>
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</body> </body>
</html> </html>

View File

@@ -49,7 +49,9 @@
</div> </div>
<div class="note-detail-component-wrapper"> <div class="note-detail-component-wrapper">
<div class="note-detail-text note-detail-component" tabindex="10000"></div> <div class="note-detail-text note-detail-component" tabindex="10000">
<div class="note-detail-text-editor"></div>
</div>
<div class="note-detail-code note-detail-component"></div> <div class="note-detail-code note-detail-component"></div>

View File

@@ -11,9 +11,13 @@
<table class="note-detail-promoted-attributes"></table> <table class="note-detail-promoted-attributes"></table>
<div class="note-detail-component-wrapper"> <div class="note-detail-component-wrapper">
<div class="note-detail-text note-detail-component" tabindex="10000"></div> <div class="note-detail-text note-detail-component">
<div class="note-detail-text-editor" tabindex="10000"></div>
</div>
<div class="note-detail-code note-detail-component"></div> <div class="note-detail-code note-detail-component">
<div class="note-detail-code-editor"></div>
</div>
<% include details/empty.ejs %> <% include details/empty.ejs %>

View File

@@ -69,6 +69,7 @@
<a class="dropdown-item show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }">Note source</a> <a class="dropdown-item show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }">Note source</a>
<a class="dropdown-item import-files-button">Import files</a> <a class="dropdown-item import-files-button">Import files</a>
<a class="dropdown-item export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a> <a class="dropdown-item export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a>
<a class="dropdown-item print-note-button">Print note</a>
<a class="dropdown-item show-note-info-button">Note info</a> <a class="dropdown-item show-note-info-button">Note info</a>
</div> </div>
</div> </div>

View File

@@ -25,6 +25,7 @@ const messagingService = require('./services/messaging');
const utils = require('./services/utils'); const utils = require('./services/utils');
const sqlInit = require('./services/sql_init'); const sqlInit = require('./services/sql_init');
const port = require('./services/port'); const port = require('./services/port');
const host = require('./services/host');
const semver = require('semver'); const semver = require('semver');
if (!semver.satisfies(process.version, ">=10.5.0")) { if (!semver.satisfies(process.version, ">=10.5.0")) {
@@ -36,8 +37,10 @@ let httpServer;
async function startTrilium() { async function startTrilium() {
const usedPort = await port; const usedPort = await port;
const usedHost = await host;
app.set('port', usedPort); app.set('port', usedPort);
app.set('host', usedHost);
if (config['Network']['https']) { if (config['Network']['https']) {
if (!config['Network']['keyPath'] || !config['Network']['keyPath'].trim().length) { if (!config['Network']['keyPath'] || !config['Network']['keyPath'].trim().length) {
@@ -70,7 +73,7 @@ async function startTrilium() {
*/ */
httpServer.keepAliveTimeout = 120000 * 5; httpServer.keepAliveTimeout = 120000 * 5;
httpServer.listen(usedPort); httpServer.listen(usedPort, usedHost);
httpServer.on('error', onError); httpServer.on('error', onError);
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port)); httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
@@ -108,4 +111,4 @@ function onError(error) {
default: default:
throw error; throw error;
} }
} }