Powershell to list all pages with their layout across the rootweb and all subwebs? - sharepoint

I need to be able to create a report of all existing pages and their page layout.
I have the following powershell script but even using Recursive its only returning me the ones from the root web.
filter Get-PublishingPages {
$pubweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($_)
$query = new-object Microsoft.SharePoint.SPQuery
$query.ViewAttributes = "Scope='Recursive'"
$pubweb.GetPublishingPages($query)
}
$url="https://xxxxl.com"
get-spweb $url | Get-PublishingPages | select Uri, Title, #{Name='PageLayout';Expression={$_.Layout.ServerRelativeUrl}}

This worked for me.
filter Get-PublishingPages {
$pubweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($_)
$query = new-object Microsoft.SharePoint.SPQuery
$query.ViewAttributes = "Scope='Recursive'"
$pubweb.GetPublishingPages($query)
}
$str = "http://yourdomain.com" // your URL
if($str -eq $null )
{
Write-Host “Enter a valid URL”
return
}
$site = Get-SPSite -Identity $str
if($site -eq $null)
{
Write-Host “Enter a valid URL”
return
}
$allweb = $site.Allwebs
foreach($web in $allweb )
{
$web | Get-PublishingPages | select Uri, Title, #{Name=’PageLayout’;Expression={$_.Layout.ServerRelativeUrl}}| Format-List
}

Bit of a shot in the dark here, but have you tried setting the scope to RecursiveAll instead of just Recursive? My understanding was that Recursive only hit all files in a folder while RecursiveAll gets all subfolders as well.
Reference: http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spviewscope.aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-1

Related

Running a powershell script using multiple threads

