mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/stable'
This commit is contained in:
		@@ -61,9 +61,11 @@ async function getRenderedContent(note, options = {}) {
 | 
				
			|||||||
        $renderedContent.append($("<pre>").text(trim(fullNote.content, options.trim)));
 | 
					        $renderedContent.append($("<pre>").text(trim(fullNote.content, options.trim)));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (type === 'image') {
 | 
					    else if (type === 'image') {
 | 
				
			||||||
 | 
					        const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $renderedContent.append(
 | 
					        $renderedContent.append(
 | 
				
			||||||
            $("<img>")
 | 
					            $("<img>")
 | 
				
			||||||
                .attr("src", `api/images/${note.noteId}/${note.title}`)
 | 
					                .attr("src", `api/images/${note.noteId}/${sanitizedTitle}`)
 | 
				
			||||||
                .css("max-width", "100%")
 | 
					                .css("max-width", "100%")
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -266,7 +266,7 @@ class NoteListRenderer {
 | 
				
			|||||||
                    .append($expander)
 | 
					                    .append($expander)
 | 
				
			||||||
                    .append($('<span class="note-icon">').addClass(note.getIcon()))
 | 
					                    .append($('<span class="note-icon">').addClass(note.getIcon()))
 | 
				
			||||||
                    .append(this.viewType === 'grid'
 | 
					                    .append(this.viewType === 'grid'
 | 
				
			||||||
                        ? note.title
 | 
					                        ? $("<span>").text(note.title)
 | 
				
			||||||
                        : await linkService.createNoteLink(notePath, {showTooltip: false, showNotePath: this.showNotePath})
 | 
					                        : await linkService.createNoteLink(notePath, {showTooltip: false, showNotePath: this.showNotePath})
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    .append($renderedAttributes)
 | 
					                    .append($renderedAttributes)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -503,7 +503,7 @@ export default class TabManager extends Component {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    updateDocumentTitle(activeNoteContext) {
 | 
					    updateDocumentTitle(activeNoteContext) {
 | 
				
			||||||
        const titleFragments = [
 | 
					        const titleFragments = [
 | 
				
			||||||
            // it helps navigating in history if note title is included in the title
 | 
					            // it helps to navigate in history if note title is included in the title
 | 
				
			||||||
            activeNoteContext.note?.title,
 | 
					            activeNoteContext.note?.title,
 | 
				
			||||||
            "Trilium Notes"
 | 
					            "Trilium Notes"
 | 
				
			||||||
        ].filter(Boolean);
 | 
					        ].filter(Boolean);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,16 +4,17 @@ import utils from "./utils.js";
 | 
				
			|||||||
function toast(options) {
 | 
					function toast(options) {
 | 
				
			||||||
    const $toast = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
 | 
					    const $toast = $(`<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
 | 
				
			||||||
    <div class="toast-header">
 | 
					    <div class="toast-header">
 | 
				
			||||||
        <strong class="mr-auto"><span class="bx bx-${options.icon}"></span> ${options.title}</strong>
 | 
					        <strong class="mr-auto"><span class="bx bx-${options.icon}"></span> <span class="toast-title"></span></strong>
 | 
				
			||||||
        <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
 | 
					        <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
 | 
				
			||||||
            <span aria-hidden="true">×</span>
 | 
					            <span aria-hidden="true">×</span>
 | 
				
			||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="toast-body">
 | 
					    <div class="toast-body"></div>
 | 
				
			||||||
        ${options.message}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</div>`);
 | 
					</div>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $toast.find('.toast-title').text(options.title);
 | 
				
			||||||
 | 
					    $toast.find('.toast-body').text(options.message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (options.id) {
 | 
					    if (options.id) {
 | 
				
			||||||
        $toast.attr("id", "toast-" + options.id);
 | 
					        $toast.attr("id", "toast-" + options.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -297,7 +297,7 @@ export default class ApperanceOptions {
 | 
				
			|||||||
            this.$themeSelect.append($("<option>")
 | 
					            this.$themeSelect.append($("<option>")
 | 
				
			||||||
                .attr("value", theme.val)
 | 
					                .attr("value", theme.val)
 | 
				
			||||||
                .attr("data-note-id", theme.noteId)
 | 
					                .attr("data-note-id", theme.noteId)
 | 
				
			||||||
                .html(theme.title));
 | 
					                .text(theme.title));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$themeSelect.val(options.theme);
 | 
					        this.$themeSelect.val(options.theme);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,7 +77,9 @@ export default class EditedNotesWidget extends CollapsibleWidget {
 | 
				
			|||||||
                );
 | 
					                );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
                $item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title);
 | 
					                $item.append(editedNote.notePath
 | 
				
			||||||
 | 
					                    ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true})
 | 
				
			||||||
 | 
					                    : $("<span>").text(editedNote.title));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (i < editedNotes.length - 1) {
 | 
					            if (i < editedNotes.length - 1) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -311,7 +311,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
 | 
				
			|||||||
        const note = await froca.getNote(noteId);
 | 
					        const note = await froca.getNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.textEditor.model.change( writer => {
 | 
					        this.textEditor.model.change( writer => {
 | 
				
			||||||
            const src = `api/images/${note.noteId}/${note.title}`;
 | 
					            const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
 | 
				
			||||||
 | 
					            const src = `api/images/${note.noteId}/${sanitizedTitle}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const imageElement = writer.createElement( 'image',  { 'src': src } );
 | 
					            const imageElement = writer.createElement( 'image',  { 'src': src } );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,7 +79,7 @@ export default class EmptyTypeWidget extends TypeWidget {
 | 
				
			|||||||
            this.$workspaceNotes.append(
 | 
					            this.$workspaceNotes.append(
 | 
				
			||||||
                $('<div class="workspace-note">')
 | 
					                $('<div class="workspace-note">')
 | 
				
			||||||
                    .append($("<div>").addClass(workspaceNote.getIcon() + " workspace-icon"))
 | 
					                    .append($("<div>").addClass(workspaceNote.getIcon() + " workspace-icon"))
 | 
				
			||||||
                    .append($("<div>").append(workspaceNote.title))
 | 
					                    .append($("<div>").text(workspaceNote.title))
 | 
				
			||||||
                    .attr("title", "Enter workspace " + workspaceNote.title)
 | 
					                    .attr("title", "Enter workspace " + workspaceNote.title)
 | 
				
			||||||
                    .on('click', () => this.triggerCommand('hoistNote', {noteId: workspaceNote.noteId}))
 | 
					                    .on('click', () => this.triggerCommand('hoistNote', {noteId: workspaceNote.noteId}))
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,7 +43,7 @@ function getClipperInboxNote() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function addClipping(req) {
 | 
					function addClipping(req) {
 | 
				
			||||||
    const {title, content, pageUrl, images} = req.body;
 | 
					    let {title, content, pageUrl, images} = req.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const clipperInbox = getClipperInboxNote();
 | 
					    const clipperInbox = getClipperInboxNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,6 +57,8 @@ function addClipping(req) {
 | 
				
			|||||||
            type: 'text'
 | 
					            type: 'text'
 | 
				
			||||||
        }).note;
 | 
					        }).note;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pageUrl = htmlSanitizer.sanitize(pageUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        clippingNote.setLabel('clipType', 'clippings');
 | 
					        clippingNote.setLabel('clipType', 'clippings');
 | 
				
			||||||
        clippingNote.setLabel('pageUrl', pageUrl);
 | 
					        clippingNote.setLabel('pageUrl', pageUrl);
 | 
				
			||||||
        clippingNote.setLabel('iconClass', 'bx bx-globe');
 | 
					        clippingNote.setLabel('iconClass', 'bx bx-globe');
 | 
				
			||||||
@@ -89,9 +91,13 @@ function createNote(req) {
 | 
				
			|||||||
        type: 'text'
 | 
					        type: 'text'
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    clipType = htmlSanitizer.sanitize(clipType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    note.setLabel('clipType', clipType);
 | 
					    note.setLabel('clipType', clipType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (pageUrl) {
 | 
					    if (pageUrl) {
 | 
				
			||||||
 | 
					        pageUrl = htmlSanitizer.sanitize(pageUrl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        note.setLabel('pageUrl', pageUrl);
 | 
					        note.setLabel('pageUrl', pageUrl);
 | 
				
			||||||
        note.setLabel('iconClass', 'bx bx-globe');
 | 
					        note.setLabel('iconClass', 'bx bx-globe');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,8 @@ const sanitizeHtml = require('sanitize-html');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// intended mainly as protection against XSS via import
 | 
					// intended mainly as protection against XSS via import
 | 
				
			||||||
// secondarily it (partly) protects against "CSS takeover"
 | 
					// secondarily it (partly) protects against "CSS takeover"
 | 
				
			||||||
 | 
					// sanitize also note titles, label values etc. - there's so many usage which make it difficult to guarantee all of them
 | 
				
			||||||
 | 
					// are properly handled
 | 
				
			||||||
function sanitize(dirtyHtml) {
 | 
					function sanitize(dirtyHtml) {
 | 
				
			||||||
    if (!dirtyHtml) {
 | 
					    if (!dirtyHtml) {
 | 
				
			||||||
        return dirtyHtml;
 | 
					        return dirtyHtml;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ const sanitizeFilename = require('sanitize-filename');
 | 
				
			|||||||
const noteRevisionService = require('./note_revisions');
 | 
					const noteRevisionService = require('./note_revisions');
 | 
				
			||||||
const isSvg = require('is-svg');
 | 
					const isSvg = require('is-svg');
 | 
				
			||||||
const isAnimated = require('is-animated');
 | 
					const isAnimated = require('is-animated');
 | 
				
			||||||
 | 
					const htmlSanitizer = require("./html_sanitizer");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
 | 
					async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
 | 
				
			||||||
    const compressImages = optionService.getOptionBool("compressImages");
 | 
					    const compressImages = optionService.getOptionBool("compressImages");
 | 
				
			||||||
@@ -66,6 +67,8 @@ function getImageMimeFromExtension(ext) {
 | 
				
			|||||||
function updateImage(noteId, uploadBuffer, originalName) {
 | 
					function updateImage(noteId, uploadBuffer, originalName) {
 | 
				
			||||||
    log.info(`Updating image ${noteId}: ${originalName}`);
 | 
					    log.info(`Updating image ${noteId}: ${originalName}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    originalName = htmlSanitizer.sanitize(originalName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    note.saveNoteRevision();
 | 
					    note.saveNoteRevision();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -160,6 +160,11 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
 | 
				
			|||||||
                attr.name = 'disabled:' + attr.name;
 | 
					                attr.name = 'disabled:' + attr.name;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (taskContext.data.safeImport) {
 | 
				
			||||||
 | 
					                attr.name = htmlSanitizer.sanitize(attr.name);
 | 
				
			||||||
 | 
					                attr.value = htmlSanitizer.sanitize(attr.value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            attributes.push(attr);
 | 
					            attributes.push(attr);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ const Branch = require('../becca/entities/branch');
 | 
				
			|||||||
const Note = require('../becca/entities/note');
 | 
					const Note = require('../becca/entities/note');
 | 
				
			||||||
const Attribute = require('../becca/entities/attribute');
 | 
					const Attribute = require('../becca/entities/attribute');
 | 
				
			||||||
const dayjs = require("dayjs");
 | 
					const dayjs = require("dayjs");
 | 
				
			||||||
 | 
					const htmlSanitizer = require("./html_sanitizer.js");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getNewNotePosition(parentNoteId) {
 | 
					function getNewNotePosition(parentNoteId) {
 | 
				
			||||||
    const note = becca.notes[parentNoteId];
 | 
					    const note = becca.notes[parentNoteId];
 | 
				
			||||||
@@ -98,6 +99,11 @@ function getNewNoteTitle(parentNote) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // this isn't in theory a good place to sanitize title, but this will catch a lot of XSS attempts
 | 
				
			||||||
 | 
					    // title is supposed to contain text only (not HTML) and be printed text only, but given the number of usages
 | 
				
			||||||
 | 
					    // it's difficult to guarantee correct handling in all cases
 | 
				
			||||||
 | 
					    title = htmlSanitizer.sanitize(title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return title;
 | 
					    return title;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -352,8 +358,10 @@ function downloadImages(noteId, content) {
 | 
				
			|||||||
            const imageService = require('../services/image');
 | 
					            const imageService = require('../services/image');
 | 
				
			||||||
            const {note} = imageService.saveImage(noteId, imageBuffer, "inline image", true, true);
 | 
					            const {note} = imageService.saveImage(noteId, imageBuffer, "inline image", true, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            content = content.substr(0, imageMatch.index)
 | 
					            content = content.substr(0, imageMatch.index)
 | 
				
			||||||
                + `<img src="api/images/${note.noteId}/${note.title}"`
 | 
					                + `<img src="api/images/${note.noteId}/${sanitizedTitle}"`
 | 
				
			||||||
                + content.substr(imageMatch.index + imageMatch[0].length);
 | 
					                + content.substr(imageMatch.index + imageMatch[0].length);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (!url.includes('api/images/')
 | 
					        else if (!url.includes('api/images/')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -241,7 +241,7 @@ function getNoteTitle(filePath, replaceUnderscoresWithSpaces, noteMeta) {
 | 
				
			|||||||
        return noteMeta.title;
 | 
					        return noteMeta.title;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        const basename = path.basename(removeTextFileExtension(filePath));
 | 
					        const basename = path.basename(removeTextFileExtension(filePath));
 | 
				
			||||||
        if(replaceUnderscoresWithSpaces) {
 | 
					        if (replaceUnderscoresWithSpaces) {
 | 
				
			||||||
            return basename.replace(/_/g, ' ').trim();
 | 
					            return basename.replace(/_/g, ' ').trim();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return basename;
 | 
					        return basename;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user