Compare commits
5
Commits
55615fe75c
...
v.3.2.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31cf67ad73 | ||
|
|
3cc8364ad4 | ||
|
|
e8fa3c2924 | ||
|
|
de250e5dbb | ||
|
|
457e58b54f |
@@ -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,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.
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+1
-1
@@ -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
@@ -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');
|
||||||
|
}
|
||||||
|
|||||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user