5 Commits
10 changed files with 115 additions and 21 deletions
+8 -2
View File
@@ -6,7 +6,7 @@ PortalSettings V3 ist ein providerbasiertes SPFx-WebPart für SharePoint Server
| Eigenschaft | Wert | | Eigenschaft | Wert |
|---|---| |---|---|
| Version | 3.2.2 | | Version | 3.2.7 |
| SharePoint Framework | 1.4.1 | | SharePoint Framework | 1.4.1 |
| Build-Node | 8.17.0 | | Build-Node | 8.17.0 |
| npm | 6.x | | npm | 6.x |
@@ -23,6 +23,12 @@ Die Oberfläche wird vollständig vom WebPart erzeugt und verwendet kein `innerH
- Auswahl des Navigationstermsets aus dem Default Site Collection Term Store - Auswahl des Navigationstermsets aus dem Default Site Collection Term Store
- Mega-Menu- oder Flyout-Modus - Mega-Menu- oder Flyout-Modus
- Cache-Dauer und Cache-Version - Cache-Dauer und Cache-Version
### Custom Branding
- sichere Header- und Footer-Elemente sowie Stylesheets
- deklarative GET-Suchformulare über die erlaubten Elementtypen `form` und `input`
- Prüfung von Formular-Action, Eingabetyp und Submit-Button vor dem Speichern
- Debug-Modus - Debug-Modus
- schemaerhaltendes Speichern unbekannter Properties - schemaerhaltendes Speichern unbekannter Properties
@@ -165,7 +171,7 @@ sharepoint/solution/portal-settings.sppkg
## Installation ## Installation
1. `portal-settings.sppkg` im App Catalog durch Version `3.2.2.0` ersetzen. 1. `portal-settings.sppkg` im App Catalog durch Version `3.2.7.0` ersetzen.
2. Die App im Root Web der gewünschten Site Collection installieren oder aktualisieren. 2. Die App im Root Web der gewünschten Site Collection installieren oder aktualisieren.
3. Die Seite mit dem PnP-PowerShell-Skript erzeugen beziehungsweise reparieren: 3. Die Seite mit dem PnP-PowerShell-Skript erzeugen beziehungsweise reparieren:
+2 -2
View File
@@ -2,7 +2,7 @@
Stand: 21.07.2026 Stand: 21.07.2026
Branch: `dev-3.0` Branch: `dev-3.0`
Zielversion: 3.2.2 Zielversion: 3.2.7
## Architektur ## Architektur
@@ -79,7 +79,7 @@ Zielversion: 3.2.2
- [ ] Per PnP-Skript provisionierte `PortalSettings.aspx` und enthaltenes WebPart prüfen. - [ ] Per PnP-Skript provisionierte `PortalSettings.aspx` und enthaltenes WebPart prüfen.
- [ ] MegaMenu-Termsets laden und speichern. - [ ] MegaMenu-Termsets laden und speichern.
- [ ] CustomBranding laden und schemaerhaltend speichern. - [ ] CustomBranding laden und schemaerhaltend speichern.
- [ ] ExpiryIndicator-Standard speichern. - [x] ExpiryIndicator-Standard über SharePoint-SE-kompatibles CSOM `ProcessQuery` speichern.
- [ ] Expiry-Bindung hinzufügen, lokal konfigurieren und wieder auf Vererbung stellen. - [ ] Expiry-Bindung hinzufügen, lokal konfigurieren und wieder auf Vererbung stellen.
- [ ] Bedienung mit Tastatur, 200-Prozent-Zoom und Windows-Hochkontrast prüfen. - [ ] Bedienung mit Tastatur, 200-Prozent-Zoom und Windows-Hochkontrast prüfen.
- [ ] Alte V2-Seite nach erfolgreicher Parallelabnahme archivieren oder entfernen. - [ ] Alte V2-Seite nach erfolgreicher Parallelabnahme archivieren oder entfernen.
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "portal-settings-client-side-solution", "name": "portal-settings-client-side-solution",
"id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286", "id": "1c98e32d-bb7b-43b7-9625-074d6e4ea286",
"version": "3.2.2.0", "version": "3.2.7.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": false "skipFeatureDeployment": false
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portal-settings", "name": "portal-settings",
"version": "3.2.2", "version": "3.2.7",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "portal-settings", "name": "portal-settings",
"version": "3.2.2", "version": "3.2.7",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@@ -13,6 +13,7 @@ export interface ICurrentNavigationSettings {
cacheVersion: string; cacheVersion: string;
maxDepth: number; maxDepth: number;
showRoot: boolean; showRoot: boolean;
initialExpansion: 'activePath' | 'expanded' | 'collapsed';
debug: boolean; debug: boolean;
[key: string]: any; // tslint:disable-line:no-any [key: string]: any; // tslint:disable-line:no-any
} }
@@ -54,6 +55,15 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
type: 'boolean', key: 'showRoot', path: 'showRoot', label: 'Root-Term anzeigen', type: 'boolean', key: 'showRoot', path: 'showRoot', label: 'Root-Term anzeigen',
description: 'Zeigt den zugeordneten Root-Term als erste Ebene an.', columnSpan: 6 description: 'Zeigt den zugeordneten Root-Term als erste Ebene an.', columnSpan: 6
}, },
{
type: 'choice', key: 'initialExpansion', path: 'initialExpansion', label: 'Initialer Aufklappzustand',
description: 'Legt fest, welche Zweige beim Laden der Navigation geöffnet sind.', columnSpan: 6,
options: [
{ key: 'activePath', text: 'Aktuellen Pfad aufklappen' },
{ key: 'expanded', text: 'Alles aufklappen' },
{ key: 'collapsed', text: 'Alles zuklappen' }
]
},
{ {
type: 'notice', appearance: 'info', title: 'Local Custom Properties', columnSpan: 12, type: 'notice', appearance: 'info', title: 'Local Custom Properties', columnSpan: 12,
text: 'CurrentNavigation.Root = true ordnet einen Term über seine URL einer Website zu. CurrentNavigation.Hidden = true blendet einen Zweig aus.' text: 'CurrentNavigation.Root = true ordnet einen Term über seine URL einer Website zu. CurrentNavigation.Hidden = true blendet einen Zweig aus.'
@@ -96,7 +106,7 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
providerKey: 'currentnavigation', contractVersion: 1, minimumPortalSettingsVersion: '3.1.0' providerKey: 'currentnavigation', contractVersion: 1, minimumPortalSettingsVersion: '3.1.0'
}, },
termSetId: '', termSetName: '', cacheMinutes: 15, cacheVersion: '1', termSetId: '', termSetName: '', cacheMinutes: 15, cacheVersion: '1',
maxDepth: 3, showRoot: false, debug: false maxDepth: 3, showRoot: false, initialExpansion: 'activePath', debug: false
}), }),
normalize: (value: any): ICurrentNavigationSettings => { // tslint:disable-line:no-any normalize: (value: any): ICurrentNavigationSettings => { // tslint:disable-line:no-any
const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any const source: any = value && typeof value === 'object' ? value : {}; // tslint:disable-line:no-any
@@ -115,6 +125,8 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
? source.cacheVersion.trim() : '1'; ? source.cacheVersion.trim() : '1';
result.maxDepth = isFinite(depth) ? Math.min(Math.max(Math.floor(depth), 1), 8) : 3; result.maxDepth = isFinite(depth) ? Math.min(Math.max(Math.floor(depth), 1), 8) : 3;
result.showRoot = source.showRoot === true; result.showRoot = source.showRoot === true;
result.initialExpansion = source.initialExpansion === 'expanded' || source.initialExpansion === 'collapsed'
? source.initialExpansion : 'activePath';
result.debug = source.debug === true; result.debug = source.debug === true;
return result as ICurrentNavigationSettings; return result as ICurrentNavigationSettings;
}, },
@@ -130,6 +142,9 @@ export const CurrentNavigationSettingsProvider: ISettingsProvider<ICurrentNaviga
if (value.maxDepth < 1 || value.maxDepth > 8) { if (value.maxDepth < 1 || value.maxDepth > 8) {
errors.push('Die maximale Tiefe muss zwischen 1 und 8 liegen.'); errors.push('Die maximale Tiefe muss zwischen 1 und 8 liegen.');
} }
if (['activePath', 'expanded', 'collapsed'].indexOf(value.initialExpansion) < 0) {
errors.push('Der initiale Aufklappzustand ist ungültig.');
}
return errors; return errors;
} }
}; };
@@ -31,7 +31,7 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
title: 'Custom Branding Application Customizer', title: 'Custom Branding Application Customizer',
description: 'MSFT-Custom-Solution:CustomBranding', description: 'MSFT-Custom-Solution:CustomBranding',
location: 'ClientSideExtension.ApplicationCustomizer', location: 'ClientSideExtension.ApplicationCustomizer',
sequence: 100 sequence: 90
} }
}, },
sections: [ sections: [
@@ -140,7 +140,7 @@ export const CustomBrandingSettingsProvider: ISettingsProvider<ICustomBrandingSe
} }
}; };
const AllowedTags: string[] = ['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section']; const AllowedTags: string[] = ['div', 'span', 'p', 'a', 'button', 'form', 'input', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
function validateElements(elements: any[], path: string, depth: number, state: { count: number }, errors: string[]): void { // tslint:disable-line:no-any function validateElements(elements: any[], path: string, depth: number, state: { count: number }, errors: string[]): void { // tslint:disable-line:no-any
if (depth > 8) { errors.push(path + ' überschreitet die maximale Tiefe von 8.'); return; } if (depth > 8) { errors.push(path + ' überschreitet die maximale Tiefe von 8.'); return; }
if (!Array.isArray(elements)) { errors.push(path + ' muss ein Array sein.'); return; } if (!Array.isArray(elements)) { errors.push(path + ' muss ein Array sein.'); return; }
@@ -154,6 +154,21 @@ function validateElements(elements: any[], path: string, depth: number, state: {
if (tag === 'img' && (!element.attributes || typeof element.attributes.alt !== 'string')) { if (tag === 'img' && (!element.attributes || typeof element.attributes.alt !== 'string')) {
errors.push(path + '[' + i + '] ist ein Bild ohne alt-Attribut.'); errors.push(path + '[' + i + '] ist ein Bild ohne alt-Attribut.');
} }
if (tag === 'form') {
const action: string = String(element.attributes && element.attributes.action || '').trim();
if (!action || !isSafeFormAction(action)) { errors.push(path + '[' + i + '] besitzt keine sichere Formular-Action.'); }
if (element.attributes && element.attributes.method && String(element.attributes.method).toLowerCase() !== 'get') {
errors.push(path + '[' + i + '] darf nur die Formularmethode GET verwenden.');
}
}
if (tag === 'input' && element.attributes && element.attributes.type &&
String(element.attributes.type).toLowerCase() !== 'search') {
errors.push(path + '[' + i + '] darf nur den Eingabetyp search verwenden.');
}
if (tag === 'button' && element.attributes && element.attributes.type &&
['button', 'submit'].indexOf(String(element.attributes.type).toLowerCase()) < 0) {
errors.push(path + '[' + i + '] verwendet einen nicht erlaubten Button-Typ.');
}
if (element.attributes && typeof element.attributes === 'object') { if (element.attributes && typeof element.attributes === 'object') {
Object.keys(element.attributes).forEach((name: string): void => { Object.keys(element.attributes).forEach((name: string): void => {
const lower: string = name.toLowerCase(); const attributeValue: string = String(element.attributes[name] || ''); const lower: string = name.toLowerCase(); const attributeValue: string = String(element.attributes[name] || '');
@@ -170,3 +185,9 @@ function validateElements(elements: any[], path: string, depth: number, state: {
if (element.children) { validateElements(element.children, path + '[' + i + '].children', depth + 1, state, errors); } if (element.children) { validateElements(element.children, path + '[' + i + '].children', depth + 1, state, errors); }
} }
} }
function isSafeFormAction(value: string): boolean {
if (/^~sitecollection(?:\/|$)/i.test(value) || /^\/(?!\/)/.test(value)) { return true; }
const protocol: RegExpMatchArray = value.match(/^([a-z][a-z0-9+.-]*):/i);
return !!protocol && (protocol[1].toLowerCase() === 'http' || protocol[1].toLowerCase() === 'https');
}
+36 -8
View File
@@ -54,7 +54,7 @@ export class PortalSettingsDataService {
const merged: any = mergePreservingUnknown(item.originalConfig, item.provider.normalize(item.config)); const merged: any = mergePreservingUnknown(item.originalConfig, item.provider.normalize(item.config));
if (item.provider.storage.kind === 'siteUserCustomAction') { if (item.provider.storage.kind === 'siteUserCustomAction') {
const persist: Promise<IUserCustomActionInfo> = item.context.action const persist: Promise<IUserCustomActionInfo> = item.context.action
? this.saveUserCustomAction(item.context.action, merged).then((): IUserCustomActionInfo => item.context.action) ? this.saveUserCustomAction(item.context.action, merged, item.provider).then((): IUserCustomActionInfo => item.context.action)
: this.createUserCustomAction(item.provider, merged); : this.createUserCustomAction(item.provider, merged);
return persist.then((action: IUserCustomActionInfo): void => { return persist.then((action: IUserCustomActionInfo): void => {
item.context.action = action; item.context.action = action;
@@ -252,12 +252,13 @@ export class PortalSettingsDataService {
.then((payload: any): any => payload.d || payload); .then((payload: any): any => payload.d || payload);
} }
private saveUserCustomAction(action: IUserCustomActionInfo, config: any): Promise<void> { private saveUserCustomAction(action: IUserCustomActionInfo, config: any, provider: ISettingsProvider<any>): Promise<void> {
const url: string = this.siteUrl + "/_api/site/UserCustomActions(guid'" + action.id + "')"; const url: string = this.siteUrl + "/_api/site/UserCustomActions(guid'" + action.id + "')";
const body: any = { const body: any = {
'__metadata': { 'type': action.entityType || 'SP.UserCustomAction' }, '__metadata': { 'type': action.entityType || 'SP.UserCustomAction' },
'ClientSideComponentProperties': JSON.stringify(config) 'ClientSideComponentProperties': JSON.stringify(config)
}; };
if (provider.storage.action) { body.Sequence = provider.storage.action.sequence; }
return this.context.spHttpClient.post(url, SPHttpClient.configurations.v1, { return this.context.spHttpClient.post(url, SPHttpClient.configurations.v1, {
headers: this.writeHeaders('MERGE'), body: JSON.stringify(body) headers: this.writeHeaders('MERGE'), body: JSON.stringify(body)
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Konfiguration konnte nicht gespeichert werden.')); }).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Konfiguration konnte nicht gespeichert werden.'));
@@ -298,11 +299,35 @@ export class PortalSettingsDataService {
private saveRootProperty(key: string, value: string): Promise<void> { private saveRootProperty(key: string, value: string): Promise<void> {
if (!key) { return Promise.reject(new Error('Der Property-Bag-Schlüssel fehlt.')); } if (!key) { return Promise.reject(new Error('Der Property-Bag-Schlüssel fehlt.')); }
const body: any = { '__metadata': { 'type': 'SP.PropertyValues' } }; const requestXml: string = '<Request SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="PortalSettingsV3" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009">' +
body[this.encodePropertyName(key)] = value; '<Actions>' +
return this.context.spHttpClient.post(this.siteUrl + '/_api/web/AllProperties', SPHttpClient.configurations.v1, { '<Method Name="SetFieldValue" Id="5" ObjectPathId="4"><Parameters>' +
headers: this.writeHeaders('MERGE'), body: JSON.stringify(body) '<Parameter Type="String">' + this.escapeXml(key) + '</Parameter>' +
}).then((response: SPHttpClientResponse): Promise<void> => this.ensureOk(response, 'Der Site-Collection-Standard konnte nicht gespeichert werden.')); '<Parameter Type="String">' + this.escapeXml(value) + '</Parameter>' +
'</Parameters></Method>' +
'<Method Name="Update" Id="6" ObjectPathId="2" />' +
'</Actions>' +
'<ObjectPaths>' +
'<StaticProperty Id="0" TypeId="{3747adcd-a3c3-41b9-bfab-4a64dd2f1e0a}" Name="Current" />' +
'<Property Id="2" ParentId="0" Name="Web" />' +
'<Property Id="4" ParentId="2" Name="AllProperties" />' +
'</ObjectPaths>' +
'</Request>';
return this.context.spHttpClient.post(this.siteUrl + '/_vti_bin/client.svc/ProcessQuery', SPHttpClient.configurations.v1, {
headers: { 'Accept': 'application/json', 'Content-Type': 'text/xml;charset="UTF-8"' }, body: requestXml
}).then((response: SPHttpClientResponse): Promise<string> => {
if (!response.ok) {
return this.responseError(response, 'Der Site-Collection-Standard konnte nicht gespeichert werden.')
.then((error: Error): Promise<string> => Promise.reject(error));
}
return response.text();
}).then((text: string): void => {
const payload: any[] = text ? JSON.parse(text) : [];
if (payload.length && payload[0] && payload[0].ErrorInfo) {
throw new Error('Der Site-Collection-Standard konnte nicht gespeichert werden. ' +
(payload[0].ErrorInfo.ErrorMessage || 'SharePoint hat den CSOM-Aufruf abgelehnt.'));
}
});
} }
private findAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined { private findAction(provider: ISettingsProvider<any>, actions: IUserCustomActionInfo[]): IUserCustomActionInfo | undefined {
@@ -378,7 +403,10 @@ export class PortalSettingsDataService {
return url; return url;
} }
private normalizePropertyName(value: string): string { return String(value || '').replace(/_x002e_/gi, '.').toLowerCase(); } private normalizePropertyName(value: string): string { return String(value || '').replace(/_x002e_/gi, '.').toLowerCase(); }
private encodePropertyName(value: string): string { return value.replace(/\./g, '_x002e_'); } private escapeXml(value: string): string {
return String(value || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&apos;');
}
private readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; } private readHeaders(): any { return { 'Accept': 'application/json;odata=verbose', 'OData-Version': '3.0' }; }
private writeHeaders(method?: string): any { private writeHeaders(method?: string): any {
+15 -1
View File
@@ -25,6 +25,7 @@ const mega: ISettingsProvider<any> = getSettingsProvider('megamenu'); // tslint:
assert(!!mega.storage.action, 'MegaMenu-Aktivierungsdefinition fehlt.'); assert(!!mega.storage.action, 'MegaMenu-Aktivierungsdefinition fehlt.');
assert(mega.storage.action.name === 'MegaMenu', 'MegaMenu Action-Name ist nicht stabil.'); assert(mega.storage.action.name === 'MegaMenu', 'MegaMenu Action-Name ist nicht stabil.');
assert(mega.storage.action.location === 'ClientSideExtension.ApplicationCustomizer', 'MegaMenu Action-Location ist ungültig.'); assert(mega.storage.action.location === 'ClientSideExtension.ApplicationCustomizer', 'MegaMenu Action-Location ist ungültig.');
assert(mega.storage.action.sequence === 100, 'MegaMenu Action-Sequenz ist nicht stabil.');
const normalizedMega: any = mega.normalize({ cacheMinutes: 5000, cacheVersion: '', unknownFuture: 'keep', menuMode: 'other' }); // tslint:disable-line:no-any const normalizedMega: any = mega.normalize({ cacheMinutes: 5000, cacheVersion: '', unknownFuture: 'keep', menuMode: 'other' }); // tslint:disable-line:no-any
assert(normalizedMega.cacheMinutes === 1440, 'MegaMenu Cache-Maximum greift nicht.'); assert(normalizedMega.cacheMinutes === 1440, 'MegaMenu Cache-Maximum greift nicht.');
assert(normalizedMega.cacheVersion === '1', 'MegaMenu Cache-Fallback fehlt.'); assert(normalizedMega.cacheVersion === '1', 'MegaMenu Cache-Fallback fehlt.');
@@ -37,21 +38,26 @@ assert(!!current.storage.action, 'CurrentNavigation-Aktivierungsdefinition fehlt
assert(current.storage.action.name === 'CurrentNavigation', 'CurrentNavigation Action-Name ist nicht stabil.'); assert(current.storage.action.name === 'CurrentNavigation', 'CurrentNavigation Action-Name ist nicht stabil.');
assert(current.storage.action.sequence === 110, 'CurrentNavigation Action-Sequenz ist nicht stabil.'); assert(current.storage.action.sequence === 110, 'CurrentNavigation Action-Sequenz ist nicht stabil.');
const normalizedCurrent: any = current.normalize({ // tslint:disable-line:no-any const normalizedCurrent: any = current.normalize({ // tslint:disable-line:no-any
cacheMinutes: 5000, cacheVersion: '', maxDepth: 99, showRoot: true, future: 'keep' cacheMinutes: 5000, cacheVersion: '', maxDepth: 99, showRoot: true,
initialExpansion: 'expanded', future: 'keep'
}); });
assert(normalizedCurrent.cacheMinutes === 1440, 'CurrentNavigation Cache-Maximum greift nicht.'); assert(normalizedCurrent.cacheMinutes === 1440, 'CurrentNavigation Cache-Maximum greift nicht.');
assert(normalizedCurrent.cacheVersion === '1', 'CurrentNavigation Cache-Fallback fehlt.'); assert(normalizedCurrent.cacheVersion === '1', 'CurrentNavigation Cache-Fallback fehlt.');
assert(normalizedCurrent.maxDepth === 8, 'CurrentNavigation Tiefenlimit greift nicht.'); assert(normalizedCurrent.maxDepth === 8, 'CurrentNavigation Tiefenlimit greift nicht.');
assert(normalizedCurrent.showRoot === true, 'CurrentNavigation Root-Anzeige geht verloren.'); assert(normalizedCurrent.showRoot === true, 'CurrentNavigation Root-Anzeige geht verloren.');
assert(normalizedCurrent.initialExpansion === 'expanded', 'CurrentNavigation Aufklappzustand geht verloren.');
assert(normalizedCurrent.future === 'keep', 'Unbekannte CurrentNavigation-Properties gehen verloren.'); assert(normalizedCurrent.future === 'keep', 'Unbekannte CurrentNavigation-Properties gehen verloren.');
const invalidCurrent: any = current.createDefault(); // tslint:disable-line:no-any const invalidCurrent: any = current.createDefault(); // tslint:disable-line:no-any
invalidCurrent.termSetName = 'Nur ein Name'; invalidCurrent.termSetName = 'Nur ein Name';
assert(current.validate(invalidCurrent).length > 0, 'CurrentNavigation muss eine Termset-ID verlangen.'); assert(current.validate(invalidCurrent).length > 0, 'CurrentNavigation muss eine Termset-ID verlangen.');
assert(current.normalize({ initialExpansion: 'invalid' }).initialExpansion === 'activePath',
'CurrentNavigation benötigt einen kompatiblen Fallback für den Aufklappzustand.');
const branding: ISettingsProvider<any> = getSettingsProvider('custombranding'); // tslint:disable-line:no-any const branding: ISettingsProvider<any> = getSettingsProvider('custombranding'); // tslint:disable-line:no-any
assert(!!branding.storage.action, 'CustomBranding-Aktivierungsdefinition fehlt.'); assert(!!branding.storage.action, 'CustomBranding-Aktivierungsdefinition fehlt.');
assert(branding.storage.action.name === 'CustomBranding', 'CustomBranding Action-Name ist nicht stabil.'); assert(branding.storage.action.name === 'CustomBranding', 'CustomBranding Action-Name ist nicht stabil.');
assert(branding.storage.action.location === 'ClientSideExtension.ApplicationCustomizer', 'CustomBranding Action-Location ist ungültig.'); assert(branding.storage.action.location === 'ClientSideExtension.ApplicationCustomizer', 'CustomBranding Action-Location ist ungültig.');
assert(branding.storage.action.sequence === 90, 'CustomBranding muss vor MegaMenu geladen werden.');
const defaultBranding: any = branding.createDefault(); // tslint:disable-line:no-any const defaultBranding: any = branding.createDefault(); // tslint:disable-line:no-any
assert(defaultBranding.placeholderbottom.elements.length === 0, assert(defaultBranding.placeholderbottom.elements.length === 0,
'PortalSettings darf keinen Runtime-Standard-Footer in die Konfiguration schreiben.'); 'PortalSettings darf keinen Runtime-Standard-Footer in die Konfiguration schreiben.');
@@ -62,6 +68,14 @@ assert(normalizedBranding.elements === undefined, 'Legacy-Root-Elemente werden n
const unsafeBranding: any = branding.createDefault(); // tslint:disable-line:no-any const unsafeBranding: any = branding.createDefault(); // tslint:disable-line:no-any
unsafeBranding.placeholdertop.elements = [{ type: 'script', content: 'alert(1)' }, { type: 'a', attributes: { onclick: 'alert(1)', href: 'javascript:alert(1)' } }]; unsafeBranding.placeholdertop.elements = [{ type: 'script', content: 'alert(1)' }, { type: 'a', attributes: { onclick: 'alert(1)', href: 'javascript:alert(1)' } }];
assert(branding.validate(unsafeBranding).length >= 3, 'Unsichere Branding-Elemente werden nicht erkannt.'); assert(branding.validate(unsafeBranding).length >= 3, 'Unsichere Branding-Elemente werden nicht erkannt.');
const searchBranding: any = branding.createDefault(); // tslint:disable-line:no-any
searchBranding.placeholdertop.elements = [{
type: 'form', attributes: { action: '~sitecollection/_layouts/15/search.aspx/siteall', method: 'get' },
children: [{ type: 'input', attributes: { type: 'search', name: 'q' } }]
}];
assert(branding.validate(searchBranding).length === 0, 'Sichere deklarative Suchformulare werden nicht akzeptiert.');
searchBranding.placeholdertop.elements[0].attributes.action = 'javascript:alert(1)';
assert(branding.validate(searchBranding).length > 0, 'Unsichere Formular-Actions werden nicht erkannt.');
const expiry: ISettingsProvider<any> = getSettingsProvider('expiryindicator'); // tslint:disable-line:no-any const expiry: ISettingsProvider<any> = getSettingsProvider('expiryindicator'); // tslint:disable-line:no-any
const normalizedExpiry: any = expiry.normalize({ baseField: 'Invalid field', future: true }); // tslint:disable-line:no-any const normalizedExpiry: any = expiry.normalize({ baseField: 'Invalid field', future: true }); // tslint:disable-line:no-any
+12 -2
View File
@@ -21,9 +21,13 @@ var sourceFiles = [
'src/ui/PortalSettingsApp.ts' 'src/ui/PortalSettingsApp.ts'
].map(read).join('\n'); ].map(read).join('\n');
var dataService = read('src/services/PortalSettingsDataService.ts'); var dataService = read('src/services/PortalSettingsDataService.ts');
var rootPropertyWriter = dataService.substring(
dataService.indexOf('private saveRootProperty'),
dataService.indexOf('private findAction')
);
assert(packageJson.version === '3.2.2', 'Package-Version ist nicht 3.2.2.'); assert(packageJson.version === '3.2.7', 'Package-Version ist nicht 3.2.7.');
assert(solution.version === '3.2.2.0', 'Solution-Version ist nicht 3.2.2.0.'); assert(solution.version === '3.2.7.0', 'Solution-Version ist nicht 3.2.7.0.');
assert(!solution.features, 'Das inkompatible Seiten-Provisioning-Feature darf nicht paketiert werden.'); assert(!solution.features, 'Das inkompatible Seiten-Provisioning-Feature darf nicht paketiert werden.');
assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.'); assert(!!config.bundles['portal-settings-web-part'], 'WebPart-Bundle fehlt.');
assert(copyAssets.deployCdnPath === 'temp/deploy', 'Ship-Assets werden nicht nach temp/deploy geschrieben.'); assert(copyAssets.deployCdnPath === 'temp/deploy', 'Ship-Assets werden nicht nach temp/deploy geschrieben.');
@@ -34,6 +38,12 @@ assert(sourceFiles.indexOf('innerHTML') < 0, 'UI darf innerHTML nicht verwenden.
assert(dataService.indexOf('/_api/site/rootweb/UserCustomActions') >= 0 && assert(dataService.indexOf('/_api/site/rootweb/UserCustomActions') >= 0 &&
dataService.indexOf('siteActions.concat(values[1])') >= 0, dataService.indexOf('siteActions.concat(values[1])') >= 0,
'Root-Web-Actions werden nicht für die Provider-Erkennung berücksichtigt.'); 'Root-Web-Actions werden nicht für die Provider-Erkennung berücksichtigt.');
assert(dataService.indexOf('body.Sequence = provider.storage.action.sequence') >= 0,
'Bestehende Actions werden nicht auf die Provider-Sequence aktualisiert.');
assert(rootPropertyWriter.indexOf('/_vti_bin/client.svc/ProcessQuery') >= 0 &&
rootPropertyWriter.indexOf('SetFieldValue') >= 0 && rootPropertyWriter.indexOf('Name="Update"') >= 0 &&
rootPropertyWriter.indexOf('/_api/web/AllProperties') < 0,
'Root-Web-Properties müssen über den SharePoint-CSOM-Endpunkt gespeichert werden.');
['themePrimary', 'neutralPrimary', 'neutralLight', 'white'].forEach(function (slot) { ['themePrimary', 'neutralPrimary', 'neutralLight', 'white'].forEach(function (slot) {
assert(styles.indexOf('[theme: ' + slot + ', default:') >= 0, 'Themeslot fehlt: ' + slot); assert(styles.indexOf('[theme: ' + slot + ', default:') >= 0, 'Themeslot fehlt: ' + slot);
}); });