I'm trying to detect if a "Microsoft Word Document" has been configured "Read-Only" by MSOffice.
Everything I have read relates to checking using Get-ItemProperty "C:\tmp\readonly.docx" | Select-Object IsReadOnly, but that is checking if the File is "read only" from the filesystem level.
The Problem is Microsoft doesn't mark it on the outside, you'd need to open/check with the Microsoft COM object I figure to query if document is read only.
PS C:\Users\Admin> Get-ItemProperty "C:\tmp\readonly.docx" | Select-Object IsReadOnly
IsReadOnly
----------
False
Update: If file is configured RO without a Password then you can simple open as RW without a prompt (via powershell), but if it is with a Password then you get the prompt to acknowledge RO status which is what I want to avoid because it's hanging my script.
Continuing from my comment and note, not using anything dealing with the Word DOM via COM.
$File = 'd:\Temp\HSGCopy.docx'
# File not in use
Set-ItemProperty -Path $File -Name IsReadOnly -Value $false
(Get-ItemProperty 'd:\Temp\HSGCopy.docx').IsReadOnly
$File |
ForEach{
try
{
$TargetFile = (New-Object System.IO.FileInfo $PSitem).Open(
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None
)
$TargetFile.Close()
Remove-Item -Path $PSItem -WhatIf
}
catch [System.Management.Automation.ItemNotFoundException]{$PSItem.Exception.Message}
catch {$PSItem.Exception.Message}
}
# Results
<#
False
What if: Performing the operation "Remove File" on target "D:\Temp\HSGCopy.docx".
#>
# File in use
Set-ItemProperty -Path $File -Name IsReadOnly -Value $false
(Get-ItemProperty 'd:\Temp\HSGCopy.docx').IsReadOnly
$File |
ForEach{
try
{
$TargetFile = (New-Object System.IO.FileInfo $PSitem).Open(
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None
)
$TargetFile.Close()
Remove-Item -Path $PSItem -WhatIf
}
catch [System.Management.Automation.ItemNotFoundException]{$PSItem.Exception.Message}
catch {$PSItem.Exception.Message}
}
# Results
<#
False
Exception calling "Open" with "3" argument(s): "The process cannot access the file 'd:\Temp\HSGCopy.docx' because it is being used by another process."
#>
# Change the file attribute
# File not in use
Set-ItemProperty -Path $File -Name IsReadOnly -Value $true
(Get-ItemProperty 'd:\Temp\HSGCopy.docx').IsReadOnly
$File |
ForEach{
try
{
$TargetFile = (New-Object System.IO.FileInfo $PSitem).Open(
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None
)
$TargetFile.Close()
Remove-Item -Path $PSItem -WhatIf
}
catch [System.Management.Automation.ItemNotFoundException]{$PSItem.Exception.Message}
catch {$PSItem.Exception.Message}
}
# Results
<#
True
Exception calling "Open" with "3" argument(s): "Access to the path 'd:\Temp\HSGCopy.docx' is denied."
#>
# File in use
Set-ItemProperty -Path $File -Name IsReadOnly -Value $true
(Get-ItemProperty 'd:\Temp\HSGCopy.docx').IsReadOnly
$File |
ForEach{
try
{
$TargetFile = (New-Object System.IO.FileInfo $PSitem).Open(
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None
)
$TargetFile.Close()
Remove-Item -Path $PSItem -WhatIf
}
catch [System.Management.Automation.ItemNotFoundException]{$PSItem.Exception.Message}
catch {$PSItem.Exception.Message}
}
# Results
<#
True
Exception calling "Open" with "3" argument(s): "Access to the path 'd:\Temp\HSGCopy.docx' is denied."
#>
When using Word document protection
# with Word doc protection off
#>
$Word = New-Object –comobject Word.Application
Try
{
($Word.documents.open($File,$false,$false)).ReadOnly
Write-Warning -Message "$File is protected ReadOnly"
}
Catch {Write-Verbose -Message "$File is not protected" -Verbose}
# then don't forget to close
$Word.Quit()
# Results
<#
VERBOSE: d:\Temp\HSGCopy.docx is not protected
#>
# With Word doc protection on
$Word = New-Object –comobject Word.Application
Try
{
($Word.documents.open($File,$false,$false)).ReadOnly
Write-Warning -Message "$File is protected ReadOnly"
}
Catch {Write-Verbose -Message "$File is not protected ReadOnly" -Verbose}
# then don't forget to close
$Word.Quit()
# Results
<#
True
WARNING: d:\Temp\HSGCopy.docx is protected ReadOnly
#>
By accident or on purpose, you could have both set in an environment. I've had this happen to me in auto-classification scenarios. Meaning when FSRM/RMS/AIP has been deployed/implemented and enforced.
Update
Here a sample of what I have in my workflow to catch this sort of stuff, as per our exchange.
Clear-Host
$Files |
ForEach{
$File = $PSItem
"Processing $PSItem"
try
{
Write-Verbose -Message 'Word properties:
DocID, FullName, HasPassword,
Permission, ReadOnly, Saved,
Creator, CurrentRsid, CompatibilityMode' -Verbose
'DocID', 'FullName', 'HasPassword',
'Permission', 'ReadOnly', 'Saved',
'Creator', 'CurrentRsid', 'CompatibilityMode' |
ForEach {($Word.documents.open($File,$false,$false)).$PSitem}
Write-Verbose -Message 'File system ReadOnly attribute:' -Verbose
(Get-ItemProperty $File).IsReadOnly
Write-Verbose -Message 'Document state' -Verbose
$TargetFile = (New-Object System.IO.FileInfo $PSitem).Open(
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None
)
$TargetFile.Close()
Remove-Item -Path $PSItem -WhatIf
}
catch [System.Management.Automation.ItemNotFoundException]{$PSItem.Exception.Message}
catch {$PSItem.Exception.Message}
}
# Results
<#
Processing d:\Temp\HSGCopy.docx
VERBOSE: Word properties:
DocID, FullName, HasPassword,
Permission, ReadOnly, Saved,
Creator, CurrentRsid, CompatibilityMode
938207550
D:\Temp\HSGCopy.docx
False
True
True
1297307460
12414886
15
VERBOSE: File system ReadOnly attribute:
False
VERBOSE: Document state
What if: Performing the operation "Remove File" on target "D:\Temp\HSGCopy.docx".
#>
Ok, so to test this, I had 3 files as follows:
A) One regular test.docx with no restrictions
B) One readonly.docx w/ a password. This is the file that hung me up w/ a prompt
C) One nopass.docx w/ a read-only setting, but no PWD configured.
A and C would open regardless of the ReadOnly setting, but B would hang on a prompt even with DisplayAlerts set to 0. You also couldn't check the ReadOnly property unless the prompt was surpassed, or if you set it to TRUE it was always true obviosuly.
There is no way I found to check the ReadOnly or HasPassword property without first openeing document. You can likely inspect the XML file for HEX but I'd say that is less reliable. My way just took some testing/trickery to get working. The important part was I had to pass a password and catch if it failed. Doc's A/C would open fine even when you pass a password argument, so no harm there. In the catch I set ReadOnly = TRUE and Visible = TRUE. You may not need to set the visible part true, but if ReadOnly = TRUE then you can't make certain adjustments via VB (like ORIENTATION) and I'll be using SENDKEYS so I'll need the UI if ReadOnly = TRUE. As well, hiding the UI is just a "bonus" but not needed. I may just set it always visible if I continue wasting time on coding IF/THEN for the OPENUI statments.
Anyway... Here is a final code snippet to test on the three files which should result in each file opening w/o a prompt.
#Constants
Clear-Variable ReadOnly
$missing = [System.Type]::Missing
$str = ''
$PASSWD = 'IsPWDProtected?'
$wdAlertsNone = 0
$FILENAME = "C:\tmp\readonly.docx"
$OPENUI = "TRUE"
#Start Word
$ObjWord = New-Object -comobject Word.Application
IF ($OPENUI -eq "FALSE"){$ObjWord.Visible = $FALSE}ELSE{$ObjWord.Visible = $TRUE}
$ObjWord.Application.DisplayAlerts = $wdAlertsNone
#.Open
IF (!$ConfirmConversions){$ConfirmConversions = $missing}
IF (!$ReadOnly){$ReadOnly = $FALSE}
IF (!$AddToRecentFiles){$AddToRecentFiles = $missing}
IF (!$PasswordDocument){$PasswordDocument = $PASSWD}
IF (!$PasswordTemplate){$PasswordTemplate = $PASSWD}
IF (!$Revert){$Revert = $False}
IF (!$WritePasswordDocument){$WritePasswordDocument = $PASSWD}
IF (!$WritePasswordTemplate){$WritePasswordTemplate = $PASSWD}
IF (!$Format){$Format = 'wdOpenFormatAuto'}
IF (!$Encoding){$Encoding = $missing}
IF (!$Visible){$Visible = $False}
try{$ObjDoc=$ObjWord.documents.open($FILENAME,$ConfirmConversions,$ReadOnly,$AddToRecentFiles,$PasswordDocument,$PasswordTemplate,$Revert,$WritePasswordDocument,$WritePasswordTemplate,$Format,$Encoding,$Visible)}
catch {
Write-Error $_
Write-Host "Opening Read_Only"
$ReadOnly = $TRUE
$Visible = $TRUE
$ObjDoc=$ObjWord.documents.open($FILENAME,$ConfirmConversions,$ReadOnly,$AddToRecentFiles,$PasswordDocument,$PasswordTemplate,$Revert,$WritePasswordDocument,$WritePasswordTemplate,$Format,$Encoding,$Visible)
}
#AllDone?
PAUSE
$ObjWord.ActiveDocument.Close(0)
$ObjWord.Quit()
[gc]::collect()
[gc]::WaitForPendingFinalizers()
[gc]::collect()
[gc]::WaitForPendingFinalizers()
sleep 2
Result:
PS C:\Users\Admin> C:\tmp\test.ps1
C:\tmp\test.ps1 : The password is incorrect. Word cannot open the document. (C:\tmp\readonly.doc)
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,test.ps1
Opening Read_Only
Press Enter to continue...:
Note: I hardcoded OPENUI = TRUE during testing because if it got hung on a prompt with the UI closed, I had to use tskill winword.exe and start over.
I would like to ask question about how I should proceed or how I should fix the code.
My problem is that I need my code to write into the Path three different paths for Logstash, Kibana and ElasticSearch, but I have no idea how to do it. It returns always the same error about missing ")" error
Here's the whole code ¨
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[string]$NewLocation.GetType($ElasticSearch)
[string]$ElasticSearch = "C:\Elastic_Test_Server\elasticsearch\bin"
[string]$Kibana = "C:\Elastic_Test_Server\kibana\bin"
[string]$Logstash = "C:\Elastic_Test_Server\logstash\bin"
)
Begin
{
#Je potřeba spustit jako Administrátor
$regPath = "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
$hklm = [Microsoft.Win32.Registry]::LocalMachine
Function GetOldPath()
{
$regKey = $hklm.OpenSubKey($regPath, $FALSE)
$envpath = $regKey.GetValue("Path", "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
return $envPath
}
}
Process
{
# Win32API errory
$ERROR_SUCCESS = 0
$ERROR_DUP_NAME = 34
$ERROR_INVALID_DATA = 13
$NewLocation = $NewLocation.Trim();
If ($NewLocation -eq "" -or $NewLocation -eq $null)
{
Exit $ERROR_INVALID_DATA
}
[string]$oldPath = GetOldPath
Write-Verbose "Old Path: $oldPath"
# Zkontroluje zda cesta již existuje
$parts = $oldPath.split(";")
If ($parts -contains $NewLocation)
{
Write-Warning "The new location is already in the path"
Exit $ERROR_DUP_NAME
}
# Nová cesta
$newPath = $oldPath + ";" + $NewLocation
$newPath = $newPath -replace ";;",""
if ($pscmdlet.ShouldProcess("%Path%", "Add $NewLocation")){
# Přidá to přítomné session
$env:path += ";$NewLocation"
# Uloží do registru
$regKey = $hklm.OpenSubKey($regPath, $True)
$regKey.SetValue("Path", $newPath, [Microsoft.Win32.RegistryValueKind]::ExpandString)
Write-Output "The operation completed successfully."
}
Exit $ERROR_SUCCESS
}
Thank you for your help.
I really think you could simplify this a lot, unless I have misunderstood. Apologies, I am not currently on a Windows machine so can't test this.
function Add-ESPath {
# Create an array of the paths we wish to add.
$ElasticSearch = #(
"C:\Elastic_Test_Server\elasticsearch\bin",
"C:\Elastic_Test_Server\kibana\bin",
"C:\Elastic_Test_Server\logstash\bin"
)
# Collect the current PATH string and split it out in to an array
$CurrentPath = [System.Environment]::GetEnvironmentVariable("PATH")
$PathArray = $CurrentPath -split ";"
# Loop though the paths we wish to add.
foreach ($Item in $ElasticSearch) {
if ($PathArray -notcontains $Item) {
$PathArray += $Item
}
else {
Write-Output -Message "$Item is already a member of the path." # Use Write-Warning if you wish. I see it more as a notification here.
}
}
# Set the path.
$PathString = $PathArray -join ";"
Try {
[System.Environment]::SetEnvironmentVariable("PATH", $PathString)
exit 0
}
Catch {
Write-Warning -Message "There was an issue setting PATH on this machine. The path was:" # Use $env:COMPUTERNAME here perhaps instead of 'this machine'.
Write-Warning -Message $PathString
Write-Warning -Message $_.Exception.Message
exit 1
}
}
Add-ESPath
Perhaps you want to add some kind of log file rather than writing messages/warnings to the console. You can use Add-Content for this.
I long time ago i wrote some functions to add a path to system path + their is an check if the path is already inside the system path. And i also did an elevation check so when i use this function and i forgot to elevate my powershell that i get a warning. Its a different approach, I hope it will help you.
I only use the begin {} proccess{} statements for when i want to write a function that excepts pipeline inputs. So its if you want to write a function that will work as the following:
$paths = #("C:\Elastic_Test_Server\elasticsearch\bin", "C:\Elastic_Test_Server\kibana\bin")
$paths | my-append-these-to-system-path-function
Elevation check:
function G-AmIelevated($warningMessage){
if([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")){
return $true
}else{
write-host "not elevated $warningMessage" -ForegroundColor Red
return $false
}
}
append something to system path with check if its already inside system path:
function G-appendSystemEnvironmentPath($str){
if(test-path $str){
if(!((Get-Itemproperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path) -like "*$str*")){
write-host "`t $str exists...`n adding $str to environmentPath" -ForegroundColor Yellow
if(G-AmIelevated){
write-host `t old: (Get-Itemproperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path).Path
Set-ItemProperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' `
-Name Path `
-Value "$((Get-Itemproperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path).Path);$str"
write-host `t new: (Get-Itemproperty -path 'hklm:\system\currentcontrolset\control\session manager\environment' -Name Path).Path
write-host `t restart the computer for the changes to take effect -ForegroundColor Red
write-host `t `$Env:Path is the merge of System Path and User Path This function set the system path
write-host `t $str appended to environmet variables. -ForegroundColor Green
}else{
write-host `t rerun ise in elevated mode -ForegroundColor Red
}
}else{
write-host "`t $str is in system environmenth path"
}
}else{
write-host `t $str does not exist
}
}
G-appendSystemEnvironmentPath -str "C:\Elastic_Test_Server\elasticsearch\bin"
G-appendSystemEnvironmentPath -str "C:\Elastic_Test_Server\kibana\bin"
G-appendSystemEnvironmentPath -str "C:\Elastic_Test_Server\logstash\bin"
$Launcher_PackageFile = "Launcher.exe"
$Launcher_PackageDestinationDirectory = "$Env:ProgramData\Launcher";
#$Launcher_StartupDirectory = (New-Object -ComObject Shell.Application).NameSpace(0x07)
$Launcher_Source = "$Env:PackageRoot\Launcher\$Launcher_PackageFile"
Script UpdateConfigurationVersion
{
GetScript = {
# For cmdlet. Must always return a hash table.
return #{
"Name"="LauncherPackage"
"Version"="Latest"
};
}
TestScript = {
# Check if the file is in the folder
$TestPath = Join-Path $Launcher_PackageDestinationDirectory $Launcher_PackageFile
if (Test-Path $TestPath) {
return $true;
}
return $false;
}
SetScript = {
# Logic
#move the exe
if (!Test-Path $Launcher_PackageDestinationDirectory) {
New-Item -Type Directory -Path $Launcher_PackageDestinationDirectory
}
Copy-Item -Path $Launcher_Source -Destination $Launcher_PackageDestinationDirectory
#TODO Create a shortcut to the startup directory
#or create a task in the scheduler
}
}
Incorrect syntax
(!Test-Path
if (!(Test-Path $Launcher_PackageDestinationDirectory)) {....
or
if (-not(Test-Path $Launcher_PackageDestinationDirectory) {....
Also add -ErrorAction SilentlyContinue to your Test-Path command to suppress error.
With that info is difficult to tell.
Add messages to TestScript and SetScript to see what's going on. For example:
Write-Verbose -Message 'Testing if already exists'
TestScript gets executed before SetScript, check whether is returning true or false (by adding messages). TestScript may be returning true and thus the SetScript is not running. Output $TestPath to see if the path is correct.
Then check the logs on C:\Windows\System32\Configuration\ConfigurationStatus
I have the below function running in a logon script, which checks whether the user has the current version of IT Self Help.exe. If the current version is not present, then it should be copied onto the desktop from the $appsource folder:
function UrgentSupportApp {
$ErrorActionPreference = "Stop"
trap {Log-Error $_ $MyInvocation.MyCommand; Return}
$desktop = $env:USERPROFILE + '\Desktop\'
$apptarget = $desktop + 'IT Self Help.exe'
$appsource = '\\file\Administration\Unused\Apps\IT Support App\IT Self Help.exe'
# Remove the old version of the app "IT Help Request.exe"
$oldapps = Get-ChildItem $desktop -Filter *"Help Request.exe"
if ($oldapps.count -gt 0) {Remove-Item $oldapps.PSPath -Force}
# Copy the new version over if it is not already present
$currentversion = (Get-Command $appsource).FileVersionInfo.FileVersion
if (Test-Path $apptarget) {
if ((Get-Command $apptarget).FileVersionInfo.FileVersion -ne $currentversion) {
Copy-Item $appsource $desktop -Force ##### Line 981 #####
}
} else {
Copy-Item $appsource $desktop -Force
}
}
function Log-Error {
param (
$error,
[string]$sub,
$detail
)
$ErrorActionPreference = "Stop"
trap {Log-Error $_ $MyInvocation.MyCommand; Return}
$filename = "\\file\administration\Unused\LogonScriptErrors\$username - $sub - $computername - $(Get-Date -Format ddMMyyyy-HHmmss).log"
New-Item $filename -ItemType File -Value "Message: `r`n`t $($error.Exception.Message) `r`n `r`nPosition: `r`n`t $($error.InvocationInfo.PositionMessage) `r`n `r`nSub: `r`n`t $sub `r`n `r`nDetail: `r`n`t $detail"
}
For a couple of users, I am seeing this error come through on line 981, char 22 (see the comment above):
Could not find file 'C:\Users\USER.NAME\Desktop\IT Self Help.exe'.
At \\DC\NETLOGON\mainlogon.ps1:981 char:22
+ Copy-Item <<<< $appsource $desktop -Force
However
The file clearly can be found, as it made it through the fisrt If condition If (Test-Path $apptarget).
If the file couldn't be found, why would the script complain on that line, where we are not even looking for it?
What is this error trying to tell me? If the file could not be found, surely the script would just continue into the Else statement
i try to do error handling within my powershell script. but i always get an fatal. i tried a few things, e. g. try{ } catch{ } - but i did it not get to work.
any ideas or Solutions?
Function Check-Path($Db)
{
If ((Test-Path $Db) –eq $false) {
Write-Output "The file $Db does not exist"
break
}
}
It Returns:
Test-Path : Zugriff verweigert
In K:\access\access.ps1:15 Zeichen:6
+ If ((Test-Path $Db) -eq $false) {
+ ~~~~~~~~~~~~~
+ CategoryInfo : PermissionDenied: (K:\ss.mdb:String) [Test-Path], UnauthorizedAccessException
+ FullyQualifiedErrorId : ItemExistsUnauthorizedAccessError,Microsoft.PowerShell.Commands.TestPathCommand
Somewhat confusingly Test-Path actually generates an error in a number of cases. Set the standard ErrorAction parameter to SilentlyContinue to ignore it.
if ((Test-Path $Db -ErrorAction SilentlyContinue) -eq $false) {
I cannot answer directly. So this has to do:
I strongly disagree with your answer. Test-Path does show $false when you run it against a network share that is not accessible, but it will be false also (without Exception) when the Server is not reachable.
So your answer simply ignores anything but a reachable share.
What is neccessary however is a try-catch-block that handles this better:
[cmdletbinding()]
param(
[boolean]$returnException = $true,
[boolean]$returnFalse = $false
)
## Try-Catch Block:
try {
if ($returnException) {
## Server Exists, but Permission is denied.
Test-Path -Path "\\Exists\Data\" -ErrorAction Stop | Out-Null
} elseif ($returnFalse) {
## Server does not exist
Test-Path -Path "\\NoExists\Data\" -ErrorAction Stop | Out-Null
}
} catch [UnauthorizedAccessException] {
## Unauthorized
write-host "No Access Exception"
} catch {
## an error has occurred
write-host "Any other Exception here"
}
The really important part however is the ErrorAction on the Test-Path command, otherwise the exception will be wrapped around a system management error and is thus not catchable. This is in detail explained here:
PowerShell catching typed exceptions