diff --git a/src/lib/PnP.Framework/Enums/BuiltInFieldId.cs b/src/lib/PnP.Framework/Enums/BuiltInFieldId.cs
index afc70f109..04ee8d039 100644
--- a/src/lib/PnP.Framework/Enums/BuiltInFieldId.cs
+++ b/src/lib/PnP.Framework/Enums/BuiltInFieldId.cs
@@ -1055,6 +1055,7 @@ public static class BuiltInFieldId
public static readonly Guid ResolvedBy = new Guid("{b4fa187b-eb65-478e-8bc6-93b0da320f03}");
public static readonly Guid ResolvedDate = new Guid("{c4995c71-4c5c-4e9f-afc1-a9033f2bfde5}");
public static readonly Guid Description = new Guid("{3f155110-a6a2-4d70-926c-94648101f0e8}");
+ public static readonly Guid _ExtendedDescription = new Guid("{cb19284a-cde7-4570-a980-1dab8bd74470}");
public static readonly Guid HolidayDate = new Guid("{335e22c3-b8a4-4234-9790-7a03eeb7b0d4}");
public static readonly Guid V4HolidayDate = new Guid("{492b1ac0-c594-4013-a2b6-ea70f5a8a506}");
public static readonly Guid IsNonWorkingDay = new Guid("{baf7091c-01fb-4831-a975-08254f87f234}");
@@ -1292,6 +1293,7 @@ public static bool Contains(Guid fid)
[End] = true,
[EncodedAbsThumbnailUrl] = true,
[Description] = true,
+ [_ExtendedDescription] = true,
[DisplayTemplateJSTargetContentType] = true,
[V4HolidayDate] = true,
[EmailSubject] = true,
diff --git a/src/lib/PnP.Framework/Extensions/BrandingExtensions.cs b/src/lib/PnP.Framework/Extensions/BrandingExtensions.cs
index b1490d114..fe0d28341 100644
--- a/src/lib/PnP.Framework/Extensions/BrandingExtensions.cs
+++ b/src/lib/PnP.Framework/Extensions/BrandingExtensions.cs
@@ -273,15 +273,20 @@ public static void SetComposedLookByUrl(this Web web, string lookName, string pa
/// URL of background image to apply
/// false (default) to apply to currently inheriting subsites only; true to force all subsites to inherit
/// false (default) to apply to subsites; true to only apply to specified site
+ /// When provided, skips the sentinel write probe.
+ /// True means property bag writes are allowed; false means blocked. Null (default) auto-detects.
[SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "OfficeDevPnP.Core.Diagnostics.Log.Debug(System.String,System.String,System.Object[])")]
- public static void SetThemeByUrl(this Web web, string paletteServerRelativeUrl, string fontServerRelativeUrl, string backgroundServerRelativeUrl, bool resetSubsitesToInherit = false, bool updateRootOnly = false)
+ public static void SetThemeByUrl(this Web web, string paletteServerRelativeUrl, string fontServerRelativeUrl, string backgroundServerRelativeUrl, bool resetSubsitesToInherit = false, bool updateRootOnly = false, bool? propertyBagWriteAllowed = null)
{
var websToUpdate = new List();
web.Context.Load(web, w => w.AllProperties, w => w.ServerRelativeUrl);
web.Context.ExecuteQueryRetry();
Log.Info(Constants.LOGGING_SOURCE, CoreResources.BrandingExtension_ApplyTheme, paletteServerRelativeUrl, web.ServerRelativeUrl);
- if (!web.IsNoScriptSite())
+ var blocked = propertyBagWriteAllowed.HasValue
+ ? !propertyBagWriteAllowed.Value
+ : web.IsPropertyBagWriteBlocked();
+ if (!blocked)
{
web.AllProperties[InheritTheme] = "False";
web.Update();
@@ -313,7 +318,7 @@ public static void SetThemeByUrl(this Web web, string paletteServerRelativeUrl,
if (resetSubsitesToInherit || inheritTheme)
{
Log.Debug(Constants.LOGGING_SOURCE, "Inherited: " + CoreResources.BrandingExtension_ApplyTheme, paletteServerRelativeUrl, childWeb.ServerRelativeUrl);
- if (!web.IsNoScriptSite())
+ if (!blocked)
{
childWeb.AllProperties[InheritTheme] = "True";
//childWeb.ThemedCssFolderUrl = themedCssFolderUrl;
diff --git a/src/lib/PnP.Framework/Extensions/WebExtensions.cs b/src/lib/PnP.Framework/Extensions/WebExtensions.cs
index 47c512a88..5e47e52e8 100644
--- a/src/lib/PnP.Framework/Extensions/WebExtensions.cs
+++ b/src/lib/PnP.Framework/Extensions/WebExtensions.cs
@@ -352,6 +352,62 @@ public static bool IsNoScriptSite(this Web web)
return false;
}
+ ///
+ /// Returns true when web property bag writes should be blocked.
+ ///
+ /// On NoScript sites (DenyAddAndCustomizePages = Enabled), checks whether the
+ /// tenant-level override AllowWebPropertyBagUpdateWhenDenyAddAndCustomizePagesIsEnabled
+ /// permits property bag writes by probing the API with a sentinel write.
+ /// Does not require tenant-admin permissions.
+ ///
+ /// Web to verify
+ /// True if property bag writes are blocked, false otherwise
+ public static bool IsPropertyBagWriteBlocked(this Web web)
+ {
+ // Fast path: if the site is not NoScript, property bag ops are always allowed.
+ if (!web.IsNoScriptSite())
+ {
+ return false;
+ }
+
+ // Probe: attempt a sentinel write to detect tenant override.
+ string sentinelKey = "_PnP_PropertyBagProbe_" + Guid.NewGuid().ToString("N");
+ try
+ {
+ var props = web.AllProperties;
+ web.Context.Load(props);
+ web.Context.ExecuteQueryRetry();
+
+ props[sentinelKey] = "probe";
+ web.Update();
+ web.Context.ExecuteQueryRetry();
+
+ // Write succeeded - tenant allows property bag updates.
+
+ // Clean up sentinel (both steps required — CSOM quirk, see RemovePropertyBagValueInternal).
+ props[sentinelKey] = null;
+ props.FieldValues.Remove(sentinelKey);
+ web.Update();
+ web.Context.ExecuteQueryRetry();
+
+ return false;
+ }
+ catch (ServerException ex) when (
+ ex.ServerErrorCode == -2147024891 // E_ACCESSDENIED
+ || ex.ServerErrorCode == -1
+ || (ex.ServerErrorTypeName != null && ex.ServerErrorTypeName.Contains("UnauthorizedAccessException")))
+ {
+ // Write genuinely blocked.
+ return true;
+ }
+ catch
+ {
+ // Any other error (connectivity, etc.) - assume blocked to preserve
+ // the pre-existing safe-by-default behaviour.
+ return true;
+ }
+ }
+
private static bool IsCannotGetSiteException(Exception ex)
{
if (ex is ServerException)
@@ -1000,9 +1056,14 @@ public static bool RemoveIndexedPropertyBagKey(this Web web, string key)
/// Queues a web for a full crawl the next incremental/continous crawl
///
/// Site to be processed
- public static void ReIndexWeb(this Web web)
+ /// When provided, skips the sentinel write probe.
+ /// True means property bag writes are allowed; false means blocked. Null (default) auto-detects.
+ public static void ReIndexWeb(this Web web, bool? propertyBagWriteAllowed = null)
{
- if (web.IsNoScriptSite())
+ var blocked = propertyBagWriteAllowed.HasValue
+ ? !propertyBagWriteAllowed.Value
+ : web.IsPropertyBagWriteBlocked();
+ if (blocked)
{
// Update individual lists instead, as web bag is no (longer) accessible
var context = web.Context;
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Extensions/CustomActionExtensions.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Extensions/CustomActionExtensions.cs
new file mode 100644
index 000000000..ab074de9f
--- /dev/null
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Extensions/CustomActionExtensions.cs
@@ -0,0 +1,17 @@
+using PnP.Framework.Provisioning.Model;
+using System;
+
+namespace PnP.Framework.Provisioning.ObjectHandlers.Extensions
+{
+ internal static class CustomActionExtensions
+ {
+ ///
+ /// Returns true when the given custom action is an SPFx extension.
+ /// Every SPFx extension has a non-empty ClientSideComponentId.
+ ///
+ internal static bool IsSPFxCustomAction(this CustomAction customAction)
+ {
+ return customAction.ClientSideComponentId != Guid.Empty;
+ }
+ }
+}
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectCustomActions.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectCustomActions.cs
index 82e9a6cb6..adc93f81b 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectCustomActions.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectCustomActions.cs
@@ -162,7 +162,7 @@ internal static void UpdateCustomAction(TokenParser parser, PnPMonitoredScope sc
{
var isDirty = false;
- if (isNoScriptSite)
+ if (isNoScriptSite && !customAction.IsSPFxCustomAction())
{
scope.LogWarning(CoreResources.Provisioning_ObjectHandlers_CustomActions_SkippingAddUpdateDueToNoScript, customAction.Name);
return;
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectListInstance.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectListInstance.cs
index 315259146..4d42cb82f 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectListInstance.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectListInstance.cs
@@ -1551,52 +1551,52 @@ private static bool UpdateCustomActions(Web web, List existingList, ListInstance
{
bool isDirty = false;
- if (!isNoScriptSite)
+ // Add any UserCustomActions
+ var existingUserCustomActions = existingList.UserCustomActions;
+ web.Context.Load(existingUserCustomActions);
+ web.Context.ExecuteQueryRetry();
+
+ foreach (CustomAction userCustomAction in templateList.UserCustomActions)
{
- // Add any UserCustomActions
- var existingUserCustomActions = existingList.UserCustomActions;
- web.Context.Load(existingUserCustomActions);
- web.Context.ExecuteQueryRetry();
+ // Per-action NoScript guard: only block non-SPFx actions
+ if (isNoScriptSite && !userCustomAction.IsSPFxCustomAction())
+ {
+ scope.LogWarning(CoreResources.Provisioning_ObjectHandlers_CustomActions_SkippingAddUpdateDueToNoScript, userCustomAction.Name);
+ continue;
+ }
- foreach (CustomAction userCustomAction in templateList.UserCustomActions)
+ // Check for existing custom actions before adding (compare by custom action name)
+ if (!existingUserCustomActions.AsEnumerable().Any(uca => uca.Name == userCustomAction.Name))
{
- // Check for existing custom actions before adding (compare by custom action name)
- if (!existingUserCustomActions.AsEnumerable().Any(uca => uca.Name == userCustomAction.Name))
- {
- CreateListCustomAction(existingList, parser, userCustomAction);
- isDirty = true;
- }
- else
+ CreateListCustomAction(existingList, parser, userCustomAction);
+ isDirty = true;
+ }
+ else
+ {
+ var existingCustomAction = existingUserCustomActions.AsEnumerable().FirstOrDefault(uca => uca.Name == userCustomAction.Name);
+ if (existingCustomAction != null)
{
- var existingCustomAction = existingUserCustomActions.AsEnumerable().FirstOrDefault(uca => uca.Name == userCustomAction.Name);
- if (existingCustomAction != null)
+ // If the custom action already exists
+ if (userCustomAction.Remove)
{
- // If the custom action already exists
- if (userCustomAction.Remove)
- {
- // And if we need to remove it, we simply delete it
- existingCustomAction.DeleteObject();
- }
- else
- {
- // Otherwise we update it, and before we force the target
- // registration type and ID to avoid issues
- userCustomAction.RegistrationType = UserCustomActionRegistrationType.List;
- userCustomAction.RegistrationId = existingList.Id.ToString("B").ToUpper();
- ObjectCustomActions.UpdateCustomAction(parser, scope, userCustomAction, existingCustomAction);
- // Blank out these values again to avoid inconsistent domain model data
- userCustomAction.RegistrationType = UserCustomActionRegistrationType.None;
- userCustomAction.RegistrationId = null;
- }
- isDirty = true;
+ // And if we need to remove it, we simply delete it
+ existingCustomAction.DeleteObject();
+ }
+ else
+ {
+ // Otherwise we update it, and before we force the target
+ // registration type and ID to avoid issues
+ userCustomAction.RegistrationType = UserCustomActionRegistrationType.List;
+ userCustomAction.RegistrationId = existingList.Id.ToString("B").ToUpper();
+ ObjectCustomActions.UpdateCustomAction(parser, scope, userCustomAction, existingCustomAction, isNoScriptSite);
+ // Blank out these values again to avoid inconsistent domain model data
+ userCustomAction.RegistrationType = UserCustomActionRegistrationType.None;
+ userCustomAction.RegistrationId = null;
}
+ isDirty = true;
}
}
}
- else
- {
- scope.LogWarning(CoreResources.Provisioning_ObjectHandlers_ListInstances_SkipAddingOrUpdatingCustomActions);
- }
return isDirty;
}
@@ -2019,19 +2019,18 @@ private Tuple CreateList(Web web, ListInstance templateList,
// Add any custom action
if (templateList.UserCustomActions.Any())
{
- if (!isNoScriptSite)
+ foreach (var userCustomAction in templateList.UserCustomActions)
{
- foreach (var userCustomAction in templateList.UserCustomActions)
+ // Per-action NoScript guard: only block non-SPFx actions
+ if (isNoScriptSite && !userCustomAction.IsSPFxCustomAction())
{
- CreateListCustomAction(createdList, parser, userCustomAction);
+ scope.LogWarning(CoreResources.Provisioning_ObjectHandlers_CustomActions_SkippingAddUpdateDueToNoScript, userCustomAction.Name);
+ continue;
}
-
- web.Context.ExecuteQueryRetry();
- }
- else
- {
- scope.LogWarning(CoreResources.Provisioning_ObjectHandlers_ListInstances_SkipAddingOrUpdatingCustomActions);
+ CreateListCustomAction(createdList, parser, userCustomAction);
}
+
+ web.Context.ExecuteQueryRetry();
}
// Process list webhooks
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectNavigation.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectNavigation.cs
index 2b48550db..74b5ea6c3 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectNavigation.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectNavigation.cs
@@ -142,8 +142,8 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
return parser;
}
- // Check if this is not a noscript site as navigation features are not supported
- bool isNoScriptSite = web.IsNoScriptSite();
+ // Check if property bag writes are blocked (NoScript without tenant override)
+ bool propertyBagWriteBlocked = applyingInformation.PropertyBagWriteAllowed == false;
// Retrieve the current web navigation settings
var navigationSettings = new WebNavigationSettings(web.Context, web);
@@ -158,7 +158,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
web.Update();
web.Context.ExecuteQueryRetry();
- if (!isNoScriptSite)
+ if (!propertyBagWriteBlocked)
{
navigationSettings.Update(TaxonomySession.GetTaxonomySession(web.Context));
web.Context.ExecuteQueryRetry();
@@ -194,7 +194,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
break;
}
- if (!isNoScriptSite)
+ if (!propertyBagWriteBlocked)
{
navigationSettings.Update(TaxonomySession.GetTaxonomySession(web.Context));
web.Context.ExecuteQueryRetry();
@@ -225,7 +225,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
navigationSettings.CurrentNavigation.TermSetId = Guid.Parse(parser.ParseString(template.Navigation.CurrentNavigation.ManagedNavigation.TermSetId));
break;
case CurrentNavigationType.StructuralLocal:
- if (!isNoScriptSite)
+ if (!propertyBagWriteBlocked)
{
web.SetPropertyBagValue(NavigationShowSiblings, "false");
}
@@ -238,7 +238,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
break;
case CurrentNavigationType.Structural:
default:
- if (!isNoScriptSite)
+ if (!propertyBagWriteBlocked)
{
web.SetPropertyBagValue(NavigationShowSiblings, "true");
}
@@ -251,7 +251,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
break;
}
- if (!isNoScriptSite)
+ if (!propertyBagWriteBlocked)
{
navigationSettings.Update(TaxonomySession.GetTaxonomySession(web.Context));
web.Context.ExecuteQueryRetry();
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPersistTemplateInfo.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPersistTemplateInfo.cs
index 9eca0326d..38510789b 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPersistTemplateInfo.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPersistTemplateInfo.cs
@@ -24,11 +24,9 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
{
using (var scope = new PnPMonitoredScope(this.Name))
{
- // Check if this is not a noscript site as we're not allowed to write to the web property bag is that one
- bool isNoScriptSite = web.IsNoScriptSite();
- if (isNoScriptSite)
+ // Check if property bag writes are blocked (NoScript without tenant override)
+ if (applyingInformation.PropertyBagWriteAllowed == false)
{
-
return parser;
}
@@ -60,7 +58,7 @@ public override bool WillProvision(Web web, ProvisioningTemplate template, Provi
{
if (!_willProvision.HasValue)
{
- _willProvision = !web.IsNoScriptSite();
+ _willProvision = applyingInformation.PropertyBagWriteAllowed != false;
}
return _willProvision.Value;
}
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs
index d5fec7eb1..4a2f226b4 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs
@@ -30,9 +30,8 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
"DesignPreview"
});
- // Check if this is not a noscript site as we're not allowed to write to the web property bag is that one
- bool isNoScriptSite = web.IsNoScriptSite();
- if (isNoScriptSite)
+ // Check if property bag writes are blocked (NoScript without tenant override)
+ if (applyingInformation.PropertyBagWriteAllowed == false)
{
return parser;
}
@@ -187,7 +186,7 @@ public override bool WillProvision(Web web, ProvisioningTemplate template, Provi
{
if (!_willProvision.HasValue)
{
- _willProvision = template.PropertyBagEntries.Any() && !web.IsNoScriptSite();
+ _willProvision = template.PropertyBagEntries.Any() && (applyingInformation.PropertyBagWriteAllowed != false);
}
return _willProvision.Value;
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectWebSettings.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectWebSettings.cs
index 8950f3299..ecdf85491 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectWebSettings.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectWebSettings.cs
@@ -343,7 +343,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ
}
}
- if (!isNoScriptSite)
+ if (applyingInformation.PropertyBagWriteAllowed != false)
{
web.NoCrawl = webSettings.NoCrawl;
}
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ProvisioningTemplateApplyingInformation.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ProvisioningTemplateApplyingInformation.cs
index e42da5388..434080fac 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ProvisioningTemplateApplyingInformation.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ProvisioningTemplateApplyingInformation.cs
@@ -129,5 +129,13 @@ public Dictionary AccessTokens
/// Defines a delay to wait for after modern site creation
///
public Int32 DelayAfterModernSiteCreation { get; set; }
+
+ ///
+ /// Controls whether property bag writes are allowed on NoScript sites.
+ /// When null (default), the provisioning engine auto-detects via a sentinel write probe.
+ /// Set to true if the tenant has AllowWebPropertyBagUpdateWhenDenyAddAndCustomizePagesIsEnabled = true.
+ /// Set to false to skip all property bag write operations without probing.
+ ///
+ public bool? PropertyBagWriteAllowed { get; set; }
}
}
diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/SiteToTemplateConversion.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/SiteToTemplateConversion.cs
index 85598cd4d..049e5cf60 100644
--- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/SiteToTemplateConversion.cs
+++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/SiteToTemplateConversion.cs
@@ -460,6 +460,11 @@ internal void ApplyRemoteTemplate(Web web, ProvisioningTemplate template, Provis
CallWebHooks(template, tokenParser, ProvisioningTemplateWebhookKind.ProvisioningTemplateStarted);
+ if (!provisioningInfo.PropertyBagWriteAllowed.HasValue)
+ {
+ provisioningInfo.PropertyBagWriteAllowed = !web.IsPropertyBagWriteBlocked();
+ }
+
foreach (var handler in objectHandlers)
{
if (handler.WillProvision(web, template, provisioningInfo))