I'm trying to execute a powershell script from within another powershell script. My current script runs fine, but I'd like to try and speed it up if possible.
What my script does is import a list of contacts into Each users Contacts folder through EWS.
In my powershell script the script that handles the importing I call it like this (ImportContacts is a Function without any arguments):
. $rootPath\ImportContacts\ImportContacts.ps1
ImportContacts
When I run it normally, as I mentioned above everything works fine, I just would like to speed it up. I tried following some examples of implementing runspacepool in Powershell to take advantage of using multiple threads, but it doesn't seem to be working properly for me. I'm sure it's a silly syntax error, but I have this currently:
Measure-Command{
$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = #()
ImportContacts | Foreach-Object {
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$PowerShell.AddScript({ImportContacts})
$Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false)
{
Start-Sleep 1
}}
This seems to do the job, but I can't tell a difference with the speed.
EDIT 5-15-21
To better assist with the question, here is how I retrieve the user data. I have a file I call called "ExportedContacts.ps1"
It's called like so:
$Users = & $rootPath\ExportContacts\ExportContacts.ps1
The contents of the file is this
$Users = Get-ADUser -Filter * -Properties extensionAttribute2, middlename, mobile, OfficePhone, GivenName, Surname, DisplayName, EmailAddress, Title, Company, Department, thumbnailPhoto | Where-Object {($_.extensionAttribute2 -like "DynamicDistro") -AND (($_.Mobile -ne $NULL) -OR ($_.OfficePhone -ne $NULL))} | Select-Object #{Name="First Name";Expression={$_.GivenName}},#{Name="Last Name";Expression={$_.Surname}},#{Name="Display Name";Expression={$_.DisplayName}},#{Name="Job Title";Expression={$_.Title}},#{Name="Company";Expression={$_.Company}},#{Name="Department";Expression={$_.Department}},#{Name="Mobile Phone";Expression={$_.Mobile}},#{Name="Home Phone";Expression={$_.OfficePhone}}, #{Name="Middle Name";Expression={$_.MiddleName}}, #{Name="E-mail Address";Expression={$_.EmailAddress}}, thumbnailPhoto | Sort-Object "Last Name"
return $Users
Then I import those contacts, similarly to how I mentioned above. The content of the import is as follows:
Function ImportContacts
{
Write-Host "Importing Contacts. This can take several minutes."
#******************************************************************
foreach ($ContactItem in $Users)
{
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
$ExchangeContact = New-Object Microsoft.Exchange.WebServices.Data.Contact($service);
$ExchangeContact.NickName = ('{0} {1}' -f $ContactItem."First Name", $ContactItem."Last Name"). Trim()
$ExchangeContact.DisplayName = $ExchangeContact.NickName;
$ExchangeContact.FileAs = $ExchangeContact.NickName;
$ExchangeContact.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1] = $ContactItem."E-mail Address";
$ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::HomePhone] = $ContactItem."Home Phone";
$ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::MobilePhone] = $ContactItem."Mobile Phone";
$ExchangeContact.Department = $ContactItem."Department";
$ExchangeContact.CompanyName = $ContactItem."Company";
$ExchangeContact.JobTitle = $ContactItem."Job Title";
$ExchangeContact.MiddleName = $ContactItem."Middle Name";
# Save the contact
$ExchangeContact.Save($ContactsFolder.Id);
}
}
I am also including the File that creates the contacts folder I specify and also deletes existing contacts (if the folder already exists), so that each import is a clean and updated import. I'd be curious if there is a faster way to clean the existing items?
Function CreateContactsFolder
{
Write-Host "Creating Contacts Folder and Cleaning up stale items. This can take a couple minutes."
Try
{
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
$RootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
$RootFolder.Load()
#Check to see if they have a contacts folder that we want
$FolderView = new-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)
$ContactsFolderSearch = $RootFolder.FindFolders($FolderView) | Where-Object {$_.DisplayName -eq $FolderName}
if($ContactsFolderSearch)
{
$ContactsFolder = [Microsoft.Exchange.WebServices.Data.ContactsFolder]::Bind($service,$ContactsFolderSearch.Id);
#If folder exists, connect to it. Clear existing Contacts, and reupload new (UPDATED) Contact Info
Write-Host "Folder alreads exists. We will remove all contacts under this folder."
# Attempt to empty the target folder up to 10 times.
$tries = 0
$max_tries = 0
while ($tries -lt 2)
{
try
{
$tries++
$ErrorActionPreference='Stop'
$ContactsFolder.Empty([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, $true)
$tries++
}
catch
{
$ErrorActionPreference='SilentlyContinue'
$rnd = Get-Random -Minimum 1 -Maximum 10
Start-Sleep -Seconds $rnd
$tries = $tries - 1
$max_tries++
if ($max_tries -gt 100)
{
Write-Host "Error; Cannot empty the target folder; `t$EmailAddress"
}
}
}
}
else
{
#Contact Folder doesn't exist. Let's create it
try
{
Write-Host "Creating new Contacts Folder called $FolderName"
$ContactsFolder = New-Object Microsoft.Exchange.WebServices.Data.ContactsFolder($service);
$ContactsFolder.DisplayName = $FolderName
$ContactsFolder.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
}
catch
{
Write-Host "Error; Cannot create the target folder; `t$EmailAddress"
}
}
return $ContactsFolder
}
Catch
{
Write-Host "Couldn't connect to the user's mailbox. Make sure the admin account you're using to connect to has App Impersonization permissions"
Write-Host "Check this link for more info: https://help.bittitan.com/hc/en-us/articles/115008098447-The-account-does-not-have-permission-to-impersonate-the-requested-user"
}
}

UserLastLogon -Export

