feat: Update version to 3.0.5 and enhance CustomBranding with new search form functionality and improved styling

This commit is contained in:
Torsten Brendgen
2026-08-19 23:33:56 +02:00
parent 36bcef54e7
commit 281dfa68de
13 changed files with 353 additions and 60 deletions
+5 -1
View File
@@ -1,6 +1,6 @@
# CustomBranding # CustomBranding
CustomBranding 3.0.4 ist ein zentraler SPFx-1.4.1-Application-Customizer für SharePoint Server Subscription Edition. Die Solution lädt freigegebene Stylesheets und rendert eine kontrollierte Komponentenstruktur im oberen oder unteren SharePoint-Placeholder. Moderne und klassische Seiten verwenden dieselben `ClientSideComponentProperties`. CustomBranding 3.0.5 ist ein zentraler SPFx-1.4.1-Application-Customizer für SharePoint Server Subscription Edition. Die Solution lädt freigegebene Stylesheets und rendert eine kontrollierte Komponentenstruktur im oberen oder unteren SharePoint-Placeholder. Moderne und klassische Seiten verwenden dieselben `ClientSideComponentProperties`.
Die Konfiguration liegt in genau einer `SPSite.UserCustomAction` pro Site Collection. Es werden weder eine versteckte Liste noch ein Property Bag benötigt. Dadurch gilt das Branding automatisch für das Root Web, vorhandene Subwebs und später angelegte Subwebs. Die Konfiguration liegt in genau einer `SPSite.UserCustomAction` pro Site Collection. Es werden weder eine versteckte Liste noch ein Property Bag benötigt. Dadurch gilt das Branding automatisch für das Root Web, vorhandene Subwebs und später angelegte Subwebs.
@@ -121,6 +121,10 @@ Das aktuelle Konfigurationsschema hat die Version 2:
Bestehende 1.x-Konfigurationen mit einem Root-Array `elements` werden weiterhin als Top-Inhalt gelesen. Unbekannte Properties ignoriert die Runtime. Das vollständige Beispiel liegt unter `examples/custom-branding.example.json`. Bestehende 1.x-Konfigurationen mit einem Root-Array `elements` werden weiterhin als Top-Inhalt gelesen. Unbekannte Properties ignoriert die Runtime. Das vollständige Beispiel liegt unter `examples/custom-branding.example.json`.
Eine eigene Suche kann deklarativ als `form` mit einem `input type="search"` und einem Submit-Button unter
`placeholdertop.elements` konfiguriert werden. Formulare werden ausschließlich als GET-Formulare akzeptiert;
unsichere oder protokollrelative Action-URLs werden verworfen. Das vollständige Beispiel enthält eine solche Suche.
Ist `placeholderbottom.elements` leer oder fehlt die Eigenschaft, rendert CustomBranding automatisch den bisherigen Ist `placeholderbottom.elements` leer oder fehlt die Eigenschaft, rendert CustomBranding automatisch den bisherigen
**Portal Settings**-Link auf `~sitecollection/SitePages/PortalSettings.aspx`. Sobald eigene Footer-Elemente konfiguriert **Portal Settings**-Link auf `~sitecollection/SitePages/PortalSettings.aspx`. Sobald eigene Footer-Elemente konfiguriert
sind, ersetzen sie diesen Standard. sind, ersetzen sie diesen Standard.
+1 -1
View File
@@ -3,7 +3,7 @@
Analysestand: 20.07.2026 Analysestand: 20.07.2026
Umgesetzt: 20.07.2026 Umgesetzt: 20.07.2026
Ausgangsversion: 1.0.4 Ausgangsversion: 1.0.4
Zielversion: 3.0.4 Zielversion: 3.0.5
Status: Implementierung und lokale Qualitätssicherung abgeschlossen Status: Implementierung und lokale Qualitätssicherung abgeschlossen
## Zielbild ## Zielbild
+23 -6
View File
@@ -1,4 +1,4 @@
/* CustomBranding 2.0.0 - safe Classic SharePoint runtime */ /* CustomBranding 3.0.5 - safe Classic SharePoint runtime */
(function (global) { (function (global) {
'use strict'; 'use strict';
@@ -6,7 +6,7 @@
var OWNER = 'CustomBranding.Classic'; var OWNER = 'CustomBranding.Classic';
var MAX_DEPTH = 8; var MAX_DEPTH = 8;
var MAX_ELEMENTS = 200; var MAX_ELEMENTS = 200;
var allowedTags = ['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section']; var allowedTags = ['div', 'span', 'p', 'a', 'button', 'form', 'input', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'];
var allowedStyles = ('align-items background background-color border border-bottom border-color border-left border-radius border-right border-style border-top border-width box-sizing color display flex flex-basis flex-direction flex-grow flex-shrink flex-wrap font-family font-size font-style font-weight gap grid-template-columns height justify-content line-height margin margin-bottom margin-left margin-right margin-top max-height max-width min-height min-width opacity overflow padding padding-bottom padding-left padding-right padding-top text-align text-decoration text-transform white-space width').split(' '); var allowedStyles = ('align-items background background-color border border-bottom border-color border-left border-radius border-right border-style border-top border-width box-sizing color display flex flex-basis flex-direction flex-grow flex-shrink flex-wrap font-family font-size font-style font-weight gap grid-template-columns height justify-content line-height margin margin-bottom margin-left margin-right margin-top max-height max-width min-height min-width opacity overflow padding padding-bottom padding-left padding-right padding-top text-align text-decoration text-transform white-space width').split(' ');
var state = { debug: false, hosts: [], css: [] }; var state = { debug: false, hosts: [], css: [] };
@@ -78,6 +78,8 @@
if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; } if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; }
if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; } if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; }
if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; } if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; }
if (tag === 'form') { return ['action', 'method'].indexOf(name) >= 0; }
if (tag === 'input') { return ['type', 'name', 'placeholder', 'autocomplete', 'maxlength'].indexOf(name) >= 0; }
return false; return false;
} }
@@ -94,11 +96,23 @@
var name = String(rawName).toLowerCase(); var name = String(rawName).toLowerCase();
var value = String(attributes[rawName] === undefined ? '' : attributes[rawName]).substring(0, 2048); var value = String(attributes[rawName] === undefined ? '' : attributes[rawName]).substring(0, 2048);
if (name.indexOf('on') === 0 || !allowedAttribute(tag, name)) { continue; } if (name.indexOf('on') === 0 || !allowedAttribute(tag, name)) { continue; }
if (name === 'href' || name === 'src') { if (name === 'href' || name === 'src' || name === 'action') {
var safeUrl = sanitizeUrl(value, name === 'href'); var safeUrl = sanitizeUrl(value, name === 'href');
if (safeUrl) { element.setAttribute(name, safeUrl); } if (safeUrl && safeUrl.indexOf('//') !== 0) { element.setAttribute(name, safeUrl); }
} else if (name === 'target') { } else if (name === 'target') {
if (value === '_blank' || value === '_self') { element.setAttribute(name, value); } if (value === '_blank' || value === '_self') { element.setAttribute(name, value); }
} else if (name === 'type' && tag === 'button') {
element.setAttribute('type', value.toLowerCase() === 'submit' ? 'submit' : 'button');
} else if (name === 'type' && tag === 'input') {
element.setAttribute('type', 'search');
} else if (name === 'method' && tag === 'form') {
element.setAttribute('method', 'get');
} else if (name === 'name' && tag === 'input') {
if (/^[a-z0-9_-]{1,64}$/i.test(value)) { element.setAttribute('name', value); }
} else if (name === 'autocomplete' && tag === 'input') {
if (value === 'on' || value === 'off') { element.setAttribute('autocomplete', value); }
} else if (name === 'maxlength' && tag === 'input') {
if (/^\d{1,4}$/.test(value)) { element.setAttribute('maxlength', value); }
} else if ((name === 'id' || name === 'class') && !/^[a-z0-9 _-]{1,256}$/i.test(value)) { } else if ((name === 'id' || name === 'class') && !/^[a-z0-9 _-]{1,256}$/i.test(value)) {
continue; continue;
} else if ((name === 'width' || name === 'height') && !/^\d{1,4}$/.test(value)) { } else if ((name === 'width' || name === 'height') && !/^\d{1,4}$/.test(value)) {
@@ -109,7 +123,10 @@
if (name === 'alt') { hasAlt = true; } if (name === 'alt') { hasAlt = true; }
} }
if (tag === 'img' && !hasAlt) { return null; } if (tag === 'img' && !hasAlt) { return null; }
if (tag === 'button') { element.setAttribute('type', 'button'); } if (tag === 'form' && !element.getAttribute('action')) { return null; }
if (tag === 'form') { element.setAttribute('method', 'get'); }
if (tag === 'input') { element.setAttribute('type', 'search'); element.setAttribute('name', element.getAttribute('name') || 'q'); }
if (tag === 'button' && element.getAttribute('type') !== 'submit') { element.setAttribute('type', 'button'); }
if (tag === 'a' && element.getAttribute('target') === '_blank') { element.setAttribute('rel', 'noopener noreferrer'); } if (tag === 'a' && element.getAttribute('target') === '_blank') { element.setAttribute('rel', 'noopener noreferrer'); }
var styles = config.styles && typeof config.styles === 'object' ? config.styles : {}; var styles = config.styles && typeof config.styles === 'object' ? config.styles : {};
@@ -121,7 +138,7 @@
if (config.content !== undefined && config.content !== null) { if (config.content !== undefined && config.content !== null) {
element.appendChild(document.createTextNode(String(config.content).substring(0, 4000))); element.appendChild(document.createTextNode(String(config.content).substring(0, 4000)));
} }
if (tag !== 'img' && Array.isArray(config.children)) { if (tag !== 'img' && tag !== 'input' && Array.isArray(config.children)) {
for (var i = 0; i < config.children.length; i++) { for (var i = 0; i < config.children.length; i++) {
var child = createElement(config.children[i], depth + 1, counter); var child = createElement(config.children[i], depth + 1, counter);
if (child) { element.appendChild(child); } if (child) { element.appendChild(child); }
+1 -1
View File
@@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "custom-branding-client-side-solution", "name": "custom-branding-client-side-solution",
"id": "03a7c4de-e031-4b83-a683-5ca5c364166e", "id": "03a7c4de-e031-4b83-a683-5ca5c364166e",
"version": "3.0.4.0", "version": "3.0.5.0",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"skipFeatureDeployment": true "skipFeatureDeployment": true
}, },
+200 -26
View File
@@ -1,44 +1,218 @@
.custom-branding-banner { /*
align-items: center; * Custom Branding und Mega Menu sind direkte Geschwister in #CustomHeader:
background: #f3f2f1; *
* #CustomHeader
* .system-ndzk-bar
* .portal-branding | #CustomNavigation
*/
#CustomHeader {
align-items: stretch;
background: #ffffff;
border-bottom: 1px solid #edebe9; border-bottom: 1px solid #edebe9;
box-sizing: border-box; box-sizing: border-box;
color: #323130; display: grid;
display: flex;
font-family: "Segoe UI", "Segoe UI Web (West European)", sans-serif; font-family: "Segoe UI", "Segoe UI Web (West European)", sans-serif;
font-size: 14px; grid-template-areas:
gap: 16px; "system system"
min-height: 40px; "branding navigation";
padding: 8px 24px; grid-template-columns: 240px minmax(0, 1fr);
grid-template-rows: 28px 309px;
gap: 0;
height: 250px;
width: 100%;
} }
.custom-branding-link { #CustomHeader > .system-ndzk-bar {
align-items: center;
background: #c50f1f;
box-sizing: border-box;
color: #ffffff;
display: flex;
font-size: 14px;
font-weight: 600;
grid-area: system;
grid-column: 1 / -1;
grid-row: 1;
line-height: 20px;
padding: 4px 16px;
}
#CustomHeader > .portal-branding {
grid-area: branding;
grid-column: 1;
grid-row: 2;
min-width: 0;
}
#CustomHeader > #CustomNavigation {
align-items: stretch;
align-self: start;
border-bottom: 0 !important;
border-top: 0 !important;
display: flex;
grid-area: navigation;
grid-column: 2;
grid-row: 2;
height: 40px;
min-width: 0;
position: relative;
--megaMenuBarHeight: 40px;
}
#CustomHeader > #CustomNavigation > * {
width: 100%;
}
#CustomHeader > #CustomNavigation .mega-menu-main {
background: #003d79;
border-top: 0 !important;
color: #ffffff;
height: 40px;
margin-top: 0;
min-height: 40px;
}
#CustomHeader > #CustomNavigation .mega-menu-top-level,
#CustomHeader > #CustomNavigation .menu-item-link,
#CustomHeader > #CustomNavigation .menu-item-text,
#CustomHeader > #CustomNavigation .menu-item-toggle {
color: #ffffff;
}
#CustomHeader > #CustomNavigation .mega-menu-top-item:hover,
#CustomHeader > #CustomNavigation .mega-menu-top-item:focus-within,
#CustomHeader > #CustomNavigation .mega-menu-top-item:active,
#CustomHeader > #CustomNavigation .mega-menu-top-item.is-open,
#CustomHeader > #CustomNavigation .mega-menu-top-item.is-active {
background: #0f548c;
color: #ffffff;
}
/* Das geöffnete Mega Menu nutzt auch die 240-px-Fläche unterhalb des Logos. */
#CustomHeader > #CustomNavigation .mega-menu {
left: -240px;
}
#CustomHeader > #CustomNavigation .mega-menu-grid {
max-width: none;
width: 100%;
}
/* Eigene Suche im freien Bereich rechts neben dem Logo. */
#CustomHeader > #CustomBrandingSearch {
align-self: start;
grid-column: 2;
grid-row: 2;
justify-self: end;
margin: 64px 2px 0 0;
width: 272px;
z-index: 2;
}
.custom-branding-search-form {
align-items: stretch;
background: #ffffff;
border: 1px solid #8a8886;
box-sizing: border-box;
display: flex;
height: 36px;
width: 100%;
}
.custom-branding-search-form:focus-within {
border-color: #605e5c;
box-shadow: inset 0 0 0 1px #605e5c;
}
.custom-branding-search-input {
background: transparent;
border: 0;
box-sizing: border-box;
color: #323130;
flex: 1 1 auto;
font-family: inherit;
font-size: 14px;
min-width: 0;
outline: 0;
padding: 0 12px;
}
.custom-branding-search-input::placeholder {
color: #605e5c;
opacity: 1;
}
.custom-branding-search-button {
align-items: center;
background: transparent;
border: 0;
color: #005a9e; color: #005a9e;
cursor: pointer;
display: flex;
flex: 0 0 40px;
font-size: 16px;
justify-content: center;
padding: 0;
}
.custom-branding-search-button:hover,
.custom-branding-search-button:focus {
background: #f3f2f1;
color: #004578;
outline: 0;
}
.custom-branding-search-button:focus-visible {
box-shadow: inset 0 0 0 2px #605e5c;
}
.ms-compositeHeader .ms-compositeHeader-searchBoxContainer {
display: none !important;
}
.portal-branding {
align-items: flex-start;
border: 0;
box-sizing: border-box;
display: flex;
height: 220px;
justify-content: flex-start;
padding: 0;
width: 240px;
}
.portal-branding-home {
align-items: flex-start;
color: #323130;
display: block;
width: 240px;
text-decoration: none; text-decoration: none;
} }
.custom-branding-link:hover { .portal-branding-home:hover {
color: #004578; color: #201f1e;
text-decoration: underline; text-decoration: none;
} }
.custom-branding-link:focus-visible, .portal-branding-home:focus-visible {
.custom-branding-banner button:focus-visible { outline: 2px solid #605e5c;
outline: 2px solid #005a9e; outline-offset: 3px;
outline-offset: 2px;
} }
@media screen and (max-width: 640px) { .portal-branding-logo {
.custom-branding-banner { display: block;
align-items: flex-start; height: 220px;
flex-direction: column; object-fit: contain;
gap: 4px; width: 240px;
padding: 8px 12px; background-image: url(http://portal.contoso.local/Branding/Images/logo-background.png);
} background-repeat: no-repeat;
} }
@media (forced-colors: active) { @media (forced-colors: active) {
.custom-branding-banner { #CustomHeader,
border-bottom: 1px solid CanvasText; .portal-branding,
.system-ndzk-bar {
border-color: CanvasText;
} }
} }
+67 -10
View File
@@ -17,24 +17,81 @@
"placeholdertop": { "placeholdertop": {
"elements": [ "elements": [
{ {
"type": "section", "type": "div",
"content": "System NDzK",
"attributes": { "attributes": {
"class": "custom-branding-banner", "class": "system-ndzk-bar",
"role": "region", "role": "note"
"aria-label": "Portalhinweis" }
},
{
"type": "div",
"attributes": {
"class": "portal-branding",
"role": "banner",
"aria-label": "Portal-Kopfbereich"
}, },
"children": [ "children": [
{ {
"type": "strong", "type": "a",
"content": "Willkommen im Portal" "attributes": {
"class": "portal-branding-home",
"href": "~sitecollection/SitePages/Home.aspx",
"title": "Zur Portal-Startseite"
},
"children": [
{
"type": "img",
"attributes": {
"class": "portal-branding-logo",
"src": "~sitecollection/SiteAssets/branding/logo.png",
"alt": "Zentrale Abbildende Aufklärung"
}
}
]
}
]
}, },
{ {
"type": "a", "type": "form",
"content": "Zur Startseite",
"attributes": { "attributes": {
"href": "~sitecollection/SitePages/Home.aspx", "id": "CustomBrandingSearch",
"class": "custom-branding-link" "class": "custom-branding-search-form",
"role": "search",
"aria-label": "Diese Website durchsuchen",
"action": "~sitecollection/_layouts/15/search.aspx/siteall",
"method": "get"
},
"children": [
{
"type": "input",
"attributes": {
"class": "custom-branding-search-input",
"type": "search",
"name": "q",
"placeholder": "Diese Website durchsuchen",
"autocomplete": "off",
"maxlength": "500",
"aria-label": "Diese Website durchsuchen"
} }
},
{
"type": "button",
"attributes": {
"class": "custom-branding-search-button",
"type": "submit",
"title": "Suchen",
"aria-label": "Suchen"
},
"children": [
{
"type": "span",
"attributes": {
"class": "ms-Icon ms-Icon--Search",
"aria-hidden": "true"
}
}
]
} }
] ]
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "custom-branding", "name": "custom-branding",
"version": "3.0.4", "version": "3.0.5",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "custom-branding", "name": "custom-branding",
"version": "3.0.4", "version": "3.0.5",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@@ -14,7 +14,7 @@ export const MaxBrandingDepth: number = 8;
export const MaxBrandingElements: number = 200; export const MaxBrandingElements: number = 200;
export const MaxBrandingConfigurationLength: number = 100000; export const MaxBrandingConfigurationLength: number = 100000;
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'];
const GlobalAttributes: string[] = ['id', 'class', 'title', 'role', 'aria-label', 'aria-hidden', 'aria-current', 'aria-live']; const GlobalAttributes: string[] = ['id', 'class', 'title', 'role', 'aria-label', 'aria-hidden', 'aria-current', 'aria-live'];
const AllowedStyles: string[] = [ const AllowedStyles: string[] = [
'align-items', 'background', 'background-color', 'border', 'border-bottom', 'border-color', 'border-left', 'align-items', 'background', 'background-color', 'border', 'border-bottom', 'border-color', 'border-left',
@@ -254,6 +254,10 @@ function normalizeElement(value: any, depth: number, state: INormalizationState)
state.warnings.push('Image without alt attribute was rejected.'); state.warnings.push('Image without alt attribute was rejected.');
return undefined; return undefined;
} }
if (tag === 'form' && !attributes.action) {
state.warnings.push('Form without a safe action URL was rejected.');
return undefined;
}
const styles: { [key: string]: string } = {}; const styles: { [key: string]: string } = {};
if (value.styles && typeof value.styles === 'object') { if (value.styles && typeof value.styles === 'object') {
@@ -272,7 +276,7 @@ function normalizeElement(value: any, depth: number, state: INormalizationState)
const content: string = value.content === undefined || value.content === null const content: string = value.content === undefined || value.content === null
? '' ? ''
: String(value.content).substring(0, 4000); : String(value.content).substring(0, 4000);
const children: IBrandingElement[] = tag === 'img' const children: IBrandingElement[] = tag === 'img' || tag === 'input'
? [] ? []
: normalizeElements(value.children, depth + 1, state); : normalizeElements(value.children, depth + 1, state);
if ((tag === 'a' || tag === 'button') && !content && children.length === 0 && !attributes['aria-label']) { if ((tag === 'a' || tag === 'button') && !content && children.length === 0 && !attributes['aria-label']) {
@@ -293,6 +297,8 @@ function normalizeAttributes(tag: string, value: any, state: INormalizationState
const result: { [key: string]: string } = {}; const result: { [key: string]: string } = {};
if (!value || typeof value !== 'object') { if (!value || typeof value !== 'object') {
if (tag === 'button') { result.type = 'button'; } if (tag === 'button') { result.type = 'button'; }
if (tag === 'form') { result.method = 'get'; }
if (tag === 'input') { result.type = 'search'; result.name = 'q'; }
return result; return result;
} }
for (const rawName in value) { for (const rawName in value) {
@@ -303,9 +309,9 @@ function normalizeAttributes(tag: string, value: any, state: INormalizationState
state.warnings.push('Attribute "' + name + '" was rejected.'); state.warnings.push('Attribute "' + name + '" was rejected.');
continue; continue;
} }
if (name === 'href' || name === 'src') { if (name === 'href' || name === 'src' || name === 'action') {
const safeUrl: string = sanitizeNavigationUrl(rawValue, state.siteCollectionUrl, name === 'href'); const safeUrl: string = sanitizeNavigationUrl(rawValue, state.siteCollectionUrl, name === 'href');
if (safeUrl) { result[name] = safeUrl; } else { state.warnings.push('URL attribute was rejected.'); } if (safeUrl && safeUrl.indexOf('//') !== 0) { result[name] = safeUrl; } else { state.warnings.push('URL attribute was rejected.'); }
} else if (name === 'target') { } else if (name === 'target') {
if (rawValue === '_blank' || rawValue === '_self') { result[name] = rawValue; } if (rawValue === '_blank' || rawValue === '_self') { result[name] = rawValue; }
} else if (name === 'id' || name === 'class') { } else if (name === 'id' || name === 'class') {
@@ -315,12 +321,24 @@ function normalizeAttributes(tag: string, value: any, state: INormalizationState
} else if (name === 'aria-hidden') { } else if (name === 'aria-hidden') {
if (rawValue === 'true' || rawValue === 'false') { result[name] = rawValue; } if (rawValue === 'true' || rawValue === 'false') { result[name] = rawValue; }
} else if (name === 'type' && tag === 'button') { } else if (name === 'type' && tag === 'button') {
result.type = 'button'; result.type = rawValue.toLowerCase() === 'submit' ? 'submit' : 'button';
} else if (name === 'type' && tag === 'input') {
result.type = 'search';
} else if (name === 'method' && tag === 'form') {
result.method = 'get';
} else if (name === 'name' && tag === 'input') {
if (/^[a-z0-9_-]{1,64}$/i.test(rawValue)) { result.name = rawValue; }
} else if (name === 'autocomplete' && tag === 'input') {
if (rawValue === 'on' || rawValue === 'off') { result.autocomplete = rawValue; }
} else if (name === 'maxlength' && tag === 'input') {
if (/^\d{1,4}$/.test(rawValue)) { result.maxlength = rawValue; }
} else { } else {
result[name] = rawValue; result[name] = rawValue;
} }
} }
if (tag === 'button') { result.type = 'button'; } if (tag === 'button' && result.type !== 'submit') { result.type = 'button'; }
if (tag === 'form') { result.method = 'get'; }
if (tag === 'input') { result.type = 'search'; result.name = result.name || 'q'; }
if (tag === 'a' && result.target === '_blank') { result.rel = 'noopener noreferrer'; } if (tag === 'a' && result.target === '_blank') { result.rel = 'noopener noreferrer'; }
return result; return result;
} }
@@ -330,6 +348,8 @@ function isAttributeAllowed(tag: string, name: string): boolean {
if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; } if (tag === 'a') { return ['href', 'target'].indexOf(name) >= 0; }
if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; } if (tag === 'img') { return ['src', 'alt', 'width', 'height'].indexOf(name) >= 0; }
if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; } if (tag === 'button') { return ['type', 'disabled', 'aria-expanded', 'aria-controls'].indexOf(name) >= 0; }
if (tag === 'form') { return ['action', 'method'].indexOf(name) >= 0; }
if (tag === 'input') { return ['type', 'name', 'placeholder', 'autocomplete', 'maxlength'].indexOf(name) >= 0; }
return false; return false;
} }
@@ -1,4 +1,4 @@
export type BrandingElementType = 'div' | 'span' | 'p' | 'a' | 'button' | 'img' | 'h1' | 'h2' | 'h3' | 'strong' | 'em' | 'nav' | 'section'; export type BrandingElementType = 'div' | 'span' | 'p' | 'a' | 'button' | 'form' | 'input' | 'img' | 'h1' | 'h2' | 'h3' | 'strong' | 'em' | 'nav' | 'section';
export interface ICssFile { export interface ICssFile {
path: string; path: string;
@@ -39,7 +39,7 @@ export default class CustomBrandingApplicationCustomizer
this.context.pageContext.site.absoluteUrl this.context.pageContext.site.absoluteUrl
); );
this._config = result.config; this._config = result.config;
this.debug(strings.Initialized + ' 3.0.4.', { this.debug(strings.Initialized + ' 3.0.5.', {
schemaVersion: this._config.schemaVersion, schemaVersion: this._config.schemaVersion,
cssFileCount: this._config.cssfiles.length, cssFileCount: this._config.cssfiles.length,
warningCount: result.warnings.length warningCount: result.warnings.length
+20 -2
View File
@@ -49,9 +49,9 @@ assert(customBottom.config.placeholderbottom.elements.length === 1 &&
var legacy = normalize({ elements: [{ type: 'span', content: 'Alt' }] }, siteUrl); var legacy = normalize({ elements: [{ type: 'span', content: 'Alt' }] }, siteUrl);
assert(legacy.config.placeholdertop.elements.length === 1, 'Das Legacy-Root-Array muss weiter funktionieren.'); assert(legacy.config.placeholdertop.elements.length === 1, 'Das Legacy-Root-Array muss weiter funktionieren.');
['div', 'span', 'p', 'a', 'button', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section'] ['div', 'span', 'p', 'a', 'button', 'form', 'input', 'img', 'h1', 'h2', 'h3', 'strong', 'em', 'nav', 'section']
.forEach(function (tag) { .forEach(function (tag) {
var attributes = tag === 'img' ? { alt: '' } : undefined; var attributes = tag === 'img' ? { alt: '' } : (tag === 'form' ? { action: '/search' } : undefined);
var content = tag === 'a' || tag === 'button' ? tag : undefined; var content = tag === 'a' || tag === 'button' ? tag : undefined;
assert(!!element({ type: tag, content: content, attributes: attributes }), 'Erlaubter Tag fehlt: ' + tag); assert(!!element({ type: tag, content: content, attributes: attributes }), 'Erlaubter Tag fehlt: ' + tag);
}); });
@@ -83,6 +83,24 @@ assert(!element({ type: 'img', attributes: { src: '/logo.png' } }), 'Bilder ohne
assert(!!element({ type: 'img', attributes: { src: '/logo.png', alt: '' } }), 'Dekorative Bilder mit leerem alt sind erlaubt.'); assert(!!element({ type: 'img', attributes: { src: '/logo.png', alt: '' } }), 'Dekorative Bilder mit leerem alt sind erlaubt.');
assert(!element({ type: 'button' }), 'Leere Buttons muessen verworfen werden.'); assert(!element({ type: 'button' }), 'Leere Buttons muessen verworfen werden.');
var searchForm = element({
type: 'form',
attributes: { action: '~sitecollection/_layouts/15/search.aspx/siteall', method: 'post' },
children: [
{ type: 'input', attributes: { type: 'text', name: 'q', placeholder: 'Suchen', autocomplete: 'off' } },
{ type: 'button', attributes: { type: 'submit', 'aria-label': 'Suchen' } }
]
});
assert(searchForm.attributes.action === siteUrl + '/_layouts/15/search.aspx/siteall' && searchForm.attributes.method === 'get',
'Suchformulare muessen eine sichere GET-Action verwenden.');
assert(searchForm.children[0].attributes.type === 'search' && searchForm.children[0].attributes.name === 'q',
'Suchfelder muessen auf type=search begrenzt sein.');
assert(searchForm.children[1].attributes.type === 'submit', 'Submit-Buttons in Suchformularen muessen erhalten bleiben.');
assert(!element({ type: 'form', attributes: { action: 'javascript:alert(1)' } }),
'Formulare mit unsicherer Action muessen verworfen werden.');
assert(!element({ type: 'form', attributes: { action: '//evil.example.org/search' } }),
'Formulare mit protokollrelativer Action muessen verworfen werden.');
var css = normalize({ var css = normalize({
allowedCssHosts: ['cdn.example.org'], allowedCssHosts: ['cdn.example.org'],
cssfiles: [ cssfiles: [
+5 -2
View File
@@ -21,18 +21,21 @@ var packageJson = json('package.json');
var solution = json('config/package-solution.json').solution; var solution = json('config/package-solution.json').solution;
var serveText = read('config/serve.json').toLowerCase(); var serveText = read('config/serve.json').toLowerCase();
var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts'); var appSource = read('src/extensions/customBranding/CustomBrandingApplicationCustomizer.ts');
var configSource = read('src/extensions/customBranding/BrandingConfig.ts');
var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts'); var rendererSource = read('src/extensions/customBranding/BrandingDomRenderer.ts');
var classicSource = read('classic/custom-branding-classic.js'); var classicSource = read('classic/custom-branding-classic.js');
var deploymentSource = read('deployment/add-custombranding.ps1'); var deploymentSource = read('deployment/add-custombranding.ps1');
assert(packageJson.version === '3.0.4', 'package.json hat nicht Version 3.0.4.'); assert(packageJson.version === '3.0.5', 'package.json hat nicht Version 3.0.5.');
assert(solution.version === '3.0.4.0', 'Solution-Version ist inkonsistent.'); assert(solution.version === '3.0.5.0', 'Solution-Version ist inkonsistent.');
assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.'); assert(solution.skipFeatureDeployment === true, 'Tenantweite Bereitstellung ist nicht aktiviert.');
assert(!solution.features, 'Die alte web-scoped Feature-Registrierung ist noch vorhanden.'); assert(!solution.features, 'Die alte web-scoped Feature-Registrierung ist noch vorhanden.');
assert(!fs.existsSync(path.join(root, 'sharepoint/assets/elements.xml')), 'elements.xml muss entfernt sein.'); assert(!fs.existsSync(path.join(root, 'sharepoint/assets/elements.xml')), 'elements.xml muss entfernt sein.');
assert(serveText.indexOf('onclick') < 0, 'serve.json enthaelt ein Event-Attribut.'); assert(serveText.indexOf('onclick') < 0, 'serve.json enthaelt ein Event-Attribut.');
assert(serveText.indexOf('placeholdertop') >= 0, 'serve.json verwendet nicht das echte Schema.'); assert(serveText.indexOf('placeholdertop') >= 0, 'serve.json verwendet nicht das echte Schema.');
assert(rendererSource.indexOf('innerHTML') < 0 && appSource.indexOf('innerHTML') < 0, 'Der moderne Renderer darf innerHTML nicht verwenden.'); assert(rendererSource.indexOf('innerHTML') < 0 && appSource.indexOf('innerHTML') < 0, 'Der moderne Renderer darf innerHTML nicht verwenden.');
assert(configSource.indexOf("'form', 'input'") >= 0 && configSource.indexOf("result.method = 'get'") >= 0,
'Sichere deklarative Suchformulare sind nicht freigeschaltet.');
assert(classicSource.indexOf('innerHTML') < 0, 'Die Classic-Runtime darf innerHTML nicht verwenden.'); assert(classicSource.indexOf('innerHTML') < 0, 'Die Classic-Runtime darf innerHTML nicht verwenden.');
assert(classicSource.indexOf('~sitecollection/SitePages/PortalSettings.aspx') >= 0, assert(classicSource.indexOf('~sitecollection/SitePages/PortalSettings.aspx') >= 0,
'Der Standard-Footer fehlt in der Classic-Runtime.'); 'Der Standard-Footer fehlt in der Classic-Runtime.');