mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 17:04:57 +03:00
Auto-build: copilot (python) from 7fa279c
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
# role-validate v1.1 — 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")
|
||||
}
|
||||
|
||||
$script:nestedRights = @("View","Edit")
|
||||
$script:channelRights = @("Use")
|
||||
$script:commandRights = @("View")
|
||||
|
||||
# --- 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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# Check object type is known
|
||||
if (-not $isNested -and -not $script:knownRights.ContainsKey($objectType)) {
|
||||
Report-Warn "${objName}: unknown object type '$objectType'"
|
||||
}
|
||||
|
||||
# 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 ($isNested) {
|
||||
if ($objName -match '\.Command\.') {
|
||||
if ($rName -notin $script:commandRights) {
|
||||
Report-Warn "${objName}: '$rName' not valid for commands (only: View)"
|
||||
}
|
||||
} elseif ($objName -match '\.IntegrationServiceChannel\.') {
|
||||
if ($rName -notin $script:channelRights) {
|
||||
Report-Warn "${objName}: '$rName' not valid for channels (only: Use)"
|
||||
}
|
||||
} else {
|
||||
if ($rName -notin $script:nestedRights) {
|
||||
Report-Warn "${objName}: '$rName' not valid for nested objects (only: View, Edit)"
|
||||
}
|
||||
}
|
||||
} elseif ($script:knownRights.ContainsKey($objectType)) {
|
||||
$validRights = $script:knownRights[$objectType]
|
||||
if ($rName -notin $validRights) {
|
||||
$similar = Find-Similar -needle $rName -haystack $validRights
|
||||
$sugStr = if ($similar.Count -gt 0) { " Did you mean: $($similar -join ', ')?" } else { "" }
|
||||
Report-Warn "${objName}: unknown right '$rName'.$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 }
|
||||
@@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env python3
|
||||
# role-validate v1.1 — Validate 1C role Rights.xml structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates role Rights.xml: root element, global flags, objects, rights, RLS, templates."""
|
||||
import sys, os, argparse, re
|
||||
from lxml import etree
|
||||
|
||||
GUID_PATTERN = re.compile(
|
||||
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
RIGHTS_NS = 'http://v8.1c.ru/8.2/roles'
|
||||
|
||||
# --- Known rights per object type ---
|
||||
KNOWN_RIGHTS = {
|
||||
'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'],
|
||||
}
|
||||
|
||||
NESTED_RIGHTS = ['View', 'Edit']
|
||||
CHANNEL_RIGHTS = ['Use']
|
||||
COMMAND_RIGHTS = ['View']
|
||||
|
||||
|
||||
def get_object_type(name):
|
||||
dot_idx = name.find('.')
|
||||
if dot_idx < 0:
|
||||
return name
|
||||
return name[:dot_idx]
|
||||
|
||||
|
||||
def is_nested_object(name):
|
||||
return name.count('.') >= 2
|
||||
|
||||
|
||||
def find_similar(needle, haystack):
|
||||
result = []
|
||||
needle_lower = needle.lower()
|
||||
for h in haystack:
|
||||
h_lower = h.lower()
|
||||
if needle_lower in h_lower or h_lower in needle_lower:
|
||||
result.append(h)
|
||||
if len(result) >= 3:
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def get_child_text(parent, local_name, ns):
|
||||
"""Get text of first child element with given local name in namespace."""
|
||||
for child in parent:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if etree.QName(child.tag).localname == local_name and etree.QName(child.tag).namespace == ns:
|
||||
return child.text or ''
|
||||
return None
|
||||
|
||||
|
||||
def get_child_el(parent, local_name, ns):
|
||||
"""Get first child element with given local name in namespace."""
|
||||
for child in parent:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if etree.QName(child.tag).localname == local_name and etree.QName(child.tag).namespace == ns:
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Validate 1C role Rights.xml structure', allow_abbrev=False
|
||||
)
|
||||
parser.add_argument('-RightsPath', '-Path', dest='RightsPath', required=True)
|
||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||
parser.add_argument('-Detailed', dest='Detailed', action='store_true')
|
||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||
args = parser.parse_args()
|
||||
|
||||
rights_path = args.RightsPath
|
||||
out_file = args.OutFile
|
||||
|
||||
if not os.path.isabs(rights_path):
|
||||
rights_path = os.path.join(os.getcwd(), rights_path)
|
||||
|
||||
# A: Directory → Ext/Rights.xml
|
||||
if os.path.isdir(rights_path):
|
||||
rights_path = os.path.join(rights_path, 'Ext', 'Rights.xml')
|
||||
# B1: Missing Ext/
|
||||
if not os.path.exists(rights_path):
|
||||
fn = os.path.basename(rights_path)
|
||||
if fn == 'Rights.xml':
|
||||
c = os.path.join(os.path.dirname(rights_path), 'Ext', fn)
|
||||
if os.path.exists(c):
|
||||
rights_path = c
|
||||
|
||||
resolved_path = os.path.abspath(rights_path)
|
||||
|
||||
# Auto-detect metadata: Roles/Name/Ext/Rights.xml → Roles/Name.xml
|
||||
ext_dir = os.path.dirname(resolved_path)
|
||||
role_dir = os.path.dirname(ext_dir)
|
||||
roles_dir = os.path.dirname(role_dir)
|
||||
role_dir_name = os.path.basename(role_dir)
|
||||
metadata_path = os.path.join(roles_dir, f'{role_dir_name}.xml')
|
||||
|
||||
# --- Output helpers ---
|
||||
lines = []
|
||||
errors = 0
|
||||
warnings = 0
|
||||
ok_count = 0
|
||||
stopped = False
|
||||
|
||||
def report_ok(msg):
|
||||
nonlocal ok_count
|
||||
ok_count += 1
|
||||
if args.Detailed:
|
||||
lines.append(f'[OK] {msg}')
|
||||
|
||||
def report_warn(msg):
|
||||
nonlocal warnings
|
||||
warnings += 1
|
||||
lines.append(f'[WARN] {msg}')
|
||||
|
||||
def report_error(msg):
|
||||
nonlocal errors, stopped
|
||||
errors += 1
|
||||
lines.append(f'[ERROR] {msg}')
|
||||
if errors >= args.MaxErrors:
|
||||
stopped = True
|
||||
|
||||
# --- 3. Validate Rights.xml ---
|
||||
|
||||
def write_output(text):
|
||||
if out_file:
|
||||
out_path = out_file if os.path.isabs(out_file) else os.path.join(os.getcwd(), out_file)
|
||||
out_dir = os.path.dirname(out_path)
|
||||
if out_dir and not os.path.exists(out_dir):
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
with open(out_path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
f.write(text)
|
||||
print(f'Written to: {out_path}')
|
||||
else:
|
||||
print(text)
|
||||
|
||||
if not os.path.exists(rights_path):
|
||||
report_error(f'File not found: {rights_path}')
|
||||
result = '\n'.join(lines)
|
||||
write_output(result)
|
||||
sys.exit(1)
|
||||
|
||||
# 3a. Parse XML
|
||||
xml_doc = None
|
||||
try:
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
xml_doc = etree.parse(rights_path, xml_parser)
|
||||
report_ok('XML well-formed')
|
||||
except etree.XMLSyntaxError as e:
|
||||
report_error(f'XML parse error: {e}')
|
||||
result = '\n'.join(lines)
|
||||
write_output(result)
|
||||
sys.exit(1)
|
||||
|
||||
root = xml_doc.getroot()
|
||||
root_local = etree.QName(root.tag).localname
|
||||
root_ns = etree.QName(root.tag).namespace or ''
|
||||
|
||||
# 3b. Check root element
|
||||
if root_local != 'Rights':
|
||||
report_error(f"Root element is '{root_local}', expected 'Rights'")
|
||||
elif root_ns != RIGHTS_NS:
|
||||
report_warn(f"Namespace is '{root_ns}', expected '{RIGHTS_NS}'")
|
||||
else:
|
||||
report_ok('Root element: <Rights> with correct namespace')
|
||||
|
||||
# 3c. Global flags
|
||||
flag_names = ['setForNewObjects', 'setForAttributesByDefault', 'independentRightsOfChildObjects']
|
||||
flags_found = 0
|
||||
for fn in flag_names:
|
||||
nodes = root.findall(f'{{{RIGHTS_NS}}}{fn}')
|
||||
if len(nodes) > 0:
|
||||
val = nodes[0].text or ''
|
||||
if val not in ('true', 'false'):
|
||||
report_warn(f"{fn} = '{val}' (expected 'true' or 'false')")
|
||||
flags_found += 1
|
||||
else:
|
||||
report_warn(f'Missing global flag: {fn}')
|
||||
if flags_found == 3:
|
||||
report_ok('3 global flags present')
|
||||
|
||||
# 3d. Objects
|
||||
objects = root.findall(f'{{{RIGHTS_NS}}}object')
|
||||
obj_count = len(objects)
|
||||
right_count = 0
|
||||
rls_count = 0
|
||||
|
||||
for obj in objects:
|
||||
obj_name = ''
|
||||
for child in obj:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if etree.QName(child.tag).localname == 'name' and etree.QName(child.tag).namespace == RIGHTS_NS:
|
||||
obj_name = child.text or ''
|
||||
break
|
||||
|
||||
if not obj_name:
|
||||
report_error('Object without <name>')
|
||||
continue
|
||||
|
||||
object_type = get_object_type(obj_name)
|
||||
is_nested = is_nested_object(obj_name)
|
||||
|
||||
# Check object type is known
|
||||
if not is_nested and object_type not in KNOWN_RIGHTS:
|
||||
report_warn(f"{obj_name}: unknown object type '{object_type}'")
|
||||
|
||||
# Check rights
|
||||
for child in obj:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
if etree.QName(child.tag).localname != 'right' or etree.QName(child.tag).namespace != RIGHTS_NS:
|
||||
continue
|
||||
|
||||
r_name = ''
|
||||
r_value = ''
|
||||
has_rls = False
|
||||
|
||||
for rc in child:
|
||||
if not isinstance(rc.tag, str):
|
||||
continue
|
||||
rc_local = etree.QName(rc.tag).localname
|
||||
rc_ns = etree.QName(rc.tag).namespace
|
||||
if rc_ns != RIGHTS_NS:
|
||||
continue
|
||||
|
||||
if rc_local == 'name':
|
||||
r_name = rc.text or ''
|
||||
elif rc_local == 'value':
|
||||
r_value = rc.text or ''
|
||||
elif rc_local == 'restrictionByCondition':
|
||||
has_rls = True
|
||||
rls_count += 1
|
||||
# Check condition not empty
|
||||
cond_node = get_child_el(rc, 'condition', RIGHTS_NS)
|
||||
if cond_node is None or not (cond_node.text or ''):
|
||||
report_warn(f"{obj_name}: RLS condition for '{r_name}' is empty")
|
||||
|
||||
if not r_name:
|
||||
report_error(f'{obj_name}: <right> without <name>')
|
||||
continue
|
||||
|
||||
if r_value not in ('true', 'false'):
|
||||
report_error(f"{obj_name}: right '{r_name}' has invalid value '{r_value}'")
|
||||
continue
|
||||
|
||||
right_count += 1
|
||||
|
||||
# Validate right name
|
||||
if is_nested:
|
||||
if '.Command.' in obj_name:
|
||||
if r_name not in COMMAND_RIGHTS:
|
||||
report_warn(f"{obj_name}: '{r_name}' not valid for commands (only: View)")
|
||||
elif '.IntegrationServiceChannel.' in obj_name:
|
||||
if r_name not in CHANNEL_RIGHTS:
|
||||
report_warn(f"{obj_name}: '{r_name}' not valid for channels (only: Use)")
|
||||
else:
|
||||
if r_name not in NESTED_RIGHTS:
|
||||
report_warn(f"{obj_name}: '{r_name}' not valid for nested objects (only: View, Edit)")
|
||||
elif object_type in KNOWN_RIGHTS:
|
||||
valid_rights = KNOWN_RIGHTS[object_type]
|
||||
if r_name not in valid_rights:
|
||||
similar = find_similar(r_name, valid_rights)
|
||||
sug_str = f' Did you mean: {", ".join(similar)}?' if similar else ''
|
||||
report_warn(f"{obj_name}: unknown right '{r_name}'.{sug_str}")
|
||||
|
||||
report_ok(f'{obj_count} objects, {right_count} rights')
|
||||
if rls_count > 0:
|
||||
report_ok(f'{rls_count} RLS restrictions')
|
||||
|
||||
# 3e. Templates
|
||||
templates = root.findall(f'{{{RIGHTS_NS}}}restrictionTemplate')
|
||||
if len(templates) > 0:
|
||||
tpl_names = []
|
||||
for tpl in templates:
|
||||
t_name = ''
|
||||
t_cond = ''
|
||||
for child in tpl:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
local = etree.QName(child.tag).localname
|
||||
ns = etree.QName(child.tag).namespace
|
||||
if ns != RIGHTS_NS:
|
||||
continue
|
||||
if local == 'name':
|
||||
t_name = child.text or ''
|
||||
elif local == 'condition':
|
||||
t_cond = child.text or ''
|
||||
if not t_name:
|
||||
report_warn('Restriction template without <name>')
|
||||
else:
|
||||
paren_idx = t_name.find('(')
|
||||
short_name = t_name[:paren_idx] if paren_idx > 0 else t_name
|
||||
tpl_names.append(short_name)
|
||||
if not t_cond:
|
||||
report_warn(f"Template '{t_name}': empty <condition>")
|
||||
report_ok(f'{len(templates)} templates: {", ".join(tpl_names)}')
|
||||
|
||||
# --- 4. Validate metadata (optional) ---
|
||||
inferred_role_name = ''
|
||||
if os.path.isfile(metadata_path):
|
||||
lines.append('')
|
||||
|
||||
try:
|
||||
meta_parser = etree.XMLParser(remove_blank_text=False)
|
||||
meta_xml = etree.parse(metadata_path, meta_parser)
|
||||
meta_root = meta_xml.getroot()
|
||||
# Find <Role> element anywhere
|
||||
role_node = None
|
||||
for el in meta_root.iter():
|
||||
if isinstance(el.tag, str) and etree.QName(el.tag).localname == 'Role':
|
||||
role_node = el
|
||||
break
|
||||
|
||||
if role_node is None:
|
||||
report_error('Metadata: <Role> element not found')
|
||||
else:
|
||||
uuid_val = role_node.get('uuid', '')
|
||||
if GUID_PATTERN.match(uuid_val):
|
||||
report_ok(f'Metadata: UUID valid ({uuid_val})')
|
||||
else:
|
||||
report_error(f"Metadata: invalid UUID format '{uuid_val}'")
|
||||
|
||||
# Find Name
|
||||
name_node = None
|
||||
for el in role_node.iter():
|
||||
if isinstance(el.tag, str) and etree.QName(el.tag).localname == 'Name':
|
||||
name_node = el
|
||||
break
|
||||
|
||||
if name_node is not None and name_node.text:
|
||||
report_ok(f'Metadata: Name = {name_node.text}')
|
||||
inferred_role_name = name_node.text
|
||||
else:
|
||||
report_error('Metadata: <Name> is empty or missing')
|
||||
|
||||
# Find Synonym
|
||||
syn_node = None
|
||||
for el in role_node.iter():
|
||||
if isinstance(el.tag, str) and etree.QName(el.tag).localname == 'Synonym':
|
||||
syn_node = el
|
||||
break
|
||||
|
||||
if syn_node is not None and len(syn_node) > 0:
|
||||
report_ok('Metadata: Synonym present')
|
||||
else:
|
||||
report_warn('Metadata: <Synonym> is empty')
|
||||
except etree.XMLSyntaxError as e:
|
||||
report_error(f'Metadata XML parse error: {e}')
|
||||
|
||||
# --- 5. Check registration in Configuration.xml ---
|
||||
config_dir = os.path.dirname(roles_dir) # config root
|
||||
config_xml_path = os.path.join(config_dir, 'Configuration.xml')
|
||||
|
||||
if not inferred_role_name:
|
||||
inferred_role_name = os.path.basename(role_dir)
|
||||
|
||||
if os.path.exists(config_xml_path):
|
||||
lines.append('')
|
||||
try:
|
||||
cfg_parser = etree.XMLParser(remove_blank_text=False)
|
||||
cfg_xml = etree.parse(config_xml_path, cfg_parser)
|
||||
cfg_ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||
child_obj = cfg_xml.getroot().find('.//md:Configuration/md:ChildObjects', cfg_ns)
|
||||
if child_obj is not None:
|
||||
role_nodes = child_obj.findall('md:Role', cfg_ns)
|
||||
found = False
|
||||
for rn in role_nodes:
|
||||
if (rn.text or '') == inferred_role_name:
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
report_ok(f'Configuration.xml: <Role>{inferred_role_name}</Role> registered')
|
||||
else:
|
||||
report_warn(f'Configuration.xml: <Role>{inferred_role_name}</Role> NOT found in ChildObjects')
|
||||
except etree.XMLSyntaxError as e:
|
||||
report_warn(f'Configuration.xml: parse error \u2014 {e}')
|
||||
|
||||
# --- 6. Summary ---
|
||||
|
||||
# Insert header at position 0
|
||||
lines.insert(0, f'=== Validation: Role.{inferred_role_name} ===')
|
||||
|
||||
checks = ok_count + errors + warnings
|
||||
if errors == 0 and warnings == 0 and not args.Detailed:
|
||||
result = f'=== Validation OK: Role.{inferred_role_name} ({checks} checks) ==='
|
||||
else:
|
||||
lines.append('')
|
||||
lines.append(f'=== Result: {errors} errors, {warnings} warnings ({checks} checks) ===')
|
||||
result = '\n'.join(lines)
|
||||
write_output(result)
|
||||
sys.exit(1 if errors > 0 else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user