Skip to content

Commit 776e731

Browse files
committed
Merge branch 'release/15.2' into v15/dev
2 parents 27aef0b + 0562757 commit 776e731

28 files changed

+206
-105
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<NeutralLanguage>en-US</NeutralLanguage>
1515
<Nullable>enable</Nullable>
1616
<WarningsAsErrors>nullable</WarningsAsErrors>
17-
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
17+
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
1818
<ImplicitUsings>enable</ImplicitUsings>
1919
<GenerateDocumentationFile>true</GenerateDocumentationFile>
2020
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>

src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeGraphicsController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.AspNetCore.Http;
66
using Microsoft.AspNetCore.Mvc;
77
using Microsoft.AspNetCore.StaticFiles;
8+
using Microsoft.Extensions.FileProviders;
89
using Microsoft.Extensions.Options;
910
using Umbraco.Cms.Api.Management.Routing;
1011
using Umbraco.Cms.Core;
@@ -55,8 +56,8 @@ public BackOfficeGraphicsController(IOptions<ContentSettings> contentSettings, I
5556

5657
private IActionResult HandleFileRequest(string virtualPath)
5758
{
58-
var filePath = Path.Combine(Constants.SystemDirectories.Umbraco, virtualPath).TrimStart(Constants.CharArrays.Tilde);
59-
var fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo(filePath);
59+
var filePath = $"{Constants.SystemDirectories.Umbraco}/{virtualPath}".TrimStart(Constants.CharArrays.Tilde);
60+
IFileInfo fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo(filePath);
6061

6162
if (fileInfo.PhysicalPath is null)
6263
{

src/Umbraco.Web.Common/Views/UmbracoViewPage.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ public void WriteUmbracoContent(TagHelperOutput tagHelperOutput)
142142
string.Format(
143143
ContentSettings.PreviewBadge,
144144
HostingEnvironment.ToAbsolute(Core.Constants.System.DefaultUmbracoPath),
145-
Context.Request.GetEncodedUrl(),
145+
System.Web.HttpUtility.HtmlEncode(Context.Request.GetEncodedUrl()), // Belt and braces - via a browser at least it doesn't seem possible to have anything other than
146+
// a valid culture code provided in the querystring of this URL.
147+
// But just to be sure of prevention of an XSS vulnterablity we'll HTML encode here too.
148+
// An expected URL is untouched by this encoding.
146149
UmbracoContext.PublishedRequest?.PublishedContent?.Key);
147150
}
148151
else

src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ describe('UmbLocalizeController', () => {
175175
expect((controller.term as any)('logout', 'Hello', 'World')).to.equal('Log out');
176176
});
177177

178+
it('should encode HTML entities', () => {
179+
expect(controller.term('withInlineToken', 'Hello', '<script>alert("XSS")</script>'), 'XSS detected').to.equal(
180+
'Hello &lt;script&gt;alert(&#34;XSS&#34;)&lt;/script&gt;',
181+
);
182+
});
183+
178184
it('only reacts to changes of its own localization-keys', async () => {
179185
const element: UmbLocalizationRenderCountElement = await fixture(
180186
html`<umb-localization-render-count></umb-localization-render-count>`,

src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
import { umbLocalizationManager } from './localization.manager.js';
2121
import type { LitElement } from '@umbraco-cms/backoffice/external/lit';
2222
import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
23+
import { escapeHTML } from '@umbraco-cms/backoffice/utils';
2324

2425
const LocalizationControllerAlias = Symbol();
2526
/**
@@ -119,29 +120,35 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
119120
}
120121

121122
const { primary, secondary } = this.getLocalizationData(this.lang());
123+
124+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
122125
let term: any;
123126

124127
// Look for a matching term using regionCode, code, then the fallback
125-
if (primary && primary[key]) {
128+
if (primary?.[key]) {
126129
term = primary[key];
127-
} else if (secondary && secondary[key]) {
130+
} else if (secondary?.[key]) {
128131
term = secondary[key];
129-
} else if (umbLocalizationManager.fallback && umbLocalizationManager.fallback[key]) {
132+
} else if (umbLocalizationManager.fallback?.[key]) {
130133
term = umbLocalizationManager.fallback[key];
131134
} else {
132135
return String(key);
133136
}
134137

138+
// As translated texts can contain HTML, we will need to render with unsafeHTML.
139+
// But arguments can come from user input, so they should be escaped.
140+
const sanitizedArgs = args.map((a) => escapeHTML(a));
141+
135142
if (typeof term === 'function') {
136-
return term(...args) as string;
143+
return term(...sanitizedArgs) as string;
137144
}
138145

139146
if (typeof term === 'string') {
140-
if (args.length > 0) {
147+
if (sanitizedArgs.length) {
141148
// Replace placeholders of format "%index%" and "{index}" with provided values
142149
term = term.replace(/(%(\d+)%|\{(\d+)\})/g, (match, _p1, p2, p3): string => {
143150
const index = p2 || p3;
144-
return String(args[index] || match);
151+
return typeof sanitizedArgs[index] !== 'undefined' ? String(sanitizedArgs[index]) : match;
145152
});
146153
}
147154
}

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator
2424
}
2525

2626
#constructGridBlockValue(propertyValue: UmbBlockGridValueModel): UmbGridBlockClipboardEntryValueModel {
27+
// TODO: investigate if structured can be remove here.
2728
const valueClone = structuredClone(propertyValue);
2829

2930
const gridBlockValue: UmbGridBlockClipboardEntryValueModel = {
@@ -38,17 +39,38 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator
3839
#constructBlockValue(propertyValue: UmbBlockGridValueModel): UmbBlockClipboardEntryValueModel {
3940
const gridBlockValue = this.#constructGridBlockValue(propertyValue);
4041

42+
const contentData: typeof gridBlockValue.contentData = [];
43+
const settingsData: typeof gridBlockValue.settingsData = [];
44+
4145
const layout: UmbBlockClipboardEntryValueModel['layout'] = gridBlockValue.layout?.map((gridLayout) => {
46+
const contentDataEntry = gridBlockValue.contentData.find(
47+
(contentData) => contentData.key === gridLayout.contentKey,
48+
);
49+
if (!contentDataEntry) {
50+
throw new Error('No content data found for layout entry');
51+
}
52+
contentData.push(contentDataEntry);
53+
54+
if (gridLayout.settingsKey) {
55+
const settingsDataEntry = gridBlockValue.settingsData.find(
56+
(settingsData) => settingsData.key === gridLayout.settingsKey,
57+
);
58+
if (!settingsDataEntry) {
59+
throw new Error('No settings data found for layout entry');
60+
}
61+
settingsData.push(settingsDataEntry);
62+
}
63+
4264
return {
4365
contentKey: gridLayout.contentKey,
4466
settingsKey: gridLayout.settingsKey,
4567
};
4668
});
4769

4870
return {
49-
contentData: gridBlockValue.contentData,
5071
layout: layout,
51-
settingsData: gridBlockValue.settingsData,
72+
contentData,
73+
settingsData,
5274
};
5375
}
5476
}

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
import type { UmbBlockGridValueModel } from '../../../types.js';
22
import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../property-editors/constants.js';
33
import type { UmbGridBlockClipboardEntryValueModel } from '../../types.js';
4-
import { forEachBlockLayoutEntryOf } from '../../../utils/index.js';
5-
import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../../context/constants.js';
64
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
75
import type { UmbClipboardCopyPropertyValueTranslator } from '@umbraco-cms/backoffice/clipboard';
86

97
export class UmbBlockGridToGridBlockClipboardCopyPropertyValueTranslator
108
extends UmbControllerBase
119
implements UmbClipboardCopyPropertyValueTranslator<UmbBlockGridValueModel, UmbGridBlockClipboardEntryValueModel>
1210
{
13-
#blockGridManager?: typeof UMB_BLOCK_GRID_MANAGER_CONTEXT.TYPE;
14-
1511
async translate(propertyValue: UmbBlockGridValueModel) {
1612
if (!propertyValue) {
1713
throw new Error('Property value is missing.');
1814
}
19-
20-
this.#blockGridManager = await this.getContext(UMB_BLOCK_GRID_MANAGER_CONTEXT);
21-
22-
return this.#constructGridBlockValue(propertyValue);
23-
}
24-
25-
#constructGridBlockValue(propertyValue: UmbBlockGridValueModel): UmbGridBlockClipboardEntryValueModel {
2615
const valueClone = structuredClone(propertyValue);
2716

2817
const layout = valueClone.layout?.[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS] ?? undefined;
@@ -33,24 +22,9 @@ export class UmbBlockGridToGridBlockClipboardCopyPropertyValueTranslator
3322
throw new Error('No layouts found.');
3423
}
3524

36-
layout.forEach((layout) => {
37-
// Find sub Blocks and append their data:
38-
forEachBlockLayoutEntryOf(layout, async (entry) => {
39-
const content = this.#blockGridManager!.getContentOf(entry.contentKey);
40-
41-
if (!content) {
42-
throw new Error('No content found');
43-
}
44-
45-
contentData.push(structuredClone(content));
46-
47-
if (entry.settingsKey) {
48-
const settings = this.#blockGridManager!.getSettingsOf(entry.settingsKey);
49-
if (settings) {
50-
settingsData.push(structuredClone(settings));
51-
}
52-
}
53-
});
25+
layout?.forEach((layoutItem) => {
26+
// @ts-expect-error - We are removing the $type property from the layout item
27+
delete layoutItem.$type;
5428
});
5529

5630
const gridBlockValue: UmbGridBlockClipboardEntryValueModel = {

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
} from '@umbraco-cms/backoffice/extension-api';
2121
import { UmbLanguageItemRepository } from '@umbraco-cms/backoffice/language';
2222
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
23+
import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation';
24+
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
2325

2426
const apiArgsCreator: UmbApiConstructorArgumentsMethodType<unknown> = (manifest: unknown) => {
2527
return [{ manifest }];
@@ -30,6 +32,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
3032
//
3133
#blockContext?: typeof UMB_BLOCK_GRID_ENTRY_CONTEXT.TYPE;
3234
#workspaceContext?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE;
35+
#variantId: UmbVariantId | undefined;
3336
#contentKey?: string;
3437
#parentUnique?: string | null;
3538
#areaKey?: string | null;
@@ -52,6 +55,9 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
5255
@state()
5356
_inlineProperty?: UmbPropertyTypeModel;
5457

58+
@state()
59+
_inlinePropertyDataPath?: string;
60+
5561
@state()
5662
private _ownerContentTypeName?: string;
5763

@@ -82,7 +88,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
8288
UMB_BLOCK_WORKSPACE_ALIAS,
8389
apiArgsCreator,
8490
(permitted, ctrl) => {
85-
const context = ctrl.api as typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE;
91+
const context = ctrl.api as typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE | undefined;
8692
if (permitted && context) {
8793
// Risky business, cause here we are lucky that it seems to be consumed and set before this is called and there for this is acceptable for now. [NL]
8894
if (this.#parentUnique === undefined || this.#areaKey === undefined) {
@@ -101,6 +107,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
101107
this.#workspaceContext.content.structure.contentTypeProperties,
102108
(contentTypeProperties) => {
103109
this._inlineProperty = contentTypeProperties[0];
110+
this.#generatePropertyDataPath();
104111
},
105112
'observeProperties',
106113
);
@@ -116,9 +123,10 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
116123
this.observe(
117124
context.variantId,
118125
async (variantId) => {
126+
this.#variantId = variantId;
127+
this.#generatePropertyDataPath();
119128
if (variantId) {
120129
context.content.setup(this, variantId);
121-
// TODO: Support segment name?
122130
const culture = variantId.culture;
123131
if (culture) {
124132
const languageRepository = new UmbLanguageItemRepository(this);
@@ -142,6 +150,16 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
142150
this.#workspaceContext.load(this.#contentKey);
143151
}
144152

153+
#generatePropertyDataPath() {
154+
if (!this.#variantId || !this._inlineProperty) return;
155+
const property = this._inlineProperty;
156+
this._inlinePropertyDataPath = `$.values[${UmbDataPathPropertyValueQuery({
157+
alias: property.alias,
158+
culture: property.variesByCulture ? this.#variantId!.culture : null,
159+
segment: property.variesBySegment ? this.#variantId!.segment : null,
160+
})}].value`;
161+
}
162+
145163
#expose = () => {
146164
this.#workspaceContext?.expose();
147165
};
@@ -189,6 +207,7 @@ export class UmbBlockGridBlockInlineElement extends UmbLitElement {
189207
return html`<div id="inside">
190208
<umb-property-type-based-property
191209
.property=${this._inlineProperty}
210+
.dataPath=${this._inlinePropertyDataPath ?? ''}
192211
slot="areas"></umb-property-type-based-property>
193212
<umb-block-grid-areas-container slot="areas"></umb-block-grid-areas-container>
194213
</div>`;

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { closestColumnSpanOption } from '../utils/index.js';
1+
import { closestColumnSpanOption, forEachBlockLayoutEntryOf } from '../utils/index.js';
22
import type { UmbBlockGridValueModel } from '../types.js';
33
import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from '../constants.js';
44
import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js';
@@ -296,6 +296,22 @@ export class UmbBlockGridEntryContext
296296
const settingsData = settings ? [structuredClone(settings)] : [];
297297
const exposes = expose ? [structuredClone(expose)] : [];
298298

299+
// Find sub Blocks and append their data:
300+
forEachBlockLayoutEntryOf(layout, async (entry) => {
301+
const content = this._manager!.getContentOf(entry.contentKey);
302+
if (!content) {
303+
throw new Error('No content found');
304+
}
305+
contentData.push(structuredClone(content));
306+
307+
if (entry.settingsKey) {
308+
const settings = this._manager!.getSettingsOf(entry.settingsKey);
309+
if (settings) {
310+
settingsData.push(structuredClone(settings));
311+
}
312+
}
313+
});
314+
299315
const propertyValue: UmbBlockGridValueModel = {
300316
layout: {
301317
[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]: layout ? [structuredClone(layout)] : undefined,

src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-value-cloner/property-value-cloner-block-grid.cloner.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ describe('UmbBlockGridPropertyValueCloner', () => {
4949
{
5050
contentKey: 'content-3',
5151
settingsKey: 'settings-3',
52-
areas: [],
5352
},
5453
],
5554
},

0 commit comments

Comments
 (0)