From 3670fbff492220f01f02133c19b8b78e83bcb0a0 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 17 Aug 2020 23:54:18 +0200 Subject: [PATCH] frontend validation of attribute name + other changes and fixes --- spec-es6/attribute_parser.spec.js | 20 +++++----- .../app/layouts/desktop_main_window_layout.js | 2 +- src/public/app/services/attribute_parser.js | 37 +++++++++---------- src/public/app/widgets/attribute_detail.js | 19 +++++++--- src/public/app/widgets/attribute_editor.js | 35 +++++++++++------- src/public/stylesheets/style.css | 2 +- src/services/note_cache/note_cache_service.js | 6 +++ 7 files changed, 71 insertions(+), 50 deletions(-) diff --git a/spec-es6/attribute_parser.spec.js b/spec-es6/attribute_parser.spec.js index 16c3b496b..62c95c3b0 100644 --- a/spec-es6/attribute_parser.spec.js +++ b/spec-es6/attribute_parser.spec.js @@ -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('

~relation = note 

')) - .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(); diff --git a/src/public/app/layouts/desktop_main_window_layout.js b/src/public/app/layouts/desktop_main_window_layout.js index 9f39417f5..c2055de91 100644 --- a/src/public/app/layouts/desktop_main_window_layout.js +++ b/src/public/app/layouts/desktop_main_window_layout.js @@ -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()) diff --git a/src/public/app/services/attribute_parser.js b/src/public/app/services/attribute_parser.js index 0513e11d6..02eb8a63b 100644 --- a/src/public/app/services/attribute_parser.js +++ b/src/public/app/services/attribute_parser.js @@ -1,17 +1,3 @@ -function preprocess(str) { - if (str.startsWith('

')) { - str = str.substr(3); - } - - if (str.endsWith('

')) { - str = str.substr(0, str.length - 4); - } - - str = str.replace(/ /g, " "); - - return str.replace(/]+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 diff --git a/src/public/app/widgets/attribute_detail.js b/src/public/app/widgets/attribute_detail.js index 31efef0a8..c494e325e 100644 --- a/src/public/app/widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_detail.js @@ -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') { diff --git a/src/public/app/widgets/attribute_editor.js b/src/public/app/widgets/attribute_editor.js index 5f076dbea..5fd72b614 100644 --- a/src/public/app/widgets/attribute_editor.js +++ b/src/public/app/widgets/attribute_editor.js @@ -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(/]+href="(#[A-Za-z0-9/]*)"[^>]*>[^<]*<\/a>/g, "$1"); + + return $("
").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); } } diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 2b060f344..84859848d 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -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 { diff --git a/src/services/note_cache/note_cache_service.js b/src/services/note_cache/note_cache_service.js index 2f2eb7536..5eaa725b5 100644 --- a/src/services/note_cache/note_cache_service.js +++ b/src/services/note_cache/note_cache_service.js @@ -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) {