Powershell ignore "Read Only" request and open without read only - excel

I have a document set to request that user open a read only version(Option "Read-only Recommended"). I would like to open the excel document without read on only in powershell (decline the prompt asking to open "Read Only"). Here is my current code.
$dir = "\\file_path\*"
$latest = Get-ChildItem -Path $dir | Sort-Object LastAccessTime -Descending | Select-Object -First 1
$latest.name
$excelObj = New-Object -ComObject Excel.Application
$excelObj.Visible = $True
$excelObj.DisplayAlerts = $False
$workBook = $excelObj.Workbooks.Open($latest)
How do I ignore the read only prompt and open the full version?

There should be a IgnoreReadOnlyRecommended argument that you can supply in the workbook open method:
$workBook = $excelObj.Workbooks.Open($latest,,,,,,$True,,,,,,,)
Workbooks.Open Method (MSDN)
Edit
Based on comments below, it appears that there is a bug preventing this method from working when the $null parameters are supplied. Thanks to this answer on another question it appears there may be a way around this:
1st, this function is required:
Function Invoke-NamedParameter {
[CmdletBinding(DefaultParameterSetName = "Named")]
param(
[Parameter(ParameterSetName = "Named", Position = 0, Mandatory = $true)]
[Parameter(ParameterSetName = "Positional", Position = 0, Mandatory = $true)]
[ValidateNotNull()]
[System.Object]$Object
,
[Parameter(ParameterSetName = "Named", Position = 1, Mandatory = $true)]
[Parameter(ParameterSetName = "Positional", Position = 1, Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]$Method
,
[Parameter(ParameterSetName = "Named", Position = 2, Mandatory = $true)]
[ValidateNotNull()]
[Hashtable]$Parameter
,
[Parameter(ParameterSetName = "Positional")]
[Object[]]$Argument
)
end { ## Just being explicit that this does not support pipelines
if ($PSCmdlet.ParameterSetName -eq "Named") {
## Invoke method with parameter names
## Note: It is ok to use a hashtable here because the keys (parameter names) and values (args)
## will be output in the same order. We don't need to worry about the order so long as
## all parameters have names
$Object.GetType().InvokeMember($Method, [System.Reflection.BindingFlags]::InvokeMethod,
$null, ## Binder
$Object, ## Target
([Object[]]($Parameter.Values)), ## Args
$null, ## Modifiers
$null, ## Culture
([String[]]($Parameter.Keys)) ## NamedParameters
)
} else {
## Invoke method without parameter names
$Object.GetType().InvokeMember($Method, [System.Reflection.BindingFlags]::InvokeMethod,
$null, ## Binder
$Object, ## Target
$Argument, ## Args
$null, ## Modifiers
$null, ## Culture
$null ## NamedParameters
)
}
}
}
Which would suggest the Workbooks.Open() method could be called like so:
$workBook = Invoke-NamedParameter $excelObj "Workbooks.Open" #{"FileName"=$latest;"IgnoreReadOnlyRecommended"=$True}

If your looking to just open the file for reading and ignore the prompt then this works:
$workBook = $excelObj.Workbooks.Open($latest,$null,$true)
The 3rd argument denotes true to open read-only.
This approach does not appear to be subject to the aforementioned bug!

Related

Why am I receiving a Connect-AzureAD error after connecting to the service?

I am working on my first GUI in Powershell. I have used small scripts with the Azure modules in the past so I am even more confused as to why this is breaking. In the script below there are two places that I tried to import and use the connect-azuread cmdlet so it is commented in the second spot but I attempted it in both places. The script is meant to present a GUI to select a csv of UPNs to find additional info in the azure tenant and return it in another csv. No matter where I place the connect-azuread command, I recieve the error: "You must call the Connect-AzureAD cmdlet before calling any other cmdlets." When I run the script it does prompt me to sign into Azure with the standard Microsoft login page but it apparently does not hold onto the credential.
This is the code that I tried:
Install-Module AzureAD -Force | Out-Null
Import-Module AzureAD | Out-Null
Connect-AzureAD
Add-Type -AssemblyName System.Windows.Forms
$LocationForm = New-Object system.Windows.Forms.Form
$LocationForm.ClientSize = '500,300'
$LocationForm.text = "Azure Information Pull"
$LocationForm.BackColor ='#ffffff'
$Title = New-Object System.Windows.Forms.Label
$Title.text = "Retrieving Data From Azure Tenant"
$Title.AutoSize = $true
$Title.Location = New-Object System.Drawing.Point(20,20)
$Title.Font = 'Microsoft Sans Serif,13'
$Description = New-Object System.Windows.Forms.Label
$Description.Text = "Retrieve UPN, Display Name, Email, and EmployeeID from Azure Tenant."
$Description.AutoSize = $False
$Description.Width = 450
$Description.Height = 50
$Description.location = New-Object System.Drawing.Point(20,50)
$Description.Font = 'Microsoft Sans Serif,10'
#$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog
#$FileBrowser.Title = "Select a file"
#$FileBrowser.InitialDirectory = [Environment]::GetFolderPath('Desktop')
#$FileBrowser.Filter = "CSV (*.csv)| *.csv"
#$SelectedFile = $FileBrowser.FileName
Function Get-FileName()
{
[System.Reflection.Assembly]::LoadWithPartialName(“System.windows.forms”) |
Out-Null
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = [Environment]::GetFolderPath('Desktop')
$OpenFileDialog.filter = "CSV (*.csv)| *.csv"
$OpenFileDialog.ShowDialog() | Out-Null
$global:Input = $OpenFileDialog.filename
$Input
} #end function Get-FileName
$InputFileBtn = New-Object System.Windows.Forms.Button
$InputFileBtn.BackColor = '#a4ba67'
$InputFileBtn.Text = "Select File"
$InputFileBtn.Width = 90
$InputFileBtn.height = 30
$InputFileBtn.Location = New-Object System.Drawing.Point(370,250)
$InputFileBtn.Font = 'Microsoft Sans Serif,10'
$InputFileBtn.ForeColor = "#ffffff"
$InputFileBtn.Add_Click({Get-FileName})
$cancelBtn = New-Object system.Windows.Forms.Button
$cancelBtn.BackColor = "#ffffff"
$cancelBtn.text = "Finish"
$cancelBtn.width = 90
$cancelBtn.height = 30
$cancelBtn.location = New-Object System.Drawing.Point(260,250)
$cancelBtn.Font = 'Microsoft Sans Serif,10'
$cancelBtn.ForeColor = "#000"
$cancelBtn.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$LocationForm.CancelButton = $cancelBtn
$LocationForm.Controls.AddRange(#($Title,$Description,$cancelBtn,$InputFileBtn))
[void]$LocationForm.ShowDialog()
#Begin Script
#Install-Module AzureAD -Force | Out-Null
#Import-Module AzureAD | Out-Null
#Connect-AzureAD
$OutputLocation = Split-Path -Path $global:Input
$UserList = import-csv -path $global:Input
foreach($user in $UserList){
Get-AzureADUser -ObjectID $user.upn | select UserPrincipalName,Displayname,mail, #{Name = 'EmployeeId'; Expression = {$_.ExtensionProperty.employeeId}} | export-csv "$OutputLocation\output.csv" -append -noTypeInformation}
I do not expect to get the error to connect to the azure ad module when I have already made the connection.
I tried to reproduce the same in my environment and got the results successfully like below:
Connect-AzureAD
Add-Type -AssemblyName System.Windows.Forms
$LocationForm = New-Object system.Windows.Forms.Form
$LocationForm.ClientSize = '500,300'
$LocationForm.text = "Azure Information Pull"
$LocationForm.BackColor ='#ffffff'
$Title = New-Object System.Windows.Forms.Label
$Title.text = "Retrieving Data From Azure Tenant"
$Title.AutoSize = $true
$Title.Location = New-Object System.Drawing.Point(20,20)
$Title.Font = 'Microsoft Sans Serif,13'
$Description = New-Object System.Windows.Forms.Label
$Description.Text = "Retrieve UPN, Display Name, Email, and EmployeeID from Azure Tenant."
$Description.AutoSize = $False
$Description.Width = 450
$Description.Height = 50
$Description.location = New-Object System.Drawing.Point(20,50)
$Description.Font = 'Microsoft Sans Serif,10'
[Environment]::GetFolderPath('Desktop')
Function Get-FileName()
{
[System.Reflection.Assembly]::LoadWithPartialName(“System.windows.forms”) |
Out-Null
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = [Environment]::GetFolderPath('Desktop')
$OpenFileDialog.filter = "CSV (*.csv)| *.csv"
$OpenFileDialog.ShowDialog() | Out-Null
$global:Input = $OpenFileDialog.filename
$Input
}
$InputFileBtn = New-Object System.Windows.Forms.Button
$InputFileBtn.BackColor = '#a4ba67'
$InputFileBtn.Text = "Select File"
$InputFileBtn.Width = 90
$InputFileBtn.height = 30
$InputFileBtn.Location = New-Object System.Drawing.Point(370,250)
$InputFileBtn.Font = 'Microsoft Sans Serif,10'
$InputFileBtn.ForeColor = "#ffffff"
$InputFileBtn.Add_Click({Get-FileName})
$cancelBtn = New-Object system.Windows.Forms.Button
$cancelBtn.BackColor = "#ffffff"
$cancelBtn.text = "Finish"
$cancelBtn.width = 90
$cancelBtn.height = 30
$cancelBtn.location = New-Object System.Drawing.Point(260,250)
$cancelBtn.Font = 'Microsoft Sans Serif,10'
$cancelBtn.ForeColor = "#000"
$cancelBtn.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$LocationForm.CancelButton = $cancelBtn
$LocationForm.Controls.AddRange(#($Title,$Description,$cancelBtn,$InputFileBtn))
[void]$LocationForm.ShowDialog()
$OutputLocation = Split-Path -Path $global:Input
$UserList = import-csv -path $global:Input
foreach($user in $UserList){
Get-AzureADUser -ObjectID $user.upn | select UserPrincipalName,Displayname,mail, #{Name = 'EmployeeId'; Expression = {$_.ExtensionProperty.employeeId}} | export-csv "$OutputLocation\output1.csv" -append -noTypeInformation}
The output file is exported successfully as below:
The error "You must call the Connect-AzureAD cmdlet before calling any other cmdlets" usually occurs if connection to the Azure AD Account is not successful.
Try Connect-AzureAD by using below command:
$credentials = Get-Credential
Connect-AzureAD -Credential $credentials
Otherwise, try to Connect Azure AD like below:
Connect-AzureAd -TenantId TenantID
If still the issue persists, try re-install the AzureAD module:
Uninstall-Module AzureAD
Install-Module AzureAD
Import-Module AzureAD

How do I fix New-Object Com Class Error for Alteryx?

I have an Alteryx workflow that runs a powershell script to convert a csv to excel. It works locally, but when I publish that workflow to Alteryx Gallery, I get an error and my log produces something like this:
New-Object: Retrieving the COM class factory for component with CLSID....
$excel = New-Object -ComObject excel.application...
Resource Unavailable...
FullyQualifiedErrorId: No COMClassIdentified,Microsoft.PowerShell.Commands.NewObjectCommand...
This reads to me like excel is not installed on the alteryx server. Is my assumption correct? What do I need to do to fix this error?
$SharedDriveFolderPath = "\\---\shares\Groups\---\---\---\---\--\B\"
$files = Get-ChildItem $SharedDriveFolderPath -Filter *.csv
foreach ($f in $files){
$outfilename = $f.BaseName +'.xlsx'
$outfilename
#Define locations and delimiter
$csv = "\\---\shares\Groups\---\---\---\---\--\B\$f" #Location of the source file
$xlsx = "\\---\shares\Groups\---\---\---\---\---\---\C\$outfilename" #Desired location of output
$delimiter = "," #Specify the delimiter used in the file
# Create a new Excel workbook with one empty sheet
$excel = New-Object -ComObject excel.application
$workbook = $excel.Workbooks.Add(1)
$worksheet = $workbook.worksheets.Item(1)
# Build the QueryTables.Add command and reformat the data
$TxtConnector = ("TEXT;" + $csv)
$Connector = $worksheet.QueryTables.add($TxtConnector,$worksheet.Range("A1"))
$query = $worksheet.QueryTables.item($Connector.name)
$query.TextFileOtherDelimiter = $delimiter
$query.TextFileParseType = 1
$query.TextFileColumnDataTypes = ,1 * $worksheet.Cells.Columns.Count
$query.AdjustColumnWidth = 1
# Execute & delete the import query
$query.Refresh()
$query.Delete()
# Save & close the Workbook as XLSX.
$Workbook.SaveAs($xlsx,51)
$excel.Quit()
}

Powershell threads logging to single output file?

Given a script the implements multi-threaded operations via a runspace pool, how does one get all the threads to output to a single file? I understand there are synchronization and/or locking issues to deal with, I'm just not sure what options are available.
Here is an example of how my threads are created. The example code hangs.
$_ps = [Powershell]::Create()
$_ps.RunspacePool = $_runspace_pool
$null = $_ps.AddScript({
Param (
[Parameter(Mandatory = $true)]
$ComputerName,
[Parameter(Mandatory = $true)]
$LibPath,
[Parameter(Mandatory=$false)]
$Logger = $null
)
$ErrorActionPreference = 'SilentlyContinue'
Import-Module -Name "$LibPath\MyObjectModule" -Force
$_obj = MyObjectModule\New-MyObject
if ( $Logger ) { $_obj.Logger = $Logger }
$_obj.InvokeDiscovery($ComputerName)
}) # end of $_ps.AddScript()
# set script parameters
$null = $_ps.AddParameters(#{ComputerName = $_computer; LibPath = $_lib_path; Logger = $_logger})
I'm thinking I might create a synchronized sorted list as a queue that is added to a logger object in my root thread and also passed to each child thread. Separate logger objects could be instantiated in each child thread that will put messages into the synchronized queue. The root thread logger would periodically flush the queue via a call like $logger.flush().
Maybe something like this...
$_queue = some synchronized queue-like object
$_logger = MyLoggerModule\New-MyLogger
$_logger.queue = $_queue
$_ps = [Powershell]::Create()
$_ps.RunspacePool = $_runspace_pool
$null = $_ps.AddScript({
Param (
[Parameter(Mandatory = $true)]
$Queue,
...
)
$ErrorActionPreference = 'SilentlyContinue'
Import-Module -Name "$LibPath\MyObjectModule" -Force
Import-Module -Name "$LibPath\MyLoggerModule" -Force
$_obj = MyObjectModule\New-MyObject
$_logger = MyLoggerModule\New-MyLogger
$_logger.queue = $Queue
$_obj.InvokeDiscovery($ComputerName)
}) # end of $_ps.AddScript()
# set script parameters
$null = $_ps.AddParameters(#{ComputerName = $_computer; LibPath = $_lib_path; Queue = $_queue})
# wait for threads to complete
do {
$_logger.flush()
Start-Sleep -seconds 5
} ( threads still running )
Assuming that made sense, is it a workable solution? Are there other options? Am I barking up an impossible tree and should abandon the idea altogether?
I was able to solve my problem by implementing creating a mutex and passing it to relevant functions. Worked like a charm.

Insufficient memory to continue the execution of the program. Creating Xlxs from CSV

I have a script that cycles through a folder and condenses multiple CSVs to one xlsx file with the names of the CSV as worksheets. However, when the script runs as part of a larger script it failes when it refreshes the query.
$Query.Refresh()
On its own the script runs fine, but when added to the larger one it fails. Can anyone advise why this is the case?
Below is the error I get:
Insufficient memory to continue the execution of the program.
At C:\Temp\Scripts\Shares_Complete.psm1:254 char:13
+ $Query.Refresh()
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], OutOfMemoryException
+ FullyQualifiedErrorId : System.OutOfMemoryException
I have tried single csv with the same code and still the same result.
$script:SP = "C:\Temp\Servers\"
$script:TP = "C:\Temp\Servers\Pc.txt"
$script:FSCSV = "C:\Temp\Server_Shares\Server Lists\"
$script:Message1 = "Unknown Hosts"
$script:Message2 = "Unable to connect"
$script:Message3 = "Unknown Errors Occurred"
$script:Txt = ".txt"
$script:OT = ".csv"
$script:FSERROR1 = $FSCSV+$Message1+$OT
$script:FSERROR2 = $FSCSV+$Message2+$OT
$script:FSERROR3 = $FSCSV+$Message2+$OT
$script:ERL3 = $E4 + "Shares_Errors_$Date.txt"
$script:ECL1 = $E4 + "Shares_Exceptions1_$Date.txt"
$script:ERL1 = $E4 + "Shares_Errors1_$Date.txt"
$script:ECL3 = $E4 + "Shares_Exceptions_$Date.txt"
function Excel-Write {
if ($V -eq "1") {
return
}
[System.GC]::Collect()
$RD = $FSCSV + "*.csv"
$CsvDir = $RD
$Ma4 = $FSCSV + "All Server Shares for Domain $CH4"
$csvs = dir -path $CsvDir # Collects all the .csv's from the driectory
$FSh = $csvs | Select-Object -First 1
$FSh = ($FSh -Split "\\")[4]
$FSh = $FSh -replace ".{5}$"
$FSh
$outputxls = "$Ma4.xlsx"
$script:Excel = New-Object -ComObject Excel.Application
$Excel.DisplayAlerts = $false
$workbook = $excel.Workbooks.Add()
# Loops through each CVS, pulling all the data from each one
foreach ($iCsv in $csvs) {
$script:iCsv
$WN = ($iCsv -Split "\\")[-1]
$WN = $WN -replace ".{4}$"
if ($WN.Length -gt 30) {
$WN = $WN.Substring(0, [Math]::Min($WN.Length, 20))
}
$Excel = New-Object -ComObject Excel.Application
$Excel.DisplayAlerts = $false
$Worksheet = $workbook.Worksheets.Add()
$Worksheet.Name = $WN
$TxtConnector = ("TEXT;" + $iCsv)
$Connector = $worksheet.Querytables.Add($txtconnector,$worksheet.Range("A1"))
$query = $Worksheet.QueryTables.Item($Connector.Name)
$query.TextfileOtherDelimiter = $Excel.Application.International(5)
$Query.TextfileParseType = 1
$Query.TextFileColumnDataTypes = ,2 * $worksheet.Cells.Column.Count
$query.AdjustColumnWidth = 1
$Query.Refresh()
$Query.Delete()
$Worksheet.Cells.EntireColumn.AutoFit()
$Worksheet.Rows.Item(1).Font.Bold = $true
$Worksheet.Rows.Item(1).HorizontalAlignment = -4108
$Worksheet.Rows.Item(1).Font.Underline = $true
$Workbook.Save()
}
$Empty = $workbook.Worksheets.Item("Sheet1")
$Empty.Delete()
$Workbook.SaveAs($outputxls,51)
$Workbook.Close()
$Excel.Quit()
$ObjForm.Close()
Delete
}
Should continue script and create the xlsx.
Looking at your script, it doesn't surprise me you eventually run out of memory, because you are continouisly creating Com objects and never release them from memory.
Whenever you have created Com objects and finished with them, use these lines to free up the memory:
$Excel.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
Also, take a good look at the code.
You are creating a $Script:Excel = New-Object -ComObject excel.application object before the foreach loop but you don't use that. Instead, you are creating new Excel and workbook objects inside the loop over and over again where there is absolutely no reason for it since you can re-use the one you created before the loop.
As an aside: The following characters are not allowed in excel worksheet names
\/?*[]:
Length limitation is 31 characters.
EDIT
I had a look at your project and especially the Shares_Complete.psm1 file.
Although I'm not willing of course to rewrite your entire project, I do have some remarks that may help you:
[System.Net.Dns]::GetHostByName() is obsolete. Use GetHostEntry()
when done with a Windows form, use $ObjForm.Dispose() to clear it from memory
you do a lot of [System.GC]::Collect(); [System.GC]::WaitForPendingFinalizers() for no reason
Why not use [System.Windows.MessageBox]::Show() instead of using a Com object $a = new-object -comobject wscript.shell. Again you leave that object lingering in memory..
use Join-Path cmdlet instead of $RD = $FSCSV + "*.csv" or $Cop = $FSCSV + "*.csv" constructs
remove invalid characters from Excel worksheet names (replace '[\\/?*:[\]]', '')
use Verb-Noun names for your functions so it becomes clear what they do. Now you have functions like Location, Delete and File that don't mean anything
you are calling functions before they are defined like in line 65 where you call function Shares. At that point it does not exist yet because the function itself is written in line 69
add [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) | Out-Null in function Excel-Write
there is no need to use the variable $Excel in script scope ($Script:Excel = New-Object -ComObject excel.application) where it is used only locally to the function.
you may need to look at Excel specifications and limits
fix your indentation of code so it is clear when a loop or if starts and ends
I would recommend using variable names with more meaning. For an outsider or even yourself after a couple of months two-letter variable names become confusing

Accessing Sharepoint UserProfile Properties with Powershell script

The following script spits out all UserProfile properties for users on Sharepoint 2007:
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles")
# Function: Get-UserProfiles
# Description: return a UserProfileManager object containing all user profiles
# Parameters: SSPName SSPName
#
Function global:Get-UserProfiles($SSPName)
{
$ServerContext = [Microsoft.Office.Server.ServerContext]::GetContext($SSPName);
$UPManager = new-object Microsoft.Office.Server.UserProfiles.UserProfileManager($ServerContext);
return $UPManager.GetEnumerator();
}
$profiles = Get-UserProfiles("SharedServices");
$profiles | ForEach-Object { $_.GetEnumerator();}
However, what I want to do is be able to return a table, or csv file of specific values in the profile, e.g. Username, WorkEmail, WorkPhone. I have tried piping the output to |ft Username, WorkEmail, Workphone and | select Username, WorkEmail, WorkPhone but this just returns blanks.
I feel like I am so close. I don't want to replace the $_.GetEnumerator() call with lots of $_.Item("property") calls and it doesn't feel like I should have to. Any ideas?
I've further developed the code so that it now accepts a comma delimited list of properties and writes them to a delimited file.
# Outputs a delimited file with specified user profile properties for each user in Sharepoint
# Create array of desired properties
$arProperties = 'UserName','FirstName','LastName','Title','WorkEmail','WorkPhone','Manager','AlternateContact','RoleDescription','PictureURL';
# Specify output file
$outfile = 'UserProfiles.csv';
#Specify delimiter character (i.e. not one that might appear in your user profile data)
$delim = '^';
# Specify Shared Service Provider that contains the user profiles.
$SSP = "SharedServices";
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles")
# Function: Get-UserProfiles
# Description: return a UserProfileManager object containing all user profiles
# Parameters: SSPName SSPName
#
Function global:Get-UserProfiles($SSPName)
{
$ServerContext = [Microsoft.Office.Server.ServerContext]::GetContext($SSPName);
$UPManager = new-object Microsoft.Office.Server.UserProfiles.UserProfileManager($ServerContext);
return $UPManager.GetEnumerator();
}
$profiles = Get-UserProfiles($SSP);
#Initialise Output file with headings
$header = [string]::join($delim,$arProperties);
Write-Output $header | Out-File $outfile
#Output the specified properties for each
$profiles | ForEach-Object {
foreach($p in $arProperties){
# Get the property name and add it to a new array, which will be used to construct the result string
$arProfileProps += $_.Item($p);
}
$results = [string]::join($delim,$arProfileProps);
# Get rid of any newlines that may be in there.
$CleanResults = $results.Replace("`n",'');
Write-Output $CleanResults
Remove-Variable -Name arProfileProps
} | Out-File -Append $outfile
This gets me a bit closer. I'd still really like a script that iterates through all the profile properties and puts them into a CSV or XML file more gracefully. This will do for now.

Resources