Hi I'm trying to export a list of AD users based on "Last Logon"
I've scripted using base powershell however I'd be interested if anyone can find a solution using "AzureAD to Powershell" commands.
I've gotten as far as getting the list however I cannot export it to any file type because of how it generates through the loop.
End result I'm looking for is to be able to organize the data to see which users have been inactive?
Import-Module ActiveDirectory
function Get-ADUserLastLogon([string]$userName) {
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$time = 0
foreach($dc in $dcs) {
$hostname = $dc.HostName
$user = Get-ADUser $userName | Get-ADObject -Properties lastLogon
if($user.LastLogon -gt $time) {
$time = $user.LastLogon
}
}
$dt = [DateTime]::FromFileTime($time)
Write-Host $username "last logged on at:" $dt
}
$unames = Get-ADUser -Filter 'ObjectClass -eq "User"' | Select -Expand SamAccountName
foreach ($uname in $unames) { Get-ADUserLastLogon($uname); }
In Azure AD, we can get all user Sign-ins records on Azure Portal or using Azure AD PowerShell.
If you are looking for a way by PowerShell to export Azure AD users last login list with user account status (enabled or not), just try the code below:
Connect-AzureAD
$AllUsers = Get-AzureADUser -All $true
$AllSiginLogs = Get-AzureADAuditSignInLogs -All $true
$results = #()
foreach($user in $AllUsers){
$LoginRecord = $AllSiginLogs | Where-Object{ $_.UserId -eq $user.ObjectId } | Sort-Object CreatedDateTime -Descending
if($LoginRecord.Count -gt 0){
$lastLogin = $LoginRecord[0].CreatedDateTime
}else{
$lastLogin = 'no login record'
}
$item = #{
userUPN=$user.UserPrincipalName
userDisplayName = $user.DisplayName
lastLogin = $lastLogin
accountEnabled = $user.AccountEnabled
}
$results += New-Object PSObject -Property $item
}
$results | export-csv -Path d:\result.csv -NoTypeInformation
export to .csv file Result:
There is one thing that you should know, for different Azure AD service tier, the time that Azure AD keep these data is different, details see here.

Default OOBX EditForm disappeared for a SharePoint document library

