Skip to content

Commit

Permalink
fix: footnotes escape html only once (#2088)
Browse files Browse the repository at this point in the history
  • Loading branch information
derschnee68 authored Feb 10, 2025
1 parent 5bdb9fc commit cc85f14
Show file tree
Hide file tree
Showing 9 changed files with 2,282 additions and 5,517 deletions.
35 changes: 33 additions & 2 deletions apps/dsp-app/cypress/e2e/system-admin/resource.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,38 @@ describe('Resource', () => {
});
});

describe.skip('footnotes', () => {
describe('footnotes', () => {
it('a created footnote should be sent in the right format (no double escape, empty text in tag)', () => {
// Intercept the POST request
cy.intercept('POST', 'http://0.0.0.0:3333/v2/resources').as('postRequest');

ResourceRequests.resourceRequest(ClassPropertyPayloads.richText(finalLastModificationDate));
po.visitAddPage();

// create
po.addInitialLabel();

cy.get('.ck-blurred').click();
cy.get('button[data-cke-tooltip-text="Footnote"]').click();
cy.get('.ck-blurred').click();
cy.get('.ck-content[contenteditable=true]').then(el => {
// @ts-ignore
const editor = el[1].ckeditorInstance; // If you're using TS, this is ReturnType<typeof InlineEditor['create']>
editor.setData('myfootnote');
});
cy.get('.ck-button-save > .ck-icon').click();
po.clickOnSubmit();

cy.wait('@postRequest').then(interception => {
// Assert that the intercepted request body matches the expected payload (X)
const v =
interception.request.body['http://0.0.0.0:3333/ontology/00FF/images/v2#property'][
'http://api.knora.org/ontology/knora-api/v2#textValueAsXml'
];
expect(v).to.contain('<footnote content="&lt;p&gt;myfootnote&lt;/p&gt;"></footnote>');
});
});

it('should be displayed, and can be edited', () => {
const footnote = {
'@type': 'http://0.0.0.0:3333/ontology/00FF/images/v2#datamodelclass',
Expand All @@ -34,7 +65,7 @@ describe('Resource', () => {
'http://0.0.0.0:3333/ontology/00FF/images/v2#property': {
'@type': 'http://api.knora.org/ontology/knora-api/v2#TextValue',
'http://api.knora.org/ontology/knora-api/v2#textValueAsXml':
'<?xml version="1.0" encoding="UTF-8"?><text><p>footnote1<footnote content="&amp;lt;p&amp;gt;fn1&amp;lt;/p&amp;gt;">[Footnote]</footnote> and footnote2<footnote content="&amp;lt;p&amp;gt;fn2&amp;lt;/p&amp;gt;">[Footnote]</footnote></p></text>',
'<?xml version="1.0" encoding="UTF-8"?> <text><p>footnote1<footnote content="&lt;p&gt;fn1&lt;/p&gt;"/> footnote2 <footnote content="&lt;p&gt;fn2&lt;/p&gt;"/></p></text>',
'http://api.knora.org/ontology/knora-api/v2#textValueHasMapping': {
'@id': 'http://rdfh.ch/standoff/mappings/StandardMapping',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { unescapeHtml } from '@dasch-swiss/vre/ui/ui';
import { FootnoteService } from './footnote.service';

@Pipe({
name: 'footnoteParser',
})
export class FootnoteParserPipe implements PipeTransform {
private readonly _footnoteRegExp = /<footnote content="([^>]+)">([^<]*)<\/footnote>/g;
private readonly _footnoteRegExp = /<footnote content="([^>]+)"\/>/g;

constructor(
private _sanitizer: DomSanitizer,
private _footnoteService: FootnoteService
) {}

transform(value: null | string): SafeHtml | null {
if (value === null) {
return value; // Return as is if value is empty or null
transform(value_: null | string | SafeHtml): SafeHtml | null {
if (value_ === null) {
return value_; // Return as is if value is empty or null
} // does nothing if only displayMode changes

const value = typeof value_ === 'string' ? value_ : value_['changingThisBreaksApplicationSecurity'];
if (!this._containsFootnote(value)) {
return this._sanitizer.bypassSecurityTrustHtml(value);
}
Expand All @@ -37,23 +39,10 @@ export class FootnoteParserPipe implements PipeTransform {
const uuid = window.crypto.getRandomValues(new Uint32Array(1))[0].toString();
const parsedFootnote = `<footnote content="${matchArray[1]}" id="${uuid}">${this._footnoteService.footnotes.length + 1}</footnote>`;
newValue = newValue.replace(matchArray[0], parsedFootnote);
this._footnoteService.addFootnote(
uuid,
this._sanitizer.bypassSecurityTrustHtml(this._unescapeHtml(this._unescapeHtml(matchArray[1])))
);
this._footnoteService.addFootnote(uuid, this._sanitizer.bypassSecurityTrustHtml(unescapeHtml(matchArray[1])));
});
}
return this._sanitizer.bypassSecurityTrustHtml(this._unescapeHtml(newValue));
}

private _unescapeHtml(str: string) {
const unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#039;': "'",
};
return str.replace(/&(amp|lt|gt|quot|#039);/g, match => unescapeMap[match]);
return this._sanitizer.bypassSecurityTrustHtml(newValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { FootnoteService } from './footnote.service';
template: `<h5>{{ 'resource.footnotes' | translate }}</h5>
<div
*ngFor="let footnote of footnoteService.footnotes; let index = index; trackBy: trackByIndex"
(click)="goToFootnote(footnote.uuid)"
class="footnote"
[attr.data-uuid]="footnote.uuid"
data-cy="footnote">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import { Component } from '@angular/core';
styleUrls: ['./footer.component.scss'],
})
export class FooterComponent {
currentYear: Date = new Date();
currentYear = new Date();
}
1 change: 1 addition & 0 deletions libs/vre/ui/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export * from './lib/hint/hint.component';
export * from './lib/validator-error.interface';
export * from './lib/pipes/internal-link-replacer.pipe';
export * from './lib/pipes/add-target-blank.pipe';
export * from './lib/ck-editor/unescape-html';
33 changes: 29 additions & 4 deletions libs/vre/ui/ui/src/lib/ck-editor/ck-editor.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import * as Editor from 'ckeditor5-custom-build/build/ckeditor';
import * as Editor from 'ckeditor5-custom-build';
import { ckEditor } from './ck-editor';
import { unescapeHtml } from './unescape-html';

@Component({
selector: 'app-ck-editor',
template: ` <ckeditor [formControl]="control" [config]="ckEditor.config" [editor]="editor" />
template: ` <ckeditor [formControl]="footnoteControl" [config]="ckEditor.config" [editor]="editor" />
<mat-error *ngIf="control.touched && control.errors as errors">{{ errors | humanReadableError }}</mat-error>`,
})
export class CkEditorComponent {
export class CkEditorComponent implements OnInit {
@Input({ required: true }) control!: FormControl<string>;
footnoteControl = new FormControl<string>('');

readonly editor = Editor;
protected readonly ckEditor = ckEditor;

ngOnInit() {
this.footnoteControl.setValue(this.control.value ? this._parseToFootnote(this.control.value) : null);

this.footnoteControl.valueChanges.subscribe(value => {
this.control.setValue(this._parseFromFootnote(value));
});
}

private _parseToFootnote(rawHtml: string) {
const _footnoteRegExp2 = /<footnote content="([^>]+)"\/>/g;
return rawHtml.replace(_footnoteRegExp2, (match, content) => {
return `<footnote content="${content}">[Footnote]</footnote>`;
});
}

private _parseFromFootnote(rawHtml: string) {
const _footnoteRegExp = /<footnote content="([^>]+)">([^<]*)<\/footnote>/g;
return rawHtml.replace(_footnoteRegExp, (match, content) => {
const escapedContent = unescapeHtml(content);
return `<footnote content="${escapedContent}"></footnote>`;
});
}
}
10 changes: 10 additions & 0 deletions libs/vre/ui/ui/src/lib/ck-editor/unescape-html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function unescapeHtml(str: string) {
const unescapeMap = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#039;': "'",
};
return str.replace(/&(amp|lt|gt|quot|#039);/g, match => unescapeMap[match]);
}
Loading

0 comments on commit cc85f14

Please sign in to comment.