UMN-Google.psm1
############### # Module for interacting with Google API # More details found at https://developers.google.com/sheets/ and https://developers.google.com/drive/ # ############### ### # Copyright 2017 University of Minnesota, Office of Information Technology # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with Foobar. If not, see <http://www.gnu.org/licenses/>. ### #region Dependancies function ConvertTo-Base64URL { <# .Synopsis convert text or byte array to URL friendly Base64 .DESCRIPTION Used for preparing the JWT token to a proper format. .PARAMETER bytes The bytes to be converted .PARAMETER text The text to be converted .EXAMPLE ConvertTo-Base64URL -text $headerJSON .EXAMPLE ConvertTo-Base64URL -Bytes $rsa.SignData($toSign,"SHA256") #> param ( [Parameter(ParameterSetName='Bytes')] [System.Byte[]]$Bytes, [Parameter(ParameterSetName='String')] [string]$text ) if($Bytes){$base = $Bytes} else{$base = [System.Text.Encoding]::UTF8.GetBytes($text)} $base64Url = [System.Convert]::ToBase64String($base) $base64Url = $base64Url.Split('=')[0] $base64Url = $base64Url.Replace('+', '-') $base64Url = $base64Url.Replace('/', '_') $base64Url } #endregion #region oAuth 2.0 function Get-GOAuthTokenService { <# .Synopsis Get google auth 2.0 token for a service account .DESCRIPTION This is used in server-server OAuth token generation .PARAMETER certPath Local or network path to .p12 used to sign the JWT token .PARAMETER certPswd Password to access the private key in the .p12 .PARAMETER iss This is the Google Service account address .PARAMATER scope The API scopes to be included in the request. Space delimited, "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive" .EXAMPLE Get-GOAuthTokenService -scope "https://www.googleapis.com/auth/spreadsheets" -certPath "C:\users\$env:username\Desktop\googleSheets.p12" -certPswd 'notasecret' -iss "serviceAccount@googleProjectName.iam.gserviceaccount.com" #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$certPath, [Parameter(Mandatory)] [string]$certPswd, [Parameter(Mandatory)] [string]$iss, [Parameter(Mandatory)] [string]$scope ) Begin { # build JWT header $headerJSON = [Ordered]@{ alg = "RS256" typ = "JWT" } | ConvertTo-Json -Compress $headerBase64 = ConvertTo-Base64URL -text $headerJSON } Process { # Build claims for JWT $now = (Get-Date).ToUniversalTime() $iat = [Math]::Floor([decimal](Get-Date($now) -UFormat "%s")) $exp = [Math]::Floor([decimal](Get-Date($now.AddMinutes(59)) -UFormat "%s")) $aud = "https://www.googleapis.com/oauth2/v4/token" $claimsJSON = [Ordered]@{ iss = $iss scope = $scope aud = $aud exp = $exp iat = $iat } | ConvertTo-Json -Compress $claimsBase64 = ConvertTo-Base64URL -text $claimsJSON ################# Create JWT # Prep JWT certificate signing $googleCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath, $certPswd,[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable ) $rsaPrivate = $googleCert.PrivateKey $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider $null = $rsa.ImportParameters($rsaPrivate.ExportParameters($true)) # Signature is our base64urlencoded header and claims, delimited by a period. $toSign = [System.Text.Encoding]::UTF8.GetBytes($headerBase64 + "." + $claimsBase64) $signature = ConvertTo-Base64URL -Bytes $rsa.SignData($toSign,"SHA256") ## this needs to be converted back to regular text # Build request $jwt = $headerBase64 + "." + $claimsBase64 + "." + $signature $fields = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion='+$jwt # Fetch token $response = Invoke-RestMethod -Uri "https://www.googleapis.com/oauth2/v4/token" -Method Post -Body $fields -ContentType "application/x-www-form-urlencoded" } End { return $response.access_token } } function Get-GOAuthTokenUser { <# .Synopsis Get Valid OAuth Token. .DESCRIPTION The access token is good for an hour, the refresh token is mostly permanent and can be used to get a new access token without having to reauthenticate. .PARAMETER appKey The google project App Key .PARAMETER appSecret The google project application secret .PARAMETER projectID The google project ID .PARAMETER redirectUri An https project redirect. Can be anything as long as https .PARAMETER refreshToken A refresh token if refreshing .PARAMATER scope The API scopes to be included in the request. Space delimited, "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive" .EXAMPLE Get-GOAuthTokenUser -appKey $appKey -appSecret $appSecret -projectID $projectID -redirectUri $redirectUri -scope $scope .EXAMPLE Get-GOAuthTokenUser -appKey $appKey -appSecret $appSecret -projectID $projectID -redirectUri $redirectUri -scope $scope -refreshToken $refreshToken .NOTES Requires GUI with Internet Explorer to get first token. #> [CmdletBinding()] [OutputType([array])] Param ( [Parameter(Mandatory)] [string]$appKey, [Parameter(Mandatory)] [string]$appSecret, [Parameter(Mandatory)] [string]$projectID, [Parameter(Mandatory)] [string]$redirectUri, [string]$refreshToken, [Parameter(Mandatory)] [string]$scope ) Begin { $requestUri = "https://accounts.google.com/o/oauth2/token" } Process { if(!($refreshToken)) { ### Get the authorization code - IE Popup and user interaction section $auth_string = "https://accounts.google.com/o/oauth2/auth?scope=$scope&response_type=code&redirect_uri=$redirectUri&client_id=$appKey&access_type=offline&approval_prompt=force" $ie = New-Object -comObject InternetExplorer.Application $ie.visible = $true $null = $ie.navigate($auth_string) #Wait for user interaction in IE, manual approval do{Start-Sleep 1}until($ie.LocationURL -match 'code=([^&]*)') $null = $ie.LocationURL -match 'code=([^&]*)' $authorizationCode = $matches[1] $null = $ie.Quit() # exchange the authorization code for a refresh token and access token $requestBody = "code=$authorizationCode&client_id=$appKey&client_secret=$appSecret&grant_type=authorization_code&redirect_uri=$redirectUri" $response = Invoke-RestMethod -Method Post -Uri $requestUri -ContentType "application/x-www-form-urlencoded" -Body $requestBody $props = @{ accessToken = $response.access_token refreshToken = $response.refresh_token } } else { # Exchange the refresh token for new tokens $requestBody = "refresh_token=$refreshToken&client_id=$appKey&client_secret=$appSecret&grant_type=refresh_token" $response = Invoke-RestMethod -Method Post -Uri $requestUri -ContentType "application/x-www-form-urlencoded" -Body $requestBody $props = @{ accessToken = $response.access_token refreshToken = $refreshToken } } } End { return new-object psobject -Property $props } } #endregion #region Permissions for Google Drive files function Get-GFilePermissions { <# .Synopsis Get Permissions on Google Drive File .DESCRIPTION Get Permission ID list on Google File .PARAMETER accessToken OAuth Access Token for authorization. .PARAMETER fileID The fileID to query. This is returned when a new file is created. .EXAMPLE Get-GFilePermissions -fileID 'String of File ID' #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, #[Alias("spreadSheetID")] [Parameter(Mandatory)] [string]$fileID ) Begin { $uri = "https://www.googleapis.com/drive/v3/files/$fileID/permissions" $headers = @{"Authorization"="Bearer $accessToken"} } Process { Invoke-RestMethod -Method Get -Uri $uri -Headers $headers } End{} } function Move-GFile { <# .Synopsis Change parent folder metadata .DESCRIPTION A function to change parent folder metadata of a file. .PARAMETER accessToken OAuth Access Token for authorization. .PARAMETER fileID The fileID to move. .PARAMETER folderID The fileID of the new parent folder. .PARAMETER parentFolderID The fileID of the parentFolder. Optional parameter. root (My Drive) is assumed if not specified. .EXAMPLE MoveGFile -fileID 'String of File ID' -folderID 'String of folder's File ID' #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, #[Alias("spreadSheetID")] [Parameter(Mandatory)] [string]$fileID, [Parameter(Mandatory)] [string]$folderID, [string]$parentFolderID='root' ) Begin { $uriAdd = "https://www.googleapis.com/drive/v3/files/$fileID"+"?removeParents=$parentFolderID" $uriRemove = "https://www.googleapis.com/drive/v3/files/$fileID"+"?addParents=$folderID" $headers = @{"Authorization"="Bearer $accessToken"} } Process { Invoke-RestMethod -Method patch -Uri $uriAdd -Headers $headers Invoke-RestMethod -Method patch -Uri $uriRemove -Headers $headers } End{} } function Remove-GFilePermissions { <# .Synopsis Remove Permissions on Google Drive File .DESCRIPTION Remove Permission ID list on Google File .PARAMETER accessToken OAuth Access Token for authorization. .PARAMETER fileID The fileID to query. This is returned when a new file is created. .PARAMETER permissionsID The permission ID to be removed. See Get-GFilePermissions .EXAMPLE Remove-GFilePermissions -fileID 'String of File ID' -accessToken $accessToken -permissionID 'ID of the permission' .NOTES A successfull removal returns no body data. #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, #[Alias("spreadSheetID")] [Parameter(Mandatory)] [string]$fileID, [Parameter(Mandatory)] [string]$permissionID ) Begin { $uri = "https://www.googleapis.com/drive/v3/files/$fileId/permissions/$permissionId" $headers = @{"Authorization"="Bearer $accessToken"} } Process { Invoke-RestMethod -Method Delete -Uri $uri -Headers $headers } End{} } function Set-GFilePermissions { <# .Synopsis Set Permissions on Google File .DESCRIPTION For use with any google drive file ID .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER emailAddress Email address of the user or group to grant permissions to .PARAMETER fileID The fileID to apply permissions to. .PARAMETER role Role to assign, select from 'writer','reader','commenter' .PARAMETER sendNotificationEmail Boolean response on sending email notification. .PARAMETER type This refers to the emailAddress, is it a user or a group .EXAMPLE set-GFilePermissions -emailAddress 'user@email.com' -role writer -sheetID $sheetID -type user .NOTES Requires drive and drive.file API scope. #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [string]$emailAddress, #[Alias("spreadhSheetID")] [Parameter(Mandatory)] [string]$fileID, [ValidateSet('writer','reader','commenter')] [string]$role = "writer", [ValidateSet($true,$false)] [boolean]$sendNotificationEmail = $false, [ValidateSet('user','group')] [string]$type ) Begin{ $json = @{emailAddress=$emailAddress;type=$type;role=$role} | ConvertTo-Json $ContentType = "application/json" $uri = "https://www.googleapis.com/drive/v3/files/$fileID/permissions/?sendNotificationEmail=$sendNotificationEmail" $headers = @{"Authorization"="Bearer $accessToken"} } Process { Invoke-RestMethod -Method post -Uri $uri -Body $json -ContentType $ContentType -Headers $headers } End{} } function Update-GFilePermissions { <# .Synopsis Update Permissions on Google File .DESCRIPTION Update Permissions on Google File .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER fileID The sheetID to apply permissions to. This is returned when a new sheet is created or use Get-GSheetID .PARAMETER permissionID The permission ID of the entiry with permissions. Sett Get-GFilePermissions to get a lsit .PARAMETER role Role to assign, select from 'writer','reader','commenter','Owner','Organizer' .PARAMETER supportTeamDrives Boolean for TeamDrive Support .PARAMETER transferOwnership Update ownership of file to permission ID .EXAMPLE Update-GFilePermissions -emailAddress 'user@email.com' -role writer -fileID $sheetID -permissionID 'ID of the permission' .NOTES This is usefull for changing ownership. You cannot change ownership from non-domain to domain. #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, #[Alias("spreadSheetID")] [Parameter(Mandatory)] [string]$fileID, [Parameter(Mandatory)] [string]$permissionID, [ValidateSet('writer','reader','commenter','owner','organizer')] [string]$role = "writer", [ValidateSet($true,$false)] [string]$supportTeamDrives = $false, [ValidateSet($true,$false)] [string]$transferOwnership = $false ) Begin{ $json = @{role=$role} | ConvertTo-Json $ContentType = "application/json" $uri = "https://www.googleapis.com/drive/v3/files/$fileID/permissions/$permissionID/?transferOwnership=$transferOwnership" $headers = @{"Authorization"="Bearer $accessToken"} } Process { Invoke-RestMethod -Method Patch -Uri $uri -Body $json -ContentType $ContentType -Headers $headers } End{} } #endregion #region Spread Sheet API Functions function Add-GSheetSheet { <# .Synopsis Add named sheets to an existing spreadSheet file. .DESCRIPTION This function will add a specified sheet name to a google spreadsheet. .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER sheetName Name to apply to new sheet .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Add-GSheetSheet -accessToken $accessToken -sheetName 'NewName' -spreadSheetID $spreadSheetID #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin { $properties = @{requests=@(@{addSheet=@{properties=@{title=$sheetName}}})} |convertto-json -Depth 10 } Process { $suffix = "$spreadSheetID" + ":batchUpdate" $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix" Invoke-RestMethod -Method Post -Uri $uri -Body $properties -ContentType 'application/json' -Headers @{"Authorization"="Bearer $accessToken"} } End{} } function Clear-GSheetSheet { <# .Synopsis Clear all data and leave formatting intact for a sheet from a spreadsheet based on sheetID .DESCRIPTION This function will delete data from a sheet .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER sheetName Name of sheet to clear .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE $pageID = 0 ## using pageID to differentiate from sheetID -- In this case, index 0 is the actual sheetID per the API and will be cleared. $sheetID = ## the id number of the file/spreadsheet clear-gsheet -pageID $pageID -sheetID $sheetID -accessToken $accessToken #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin{} Process { $sheetID = Get-GSheetSheetID -accessToken $accessToken -spreadSheetID $spreadSheetID -sheetName $sheetName $properties = @{requests=@(@{updateCells=@{range=@{sheetId=$sheetID};fields="userEnteredValue"}})} |ConvertTo-Json -Depth 10 $suffix = "$spreadSheetID" + ":batchUpdate" $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix" Invoke-RestMethod -Method Post -Uri $uri -Body $properties -ContentType 'application/json' -Headers @{"Authorization"="Bearer $accessToken"} } End{} } function Get-GSheetData { <# .Synopsis Basic function for retrieving data from a specific Sheet in a Google SpreadSheet. .DESCRIPTION Basic function for retrieving data from a specific Sheet in a Google SpreadSheet. .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER cell Required switch for getting all data, or a subset of cells. .PARAMETER rangeA1 Range in A1 notation https://msdn.microsoft.com/en-us/library/bb211395(v=office.12).aspx. The dimensions of the $values you put in MUST fit within this range .PARAMETER sheetName Name of sheet to data from .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .PARAMETER valueRenderOption How the data is renderd. Switch option from formatted to unformatted data or 'formula' .EXAMPLE Get-GSheetData -accessToken $accessToken -cell 'AllData' -sheetName 'Sheet1' -spreadSheetID $spreadSheetID .EXAMPLE Get-GSheetData -accessToken $accessToken -cell 'Range' -rangeA1 'A0:F77' -sheetName 'Sheet1' -spreadSheetID $spreadSheetID #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [ValidateSet('AllData','Range')] [string]$cell, [string]$rangeA1, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID, [Parameter()] [ValidateSet('FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA')] [string]$valueRenderOption = "FORMATTED_VALUE" ) Begin{} Process { $uri = "https://sheets.googleapis.com/v4/spreadsheets/$spreadSheetID/values/$sheetName" if($cell -eq "Range") { $uri += "!$rangeA1" } $uri += "?valueRenderOption=$valueRenderOption" $result = Invoke-RestMethod -Method GET -Uri $uri -Headers @{"Authorization"="Bearer $accessToken"} # Formatting the returned data $sheet = $result.values $Rows = $sheet.Count $Columns = $sheet[0].Count $HeaderRow = 0 $Header = $sheet[0] foreach ($Row in (($HeaderRow + 1)..($Rows-1))) { $h = [Ordered]@{} foreach ($Column in 0..($Columns-1)) { if ($sheet[0][$Column].Length -gt 0) { $Name = $Header[$Column] $h.$Name = $Sheet[$Row][$Column] } } [PSCustomObject]$h } } End{} } function Get-GSheetSheetID { <# .Synopsis Get ID of specific sheet in a Spreadsheet .DESCRIPTION Get ID of specific sheet in a Spreadsheet .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER sheetName The name of the sheet .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Get-GSheetSheetID -accessToken $accessToken -sheetName 'Sheet1' -spreadSheetID $spreadSheetID #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin{} Process { $spreadSheet = Get-GSheetSpreadSheetProperties -spreadSheetID $spreadSheetID -accessToken $accessToken ($spreadSheet.sheets.properties | Where-Object {$_.title -eq $sheetName}).sheetID } End{} } function Get-GSheetSpreadSheetID { <# .Synopsis Get a spreadsheet ID. .DESCRIPTION Provide a case sensative file name to the function to get back the sheetID used in many other API calls. mimeTymes are split out to only retrieve spreadSheet IDs (no folders or other files) .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER fileName Name of file to retrive ID for. Case sensitive .EXAMPLE Get-GSheetSpreadSheetID -accessToken $accessToken -fileName 'Name of some file' #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [Alias("spreadSheetName")] [string]$fileName ) Begin{} Process { $uri = "https://www.googleapis.com/drive/v3/files?q=name%3D'$fileName'" $spreadSheetID = (((Invoke-RestMethod -Method get -Uri $uri -Headers @{"Authorization"="Bearer $accessToken"}).files) | where {$_.mimetype -eq "application/vnd.google-apps.spreadsheet"}).id # Logic on multiple IDs being returned If ($spreadSheetID.count -eq 0){Write-Warning "There are no files matching the name $fileName"} If ($spreadSheetID.count -gt 1){Write-Warning "There are $($spreadSheetID.Count) files matching the provided name. Please investigate the following sheet IDs to verify which file you want.";return($spreadSheetID)} Else{return($spreadSheetID)} } End{} } function Get-GSheetSpreadSheetProperties { <# .Synopsis Get the properties of a SpreadSheet .DESCRIPTION Get all properties of a SpreadSheet .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .EXAMPLE #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin{} Process { $uri = "https://sheets.googleapis.com/v4/spreadsheets/$spreadSheetID" Invoke-RestMethod -Method GET -Uri $uri -Headers @{"Authorization"="Bearer $accessToken"} } End{} } function Move-GSheetData { <# .Synopsis Move data around between sheets in a spreadSheet. .DESCRIPTION This is a cut and paste between sheets in a spreadsheet. The function will find the row index based on search criteria, and copy/paste between the sheets provided. .PARAMETER accessToken oAuth access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER columnKey Row 0 column value. A key to search for data by. $columnKey = 'Column header' .PARAMETER currentSheetName Name of sheet to be searched, and copied from. .PARAMETER newSheetName Name of destination sheet data is to be written to. .PARAMETER query Value to be queried for in specified column (see columnKey) $query = 'Cell Content' .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Move-GSheetData -accessToken $accessToken -columnKey 'Column Header -destinationSheetName 'New Sheet!' -query 'Cell Content' -sourceSheetName 'Old Sheet' -spreadSheetID $spreadSheetID #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [string]$columnKey, [Parameter(Mandatory)] [string]$destinationSheetName, [Parameter(Mandatory)] [string]$query, [Parameter(Mandatory)] [string]$sourceSheetName, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin{} Process { ## Query all data from sheet $data = Get-GSheetData -spreadSheetID $spreadSheetID -accessToken $accessToken -sheetName $sourceSheetName -cell AllData $destinationData = Get-GSheetData -spreadSheetID $spreadSheetID -accessToken $accessToken -sheetName $destinationSheetName -cell AllData ## Get row query belongs to $Index = (0..($data.count -1) | where {$Data[$_].$columnKey -eq $query}) ## Sanity Check - is this the data? if (-not $Index) { write-Warning "$Query in $columnKey does not exist" return $null } Else { $rowIndex = $index[0] + 2 $startRow = $Index[0] + 1 $destinationRow = ($destinationData).count + 2 $destinationStartRow = ($destinationData).count + 1 } ## Get sheet index ID numbers $allSheetProperties = (Get-GSheetSpreadSheetProperties -spreadSheetID $spreadSheetID -accessToken $accessToken).sheets.properties $srcSheetIndex = ($allSheetProperties | where {$_.title -eq $sourceSheetName}).sheetID $dstSheetIndex = ($allSheetProperties | where {$_.title -eq $destinationSheetName}).sheetID $method = 'POST' $uri = "https://sheets.googleapis.com/v4/spreadsheets/$spreadSheetID"+":batchUpdate" $ContentType = "application/json" ## cutPaste row to row $values = @{"cutPaste"=@{"source"=@{"sheetId"=$srcSheetIndex;"startRowIndex"=$startRow;"endRowIndex"=$rowIndex};"destination"=@{"sheetId"=$dstSheetIndex;"rowIndex"=$destinationRow};"pasteType"="PASTE_NORMAL"}} $JSON = @{"requests"=$values} |ConvertTo-Json -Depth 20 Invoke-RestMethod -Method $method -Uri $uri -Body $json -ContentType $ContentType -Headers @{"Authorization"="Bearer $accessToken"} } End{} } function New-GSheetSpreadSheet { <# .Synopsis Create a new Google SpreadSheet. .DESCRIPTION Create a new Google SpreadSheet. .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER properties Alternatively, the properties that can be set are extensive. Cell color, formatting etc. If you use this you MUST include @{properties=@{title='mY sheet'}} |convertto-json at a minimum. More details at https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create .PARAMETER title Use this in the simplest case to just create a new sheet with a Title/name .EXAMPLE Create-GSheet -properties $properties -accessToken $accessToken .EXAMPLE create-GSheet -title 'My sheet' -accessToken $accessToken #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(ParameterSetName='properties')] [array]$properties, [Parameter(ParameterSetName='title')] [string]$title ) Begin { If (!$properties) { $properties = @{properties=@{title=$title}} |convertto-json } $uri = "https://sheets.googleapis.com/v4/spreadsheets" } Process { Invoke-RestMethod -Method Post -Uri $uri -Body $properties -ContentType "application/json" -Headers @{"Authorization"="Bearer $accessToken"} } End{} } function Remove-GSheetSheet { <# .Synopsis Removes a sheet from a spreadsheet based on sheetID .DESCRIPTION This function will delete an individual sheet. .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER sheetName Name of sheet to delete .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Remove-GSheetSheet -accessToken $accessToken -sheetName 'Name to delete' -spreadSheetID $spreadSheetID #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin{} Process { $sheetID = Get-GSheetSheetID -accessToken $accessToken -sheetName $sheetName -spreadSheetID $spreadSheetID $properties = @{requests=@(@{deleteSheet=@{sheetId=$sheetID}})} |convertto-json -Depth 10 $suffix = "$spreadSheetID" + ":batchUpdate" $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix" $data = Invoke-RestMethod -Method Post -Uri $uri -Body $properties -ContentType "application/json" -Headers @{"Authorization"="Bearer $accessToken"} } End { return([array]$data) } } function Remove-GSheetSpreadSheet { <# .Synopsis Delete a SpreadSheet .DESCRIPTION Uses the google File Drive API to delete a file. .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER fileID ID for the target file/spreadSheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Remove-GSheetSpreadSheet -accessToken $accessToken -spreadSheetID $spreadSheetID #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, #[Alias("spreadSheetID")] [Parameter(Mandatory)] [string]$fileID ) Begin{} Process { $uri = "https://www.googleapis.com/drive/v3/files/$fileID" Invoke-RestMethod -Method Delete -Uri $uri -Headers @{"Authorization"="Bearer $accessToken"} } End{} } #region Remove-GSheetSheetRowColumn function Remove-GSheetSheetRowColumn { <# .Synopsis Remove row(s) or column(s) .DESCRIPTION Remove row(s) or column(s) .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER startIndex Index of row or column to start deleting .PARAMETER endIndex Index of row or column to stop deleting .PARAMETER dimension Remove Rows or Columns .PARAMETER sheetName Name of sheet in spreadSheet .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Remove-GSheetSheetRowColumn -accessToken $accessToken -sheetName "Sheet1" -spreadSheetID $spreadSheetID -dimension ROWS -startIndex 5 -endIndex 10 #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [int]$startIndex, [Parameter(Mandatory)] [int]$endIndex, [Parameter(Mandatory)] [ValidateSet("COLUMNS", "ROWS")] [string]$dimension, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin { $sheetID = Get-GSheetSheetID -accessToken $accessToken -spreadSheetID $spreadSheetID -sheetName $sheetName if ($startIndex -eq $endIndex){$endIndex++} } Process { $request = @{"deleteDimension" = @{"range" = @{"sheetId" = $sheetID; "dimension" = $dimension; "startIndex" = $startIndex; "endIndex" = $endIndex}}} $json = @{requests=@($request)} | ConvertTo-Json -Depth 20 $suffix = "$spreadSheetID" + ":batchUpdate" $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix" write-verbose -Message $json Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType "application/json" -Headers @{"Authorization"="Bearer $accessToken"} } End{} } #endregion function Set-GSheetColumnWidth { <# .Synopsis Set the width of a column on a sheet .DESCRIPTION This function calls the bulk update method to set column dimensions to 'autoResize'. .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER numberOfColumns An optional parameter to specify how many columns to autosize. Default to 26 .PARAMETER sheetName Name of sheet in spreadSheet .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Set-GSheetColumnWidth -spreadSheetID $id -sheetName 'Sheet1' -accessToken $token -numberOfColumns ($property.count) #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [string]$numberOfColumns = '26', [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID ) Begin { $sheetID = Get-GSheetSheetID -accessToken $accessToken -spreadSheetID $spreadSheetID -sheetName $sheetName $json = @{requests=@(@{autoResizeDimensions=@{dimensions=@{sheetId=$sheetID;dimension='COLUMNS';startIndex='0';endIndex='26'}}})} |ConvertTo-Json -Depth 20 $suffix = "$spreadSheetID" + ":batchUpdate" $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix" } Process { Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType "application/json" -Headers @{"Authorization"="Bearer $accessToken"} } End{} } function Set-GSheetData { <# .Synopsis Set values in sheet in specific cell locations or append data to a sheet .DESCRIPTION Set json data values on a sheet in specific cell locations or append data to a sheet .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER append Switch option to append data. See rangeA1 if not appending .PARAMETER rangeA1 Range in A1 notation https://msdn.microsoft.com/en-us/library/bb211395(v=office.12).aspx . The dimensions of the $values you put in MUST fit within this range .PARAMETER sheetName Name of sheet to set data in .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .PARAMETER valueInputOption Default to RAW. Optionally, you can specify if you want it processed as a formula and so forth. .PARAMETER values The values to write to the sheet. This should be an array list. Each list array represents one ROW on the sheet. .EXAMPLE Set-GSheetData -accessToken $accessToken -rangeA1 'A1:B2' -sheetName 'My Sheet' -spreadSheetID $spreadSheetID -values @(@("a","b"),@("c","D")) .EXAMPLE Set-GSheetData -accessToken $accessToken -append 'Append'-sheetName 'My Sheet' -spreadSheetID $spreadSheetID -values $arrayValues #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(ParameterSetName='Append')] [switch]$append, [Parameter(ParameterSetName='set')] [string]$rangeA1, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID, [string]$valueInputOption = 'RAW', [Parameter(Mandatory)] [System.Collections.ArrayList]$values ) Begin { if ($append) { $method = 'POST' $uri = "https://sheets.googleapis.com/v4/spreadsheets/$spreadSheetID/values/$sheetName"+":append?valueInputOption=$valueInputOption" } else { $method = 'PUT' $uri = "https://sheets.googleapis.com/v4/spreadsheets/$spreadSheetID/values/$sheetName!$rangeA1"+"?valueInputOption=$valueInputOption" } } Process { $json = @{values=$values} | ConvertTo-Json Invoke-RestMethod -Method $method -Uri $uri -Body $json -ContentType "application/json" -Headers @{"Authorization"="Bearer $accessToken"} } End{} } #region Set-GSheetDropDownList function Set-GSheetDropDownList { <# .Synopsis Set Drop Down List Data validation on cells in a column .DESCRIPTION Set Drop Down List Data validation on cells in a column .PARAMETER accessToken access token used for authentication. Get from Get-GOAuthTokenUser or Get-GOAuthTokenService .PARAMETER columnIndex Index of column to update .PARAMETER startRowIndex Index of row to start updating .PARAMETER endRowIndex Index of last row to update .PARAMETER values List of string values that the use can chose from in an array. Google API only takes strings .PARAMETER inputMessage A message to show the user when adding data to the cell. .PARAMETER showCustomUi True if the UI should be customized based on the kind of condition. If true, $values will show a dropdown. .PARAMETER sheetName Name of sheet in spreadSheet .PARAMETER spreadSheetID ID for the target Spreadsheet. This is returned when a new sheet is created or use Get-GSheetSpreadSheetID .EXAMPLE Set-GSheetDropDownList -accessToken $accessToken -startRowIndex 1 -endRowIndex 10 -columnIndex 9 -sheetName 'Sheet1' -spreadSheetID $spreadSheetID -inputMessage "Must be one of 'Public','Private Restricted','Private, Highly-Restricted'" -values @('Public','Private Restricted','Private, Highly-Restricted') #> [CmdletBinding()] Param ( [Parameter(Mandatory)] [string]$accessToken, [Parameter(Mandatory)] [int]$startRowIndex, [Parameter(Mandatory)] [int]$endRowIndex, [Parameter(Mandatory)] [int]$columnIndex, [Parameter(Mandatory)] [string]$sheetName, [Parameter(Mandatory)] [string]$spreadSheetID, [Parameter(Mandatory)] [string[]]$values, [string]$inputMessage, [boolean]$showCustomUi=$true ) Begin { $sheetID = Get-GSheetSheetID -accessToken $accessToken -spreadSheetID $spreadSheetID -sheetName $sheetName $valueList = [Collections.ArrayList]@() foreach ($value in $values){$valueList.Add(@{userEnteredValue=$value})} $validation = @{ setDataValidation = @{ range=@{sheetId = $sheetID;startRowIndex=$startRowIndex;endRowIndex=$endRowIndex;startColumnIndex=$columnIndex;endColumnIndex=($columnIndex+1)}; rule=@{ condition = @{ type= 'ONE_OF_LIST'; values=$valueList }; inputMessage=$inputMessage;strict=$true;showCustomUi=$showCustomUi } } } $json = @{requests=@($validation)} | ConvertTo-Json -Depth 20 $suffix = "$spreadSheetID" + ":batchUpdate" $uri = "https://sheets.googleapis.com/v4/spreadsheets/$suffix" $json $uri } Process { Invoke-RestMethod -Method Post -Uri $uri -Body $json -ContentType "application/json" -Headers @{"Authorization"="Bearer $accessToken"} } End{} } #endregion Export-ModuleMember -Function * # SIG # Begin signature block # MIIaxgYJKoZIhvcNAQcCoIIatzCCGrMCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUoYHDVxhsjZiG0ip20otDoDgR # DKKgghW3MIIEmTCCA4GgAwIBAgIPFojwOSVeY45pFDkH5jMLMA0GCSqGSIb3DQEB # BQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQg # TGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNV # BAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTEdMBsGA1UEAxMUVVROLVVTRVJG # aXJzdC1PYmplY3QwHhcNMTUxMjMxMDAwMDAwWhcNMTkwNzA5MTg0MDM2WjCBhDEL # MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE # BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKjAoBgNVBAMT # IUNPTU9ETyBTSEEtMSBUaW1lIFN0YW1waW5nIFNpZ25lcjCCASIwDQYJKoZIhvcN # AQEBBQADggEPADCCAQoCggEBAOnpPd/XNwjJHjiyUlNCbSLxscQGBGue/YJ0UEN9 # xqC7H075AnEmse9D2IOMSPznD5d6muuc3qajDjscRBh1jnilF2n+SRik4rtcTv6O # KlR6UPDV9syR55l51955lNeWM/4Og74iv2MWLKPdKBuvPavql9LxvwQQ5z1IRf0f # aGXBf1mZacAiMQxibqdcZQEhsGPEIhgn7ub80gA9Ry6ouIZWXQTcExclbhzfRA8V # zbfbpVd2Qm8AaIKZ0uPB3vCLlFdM7AiQIiHOIiuYDELmQpOUmJPv/QbZP7xbm1Q8 # ILHuatZHesWrgOkwmt7xpD9VTQoJNIp1KdJprZcPUL/4ygkCAwEAAaOB9DCB8TAf # BgNVHSMEGDAWgBTa7WR0FJwUPKvdmam9WyhNizzJ2DAdBgNVHQ4EFgQUjmstM2v0 # M6eTsxOapeAK9xI1aogwDgYDVR0PAQH/BAQDAgbAMAwGA1UdEwEB/wQCMAAwFgYD # VR0lAQH/BAwwCgYIKwYBBQUHAwgwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2Ny # bC51c2VydHJ1c3QuY29tL1VUTi1VU0VSRmlyc3QtT2JqZWN0LmNybDA1BggrBgEF # BQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20w # DQYJKoZIhvcNAQEFBQADggEBALozJEBAjHzbWJ+zYJiy9cAx/usfblD2CuDk5oGt # Joei3/2z2vRz8wD7KRuJGxU+22tSkyvErDmB1zxnV5o5NuAoCJrjOU+biQl/e8Vh # f1mJMiUKaq4aPvCiJ6i2w7iH9xYESEE9XNjsn00gMQTZZaHtzWkHUxY93TYCCojr # QOUGMAu4Fkvc77xVCf/GPhIudrPczkLv+XZX4bcKBUCYWJpdcRaTcYxlgepv84n3 # +3OttOe/2Y5vqgtPJfO44dXddZhogfiqwNGAwsTEOYnB9smebNd0+dmX+E/CmgrN # Xo/4GengpZ/E8JIh5i15Jcki+cPwOoRXrToW9GOUEB1d0MYwggV3MIIEX6ADAgEC # AhAT6ihwW/Ts7Qw2YwmAYUM2MA0GCSqGSIb3DQEBDAUAMG8xCzAJBgNVBAYTAlNF # MRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJu # YWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJv # b3QwHhcNMDAwNTMwMTA0ODM4WhcNMjAwNTMwMTA0ODM4WjCBiDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4w # HAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVz # dCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzO # iZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwW # IJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YU # VD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1da # t//O+T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+ # UzeQc0PzMsNT79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/z # JSZrM233bkf6c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLa # qUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxb # gtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9A # qURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ # eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwID # AQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8DveAky1QaMB0GA1Ud # DgQWBBRTeb9aqitKz1SA4dibwJ3ysgNmyzAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T # AQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwRAYDVR0fBD0wOzA5oDegNYYz # aHR0cDovL2NybC51c2VydHJ1c3QuY29tL0FkZFRydXN0RXh0ZXJuYWxDQVJvb3Qu # Y3JsMDUGCCsGAQUFBwEBBCkwJzAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNl # cnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAQEAk2X2N4OVD17Dghwf1nfnPIrA # qgnw6Qsm8eDCanWhx3nJuVJgyCkSDvCtA9YJxHbf5aaBladG2oJXqZWSxbaPAyJs # M3fBezIXbgfOWhRBOgUkG/YUBjuoJSQOu8wqdd25cEE/fNBjNiEHH0b/YKSR4We8 # 3h9+GRTJY2eR6mcHa7SPi8BuQ33DoYBssh68U4V93JChpLwt70ZyVzUFv7tGu25t # N5m2/yOSkcZuQPiPKVbqX9VfFFOs8E9h6vcizKdWC+K4NB8m2XsZBWg/ujzUOAai # 0+aPDuO0cW1AQsWEtECVK/RloEh59h2BY5adT3Xg+HzkjqnR8q2Ks4zHIc3C7zCC # BawwggSUoAMCAQICEHJNXiAT1cKRQFXzfFSJVHEwDQYJKoZIhvcNAQELBQAwfDEL # MAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1JMRIwEAYDVQQHEwlBbm4gQXJib3IxEjAQ # BgNVBAoTCUludGVybmV0MjERMA8GA1UECxMISW5Db21tb24xJTAjBgNVBAMTHElu # Q29tbW9uIFJTQSBDb2RlIFNpZ25pbmcgQ0EwHhcNMTcxMjE0MDAwMDAwWhcNMjAx # MjEzMjM1OTU5WjCByzELMAkGA1UEBhMCVVMxDjAMBgNVBBEMBTU1NDU1MRIwEAYD # VQQIDAlNaW5uZXNvdGExFDASBgNVBAcMC01pbm5lYXBvbGlzMRgwFgYDVQQJDA8x # MDAgVW5pb24gU3QgU0UxIDAeBgNVBAoMF1VuaXZlcnNpdHkgb2YgTWlubmVzb3Rh # MSQwIgYDVQQLDBtDb21wdXRlciBhbmQgRGV2aWNlIFN1cHBvcnQxIDAeBgNVBAMM # F1VuaXZlcnNpdHkgb2YgTWlubmVzb3RhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A # MIIBCgKCAQEAwk6kLE9u+tWv0JUkIJSn5pWfa09g6cqFLucCXomNj9NYj8t+JfPn # a3gC6LHv3OQAUDHOoC5H+8N3ea7qVGYIiwPRHzXOGqG/tVaiU5s5hG3vBhfRX8W1 # /2g4/hpgeXUzrxYn/2c5SOGGy0MU1ZJyUSFEdsjXHEV7HXK4qmFGV9RJxtiLZH1q # UldCglxcj7zw0QnUdG6oAxpwTCeVp057/WXbnIR8a0gPse+y/new5+CBUGTAvrw6 # K2BrJQVsdIIVn/q+BbcZxh9PpeZfTtsi6lgkvy0bUWtl5sSpd75+hvw4Sl3HAaWZ # toWN7LPmbDbbVRO2Arv4doh4Chod4wJ5xQIDAQABo4IB2DCCAdQwHwYDVR0jBBgw # FoAUrjUjF///Bj2cUOCMJGUzHnAQiKIwHQYDVR0OBBYEFF4LEhElVUvT8n5txOJS # NAczooSAMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoG # CCsGAQUFBwMDMBEGCWCGSAGG+EIBAQQEAwIEEDBmBgNVHSAEXzBdMFsGDCsGAQQB # riMBBAMCATBLMEkGCCsGAQUFBwIBFj1odHRwczovL3d3dy5pbmNvbW1vbi5vcmcv # Y2VydC9yZXBvc2l0b3J5L2Nwc19jb2RlX3NpZ25pbmcucGRmMEkGA1UdHwRCMEAw # PqA8oDqGOGh0dHA6Ly9jcmwuaW5jb21tb24tcnNhLm9yZy9JbkNvbW1vblJTQUNv # ZGVTaWduaW5nQ0EuY3JsMH4GCCsGAQUFBwEBBHIwcDBEBggrBgEFBQcwAoY4aHR0 # cDovL2NydC5pbmNvbW1vbi1yc2Eub3JnL0luQ29tbW9uUlNBQ29kZVNpZ25pbmdD # QS5jcnQwKAYIKwYBBQUHMAGGHGh0dHA6Ly9vY3NwLmluY29tbW9uLXJzYS5vcmcw # GQYDVR0RBBIwEIEOb2l0bXB0QHVtbi5lZHUwDQYJKoZIhvcNAQELBQADggEBAENR # lesMKmBaZ0g68lttYEMtaPiz+DaNpOlXBs1gH66aghB1aP6iiRJcFVasGLUVFncd # G1xbw503LTrBUc5PECMVDVF7KKCfHA1OeFV9vOWyvdVgbe3paDy1sj4CADO2D0gn # xcGiZoFhEZiBkTvSsj4S3GXZEvoFHJxJLw2kvdLnzy0gH/b/b/yblwA1fKXw4loc # UpDM6qTwM7SiKgkQ5W7/280EYu8BI6c8rpiJmqM1tZLcpswuavB00T52Y+ZZmz3t # MMVgFHn9pFFltYr3s3bEek7I6pU8unISbiyQzxqhIUKaBi8hy8LgoY5UnGjX5jHs # IvINzms+JX5Ity02sL0wggXrMIID06ADAgECAhBl4eLj1d5QRYXzJiSABeLUMA0G # CSqGSIb3DQEBDQUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKTmV3IEplcnNl # eTEUMBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1Qg # TmV0d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0aWZpY2F0aW9uIEF1 # dGhvcml0eTAeFw0xNDA5MTkwMDAwMDBaFw0yNDA5MTgyMzU5NTlaMHwxCzAJBgNV # BAYTAlVTMQswCQYDVQQIEwJNSTESMBAGA1UEBxMJQW5uIEFyYm9yMRIwEAYDVQQK # EwlJbnRlcm5ldDIxETAPBgNVBAsTCEluQ29tbW9uMSUwIwYDVQQDExxJbkNvbW1v # biBSU0EgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB # CgKCAQEAwKAvix56u2p1rPg+3KO6OSLK86N25L99MCfmutOYMlYjXAaGlw2A6O2i # gTXrC/Zefqk+aHP9ndRnec6q6mi3GdscdjpZh11emcehsriphHMMzKuHRhxqx+85 # Jb6n3dosNXA2HSIuIDvd4xwOPzSf5X3+VYBbBnyCV4RV8zj78gw2qblessWBRyN9 # EoGgwAEoPgP5OJejrQLyAmj91QGr9dVRTVDTFyJG5XMY4DrkN3dRyJ59UopPgNwm # ucBMyvxR+hAJEXpXKnPE4CEqbMJUvRw+g/hbqSzx+tt4z9mJmm2j/w2nP35MViPW # Cb7hpR2LB8W/499Yqu+kr4LLBfgKCQIDAQABo4IBWjCCAVYwHwYDVR0jBBgwFoAU # U3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFK41Ixf//wY9nFDgjCRlMx5w # EIiiMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQM # MAoGCCsGAQUFBwMDMBEGA1UdIAQKMAgwBgYEVR0gADBQBgNVHR8ESTBHMEWgQ6BB # hj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNh # dGlvbkF1dGhvcml0eS5jcmwwdgYIKwYBBQUHAQEEajBoMD8GCCsGAQUFBzAChjNo # dHRwOi8vY3J0LnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNBQWRkVHJ1c3RDQS5j # cnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZI # hvcNAQENBQADggIBAEYstn9qTiVmvZxqpqrQnr0Prk41/PA4J8HHnQTJgjTbhuET # 98GWjTBEE9I17Xn3V1yTphJXbat5l8EmZN/JXMvDNqJtkyOh26owAmvquMCF1pKi # QWyuDDllxR9MECp6xF4wnH1Mcs4WeLOrQPy+C5kWE5gg/7K6c9G1VNwLkl/po9OR # PljxKKeFhPg9+Ti3JzHIxW7LdyljffccWiuNFR51/BJHAZIqUDw3LsrdYWzgg4x0 # 6tgMvOEf0nITelpFTxqVvMtJhnOfZbpdXZQ5o1TspxfTEVOQAsp05HUNCXyhznlV # Lr0JaNkM7edgk59zmdTbSGdMq8Ztuu6VyrivOlMSPWmay5MjvwTzuNorbwBv0DL+ # 7cyZBp7NYZou+DoGd1lFZN0jU5IsQKgm3+00pnnJ67crdFwfz/8bq3MhTiKOWEb0 # 4FT3OZVp+jzvaChHWLQ8gbCORgClaZq1H3aqI7JeRkWEEEp6Tv4WAVsr/i7LoXU7 # 2gOb8CAzPFqwI4Excdrxp0I4OXbECHlDqU4sTInqwlMwofmxeO4u94196qIqJQl+ # 8Sykl06VktqMux84Iw3ZQLH08J8LaJ+WDUycc4OjY61I7FGxCDkbSQf3npXeRFm0 # IBn8GiW+TRDk6J2XJFLWEtVZmhboFlBLoUlqHUCKu0QOhU/+AEOqnY98j2zRMYIE # eTCCBHUCAQEwgZAwfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1JMRIwEAYDVQQH # EwlBbm4gQXJib3IxEjAQBgNVBAoTCUludGVybmV0MjERMA8GA1UECxMISW5Db21t # b24xJTAjBgNVBAMTHEluQ29tbW9uIFJTQSBDb2RlIFNpZ25pbmcgQ0ECEHJNXiAT # 1cKRQFXzfFSJVHEwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAIoAKAAKEC # gAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwG # CisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFLQcxlicVOKHOIeQRLmyGi6Kx0CH # MA0GCSqGSIb3DQEBAQUABIIBAAFM8fWi3iGpoD5T/T5LIPSJAmxONjsL3Hmz+Mle # 8rPmfaMiG9RM2E0ax5WNw1c+zF5W2EG5kVBWUzKr6jQwINIIOmScsZPUPGjUyWzM # zWTvHlUlFWpaZPXv3OtFaUw7pgKxqHuhT5sl7w+rLIvfmMvZbXavD+0pNy3rfNFs # 9f3hwr32I3lytlxUagsUJlV1gd33E6mJe3LmUoZxv2Bhd84anwe3qViZXgQ8fdgX # j4DcMHfqtkm0rRSBagaW9wt75WDOchR3RX0+heM0zdClsqDUF6yxQkvDk0a/6JJ4 # /0qD+fX74b+5rymswYdWAEc8jyRwz9xLnPLI9dqX1I035i2hggJDMIICPwYJKoZI # hvcNAQkGMYICMDCCAiwCAQEwgakwgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJV # VDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJV # U1QgTmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR0w # GwYDVQQDExRVVE4tVVNFUkZpcnN0LU9iamVjdAIPFojwOSVeY45pFDkH5jMLMAkG # BSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ # BTEPFw0xODA1MjEyMTIyNTNaMCMGCSqGSIb3DQEJBDEWBBRlK7icZVxty6YEK4hL # ScoOpYddXTANBgkqhkiG9w0BAQEFAASCAQB++cUY8J29XzuPpR5ysrqM5gmtZSYD # mGVmGFWSjh9z3+cc2pA06q/HczKCRuh6RimsV2BxjXtXtpn3fMuqwuNO1C/O3sXI # FCA2+wqxcXiMaKUu+ePm7wUsw9PCPQw9tNStspL+jUu+KfAcqjBtQzWj2pv7RdM+ # JFIDI7EXAm7WB8dE04O+dIdJJhTt0niSc2o2LnpIko6WeiG621g0kQSO5IoHvx3y # RFJ4Ocmmd3y8JaUwZszrtQIQmwjFGrqATdxkbkRo7b5l0ex4mUdplVXkUPsZ3uMO # kenkH/wE9TjTAg6yiWaM3QnXipaF1GavnWuakGLCTJ7mUjvSSHgT6gl8 # SIG # End signature block |