Default OOBX EditForm disappeared for a SharePoint document library. We tried to create a new edit form in SharePoint designer but got an error that Could not save the List Changes to Server.
Then tried another approach, go to All files/Documentlibrary/Forms -- here got list view threshold error. We have more than 10 thousands items in the library, can't open up forms folder.
Please advise how to get the default edit form back.
You'll need to do it through PowerShell:
$webpartTemplate = #"
<WebPart xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/WebPart/v2">
<Assembly>Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.ListFormWebPart</TypeName>
<ListName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">{{{LIST_ID}}}</ListName>
<ListId xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">{{LIST_ID}}</ListId>
<PageType xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">{{PAGE_TYPE}}</PageType>
<FormType xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">{{FORM_TYPE}}</FormType>
<ControlMode xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">{{CONTROL_MODE}}</ControlMode>
<ViewFlag xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">1048576</ViewFlag>
<ViewFlags xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">Default</ViewFlags>
<ListItemId xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">0</ListItemId>
</WebPart>
"#
Function Create-DefaultListForm
{
param(
[parameter(Mandatory=$true)]$List,
[parameter(Mandatory=$true)][string]$FormUrl,
[parameter(Mandatory=$true)][ValidateSet("Display", "Edit", "New")]$FormType
)
begin { }
process
{
$webpartXml = $webpartTemplate -replace "{{LIST_ID}}", $List.Id.ToString()
switch ($FormType)
{
"Display" {
$webpartXml = $webpartXml -replace "{{PAGE_TYPE}}", "PAGE_DISPLAYFORM"
$webpartXml = $webpartXml -replace "{{FORM_TYPE}}", "4"
$webpartXml = $webpartXml -replace "{{CONTROL_MODE}}", "Display"
break;
}
"Edit" {
$webpartXml = $webpartXml -replace "{{PAGE_TYPE}}", "PAGE_EDITFORM"
$webpartXml = $webpartXml -replace "{{FORM_TYPE}}", "6"
$webpartXml = $webpartXml -replace "{{CONTROL_MODE}}", "Edit"
break;
}
"New" {
$webpartXml = $webpartXml -replace "{{PAGE_TYPE}}", "PAGE_NEWFORM"
$webpartXml = $webpartXml -replace "{{FORM_TYPE}}", "8"
$webpartXml = $webpartXml -replace "{{CONTROL_MODE}}", "New"
break;
}
}
try
{
#Check if form page already exists
$listPages = Get-PnPProperty -ClientObject $List.RootFolder -Property Files
$formPage = $listPages | Where-Object { $_.ServerRelativeUrl.ToLower() -eq $FormUrl.ToLower() }
if ($null -eq $formPage) {
Write-Output " [Creating Form Page] $FormUrl"
#Create Form
Remove-PnPFile -ServerRelativeUrl $FormUrl -Force
$formPage = $List.RootFolder.Files.AddTemplateFile($FormUrl, [Microsoft.SharePoint.Client.TemplateFileType]::FormPage)
}
else {
#Form page exists, check if form is recognized by list (i.e. form page has a form webpart on it)
$listForms = Get-PnPProperty -ClientObject $List -Property Forms
if ($null -ne $listForms -and $listForms.Count -gt 0) {
$existingForm = $list.Forms | Where-Object { $_.ServerRelativeUrl.ToLower() -eq $FormUrl.ToLower() }
if ($null -ne $existingForm) {
Write-Warning " [Form Already Exists] $FormUrl"
return;
}
}
}
Write-Output " [Adding Form Webpart] $FormUrl"
#Get Webpart Manager for Form
$wpm = $formPage.GetLimitedWebPartManager([Microsoft.SharePoint.Client.WebParts.PersonalizationScope]::Shared)
#Import Webpart on page
$wp = $wpm.ImportWebPart($webpartXml)
#Add webpart to Form
$wpm.AddWebPart($wp.WebPart, "Main", 1) | Out-Null
#Execute changes
$List.Context.ExecuteQuery()
}
catch
{
Write-Error "Error creating form $FormType at $FormUrl. Error: $($_.Exception)"
}
}
end { }
}
Use the script something like this. If you only need to restore the EditForm, then you obviously you can skip the Upload, New, and DispForms below:
Connect-PnPOnline -Url $problemSiteUrl -UseWebLogin
$list = Get-PnPList $problemlistTitle
$listUrl = $list.RootFolder.ServerRelativeUrl
# Handle Document Library Types
if ($list.BaseType -eq [Microsoft.SharePoint.Client.BaseType]::DocumentLibrary) {
Write-Host " > Processing Library: $listUrl"
Create-DefaultListForm -List $list -FormUrl "$listUrl/Forms/Upload.aspx" -FormType New
Create-DefaultListForm -List $list -FormUrl "$listUrl/Forms/DispForm.aspx" -FormType Display
Create-DefaultListForm -List $list -FormUrl "$listUrl/Forms/EditForm.aspx" -FormType Edit
}
# Handle Generic List Types
else {
Write-Host " > Processing List: $listUrl"
Create-DefaultListForm -List $list -FormUrl "$listUrl/NewForm.aspx" -FormType New
Create-DefaultListForm -List $list -FormUrl "$listUrl/DispForm.aspx" -FormType Display
Create-DefaultListForm -List $list -FormUrl "$listUrl/EditForm.aspx" -FormType Edit
}
If you only need the Edit form, use the Create-DefaultListForm function like this:
Connect-PnPOnline -Url 'UrlOfProblemSite' -UseWebLogin
$list = Get-PnPList 'TitleOfTheProblemLibraryHere'
$listUrl = $list.RootFolder.ServerRelativeUrl
Create-DefaultListForm -List $list -FormUrl "$listUrl/Forms/EditForm.aspx" -FormType Edit

Display all sites and bindings in PowerShell

