diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 4a3a236e7..39c709023 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -707,6 +707,18 @@ messagingService.subscribeToMessages(message => { if (message.type === 'refresh-tree') { reload(); } + else if (message.type === 'open-note') { + noteDetailService.loadNoteDetail(message.noteId, { + newTab: true, + activate: true + }); + + if (utils.isElectron()) { + const currentWindow = require("electron").remote.getCurrentWindow(); + + currentWindow.show(); + } + } }); messagingService.subscribeToSyncMessages(syncData => { diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 34b03edb6..83b29dba1 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -832,4 +832,8 @@ a.external:after, a[href^="http://"]:after, a[href^="https://"]:after { .note-detail-empty { margin: 50px; +} + +.modal-header { + padding: 0.7rem 1rem !important; /* make modal header padding slightly smaller */ } \ No newline at end of file diff --git a/src/routes/api/clipper.js b/src/routes/api/clipper.js new file mode 100644 index 000000000..5cb5099e7 --- /dev/null +++ b/src/routes/api/clipper.js @@ -0,0 +1,121 @@ +"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 messagingService = require('../../services/messaging'); +const log = require('../../services/log'); +const path = require('path'); +const Link = require('../../entities/link'); + +async function createNote(req) { + const {title, html, url, images} = req.body; + + const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate()); + + const {note} = await noteService.createNote(todayNote.noteId, title, html, { + attributes: [ + { + type: 'label', + name: 'sourceUrl', + value: url + } + ] + }); + + let rewrittenHtml = html; + + 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}`); + + rewrittenHtml = rewrittenHtml.replace(imageId, url); + } + + console.log("Done", rewrittenHtml); + + await note.setContent(rewrittenHtml); + + return { + noteId: note.noteId + }; +} + +async function createImage(req) { + let {dataUrl, title, sourceUrl, pageUrl} = req.body; + + if (!dataUrl) { + dataUrl = sourceUrl; + sourceUrl = null; + } + + if (!dataUrl.startsWith("data:image/")) { + const message = "Unrecognized prefix: " + dataUrl.substr(0, Math.min(dataUrl.length, 100)); + log.info(message); + + return [400, message]; + } + + if (!title && sourceUrl) { + title = path.basename(sourceUrl); + } + + if (!title) { + title = "clipped image"; + } + + const buffer = Buffer.from(dataUrl.split(",")[1], 'base64'); + + const todayNote = await dateNoteService.getDateNote(dateUtils.localNowDate()); + + const {note} = await imageService.saveImage(buffer, title, todayNote.noteId, true); + + if (sourceUrl) { + await note.setLabel('sourceUrl', sourceUrl); + } + + if (pageUrl) { + await note.setLabel('pageUrl', pageUrl); + } + + return { + noteId: note.noteId + }; +} + +async function openNote(req) { + messagingService.sendMessageToAllClients({ + type: 'open-note', + noteId: req.params.noteId + }); +} + +async function ping(req, res) { + console.log("PING!!!!"); + + res.status(200).send("TriliumClipperServer"); +} + +module.exports = { + createNote, + createImage, + openNote, + ping +}; \ No newline at end of file diff --git a/src/routes/api/image.js b/src/routes/api/image.js index f22903799..a0f6ba4aa 100644 --- a/src/routes/api/image.js +++ b/src/routes/api/image.js @@ -34,7 +34,7 @@ async function uploadImage(req) { 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]; } diff --git a/src/routes/api/login.js b/src/routes/api/login.js index d661b5b87..e3ed0c5e5 100644 --- a/src/routes/api/login.js +++ b/src/routes/api/login.js @@ -11,6 +11,8 @@ const eventService = require('../../services/events'); const cls = require('../../services/cls'); const sqlInit = require('../../services/sql_init'); const sql = require('../../services/sql'); +const optionService = require('../../services/options'); +const ApiToken = require('../../entities/api_token'); async function loginSync(req) { 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 = { loginSync, - loginToProtectedSession + loginToProtectedSession, + token }; \ No newline at end of file diff --git a/src/routes/api/sender.js b/src/routes/api/sender.js index 81a61b245..c1f59b5ee 100644 --- a/src/routes/api/sender.js +++ b/src/routes/api/sender.js @@ -1,33 +1,8 @@ "use strict"; const imageService = require('../../services/image'); -const utils = require('../../services/utils'); const dateNoteService = require('../../services/date_notes'); -const sql = require('../../services/sql'); 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) { const file = req.file; @@ -64,7 +39,6 @@ async function saveNote(req) { } module.exports = { - login, uploadImage, saveNote }; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index 9b8655dae..232687057 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -32,6 +32,7 @@ const filesRoute = require('./api/file_upload'); const searchRoute = require('./api/search'); const dateNotesRoute = require('./api/date_notes'); const linkMapRoute = require('./api/link_map'); +const clipperRoute = require('./api/clipper'); const log = require('../services/log'); const express = require('express'); @@ -212,7 +213,7 @@ function register(app) { apiRoute(GET, '/api/script/relation/:noteId/:relationName', scriptRoute.getRelationBundles); // 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/note', [auth.checkSenderToken], senderRoute.saveNote, apiResultHandler); @@ -222,6 +223,12 @@ function register(app) { 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) apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession); + route(POST, '/api/login/token', [], loginApiRoute.token, apiResultHandler); + + route(POST, '/api/clipper/notes', [], clipperRoute.createNote, apiResultHandler); + route(POST, '/api/clipper/image', [], clipperRoute.createImage, apiResultHandler); + route(POST, '/api/clipper/open/:noteId', [], clipperRoute.openNote, apiResultHandler); + route(GET, '/api/clipper/ping', [], clipperRoute.ping); app.use('', router); } diff --git a/src/services/app_info.js b/src/services/app_info.js index 2787ba67c..6b59dc536 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -6,6 +6,7 @@ const {TRILIUM_DATA_DIR} = require('./data_dir'); const APP_DB_VERSION = 136; const SYNC_VERSION = 9; +const CLIPPER_VERSION = 1; module.exports = { appVersion: packageJson.version, diff --git a/src/services/image.js b/src/services/image.js index 90e34139c..9712a3873 100644 --- a/src/services/image.js +++ b/src/services/image.js @@ -13,6 +13,13 @@ const imageType = require('image-type'); const sanitizeFilename = require('sanitize-filename'); 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 imageFormat = imageType(finalImageBuffer); @@ -48,7 +55,7 @@ async function shrinkImage(buffer, originalName) { try { finalImageBuffer = await optimize(resizedImage); } catch (e) { - log.error("Failed to optimize image '" + originalName + "\nStack: " + e.stack); + log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack); finalImageBuffer = resizedImage; } @@ -93,7 +100,7 @@ async function optimize(buffer) { quality: 50 }), imageminPngQuant({ - quality: "0-70" + quality: [0, 0.7] }), imageminGifLossy({ lossy: 80, diff --git a/src/services/import/enex.js b/src/services/import/enex.js index 6ec07fb1a..854be6f6b 100644 --- a/src/services/import/enex.js +++ b/src/services/import/enex.js @@ -251,7 +251,7 @@ async function importEnex(importContext, file, parentNote) { 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 { const originalName = "image." + resource.mime.substr(6); diff --git a/src/services/import/single.js b/src/services/import/single.js index 37ba50b4f..3dd1adb37 100644 --- a/src/services/import/single.js +++ b/src/services/import/single.js @@ -100,7 +100,7 @@ async function importSingleFile(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); } diff --git a/src/services/port.js b/src/services/port.js index 4748c98e6..fa6f7fda9 100644 --- a/src/services/port.js +++ b/src/services/port.js @@ -3,7 +3,7 @@ const config = require('./config'); const utils = require('./utils'); if (utils.isElectron()) { - module.exports = getPort(); + module.exports = 53010;//getPort(); } else { module.exports = Promise.resolve(config['Network']['port'] || '3000');