mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-14 15:33:20 +03:00
603 lines
22 KiB
PowerShell
603 lines
22 KiB
PowerShell
# role-validate v1.3 — Validate 1C role structure
|
||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
param(
|
||
[Parameter(Mandatory)]
|
||
[Alias('Path')]
|
||
[string]$RightsPath,
|
||
|
||
[string]$OutFile,
|
||
|
||
[switch]$Detailed,
|
||
|
||
[int]$MaxErrors = 30
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||
|
||
# --- 1. Known rights per object type ---
|
||
|
||
$script:knownRights = @{
|
||
"Configuration" = @(
|
||
"Administration","DataAdministration","UpdateDataBaseConfiguration",
|
||
"ConfigurationExtensionsAdministration","ActiveUsers","EventLog","ExclusiveMode",
|
||
"ThinClient","ThickClient","WebClient","MobileClient","ExternalConnection",
|
||
"Automation","Output","SaveUserData","TechnicalSpecialistMode",
|
||
"InteractiveOpenExtDataProcessors","InteractiveOpenExtReports",
|
||
"AnalyticsSystemClient","CollaborationSystemInfoBaseRegistration",
|
||
"MainWindowModeNormal","MainWindowModeWorkplace",
|
||
"MainWindowModeEmbeddedWorkplace","MainWindowModeFullscreenWorkplace","MainWindowModeKiosk"
|
||
)
|
||
"Catalog" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete","InteractiveDeleteMarked",
|
||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
||
)
|
||
"Document" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"Posting","UndoPosting",
|
||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete","InteractiveDeleteMarked",
|
||
"InteractivePosting","InteractivePostingRegular","InteractiveUndoPosting",
|
||
"InteractiveChangeOfPosted",
|
||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
||
)
|
||
"InformationRegister" = @(
|
||
"Read","Update","View","Edit","TotalsControl",
|
||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
||
"UpdateDataHistoryOfMissingData","ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
||
)
|
||
"AccumulationRegister" = @("Read","Update","View","Edit","TotalsControl")
|
||
"AccountingRegister" = @("Read","Update","View","Edit","TotalsControl")
|
||
"CalculationRegister" = @("Read","View")
|
||
"Constant" = @(
|
||
"Read","Update","View","Edit",
|
||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
||
)
|
||
"ChartOfAccounts" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete",
|
||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
||
"ReadDataHistory","ReadDataHistoryOfMissingData",
|
||
"UpdateDataHistory","UpdateDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment"
|
||
)
|
||
"ChartOfCharacteristicTypes" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete","InteractiveDeleteMarked",
|
||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData",
|
||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
||
"ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
||
)
|
||
"ChartOfCalculationTypes" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete",
|
||
"InteractiveDeletePredefinedData","InteractiveSetDeletionMarkPredefinedData",
|
||
"InteractiveClearDeletionMarkPredefinedData","InteractiveDeleteMarkedPredefinedData"
|
||
)
|
||
"ExchangePlan" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete","InteractiveDeleteMarked",
|
||
"ReadDataHistory","ViewDataHistory","UpdateDataHistory",
|
||
"ReadDataHistoryOfMissingData","UpdateDataHistoryOfMissingData",
|
||
"UpdateDataHistorySettings","UpdateDataHistoryVersionComment",
|
||
"EditDataHistoryVersionComment","SwitchToDataHistoryVersion"
|
||
)
|
||
"BusinessProcess" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"Start","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete","InteractiveActivate","InteractiveStart"
|
||
)
|
||
"Task" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"Execute","InteractiveInsert","InteractiveSetDeletionMark","InteractiveClearDeletionMark",
|
||
"InteractiveDelete","InteractiveActivate","InteractiveExecute"
|
||
)
|
||
"DataProcessor" = @("Use","View")
|
||
"Report" = @("Use","View")
|
||
"CommonForm" = @("View")
|
||
"CommonCommand" = @("View")
|
||
"Subsystem" = @("View")
|
||
"FilterCriterion" = @("View")
|
||
"DocumentJournal" = @("Read","View")
|
||
"Sequence" = @("Read","Update")
|
||
"WebService" = @("Use")
|
||
"HTTPService" = @("Use")
|
||
"IntegrationService" = @("Use")
|
||
"SessionParameter" = @("Get","Set")
|
||
"CommonAttribute" = @("View","Edit")
|
||
"ExternalDataSource" = @(
|
||
"Use","Administration","StandardAuthenticationChange",
|
||
"SessionStandardAuthenticationChange","SessionOSAuthenticationChange"
|
||
)
|
||
}
|
||
|
||
# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса
|
||
# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора:
|
||
# догадкам тут не место — закрытый список превращает промах в ложный отказ.
|
||
$script:nestedKindRights = @{
|
||
"Attribute" = @("View","Edit")
|
||
"StandardAttribute" = @("View","Edit")
|
||
"TabularSection" = @("View","Edit")
|
||
"StandardTabularSection" = @("View","Edit")
|
||
"Dimension" = @("View","Edit")
|
||
"Resource" = @("View","Edit")
|
||
"AccountingFlag" = @("View","Edit")
|
||
"ExtDimensionAccountingFlag" = @("View","Edit")
|
||
"AddressingAttribute" = @("View","Edit")
|
||
"Field" = @("View","Edit")
|
||
"Command" = @("View")
|
||
"Subsystem" = @("View")
|
||
"Operation" = @("Use")
|
||
"Method" = @("Use")
|
||
"IntegrationServiceChannel" = @("Use")
|
||
"Recalculation" = @("Read","Update")
|
||
"Cube" = @("Read","View")
|
||
"DimensionTable" = @("Read","View")
|
||
"Function" = @("Use","View")
|
||
"Table" = @(
|
||
"Read","Insert","Update","Delete","View","Edit","InputByString",
|
||
"InteractiveInsert","InteractiveDelete"
|
||
)
|
||
}
|
||
|
||
# Виды, существующие только у одного типа-родителя: без этой привязки
|
||
# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект.
|
||
$script:kindOwners = @{
|
||
"Table" = "ExternalDataSource"
|
||
"Cube" = "ExternalDataSource"
|
||
"Function" = "ExternalDataSource"
|
||
"Field" = "ExternalDataSource"
|
||
"DimensionTable" = "ExternalDataSource"
|
||
"Recalculation" = "CalculationRegister"
|
||
"Operation" = "WebService"
|
||
"Method" = "HTTPService"
|
||
"IntegrationServiceChannel" = "IntegrationService"
|
||
}
|
||
|
||
# Один и тот же вид под разными родителями имеет разный набор: измерение регистра —
|
||
# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя,
|
||
# объединение молча разрешило бы Edit там, где платформа его не даёт.
|
||
$script:nestedKindRightsByType = @{
|
||
"ExternalDataSource" = @{
|
||
"Dimension" = @("View")
|
||
"Resource" = @("View")
|
||
}
|
||
}
|
||
|
||
# Типы без прав в ролях (в дереве редактора ролей их нет). Таблица НЕ управляет поведением —
|
||
# отказ даёт отсутствие типа в $knownRights; здесь только причина для сообщения.
|
||
$script:noRightsTypes = @(
|
||
"Enum","CommonModule","DefinedType","CommonPicture","CommonTemplate","Language",
|
||
"FunctionalOption","FunctionalOptionsParameter","EventSubscription","ScheduledJob",
|
||
"StyleItem","Style","SettingsStorage","XDTOPackage","WSReference","DocumentNumerator"
|
||
)
|
||
|
||
# --- 2. Output helpers ---
|
||
|
||
$script:errors = 0
|
||
$script:warnings = 0
|
||
$script:okCount = 0
|
||
$script:stopped = $false
|
||
$script:output = New-Object System.Text.StringBuilder 8192
|
||
|
||
function Out-Line {
|
||
param([string]$msg)
|
||
$script:output.AppendLine($msg) | Out-Null
|
||
}
|
||
|
||
function Report-OK {
|
||
param([string]$msg)
|
||
$script:okCount++
|
||
if ($Detailed) { Out-Line "[OK] $msg" }
|
||
}
|
||
|
||
function Report-Error {
|
||
param([string]$msg)
|
||
$script:errors++
|
||
Out-Line "[ERROR] $msg"
|
||
if ($script:errors -ge $MaxErrors) {
|
||
$script:stopped = $true
|
||
}
|
||
}
|
||
|
||
function Report-Warn {
|
||
param([string]$msg)
|
||
$script:warnings++
|
||
Out-Line "[WARN] $msg"
|
||
}
|
||
|
||
function Get-ObjectType {
|
||
param([string]$name)
|
||
$dotIdx = $name.IndexOf(".")
|
||
if ($dotIdx -lt 0) { return $name }
|
||
return $name.Substring(0, $dotIdx)
|
||
}
|
||
|
||
function Is-NestedObject {
|
||
param([string]$name)
|
||
return ($name.Split(".").Count -ge 3)
|
||
}
|
||
|
||
# Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным
|
||
# (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца.
|
||
function Get-NestedKind {
|
||
param([string]$name)
|
||
$parts = $name.Split(".")
|
||
if ($parts.Count -lt 3) { return $null }
|
||
return $parts[$parts.Count - 2]
|
||
}
|
||
|
||
function Get-NestedRights {
|
||
param([string]$objectType, [string]$kind)
|
||
if ($script:nestedKindRightsByType.ContainsKey($objectType) -and
|
||
$script:nestedKindRightsByType[$objectType].ContainsKey($kind)) {
|
||
return @($script:nestedKindRightsByType[$objectType][$kind])
|
||
}
|
||
if ($script:nestedKindRights.ContainsKey($kind)) { return @($script:nestedKindRights[$kind]) }
|
||
return $null
|
||
}
|
||
|
||
# Запрещённый тип и незнакомый тип — разные диагнозы: первый документирован, второй
|
||
# скорее опечатка или пробел в таблице.
|
||
function Format-TypeError {
|
||
param([string]$objName, [string]$objectType)
|
||
if ($script:noRightsTypes -contains $objectType) {
|
||
return "${objName}: тип '$objectType' не имеет прав в роли — блок нужно удалить"
|
||
}
|
||
$similar = Find-Similar -needle $objectType -haystack @($script:knownRights.Keys)
|
||
$sug = if ($similar.Count -gt 0) { " Возможно: $($similar -join ', ')?" } else { "" }
|
||
return "${objName}: неизвестный тип объекта '$objectType'.$sug"
|
||
}
|
||
|
||
function Find-Similar {
|
||
param([string]$needle, [string[]]$haystack)
|
||
$result = @($haystack | Where-Object {
|
||
$_ -like "*$needle*" -or $needle -like "*$_*"
|
||
})
|
||
if ($result.Count -gt 3) { $result = $result[0..2] }
|
||
return $result
|
||
}
|
||
|
||
# --- Resolve path ---
|
||
if (-not [System.IO.Path]::IsPathRooted($RightsPath)) {
|
||
$RightsPath = Join-Path (Get-Location).Path $RightsPath
|
||
}
|
||
# A: Directory → Ext/Rights.xml
|
||
if (Test-Path $RightsPath -PathType Container) {
|
||
$RightsPath = Join-Path (Join-Path $RightsPath "Ext") "Rights.xml"
|
||
}
|
||
# B1: Missing Ext/ (e.g. Roles/МояРоль/Rights.xml → Roles/МояРоль/Ext/Rights.xml)
|
||
if (-not (Test-Path $RightsPath)) {
|
||
$fn = [System.IO.Path]::GetFileName($RightsPath)
|
||
if ($fn -eq "Rights.xml") {
|
||
$c = Join-Path (Join-Path (Split-Path $RightsPath) "Ext") $fn
|
||
if (Test-Path $c) { $RightsPath = $c }
|
||
}
|
||
}
|
||
|
||
# --- 3. Validate Rights.xml ---
|
||
|
||
if (-not (Test-Path $RightsPath)) {
|
||
Report-Error "File not found: $RightsPath"
|
||
$result = $script:output.ToString()
|
||
Write-Host $result
|
||
if ($OutFile) {
|
||
$outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile }
|
||
$outDir = [System.IO.Path]::GetDirectoryName($outPath)
|
||
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }
|
||
$utf8Bom = New-Object System.Text.UTF8Encoding $true
|
||
[System.IO.File]::WriteAllText($outPath, $result, $utf8Bom)
|
||
Write-Host "Written to: $outPath"
|
||
}
|
||
exit 1
|
||
}
|
||
|
||
# Auto-detect metadata: Roles/Name/Ext/Rights.xml → Roles/Name.xml
|
||
$resolvedRights = (Resolve-Path $RightsPath).Path
|
||
$extDir = Split-Path $resolvedRights -Parent
|
||
$roleDir = Split-Path $extDir -Parent
|
||
$rolesDir = Split-Path $roleDir -Parent
|
||
$roleDirName = Split-Path $roleDir -Leaf
|
||
$MetadataPath = Join-Path $rolesDir "$roleDirName.xml"
|
||
|
||
# 3a. Parse XML
|
||
try {
|
||
[xml]$xml = Get-Content -Path $RightsPath -Encoding UTF8
|
||
Report-OK "XML well-formed"
|
||
} catch {
|
||
Report-Error "XML parse error: $($_.Exception.Message)"
|
||
$result = $script:output.ToString()
|
||
Write-Host $result
|
||
if ($OutFile) {
|
||
$outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile }
|
||
$outDir = [System.IO.Path]::GetDirectoryName($outPath)
|
||
if (-not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }
|
||
$utf8Bom = New-Object System.Text.UTF8Encoding $true
|
||
[System.IO.File]::WriteAllText($outPath, $result, $utf8Bom)
|
||
Write-Host "Written to: $outPath"
|
||
}
|
||
exit 1
|
||
}
|
||
|
||
$root = $xml.DocumentElement
|
||
$rightsNs = "http://v8.1c.ru/8.2/roles"
|
||
|
||
# 3b. Check root element
|
||
if ($root.LocalName -ne "Rights") {
|
||
Report-Error "Root element is '$($root.LocalName)', expected 'Rights'"
|
||
} elseif ($root.NamespaceURI -ne $rightsNs) {
|
||
Report-Warn "Namespace is '$($root.NamespaceURI)', expected '$rightsNs'"
|
||
} else {
|
||
Report-OK "Root element: <Rights> with correct namespace"
|
||
}
|
||
|
||
# 3c. Global flags
|
||
$flagNames = @("setForNewObjects","setForAttributesByDefault","independentRightsOfChildObjects")
|
||
$flagsFound = 0
|
||
foreach ($fn in $flagNames) {
|
||
$node = $root.GetElementsByTagName($fn, $rightsNs)
|
||
if ($node.Count -gt 0) {
|
||
$val = $node[0].InnerText
|
||
if ($val -ne "true" -and $val -ne "false") {
|
||
Report-Warn "$fn = '$val' (expected 'true' or 'false')"
|
||
}
|
||
$flagsFound++
|
||
} else {
|
||
Report-Warn "Missing global flag: $fn"
|
||
}
|
||
}
|
||
if ($flagsFound -eq 3) {
|
||
Report-OK "3 global flags present"
|
||
}
|
||
|
||
# 3d. Objects
|
||
$objects = $root.GetElementsByTagName("object", $rightsNs)
|
||
$objCount = $objects.Count
|
||
$rightCount = 0
|
||
$rlsCount = 0
|
||
|
||
foreach ($obj in $objects) {
|
||
$objName = ""
|
||
foreach ($child in $obj.ChildNodes) {
|
||
if ($child.LocalName -eq "name") {
|
||
$objName = $child.InnerText
|
||
break
|
||
}
|
||
}
|
||
|
||
if (-not $objName) {
|
||
Report-Error "Object without <name>"
|
||
continue
|
||
}
|
||
|
||
$objectType = Get-ObjectType $objName
|
||
$isNested = Is-NestedObject $objName
|
||
|
||
# Тип проверяется ВСЕГДА, включая вложенные пути: раньше `Enum.Х.Attribute.Y` не проверялся
|
||
# вообще, потому что ветка «unknown» стояла под `-not $isNested`.
|
||
$nestedKind = if ($isNested) { Get-NestedKind $objName } else { $null }
|
||
$typeKnown = $script:knownRights.ContainsKey($objectType)
|
||
if (-not $typeKnown) {
|
||
Report-Error (Format-TypeError $objName $objectType)
|
||
} elseif ($isNested -and $script:kindOwners.ContainsKey($nestedKind) -and $objectType -ne $script:kindOwners[$nestedKind]) {
|
||
Report-Error "${objName}: вид '$nestedKind' бывает только у $($script:kindOwners[$nestedKind])"
|
||
} elseif ($isNested -and $null -eq (Get-NestedRights $objectType $nestedKind)) {
|
||
Report-Error "${objName}: неизвестный вид вложенности '$nestedKind'"
|
||
}
|
||
|
||
# Check rights
|
||
foreach ($child in $obj.ChildNodes) {
|
||
if ($child.LocalName -ne "right") { continue }
|
||
|
||
$rName = ""
|
||
$rValue = ""
|
||
$hasRLS = $false
|
||
|
||
foreach ($rc in $child.ChildNodes) {
|
||
if ($rc.LocalName -eq "name") { $rName = $rc.InnerText }
|
||
if ($rc.LocalName -eq "value") { $rValue = $rc.InnerText }
|
||
if ($rc.LocalName -eq "restrictionByCondition") {
|
||
$hasRLS = $true
|
||
$rlsCount++
|
||
# Check condition not empty
|
||
$condNode = $null
|
||
foreach ($rcc in $rc.ChildNodes) {
|
||
if ($rcc.LocalName -eq "condition") { $condNode = $rcc }
|
||
}
|
||
if (-not $condNode -or -not $condNode.InnerText) {
|
||
Report-Warn "${objName}: RLS condition for '$rName' is empty"
|
||
}
|
||
}
|
||
}
|
||
|
||
if (-not $rName) {
|
||
Report-Error "${objName}: <right> without <name>"
|
||
continue
|
||
}
|
||
|
||
if ($rValue -ne "true" -and $rValue -ne "false") {
|
||
Report-Error "${objName}: right '$rName' has invalid value '$rValue'"
|
||
continue
|
||
}
|
||
|
||
$rightCount++
|
||
|
||
# Validate right name. Тип уже отвергнут выше — второй раз про него не пишем.
|
||
if (-not $typeKnown) {
|
||
# уже сообщено
|
||
} elseif ($isNested) {
|
||
$validNested = Get-NestedRights $objectType $nestedKind
|
||
if ($null -ne $validNested -and $rName -notin $validNested) {
|
||
Report-Error "${objName}: право '$rName' недопустимо для вида '$nestedKind' (допустимо: $($validNested -join ', '))"
|
||
}
|
||
} else {
|
||
$validRights = $script:knownRights[$objectType]
|
||
if ($rName -notin $validRights) {
|
||
$similar = Find-Similar -needle $rName -haystack $validRights
|
||
$sugStr = if ($similar.Count -gt 0) { " Возможно: $($similar -join ', ')?" } else { "" }
|
||
Report-Error "${objName}: право '$rName' не существует у типа '$objectType'.$sugStr"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Report-OK "$objCount objects, $rightCount rights"
|
||
if ($rlsCount -gt 0) {
|
||
Report-OK "$rlsCount RLS restrictions"
|
||
}
|
||
|
||
# 3e. Templates
|
||
$templates = $root.GetElementsByTagName("restrictionTemplate", $rightsNs)
|
||
if ($templates.Count -gt 0) {
|
||
$tplNames = @()
|
||
foreach ($tpl in $templates) {
|
||
$tName = ""
|
||
$tCond = ""
|
||
foreach ($child in $tpl.ChildNodes) {
|
||
if ($child.LocalName -eq "name") { $tName = $child.InnerText }
|
||
if ($child.LocalName -eq "condition") { $tCond = $child.InnerText }
|
||
}
|
||
if (-not $tName) {
|
||
Report-Warn "Restriction template without <name>"
|
||
} else {
|
||
$parenIdx = $tName.IndexOf("(")
|
||
$shortName = if ($parenIdx -gt 0) { $tName.Substring(0, $parenIdx) } else { $tName }
|
||
$tplNames += $shortName
|
||
}
|
||
if (-not $tCond) {
|
||
Report-Warn "Template '$tName': empty <condition>"
|
||
}
|
||
}
|
||
Report-OK "$($templates.Count) templates: $($tplNames -join ', ')"
|
||
}
|
||
|
||
# --- 4. Validate metadata ---
|
||
|
||
if (Test-Path $MetadataPath) {
|
||
Out-Line ""
|
||
try {
|
||
[xml]$metaXml = Get-Content -Path $MetadataPath -Encoding UTF8
|
||
$roleNode = $metaXml.DocumentElement.SelectSingleNode("//*[local-name()='Role']")
|
||
if (-not $roleNode) {
|
||
Report-Error "Metadata: <Role> element not found"
|
||
} else {
|
||
$uuid = $roleNode.GetAttribute("uuid")
|
||
if ($uuid -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
|
||
Report-OK "Metadata: UUID valid ($uuid)"
|
||
} else {
|
||
Report-Error "Metadata: invalid UUID format '$uuid'"
|
||
}
|
||
|
||
$nameNode = $roleNode.SelectSingleNode(".//*[local-name()='Name']")
|
||
if ($nameNode -and $nameNode.InnerText) {
|
||
Report-OK "Metadata: Name = $($nameNode.InnerText)"
|
||
} else {
|
||
Report-Error "Metadata: <Name> is empty or missing"
|
||
}
|
||
|
||
$synNode = $roleNode.SelectSingleNode(".//*[local-name()='Synonym']")
|
||
if ($synNode -and $synNode.InnerXml) {
|
||
Report-OK "Metadata: Synonym present"
|
||
} else {
|
||
Report-Warn "Metadata: <Synonym> is empty"
|
||
}
|
||
}
|
||
} catch {
|
||
Report-Error "Metadata XML parse error: $($_.Exception.Message)"
|
||
}
|
||
}
|
||
|
||
# --- 5. Check registration in Configuration.xml ---
|
||
|
||
$configDir = Split-Path $rolesDir -Parent
|
||
$configXmlPath = Join-Path $configDir "Configuration.xml"
|
||
$inferredRoleName = $roleDirName
|
||
|
||
# Use metadata name if available
|
||
if (Test-Path $MetadataPath) {
|
||
try {
|
||
[xml]$metaXml2 = Get-Content -Path $MetadataPath -Encoding UTF8
|
||
$nameNode2 = $metaXml2.DocumentElement.SelectSingleNode("//*[local-name()='Role']//*[local-name()='Name']")
|
||
if ($nameNode2 -and $nameNode2.InnerText) {
|
||
$inferredRoleName = $nameNode2.InnerText
|
||
}
|
||
} catch { }
|
||
}
|
||
|
||
if (Test-Path $configXmlPath) {
|
||
Out-Line ""
|
||
try {
|
||
[xml]$cfgXml = Get-Content -Path $configXmlPath -Encoding UTF8
|
||
$cfgNs = New-Object System.Xml.XmlNamespaceManager($cfgXml.NameTable)
|
||
$cfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||
$childObj = $cfgXml.SelectSingleNode("//md:Configuration/md:ChildObjects", $cfgNs)
|
||
if ($childObj) {
|
||
$roleNodes = $childObj.SelectNodes("md:Role", $cfgNs)
|
||
$found = $false
|
||
foreach ($rn in $roleNodes) {
|
||
if ($rn.InnerText -eq $inferredRoleName) {
|
||
$found = $true
|
||
break
|
||
}
|
||
}
|
||
if ($found) {
|
||
Report-OK "Configuration.xml: <Role>$inferredRoleName</Role> registered"
|
||
} else {
|
||
Report-Warn "Configuration.xml: <Role>$inferredRoleName</Role> NOT found in ChildObjects"
|
||
}
|
||
}
|
||
} catch {
|
||
Report-Warn "Configuration.xml: parse error — $($_.Exception.Message)"
|
||
}
|
||
}
|
||
|
||
# --- 6. Summary ---
|
||
|
||
# Insert header
|
||
$script:output.Insert(0, "=== Validation: Role.$inferredRoleName ===$([Environment]::NewLine)") | Out-Null
|
||
|
||
$checks = $script:okCount + $script:errors + $script:warnings
|
||
if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) {
|
||
$result = "=== Validation OK: Role.$inferredRoleName ($checks checks) ==="
|
||
} else {
|
||
Out-Line ""
|
||
Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ==="
|
||
$result = $script:output.ToString()
|
||
}
|
||
Write-Host $result
|
||
|
||
if ($OutFile) {
|
||
$outPath = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path (Get-Location) $OutFile }
|
||
$outDir = [System.IO.Path]::GetDirectoryName($outPath)
|
||
if (-not (Test-Path $outDir)) {
|
||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
||
}
|
||
$utf8Bom = New-Object System.Text.UTF8Encoding $true
|
||
[System.IO.File]::WriteAllText($outPath, $result, $utf8Bom)
|
||
Write-Host "Written to: $outPath"
|
||
}
|
||
|
||
if ($script:errors -gt 0) { exit 1 } else { exit 0 }
|