I have a function which gets data from all APs in a given store.
Function Get-AllAP {
Write-Verbose "Function Start: Get-AllAP"
Write-Host "Getting all Access Points in Store $Store .."
Write-Host " "
Write-Verbose "Getting all APs for Store $Store"
$storeApReq = "https://cpist/webacs/api/v3/data/AccessPointDetails.json?.group=$Store"
Write-Verbose "Making request to $storeApReq"
$Global:apIdListReq = Invoke-RestMethod -uri $storeApReq -method Get -ContentType 'application/json' -headers #{ Authorization = $auth }
$Global:apIdList = $apIdListReq.queryResponse.entityId
$Global:apIdCount = $apIdListReq.queryResponse."#count"
Write-Verbose "Found $siteAPCount APs in Sites Database. $apIdCount out of $siteAPCount APs found."
Write-Verbose "Response Received: $apIdList"
$Global:apIdURL = $apIdListReq.queryResponse.entityId
$Global:apURLs = $apIdListReq.queryResponse.entityId | ForEach-Object -MemberName '#url'
Write-Verbose "Looping through APs."
$Global:apLoop = ForEach($apURL in $apURLs) {
$apFullReq = Invoke-RestMethod -uri $apURL'.json' -Method Get -ContentType 'application/json' -headers #{ Authorization = $auth }
Write-Verbose "Response: $apFullReq"
$Global:allApData = $apFullReq.queryResponse.entity.accessPointDetailsDTO
## Format ALL AP data
$apStatus =$allApData.status
$apName = $allApData.name
$apPing = $allApData.reachabilitystatus
## Format data
Write-Host $apName
Write-Verbose $apStatus
Write-Verbose $apPing
## Output data
Write-Host " "
Write-Host "AP Name: $apName"
Write-Host "AP Status: $apStatus"
Write-Host "AP Ping: $apPing"
}
## Clear
## Give option to reset AP
$doReset = Read-Host "Type 'reset' to reset all Access Points in Store $Store"
IF($doReset-eq 'reset') {
Manage-APResetStore
} else {
Repeat
}
After this function is run, the user is given a choice to Repeat or reset using Manage-APResetStore which is as follows;
Function Manage-APResetStore {
Write-Verbose "Function started: Manage-APResetStore"
## Create our batch job
ForEach($apName in $allAPData) {
Write-Host $apName
}
## Send our job
}
This is what it outputs currently, which shows the full JSON for $allAPData rather than $apName.
VERBOSE: Function started: Manage-APResetStore
Establishing SSH connection to Cisco Controller dc1flexwlc02
#{#displayName=16162527426; #id=16162527426; adminStatus=ENABLE; apType=AP3500I; cdpNeighbors=; clientCount=3; clientC
ount_2_4GHz=0; clientCount_5GHz=3; ethernetMac=50:57:a8:a1:7c:38; ipAddress=10.0.126.69; locationHierarchy=Dicks Sport
ing Goods > 0026 > 1st floor; macAddress=5c:50:15:1c:43:30; mapLocation=default location; model=AIR-CAP3502I-A-K9; nam
e=0026AP5; reachabilityStatus=REACHABLE; serialNumber=FTX1611E55H; softwareVersion=8.5.131.0; status=CLEARED; type=Uni
fiedAp; unifiedApInfo=; upTime=68916194}
VERBOSE: Function start: Check-DevMode
VERBOSE: Function Start: Execute-Application
What the Manage-APResetStore function is meant to do is send a list of "reset" commands to the controller for every AP that exists in that store. So what I want to do, is make a list out of my ForEach loop that I can use to generate a batch job to send to the controller.
To be able to loop through again after the data is formatted, I created a new array with just the apName objects so we can use it in other functions.
Create the array outside the loop:
## Create array object
$apArray = New-Object System.Collections.ArrayList
Add the apName object to the array:
## Format ALL AP data
$apStatus =$allApData.status
$apName = $allApData.name
$apPing = $allApData.reachabilitystatus
## Format data
Write-Output $apName
Write-Output $apStatus
Write-Output $apPing
## Output data to array
$apArray.Add($apName)
}
Write-Host $apArray
## Clear
## Give option to reset AP
$doReset = Read-Host "Type 'reset' to reset all Access Points in Store $Store"
Using the array:
## Loop through our commands
ForEach($apName in $apArray) {
$stream.WriteLine("show loginsessions $apName")
sleep 1
$stream.WriteLine('y')
sleep 2
$stream.Read()
Write-Host "$apName has been reset"
}
Try replacing your ForEach section as follows:
ForEach ($apName in $allApData.ApName)
This will mean that $apName within the loop is just the apName property, and not the entire part of $allApData.
Related
I am trying to write a PowerShell script that allows me to update all the names of our devices in Intune [430ish devices] to reflect our asset tags. When they were imported into our tenant, they were given the serialNumber of the device as their deviceName. All permissions for the API have been applied:
API Permissions:
Device Read
Device Read all
DeviceManagementApps.ReadAll
DeviceManagementApps.ReadWriteAll
DeviceManagementConfiguration.ReadAll
DeviceManagementConfiguration.ReadWriteAll
DeviceManagementManagedDevices.PrivilegedOperations.All
DeviceManagementManagedDevices.ReadAll
DeviceManagementManagedDevices.ReadWriteAll
DeviceManagementRBAC.ReadAll
DeviceManagementRBAC.ReadWriteALL
DeviceManagementServiceConfig.ReadAll
DeviceManagementServiceConfig.ReadWriteAll
User Read
This is the code as far as I can get it, but I am still getting the following error [I apologise for ugly or poorly formatted code, I have had no formal training, all learnt using google-fu!]:
# Setting variables for connecting to the MS API
$ApplicationID = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
$TenantDomainName = "contoso.com"
$AccessSecret = Read-Host "Enter Secret"
# Connect to MSGraph command to run
Connect-MSGraph
# Setting the body of the json
$Body = #{
Grant_Type = "client_credentials"
Scope = "https://graph.microsoft.com/.default"
client_Id = $ApplicationID
Client_Secret = $AccessSecret
}
# Authenticating the connection to MSGraph
$ConnectGraph = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantDomainName/oauth2/v2.0/token" `
-Method POST -Body $Body
$token = $ConnectGraph.access_token
# Importing the CSV of device information
$csvfile = "C:\<Path to file>"
Import-Csv $csvfile | ForEach-Object {
$serialNumber = $_.serialNumber;
$tag = $_.tag;
$deviceId = $serialNumber
Write-Host "Renaming machine from: $deviceID to: $tag" -ForegroundColor Cyan
# Getting the Device from the CSV and then putting it into MSGraph compatible Json
$DeviceToRename = Get-IntuneManagedDevice -Filter ("serialNumber eq '$serialNumber'")
Foreach ($Device in $DeviceToRename) {
$Resource = "deviceManagement/managedDevices('$DeviceId')/setDeviceName"
$graphApiVersion = "Beta"
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/executeAction"
#This JSON format doesnt work
# $JSONPayload = #"
# { <NEW>
# "body": <NEW>
# {
# action: "setDeviceName",
# actionName: "setDeviceName",
# deviceName: "$tag",
# realaction: "setDeviceName",
# restartNow: false
# }
# } <NEW>
#"#
#Don't know if this works properly either?
$JSONPayload = #"
{
"#odata.type": "#microsoft.graph.managedDevice",
"actionName": "setDeviceName",
"deviceName": "$tag"
}
"#
# Writing out to check if this is working correctly
Write-Host $JSONPayload
# Converting $JSONPayload to an actual workable JSON
$convertedJSON = ConvertTo-Json $JSONPayload
try {
Invoke-MSGraphRequest -Url $uri -HttpMethod PATCH -Body $JSONPayload -ContentType "application/Json" -Verbose
} catch {
# Dig into the exception to get the Response details.
Write-Host "StatusCode:" "$_.Exception.Response.StatusCode.value__"
Write-Host "StatusDescription:" "$_.Exception.Response.StatusDescription"
Write-Host "StatusCode2:" "$_.ErrorDetails.Message"
}
}
}
Error response:
StatusCode: A parameter cannot be found that matches parameter name 'Body'..Exception.Response.StatusCode.value__
StatusDescription: A parameter cannot be found that matches parameter name 'Body'..Exception.Response.StatusDescription
StatusCode2: A parameter cannot be found that matches parameter name 'Body'..ErrorDetails.Message
Thanks
Tom
I had similar problems some months ago manipulating intune devices from an powershell runbook over graph. In my case the json body was the problem. I had to define the body first as hashtable and then convert it to json. Try something like this:
# JSONPayload as hashtable instead of string
$JSONPayload = #{
"#odata.type" = "#microsoft.graph.managedDevice"
"actionName" = "setDeviceName"
"deviceName" = "$tag"
}
# Writing out to check if this is working correctly
$JSONPayload
# Converting $JSONPayload to an actual workable JSON
$convertedJSON = $JSONPayload | ConvertTo-Json
And then pass the $convertedJSON to your graph call as body:
Invoke-MSGraphRequest -Url $uri -HttpMethod POST -Content $convertedJSON -Verbose
EDIT:
You are calling the endpoint /deviceManagement/managedDevices/executeAction with the http method PATCH. According to this ms docs article you have to call the endpoint with the http method POST.
I've tried invoke-restmethod, new-object and many other methods to achieve what I'm trying to do. Here are the latest two iterations:
$req = Invoke-WebRequest -uri $scripturl -OutFile "$($scriptpath)\fls.core.ps1"
Write-Host "StatusCode:" $req.StatusCode
$req = Invoke-WebRequest -uri $scripturl -OutFile "$($scriptpath)\fls.core.ps1" | Select-Object -Expand StatusCode
Write-Host "StatusCode:" $req
Basically I'm attempting to download another PowerShell script and execute it. So obviously it needs to be synchronous. I also need the status so I can determine if it updated or not.
Here is pseudo code for what I'm trying to accomplish:
try {
download file
} catch {
output error
if (local copy exists) {
log warning that local copy is being used
} else {
log error could not download and no local copy available
exit script
}
}
run script (only after downloading new one if available)
Here is my current code in full:
$param1=$args[0]
if ($param1 -eq "-d" -or $param1 -eq "-D") {
$isDev = $true
}
#todo: Move to config file
$logpath = "c:\company\logs\loginscript"
$scriptpath = "c:\company\scripts\"
$scripturl = "http://downloads.company.com/fls.core.ps1"
$logfile="$(Get-Date -Format "yyyy-MM-dd hhmmss").log"
Function log($message) {
Write-Output "[$(Get-Date -Format "yyyy-MM-dd hhmmss")] $message" | Out-file "$($logpath)\$($logfile)" -append
if ($isDev) { Write-Host "[$(Get-Date -Format "yyyy-MM-dd hhmmss")] $message" }
}
Function createFolder($path) {
if (-!(Test-Path $path)) { New-Item -Type Directory -Path $path }
}
function updateScripts() {
try {
$req = Invoke-WebRequest -uri $scripturl -OutFile "$($scriptpath)\fls.core.ps1"
Write-Host "StatusCode:" $req.StatusCode
} catch {
Write-Host "StatusCode:" $req.StatusCode
if ($req.StatusCode -eq 404) {
log "WARNING: Script not found at $scripturl"
} else {
log "ERROR: Script download error: $req.StatusCode"
}
if (Test-Path "$($scriptpath)\fls.core.ps1") {
log "WARNING: Using local script"
} else {
log "ERROR: Unable to update script and no local script found. Exiting."
exit
}
}
}
#----------------------------------------------#
#---- MAIN CODE BLOCK -------------------------#
#----------------------------------------------#
createFolder $logpath
createFolder $scriptpath
#update scripts
updateScripts
#execute core loginscript
& $scriptpath/fls.core.ps1
$req.StatusCode appears to be null.
Invoke-WebRequest reports errors as statement-terminating errors, which means that no assignment to variable $req (in statement $req = Invoke-WebRequest ...) takes place in case an error occurs.
Instead, unfortunately, if an error occurs, the response object[1] must be gleaned from the [ErrorRecord] instance representing the error, which is available via $Error[0] after the fact, or via $_ in the catch block of a try { ... } catch { ... } statement (adapted from this answer):
try {
Invoke-WebRequest -Uri $scripturl -OutFile "$scriptpath\fls.core.ps1"
} catch [Microsoft.PowerShell.Commands.HttpResponseException] {
# Get the status code...
$statuscode = $_.Exception.Response.StatusCode
# ... and work with it.
# if ($statusCode -eq 404) { ...
} catch {
# Unexpected error, re-throw
throw
}
Strictly speaking, $_.Exception.Response.StatusCode returns a value from an enumeration type, System.Net.HttpStatusCode, not an [int] value, but you can use it like an integer. To return an integer to begin with, append .Value__ or cast to [int].
Note that Invoke-WebRequest is always synchronous; if you download a file (successfully), the call won't return until the download is completed.
[1] As the linked answer explains, the response object contained in the error record is of a different type than the one that Invoke-WebRequest returns in case of success (which requires -PassThru if -OutFile is also specified): The error record's .Exception.Response property contains a System.Net.Http.HttpResponseMessage instance, whereas Invoke-WebRequest returns an instance (derived from) Microsoft.PowerShell.Commands.WebResponseObject, which incorporates an instance of the former type, in its .BaseResponse property.
I can't find any clue about it except a deprecated article using tweetpic which is now closed.
Any alternative that works in Powershell ?
Update: my question is not about if there is a Twitter API of course I know there is, but as it is not trivial to use like this Powershell Guy who is stuck https://twittercommunity.com/t/media-upload-doesnt-work-powershell/93861 I'm looking for a module.
Credit to Adam Bertram from https://www.adamtheautomator.com/twitter-module-powershell/.
Disclaimer: this only allows to post tweets and DM.
You can try using the PSM1 module below, but you need to create your own Twitter application on apps.twitter.com and generate an access token under the API keys section of the application. Once you do so, I recommend copying/pasting your API key, API secret, access token and access token secret as default parameters under the Get-OAuthAuthorization function.
<#
===========================================================================
Created on: 8/31/2014 3:11 PM
Created by: Adam Bertram
Filename: MyTwitter.psm1
------------------------------------------------------------------------
===========================================================================
#>
function Get-OAuthAuthorization {
<#
.SYNOPSIS
This function is used to setup all the appropriate security stuff needed to issue
API calls against Twitter's API. It has been tested with v1.1 of the API. It currently
includes support only for sending tweets from a single user account and to send DMs from
a single user account.
.EXAMPLE
Get-OAuthAuthorization -DmMessage 'hello' -HttpEndPoint 'https://api.twitter.com/1.1/direct_messages/new.json' -Username adam
This example gets the authorization string needed in the HTTP POST method to send a direct
message with the text 'hello' to the user 'adam'.
.EXAMPLE
Get-OAuthAuthorization -TweetMessage 'hello' -HttpEndPoint 'https://api.twitter.com/1.1/statuses/update.json'
This example gets the authorization string needed in the HTTP POST method to send out a tweet.
.PARAMETER HttpEndPoint
This is the URI that you must use to issue calls to the API.
.PARAMETER TweetMessage
Use this parameter if you're sending a tweet. This is the tweet's text.
.PARAMETER DmMessage
If you're sending a DM to someone, this is the DM's text.
.PARAMETER Username
If you're sending a DM to someone, this is the username you'll be sending to.
.PARAMETER ApiKey
The API key for the Twitter application you previously setup.
.PARAMETER ApiSecret
The API secret key for the Twitter application you previously setup.
.PARAMETER AccessToken
The access token that you generated within your Twitter application.
.PARAMETER
The access token secret that you generated within your Twitter application.
#>
[CmdletBinding(DefaultParameterSetName = 'None')]
[OutputType('System.Management.Automation.PSCustomObject')]
param (
[Parameter(Mandatory)]
[string]$HttpEndPoint,
[Parameter(Mandatory, ParameterSetName = 'NewTweet')]
[string]$TweetMessage,
[Parameter(Mandatory, ParameterSetName = 'DM')]
[string]$DmMessage,
[Parameter(Mandatory, ParameterSetName = 'DM')]
[string]$Username,
[Parameter()]
[string]$ApiKey = '2R3aJXohHmSABPaiQGaeprny7',
[Parameter()]
[string]$ApiSecret = '',
[Parameter()]
[string]$AccessToken = '',
[Parameter()]
[string]$AccessTokenSecret = ''
)
begin {
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
Set-StrictMode -Version Latest
try {
[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
[Reflection.Assembly]::LoadWithPartialName("System.Net") | Out-Null
} catch {
Write-Error $_.Exception.Message
}
}
process {
try {
## Generate a random 32-byte string. I'm using the current time (in seconds) and appending 5 chars to the end to get to 32 bytes
## Base64 allows for an '=' but Twitter does not. If this is found, replace it with some alphanumeric character
$OauthNonce = [System.Convert]::ToBase64String(([System.Text.Encoding]::ASCII.GetBytes("$([System.DateTime]::Now.Ticks.ToString())12345"))).Replace('=', 'g')
Write-Verbose "Generated Oauth none string '$OauthNonce'"
## Find the total seconds since 1/1/1970 (epoch time)
$EpochTimeNow = [System.DateTime]::UtcNow - [System.DateTime]::ParseExact("01/01/1970", "dd/MM/yyyy", $null)
Write-Verbose "Generated epoch time '$EpochTimeNow'"
$OauthTimestamp = [System.Convert]::ToInt64($EpochTimeNow.TotalSeconds).ToString();
Write-Verbose "Generated Oauth timestamp '$OauthTimestamp'"
## Build the signature
$SignatureBase = "$([System.Uri]::EscapeDataString($HttpEndPoint))&"
$SignatureParams = #{
'oauth_consumer_key' = $ApiKey;
'oauth_nonce' = $OauthNonce;
'oauth_signature_method' = 'HMAC-SHA1';
'oauth_timestamp' = $OauthTimestamp;
'oauth_token' = $AccessToken;
'oauth_version' = '1.0';
}
if ($TweetMessage) {
$SignatureParams.status = $TweetMessage
} elseif ($DmMessage) {
$SignatureParams.screen_name = $Username
$SignatureParams.text = $DmMessage
}
## Create a string called $SignatureBase that joins all URL encoded 'Key=Value' elements with a &
## Remove the URL encoded & at the end and prepend the necessary 'POST&' verb to the front
$SignatureParams.GetEnumerator() | sort name | foreach {
Write-Verbose "Adding '$([System.Uri]::EscapeDataString(`"$($_.Key)=$($_.Value)&`"))' to signature string"
$SignatureBase += [System.Uri]::EscapeDataString("$($_.Key)=$($_.Value)&".Replace(',','%2C').Replace('!','%21'))
}
$SignatureBase = $SignatureBase.TrimEnd('%26')
$SignatureBase = 'POST&' + $SignatureBase
Write-Verbose "Base signature generated '$SignatureBase'"
## Create the hashed string from the base signature
$SignatureKey = [System.Uri]::EscapeDataString($ApiSecret) + "&" + [System.Uri]::EscapeDataString($AccessTokenSecret);
$hmacsha1 = new-object System.Security.Cryptography.HMACSHA1;
$hmacsha1.Key = [System.Text.Encoding]::ASCII.GetBytes($SignatureKey);
$OauthSignature = [System.Convert]::ToBase64String($hmacsha1.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($SignatureBase)));
Write-Verbose "Using signature '$OauthSignature'"
## Build the authorization headers using most of the signature headers elements. This is joining all of the 'Key=Value' elements again
## and only URL encoding the Values this time while including non-URL encoded double quotes around each value
$AuthorizationParams = $SignatureParams
$AuthorizationParams.Add('oauth_signature', $OauthSignature)
## Remove any API call-specific params from the authorization params
$AuthorizationParams.Remove('status')
$AuthorizationParams.Remove('text')
$AuthorizationParams.Remove('screen_name')
$AuthorizationString = 'OAuth '
$AuthorizationParams.GetEnumerator() | sort name | foreach { $AuthorizationString += $_.Key + '="' + [System.Uri]::EscapeDataString($_.Value) + '", ' }
$AuthorizationString = $AuthorizationString.TrimEnd(', ')
Write-Verbose "Using authorization string '$AuthorizationString'"
$AuthorizationString
} catch {
Write-Error $_.Exception.Message
}
}
}
function Send-Tweet {
<#
.SYNOPSIS
This sends a tweet under a username.
.EXAMPLE
Send-Tweet -Message 'hello, world'
This example will send a tweet with the text 'hello, world'.
.PARAMETER Message
The text of the tweet.
#>
[CmdletBinding()]
[OutputType('System.Management.Automation.PSCustomObject')]
param (
[Parameter(Mandatory)]
[ValidateLength(1, 140)]
[string]$Message
)
process {
$HttpEndPoint = 'https://api.twitter.com/1.1/statuses/update.json'
$AuthorizationString = Get-OAuthAuthorization -TweetMessage $Message -HttpEndPoint $HttpEndPoint
## Convert the message to a Byte array
#$Body = [System.Text.Encoding]::ASCII.GetBytes("status=$Message");
$Body = "status=$Message"
Write-Verbose "Using POST body '$Body'"
Invoke-RestMethod -URI $HttpEndPoint -Method Post -Body $Body -Headers #{ 'Authorization' = $AuthorizationString } -ContentType "application/x-www-form-urlencoded"
}
}
function Send-TwitterDm {
<#
.SYNOPSIS
This sends a DM to another Twitter user. NOTE: You can only send up to
250 DMs in a 24 hour period.
.EXAMPLE
Send-TwitterDm -Message 'hello, Adam' -Username 'adam','bill'
This sends a DM with the text 'hello, Adam' to the username 'adam' and 'bill'
.PARAMETER Message
The text of the DM.
.PARAMETER Username
The username(s) you'd like to send the DM to.
#>
[CmdletBinding()]
[OutputType('System.Management.Automation.PSCustomObject')]
param (
[Parameter(Mandatory)]
[ValidateLength(1, 140)]
[string]$Message,
[Parameter(Mandatory)]
[string[]]$Username
)
process {
$HttpEndPoint = 'https://api.twitter.com/1.1/direct_messages/new.json'
## Convert the message to a Byte array
#$Message = [System.Uri]::EscapeDataString($Message)
foreach ($User in $Username) {
$AuthorizationString = Get-OAuthAuthorization -DmMessage $Message -HttpEndPoint $HttpEndPoint -Username $User -Verbose
$User = [System.Uri]::EscapeDataString($User)
$Body ="text=$Message&screen_name=$User"
Write-Verbose "Using POST body '$Body'"
Invoke-RestMethod -URI $HttpEndPoint -Method Post -Body $Body -Headers #{ 'Authorization' = $AuthorizationString } -ContentType "application/x-www-form-urlencoded"
}
}
}
Export-ModuleMember Send-Tweet
Export-ModuleMember Send-TwitterDm
Here is a script I used to check status of my URL whether it is working or not. I need to write a script in such a way that it accepts all the URLs present in a text file which is placed in local drive and displays their information.
[string] $url = 'http://mywebsite.net'
function CheckForStatus($url) {
try {
[net.httpWebRequest] $req = [net.webRequest]::create($url)
$req.Method = "HEAD"
[net.httpWebResponse] $res = $req.getResponse()
if ($res.StatusCode -eq "200") {
write-host "`nSite $url is up (Return code: $($res.StatusCode) -
$([int] $res.StatusCode))`n" -ForegroundColor green
}
else {
write-host "`nSite $url is not available (Return code:
$($res.StatusCode) - $([int] $res.StatusCode))`n" -ForegroundColor red
}
} catch {
write-host "`nSite $url is having some DNS issues`n" -ForegroundColor
red
}
}
CheckForStatus $url
Use the Get-Content cmdlet and pipe it for processing each line.
Get-Content "some_file" | % { CheckForStatus($_) }
I am trying to delete all Version History files in SharePoint Online through PowerShell. My research has provided plenty of examples on how to do this in SharePoint 2010 and 2013 but not SharePoint Online. The files reside in Document Libraries. The script below seemed promising for my task but I have been unable to modify it to work for SharePoint Online. What changes would be necessary to make it work for SharePoint Online?
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
# get site
$site = new-object Microsoft.SharePoint.SPSite("http://xxx.sharepoint.com")
# loop through webs
foreach ($web in $site.AllWebs)
{
write-host $web.url
# loop through all lists in web
foreach ($list in $web.Lists)
{
# examine if BaseType of list is NOT a Document Library
if (($list.BaseType -eq "DocumentLibrary") -and ($list.EnableVersioning))
{
# Delete all version history
foreach ($item in $list.Items)
{
# work with the file object as we're in a document library
$file = $item.File
# delete all versions
$file.Versions.DeleteAll()
}
}
}
}
$web.Dispose();
$site.Dispose();
The code below combines PowerShell and CSOM. It worked for me. You can delete all Versions or adjust the Versions to be deleted using the Counter in the Loop.
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client")
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime")
$password = Read-Host -Prompt "Enter password" -AsSecureString
$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials("userID#yourtenant.com", $password)
$siteUrl = "https://yourtenant.sharepoint.com/sites/yoursitecollection"
$context = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$context.Credentials = $credentials
$fileUrl = "/sites/yoursitecollection/path_to_file/filename";
$versions = $context.Web.GetFileByServerRelativeUrl($fileUrl).Versions;
$context.Load($versions)
$context.ExecuteQuery()
for($i=2;$i -lt $versions.Count-2; $i++)
{
$versions[$i].DeleteObject()
$context.ExecuteQuery()
}
For older versions of SharePoint you may need to mimic the browser. Essentially, you would request the version history page content, get the REQUESTDIGEST and VIEWSTATE, and perform POST requests to specially crafted URL and request body for each unneeded version using the obtained values.
E.g.:
$httpResponse = Invoke-WebRequest -Uri "$baseUri/_layouts/15/Versions.aspx?list=$listId&ID=$id" -UseDefaultCredentials
$httpResponse.Forms["aspnetForm"].Fields["__VIEWSTATE"]
$httpResponse.Forms["aspnetForm"].Fields["__REQUESTDIGEST"].Replace(" ","+")
...
$httpResponse = Invoke-WebRequest -UseDefaultCredentials -MaximumRedirection 0 -ErrorAction SilentlyContinue -Method "POST" `
-Uri "$baseUri/_layouts/15/versions.aspx?list=$listId&ID=$($_.ID)&col=Number&order=d&op=Delete&ver=$($_.Version)" `
-ContentType "application/x-www-form-urlencoded" -Body ("MSOWebPartPage_PostbackSource=&MSOTlPn_SelectedWpId=&MSOTlPn_View=0&MSOTlPn_ShowSettings=False&MSOGallery_SelectedLibrary=&MSOGallery_FilterString="+
"&MSOTlPn_Button=none&__EVENTTARGET=&__EVENTARGUMENT=&MSOSPWebPartManager_DisplayModeName=Browse&MSOSPWebPartManager_ExitingDesignMode=false&MSOWebPartPage_Shared=&MSOLayout_LayoutChanges=&MSOLayout_InDesignMode=&MSOSPWebPartManager_OldDisplayModeName=Browse&MSOSPWebPartManager_StartWebPartEditingName=false&MSOSPWebPartManager_EndWebPartEditing=false&_maintainWorkspaceScrollPosition=0"+
"&__REQUESTDIGEST=$digest&__VIEWSTATE=$([System.Net.WebUtility]::UrlEncode($viewstate))&__VIEWSTATEGENERATOR=01175E75&__SCROLLPOSITIONX=0&__SCROLLPOSITIONY=0"+
"&__EVENTVALIDATION=$([System.Net.WebUtility]::UrlEncode($ev))")
if ($httpResponse.StatusCode -eq 302) { #success
write-host "v" -NoNewline -foregroundcolor DarkGreen
$vCount++
} else { #some error
write-host "x" -NoNewline -foregroundcolor Red
if ($httpResponse.Forms[0].Action.StartsWith("error")) {
write-host $httpResponse.Content.Substring($httpResponse.Content.IndexOf("ctl00_PlaceHolderMain_LabelMessage")+36,300).Split("<")[0] -ForegroundColor Magenta
}
...
}
Note that it is unnecessary to follow the 302 redirect after the POST.