frontend validation of attribute name + other changes and fixes

This commit is contained in:
zadam 2020-08-17 23:54:18 +02:00
parent f24e27dadd
commit 3670fbff49
7 changed files with 71 additions and 50 deletions

View File

@ -1,13 +1,6 @@
import attributeParser from '../src/public/app/services/attribute_parser.js'; import attributeParser from '../src/public/app/services/attribute_parser.js';
import {describe, it, expect, execute} from './mini_test.js'; import {describe, it, expect, execute} from './mini_test.js';
describe("Preprocessor", () => {
it("relation with value", () => {
expect(attributeParser.preprocess('<p>~relation&nbsp;= <a class="reference-link" href="#root/RclIpMauTOKS/NFi2gL4xtPxM" some-attr="abc" data-note-path="root/RclIpMauTOKS/NFi2gL4xtPxM">note</a>&nbsp;</p>'))
.toEqual("~relation = #root/RclIpMauTOKS/NFi2gL4xtPxM ");
});
});
describe("Lexer", () => { describe("Lexer", () => {
it("simple label", () => { it("simple label", () => {
expect(attributeParser.lexer("#label").map(t => t.text)) expect(attributeParser.lexer("#label").map(t => t.text))
@ -95,11 +88,16 @@ describe("Parser", () => {
expect(attrs[0].name).toEqual("token"); expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); expect(attrs[0].value).toEqual('NFi2gL4xtPxM');
}); });
});
// it("error cases", () => { describe("error cases", () => {
// expect(() => attributeParser.parser(["~token"].map(t => ({text: t})), "~token")) it("error cases", () => {
// .toThrow('Relation "~token" should point to a note.'); 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(); execute();

View File

@ -131,7 +131,7 @@ export default class DesktopMainWindowLayout {
.child(new FlexContainer('column').id('center-pane') .child(new FlexContainer('column').id('center-pane')
.child(new FlexContainer('row').class('title-row') .child(new FlexContainer('row').class('title-row')
.cssBlock('.title-row > * { margin: 5px; }') .cssBlock('.title-row > * { margin: 5px; }')
.css('height', '55px') .overflowing()
.child(new NoteTitleWidget()) .child(new NoteTitleWidget())
.child(new RunScriptButtonsWidget().hideInZenMode()) .child(new RunScriptButtonsWidget().hideInZenMode())
.child(new NoteTypeWidget().hideInZenMode()) .child(new NoteTypeWidget().hideInZenMode())

View File

@ -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(/&nbsp;/g, " ");
return str.replace(/<a[^>]+href="(#[A-Za-z0-9/]*)"[^>]*>[^<]*<\/a>/g, "$1");
}
function lexer(str) { function lexer(str) {
const tokens = []; const tokens = [];
@ -117,6 +103,14 @@ function lexer(str) {
return tokens; 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) { function parser(tokens, str, allowEmptyRelations = false) {
const attrs = []; const attrs = [];
@ -149,9 +143,13 @@ function parser(tokens, str, allowEmptyRelations = false) {
} }
if (text.startsWith('#')) { if (text.startsWith('#')) {
const labelName = text.substr(1);
checkAttributeName(labelName);
const attr = { const attr = {
type: 'label', type: 'label',
name: text.substr(1), name: labelName,
isInheritable: isInheritable(), isInheritable: isInheritable(),
startIndex: startIndex, startIndex: startIndex,
endIndex: tokens[i].endIndex // i could be moved by isInheritable endIndex: tokens[i].endIndex // i could be moved by isInheritable
@ -171,9 +169,13 @@ function parser(tokens, str, allowEmptyRelations = false) {
attrs.push(attr); attrs.push(attr);
} }
else if (text.startsWith('~')) { else if (text.startsWith('~')) {
const relationName = text.substr(1);
checkAttributeName(relationName);
const attr = { const attr = {
type: 'relation', type: 'relation',
name: text.substr(1), name: relationName,
isInheritable: isInheritable(), isInheritable: isInheritable(),
startIndex: startIndex, startIndex: startIndex,
endIndex: tokens[i].endIndex // i could be moved by isInheritable 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) { function lexAndParse(str, allowEmptyRelations = false) {
str = preprocess(str);
const tokens = lexer(str); const tokens = lexer(str);
return parser(tokens, str, allowEmptyRelations); return parser(tokens, str, allowEmptyRelations);
} }
export default { export default {
preprocess,
lexer, lexer,
parser, parser,
lexAndParse lexAndParse

View File

@ -158,6 +158,8 @@ const ATTR_TITLES = {
"relation-definition": "Relation definition detail" "relation-definition": "Relation definition detail"
}; };
const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
export default class AttributeDetailWidget extends TabAwareWidget { export default class AttributeDetailWidget extends TabAwareWidget {
async refresh() { async refresh() {
// this widget is not activated in a standard way // this widget is not activated in a standard way
@ -280,7 +282,7 @@ export default class AttributeDetailWidget extends TabAwareWidget {
return; return;
} }
console.log("RENDERING");
this.attrType = this.getAttrType(attribute); this.attrType = this.getAttrType(attribute);
const attrName = const attrName =
@ -365,16 +367,16 @@ console.log("RENDERING");
this.toggleInt(true); this.toggleInt(true);
this.$widget.css("left", x - this.$widget.outerWidth() / 2); const offset = this.parent.$widget.offset();
this.$widget.css("top", y + 25);
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 // so that the detail window always fits
this.$widget.css("max-height", this.$widget.css("max-height",
this.$widget.outerHeight() + y > $(window).height() - 50 this.$widget.outerHeight() + y > $(window).height() - 50
? $(window).height() - y - 50 ? $(window).height() - y - 50
: 10000); : 10000);
console.log("RENDERING DONE");
} }
async updateRelatedNotes() { async updateRelatedNotes() {
@ -435,6 +437,13 @@ console.log("RENDERING");
updateAttributeInEditor() { updateAttributeInEditor() {
let attrName = this.$inputName.val(); 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') { if (this.attrType === 'label-definition') {
attrName = 'label:' + attrName; attrName = 'label:' + attrName;
} else if (this.attrType === 'relation-definition') { } else if (this.attrType === 'relation-definition') {

View File

@ -293,15 +293,22 @@ export default class AttributeEditorWidget extends TabAwareWidget {
parseAttributes() { parseAttributes() {
try { try {
const attrs = attributesParser.lexAndParse(this.textEditor.getData()); const attrs = attributesParser.lexAndParse(this.getPreprocessedData());
return attrs; return attrs;
} }
catch (e) { 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() { async initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR); 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(); 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); const clickIndex = this.getClickIndex(pos);
let parsedAttrs; let parsedAttrs;
try { 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 // the input is incorrect because user messed up with it and now needs to fix it manually
return null; return null;
} }
@ -357,13 +364,15 @@ export default class AttributeEditorWidget extends TabAwareWidget {
} }
} }
this.attributeDetailWidget.showAttributeDetail({ setTimeout(() => {
allAttributes: parsedAttrs, this.attributeDetailWidget.showAttributeDetail({
attribute: matchedAttr, allAttributes: parsedAttrs,
isOwned: true, attribute: matchedAttr,
x: e.pageX, isOwned: true,
y: e.pageY x: e.pageX,
}); y: e.pageY
});
}, 100);
} }
} }

View File

@ -636,7 +636,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
} }
.component { .component {
contain: strict; contain: layout size;
} }
.toast { .toast {

View File

@ -4,6 +4,7 @@ const noteCache = require('./note_cache');
const hoistedNoteService = require('../hoisted_note'); const hoistedNoteService = require('../hoisted_note');
const protectedSessionService = require('../protected_session'); const protectedSessionService = require('../protected_session');
const stringSimilarity = require('string-similarity'); const stringSimilarity = require('string-similarity');
const log = require('../log');
function isNotePathArchived(notePath) { function isNotePathArchived(notePath) {
const noteId = notePath[notePath.length - 1]; const noteId = notePath[notePath.length - 1];
@ -62,6 +63,11 @@ function getNoteTitle(childNoteId, parentNoteId) {
const childNote = noteCache.notes[childNoteId]; const childNote = noteCache.notes[childNoteId];
const parentNote = noteCache.notes[parentNoteId]; const parentNote = noteCache.notes[parentNoteId];
if (!childNote) {
log.info(`Cannot find note in cache for noteId ${childNoteId}`);
return "[error fetching title]";
}
let title; let title;
if (childNote.isProtected) { if (childNote.isProtected) {