Skip to content

Commit 36c65e9

Browse files
feat(SharePoint): add site language support for SharePoint site creation
Synced from CyberDrain/CIPP@f079294
1 parent f6f95d9 commit 36c65e9

2 files changed

Lines changed: 99 additions & 18 deletions

File tree

Modules/CIPPCore/Public/Invoke-CIPPSharePointTemplateDeploy.ps1

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ function Invoke-CIPPSharePointTemplateDeploy {
4949
Set-CIPPAsyncDeploymentStep -JobId $DeploymentId -Name $TenantFilter -StepIndex $Index -StepStatus $Status -Message $Message
5050
}
5151

52-
# Extracts the group display name from a stored permission entry: the frontend saves plain
53-
# strings, but older entries may be autocomplete objects ({label,value}).
54-
$GetPrincipalName = { param($Principal) $Principal.value ?? $Principal }
5552
$CreateMissingGroups = $TemplateData.createMissingGroups -eq $true
5653
$SkipIfExists = $TemplateData.skipIfExists -eq $true
5754

@@ -108,9 +105,34 @@ function Invoke-CIPPSharePointTemplateDeploy {
108105
$Team = New-CIPPTeam -DisplayName $SiteTemplate.displayName -Description ($SiteTemplate.description ?? '') -Owner $SiteOwner -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
109106
$SiteUrl = $Team.SiteUrl
110107
$Results.Add("[$TenantFilter] Created Team '$($SiteTemplate.displayName)' with site $SiteUrl")
108+
$RawLanguage = [string](($SiteTemplate.language.value ?? $SiteTemplate.language))
109+
if ($RawLanguage -and $RawLanguage -ne 'default') {
110+
$Results.Add("[$TenantFilter] $($SiteTemplate.displayName): site language '$RawLanguage' was not applied — Teams sites use the tenant default SharePoint language.")
111+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "SharePoint template site '$($SiteTemplate.displayName)': language LCID $RawLanguage skipped for Teams site (tenant default applies)." -sev Info
112+
}
111113
} else {
112114
Update-DeployStep -Index $SiteIndex -Status 'running' -Message "Step 2 of ${TotalSteps}: Creating SharePoint site"
113-
$null = New-CIPPSharepointSite -SiteName $SiteTemplate.displayName -SiteDescription ($SiteTemplate.description ?? $SiteTemplate.displayName) -SiteOwner $SiteOwner -TemplateName 'Team' -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
115+
$SiteParams = @{
116+
SiteName = $SiteTemplate.displayName
117+
SiteDescription = ($SiteTemplate.description ?? $SiteTemplate.displayName)
118+
SiteOwner = $SiteOwner
119+
TemplateName = 'Team'
120+
TenantFilter = $TenantFilter
121+
Headers = $Headers
122+
APIName = $APIName
123+
}
124+
# language "default" (or missing) → Lcid 0 so New-CIPPSharepointSite uses tenant default.
125+
# A specific language → pass that LCID. Always pass Lcid so we don't fall back to the
126+
# helper's legacy English default used by AddSite.
127+
$RawLanguage = [string](($SiteTemplate.language.value ?? $SiteTemplate.language))
128+
$ParsedLcid = 0
129+
if ($RawLanguage -and $RawLanguage -ne 'default') {
130+
if (-not [int]::TryParse($RawLanguage, [ref]$ParsedLcid) -or $ParsedLcid -le 0) {
131+
throw "Site language '$RawLanguage' on '$($SiteTemplate.displayName)' is not a valid LCID."
132+
}
133+
}
134+
$SiteParams.Lcid = $ParsedLcid
135+
$null = New-CIPPSharepointSite @SiteParams
114136
$SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
115137
$SitePath = $SiteTemplate.displayName -replace ' ' -replace '[^A-Za-z0-9-]'
116138
$SiteUrl = "https://$($SharePointInfo.TenantName).sharepoint.com/sites/$SitePath"
@@ -121,25 +143,22 @@ function Invoke-CIPPSharePointTemplateDeploy {
121143
# here must mark this site step failed — do not report succeeded after swallowing errors.
122144
$StepFailures = [System.Collections.Generic.List[string]]::new()
123145

146+
# Root-level permissions, grouped per permission level.
124147
# Set-CIPPSharePointObjectPermission only throws when nothing was granted. Partial
125148
# outcomes (some Failed / Not found) still return a message — treat those as failures.
126-
$TestPermissionOutcome = {
127-
param([string]$Outcome, [string]$Context)
128-
if ($Outcome -match 'Failed for|Not found by display name') {
129-
$StepFailures.Add("${Context}: $Outcome")
130-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "SharePoint template site '$($SiteTemplate.displayName)': ${Context}: $Outcome" -sev Error
131-
}
132-
}
133-
134-
# Root-level permissions, grouped per permission level.
135149
Update-DeployStep -Index $SiteIndex -Status 'running' -Message "Step 3 of ${TotalSteps}: Applying site permissions"
136150
$RootPermGroups = @($SiteTemplate.permissions) | Group-Object -Property permissionLevel
137151
foreach ($PermGroup in $RootPermGroups) {
138-
$GroupNames = @($PermGroup.Group | ForEach-Object { & $GetPrincipalName $_.principal }) | Where-Object { $_ }
152+
# Frontend may store principals as plain strings or autocomplete {label,value} objects.
153+
$GroupNames = @($PermGroup.Group | ForEach-Object { $_.principal.value ?? $_.principal }) | Where-Object { $_ }
139154
try {
140155
$PermResult = Set-CIPPSharePointObjectPermission -SiteUrl $SiteUrl -PermissionLevel $PermGroup.Name -GroupNames $GroupNames -CreateMissingGroups:$CreateMissingGroups -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
141156
$Results.Add("[$TenantFilter] $($SiteTemplate.displayName): $PermResult")
142-
& $TestPermissionOutcome $PermResult "Root permissions ($($PermGroup.Name))"
157+
if ($PermResult -match 'Failed for|Not found by display name') {
158+
$FailMsg = "Root permissions ($($PermGroup.Name)): $PermResult"
159+
$StepFailures.Add($FailMsg)
160+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "SharePoint template site '$($SiteTemplate.displayName)': $FailMsg" -sev Error
161+
}
143162
} catch {
144163
$FailMsg = "Root permissions ($($PermGroup.Name)) failed: $($_.Exception.Message)"
145164
$StepFailures.Add($FailMsg)
@@ -159,11 +178,15 @@ function Invoke-CIPPSharePointTemplateDeploy {
159178

160179
$LibPermGroups = @($Library.permissions) | Group-Object -Property permissionLevel
161180
foreach ($PermGroup in $LibPermGroups) {
162-
$GroupNames = @($PermGroup.Group | ForEach-Object { & $GetPrincipalName $_.principal }) | Where-Object { $_ }
181+
$GroupNames = @($PermGroup.Group | ForEach-Object { $_.principal.value ?? $_.principal }) | Where-Object { $_ }
163182
try {
164183
$PermResult = Set-CIPPSharePointObjectPermission -SiteUrl $SiteUrl -ListId $NewLibrary.ListId -PermissionLevel $PermGroup.Name -GroupNames $GroupNames -CreateMissingGroups:$CreateMissingGroups -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
165184
$Results.Add("[$TenantFilter] $($SiteTemplate.displayName)/$($Library.name): $PermResult")
166-
& $TestPermissionOutcome $PermResult "Library '$($Library.name)' permissions ($($PermGroup.Name))"
185+
if ($PermResult -match 'Failed for|Not found by display name') {
186+
$FailMsg = "Library '$($Library.name)' permissions ($($PermGroup.Name)): $PermResult"
187+
$StepFailures.Add($FailMsg)
188+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "SharePoint template site '$($SiteTemplate.displayName)': $FailMsg" -sev Error
189+
}
167190
} catch {
168191
$FailMsg = "Library '$($Library.name)' permissions ($($PermGroup.Name)) failed: $($_.Exception.Message)"
169192
$StepFailures.Add($FailMsg)

Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ function New-CIPPSharepointSite {
2727
.PARAMETER SensitivityLabel
2828
The Purview sensitivity label to apply to the site
2929
30+
.PARAMETER Lcid
31+
SharePoint UI language LCID. Omit to keep the legacy English (1033) default used by
32+
Add Site. Pass 0 to use the tenant default (SharePoint Online root site language).
33+
Pass a positive LCID to force that language — must be a SharePoint Online site-creation
34+
language (same allowlist as the template builder). If 0 is passed and the root language
35+
cannot be read, site creation fails instead of falling back to English.
36+
3037
.PARAMETER TenantFilter
3138
The tenant associated with the site
3239
@@ -59,17 +66,68 @@ function New-CIPPSharepointSite {
5966

6067
[string]$Classification,
6168

69+
[Parameter(Mandatory = $false)]
70+
[int]$Lcid,
71+
6272
[Parameter(Mandatory = $true)]
6373
[string]$TenantFilter,
6474

6575
$APIName = 'Create SharePoint Site',
6676
$Headers
6777
)
6878

79+
# SharePoint Online site-creation UI languages (not full Windows LCIDs — e.g. en-GB 2057 is invalid).
80+
# Keep in sync with SITE_LANGUAGE_OPTIONS in CippSharePointTemplateBuilder.jsx.
81+
$AllowedSiteLcids = @(
82+
1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1035, 1036, 1037, 1038, 1040, 1041,
83+
1042, 1043, 1044, 1045, 1046, 1048, 1049, 1050, 1051, 1053, 1054, 1055, 1057, 1058, 1060,
84+
1061, 1062, 1063, 1066, 1069, 1081, 1086, 1087, 1106, 1110, 2052, 2070, 2074, 3082
85+
)
86+
6987
$SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
7088
$SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]'
7189
$SiteUrl = "https://$($SharePointInfo.TenantName).sharepoint.com/sites/$SitePath"
7290

91+
# Resolve site language:
92+
# - Explicit positive LCID → use it (must be in $AllowedSiteLcids)
93+
# - Explicit 0 (or negative) → tenant default. In SharePoint Online that is the root
94+
# site language (https://{tenant}.sharepoint.com); there is no separate admin-center
95+
# "default language" API (Graph sharepointSettings has timezone, not language).
96+
# - Parameter omitted → English (1033), preserving AddSite / bulk-create behaviour
97+
#
98+
# When tenant default is requested but the root language cannot be read, fail instead of
99+
# silently creating an English site on a non-English tenant.
100+
if ($PSBoundParameters.ContainsKey('Lcid')) {
101+
if ($Lcid -gt 0) {
102+
if ($Lcid -notin $AllowedSiteLcids) {
103+
$Result = "LCID $Lcid is not a supported SharePoint Online site language. Choose a language from the template builder list (or tenant default)."
104+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error
105+
throw $Result
106+
}
107+
$ResolvedLcid = $Lcid
108+
} else {
109+
$ResolvedLcid = 0
110+
$RootLanguageError = $null
111+
try {
112+
$JsonAccept = @{ Accept = 'application/json;odata=nometadata' }
113+
$RootWeb = New-GraphGetRequest -uri "https://$($SharePointInfo.TenantName).sharepoint.com/_api/web?`$select=Language" -tenantid $TenantFilter -scope "$($SharePointInfo.SharePointUrl)/.default" -extraHeaders $JsonAccept -UseCertificate -AsApp $true
114+
if ($RootWeb.Language -gt 0) {
115+
$ResolvedLcid = [int]$RootWeb.Language
116+
}
117+
} catch {
118+
$RootLanguageError = $_.Exception.Message
119+
}
120+
if ($ResolvedLcid -le 0) {
121+
$Detail = if ($RootLanguageError) { $RootLanguageError } else { 'Root site Language was missing or zero.' }
122+
$Result = "Could not resolve tenant default SharePoint language for $TenantFilter (root site). $Detail Choose an explicit site language in the template, or ensure the tenant root site is readable."
123+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Warning
124+
throw $Result
125+
}
126+
}
127+
} else {
128+
$ResolvedLcid = 1033
129+
}
130+
73131
switch ($TemplateName) {
74132
'Communication' {
75133
$WebTemplate = 'SITEPAGEPUBLISHING#0'
@@ -110,7 +168,7 @@ function New-CIPPSharepointSite {
110168
$Request = @{
111169
Title = $SiteName
112170
Url = $SiteUrl
113-
Lcid = 1033
171+
Lcid = $ResolvedLcid
114172
ShareByEmailEnabled = $false
115173
Description = $SiteDescription
116174
WebTemplate = $WebTemplate

0 commit comments

Comments
 (0)