mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	frontend validation of attribute name + other changes and fixes
This commit is contained in:
		| @@ -1,13 +1,6 @@ | ||||
| import attributeParser from '../src/public/app/services/attribute_parser.js'; | ||||
| import {describe, it, expect, execute} from './mini_test.js'; | ||||
|  | ||||
| describe("Preprocessor", () => { | ||||
|     it("relation with value", () => { | ||||
|         expect(attributeParser.preprocess('<p>~relation = <a class="reference-link" href="#root/RclIpMauTOKS/NFi2gL4xtPxM" some-attr="abc" data-note-path="root/RclIpMauTOKS/NFi2gL4xtPxM">note</a> </p>')) | ||||
|             .toEqual("~relation = #root/RclIpMauTOKS/NFi2gL4xtPxM "); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| describe("Lexer", () => { | ||||
|     it("simple label", () => { | ||||
|         expect(attributeParser.lexer("#label").map(t => t.text)) | ||||
| @@ -95,11 +88,16 @@ describe("Parser", () => { | ||||
|         expect(attrs[0].name).toEqual("token"); | ||||
|         expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
|     // it("error cases", () => { | ||||
|     //     expect(() => attributeParser.parser(["~token"].map(t => ({text: t})), "~token")) | ||||
|     //         .toThrow('Relation "~token" should point to a note.'); | ||||
|     // }); | ||||
| describe("error cases", () => { | ||||
|     it("error cases", () => { | ||||
|         expect(() => attributeParser.lexAndParse('~token')) | ||||
|             .toThrow('Relation "~token" in "~token" should point to a note.'); | ||||
|  | ||||
|         expect(() => attributeParser.lexAndParse("#a&b/s")) | ||||
|             .toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| execute(); | ||||
|   | ||||
| @@ -131,7 +131,7 @@ export default class DesktopMainWindowLayout { | ||||
|                 .child(new FlexContainer('column').id('center-pane') | ||||
|                     .child(new FlexContainer('row').class('title-row') | ||||
|                         .cssBlock('.title-row > * { margin: 5px; }') | ||||
|                         .css('height', '55px') | ||||
|                         .overflowing() | ||||
|                         .child(new NoteTitleWidget()) | ||||
|                         .child(new RunScriptButtonsWidget().hideInZenMode()) | ||||
|                         .child(new NoteTypeWidget().hideInZenMode()) | ||||
|   | ||||
| @@ -1,17 +1,3 @@ | ||||
| function preprocess(str) { | ||||
|     if (str.startsWith('<p>')) { | ||||
|         str = str.substr(3); | ||||
|     } | ||||
|  | ||||
|     if (str.endsWith('</p>')) { | ||||
|         str = str.substr(0, str.length - 4); | ||||
|     } | ||||
|  | ||||
|     str = str.replace(/ /g, " "); | ||||
|  | ||||
|     return str.replace(/<a[^>]+href="(#[A-Za-z0-9/]*)"[^>]*>[^<]*<\/a>/g, "$1"); | ||||
| } | ||||
|  | ||||
| function lexer(str) { | ||||
|     const tokens = []; | ||||
|  | ||||
| @@ -117,6 +103,14 @@ function lexer(str) { | ||||
|     return tokens; | ||||
| } | ||||
|  | ||||
| const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); | ||||
|  | ||||
| function checkAttributeName(attrName) { | ||||
|     if (!attrNameMatcher.test(attrName)) { | ||||
|         throw new Error(`Attribute name "${attrName}" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function parser(tokens, str, allowEmptyRelations = false) { | ||||
|     const attrs = []; | ||||
|  | ||||
| @@ -149,9 +143,13 @@ function parser(tokens, str, allowEmptyRelations = false) { | ||||
|         } | ||||
|  | ||||
|         if (text.startsWith('#')) { | ||||
|             const labelName = text.substr(1); | ||||
|  | ||||
|             checkAttributeName(labelName); | ||||
|  | ||||
|             const attr = { | ||||
|                 type: 'label', | ||||
|                 name: text.substr(1), | ||||
|                 name: labelName, | ||||
|                 isInheritable: isInheritable(), | ||||
|                 startIndex: startIndex, | ||||
|                 endIndex: tokens[i].endIndex // i could be moved by isInheritable | ||||
| @@ -171,9 +169,13 @@ function parser(tokens, str, allowEmptyRelations = false) { | ||||
|             attrs.push(attr); | ||||
|         } | ||||
|         else if (text.startsWith('~')) { | ||||
|             const relationName = text.substr(1); | ||||
|  | ||||
|             checkAttributeName(relationName); | ||||
|  | ||||
|             const attr = { | ||||
|                 type: 'relation', | ||||
|                 name: text.substr(1), | ||||
|                 name: relationName, | ||||
|                 isInheritable: isInheritable(), | ||||
|                 startIndex: startIndex, | ||||
|                 endIndex: tokens[i].endIndex // i could be moved by isInheritable | ||||
| @@ -211,15 +213,12 @@ function parser(tokens, str, allowEmptyRelations = false) { | ||||
| } | ||||
|  | ||||
| function lexAndParse(str, allowEmptyRelations = false) { | ||||
|     str = preprocess(str); | ||||
|  | ||||
|     const tokens = lexer(str); | ||||
|  | ||||
|     return parser(tokens, str, allowEmptyRelations); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     preprocess, | ||||
|     lexer, | ||||
|     parser, | ||||
|     lexAndParse | ||||
|   | ||||
| @@ -158,6 +158,8 @@ const ATTR_TITLES = { | ||||
|     "relation-definition": "Relation definition detail" | ||||
| }; | ||||
|  | ||||
| const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); | ||||
|  | ||||
| export default class AttributeDetailWidget extends TabAwareWidget { | ||||
|     async refresh() { | ||||
|         // this widget is not activated in a standard way | ||||
| @@ -280,7 +282,7 @@ export default class AttributeDetailWidget extends TabAwareWidget { | ||||
|  | ||||
|             return; | ||||
|         } | ||||
| console.log("RENDERING"); | ||||
|  | ||||
|         this.attrType = this.getAttrType(attribute); | ||||
|  | ||||
|         const attrName = | ||||
| @@ -365,16 +367,16 @@ console.log("RENDERING"); | ||||
|  | ||||
|         this.toggleInt(true); | ||||
|  | ||||
|         this.$widget.css("left", x - this.$widget.outerWidth() / 2); | ||||
|         this.$widget.css("top", y + 25); | ||||
|         const offset = this.parent.$widget.offset(); | ||||
|  | ||||
|         this.$widget.css("left", x - offset.left - this.$widget.outerWidth() / 2); | ||||
|         this.$widget.css("top", y - offset.top + 70); | ||||
|  | ||||
|         // so that the detail window always fits | ||||
|         this.$widget.css("max-height", | ||||
|             this.$widget.outerHeight() + y > $(window).height() - 50 | ||||
|                         ? $(window).height() - y - 50 | ||||
|                         : 10000); | ||||
|  | ||||
|         console.log("RENDERING DONE"); | ||||
|     } | ||||
|  | ||||
|     async updateRelatedNotes() { | ||||
| @@ -435,6 +437,13 @@ console.log("RENDERING"); | ||||
|     updateAttributeInEditor() { | ||||
|         let attrName = this.$inputName.val(); | ||||
|  | ||||
|         if (!ATTR_NAME_MATCHER.test(attrName)) { | ||||
|             // invalid characters are simply ignored (from user perspective they are not even entered) | ||||
|             attrName = attrName.replace(/[^\p{L}\p{N}_:]/ug, ""); | ||||
|  | ||||
|             this.$inputName.val(attrName); | ||||
|         } | ||||
|  | ||||
|         if (this.attrType === 'label-definition') { | ||||
|             attrName = 'label:' + attrName; | ||||
|         } else if (this.attrType === 'relation-definition') { | ||||
|   | ||||
| @@ -293,15 +293,22 @@ export default class AttributeEditorWidget extends TabAwareWidget { | ||||
|  | ||||
|     parseAttributes() { | ||||
|         try { | ||||
|             const attrs = attributesParser.lexAndParse(this.textEditor.getData()); | ||||
|             const attrs = attributesParser.lexAndParse(this.getPreprocessedData()); | ||||
|  | ||||
|             return attrs; | ||||
|         } | ||||
|         catch (e) { | ||||
|             this.$errors.show().text(e.message); | ||||
|             this.$errors.text(e.message).slideDown(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getPreprocessedData() { | ||||
|         const str = this.textEditor.getData() | ||||
|             .replace(/<a[^>]+href="(#[A-Za-z0-9/]*)"[^>]*>[^<]*<\/a>/g, "$1"); | ||||
|  | ||||
|         return $("<div>").html(str).text(); | ||||
|     } | ||||
|  | ||||
|     async initEditor() { | ||||
|         await libraryLoader.requireLibrary(libraryLoader.CKEDITOR); | ||||
|  | ||||
| @@ -332,18 +339,18 @@ export default class AttributeEditorWidget extends TabAwareWidget { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async handleEditorClick(e) { | ||||
|     async handleEditorClick(e) {console.log("click") | ||||
|         const pos = this.textEditor.model.document.selection.getFirstPosition(); | ||||
|  | ||||
|         if (pos && pos.textNode && pos.textNode.data) { | ||||
|         if (pos && pos.textNode && pos.textNode.data) {console.log(pos); | ||||
|             const clickIndex = this.getClickIndex(pos); | ||||
|  | ||||
|             let parsedAttrs; | ||||
|  | ||||
|             try { | ||||
|                 parsedAttrs = attributesParser.lexAndParse(this.textEditor.getData(), true); | ||||
|                 parsedAttrs = attributesParser.lexAndParse(this.getPreprocessedData(), true); | ||||
|             } | ||||
|             catch (e) { | ||||
|             catch (e) {console.log(e); | ||||
|                 // the input is incorrect because user messed up with it and now needs to fix it manually | ||||
|                 return null; | ||||
|             } | ||||
| @@ -357,13 +364,15 @@ export default class AttributeEditorWidget extends TabAwareWidget { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.attributeDetailWidget.showAttributeDetail({ | ||||
|                 allAttributes: parsedAttrs, | ||||
|                 attribute: matchedAttr, | ||||
|                 isOwned: true, | ||||
|                 x: e.pageX, | ||||
|                 y: e.pageY | ||||
|             }); | ||||
|             setTimeout(() => { | ||||
|                 this.attributeDetailWidget.showAttributeDetail({ | ||||
|                     allAttributes: parsedAttrs, | ||||
|                     attribute: matchedAttr, | ||||
|                     isOwned: true, | ||||
|                     x: e.pageX, | ||||
|                     y: e.pageY | ||||
|                 }); | ||||
|             }, 100); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -636,7 +636,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href | ||||
| } | ||||
|  | ||||
| .component { | ||||
|     contain: strict; | ||||
|     contain: layout size; | ||||
| } | ||||
|  | ||||
| .toast { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ const noteCache = require('./note_cache'); | ||||
| const hoistedNoteService = require('../hoisted_note'); | ||||
| const protectedSessionService = require('../protected_session'); | ||||
| const stringSimilarity = require('string-similarity'); | ||||
| const log = require('../log'); | ||||
|  | ||||
| function isNotePathArchived(notePath) { | ||||
|     const noteId = notePath[notePath.length - 1]; | ||||
| @@ -62,6 +63,11 @@ function getNoteTitle(childNoteId, parentNoteId) { | ||||
|     const childNote = noteCache.notes[childNoteId]; | ||||
|     const parentNote = noteCache.notes[parentNoteId]; | ||||
|  | ||||
|     if (!childNote) { | ||||
|         log.info(`Cannot find note in cache for noteId ${childNoteId}`); | ||||
|         return "[error fetching title]"; | ||||
|     } | ||||
|  | ||||
|     let title; | ||||
|  | ||||
|     if (childNote.isProtected) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user