I have following powershell script reading from csv and exporting to another csv. It's working in terms of basic functionality. Script below is currently exporting as such:
USERS
jdoe
mprice
tsmith
Add-PSSnapin microsoft.sharepoint.powershell -ErrorAction SilentlyContinue
# csv file name
[parameter(Mandatory=$false)][string]$CsvFilePath = ".\AllSiteCollectionsLocal.csv"
$csvItems = Import-Csv $CsvFilePath
$resultsarray = #()
$firstObject = New-Object PSObject
# iterate lines in csv
foreach($Item in $csvItems)
{
$site = new-object Microsoft.SharePoint.SPSite($Item.SiteCollection)
$web = $site.openweb()
$siteUsers = $web.SiteUsers
Write-Host $Item.SiteCollection -ForegroundColor Green
foreach($user in $siteUsers)
{
Write-Host $user.LoginName
$loginnames = #{
USERS = $user.LoginName
}
$resultsarray += New-Object PSObject -Property $loginnames
}
$web.Dispose()
$site.Dispose()
$resultsarray | export-csv -Path c:\temp\sitesandusers.csv -NoTypeInformation
}
I need to export as below. Note, I dont even need a header, but do need $Item.SiteCollection value to print out between each iteration of users under each site, so the outer foreach needs to print $Item.SiteCollection then the inner foreach would print $user.LoginName
http://test1.com
jdoe
mprice
http://test2.com
tsmith
I'm guessing you wanted to do parameters for your script to be called from elsewhere? As of now, your metadata attribute on $CsvFilePath are redundant to what PowerShell already does for you.
As for your question, you would just have to append $Item.SiteCollection to your PSObject. This too isn't needed as PowerShell streaming capabilities allow you to assign directly to a variable; so no need for += - which can be computationally expensive on larger lists slowing overall performance. Now we end up with:
Param (
[parameter(Mandatory=$false)]
[string]$CsvFilePath = ".\AllSiteCollectionsLocal.csv"
)
Add-PSSnapin microsoft.sharepoint.powershell -ErrorAction SilentlyContinue
$csvItems = Import-Csv $CsvFilePath
$variable = foreach($Item in $csvItems)
{
$site = new-object Microsoft.SharePoint.SPSite($Item.SiteCollection)
$web = $site.openweb()
$siteUsers = $web.SiteUsers
Write-Host -Object $Item.SiteCollection -ForegroundColor Green
Write-Output -InputObject $Item.SiteCollection
foreach($user in $siteUsers)
{
Write-Host -Object $user.LoginName
Write-Output -InputObject $user.LoginName
}
$null = $web.Dispose()
$null = $site.Dispose()
}
$variable | Out-File -FilePath 'c:\temp\sitesandusers.csv'
Bypassing $variable you can assign the output directly to the file placing the export outside the first foreach statement.
This requires the use of a sub-expression operator $() to wrap around the loop.
Also added a Param ( ) statement for your parameter declaration.
Didn't mess with the parameter attributes as it can show the Authors intentions regardless if it's needed or not.
Probably should add that, Write-Output will explicitly write to the success stream allowing the values to be assigned to the variable, whereas Write-Host writes to the information stream, so no object pollution (duplicates) occur.
Overview: I have a script which runs a query to extract all Audit Logs from the past day, from a SharePoint site. It then creates a .csv file, and uploads it to the "Shared Documents" folder on SharePoint.
The script works perfectly when I manually run it from PowerShell console, regardless if it's run with admin rights, or not.
NOTE: This SharePoint solution is On-Premise, not Online.
The Issue: When I run the script from Task Scheduler it generates the .csv file, and completes the whole script, but the file is never uploaded. There's no error messages.
Task Scheduler Settings: I use "Run whether user is logged on or not" and "Run with highest privileges"
Here's the full script:
Start-Transcript -Path "C:\Timer Jobs\exampleDomain.Audit.CreateAndTrimLog\transcript17.txt" -NoClobber
Add-PSSnapin "Microsoft.SharePoint.PowerShell"
#Get date, and format it. Used for file name, and for deleting old Audit files
$today = Get-Date
$startDate = $today.AddDays(-1)
$todayFormatted = $today.ToString('dd-MM/yyyy_HH-mm-ss')
#Get site and save siteURL
$siteUrl = "https://example.com"
$docLibName = "Shared Documents"
#site is needed for Audit Query
$site = Get-SPSite -Identity $siteUrl
#web is needed to upload the file
$web = Get-SPWeb -Identity $siteUrl
$list = $web.Lists[$docLibName]
$folder = $list.RootFolder
$files = $folder.Files
#Creaty query for Audits
$wssQuery = New-Object -TypeName Microsoft.SharePoint.SPAuditQuery($site)
$wssQuery.SetRangeStart($startDate)
$wssQuery.SetRangeEnd($today)
#Create Audit Collection object
$auditCol = $site.Audit.GetEntries($wssQuery)
#Get all users on site
$users = $site.RootWeb.SiteUsers
#Get and add $userName to each $audit
#Ignore when it says "User cannot be found" in log
$CachedUsers = #{};
foreach($audit in $auditCol){
$curUserId = $audit.UserId;
$user = $null;
if($CachedUsers.$curUserId -eq $null){
$user = $users.GetById($curUserId);
$CachedUsers.$curUserUI = $user;
} else {
$user = $CachedUsers.$curUserId;
}
$userName = ($user.DisplayName + " <" + $user.LoginName) + ">";
$audit | Add-Member NoteProperty -Name "UserName" -Value $userName -Force;
}
#Export CSV-file. Save fileName and filePath for when uploading to SharePoint
$fileName = ("domain_Audit_Log_" + $todayFormatted + ".csv")
$filePath = "C:\Users\xml\" + ($fileName)
$auditCol | Export-Csv -Append -path ($filePath) -NoTypeInformation -Delimiter ";" -Encoding UTF8
$file = Get-ChildItem $filePath
[Microsoft.SharePoint.SPFile]$spFile = $web.GetFile("/" + $folder.Url + "/" + $file.Name)
if($spFile.Exists -eq $false) {
#Open FileStream
$fileStream = ([System.IO.FileInfo] (Get-Item $file.FullName)).OpenRead()
#Add File
Write-Host "Uploading file" $file.Name "to" $folder.ServerRelativeUrl "..."
try {
[Microsoft.SharePoint.SPFile]$spFile = $files.Add($folder.Url + "/" + $file.Name, [System.IO.Stream]$fileStream, $true)
Write-Host "Success"
} catch {
Write-Host "Error"
}
#Close FileStream
$fileStream.Close()
}
$web.Dispose()
#$site.Audit.DeleteEntries($startDate)
Only difference in the Transcript.txt file, when I run it Manually vs. Task Scheduler is these lines, that are added in the Task Scheduler Tanscript:
PS>$global:?
True
The Transcript prints the "Success" line from my Try-Catch
Problem was in SharePoint. Whenever I uploaded a file using Task Scheduler the file was marked as "Checked Out", so it was not viewable for other users.
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.
When I write PowerShell scripts they are typically to be used against many computers. Since we use a text file with a list of computers, I write my scripts with the following function:
Write-host "Select Text/CSV File with List of Computers"
#Provides Dialog Box to select a file with list of computers. File must contain only 1 of each of the Computer name(s) per line
Function Get-OpenFile($initialDirectory)
{
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") |Out-Null
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $initialDirectory
$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
$OpenFileDialog.ShowHelp = $true
}
$InputFile = Get-OpenFile
If($InputFile -eq "Cancel"){
Write-host "Canceled By User"
exit}
Else{
$Computers = #(get-content -path $InputFile)
}
If I'm writing a script to be used for a single computer, I use the following:
$computer = Read-Host "Enter hostname"
My question is, how can I write these scripts to do the following:
A) the Get-OpenFile command to parse the computer names
B) if the Get-OpenFile is canceled, have the script prompt for a computer name using Read-Host
C) if the Read-Host entry is empty, then cancel the script
Any ideas?
I would combine the two into a single function:
function Get-ComputerName
{
param(
$InitialDirectory = ([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments))
)
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.InitialDirectory = $InitialDirectory
$OpenFileDialog.ShowHelp = $true
$OpenFileDialog.Filter = 'Text files (*.txt)|*.txt|All files (*.*)|*.*'
# Show OpenFileDialog window and test result
if($OpenFileDialog.ShowDialog() -eq 'OK'){
# A file was picked, read it
$Computers = Get-Content -Path $OpenFileDialog.FileName
} else {
# No file chosen, prompt user
$Computers = #(Read-Host 'Enter hostname:')
if($Computers[0] -like '*,*'){
# Input contains comma, assume multiple names
$Computers = $Computers[0] -split ','
}
}
# Dispose of file dialog
if($OpenFileDialog){
$OpenFileDialog.Dispose()
}
# Return computer names
return $Computers
}
I have a script which returns the location of a list of computers within AD. This works fine for my current domain and if I specify "-searchroot" it works for the others.
If I query a computer not on the correct domain, with 'get-qadcomputer' it does not return any information to the console, so need a method to catch and try alternatives?
I have tried this, which just works for all computer in DC=Domain1 (my current domain):
Add-PSSnapin Quest.ActiveRoles.ADManagement
$strPath = "C:\sample.xlsx"
$objExcel = new-object -ComObject Excel.Application
$objExcel.Visible = $false
$WorkBook = $objExcel.Workbooks.Open($strPath)
$worksheetIn = $workbook.sheets.item("Asset")
$worksheetOut = $workbook.sheets.item("Asset")
$intRowMax = ($worksheetIn.UsedRange.Rows).count
$Columnnumber = 1
For($intRow = 2 ; $intRow -le $intRowMax ; $intRow++) {
Try
{
$name = $worksheetIn.cells.item($intRow,$ColumnNumber).value2
"Querying $name...$introw of $intRowMax"
$OU = Get-QADcomputer -searchroot 'OU=Workstations,DC=DOMAIN1'-LdapFilter "(CN=$Name)"`
| ft Location -HideTableHeaders
$Out = Format-list -InputObject $OU | out-string
$worksheetOut.cells.item($intRow,6) = "$Out"
}
Catch
{
$name = $worksheetIn.cells.item($intRow,$ColumnNumber)
"Querying $name...$introw of $intRowMax"
$OU = Get-QADComputer -SearchRoot 'OU=Workstations,DC=DOMAIN2' -LdapFilter (CN=$Name)"`
| ft Location -HideTableHeaders
$Out = Format-list -InputObject $OU | out-string
$worksheetOut.cells.item($intRow,6) = "$Out"
}
}
$objexcel.save()
$objExcel.workbooks.close()
$objexcel.application.quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Workbook)
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($objExcel)
Remove-Variable objExcel
You could check if $OU is equal to $null. I suspect it just isn't finding anything if you get no error text or it doesn't take you to the catch handler. Another option is to check $? right after the line calling Get-QADComputer but I suspect you will get True (command succeeded). But if you get False, then that tells you it didn't succeed.