From f30c81ec858a2b9fe6f9968c60d9a273739e090e Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Mon, 27 Nov 2023 11:57:02 +0100 Subject: [PATCH] UI: html entities not being rendered on object version page Fixes: https://github.com/aquarist-labs/s3gw/issues/839 Signed-off-by: Volker Theile --- CHANGELOG.md | 6 +++ src/frontend/src/app/functions.helper.spec.ts | 45 +++++++++++++++++++ src/frontend/src/app/functions.helper.ts | 31 +++++++++++++ .../user-key-form-page.component.ts | 6 ++- ...object-version-datatable-page.component.ts | 7 +-- .../pages/user/user-pages-routing.module.ts | 2 +- .../page-title/page-title.component.ts | 11 ++--- 7 files changed, 97 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e4d4b8..ee6cc168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.24.0] + +### Fixed + +- Do not escape object key in page title (aquarist-labs/s3gw#839). + ## [0.23.0] ### Fixed diff --git a/src/frontend/src/app/functions.helper.spec.ts b/src/frontend/src/app/functions.helper.spec.ts index a0010882..3d41873b 100644 --- a/src/frontend/src/app/functions.helper.spec.ts +++ b/src/frontend/src/app/functions.helper.spec.ts @@ -1,6 +1,7 @@ import { basename, bytesToSize, + decodeURIComponents, extractErrorCode, extractErrorDescription, extractErrorMessage, @@ -65,6 +66,22 @@ describe('functions.helper', () => { ); }); + it('should format a string [3]', () => { + expect(format('{{ foo | basename }}', { foo: 'foo/bar/baz' })).toBe('baz'); + }); + + it('should format a string [4]', () => { + expect(format('{{ foo | decodeUriComponent }}', { foo: encodeURIComponent('foo & baz') })).toBe( + 'foo & baz' + ); + }); + + it('should format a string [5]', () => { + expect( + format('{{ foo | decodeUriComponent | safe }}', { foo: encodeURIComponent('foo & baz') }) + ).toBe('foo & baz'); + }); + it('should isEqualOrUndefined [1]', () => { expect(isEqualOrUndefined('foo', undefined)).toBeTruthy(); }); @@ -205,4 +222,32 @@ describe('functions.helper', () => { it('should check object version ID [5]', () => { expect(isObjectVersionID(1234)).toBeFalsy(); }); + + it('should decode URI components [1]', () => { + let data: Record = { + foo: encodeURIComponent('foo & foo'), + bar: encodeURIComponent('a=10'), + baz: 10, + xyz: encodeURIComponent('xyz & xyz') + }; + data = decodeURIComponents(data); + expect(data['foo']).toBe('foo & foo'); + expect(data['bar']).toBe('a=10'); + expect(data['baz']).toBe(10); + expect(data['xyz']).toBe('xyz & xyz'); + }); + + it('should decode URI components [2]', () => { + let data: Record = { + foo: encodeURIComponent('foo & foo'), + bar: encodeURIComponent('a=10'), + baz: 10, + xyz: encodeURIComponent('xyz & xyz') + }; + data = decodeURIComponents(data, ['foo', 'bar', 'baz']); + expect(data['foo']).toBe('foo & foo'); + expect(data['bar']).toBe('a=10'); + expect(data['baz']).toBe(10); + expect(data['xyz']).toBe(encodeURIComponent('xyz & xyz')); + }); }); diff --git a/src/frontend/src/app/functions.helper.ts b/src/frontend/src/app/functions.helper.ts index bd307804..a339cc67 100644 --- a/src/frontend/src/app/functions.helper.ts +++ b/src/frontend/src/app/functions.helper.ts @@ -49,6 +49,8 @@ export const bytesToSize = (value: undefined | null | number | string): string = * mustache style for that seems to be the better approach than using * the ES string interpolate style. * + * Note, output with dangerous characters is escaped automatically. + * * Example: * format('Hello {{ username }}', {username: 'foo'}) * @@ -175,6 +177,35 @@ export const isObjectVersionID = (value: any, excludeNull = false): boolean => { return _.isString(value) && !invalidValues.includes(value); }; +/** + * Decode all specified Uniform Resource Identifier (URI) components + * previously created by encodeURIComponent() or by a similar routine + * in the specified object. + * + * @param data The object containing the encoded components to be + * processed. + * @param encodedURIComponents An optional list of encoded components of + * Uniform Resource Identifiers. If not set, the all keys of the given + * data are used. + * @return Returns a new object containing the decoded versions of the + * given encoded Uniform Resource Identifier (URI) components. + */ +export const decodeURIComponents = ( + data: Record, + encodedURIComponents?: string[] +): Record => { + const newData: Record = _.cloneDeep(data); + if (!_.isArray(encodedURIComponents)) { + encodedURIComponents = _.keys(data); + } + _.forEach(encodedURIComponents, (encodedURIComponent: string) => { + if (encodedURIComponent in newData && _.isString(newData[encodedURIComponent])) { + newData[encodedURIComponent] = decodeURIComponent(newData[encodedURIComponent]); + } + }); + return newData; +}; + /** * Append various Nunjucks filter. */ diff --git a/src/frontend/src/app/pages/admin/user/user-key-form-page/user-key-form-page.component.ts b/src/frontend/src/app/pages/admin/user/user-key-form-page/user-key-form-page.component.ts index eef0b1e6..1651d843 100644 --- a/src/frontend/src/app/pages/admin/user/user-key-form-page/user-key-form-page.component.ts +++ b/src/frontend/src/app/pages/admin/user/user-key-form-page/user-key-form-page.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { marker as TEXT } from '@ngneat/transloco-keys-manager/marker'; +import { decodeURIComponents } from '~/app/functions.helper'; import { DeclarativeFormComponent } from '~/app/shared/components/declarative-form/declarative-form.component'; import { PageStatus } from '~/app/shared/components/page-wrapper/page-wrapper.component'; import { DeclarativeFormConfig } from '~/app/shared/models/declarative-form-config.type'; @@ -33,9 +34,10 @@ export class UserKeyFormPageComponent implements OnInit, IsDirty { ngOnInit(): void { this.route.params.subscribe((value: Params) => { + value = decodeURIComponents(value); this.pageStatus = PageStatus.ready; - this.uid = decodeURIComponent(value['uid']); - this.user = decodeURIComponent(value['user']); + this.uid = value['uid']; + this.user = value['user']; this.createForm(); }); } diff --git a/src/frontend/src/app/pages/user/object/object-version-datatable-page/object-version-datatable-page.component.ts b/src/frontend/src/app/pages/user/object/object-version-datatable-page/object-version-datatable-page.component.ts index 1ef328fb..c4b5c66c 100644 --- a/src/frontend/src/app/pages/user/object/object-version-datatable-page/object-version-datatable-page.component.ts +++ b/src/frontend/src/app/pages/user/object/object-version-datatable-page/object-version-datatable-page.component.ts @@ -5,7 +5,7 @@ import * as _ from 'lodash'; import { Subscription } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { format } from '~/app/functions.helper'; +import { decodeURIComponents, format } from '~/app/functions.helper'; import { Unsubscribe } from '~/app/functions.helper'; import { PageStatus } from '~/app/shared/components/page-wrapper/page-wrapper.component'; import { Icon } from '~/app/shared/enum/icon.enum'; @@ -61,8 +61,9 @@ export class ObjectVersionDatatablePageComponent implements OnInit { this.pageStatus = PageStatus.ready; return; } - this.bid = decodeURIComponent(value['bid']); - this.key = decodeURIComponent(value['key']); + value = decodeURIComponents(value, ['bid', 'key']); + this.bid = value['bid']; + this.key = value['key']; this.loadData(); }); this.datatableActions = [ diff --git a/src/frontend/src/app/pages/user/user-pages-routing.module.ts b/src/frontend/src/app/pages/user/user-pages-routing.module.ts index 3ff1ade1..eef09685 100644 --- a/src/frontend/src/app/pages/user/user-pages-routing.module.ts +++ b/src/frontend/src/app/pages/user/user-pages-routing.module.ts @@ -56,7 +56,7 @@ const routes: Routes = [ { path: 'versions/:key', data: { - subTitle: '{{ bid }}/{{ key | decodeUriComponent }} - Versions', + subTitle: '{{ bid }}/{{ key | safe }} - Versions', title: TEXT('Object:'), url: '../..' }, diff --git a/src/frontend/src/app/shared/components/page-title/page-title.component.ts b/src/frontend/src/app/shared/components/page-title/page-title.component.ts index 7c7c31e4..e491b4a2 100644 --- a/src/frontend/src/app/shared/components/page-title/page-title.component.ts +++ b/src/frontend/src/app/shared/components/page-title/page-title.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Params } from '@angular/router'; -import { format } from '~/app/functions.helper'; +import { decodeURIComponents, format } from '~/app/functions.helper'; import { AppConfigService } from '~/app/shared/services/app-config.service'; @Component({ @@ -20,15 +20,16 @@ export class PageTitleComponent { private appConfigService: AppConfigService, private titleService: Title ) { - this.activatedRoute.params.subscribe((params: Params) => { + this.activatedRoute.params.subscribe((value: Params) => { + value = decodeURIComponents(value); this.subTitle = this.activatedRoute.snapshot.data?.['subTitle'] - ? format(this.activatedRoute.snapshot.data['subTitle'], params) + ? format(this.activatedRoute.snapshot.data['subTitle'], value) : undefined; this.title = this.activatedRoute.snapshot.data?.['title'] - ? format(this.activatedRoute.snapshot.data['title'], params) + ? format(this.activatedRoute.snapshot.data['title'], value) : undefined; this.url = this.activatedRoute.snapshot.data?.['url'] - ? format(this.activatedRoute.snapshot.data['url'], params) + ? format(this.activatedRoute.snapshot.data['url'], value) : undefined; if (this.title) { let newTitle = `${this.appConfigService.config?.title} - ${this.title}`;