I am documenting all the sites and binding related to the site from the IIS. Is there an easy way to get this list through a PowerShell script rather than manually typing looking at IIS?
I want the output to be something like this:
Site Bindings
TestSite www.hello.com
www.test.com
JonDoeSite www.johndoe.site
Try this:
Import-Module Webadministration
Get-ChildItem -Path IIS:\Sites
It should return something that looks like this:
Name ID State Physical Path Bindings
---- -- ----- ------------- --------
ChristophersWeb 22 Started C:\temp http *:8080:ChristophersWebsite.ChDom.com
From here you can refine results, but be careful. A pipe to the select statement will not give you what you need. Based on your requirements I would build a custom object or hashtable.
Try something like this to get the format you wanted:
Get-WebBinding | % {
$name = $_.ItemXPath -replace '(?:.*?)name=''([^'']*)(?:.*)', '$1'
New-Object psobject -Property #{
Name = $name
Binding = $_.bindinginformation.Split(":")[-1]
}
} | Group-Object -Property Name |
Format-Table Name, #{n="Bindings";e={$_.Group.Binding -join "`n"}} -Wrap
If you just want to list all the sites (ie. to find a binding)
Change the working directory to "C:\Windows\system32\inetsrv"
cd c:\Windows\system32\inetsrv
Next run "appcmd list sites" (plural) and output to a file. e.g c:\IISSiteBindings.txt
appcmd list sites > c:\IISSiteBindings.txt
Now open with notepad from your command prompt.
notepad c:\IISSiteBindings.txt
The most easy way as I saw:
Foreach ($Site in get-website) { Foreach ($Bind in $Site.bindings.collection) {[pscustomobject]#{name=$Site.name;Protocol=$Bind.Protocol;Bindings=$Bind.BindingInformation}}}
Try this
function DisplayLocalSites
{
try{
Set-ExecutionPolicy unrestricted
$list = #()
foreach ($webapp in get-childitem IIS:\Sites\)
{
$name = "IIS:\Sites\" + $webapp.name
$item = #{}
$item.WebAppName = $webapp.name
foreach($Bind in $webapp.Bindings.collection)
{
$item.SiteUrl = $Bind.Protocol +'://'+ $Bind.BindingInformation.Split(":")[-1]
}
$obj = New-Object PSObject -Property $item
$list += $obj
}
$list | Format-Table -a -Property "WebAppName","SiteUrl"
$list | Out-File -filepath C:\websites.txt
Set-ExecutionPolicy restricted
}
catch
{
$ExceptionMessage = "Error in Line: " + $_.Exception.Line + ". " + $_.Exception.GetType().FullName + ": " + $_.Exception.Message + " Stacktrace: " + $_.Exception.StackTrace
$ExceptionMessage
}
}
function Get-ADDWebBindings {
param([string]$Name="*",[switch]$http,[switch]$https)
try {
if (-not (Get-Module WebAdministration)) { Import-Module WebAdministration }
Get-WebBinding | ForEach-Object { $_.ItemXPath -replace '(?:.*?)name=''([^'']*)(?:.*)', '$1' } | Sort | Get-Unique | Where-Object {$_ -like $Name} | ForEach-Object {
$n=$_
Get-WebBinding | Where-Object { ($_.ItemXPath -replace '(?:.*?)name=''([^'']*)(?:.*)', '$1') -like $n } | ForEach-Object {
if ($http -or $https) {
if ( ($http -and ($_.protocol -like "http")) -or ($https -and ($_.protocol -like "https")) ) {
New-Object psobject -Property #{Name = $n;Protocol=$_.protocol;Binding = $_.bindinginformation}
}
} else {
New-Object psobject -Property #{Name = $n;Protocol=$_.protocol;Binding = $_.bindinginformation}
}
}
}
}
catch {
$false
}
}
I found this page because I needed to migrate a site with many many bindings to a new server. I used some of the code here to generate the powershell script below to add the bindings to the new server. Sharing in case it is useful to someone else:
Import-Module WebAdministration
$Websites = Get-ChildItem IIS:\Sites
$site = $Websites | Where-object { $_.Name -eq 'site-name-in-iis-here' }
$Binding = $Site.bindings
[string]$BindingInfo = $Binding.Collection
[string[]]$Bindings = $BindingInfo.Split(" ")
$i = 0
$header = ""
Do{
[string[]]$Bindings2 = $Bindings[($i+1)].Split(":")
Write-Output ("New-WebBinding -Name `"site-name-in-iis-here`" -IPAddress " + $Bindings2[0] + " -Port " + $Bindings2[1] + " -HostHeader `"" + $Bindings2[2] + "`"")
$i=$i+2
} while ($i -lt ($bindings.count))
It generates records that look like this:
New-WebBinding -Name "site-name-in-iis-here" -IPAddress "*" -Port 80 -HostHeader www.aaa.com
I found this question because I wanted to generate a web page with links to all the websites running on my IIS instance. I used Alexander Shapkin's answer to come up with the following to generate a bunch of links.
$hostname = "localhost"
Foreach ($Site in get-website) {
Foreach ($Bind in $Site.bindings.collection) {
$data = [PSCustomObject]#{
name=$Site.name;
Protocol=$Bind.Protocol;
Bindings=$Bind.BindingInformation
}
$data.Bindings = $data.Bindings -replace '(:$)', ''
$html = "" + $data.name + ""
$html.Replace("*", $hostname);
}
}
Then I paste the results into this hastily written HTML:
<html>
<style>
a { display: block; }
</style>
{paste PowerShell results here}
</body>
</html>

How to update SharePoint enterprise keywords with multiple values using powershell?

I'm trying to write a powershell script that looks at a user defined variable and updates the enterprise keywords for each item in a list that matches that value.
For instance, say you have a page in SP that has the managed metadata keywords: new, fresh, clean
I want a script that asks a user what keyword they want to swap out. So a user would specify a variable as: fresh and another variable as: fresher and it would update any item with the keyword fresh to fresher.
Here's what I 've used before but doesn't work now because there are mutliple values:
Add-PSSnapin Microsoft.SharePoint.PowerShell -EA SilentlyContinue
$webURL = <MY SP URL>
$listName = <MY LIST NAME>
$web = Get-SPWeb $webURL
$list = $web.Lists[$listName]
$listitems = $list.items
$session = Get-SPTaxonomySession -Site $web.Site
$termStore = $session.TermStores["Managed Metadata Service"]
$group = $termStore.Groups["Resources"]
$termset = $group.TermSets["Wiki Categories"]
$terms = $termSet.GetTerms(100)
$wiki1 = read-host "Enter the wiki category you want to update"
$wiki2 = read-host "Enter the replacement wiki category"
$term = $terms | ?{$_.name -eq $wiki2}
Foreach($item in $listitems)
{$wiki = $item["Wiki Categories"]
if($wiki.label -eq $term)
{
$spitem = [Microsoft.SharePoint.SPListItem]$item;
$taxfield = [Microsoft.SharePoint.Taxonomy.TaxonomyField]$spitem.Fields["Wiki Categories"]
$taxfield.SetFieldValue($spitem, $term)
$spitem.Update()
$spitem.File.Publish("True")
}
}
I'm pretty sure the issue is with this line:
$term = $terms | ?{$_.name -eq $wiki2}
And this line:
$taxfield.SetFieldValue($spitem, $term)
The problem is that you passing a TermCollection ($term) to SetFieldValue where you should pass in a TaxonomyFieldValueCollection.
You can convert them like this:
$taxfield = $spitem.Fields["Wiki Categories"]
$tfvc = new-object -typename Microsoft.SharePoint.Taxonomy.TaxonomyFieldValueCollection -argumentlist $taxfield;
foreach($t in $ term)
{
$tfv = new-object -typename Microsoft.SharePoint.Taxonomy.TaxonomyFieldValue -argumentlist $taxfield
$tfv.TermGuid = $t.Id
$tfv.Label = $t.Name
$tfvc.Add($tfv)
}
...
$taxfield.SetFieldValue($spitem, $tfvc)

Resources