├── .gitignore ├── Azure-Admin-Roles ├── Azure-Admin-Roles.ps1 └── AzureAD-Admin-Roles-2022-07-12-06-57-54.csv ├── Backup-All-MS-Flow-JSON-and-ZIP.ps1 ├── Connect-EXOPSSession.ps1 ├── LICENSE ├── MS-Teams-Lockdown-Creators.ps1 ├── MS-Teams-Purge-Profile.ps1 ├── SPJeff-Inventory-SPKG-Available.ps1 ├── SPO-AZ-AppReg └── SPO-AZ-AppReg.ps1 ├── blog ├── SPJeff - PNP SPO Provision list schema.csv └── SPJeff - PNP SPO Provision list schema.ps1 ├── depend-js ├── depend.js ├── middle.js ├── middle2.js ├── test.html └── test2.html ├── m365-assessment-tool ├── 1-Register Azure AD Application.ps1 ├── 2-Run Microsoft 365 Assessment Tool.ps1 └── 3-PowerBI Report.ps1 ├── office365-PowerBI-download-PBIVIZ └── office365-PowerBI-download-PBIVIZ.ps1 ├── office365-auto-license └── O365-License.ps1 ├── office365-contact └── O365-Add-Contact.ps1 ├── office365-desktop-icon ├── README.md ├── office365-desktop-icon.ps1 └── video_thumb.png ├── office365-export-profiles ├── Export-SPO-All-UPS.csv ├── Export-SPO-All-UPS.ps1 └── create-personal-site-enqueue-bulk.ps1 ├── office365-gpo ├── office365-gpo.js ├── office365-gpo.ps1 ├── onedrive-gpo.ps1 └── onedrive-gpo.xml ├── office365-hybrid-search-cssa ├── AdministrationConfig-en.msi ├── Cloud Hybrid Search Service Application - SharePoint Escalation Services Team Blog.website ├── Configure cloud hybrid search - roadmap.website ├── CreateCloudSSA.ps1 ├── Delete-CloudHybridSearchContent.ps1 ├── HybridSPSetup.exe ├── Install-HybridSearchConfigPreqrequisites.ps1 ├── Onboard-CloudHybridSearch.ps1 ├── Test-HybridSearchConfigPreqrequisites.ps1 └── msoidcli_64.msi ├── office365-migration-banner └── o365-migration-banner.js ├── office365-migration ├── office365-CreatePersonalSiteEnqueueBulk.ps1 └── office365-migration-report.sql ├── office365-msteams-guest-report └── O365-MSTeams-Guest-Report.ps1 ├── office365-permission-report ├── Enumerate_Permissions.ps1 ├── Generate a Full Permission Report in PowerShell.url ├── GetUniquePermissions_ClientSideCode.ps1 ├── Load-CSOMProperties.ps1 ├── Loading Specific Values Using Lambda Expressions and the SharePoint CSOM API with Windows PowerShell - IT Unity.url └── Office SharePoint Unique Permissions Report using CSOM and PowerShell.url ├── office365-read-all-calendar └── O365-ReadAllCalendar.ps1 ├── office365-reduce-sppkg └── Reduce-Sppkg.ps1 ├── office365-runbook-spo-storage └── SPO-Storage.ps1 ├── office365-site-directory └── PnP-Site-Directory-JSON.ps1 ├── office365-speed └── o365-speed.ps1 ├── office365-spo-banner ├── configure-page.aspx └── spo-banner.js ├── office365-spo-custom-permission └── office365-spo-custom-permission.ps1 ├── office365-spo-export-splist-csv-and-email └── office365-spo-export-splist-csv-and-email.ps1 ├── office365-spo-modern-column-JSON ├── attach.json └── view.json ├── office365-spo-upload-slices ├── CoralReef.mp4 └── UploadFileInSlice.ps1 ├── office365-stale-webs ├── Stale_Webs_Email_Site_Final.htm ├── Stale_Webs_Email_Site_Owner.htm ├── Stale_Webs_Email_Summary.htm └── office365-stale-webs.ps1 ├── office365-video └── o365-video-channels-info.ps1 ├── office365-wait-dom ├── waitDom.gif ├── waitDom.html └── waitDom.js ├── outlook-reply-with-OFT-template-attachment-tokens.vb ├── pnp-connect ├── PNP-Register.ps1 ├── PnP-PowerShell-spjeff-Connect-PNPOnline.ps1 └── PnP-PowerShell-spjeff.txt ├── spo-modern-CEWP-wide-CSS ├── Modern CEWP wide CSS.html └── modern-cewp.sppkg └── xrm-toolbox └── XRM-Cmdlet-Query-Dataverse-Rows.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | pnp-connect/PnP-PowerShell-spjeff.cer 2 | pnp-connect/PnP-PowerShell-spjeff.pfx 3 | -------------------------------------------------------------------------------- /Azure-Admin-Roles/Azure-Admin-Roles.ps1: -------------------------------------------------------------------------------- 1 | # Download Azure AD users granted Admin Role Assignments 2 | 3 | # Connect 4 | Connect-AzureAD 5 | 6 | # Loop Role Definition 7 | $coll = @() 8 | foreach ($rd in (Get-AzureADMSRoleDefinition)) { 9 | $roleAssignment = Get-AzureADMSRoleAssignment -Filter "roleDefinitionId eq '$($rd.Id)'" -ErrorAction "SilentlyContinue" 10 | if ($roleAssignment) { 11 | foreach ($ra in $roleAssignment) { 12 | $users = Get-AzureADObjectByObjectId -ObjectIds $ra.PrincipalId 13 | foreach ($u in $users) { 14 | if ($u.ObjectType -eq "User") { 15 | $obj = [PSCustomObject]@{ 16 | 'Id' = $ra.Id 17 | 'RoleDefinitionId' = $ra.RoleDefinitionId 18 | 'PrincipalId' = $ra.PrincipalId 19 | 'RoleDisplayName' = $rd.DisplayName 20 | 'RoleIsBuiltIn' = $rd.IsBuiltIn 21 | 'RoleDescription' = $rd.Description 22 | 'RoleIsEnabled' = $rd.IsEnabled 23 | 'UserDisplayName' = $u.DisplayName 24 | 'UserPrincipalName' = $u.UserPrincipalName 25 | 'UserObjectType' = $u.UserType 26 | } 27 | $coll += $obj 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | # Write CSV 35 | Write-Host "Found $($coll.count) Azure admins" -ForegroundColor "Green" 36 | $stamp = Get-Date -UFormat "%Y-%m-%d-%H-%M-%S" 37 | $file = "AzureAD-Admin-Roles-$stamp.csv" 38 | $coll | Export-Csv $file -NoTypeInformation 39 | Start-Process $file -------------------------------------------------------------------------------- /Azure-Admin-Roles/AzureAD-Admin-Roles-2022-07-12-06-57-54.csv: -------------------------------------------------------------------------------- 1 | "Id","RoleDefinitionId","PrincipalId","RoleDisplayName","RoleIsBuiltIn","RoleDescription","RoleIsEnabled","UserDisplayName","UserPrincipalName","UserObjectType" 2 | "lAPpYvVpN0KRkAEhdxReEDrWY6LQ63NPvFsdgr_7YOI-1","62e90394-69f5-4237-9190-012177145e10","a263d63a-ebd0-4f73-bc5b-1d82bffb60e2","Global Administrator","True","Can manage all aspects of Azure AD and Microsoft services that use Azure AD identities.","True","SPJeff Dev","spjeffdev@spjeffdev.onmicrosoft.com","Member" 3 | "lAPpYvVpN0KRkAEhdxReEDd2Ub8P9olBvbOiz0vemeE-1","62e90394-69f5-4237-9190-012177145e10","bf517637-f60f-4189-bdb3-a2cf4bde99e1","Global Administrator","True","Can manage all aspects of Azure AD and Microsoft services that use Azure AD identities.","True","USER","USER@spjeffdev.onmicrosoft.com","Member" 4 | "UB-K8uf2cUWBi2oS8q9rbCqD6lKSP-RJnjZP-fYm-dg-1","f28a1f50-f6e7-4571-818b-6a12f2af6b6c","52ea832a-3f92-49e4-9e36-4ff9f626f9d8","SharePoint Administrator","True","Can manage all aspects of the SharePoint service.","True","USER2","USER2@spjeffdev.onmicrosoft.com","Member" 5 | "UB-K8uf2cUWBi2oS8q9rbNWFEMPA5hxJgUmZV2G02Z0-1","f28a1f50-f6e7-4571-818b-6a12f2af6b6c","c31085d5-e6c0-491c-8149-995761b4d99d","SharePoint Administrator","True","Can manage all aspects of the SharePoint service.","True","USER3","USER3@spjeffdev.onmicrosoft.com","Member" 6 | -------------------------------------------------------------------------------- /Backup-All-MS-Flow-JSON-and-ZIP.ps1: -------------------------------------------------------------------------------- 1 | # Backup-All-MS-Flow-JSON-and-ZIP.ps1 2 | # from https://github.com/pnp/script-samples/blob/main/scripts/flow-export-all-flows-in-environment/README.md 3 | # NOTE - Reference code above needs several changes to support new Micrsoft MS Flow V2 API standards. Below code uses latest V2 API standards. 4 | 5 | # Load PnP PowerShell Module 6 | Import-Module "PnP.PowerShell" 7 | 8 | # Connect to SharePoint Online 9 | $url = "https://spjeffdev-admin.sharepoint.com" 10 | Connect-PnPOnline -Url $url -Interactive 11 | 12 | # Loop all Flows in all Environments and export as ZIP & JSON 13 | $FlowEnvs = Get-PnPPowerPlatformEnvironment 14 | foreach ($FlowEnv in $FlowEnvs) { 15 | 16 | # Display Name of Environment 17 | $environmentName = $FlowEnv.Name 18 | Write-Host "Getting All Flows in $environmentName Environment" 19 | 20 | #Remove -AsAdmin Parameter to only target Flows you have permission to access 21 | $flows = Get-AdminFlow -Environment $environmentName 22 | 23 | # Display Count of Flows 24 | Write-Host "Found $($flows.Count) Flows to export..." 25 | 26 | # Loop all Flows and export as ZIP & JSON 27 | foreach ($flow in $flows) { 28 | # Display Name of Flow 29 | Write-Host "Exporting as ZIP & JSON... $($flow.DisplayName)" 30 | $filename = $flow.DisplayName.Replace(" ", "") 31 | 32 | # Build Export Path 33 | $timestamp = Get-Date -Format "yyyyMMddhhmmss" 34 | $exportPath = "$($filename)_$($timestamp)" 35 | $exportPath = $exportPath.Split([IO.Path]::GetInvalidFileNameChars()) -join '_' 36 | 37 | # Execute Export to ZIP & JSON 38 | $flow | ft -a 39 | Export-PnPFlow -Environment $FlowEnv -Identity $flow.FlowName -PackageDisplayName $flow.DisplayName -AsZipPackage -OutPath "$exportPath.zip" -Force 40 | Export-PnPFlow -Environment $FlowEnv -Identity $flow.FlowName | Out-File "$exportPath.json" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Connect-EXOPSSession.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Connect to Exchange Online without the Click2Run 4 | 5 | .DESCRIPTION 6 | Connect to Exchange Online without the Click2Run 7 | 8 | .PARAMETER UserPrincipalName 9 | UserPrincipalName of the Admin Account 10 | 11 | .EXAMPLE 12 | Connect to Exchange Online 13 | Connect-EXOPSSession -UserPrincipalName admin@contoso.com 14 | 15 | .NOTES 16 | Ref : https://www.michev.info/Blog/Post/1771/hacking-your-way-around-modern-authentication-and-the-powershell-modules-for-office-365 17 | Only Support User Connection no Application Connect (As Of : 2019-05) 18 | 19 | #> 20 | 21 | Function Connect-EXOPSSession 22 | { 23 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] 24 | [cmdletbinding()] 25 | param ( 26 | [parameter(Mandatory=$False)] 27 | $UserPrincipalName 28 | ) 29 | if([string]::IsNullOrEmpty($UserPrincipalName)) 30 | { 31 | $UserPrincipalName = Get-CurrentUPN 32 | } 33 | if([string]::IsNullOrEmpty($UserPrincipalName)) 34 | { 35 | Throw "Can't determine User Principal Name, please use the parameter -UserPrincipalName to specify it." 36 | } 37 | else 38 | { 39 | $resourceUri = "https://outlook.office365.com" 40 | $redirectUri = "urn:ietf:wg:oauth:2.0:oob" 41 | $clientid = "a0c73c16-a7e3-4564-9a95-2bdf47383716" 42 | 43 | if($Script:UPNEXOHeader){ 44 | # Setting DateTime to Universal time to work in all timezones 45 | $DateTime = (Get-Date).ToUniversalTime() 46 | 47 | # If the authToken exists checking when it expires 48 | $TokenExpires = ($Script:UPNEXOHeader.ExpiresOn.datetime - $DateTime).Minutes 49 | $UPNMismatch = $UserPrincipalName -ne $Script:UPNEXOHeader.UserID 50 | $AppIDMismatch = $ClientID -ne $Script:UPNEXOHeader.AppID 51 | if($TokenExpires -le 0 -or $UPNMismatch -or $AppIDMismatch){ 52 | Write-PSFMessage -Level Host -Message "Authentication need to be refresh" -ForegroundColor Yellow 53 | $Script:UPNEXOHeader = Get-OAuthHeaderUPN -clientId $ClientID -redirectUri $redirectUri -resourceAppIdURI $resourceURI -UserPrincipalName $UserPrincipalName 54 | } 55 | } 56 | # Authentication doesn't exist, calling Get-GraphAuthHeaderBasedOnUPN function 57 | else { 58 | $Script:UPNEXOHeader = Get-OAuthHeaderUPN -clientId $ClientID -redirectUri $redirectUri -resourceAppIdURI $resourceURI -UserPrincipalName $UserPrincipalName 59 | } 60 | $Result = $Script:UPNEXOHeader 61 | 62 | $Authorization = $Result.Authorization 63 | $Password = ConvertTo-SecureString -AsPlainText $Authorization -Force 64 | $Ctoken = New-Object System.Management.Automation.PSCredential -ArgumentList $UserPrincipalName, $Password 65 | $EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true -Credential $Ctoken -Authentication Basic -AllowRedirection 66 | Import-Module (Import-PSSession $EXOSession -AllowClobber) -Global -DisableNameChecking 67 | } 68 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeff Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MS-Teams-Lockdown-Creators.ps1: -------------------------------------------------------------------------------- 1 | # from https://www.knowledgewave.com/blog/how-to-disable-microsoft-teams-creation 2 | 3 | $GroupName = "MSTeamsCreators" 4 | $AllowGroupCreation = "False" 5 | Connect-AzureAD 6 | 7 | $settingsObjectID = (Get-AzureADDirectorySetting | Where-object -Property Displayname -Value "Group.Unified" -EQ).id 8 | if(!$settingsObjectID) 9 | { 10 | $template = Get-AzureADDirectorySettingTemplate | Where-object {$_.displayname -eq "group.unified"} 11 | $settingsCopy = $template.CreateDirectorySetting() 12 | New-AzureADDirectorySetting -DirectorySetting $settingsCopy 13 | $settingsObjectID = (Get-AzureADDirectorySetting | Where-object -Property Displayname -Value "Group.Unified" -EQ).id 14 | } 15 | 16 | $settingsCopy = Get-AzureADDirectorySetting -Id $settingsObjectID 17 | $settingsCopy["EnableGroupCreation"] = $AllowGroupCreation 18 | 19 | if($GroupName) 20 | { 21 | $settingsCopy["GroupCreationAllowedGroupId"] = (Get-AzureADGroup -Filter "DisplayName eq '$GroupName'").objectId 22 | } else { 23 | $settingsCopy["GroupCreationAllowedGroupId"] = $GroupName 24 | } 25 | Set-AzureADDirectorySetting -Id $settingsObjectID -DirectorySetting $settingsCopy 26 | 27 | (Get-AzureADDirectorySetting -Id $settingsObjectID).Values -------------------------------------------------------------------------------- /MS-Teams-Purge-Profile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | Purge inactive local MS Teams profile data. Comments and suggestions always welcome. 4 | .EXAMPLE 5 | .\MS-Teams-Purge-Profile.ps1 6 | 7 | .NOTES 8 | File Name: MS-Teams-Purge-Profile.ps1 9 | Author : Jeff Jones 10 | Version : 1.0 11 | Modified : 2021-08-21 12 | #> 13 | 14 | function Main() { 15 | # Discover all local users https://stackoverflow.com/questions/9725521/how-to-get-the-parents-parent-directory-in-powershell 16 | $profile = $env:USERPROFILE 17 | $root = (Get-ChildItem $profile)[0].Parent.Parent.FullName 18 | $allUsers = $root | Get-ChildItem -Directory -Exclude "Public" 19 | 20 | # Compare to Active Directory https://techibee.com/active-directory/powershell-search-for-a-user-without-using-ad-module/2872 21 | foreach ($u in $allUsers) { 22 | # Define scope 23 | $name = $u.Name 24 | $ldap = "(&(ObjectCategory=Person)(ObjectClass=User)(SamAccountName=" + $name + "))" 25 | $search = [adsisearcher]$ldap 26 | $results = $search.FindAll() 27 | 28 | # Search Active Directory 29 | if ($results.Count -eq 0) { 30 | Write-Host "NOT FOUND [" + $name + "] IN AD. DELETE FOLDER." -ForegroundColor "Yellow" 31 | Remove-Item "$root\$name" -Force 32 | } else { 33 | Write-Host "FOUND [" + $name + "] IN AD" -ForegroundColor "Green" 34 | } 35 | } 36 | } 37 | 38 | # Open Log 39 | $prefix = $MyInvocation.MyCommand.Name 40 | $host.UI.RawUI.WindowTitle = $prefix 41 | $stamp = Get-Date -UFormat "%Y-%m-%d-%H-%M-%S" 42 | Start-Transcript "$PSScriptRoot\log\$prefix-$stamp.log" 43 | $start = Get-Date 44 | 45 | Main 46 | 47 | # Close Log 48 | $end = Get-Date 49 | $totaltime = $end - $start 50 | Write-Host "`nTime Elapsed: $($totaltime.tostring("hh\:mm\:ss"))" 51 | Stop-Transcript -------------------------------------------------------------------------------- /SPJeff-Inventory-SPKG-Available.ps1: -------------------------------------------------------------------------------- 1 | # SPJeff-Inventory-SPKG-Available.ps1 2 | # Scan all site collection app catalogs for available SPPKG files and write report to CSV with full SPPKG details 3 | 4 | # Load Modules 5 | Import-Module PnP.PowerShell 6 | 7 | # Memory collection 8 | $coll = @() 9 | 10 | # Load CSV with SiteURLs and loop through each site 11 | $tenantAdminUrl = "https://spjeffdev-admin.sharepoint.com" 12 | 13 | # Connect to the site 14 | Connect-PnPOnline -Url $tenantAdminUrl -UseWebLogin -WarningAction SilentlyContinue 15 | # $token = Get-PnPAccessToken 16 | 17 | # Open the Tenant App Catalog 18 | $catalogUrl = Get-PnPTenantAppCatalogUrl 19 | 20 | # Open the Site Collection App Catalogs 21 | $tenantAppCatalogSite = Get-PnPSiteCollectionAppCatalog 22 | $tenantAppCatalogSite.Count 23 | 24 | # Append array with Tenant App Catalog new PSObject with property AbsoluteUrl 25 | $tenantAppCatalogSite += [PSCustomObject]@{ 26 | AbsoluteUrl = $catalogUrl 27 | } 28 | 29 | # Loop through each site collection app catalog 30 | foreach ($site in $tenantAppCatalogSite) { 31 | $global:siteUrl = $site.AbsoluteUrl 32 | $global:siteUrl 33 | 34 | # Connect to the site 35 | Connect-PnPOnline -Url $global:siteUrl -UseWebLogin -WarningAction SilentlyContinue 36 | 37 | # Get all SPPKG files in the Tenant App Catalog 38 | $files = Get-PnPListItem -List "Apps for SharePoint" -Fields "FileLeafRef", "FileRef", "Title", "ID" 39 | 40 | # Loop through each file 41 | foreach ($file in $files) { 42 | $fileUrl = $file["FileRef"] 43 | $fileName = $file["FileLeafRef"] 44 | $fileTitle = $file["Title"] 45 | $fileID = $file["ID"] 46 | 47 | # Match with PNP App 48 | $app = Get-PnPApp -Scope "Site" | Where-Object { $_.Title -eq $fileTitle } 49 | 50 | # Write to CSV 51 | $coll += [PSCustomObject]@{ 52 | SiteUrl = $global:siteUrl 53 | FileUrl = $fileUrl 54 | FileName = $fileName 55 | FileTitle = $fileTitle 56 | FileID = $fileID 57 | AppCatalogVersion = $app.AppCatalogVersion 58 | Deployed = $app.Deployed 59 | AppId = $app.Id 60 | IsClientSideSolution = $app.IsClientSideSolution 61 | } 62 | } 63 | } 64 | 65 | # Write to CSV 66 | $coll | Export-Csv -Path "SPJeff-SPKG-Available.csv" -NoTypeInformation -Force -------------------------------------------------------------------------------- /SPO-AZ-AppReg/SPO-AZ-AppReg.ps1: -------------------------------------------------------------------------------- 1 | # BLOG AT 2 | # 3 | # REFERENCES 4 | # ************************************************************ 5 | # https://pnp.github.io/powershell/articles/connecting.html 6 | # https://pnp.github.io/powershell/articles/authentication.html 7 | # https://docs.microsoft.com/en-us/powershell/module/sharepoint-pnp/register-pnpazureadapp?view=sharepoint-ps 8 | # https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps 9 | # https://mmsharepoint.wordpress.com/2018/12/19/modern-sharepoint-authentication-in-azure-automation-runbook-with-pnp-powershell/ 10 | 11 | # STEP 1 - PNP Register AZ AppReg 12 | # ************************************************************ 13 | 14 | # Modules 15 | Install-Module "PNP.PowerShell" 16 | Import-Module "PNP.PowerShell" 17 | Install-Module "Azure" 18 | Import-Module "Azure" 19 | 20 | # Scope 21 | $tenant = "spjeffdev" 22 | $password = "password" 23 | $certname = "SPO-AZ-AppReg-$tenant" 24 | 25 | # Register into Azure Application Registration Portal 26 | $secPassword = ConvertTo-SecureString -String $password -AsPlainText -Force 27 | $reg = Register-PnPAzureADApp -ApplicationName "SPO-AZ-AppReg-$tenant" -Tenant "$tenant.onmicrosoft.com" -CertificatePassword $secPassword -Interactive 28 | $reg."AzureAppId/ClientId" | Out-File "$certname-ClientID.txt" -Force 29 | 30 | # STEP 2 - PNP Connect 31 | # ************************************************************ 32 | 33 | # Connect PNP 34 | $clientId = Get-Content "$certname-ClientID.txt" 35 | $certFilename = "$certname.pfx" 36 | Connect-PnPOnline -Url "https://$tenant.sharepoint.com" -ClientId $clientId -Tenant "$tenant.onmicrosoft.com" -CertificatePath $certFilename -CertificatePassword $secPassword 37 | 38 | # PNP query to verify. Pass unit test. 39 | Get-PnPTenantSite | Format-Table -AutoSize 40 | 41 | # Add item to SPList 42 | $siteURL = "https://spjeffdev.sharepoint.com/" 43 | $listTitle = "Test" 44 | Connect-PnPOnline -Url $siteURL -ClientId $clientId -Tenant "$tenant.onmicrosoft.com" -CertificatePath $certFilename -CertificatePassword $secPassword 45 | Add-PnPListItem -List $listTitle -Values @{"Title" = "Test"} | Out-Null -------------------------------------------------------------------------------- /blog/SPJeff - PNP SPO Provision list schema.csv: -------------------------------------------------------------------------------- 1 | Name,Type 2 | FirstName,Text 3 | LastName,Text 4 | Addresss,Text 5 | City,Text 6 | State,Text 7 | Zip,Text 8 | Email,Text 9 | Phone,Text 10 | OrderNumber1,Text 11 | OrderNumber2,Text 12 | OrderNumber3,Text 13 | OrderNumber4,Text 14 | OrderNumber5,Text 15 | OrderNumber6,Text 16 | OrderNumber7,Text 17 | OrderNumber8,Text 18 | OrderNumber9,Text 19 | OrderNumber10,Text 20 | OrderNumber11,Text 21 | OrderNumber12,Text 22 | OrderNumber13,Text 23 | OrderNumber14,Text 24 | OrderNumber15,Text 25 | OrderNumber16,Text 26 | OrderNumber17,Text 27 | OrderNumber18,Text 28 | OrderNumber19,Text 29 | OrderNumber20,Text 30 | Amount1,Number 31 | Amount2,Number 32 | Amount3,Number 33 | Amount4,Number 34 | Amount5,Number 35 | Amount6,Number 36 | Amount7,Number 37 | Amount8,Number 38 | Amount9,Number 39 | Amount10,Number 40 | Amount11,Number 41 | Amount12,Number 42 | Amount13,Number 43 | Amount14,Number 44 | Amount15,Number 45 | Amount16,Number 46 | Amount17,Number 47 | Amount18,Number 48 | Amount19,Number 49 | Amount20,Number 50 | OrderDate1,Date 51 | OrderDate2,Date 52 | OrderDate3,Date 53 | OrderDate4,Date 54 | OrderDate5,Date 55 | OrderDate6,Date 56 | OrderDate7,Date 57 | OrderDate8,Date 58 | OrderDate9,Date 59 | OrderDate10,Date 60 | OrderDate11,Date 61 | OrderDate12,Date 62 | OrderDate13,Date 63 | OrderDate14,Date 64 | OrderDate15,Date 65 | OrderDate16,Date 66 | OrderDate17,Date 67 | OrderDate18,Date 68 | OrderDate19,Date 69 | OrderDate20,Date 70 | OrderDelivered1,Boolean 71 | OrderDelivered2,Boolean 72 | OrderDelivered3,Boolean 73 | OrderDelivered4,Boolean 74 | OrderDelivered5,Boolean 75 | OrderDelivered6,Boolean 76 | OrderDelivered7,Boolean 77 | OrderDelivered8,Boolean 78 | OrderDelivered9,Boolean 79 | OrderDelivered10,Boolean 80 | OrderDelivered11,Boolean 81 | OrderDelivered12,Boolean 82 | OrderDelivered13,Boolean 83 | OrderDelivered14,Boolean 84 | OrderDelivered15,Boolean 85 | OrderDelivered16,Boolean 86 | OrderDelivered17,Boolean 87 | OrderDelivered18,Boolean 88 | OrderDelivered19,Boolean 89 | OrderDelivered20,Boolean 90 | -------------------------------------------------------------------------------- /blog/SPJeff - PNP SPO Provision list schema.ps1: -------------------------------------------------------------------------------- 1 | # Provision list schema with higher number of fields 2 | 3 | # Configuration 4 | $siteUrl = "https://spjeffdev.sharepoint.com/sites/demo/" 5 | $listTitle = "Customer Tracking" 6 | 7 | # Connect to SharePoint Online with App ID and App Secret 8 | Connect-PnPOnline -Url $siteUrl -ClientID "TBD" -ClientSecret "TBD" 9 | 10 | # MFA popup support 11 | 12 | # Connect-PnPOnline -Url $siteUrl -UseWebLogin 13 | Get-PnPWeb 14 | 15 | # Open SPList 16 | $list = Get-PnPList -Identity $listTitle 17 | 18 | # Open CSV 19 | $csv = Import-Csv "SPJeff - PNP SPO Provision list schema.csv" 20 | # Loop through CSV and add fields to SPList with data type 21 | foreach ($row in $csv) { 22 | $row |Ft -a 23 | Add-PnPField -List $list -DisplayName $row.Name -InternalName $row.Name -Type $row.Type -AddToDefaultView 24 | } -------------------------------------------------------------------------------- /depend-js/depend.js: -------------------------------------------------------------------------------- 1 | function depend(src, callback) { 2 | // Already loaded 3 | var tags = document.getElementsByTagName('script') 4 | for (var i = 0; i < tags.length; i++) { 5 | if (tags[i].src == src) { 6 | console.log('dep-found'); 7 | callback(); 8 | return; 9 | } 10 | } 11 | 12 | // First load 13 | console.log('dep-new ' + src); 14 | var tag = document.createElement('script'); 15 | tag.src = src; 16 | tag.onload = callback; 17 | document.head.appendChild(tag); //or something of the likes 18 | } -------------------------------------------------------------------------------- /depend-js/middle.js: -------------------------------------------------------------------------------- 1 | var middle = 'middle'; -------------------------------------------------------------------------------- /depend-js/middle2.js: -------------------------------------------------------------------------------- 1 | var middle2 = 'middle2'; -------------------------------------------------------------------------------- /depend-js/test.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /depend-js/test2.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /m365-assessment-tool/1-Register Azure AD Application.ps1: -------------------------------------------------------------------------------- 1 | # from https://pnp.github.io/pnpassessment/using-the-assessment-tool/setupauth.html 2 | 3 | # Sample for the Microsoft Syntex adoption module. Remove the application/delegated permissions depending on your needs 4 | # and update the Tenant and Username properties to match your environment. 5 | # 6 | # If you prefer to have a password set to secure the created PFX file then add below parameter 7 | # -CertificatePassword (ConvertTo-SecureString -String "password" -AsPlainText -Force) 8 | # 9 | # See https://pnp.github.io/powershell/cmdlets/Register-PnPAzureADApp.html for more options 10 | # 11 | 12 | Install-Module PnP.PowerShell 13 | Import-Module PnP.PowerShell 14 | Get-Command -Module PnP.PowerShell 15 | 16 | Register-PnPAzureADApp -ApplicationName Microsoft365AssessmentToolForSyntex ` 17 | -Tenant spjeffdev.onmicrosoft.com ` 18 | -Store CurrentUser ` 19 | -GraphApplicationPermissions "Sites.Read.All" ` 20 | -SharePointApplicationPermissions "Sites.FullControl.All" ` 21 | -GraphDelegatePermissions "Sites.Read.All", "User.Read" ` 22 | -SharePointDelegatePermissions "AllSites.Manage" ` 23 | -Username "spjeffdev@spjeffdev.onmicrosoft.com" ` 24 | -Interactive 25 | 26 | <# 27 | 28 | #> -------------------------------------------------------------------------------- /m365-assessment-tool/2-Run Microsoft 365 Assessment Tool.ps1: -------------------------------------------------------------------------------- 1 | # from https://pnp.github.io/pnpassessment/addinsacs/assess.html 2 | 3 | .\microsoft365-assessment.exe start --mode AddInsACS --authmode application --tenant spjeffdev.sharepoint.com --applicationid 271e8c5b-7657-4609-95bf-08a1b7e9b392 --certpath "My|CurrentUser|5B6F76ED9A23CD463E80DE4561E2209FEAEBAAF2" 4 | 5 | .\microsoft365-assessment.exe status 6 | .\microsoft365-assessment.exe -help 7 | .\microsoft365-assessment.exe stop 8 | .\microsoft365-assessment.exe list -------------------------------------------------------------------------------- /m365-assessment-tool/3-PowerBI Report.ps1: -------------------------------------------------------------------------------- 1 | # from https://pnp.github.io/pnpassessment/addinsacs/assess.html 2 | 3 | .\microsoft365-assessment.exe report --id a277c2f2-001c-457a-87f0-34870a5e4717 4 | 5 | # mkdir "report-csv" 6 | #.\microsoft365-assessment.exe report --id a277c2f2-001c-457a-87f0-34870a5e4717 --mode CsvOnly --path "report-csv" -------------------------------------------------------------------------------- /office365-PowerBI-download-PBIVIZ/office365-PowerBI-download-PBIVIZ.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Download all PBIVIZ visualization files from the MS Gallery. 4 | 5 | .DESCRIPTION 6 | Download JSON feed with list of available visulizations and downloads each to the loca folder. 7 | 8 | Comments and suggestions always welcome! spjeff@spjeff.com or @spjeff 9 | .NOTES 10 | File Name : office365-PowerBI-download-PBIVIZ.ps1 11 | Author : Jeff Jones - @spjeff 12 | Version : 0.10 13 | Last Modified : 02-28-2017 14 | .LINK 15 | Source Code 16 | https://github.com/spjeff/office365/blob/master/office365-PowerBI-download-PBIVIZ/ 17 | #> 18 | 19 | # Configure 20 | $path = Split-Path $MyInvocation.MyCommand.Path 21 | $base = "https://visuals.azureedge.net/gallery-prod/" 22 | $catalog = $base + "visualCatalog.json" 23 | 24 | # Download JSON catalog 25 | $wr = Invoke-WebRequest -Uri $catalog 26 | $json = $wr.Content | ConvertFrom-Json 27 | 28 | # Download each PBIVIZ binary file 29 | $client = New-Object System.Net.WebClient 30 | foreach ($j in $json) { 31 | $u = $base + $j.downloadUrl 32 | $u 33 | $file = $path + $j.downloadUrl 34 | $client.DownloadFile($u, $file) 35 | } 36 | 37 | # Summary 38 | $n = $json.Count 39 | Write-Host "Downloaded $n Files" -Fore Green -------------------------------------------------------------------------------- /office365-auto-license/O365-License.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Insane Move - Copy sites to Office 365 in parallel. ShareGate Insane Mode times ten! 4 | .DESCRIPTION 5 | Copy SharePoint site collections to Office 365 in parallel. CSV input list of source/destination URLs. XML with general preferences. 6 | #> 7 | 8 | [CmdletBinding()] 9 | param ( 10 | [Parameter(Mandatory = $false, ValueFromPipeline = $false, HelpMessage = 'Verify all Office 365 site collections. Prep step before real migration.')] 11 | [Alias("ro")] 12 | [switch]$readonly = $false, 13 | 14 | [Parameter(Mandatory = $false, ValueFromPipeline = $false, HelpMessage = 'Verify all Office 365 site collections. Prep step before real migration.')] 15 | [Alias("r")] 16 | [switch]$report = $false 17 | ) 18 | 19 | # Config 20 | $srvlic = "lic@company.com" 21 | $domain = "@company.com" 22 | $tenant = "tenant" 23 | 24 | # Services in License Plan 25 | $EPServicesToAdd = "OFFICESUBSCRIPTION", "EXCHANGE_S_ENTERPRISE", "YAMMER_ENTERPRISE", "SHAREPOINTWAC", "SHAREPOINTENTERPRISE", "TEAMS1", "PROJECTWORKMANAGEMENT" 26 | $EMSServicesToAdd = "RMS_S_PREMIUM", "INTUNE_A", "RMS_S_ENTERPRISE", "AAD_PREMIUM", "MFA_PREMIUM" 27 | $KioskServicesToAdd = "OFFICESUBSCRIPTION" 28 | $VisioServicesToAdd = "VISIO_CLIENT_SUBSCRIPTION" 29 | $ProjectServicesToAdd = "PROJECT_CLIENT_SUBSCRIPTION" 30 | 31 | # Plugin 32 | Import-Module ActiveDirectory -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 33 | Import-Module MSOnline -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 34 | Import-Module MSOnlineExtended -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 35 | Import-Module CredentialManager -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 36 | 37 | # Log 38 | $datestamp = (Get-Date).tostring("yyyy-MM-dd-hh-mm-ss") 39 | $root = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition 40 | mkdir "$root\log\" -ErrorAction SilentlyContinue | Out-Null 41 | 42 | function Report { 43 | Write-Host "> PrepareReport" -Fore Yellow 44 | # Create table with Schema (columns) 45 | $global:dtReport = New-Object System.Data.DataTable("Report") 46 | @("UserPrincipalName", "DisplayName", "IsLicensed", "AccountSkuId", "ServicePlan", "ProvisioningStatus") | % { 47 | $global:dtReport.Columns.Add($_) | Out-Null 48 | } 49 | 50 | # All users 51 | $users = Get-MsolUser -All 52 | 53 | # Gather data 54 | foreach ($u in $users) { 55 | foreach ($l in $u.Licenses) { 56 | foreach ($ss in $l.ServiceStatus) { 57 | $row = $global:dtReport.NewRow() 58 | $row["UserPrincipalName"] = $u.UserPrincipalName 59 | $row["DisplayName"] = $u.DisplayName 60 | $row["IsLicensed"] = $u.IsLicensed 61 | $row["AccountSkuId"] = $l.AccountSkuId 62 | $row["ServicePlan"] = $ss.ServicePlan.ServiceName 63 | $row["ProvisioningStatus"] = $ss.ProvisioningStatus 64 | $global:dtReport.Rows.Add($row) 65 | } 66 | } 67 | } 68 | 69 | # Save CSV 70 | $global:dtReport | Export-Csv "d:\O365-report.csv" -NoTypeInformation 71 | } 72 | function ConnectO365 { 73 | # Read from Windows O/S Credential Manager 74 | $cred = Get-StoredCredential -Target $srvlic 75 | if (!$cred) { 76 | # Prompt and save 77 | Write-Host $srvlic -Fore Green 78 | $secpw = Read-Host -AsSecureString -Prompt "Enter Password: " 79 | New-StoredCredential -Target $srvlic -Username $srvlic -SecurePassword $secpw 80 | $cred = Get-StoredCredential -Target $srvlic 81 | } 82 | # Connect to Office 365 83 | Connect-MsolService -Credential $cred 84 | 85 | # Display SKU summary 86 | Get-MsolAccountSku | ft -AutoSize 87 | } 88 | function PrepareTable() { 89 | Write-Host "> PrepareTable" -Fore Yellow 90 | # Create table with Schema (columns) 91 | $global:dtLicenseO365 = New-Object System.Data.DataTable("O365") 92 | $global:dtLicenseNeed = New-Object System.Data.DataTable("Need") 93 | @("login", "SKU") | % { 94 | $global:dtLicenseO365.Columns.Add($_) | Out-Null 95 | $global:dtLicenseNeed.Columns.Add($_) | Out-Null 96 | } 97 | } 98 | function RecordLicense($need, $msg, $users, $skuActiveDirectory) { 99 | # Append to tracking table 100 | Write-Host "> RecordLicense $msg - $($users.count) $skuActiveDirectory" -Fore Green 101 | foreach ($u in $users) { 102 | # Add to table 103 | if ($need) { 104 | # AD License Need 105 | # AD Need with PACK 106 | $rowNeed = $global:dtLicenseNeed.NewRow() 107 | $login = $u.SamAccountName 108 | $rowNeed["login"] = $login 109 | $rowNeed["SKU"] = $skuActiveDirectory 110 | $global:dtLicenseNeed.Rows.Add($rowNeed) | Out-Null 111 | } 112 | else { 113 | # O365 License Have 114 | # Loop SKU 115 | $sku = "" 116 | foreach ($l in $u.Licenses) { 117 | # User Login 118 | $login = $u.UserPrincipalName.split("@")[0] 119 | $sku = $l.AccountSkuId 120 | $rowHave = $global:dtLicenseO365.NewRow() 121 | $rowHave["login"] = $login 122 | $rowHave["SKU"] = $sku 123 | $global:dtLicenseO365.Rows.Add($rowHave) 124 | } 125 | 126 | } 127 | } 128 | } 129 | function DetectLicenceO365() { 130 | Write-Host "> DetectLicenceO365" -Fore Yellow 131 | $users = Get-MsolUser -All 132 | #REM $users = $users |? {$_.userprincipalname -eq "user@company.com"} 133 | RecordLicense $false "DetectLicenceO365" $users "DetectLicenceO365" 134 | } 135 | function DetectLicenseNeed() { 136 | Write-Host "> DetectLicenseNeed" -Fore Yellow 137 | 138 | 139 | # DefaultUsers 140 | $users = Get-ADUser -Properties extensionAttribute9,extensionAttribute15,msExchRecipientTypeDetails,employeeType -ResultSetSize $null -Filter {UserprincipalName -like "*$domain" -and enabled -eq $True} | ?{$_.employeeType -eq "Contractor" -or $_.employeeType -eq "Employee" -and ($_.msExchRecipientTypeDetails -eq 1 -or $_.msExchRecipientTypeDetails -eq 2147483648)} 141 | $users = Get-ADUser s6ujikx -Properties extensionAttribute9, extensionAttribute15, msExchRecipientTypeDetails, employeeType 142 | $users.Count 143 | RecordLicense $true "DefaultUsers" $users "$($tenant):ENTERPRISEPACK" 144 | 145 | # EMSUsers 146 | $users.Count 147 | RecordLicense $true "EMSUsers" $users "$($tenant):EMS" 148 | 149 | # KIOSKUsers 150 | $users = Get-ADUser -Properties extensionAttribute9,extensionAttribute15,msExchRecipientTypeDetails,employeeType -ResultSetSize $null -Filter {UserprincipalName -like "*$domain" -and enabled -eq $True} | ?{$_.employeeType -eq "Contractor" -or $_.employeeType -eq "Employee" -and $_.msExchRecipientTypeDetails -eq $null} 151 | $users.Count 152 | RecordLicense $true "KIOSKUsers" $users "$($tenant):KIOSK" 153 | 154 | # VisioUsers 155 | $group = Get-ADGroup "SG-MSFT-VISIO-365-C2R-X86" 156 | $users = Get-ADGroupMember -Identity $group | Get-ADUser -Properties extensionAttribute15,employeeType,userAccountControl | ?{$_.employeeType -eq "Contractor" -or $_.employeeType -eq "Employee" -and $_.userAccountControl -ne 514 -and $_.userAccountControl -ne 546 -and $_.userAccountControl -ne 66050} 157 | $users.Count 158 | RecordLicense $true "VisioUsers" $users "$($tenant):VISIOCLIENT" 159 | 160 | # ProjectUsers 161 | $group = Get-ADGroup "SG-MSFT-PROJECT-365-C2R-X86" 162 | $users = Get-ADGroupMember -Identity $group | Get-ADUser -Properties extensionAttribute15,employeeType,userAccountControl | ?{$_.employeeType -eq "Contractor" -or $_.employeeType -eq "Employee" -and $_.userAccountControl -ne 514 -and $_.userAccountControl -ne 546 -and $_.userAccountControl -ne 66050} 163 | $users.Count 164 | RecordLicense $true "ProjectUsers" $users "$($tenant):PROJECTCLIENT" 165 | 166 | # Summary 167 | Write-Host "dtLicenseNeed rows = $($global:dtLicenseNeed.Rows.Count)" 168 | } 169 | function GrantRevoke() { 170 | $global:dtLicenseO365.WriteXML("d:\365.xml", $true); 171 | $global:dtLicenseNeed.WriteXML("d:\need.xml", $true); 172 | Write-Host "> GrantRevoke" -Fore Yellow 173 | 174 | # Grant - Need and Missing in O365 175 | $grant = 0 176 | $dv = New-Object System.Data.DataView $global:dtLicenseO365 177 | $dv.Sort = "login" 178 | foreach ($row in $global:dtLicenseNeed.Rows) { 179 | $login = $row["login"] 180 | $sku = $row["SKU"] 181 | 182 | if ($sku -like "*KIOSK") { 183 | # KIOSK Sublicense 184 | $sku = $sku.Replace("KIOSK","ENTERPRISEPACK") 185 | $EPSubLicense = $KioskServicesToAdd 186 | } else { 187 | # ENTERPRISEPACK Pack Sublicense 188 | $EPSubLicense = $EPServicesToAdd 189 | } 190 | 191 | $dv.RowFilter = "login='" + $login + "' AND SKU='" + $sku + "'" 192 | if ($dv.Count -eq 0) { 193 | # Grant Display 194 | Write-Host "GRANT >> $sku - $login" -Fore Green 195 | $grant++ 196 | 197 | # Prepare Sublicense 198 | if ($sku -like "*ENTERPRISEPACK") { 199 | $sub = $EPSubLicense 200 | } 201 | if ($sku -like "*EMS") { 202 | $sub = $EMSServicesToAdd 203 | } 204 | if ($sku -like "*VISIOCLIENT") { 205 | $sub = $VisioServicesToAdd 206 | } 207 | if ($sku -like "*PROJECTCLIENT") { 208 | $sub = $ProjectServicesToAdd 209 | 210 | } 211 | 212 | # Grant 213 | Modify-SubLicense -upn ($login + $domain) -PrimaryLicense $sku -SublicensesToAdd $sub 214 | } 215 | } 216 | 217 | # Revoke - Don't need and Have in O365 218 | $revoke = 0 219 | $dv = New-Object System.Data.DataView $global:dtLicenseNeed 220 | $dv.Sort = "login" 221 | foreach ($row in $global:dtLicenseO365.Rows) { 222 | $login = $row["login"] 223 | $sku = $row["SKU"] 224 | $dv.RowFilter = "login='" + $login + "' AND SKU='" + $sku + "'" 225 | if ($dv.Count -eq 0) { 226 | # Revoke Display 227 | if ($login -eq "s6ujikx") { 228 | Write-Host "REVOKE >> $sku - $login" -Fore Red 229 | $revoke++ 230 | 231 | # Revoke Permission 232 | Modify-SubLicense -upn ($login + $domain) -PrimaryRevoke $sku 233 | } 234 | } 235 | } 236 | 237 | # Summary 238 | Write-Host "Grant : $grant" -Fore Green 239 | Write-Host "Revoke: $revoke" -Fore Red 240 | } 241 | 242 | # http://sharepointjack.com/2016/modify-sublicense-powershell-function-for-modifying-office-365-sublicenses/ 243 | function Modify-SubLicense($upn, $PrimaryLicense, $SublicensesToAdd, $SublicensesToRemove, $PrimaryRevoke) { 244 | Write-Host "Modify-SubLicense" 245 | 246 | # Revoke primary 247 | if ($PrimaryRevoke) { 248 | Set-MsolUserLicense -UserPrincipalName $upn -RemoveLicenses $PrimaryRevoke 249 | return 250 | } 251 | 252 | #assemble a list of sub-licenses types the user has that are currently disabled, minus the one we're trying to add 253 | $spouser = Get-MsolUser -UserPrincipalName $upn 254 | $disabledServices = $($spouser.Licenses |? {$_.AccountSkuID -eq $PrimaryLicense}).servicestatus | where {$_.ProvisioningStatus -eq "Disabled"} | select -expand serviceplan | select ServiceName 255 | 256 | #disabled items need to be in an array form, next 2 lines build that... 257 | $disabled = @() 258 | foreach ($item in $disabledServices.servicename) {$disabled += $item} 259 | Write-Host " DisabledList before changes: $disabled" -Foregroundcolor yellow 260 | 261 | # If there are other sublicenses to be removed (Passed in via -SublicensesToRemove) then lets add those to the disabled list. 262 | foreach ($license in $SublicensesToRemove) {$disabled += $license } 263 | 264 | # Cleanup duplicates in case the license to remove was already missing 265 | $disabled = $disabled | select -unique 266 | 267 | # If there are licenses to ADD, we need to REMOVE them from the list of disabled licenses 268 | # http://stackoverflow.com/questions/8609204/union-and-intersection-in-powershell 269 | $disabled = $disabled | ? {$SublicensesToAdd -NotContains $_} 270 | Write-Host " DisabledList after changes: $Disabled" -ForegroundColor green 271 | 272 | # Apply 273 | $LicenseOptions = New-MsolLicenseOptions -AccountSkuId $PrimaryLicense -DisabledPlans $disabled 274 | $LicenseOptions 275 | Set-MsolUserLicense -UserPrincipalName $upn -AddLicenses $PrimaryLicense 276 | Set-MsolUserLicense -UserPrincipalName $upn -LicenseOptions $LicenseOptions 277 | Write-Host "OK" 278 | } 279 | 280 | 281 | function Main() { 282 | # Log 283 | $log = "$root\log\O365-License-$datestamp.log" 284 | Start-Transcript $log 285 | $start = Get-Date 286 | 287 | if ($report) { 288 | # Report 289 | ConnectO365 290 | Report 291 | } 292 | else { 293 | # Core 294 | ConnectO365 295 | PrepareTable 296 | DetectLicenceO365 297 | DetectLicenseNeed 298 | GrantRevoke 299 | } 300 | 301 | # Cleanup 302 | Write-Host "--- Run Duration" 303 | $now = Get-Date 304 | ($now - $start) 305 | Stop-Transcript 306 | } 307 | Main -------------------------------------------------------------------------------- /office365-contact/O365-Add-Contact.ps1: -------------------------------------------------------------------------------- 1 | # from https://blog.mastykarz.nl/building-applications-office-365-apis-any-platform/ 2 | 3 | # Config 4 | $clientID = "34f34d49-86b7-4437-a332-6fecaf95a244" 5 | $tenantName = "spjeff.onmicrosoft.com" 6 | $ClientSecret = "secret-goes-here" 7 | $Username = "spjeff@spjeff.com" 8 | $Password = "password-goes-here" 9 | 10 | # Access Token 11 | $ReqTokenBody = @{ 12 | Grant_Type = "Password" 13 | client_Id = $clientID 14 | Client_Secret = $clientSecret 15 | Username = $Username 16 | Password = $Password 17 | Scope = "https://graph.microsoft.com/.default" 18 | } 19 | $TokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method POST -Body $ReqTokenBody 20 | $TokenResponse 21 | 22 | 23 | # Data call - READ 24 | $apis = @( 25 | 'https://graph.microsoft.com/v1.0/me/contacts', 26 | 'https://graph.microsoft.com/v1.0/me', 27 | 'https://graph.microsoft.com/v1.0/users', 28 | 'https://graph.microsoft.com/v1.0/users/george@spjeff.com/contacts') 29 | $apis |% { 30 | Write-Host $_ -Fore Yellow 31 | Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $_ -Method GET -Body $body -ContentType "text/plain" 32 | } 33 | 34 | # Data call - WRITE 35 | $newcontact = '{"givenName": "Test","surname": "Contact","emailAddresses": [{"address": "test@contact.com","name": "Pavel Bansky"}],"businessPhones": ["+1 732 555 0102"]}' 36 | $api = 'https://graph.microsoft.com/v1.0/users/george@spjeff.com/contacts' 37 | Invoke-RestMethod -Headers @{Authorization = "Bearer $($Tokenresponse.access_token)"} -Uri $api -Method "POST" -Body $newcontact -ContentType "application/json" -------------------------------------------------------------------------------- /office365-desktop-icon/README.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | Desktop Icon to launch Office 365 PowerShell 3 | 4 | 5 | ## Description 6 | Check this video out to see a Desktop Icon that launches PowerShell and automatically logs in to your Office 365 Tenant URL. It leverages both the SPO (TechNet) and PNP (GitHub) cmdlets to open two connections. From here, you can easily work on your O365 tenant without having to memorize login steps and repeat each time. From blog post - [http://www.spjeff.com/2016/07/26/desktop-icon-launch-office-365-powershell/](http://www.spjeff.com/2016/07/26/desktop-icon-launch-office-365-powershell/) 7 | 8 | ## Sceenshot 9 | ![image](http://www.spjeff.com/wp-content/ftp_uploads/48900ff24251_D30B/image.png) 10 | ![image](http://www.spjeff.com/wp-content/ftp_uploads/48900ff24251_D30B/2016-07-26_14-42-35.png) 11 | 12 | ## Video 13 | [![](https://raw.githubusercontent.com/spjeff/office365/master/office365-desktop-icon/video_thumb.png)](https://vimeo.com/176372983 "VIDEO - Desktop Icon to launch Office 365 PowerShell") 14 | 15 | ## Contact 16 | Please drop a line to [@spjeff](https://twitter.com/spjeff) or [spjeff@spjeff.com](mailto:spjeff@spjeff.com) 17 | Thanks! =) 18 | 19 | ![image](http://img.shields.io/badge/first--timers--only-friendly-blue.svg?style=flat-square) 20 | 21 | 22 | ## License 23 | 24 | The MIT License (MIT) 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /office365-desktop-icon/office365-desktop-icon.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | SharePoint Central Admin - View active services across entire farm. No more select machine drop down dance! 4 | .DESCRIPTION 5 | Create Desktop Icon to launch Office 365 with securely saved credentials 6 | 7 | Comments and suggestions always welcome! spjeff@spjeff.com or @spjeff 8 | .NOTES 9 | File Name : office365-desktop-icon.ps1 10 | Author : Jeff Jones - @spjeff 11 | Version : 0.12 12 | Last Modified : 03-30-2017 13 | .LINK 14 | Source Code 15 | http://www.github.com/spjeff/o365/office365-desktop-icon.ps1 16 | 17 | Download PowerShell Plugins 18 | * SPO - SharePoint Online 19 | https://www.microsoft.com/en-us/download/details.aspx?id=35588 20 | 21 | * PNP - Patterns and Practices 22 | https://github.com/officedev/pnp-powershell 23 | #> 24 | 25 | # input 26 | Write-Host "=== Make Office 365 PowerShell desktop icon ===" 27 | $url = Read-Host "Tenant - Admin URL" 28 | $user = Read-Host "Tenant - Username" 29 | $pw = Read-Host "Tenant - Password" -AsSecureString 30 | $runAsAdmin = Read-Host "Do you want to Run As Administrator? (Y/N)" 31 | $split = $url.Split(".")[0].Split("/") 32 | $tenant = $split[$split.length - 1] 33 | 34 | # save to registry 35 | $hash = $pw | ConvertFrom-SecureString 36 | 37 | # command 38 | "Write-Host "" ____ __ __ _ ____ __ _____ "" -Fore Yellow`nWrite-Host "" / __ \ / _|/ _(_) |___ \ / /| ____|"" -Fore Yellow`nWrite-Host "" | | | | |_| |_ _ ___ ___ __) |/ /_| |__ "" -Fore Yellow`nWrite-Host "" | | | | _| _| |/ __/ _ \ |__ <| '_ \___ \ "" -Fore Yellow`nWrite-Host "" | |__| | | | | | | (_| __/ ___) | (_) |__) |"" -Fore Yellow`nWrite-Host "" \____/|_| |_| |_|\___\___| |____/ \___/____/ "" -Fore Yellow`nWrite-Host "" "" -Fore Yellow`nWrite-Host ""Connecting ..."" -NoNewLine`n`$h = ""$hash""`n`$secpw = ConvertTo-SecureString -String `$h`n`$c = New-Object System.Management.Automation.PSCredential (""$user"", `$secpw)`n`$pnp = Get-Module -ListAvailable SharePointPnPPowerShellOnline -ErrorAction SilentlyContinue`nImport-Module -WarningAction SilentlyContinue SharePointPnPPowerShellOnline -ErrorAction SilentlyContinue`n`$pnpurl = ""https://github.com/OfficeDev/PnP-PowerShell""`nif (`$pnp) {`n Connect-PnPOnline -URL ""$url"" -Credential `$c`n} else {`n Write-Warning ""Missing PNP cmds. Download at `$pnpurl""`n start `$pnpurl`n}`nWrite-Host ""[OK]"" -Fore Green`n""PNP commands: `$((get-command *-pnp*).count)""`n`$site=Get-PnpTenantSite`n`$site`n""`nSite Count: `$(`$site.count)""" | Out-File "$home\o365-icon-$tenant.ps1" 39 | 40 | # create desktop shortcut 41 | $folder = [Environment]::GetFolderPath("Desktop") 42 | $Target = "c:\Windows\System32\cmd.exe" 43 | $ShortcutFile = "$folder\Office365 $tenant.lnk" 44 | $WScriptShell = New-Object -ComObject WScript.Shell 45 | $Shortcut = $WScriptShell.CreateShortcut($ShortcutFile) 46 | $Shortcut.Arguments = "/c ""start powershell -noexit """"$home\o365-icon-$tenant.ps1""""""" 47 | $Shortcut.IconLocation = "imageres.dll, 1"; 48 | $Shortcut.TargetPath = $Target 49 | $Shortcut.Save() 50 | 51 | #run as admin - @brianlala http://stackoverflow.com/questions/28997799/how-to-create-a-run-as-administrator-shortcut-using-powershell 52 | if ($runAsAdmin -like 'Y*') { 53 | $bytes = [System.IO.File]::ReadAllBytes($ShortcutFile) 54 | $bytes[0x15] = $bytes[0x15] -bor 0x20 #set byte 21 (0x15) bit 6 (0x20) ON 55 | [System.IO.File]::WriteAllBytes($ShortcutFile, $bytes) 56 | } -------------------------------------------------------------------------------- /office365-desktop-icon/video_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/office365-desktop-icon/video_thumb.png -------------------------------------------------------------------------------- /office365-export-profiles/Export-SPO-All-UPS.csv: -------------------------------------------------------------------------------- 1 | "UserProfile_GUID","SID","ADGuid","AccountName","FirstName","SPS-PhoneticFirstName","LastName","SPS-PhoneticLastName","PreferredName","SPS-PhoneticDisplayName","WorkPhone","Department","Title","SPS-Department","Manager","AboutMe","PersonalSpace","PictureURL","UserName","QuickLinks","WebSite","PublicSiteRedirect","SPS-JobTitle","SPS-DataSource","SPS-MemberOf","SPS-Dotted-line","SPS-Peers","SPS-Responsibility","SPS-SipAddress","SPS-MySiteUpgrade","SPS-DontSuggestList","SPS-ProxyAddresses","SPS-HireDate","SPS-DisplayOrder","SPS-ClaimID","SPS-ClaimProviderID","SPS-LastColleagueAdded","SPS-OWAUrl","SPS-ResourceSID","SPS-ResourceAccountName","SPS-MasterAccountName","SPS-UserPrincipalName","SPS-O15FirstRunExperience","SPS-PersonalSiteInstantiationState","SPS-DistinguishedName","SPS-SourceObjectDN","SPS-LastKeywordAdded","SPS-ClaimProviderType","SPS-SavedAccountName","SPS-SavedSID","SPS-ObjectExists","SPS-PersonalSiteCapabilities","SPS-PersonalSiteFirstCreationTime","SPS-PersonalSiteLastCreationTime","SPS-PersonalSiteNumberOfRetries","SPS-PersonalSiteFirstCreationError","SPS-FeedIdentifier","WorkEmail","CellPhone","Fax","HomePhone","Office","SPS-Location","Assistant","SPS-PastProjects","SPS-Skills","SPS-School","SPS-Birthday","SPS-StatusNotes","SPS-Interests","SPS-HashTags","SPS-EmailOptin","SPS-PrivacyPeople","SPS-PrivacyActivity","SPS-PictureTimestamp","SPS-PicturePlaceholderState","SPS-PictureExchangeSyncState","SPS-MUILanguages","SPS-ContentLanguages","SPS-TimeZone","SPS-RegionalSettings-FollowWeb","SPS-Locale","SPS-CalendarType","SPS-AltCalendarType","SPS-AdjustHijriDays","SPS-ShowWeeks","SPS-WorkDays","SPS-WorkDayStartHour","SPS-WorkDayEndHour","SPS-Time24","SPS-FirstDayOfWeek","SPS-FirstWeekOfYear","SPS-RegionalSettings-Initialized","OfficeGraphEnabled","SPS-UserType","SPS-HideFromAddressLists","SPS-RecipientTypeDetails","DelveFlags","VideoUserPopup","PulseMRUPeople","msOnline-ObjectId","SPS-PointPublishingUrl","SPS-TenantInstanceId","SPS-SharePointHomeExperienceState","SPS-RefreshToken","SPS-MultiGeoFlags","PreferredDataLocation" 2 | "f98c7b69-e3b3-459b-9f79-a8aff898878d","i:0h.f|membership|1003bffd99f91c6b@live.com","System.Byte[]","i:0#.f|membership|spjeff@spjeff.com","Jeff","","Jones","","Jeff Jones","","","","","","","","/personal/spjeff_spjeff_com/","https://spjeff-my.sharepoint.com:443/User%20Photos/Profile%20Pictures/spjeff_spjeff_com_MThumb.jpg","spjeff@spjeff.com","","","","","","","","","","spjeff@spjeff.com","","","","","","spjeff@spjeff.com","membership","","","","","","spjeff@spjeff.com","1310736","2","CN=45f4af2f-e9e9-48a9-93c5-00df32aa7488,OU=0a9449ca-3619-4fca-8644-bdd67d0c8ca6,OU=Tenants,OU=MSOnline,DC=SPODS62317547,DC=msoprd,DC=msft,DC=net","","8/14/2018 12:00:00 AM","Forms","i:0#.f|membership|spjeff@spjeff.com","System.Byte[]","","4","8/22/2016 6:09:03 PM","8/22/2016 6:09:03 PM","1","","","spjeff@spjeff.com","","","","","","","","","","","","","","0","True","4095","63672550085","0","1","en-US","ui-ui","","True","","","","","False","","","","False","","","True","False","0","","","","","","45f4af2f-e9e9-48a9-93c5-00df32aa7488","/portals/personal/jeff","","526719","","","" 3 | -------------------------------------------------------------------------------- /office365-export-profiles/Export-SPO-All-UPS.ps1: -------------------------------------------------------------------------------- 1 | # from https://gist.github.com/asadrefai/ecfb32db81acaa80282d 2 | # from https://www.microsoft.com/en-us/download/confirmation.aspx?id=42038 3 | # installed file [sharepointclientcomponents_16-6906-1200_x64-en-us.msi] 4 | Try{ 5 | Add-Type -Path 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll' 6 | Add-Type -Path 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll' 7 | Add-Type -Path 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.UserProfiles.dll' 8 | } 9 | catch { 10 | Write-Host $_.Exception.Message 11 | Write-Host "No further parts of the migration will be completed" 12 | } 13 | # [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") 14 | # [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") 15 | # [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.UserProfiles") 16 | 17 | # from https://community.idera.com/database-tools/powershell/ask_the_experts/f/learn_powershell_from_don_jones-24/2824/exporting-key-value-pair-using-export-csv-cmdlet 18 | function ConvertTo-Object($hashtable) 19 | { 20 | $hashtable = $ups 21 | $object = New-Object PSObject 22 | $hashtable.Keys | 23 | ForEach-Object { 24 | Add-Member -inputObject $object -memberType NoteProperty -name $_ -value $hashtable[$_] 25 | } 26 | $object 27 | } 28 | 29 | # from https://sharepoint.stackexchange.com/questions/108664/powershell-script-for-user-profile-properties-in-sharepoint-online-2013 30 | $SiteURL = "https://spjeff.sharepoint.com" 31 | Connect-pnponline -Url $SiteURL 32 | $Context = Get-PNPContext 33 | 34 | #Identify users in the Site Collection 35 | $Users = $Context.Web.SiteUsers 36 | $Context.Load($Users) 37 | $Context.ExecuteQuery() 38 | 39 | #Create People Manager object to retrieve profile data 40 | $PeopleManager = New-Object Microsoft.SharePoint.Client.UserProfiles.PeopleManager($Context) 41 | $coll = @() 42 | $i=0 43 | Foreach ($User in $Users) 44 | { 45 | $i++ 46 | $UserProfile = $PeopleManager.GetPropertiesFor($User.LoginName) 47 | $Context.Load($UserProfile) 48 | $Context.ExecuteQuery() 49 | If ($UserProfile.Email -ne $null) 50 | { 51 | Write-Host "User:" $User.LoginName -ForegroundColor Green 52 | $ups = $UserProfile.UserProfileProperties 53 | $obj = ConvertTo-Object $ups 54 | $coll += $obj 55 | Write-Host "" 56 | } 57 | } 58 | 59 | $coll | Export-Csv "Export-SPO-All-UPS.csv" -NoTypeInformation 60 | Write-Host "DONE" -------------------------------------------------------------------------------- /office365-export-profiles/create-personal-site-enqueue-bulk.ps1: -------------------------------------------------------------------------------- 1 | # from https://blogs.msdn.microsoft.com/frank_marasco/2014/03/25/so-you-want-to-programmatically-provision-personal-sites-one-drive-for-business-in-office-365/ 2 | 3 | # tenant 4 | $webUrl = "https://tenant-admin.SharePoint.com" 5 | $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($webUrl) 6 | 7 | # admin user 8 | $web = $ctx.Web 9 | $username = "admin@tenant.onmicrosoft.com" 10 | $password = read-host -AsSecureString 11 | 12 | # context 13 | $ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username,$password ) 14 | $ctx.Load($web) 15 | $ctx.ExecuteQuery() 16 | 17 | # assembly 18 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.UserProfiles") 19 | $loader =[Microsoft.SharePoint.Client.UserProfiles.ProfileLoader]::GetProfileLoader($ctx) 20 | 21 | #To Get Profile 22 | $profile = $loader.GetUserProfile() 23 | $ctx.Load($profile) 24 | $ctx.ExecuteQuery() 25 | $profile 26 | 27 | #To enqueue Profile 28 | $loader.CreatePersonalSiteEnqueueBulk(@("admin@tenant.onmicrosoft.com")) 29 | $loader.Context.ExecuteQuery() -------------------------------------------------------------------------------- /office365-gpo/office365-gpo.js: -------------------------------------------------------------------------------- 1 | /* Office365 - Group Policy 2 | 3 | - hide Site Setting links 4 | - hide Site Features 5 | - hide Web Features 6 | - hide Custom Permission Levels 7 | 8 | last updated 03-12-18 9 | */ 10 | 11 | (function() { 12 | //wait for DOM element to appear 13 | function waitDom(id, fn) { 14 | var checkExist = setInterval(function() { 15 | if (document.getElementById(id)) { 16 | clearInterval(checkExist); 17 | fn(); 18 | } 19 | }, 250); 20 | } 21 | 22 | //Inject CSS 23 | function injectCss(css) { 24 | var style = document.createElement("style"); 25 | style.type = "text/css"; 26 | style.innerHTML = style; 27 | document.body.appendChild(style); 28 | } 29 | 30 | //hide site feature 31 | function hideSPFeature(name) { 32 | var headers = document.querySelectorAll('h3.ms-standardheader'); 33 | if (headers) { 34 | for (var i = 0; i < headers.length; i++) { 35 | if (headers[i].innerText.indexOf(name) >= 0) { 36 | headers[i].parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.style.display = 'none'; 37 | } 38 | } 39 | } 40 | } 41 | 42 | //remove alternating row color 43 | function hideAltRowColor() { 44 | var className = 'ms-featurealtrow'; 45 | var rows = document.querySelectorAll('td.' + className); 46 | if (rows) { 47 | for (var i = 0; i < rows.length; i++) { 48 | var el = rows[i]; 49 | if (el.classList) { 50 | el.classList.remove(className); 51 | } else { 52 | el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 53 | } 54 | 55 | } 56 | } 57 | } 58 | 59 | //menu - hide Web Features 60 | function menuWebFeatures() { 61 | //hide rows 62 | var features = ['Access App', 63 | 'Announcement Tiles', 64 | 'Community Site Feature', 65 | 'Duet Enterprise - SAP Workflow', 66 | 'Duet Enterprise Reporting', 67 | 'Duet Enterprise Site Branding', 68 | 'External System Events', 69 | 'Getting Started with Project Web App', 70 | 'Hold', 71 | 'Minimal Download Strategy', 72 | 'Offline Synchronization for External Lists', 73 | 'Project Functionality', 74 | 'Project Proposal Workflow', 75 | 'Project Web App Connectivity', 76 | 'SAP Workflow Web Parts', 77 | 'Search Config Data Content Types', 78 | 'Search Config Data Site Columns', 79 | 'Search Config List Instance Feature', 80 | 'Search Config Template Feature', 81 | 'Site Feed', 82 | 'SharePoint Server Publishing' 83 | ]; 84 | features.forEach(function(feature, i) { 85 | hideSPFeature(feature); 86 | }); 87 | 88 | //hide row background color 89 | hideAltRowColor(); 90 | } 91 | 92 | //menu - hide Site Features 93 | function menuSiteFeatures() { 94 | //hide rows 95 | var features = ['Content Type Syndication Hub', 96 | 'Custom Site Collection Help', 97 | 'Cross-Site Collection Publishing', 98 | 'Duet End User Help Collection', 99 | 'Duet Enterprise Reports Content Types', 100 | 'In Place Records Management', 101 | 'Library and Folder Based Retention', 102 | 'Limited-access user permission lockdown mode', 103 | 'Project Server Approval Content Type', 104 | 'Project Web App Permission for Excel Web App Refresh', 105 | 'Project Web App Ribbon', 106 | 'Project Web App Settings', 107 | 'Publishing Approval Workflow', 108 | 'Reports and Data Search Support', 109 | 'Sample Proposal', 110 | 'Search Engine Sitemap', 111 | 'SharePoint 2007 Workflows', 112 | 'SharePoint Server Publishing Infrastructure', 113 | 'Site Policy', 114 | 'Workflows' 115 | ]; 116 | features.forEach(function(feature, i) { 117 | hideSPFeature(feature); 118 | }); 119 | 120 | //hide row background color 121 | hideAltRowColor(); 122 | } 123 | 124 | //menu - hide Settings Links 125 | function menuSettings() { 126 | //hide links 127 | var links = ['ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_AuditSettings', 128 | 'ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_SharePointDesignerSettings', 129 | 'ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_PolicyPolicies', 130 | 'ctl00_PlaceHolderMain_SiteAdministration_RptControls_PolicyPolicyAndLifecycle', 131 | 'ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_HubUrlLinks', 132 | 'ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_Portal', 133 | 'ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_HtmlFieldSecurity', 134 | 'ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_SearchConfigurationImportSPSite', 135 | 'ctl00_PlaceHolderMain_SiteCollectionAdmin_RptControls_SearchConfigurationExportSPSite' 136 | ]; 137 | links.forEach(function(id, i) { 138 | var el = document.getElementById(id); 139 | if (el) { 140 | el.style.display = 'none'; 141 | } 142 | }); 143 | 144 | // Change Owner link 145 | // find group 146 | var match; 147 | var section = document.querySelectorAll('h3.ms-linksection-title'); 148 | if (section) { 149 | for (var i = 0; i < section.length; i++) { 150 | var el = section[i]; 151 | if (el.innerHTML.indexOf("Users and Permissions") > 0) { 152 | match = el.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode; 153 | } 154 | } 155 | } 156 | if (match) { 157 | //append new child link 158 | var group = match.querySelector("ul"); 159 | var li = document.createElement("li"); 160 | li.innerHTML = 'Change Site Owner'; 161 | group.appendChild(li); 162 | } 163 | } 164 | 165 | //toolbar - permission levels 166 | function userSettings() { 167 | var el = document.getElementById('Ribbon.Permission.Manage-LargeMedium-0-0'); 168 | if (el) { 169 | el.style.display = 'none'; 170 | } 171 | } 172 | 173 | //body - permission levels 174 | function roleSettings() { 175 | var el = document.getElementById('DeltaPlaceHolderMain'); 176 | if (el) { 177 | el.innerHTML = '

Disabled by SharePoint Support team.

'; 178 | } 179 | } 180 | 181 | //access request - checkboxes 182 | function accessSettings() { 183 | var css = '#ctl00_PlaceHolderMain_ctl00_chkMembersCanShare,label[for="ctl00_PlaceHolderMain_ctl00_chkMembersCanShare"],#ctl00_PlaceHolderMain_ctl00_chkMembersCanAddToGroup,label[for="ctl00_PlaceHolderMain_ctl00_chkMembersCanAddToGroup"] {display:none;}'; 184 | injectCss(css); 185 | } 186 | 187 | //wait until document ready http://youmightnotneedjquery.com/ 188 | function ready(fn) { 189 | if (document.readyState != 'loading') { 190 | fn(); 191 | } else { 192 | document.addEventListener('DOMContentLoaded', fn); 193 | } 194 | } 195 | 196 | //URL contains expression 197 | function urlContains(expr) { 198 | return document.location.href.toLowerCase().indexOf(expr.toLowerCase()) > 0; 199 | } 200 | 201 | //core logic 202 | function main() { 203 | if (!urlContains('skip')) { 204 | //Web Features 205 | if (urlContains('ManageFeatures.aspx') && !urlContains('Scope=Site')) { 206 | waitDom('DeltaPlaceHolderMain', menuWebFeatures); 207 | } 208 | 209 | //Site Features 210 | if (urlContains('ManageFeatures.aspx?Scope=Site')) { 211 | waitDom('DeltaPlaceHolderMain', menuSiteFeatures); 212 | } 213 | 214 | //Site Settings 215 | if (urlContains('settings.aspx')) { 216 | waitDom('DeltaPlaceHolderMain', menuSettings); 217 | } 218 | 219 | //User 220 | if (urlContains('user.aspx')) { 221 | waitDom('Ribbon.Permission.Manage', userSettings); 222 | } 223 | 224 | //Role 225 | if (urlContains('role.aspx')) { 226 | waitDom('DeltaPlaceHolderMain', roleSettings); 227 | } 228 | 229 | //Access Request 230 | if (urlContains('setrqacc.aspx')) { 231 | waitDom('DeltaPlaceHolderMain', accessSettings); 232 | } 233 | 234 | } 235 | } 236 | 237 | 238 | //initialize 239 | ready(main); 240 | })(); -------------------------------------------------------------------------------- /office365-gpo/office365-gpo.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $false, HelpMessage = 'Optional URL to configure one site only')] 4 | [Alias("url")] 5 | [string]$matchURL 6 | ) 7 | <# 8 | Office365 - Group Policy 9 | 10 | * leverages 3 libraries (SPO, PNP, CSOM) 11 | * leverages parallel PowerShell 12 | * grant Site Collection Admin for support staff 13 | * apply Site Collection quota 5GB (if none) 14 | * enable Site Collection Auditing 15 | * enable Site Collection Custom Action JS ("office365-gpo.js") 16 | #> 17 | 18 | #Core 19 | workflow GPOWorkflow { 20 | param ($sites, $UserName, $Password) 21 | 22 | Function VerifySite([string]$SiteUrl, $UserName, $Password) { 23 | Function Get-SPOCredentials([string]$UserName, [string]$Password) { 24 | if ([string]::IsNullOrEmpty($Password)) { 25 | $SecurePassword = Read-Host -Prompt "Enter the password" -AsSecureString 26 | } 27 | else { 28 | $SecurePassword = $Password | ConvertTo-SecureString -AsPlainText -Force 29 | } 30 | return New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $SecurePassword) 31 | } 32 | Function Get-ActionBySequence([Microsoft.SharePoint.Client.ClientContext]$context, [int]$Sequence) { 33 | $customActions = $context.Site.UserCustomActions 34 | $context.Load($customActions) 35 | $context.ExecuteQuery() 36 | $customActions | where { $_.Sequence -eq $Sequence } 37 | } 38 | Function Delete-Action($UserCustomAction) { 39 | $context = $UserCustomAction.Context 40 | $UserCustomAction.DeleteObject() 41 | $context.ExecuteQuery() 42 | "DELETED" 43 | } 44 | Function Verify-ScriptLinkAction([Microsoft.SharePoint.Client.ClientContext]$context, [string]$ScriptSrc, [string]$ScriptBlock, [int]$Sequence) { 45 | $actions = Get-ActionBySequence -Context $context -Sequence $Sequence 46 | 47 | if (!$actions) { 48 | $action = $context.Site.UserCustomActions.Add(); 49 | $action.Location = "ScriptLink" 50 | if ($ScriptSrc) { 51 | $action.ScriptSrc = $ScriptSrc 52 | } 53 | if ($ScriptBlock) { 54 | $action.ScriptBlock = $ScriptBlock 55 | } 56 | $action.Sequence = $Sequence 57 | $action.Update() 58 | $context.ExecuteQuery() 59 | } 60 | } 61 | Function Verify-Features([Microsoft.SharePoint.Client.ClientContext]$context) { 62 | #list of Site features 63 | $feat = $context.Site.Features 64 | $context.Load($feat) 65 | $context.ExecuteQuery() 66 | 67 | #SPSite - Enable Workflow 68 | $id = New-Object System.Guid "0af5989a-3aea-4519-8ab0-85d91abe39ff" 69 | $found = $feat |? {$_.DefinitionId -eq $id} 70 | if (!$found) { 71 | $feat.Add($id, $true, [Microsoft.SharePoint.Client.FeatureDefinitionScope]::Farm) 72 | } 73 | $context.ExecuteQuery() 74 | 75 | #SPWeb - Disable Minimal Download Strategy (MDS) 76 | Loop-WebFeature $context $context.Site.RootWeb $false "87294c72-f260-42f3-a41b-981a2ffce37a" 77 | } 78 | Function Loop-WebFeature ($context, $currWeb, $wantActive, $featureId) { 79 | #get parent 80 | $context.Load($currWeb) 81 | $context.ExecuteQuery() 82 | 83 | #ensure parent 84 | Ensure-WebFeature $context $currWeb $wantActive $featureId 85 | 86 | #get child 87 | $webs = $currWeb.Webs 88 | $context.Load($webs) 89 | $context.ExecuteQuery() 90 | 91 | #loop child subwebs 92 | foreach ($web in $webs) { 93 | Write-Host "ensure feature on " + $web.url 94 | #ensure child 95 | Ensure-WebFeature $context $web $wantActive $featureId 96 | 97 | #Recurse 98 | $subWebs = $web.Webs 99 | $context.Load($subWebs) 100 | $context.ExecuteQuery() 101 | $subWebs | ForEach-Object { Loop-WebFeature $context $_ $wantActive $featureId } 102 | } 103 | } 104 | Function Ensure-WebFeature ($context, $web, $wantActive, $featureId) { 105 | #list of Web features 106 | if ($web.Url) { 107 | Write-Host " - $($web.Url)" 108 | $feat = $web.Features 109 | $context.Load($feat) 110 | $context.ExecuteQuery() 111 | 112 | #Disable/Enable Web features 113 | $id = New-Object System.Guid $featureId 114 | $found = $feat |? {$_.DefinitionId -eq $id} 115 | if ($wantActive) { 116 | Write-Host "ADD FEAT" -Fore Yellow 117 | if (!$found) { 118 | $feat.Add($id, $true, [Microsoft.SharePoint.Client.FeatureDefinitionScope]::Farm) 119 | $context.ExecuteQuery() 120 | } 121 | } 122 | else { 123 | Write-Host "REMOVE FEAT" -Fore Yellow 124 | if ($found) { 125 | $feat.Remove($id, $true) 126 | $context.ExecuteQuery() 127 | } 128 | } 129 | #no changes. already OK 130 | } 131 | } 132 | Function Verify-General([Microsoft.SharePoint.Client.ClientContext]$context) { 133 | #SPSite 134 | $site = $context.Site 135 | $context.Load($site) 136 | $context.ExecuteQuery() 137 | 138 | #RootWeb 139 | $rootWeb = $site.RootWeb 140 | $context.Load($rootWeb) 141 | $context.ExecuteQuery() 142 | 143 | #RootLists 144 | $rootLists = $rootWeb.Lists 145 | $context.Load($rootLists) 146 | $context.ExecuteQuery() 147 | 148 | #Access Request List 149 | $arList = $rootLists.GetByTitle("Access Requests"); 150 | $context.Load($arList) 151 | $context.ExecuteQuery() 152 | if ($arList) { 153 | if ($arList.Hidden) { 154 | $arList.Hidden = $false 155 | $arList.Update() 156 | $context.ExecuteQuery() 157 | } 158 | } 159 | 160 | #Trim Audit Log 161 | if (!$site.TrimAuditLog) { 162 | $site.TrimAuditLog = $true 163 | $site.AuditLogTrimmingRetention = 180 164 | $site.Update() 165 | $context.ExecuteQuery() 166 | } 167 | 168 | #Audit Log Storage 169 | if (!$rootWeb.AllProperties["_auditlogreportstoragelocation"]) { 170 | $url = $context.Site.ServerRelativeUrl 171 | if ($url -eq "/") { 172 | $url = "" 173 | } 174 | $rootWeb.AllProperties["_auditlogreportstoragelocation"] = "$url/SiteAssets" 175 | $rootWeb.Update() 176 | $context.ExecuteQuery() 177 | } 178 | } 179 | 180 | #Assembly CSOM 181 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null 182 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") | Out-Null 183 | 184 | try { 185 | $context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl) 186 | $cred = Get-SPOCredentials -UserName $UserName -Password $Password 187 | $context.Credentials = $cred 188 | 189 | #JS CUSTOM ACTION 190 | $scriptUrl = "https://tenant.sharepoint.com/SiteAssets/Office365-GPO/office365-gpo.js" 191 | Verify-ScriptLinkAction -Context $context -ScriptSrc $scriptUrl -Sequence 2001 192 | 193 | #SITE COLLECTION CONFIG 194 | Verify-General -Context $context 195 | 196 | #FEATURES 197 | Verify-Features -Context $context 198 | 199 | $context.Dispose() 200 | } 201 | catch { 202 | Write-Error "ERROR -- $SiteUrl -- $($_.Exception.Message)" 203 | } 204 | } 205 | 206 | 207 | #Parallel Loop - CSOM 208 | ForEach -Parallel -ThrottleLimit 100 ($s in $sites) { 209 | Write-Output "Start thread >> $($s.Url)" 210 | VerifySite $s.Url $UserName $Password 211 | } 212 | "DONE" 213 | } 214 | 215 | Function Main { 216 | #Start 217 | $start = Get-Date 218 | 219 | #SPO and PNP modules 220 | Import-Module -WarningAction SilentlyContinue Microsoft.Online.SharePoint.PowerShell -Prefix M 221 | Import-Module -WarningAction SilentlyContinue SharePointPnPPowerShellOnline -Prefix P 222 | 223 | #Config 224 | $AdminUrl = "https://tenant-admin.sharepoint.com" 225 | $UserName = "admin@tenant.onmicrosoft.com" 226 | $Password = "pass@word1" 227 | 228 | #Credential 229 | $secpw = ConvertTo-SecureString -String $Password -AsPlainText -Force 230 | $c = New-Object System.Management.Automation.PSCredential ($UserName, $secpw) 231 | 232 | #Connect Office 365 233 | Connect-MSPOService -URL $AdminUrl -Credential $c 234 | 235 | #Scope 236 | Write-Host "Opening list of sites ... $matchURL" -Fore Green 237 | if ($matchURL) { 238 | $sites = Get-MSPOSite -Filter "url -like ""$matchURL""" 239 | } 240 | else { 241 | $sites = Get-MSPOSite 242 | } 243 | $sites.Count 244 | 245 | #Serial loop 246 | Write-Host "Serial loop" 247 | ForEach ($s in $sites) { 248 | Write-Host "." -NoNewLine 249 | #SPO 250 | #Storage quota 251 | if (!$s.StorageQuota) { 252 | Set-MSPOSite -Identity $s.Url -StorageQuota 5000 -StorageQuotaWarningLevel 4000 253 | Write-Output "set 2GB quota on $($s.Url)" 254 | } 255 | 256 | #Site collection admin 257 | $scaUser = "SharePoint Service Administrator" 258 | try { 259 | $user = Get-MSPOUser -Site $s.Url -Loginname $scaUser -ErrorAction SilentlyContinue 260 | if (!$user.IsSiteAdmin) { 261 | Set-MSPOUser -Site $s.Url -Loginname $scaUser -IsSiteCollectionAdmin $true -ErrorAction SilentlyContinue | Out-Null 262 | } 263 | } 264 | catch { 265 | } 266 | 267 | #PNP 268 | Connect-PSPOnline -Url $s.Url -Credentials $c 269 | 270 | # SharePoint Designer force OFF unless we see "keepspd=1" manual exclude flag ON 271 | $keepspd = Get-PSPOPropertyBag -Key "keepspd" 272 | if ($keepspd -ne 1) { 273 | Set-PSPOPropertyBagValue -Key "allowdesigner" -Value 0 274 | } 275 | 276 | # Verify Auditing 277 | $audit = Get-PSPOAuditing 278 | if ($audit.AuditFlags -ne 7099) { 279 | Set-PSPOAuditing -RetentionTime 180 -TrimAuditLog -EditItems -CheckOutCheckInItems -MoveCopyItems -DeleteRestoreItems -EditContentTypesColumns -EditUsersPermissions -ErrorAction SilentlyContinue 280 | Write-Output "set audit flags on $($s.Url)" 281 | } 282 | } 283 | 284 | #Parallel loop 285 | #CSOM 286 | Write-Host "Parallel loop" 287 | GPOWorkflow $sites $UserName $Password 288 | 289 | #Duration 290 | $min = [Math]::Round(((Get-Date) - $start).TotalMinutes, 2) 291 | Write-Host "Duration Min : $min" 292 | } 293 | Main -------------------------------------------------------------------------------- /office365-gpo/onedrive-gpo.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Disables allow Allow members to share setting on OneDrive sites and webs 4 | 5 | .DESCRIPTION 6 | Disables Members Can Share setting 7 | #> 8 | 9 | $startTime = (Get-Date) 10 | $datestamp = $startTime.ToString("yyyy-MM-dd-hh-mm-ss") 11 | 12 | Start-Transcript "OneDriveGPO-$datestamp.csv" 13 | 14 | # Plugins 15 | Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue | Out-Null 16 | Import-Module SharePointPnPPowerShellOnline -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 17 | Import-Module Microsoft.Online.SharePoint.PowerShell -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 18 | 19 | # Settings 20 | [xml]$settings = Get-Content "OneDriveGPO.xml" 21 | $admins = @() 22 | $adminUsers = $settings.settings.adminUsers.split(",") 23 | foreach ($adminUser in $adminUsers) { 24 | $admins += $adminUser.Trim() 25 | } 26 | $tenant = $settings.settings.tenant 27 | $adminURL = "https://$tenant-admin.sharepoint.com" 28 | 29 | # Grant Site Collection Admin with SPO 30 | Function GrantSCA ($url) { 31 | $site = Get-SPOSite $url 32 | foreach ($admin in $admins) { 33 | $user = Get-SPOUser -Site $site -LoginName $admin 34 | if (!$user.IsSiteAdmin) { 35 | $displayName = $user.DisplayName 36 | Set-SPOUser -Site $site -LoginName $admin -IsSiteCollectionAdmin $true 37 | Write-Host "$displayName granted SCA" -Fore Green 38 | } else { 39 | Write-Host "$displayName already SCA" -Fore Yellow 40 | } 41 | } 42 | } 43 | 44 | # Main 45 | Function Main() { 46 | # Credentials 47 | $cloudpw = ConvertTo-SecureString -String $settings.settings.adminPass -AsPlainText -Force 48 | $cloudCred = New-Object System.Management.Automation.PSCredential ($settings.settings.adminUserName, $cloudpw) 49 | Connect-PnPOnline $adminURL 50 | Connect-SPOService -URL $adminURL -Credential $cloudCred 51 | 52 | # Azure AD (AAD) - All Profiles 53 | Write-Host "Azure AD (AAD)" 54 | $tracker = @() 55 | Connect-MsolService -Credential $cloudCred 56 | #$msolUsers = Get-MsolUser -All -EnabledFilter EnabledOnly 57 | $msolUsers = Get-MsolUser -EnabledFilter EnabledOnly -MaxResults 10 58 | $personalURLs = @() 59 | foreach ($msolUser in $msolUsers) { 60 | $profile = Get-PNPUserProfileProperty -Account $msolUser.UserPrincipalName 61 | 62 | if ($profile.PersonalURL -like "https://tenant-my.sharepoint.com/personal/*") { 63 | $profileObj = New-Object -TypeName PSObject -Prop (@{"UPN"=$msolUser.UserPrincipalName; "Url"=$profile.PersonalURL.TrimEnd("/"); "State"="New"}) 64 | $tracker += $profileObj 65 | } 66 | } 67 | 68 | # Grant Site Collection Admin 69 | foreach ($personalURL in $tracker) { 70 | $personalURL 71 | $url = $personalURL.Url 72 | GrantSCA $url 73 | } 74 | 75 | # ScriptBlock 76 | $sb = { param($url) 77 | 78 | # Connect 79 | Connect-PnPOnline -Url $url 80 | 81 | # Enable Versioning 82 | $DocLib = Get-PnPList -Identity "Documents" 83 | if (!$DocLib.EnableVersioning) { 84 | Set-PnPList -Identity "Documents" -EnableVersioning $true -MajorVersions 99 85 | Write-Host "$url - Versioning Enabled" -Fore Green 86 | } 87 | else { 88 | Write-Host "$url - already enabled" -Fore Magenta 89 | } 90 | 91 | # Disable AllowMembersEditMembership for SubWeb Member Groups 92 | $RootWeb = Get-PnPWeb -Includes MembersCanShare 93 | $SubWebs = Get-PnPSubWebs -Recurse 94 | $AllWebs = @() 95 | $AllWebs += $RootWeb 96 | 97 | # Member Invites Disabled 98 | if ($SubWebs) { 99 | foreach ($SubWeb in $SubWebs) { 100 | $Sub = Get-PnPWeb -Identity $SubWeb -Includes MembersCanShare, AssociatedMemberGroup 101 | $AllWebs += $Sub 102 | $MemberGroupTitle = $Sub.AssociatedMemberGroup.Title 103 | if ($MemberGroupTitle) { 104 | $MemberGroup = Get-PnPGroup -Identity $MemberGroupTitle -Includes AllowMembersEditMembership 105 | if ($MemberGroup.AllowMembersEditMembership) { 106 | $GroupTitle = $MemberGroup.Title 107 | Set-PnPGroup -Identity $GroupTitle -AllowMembersEditMembership $False 108 | Write-Host "$GroupTitle - Member Invites Disabled" -Fore Yellow 109 | } 110 | else { 111 | Write-Host "$GroupTitle - Member Invites Already Disabled" -Fore DarkCyan 112 | } 113 | } 114 | else { 115 | Write-Host "No Member Group Found" -Fore Red 116 | } 117 | } 118 | } 119 | 120 | # Disable MembersCanShare in All Webs 121 | foreach ($Web in $AllWebs) { 122 | $WebTitle = $Web.ServerRelativeUrl 123 | if ($Web.MembersCanShare) { 124 | $Web.MembersCanShare = $false 125 | $Web.Update() 126 | $Web.Context.ExecuteQuery() 127 | Write-Host "$WebTitle - MembersCanShare Disabled" -Fore Yellow 128 | } 129 | else { 130 | Write-Host "$WebTitle - MembersCanShare Already Disabled" -Fore DarkCyan 131 | } 132 | } 133 | 134 | # Enable Audit 135 | $audit = Get-PnPAuditing 136 | 137 | if ($audit.AuditFlags -eq "All") { 138 | Write-Host "Audit OK" -Fore Green 139 | } 140 | else { 141 | Write-Host "Enabling Audit" -Fore Yellow 142 | Set-PnPAuditing -RetentionTime 30 -TrimAuditLog -EnableAll 143 | } 144 | 145 | # Add Custom JS 146 | $scriptName = $settings.settings.scriptName 147 | $scriptUrl = $settings.settings.scriptUrl 148 | 149 | # Detect Custom JS 150 | $jsFound = Get-PnPJavaScriptLink -Scope All |? {$_.Name -eq $scriptName} 151 | if ($jsFound) { 152 | # Found 153 | Write-Host "Found $scriptName" -Fore Green 154 | $jsFound | Format-Table -a 155 | } 156 | else { 157 | # Add JS Script Link missing 158 | Write-Host "Adding $scriptName" -Fore Yellow 159 | Add-PnPJavaScriptLink -Name $scriptName -Url $scriptUrl -Sequence 99 -Scope Site 160 | } 161 | } 162 | 163 | #Open session to local host 164 | $farmpw = ConvertTo-SecureString -String $settings.settings.farmPass -AsPlainText -Force 165 | $farmCred = New-Object System.Management.Automation.PSCredential ($settings.settings.farmUserName, $farmpw) 166 | $s = New-PSSession -ComputerName $env:COMPUTERNAME -Credential $farmCred -Authentication Credssp 167 | 168 | # Parallel PNP to apply standard settings to destiation sites 169 | $numWorkers = $settings.settings.workers 170 | $pending = $tracker |? {$_.State -ne "Complete"} 171 | do { 172 | #Pull active workers 173 | $pending = $tracker |? {$_.State -ne "Complete"} 174 | $activeWorkers = Get-Job |? {$_.State -eq "Running" -or $_.State -eq "NotStarted"} 175 | 176 | if ($activeWorkers.count -lt $numWorkers -and $pending) { 177 | #Create new worker 178 | $nextSite = $pending[0].Url 179 | $found = $tracker |? {$_.UPN -eq $pending[0].UPN} 180 | $found.State = "Complete" 181 | Write-Host "Next site..." -ForegroundColor Yellow 182 | Invoke-Command -ScriptBlock $sb -ArgumentList $nextSite -AsJob -Session $s 183 | } 184 | $activeWorkers 185 | 186 | #Clean up 187 | $idleWorkers = Get-Job |? {$_.State -ne "Running" -and $_.State -ne "NotStarted"} 188 | $idleWorkers | Remove-Job 189 | } 190 | while ($pending) 191 | } 192 | Main 193 | 194 | # Time stamps 195 | $endTime = (Get-Date) 196 | $totalSec = (($endTime -$startTime).totalseconds) 197 | $timeSpan = [timespan]::fromseconds($totalSec) 198 | $timeHours = "{0:HH:mm:ss}" -f ([datetime]$timeSpan.Ticks) 199 | 200 | # Log 201 | Write-Host "Total Elapsed Time: $timeHours" -ForegroundColor Green 202 | Stop-Transcript -------------------------------------------------------------------------------- /office365-gpo/onedrive-gpo.xml: -------------------------------------------------------------------------------- 1 | 2 | tenant 3 | admin@tenant.onmicrosoft.com 4 | pass@word1 5 | admin@tenant.onmicrosoft.com, c:0-.f|rolemanager|s-1-5-21-1234567890-123456789-123456789-1234567 6 | office365-gpo.js 7 | https://tenant.sharepoint.com/SiteAssets/office365-gpo/office365-gpo.js 8 | 4 9 | -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/AdministrationConfig-en.msi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/office365-hybrid-search-cssa/AdministrationConfig-en.msi -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/Cloud Hybrid Search Service Application - SharePoint Escalation Services Team Blog.website: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/office365-hybrid-search-cssa/Cloud Hybrid Search Service Application - SharePoint Escalation Services Team Blog.website -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/Configure cloud hybrid search - roadmap.website: -------------------------------------------------------------------------------- 1 | [{000214A0-0000-0000-C000-000000000046}] 2 | Prop3=19,11 3 | Prop4=31,Configure cloud hybrid search - roadmap 4 | [InternetShortcut] 5 | IDList= 6 | URL=https://technet.microsoft.com/en-us/library/dn720906.aspx 7 | [{A7AF692E-098D-4C08-A225-D433CA835ED0}] 8 | Prop5=3,0 9 | Prop9=19,0 10 | [{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}] 11 | Prop5=8,Microsoft.Website.B4BD2547.B7D34BFA 12 | -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/CreateCloudSSA.ps1: -------------------------------------------------------------------------------- 1 | ## Gather mandatory parameters ## 2 | ## Note: SearchServiceAccount needs to already exist in Windows Active Directory as per Technet Guidelines https://technet.microsoft.com/library/gg502597.aspx ## 3 | Param( 4 | [Parameter(Mandatory=$true)][string] $SearchServerName, 5 | [Parameter(Mandatory=$true)][string] $SearchServiceAccount, 6 | [Parameter(Mandatory=$true)][string] $SearchServiceAppName, 7 | [Parameter(Mandatory=$true)][string] $DatabasePrefix, 8 | [Parameter(Mandatory=$true)][string] $DatabaseServerName 9 | ) 10 | Start-Transcript 11 | (Get-Date) 12 | Add-PSSnapin Microsoft.SharePoint.Powershell -ea 0 13 | ## Validate if the supplied account exists in Active Directory and whether supplied as domain\username 14 | if ($SearchServiceAccount.Contains("\")) { 15 | # if True then domain\username was used 16 | $Account = $SearchServiceAccount.Split("\") 17 | $Account = $Account[1] 18 | } else { 19 | # no domain was specified at account entry 20 | $Account = $SearchServiceAccount 21 | } 22 | 23 | $domainRoot = New-Object System.DirectoryServices.DirectoryEntry 24 | $dirSearcher = New-Object System.DirectoryServices.DirectorySearcher($domainRoot) 25 | $dirSearcher.filter = "(&(objectClass=user)(sAMAccountName=$Account))" 26 | $results = $dirSearcher.findall() 27 | if ($results.Count -gt 0) { 28 | # Test for user not found 29 | 30 | Write-Output "Active Directory account $Account exists. Proceeding with configuration" 31 | ## Validate whether the supplied SearchServiceAccount is a managed account. If not make it one. 32 | if(Get-SPManagedAccount | ?{$_.username -eq $SearchServiceAccount}) { 33 | Write-Output "Managed account $SearchServiceAccount already exists!" 34 | } 35 | else { 36 | Write-Output "Managed account does not exists � creating it" 37 | $ManagedCred = Get-Credential -Message "Please provide the password for $SearchServiceAccount" -UserName $SearchServiceAccount 38 | try { 39 | New-SPManagedAccount -Credential $ManagedCred 40 | } 41 | catch { 42 | Write-Output "Unable to create managed account for $SearchServiceAccount. Please validate user and domain details" 43 | break 44 | } 45 | } 46 | Write-Output "Creating Application Pool" 47 | $appPoolName=$SearchServiceAppName+"_AppPool" 48 | $appPool = Get-SPServiceApplicationPool $appPoolName -ErrorAction SilentlyContinue 49 | if (!$appPool) { 50 | $appPool = New-SPServiceApplicationPool -name $appPoolName -account $SearchServiceAccount 51 | Write-Output "APPPOOL created - $appPoolName" 52 | } else { 53 | Write-Output "APPPOOL found - $appPoolName" 54 | } 55 | Write-Output "Starting Search Service Instance" 56 | Start-SPEnterpriseSearchServiceInstance $SearchServerName 57 | 58 | Write-Output "Creating Cloud Search Service Application" 59 | 60 | $searchApp = New-SPEnterpriseSearchServiceApplication -Name $SearchServiceAppName -ApplicationPool $appPool -DatabaseName "$DatabasePrefix$SearchServiceAppName" -DatabaseServer $DatabaseServerName -CloudIndex $true 61 | Write-Output "Configuring Admin Component" 62 | $searchInstance = Get-SPEnterpriseSearchServiceInstance $SearchServerName 63 | $searchApp | get-SPEnterpriseSearchAdministrationComponent | set-SPEnterpriseSearchAdministrationComponent -SearchServiceInstance $searchInstance 64 | $admin = ($searchApp | get-SPEnterpriseSearchAdministrationComponent) 65 | Write-Output "Waiting for the admin component to be initialized" 66 | $timeoutTime=(Get-Date).AddMinutes(20) 67 | do { 68 | Write-Output .;Start-Sleep 10; 69 | } while ((-not $admin.Initialized) -and ($timeoutTime -ge (Get-Date))) 70 | if (-not $admin.Initialized) { 71 | throw "Admin Component could not be initialized" 72 | } 73 | 74 | Write-Output "Inspecting Cloud Search Service Application" 75 | 76 | $searchApp = Get-SPEnterpriseSearchServiceApplication $SearchServiceAppName 77 | 78 | Write-Output "Setting IsHybrid Property to 1" 79 | $searchApp.SetProperty("IsHybrid",1) 80 | #Output some key properties of the Search Service Application 81 | 82 | Write-Host "Search Service Properties" 83 | Write-Host "Hybrid Cloud SSA Name : " $searchapp.Name 84 | Write-Host "Hybrid Cloud SSA Status : " $searchapp.Status 85 | Write-Host "Cloud Index Enabled : " $searchApp.CloudIndex 86 | Write-Output "Configuring Search Topology" 87 | $searchApp = Get-SPEnterpriseSearchServiceApplication $SearchServiceAppName 88 | $topology = $searchApp.ActiveTopology.Clone() 89 | 90 | $oldComponents = @($topology.GetComponents()) 91 | 92 | if (@($oldComponents | ? { $_.GetType().Name -eq "AdminComponent" }).Length -eq 0) { 93 | $topology.AddComponent((New-Object Microsoft.Office.Server.Search.Administration.Topology.AdminComponent $SearchServerName)) 94 | } 95 | $topology.AddComponent((New-Object Microsoft.Office.Server.Search.Administration.Topology.CrawlComponent $SearchServerName)) 96 | $topology.AddComponent((New-Object Microsoft.Office.Server.Search.Administration.Topology.ContentProcessingComponent $SearchServerName)) 97 | $topology.AddComponent((New-Object Microsoft.Office.Server.Search.Administration.Topology.AnalyticsProcessingComponent $SearchServerName)) 98 | $topology.AddComponent((New-Object Microsoft.Office.Server.Search.Administration.Topology.QueryProcessingComponent $SearchServerName)) 99 | $topology.AddComponent((New-Object Microsoft.Office.Server.Search.Administration.Topology.IndexComponent $SearchServerName,0)) 100 | 101 | $oldComponents |? { $_.GetType().Name -ne "AdminComponent" } | foreach { $topology.RemoveComponent($_) } 102 | Write-Output "Activating topology" 103 | $topology.Activate() 104 | $timeoutTime=(Get-Date).AddMinutes(20) 105 | do { 106 | Write-Output . 107 | Start-Sleep 10 108 | } while (($searchApp.GetTopology($topology.TopologyId).State -ne "Active") -and ($timeoutTime -ge (Get-Date))) 109 | if ($searchApp.GetTopology($topology.TopologyId).State -ne "Active") { 110 | throw 'Could not activate the search topology' 111 | } 112 | Write-Output "Creating Proxy" 113 | $searchAppProxy = new-spenterprisesearchserviceapplicationproxy -name ($SearchServiceAppName+"_proxy") -SearchApplication $searchApp 114 | Write-Output " Cloud hybrid search service application provisioning completed successfully." 115 | } 116 | else { 117 | # The Account Must Exist so we can proceed with the script 118 | Write-Output "Account supplied for Search Service does not exist in Active Directory." 119 | Write-Output "Script is quitting. Please create the account and run again." 120 | 121 | Break 122 | } # End Else 123 | Stop-Transcript -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/Delete-CloudHybridSearchContent.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Issue a call to SharePoint Online to delete all metadata from on-premises content that was 4 | indexed through cloud hybrid search. This operation is asynchronous. 5 | .PARAMETER PortalUrl 6 | SharePoint Online portal URL, for example 'https://tenant.sharepoint.com'. 7 | .PARAMETER Credential 8 | Logon credential for tenant admin. Will prompt for credential if not specified. 9 | #> 10 | param( 11 | [Parameter(Mandatory=$true, HelpMessage="SharePoint Online portal URL, for example 'https://tenant.sharepoint.com'.")] 12 | [ValidateNotNullOrEmpty()] 13 | [String] $PortalUrl, 14 | 15 | [Parameter(Mandatory=$false, HelpMessage="Logon credential for tenant admin. Will be prompted if not specified.")] 16 | [PSCredential] $Credential 17 | ) 18 | 19 | $SP_VERSION = "15" 20 | $regKey = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Office Server\15.0\Search" -ErrorAction SilentlyContinue 21 | if ($regKey -eq $null) { 22 | $regKey = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Office Server\16.0\Search" -ErrorAction SilentlyContinue 23 | if ($regKey -eq $null) { 24 | throw "Unable to detect SharePoint Server installation." 25 | } 26 | $SP_VERSION = "16" 27 | } 28 | 29 | Add-Type -AssemblyName ("Microsoft.SharePoint.Client, Version=$SP_VERSION.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") 30 | Add-Type -AssemblyName ("Microsoft.SharePoint.Client.Search, Version=$SP_VERSION.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") 31 | Add-Type -AssemblyName ("Microsoft.SharePoint.Client.Runtime, Version=$SP_VERSION.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c") 32 | 33 | if ($Credential -eq $null) { 34 | $Credential = Get-Credential -Message "SharePoint Online tenant admin credential" 35 | } 36 | 37 | $context = New-Object Microsoft.SharePoint.Client.ClientContext($PortalUrl) 38 | $spocred = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Credential.UserName, $Credential.Password) 39 | $context.Credentials = $spocred 40 | 41 | $manager = New-Object Microsoft.SharePoint.Client.Search.ContentPush.PushTenantManager $context 42 | $task = $manager.DeleteAllCloudHybridSearchContent() 43 | $context.ExecuteQuery() 44 | 45 | Write-Host "Started delete task (id=$($task.Value))" 46 | -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/HybridSPSetup.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/office365-hybrid-search-cssa/HybridSPSetup.exe -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/Install-HybridSearchConfigPreqrequisites.ps1: -------------------------------------------------------------------------------- 1 | # This script installs the two required prerequisites: 2 | # AdministrationConfig-EN.msi and msoidcli_64.msi. 3 | # It is assumed that these are available in the same folder as the script itself. 4 | # See the following links for downloading manually: 5 | # – http://www.microsoft.com/en-us/download/details.aspx?id=39267 6 | # – http://go.microsoft.com/fwlink/p/?linkid=236297 7 | # 8 | function Install-MSI { 9 | param( 10 | [Parameter(Mandatory=$true)] 11 | [ValidateNotNullOrEmpty()] 12 | [String] $path 13 | ) 14 | $parameters = "/qn /i " + $path 15 | $installStatement = [System.Diagnostics.Process]::Start( "msiexec", $parameters ) 16 | $installStatement.WaitForExit() 17 | } 18 | $scriptFolder = Split-Path $script:MyInvocation.MyCommand.Path 19 | $MSOIdCRLRegKey = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\MSOIdentityCRL" -ErrorAction SilentlyContinue 20 | if ($MSOIdCRLRegKey -eq $null) { 21 | Write-Host "Installing Office Single Sign On Assistant" -Foreground Yellow 22 | Install-MSI ($scriptFolder + "\msoidcli_64.msi") 23 | Write-Host "Successfully installed!" -Foreground Green 24 | } 25 | else { 26 | Write-Host "Office Single Sign On Assistant is already installed." -Foreground Green 27 | } 28 | $MSOLPSRegKey = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\MSOnlinePowershell" -ErrorAction SilentlyContinue 29 | if ($MSOLPSRegKey -eq $null) { 30 | Write-Host "Installing AAD PowerShell" -Foreground Yellow 31 | Install-MSI ($scriptFolder + "\AdministrationConfig-EN.msi") 32 | Write-Host "Successfully installed!" -Foreground Green 33 | } 34 | else { 35 | Write-Host "AAD PowerShell is already installed." -Foreground Green 36 | } 37 | -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/Test-HybridSearchConfigPreqrequisites.ps1: -------------------------------------------------------------------------------- 1 | (Get-Service msoidsvc).Status 2 | Import-Module msonline -Verbose 3 | Import-Module msonlineextended -Verbose -------------------------------------------------------------------------------- /office365-hybrid-search-cssa/msoidcli_64.msi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/office365-hybrid-search-cssa/msoidcli_64.msi -------------------------------------------------------------------------------- /office365-migration-banner/o365-migration-banner.js: -------------------------------------------------------------------------------- 1 | // O365 - Migration Banner 2 | function addcss(css) { 3 | var head = document.getElementsByTagName('head')[0]; 4 | var s = document.createElement('style'); 5 | s.setAttribute('type', 'text/css'); 6 | if (s.styleSheet) { 7 | // IE 8 | s.styleSheet.cssText = css; 9 | } else { 10 | // the world 11 | s.appendChild(document.createTextNode(css)); 12 | } 13 | head.appendChild(s); 14 | } 15 | 16 | // CSS Inject 17 | addcss('#status_preview_body {display:none}'); 18 | 19 | // Migration Function 20 | MigrationBannerCount = 0; 21 | function MigrationBanner() { 22 | var sp = document.getElementById('status_preview'); 23 | if (MigrationBannerCount > 10) { 24 | console.log('MigrationBanner - safety, max attempts, to prevent infinite loop'); 25 | //safety, max attempts, to prevent infinite loop 26 | return 27 | } 28 | if (!sp) { 29 | console.log('MigrationBanner - wait and check later'); 30 | //wait and check later 31 | MigrationBannerCount++; 32 | window.setTimeout(MigrationBanner, 200); 33 | return; 34 | } else { 35 | console.log('MigrationBanner - found and modify'); 36 | //found and modify 37 | var h = sp.innerHTML; 38 | h = h.replace("This site is read only at the farm administrator's request.", '
MIGRATION IN PROGRESS: This site is being moved to Office 365 and will be locked as Read-Only until its migration is complete.
Please check in soon to be automatically redirected to this site’s new location in the cloud. Learn more...
'); 39 | sp.innerHTML = h; 40 | addcss('#status_preview_body {display:inherit}'); 41 | } 42 | } 43 | function MigrationBannerLoad() { 44 | ExecuteOrDelayUntilScriptLoaded(MigrationBanner, "sp.js") 45 | } 46 | // Delay Load 47 | window.setTimeout('MigrationBannerLoad()', 1000); -------------------------------------------------------------------------------- /office365-migration/office365-CreatePersonalSiteEnqueueBulk.ps1: -------------------------------------------------------------------------------- 1 | # CSOM method to provision MySite /personal/ sites in Office 365 2 | 3 | # tenant 4 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") 5 | $webUrl = "https://tenant.sharepoint.com" 6 | $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($webUrl) 7 | 8 | # admin user 9 | $web = $ctx.Web 10 | $username = "admin@tenant.onmicrosoft.com" 11 | $password = read-host -AsSecureString 12 | 13 | # context 14 | $ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username,$password ) 15 | $ctx.Load($web) 16 | $ctx.ExecuteQuery() 17 | 18 | # assembly 19 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.UserProfiles") 20 | $loader =[Microsoft.SharePoint.Client.UserProfiles.ProfileLoader]::GetProfileLoader($ctx) 21 | 22 | #To Get Profile 23 | $profile = $loader.GetUserProfile() 24 | $ctx.Load($profile) 25 | $ctx.ExecuteQuery() 26 | $profile 27 | 28 | #To enqueue Profile 29 | $loader.CreatePersonalSiteEnqueueBulk(@("user@tenant.onmicrosoft.com")) 30 | $loader.Context.ExecuteQuery() -------------------------------------------------------------------------------- /office365-migration/office365-migration-report.sql: -------------------------------------------------------------------------------- 1 | SELECT G.*, 2 | A.[Files-XOML],A.[Files-XSN],A.[Files-CSS],A.[Files-JS],A.[Files-JQuery],A.[Files-Angular],A.[Files-Bootstrap],A.[Files-UrlOver260],A.[Files-CheckedOut], 3 | B.[Lists-Over5KItems],B.[Lists-Unthrottled],B.[Lists-NumInboundEmail],B.[Lists-LastModifiedInboundEmail],B.[Lists-LastModified], 4 | C.[Feature-PublishingSite],C.[Feature-MinimalPublishingSite], 5 | D.[Feature-PublishingWeb],D.[Feature-MinimalPublishingWeb], 6 | E.[Alerts-Immed], 7 | F.[Alerts-Sched] 8 | FROM 9 | ( 10 | SELECT Webs.Id, 11 | SUM(CASE WHEN ((AllDocs.ExtensionForFile = 'xoml') AND (AllDocs.DirName NOT LIKE '%_catalogs%')) THEN 1 ELSE 0 END) AS 'Files-XOML', 12 | SUM(CASE WHEN ((AllDocs.ExtensionForFile = 'xsn') AND (AllDocs.DirName NOT LIKE '%_catalogs%')) THEN 1 ELSE 0 END) AS 'Files-XSN', 13 | SUM(CASE WHEN ((AllDocs.ExtensionForFile = 'css') AND (AllDocs.DirName NOT LIKE '%_catalogs%') AND (AllDocs.DirName NOT LIKE '%en-us%Core Styles%')) THEN 1 ELSE 0 END) AS 'Files-CSS', 14 | SUM(CASE WHEN ((AllDocs.ExtensionForFile = 'js') AND (AllDocs.DirName NOT LIKE '%_catalogs%')) THEN 1 ELSE 0 END) AS 'Files-JS', 15 | SUM(CASE WHEN ((AllDocs.LeafName LIKE '%jquery%js') AND (AllDocs.DirName NOT LIKE '%_catalogs%')) THEN 1 ELSE 0 END) AS 'Files-JQuery', 16 | SUM(CASE WHEN ((AllDocs.LeafName LIKE '%angular%js') AND (AllDocs.DirName NOT LIKE '%_catalogs%')) THEN 1 ELSE 0 END) AS 'Files-Angular', 17 | SUM(CASE WHEN ((AllDocs.LeafName LIKE '%bootstrap%css') AND (AllDocs.DirName NOT LIKE '%_catalogs%')) THEN 1 ELSE 0 END) AS 'Files-Bootstrap', 18 | SUM(CASE WHEN (LEN(AllDocs.DirName)+LEN(AllDocs.LeafName)+1>=260 AND AllDocs.IsCurrentVersion = 1 AND AllDocs.DeleteTransactionId = 0 ) THEN 1 ELSE 0 END) AS 'Files-UrlOver260', 19 | SUM(CASE WHEN (AllDocs.DeleteTransactionId=0 AND AllDocs.IsCurrentVersion=1 AND (AllDocs.CheckoutUserId IS NOT NULL OR AllDocs.LTCheckoutUserId IS NOT NULL OR Level=255)) THEN 1 ELSE 0 END) AS 'Files-CheckedOut' 20 | 21 | 22 | FROM Webs 23 | LEFT JOIN AllDocs 24 | ON Webs.Id = AllDocs.WebId 25 | GROUP BY Webs.Id 26 | ) AS A 27 | INNER JOIN 28 | ( 29 | SELECT Webs.Id, 30 | SUM(CASE WHEN AllListsAux.ItemCount > 5000 THEN 1 ELSE 0 END) AS 'Lists-Over5KItems', 31 | SUM(CASE WHEN AllLists.tp_NoThrottleListOperations <> 0 THEN 1 ELSE 0 END) AS 'Lists-Unthrottled', 32 | SUM(CASE WHEN AllLists.tp_EmailAlias IS NOT NULL THEN 1 ELSE 0 END) AS 'Lists-NumInboundEmail', 33 | MAX(CASE WHEN AllLists.tp_EmailAlias IS NOT NULL THEN AllListsAux.Modified ELSE 0 END) AS 'Lists-LastModifiedInboundEmail', 34 | MAX(AllListsAux.Modified) AS 'Lists-LastModified' 35 | FROM Webs 36 | LEFT JOIN AllLists 37 | ON Webs.Id = AllLists.tp_WebId 38 | LEFT JOIN AllListsAux 39 | ON AllLists.tp_Id = AllListsAux.ListId 40 | GROUP BY Webs.Id 41 | ) AS B 42 | ON A.Id=B.Id 43 | INNER JOIN 44 | ( 45 | SELECT Webs.Id, 46 | SUM(CASE WHEN F1.FeatureId = 'F6924D36-2FA8-4F0B-B16D-06B7250180FA' THEN 1 ELSE 0 END) AS 'Feature-PublishingSite', 47 | SUM(CASE WHEN F1.FeatureId = '63FDC6AC-DBB4-4247-B46E-A091AEFC866F' THEN 1 ELSE 0 END) AS 'Feature-MinimalPublishingSite' 48 | FROM Webs 49 | LEFT JOIN Features AS F1 50 | ON Webs.SiteId = F1.SiteId 51 | GROUP BY Webs.Id 52 | ) AS C 53 | ON A.Id=C.Id 54 | INNER JOIN 55 | ( 56 | SELECT Webs.Id, 57 | SUM(CASE WHEN F2.FeatureId = '94C94CA6-B32F-4DA9-A9E3-1F3D343D7ECB' THEN 1 ELSE 0 END) AS 'Feature-PublishingWeb', 58 | SUM(CASE WHEN F2.FeatureId = 'A4A489B1-5420-40C3-8DB7-247C9FC51CA9' THEN 1 ELSE 0 END) AS 'Feature-MinimalPublishingWeb' 59 | FROM Webs 60 | LEFT JOIN Features AS F2 61 | ON Webs.Id = F2.WebId 62 | GROUP BY Webs.Id 63 | ) AS D 64 | ON A.Id=D.Id 65 | INNER JOIN 66 | ( 67 | SELECT Webs.Id, 68 | SUM(CASE WHEN ImmedSubscriptions.Id IS NULL THEN 0 ELSE 1 END) AS 'Alerts-Immed' 69 | FROM Webs 70 | LEFT JOIN ImmedSubscriptions 71 | ON Webs.Id = ImmedSubscriptions.WebId 72 | GROUP BY Webs.Id 73 | ) AS E 74 | ON A.Id=E.Id 75 | INNER JOIN 76 | ( 77 | SELECT Webs.Id, 78 | SUM(CASE WHEN SchedSubscriptions.Id IS NULL THEN 0 ELSE 1 END) AS 'Alerts-Sched' 79 | FROM Webs 80 | LEFT JOIN SchedSubscriptions 81 | ON Webs.Id = SchedSubscriptions.WebId 82 | GROUP BY Webs.Id 83 | ) AS F 84 | ON A.Id=F.Id 85 | INNER JOIN 86 | ( 87 | SELECT Id,FullUrl,title,RequestAccessEmail,WebTemplate,AlternateCSSUrl,CustomJSUrl,MasterUrl,CustomMasterUrl 88 | FROM Webs 89 | ) AS G 90 | ON A.Id=G.Id -------------------------------------------------------------------------------- /office365-msteams-guest-report/O365-MSTeams-Guest-Report.ps1: -------------------------------------------------------------------------------- 1 | # from https://techcommunity.microsoft.com/t5/microsoft-teams/microsoft-teams-tenant-wide-csv-report/m-p/151875 2 | # from https://docs.microsoft.com/en-us/powershell/exchange/connect-to-exchange-online-powershell?view=exchange-ps 3 | 4 | ## Created by SAMCOS @ MSFT, Collaboration with others! 5 | ## You must first connect to Microsoft Teams Powershell & Exchange Online Powershell for this to work. 6 | ## Links: 7 | ## Teams: https://www.powershellgallery.com/packages/MicrosoftTeams/1.0.0 8 | ## Exchange: https://docs.microsoft.com/en-us/powershell/exchange/exchange-online/connect-to-exchange-online-powershell/connect-to-exchange-online-powershell?view=exchange-ps 9 | ## Have fun! Let me know if you have any comments or asks! 10 | 11 | # Transcript 12 | Start-Transcript 13 | 14 | # Install 15 | Install-Module "ExchangeOnlineManagement" 16 | Install-Module -Name "MicrosoftTeams" 17 | 18 | # Import 19 | Import-Module "ExchangeOnlineManagement" 20 | Import-Module -Name "MicrosoftTeams" 21 | 22 | # Connect 23 | Connect-ExchangeOnline 24 | Connect-MicrosoftTeams 25 | 26 | # Default 27 | $AllTeamsInOrg = (Get-Team).GroupID 28 | $TeamList = @() 29 | 30 | # Loop 31 | Write-Output "This may take a little bit of time... Please sit back, relax and enjoy some GIFs inside of Teams!" 32 | Foreach ($Team in $AllTeamsInOrg) { 33 | # Parse Inputs 34 | $TeamGUID = $Team.ToString() 35 | $TeamGroup = Get-UnifiedGroup -identity $Team.ToString() 36 | $TeamName = (Get-Team | ? { $_.GroupID -eq $Team }).DisplayName 37 | $TeamOwner = (Get-TeamUser -GroupId $Team | ? { $_.Role -eq 'Owner' }).User 38 | $TeamUserCount = ((Get-TeamUser -GroupId $Team).UserID).Count 39 | $TeamCreationDate = Get-unifiedGroup -identity $team.ToString() | Select -expandproperty WhenCreatedUTC 40 | $TeamGuest = (Get-UnifiedGroupLinks -LinkType Members -identity $Team | ? { $_.Name -match "#EXT#" }).Name 41 | 42 | # Zero Guests 43 | if ($TeamGuest -eq $null) { 44 | $TeamGuest = "No Guests in Team" 45 | } 46 | 47 | # Append for CSV 48 | $TeamList = $TeamList + [PSCustomObject]@{ 49 | TeamName = $TeamName; 50 | TeamObjectID = $TeamGUID; 51 | TeamCreationDate = $TeamCreationDate; 52 | TeamOwners = $TeamOwner -join ', '; 53 | TeamMemberCount = $TeamUserCount; 54 | TeamSite = $TeamGroup.SharePointSiteURL; 55 | AccessType = $TeamGroup.AccessType; 56 | TeamGuests = $TeamGuest -join ',' 57 | } 58 | } 59 | 60 | # Write CSV 61 | $TempFolder = "c:\temp" 62 | New-Item -ItemType "Directory" -Path $TempFolder -ErrorAction "SilentlyContinue" | Out-Null 63 | $TeamList | Export-Csv "$TempFolder\TeamsDatav2.csv" -NoTypeInformation 64 | 65 | # Transcript 66 | Stop-Transcript -------------------------------------------------------------------------------- /office365-permission-report/Enumerate_Permissions.ps1: -------------------------------------------------------------------------------- 1 | #Adapted from: https://github.com/OfficeDev/PnP/blob/master/Samples/Core.PermissionListing/Core.PermissionListingWeb/Pages/Default.aspx.cs 2 | #Help from gary LaPointe: https://www.itunity.com/article/loading-specific-values-lambda-expressions-sharepoint-csom-api-windows-powershell-1249 3 | #Only logs things with unique permissions, subsites and list which inherit from the parent are not included in the output 4 | 5 | 6 | #region configuration 7 | #the filename and path to store the file 8 | $filename = "C:\Users\$env:USERNAME\Desktop\PermissionsReport.csv" 9 | $domainSuffix = "*domain.com" 10 | 11 | #email configuration options 12 | $To = "someone@domain.com" 13 | $From = "no-reply@sharepointonline.com" 14 | $Subject = "SharePoint Online Permissions Report" 15 | $Body = "Attached is the output of the permissions report." 16 | $SMTPServer = "smtp.domian.com" 17 | #endregion 18 | 19 | #region functions 20 | function Process-RoleAssignments($securableObject, $clientContext, $SiteUrl){ 21 | $objResults = @() 22 | #This line was from the PnP implementation, since there are no lambdas in PowerShell 23 | #we use Gary's Get-CSOMProperties function to get it 24 | #$clientContext.Load($securableObject, $x => $x.HasUniqueRoleAssignments) 25 | Load-CSOMProperties -object $securableObject -propertyNames @("HasUniqueRoleAssignments") 26 | $clientContext.ExecuteQuery() 27 | 28 | #if the object has unique permissions we will process it, if it inherits from the parent we skip it 29 | if($securableObject.HasUniqueRoleAssignments){ 30 | $roleAssignments = $securableObject.RoleAssignments 31 | $clientContext.Load($roleAssignments) 32 | $clientContext.ExecuteQuery() 33 | 34 | foreach ($roleAssignment in $roleAssignments){ 35 | $member = $roleAssignment.Member 36 | $roleDef = $roleAssignment.RoleDefinitionBindings 37 | 38 | $clientContext.Load($member) 39 | $clientContext.Load($roleDef) 40 | $clientContext.ExecuteQuery() 41 | 42 | foreach ($binding in $roleDef){ 43 | #We are skipping role bindings of limited access, they should get picked up at another point 44 | if($binding.Name -ne "Limited Access" ){ 45 | #write-host "$($member.PrincipalType) $($member.LoginName) $($binding.Name)" -ForegroundColor White 46 | #if the principal type is a SharePointGroup 47 | if($member.PrincipalType -eq "SharePointGroup"){ 48 | #Get the group membership 49 | $group = Get-SPOSiteGroup -Site $SiteUrl -Group $member.LoginName 50 | #if the group has users in it 51 | if($group.Users.Count -gt 0){ 52 | #run the group members through the group processing function to get the display names 53 | $groupMembership = Process-GroupMembers -SiteUrl $SiteUrl -members $group.Users 54 | } 55 | else{ 56 | #an empty group has permission to the object, need to log it for transparency 57 | $groupMembership = "Empty Group" 58 | } 59 | $objResults += New-Object PSObject -Property @{ 60 | "Principal" = $member.LoginName 61 | "Role" = $binding.Name 62 | "Members" = $groupMembership 63 | "Everyone" = $group.Users.Contains("true") 64 | "EveryoneExcept" = $group.Users.Contains("spo-grid-all-users") 65 | "NTAuthority" = $group.Users.Contains("windows") 66 | } 67 | } 68 | #otherwise it is a user 69 | else { 70 | #run the user through the group processing function to get the display name 71 | $userName = Process-GroupMembers -SiteUrl $SiteUrl -members $member.LoginName.Split("|")[2] 72 | $objResults += New-Object PSObject -Property @{ 73 | "Principal" = "Explicit User" 74 | "Role" = $binding.Name 75 | "Members" = $userName 76 | "Everyone" = $($member.LoginName.Split("|")[1] -eq "true") 77 | "EveryoneExcept" = $($member.LoginName.Split("|")[2] -like "spo-grid-all-users*") 78 | "NTAuthority" = $($member.LoginName.Split("|")[1] -eq "windows") 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | return $objResults 87 | } 88 | function Process-GroupMembers($SiteUrl, $members){ 89 | #The output of the get-spogroup gives the users login name 90 | #this function processes the results and returns the membership as display names of people and groups 91 | $returnMembers = @() 92 | foreach($member in $members){ 93 | #Everyone claim 94 | if($member -eq "true"){ 95 | $returnMembers += "Everyone" 96 | } 97 | #Everyone except external users claim 98 | elseif($member -like "spo-grid-all-users*"){ 99 | $returnMembers += "Everyone except external users" 100 | } 101 | #Sharepoint account 102 | elseif($member -eq "SHAREPOINT\system"){ 103 | $returnMembers += $member 104 | } 105 | #a named user 106 | elseif($member -like $domainSuffix){ 107 | $user = get-aduser -Filter {mail -eq $member} 108 | $returnMembers += $user.Name 109 | } 110 | #an AD group 111 | elseif($member -like "s-1*"){ 112 | $group = get-spouser -Site $SiteUrl -LoginName "c:0-.f|rolemanager|$member" 113 | $returnMembers += $group.DisplayName 114 | } 115 | #windows claim, probaly only seen if migrated to SPO from on-prem 116 | elseif($member -eq "windows"){ 117 | $returnMembers += "NT AUTHORITY\authenticated users" 118 | } 119 | #if it didn't match above, IDK what it is! 120 | else{ 121 | $returnMembers += $member 122 | } 123 | } 124 | #return the results 125 | return $returnMembers 126 | } 127 | #endregion 128 | 129 | #capture output 130 | $Results = @() 131 | #get sites 132 | $sites = get-sposite -detailed -limit All 133 | 134 | #process sites 135 | foreach($site in $sites){ 136 | #only looking at project and team sites 137 | if($site.Template -eq "PROJECTSITE#0" -or $site.Template -eq "STS#0"){ 138 | write-host "Processing site collection - $($site.Title)" 139 | $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($site.Url) 140 | $ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($credential.UserName,$credential.Password) 141 | 142 | #root site 143 | $web = $ctx.Web 144 | $ctx.Load($web) 145 | #Get the web permissions 146 | $rootResults = Process-RoleAssignments -securableObject $web -clientContext $ctx -SiteUrl $site.Url 147 | foreach($webresult in $rootResults){ 148 | #Build a new object from the results 149 | $results += New-Object PSObject -Property @{ 150 | "Site Name" = $site.Title 151 | "URL" = $site.Url 152 | "SecurableObject" = $web.Title 153 | "Principal" = $webresult.Principal 154 | "Role" = $webresult.Role 155 | "Members" = $webresult.Members 156 | "Everyone" = $webresult.Everyone 157 | "EveryoneExcept" = $webresult.EveryoneExcept 158 | "NTAuthority" = $webresult.NTAuthority 159 | } 160 | } 161 | 162 | #root site Lists 163 | $lists = $web.Lists 164 | $ctx.Load($lists) 165 | $ctx.ExecuteQuery() 166 | foreach ($list in $lists){ 167 | #skipping over hidden lists 168 | if(!$list.Hidden){ 169 | $ctx.Load($list.RootFolder) 170 | $ctx.ExecuteQuery() 171 | 172 | $rootListResults = Process-RoleAssignments -securableObject $list -clientContext $ctx -SiteUrl $site.Url 173 | foreach($listresult in $rootListResults){ 174 | $results += New-Object PSObject -Property @{ 175 | "Site Name" = $site.Title 176 | "URL" = $list.RootFolder.ServerRelativeUrl 177 | "SecurableObject" = "List - $($list.Title)" 178 | "Principal" = $listresult.Principal 179 | "Role" = $listresult.Role 180 | "Members" = $listresult.Members 181 | "Everyone" = $listresult.Everyone 182 | "EveryoneExcept" = $listresult.EveryoneExcept 183 | "NTAuthority" = $listresult.NTAuthority 184 | } 185 | } 186 | } 187 | } 188 | 189 | if($site.WebsCount -gt 0){ 190 | #subsites 191 | $webs = $web.Webs 192 | $ctx.Load($webs) 193 | $ctx.ExecuteQuery() 194 | 195 | foreach ($subWeb in $webs){ 196 | $subWebResults = Process-RoleAssignments -securableObject $subWeb -clientContext $ctx -SiteUrl $site.Url 197 | foreach($subwebresult in $subWebResults){ 198 | $results += New-Object PSObject -Property @{ 199 | "Site Name" = $site.Title 200 | "URL" = $subWeb.Url 201 | "SecurableObject" = "Subsite - $($subWeb.Title)" 202 | "Principal" = $subwebresult.Principal 203 | "Role" = $subwebresult.Role 204 | "Members" = $subwebresult.Members 205 | "Everyone" = $subwebresult.Everyone 206 | "EveryoneExcept" = $subwebresult.EveryoneExcept 207 | "NTAuthority" = $subwebresult.NTAuthority 208 | } 209 | } 210 | #subsite lists 211 | $subLists = $subWeb.Lists 212 | $ctx.Load($subLists) 213 | $ctx.ExecuteQuery() 214 | foreach ($subList in $subLists){ 215 | #skipping over hidden lists 216 | if(!$subList.Hidden){ 217 | $ctx.Load($subList.RootFolder) 218 | $ctx.ExecuteQuery() 219 | 220 | $subWebListResults = Process-RoleAssignments -securableObject $subList -clientContext $ctx -SiteUrl $site.Url 221 | foreach($subweblistresult in $subWebListResults){ 222 | $results += New-Object PSObject -Property @{ 223 | "Site Name" = $site.Title 224 | "URL" = $subList.RootFolder.ServerRelativeUrl 225 | "SecurableObject" = "List - $($subList.Title)" 226 | "Principal" = $subweblistresult.Principal 227 | "Role" = $subweblistresult.Role 228 | "Members" = $subweblistresult.Members 229 | "Everyone" = $subweblistresult.Everyone 230 | "EveryoneExcept" = $subweblistresult.EveryoneExcept 231 | "NTAuthority" = $subweblistresult.NTAuthority 232 | } 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | 241 | #Pipe the results out to a csv 242 | $Results | Select "Site Name","URL","SecurableObject","Principal", "Role",@{n="Members";e={(@($_.Members) | Out-String).Trim()}},"Everyone", "EveryoneExcept", "NTAuthority" | Export-Csv $filename -NoTypeInformation 243 | Write-Host "Report saved to $filename" -ForegroundColor Green 244 | 245 | #Send email 246 | Send-MailMessage -Attachments $filename -Body $Body -From $From -To $To -Subject $Subject -BodyAsHtml -SmtpServer $SMTPServer -------------------------------------------------------------------------------- /office365-permission-report/Generate a Full Permission Report in PowerShell.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=http://ericjalexander.com/blog/2016/11/20/Full-Permission-Report-PowerShell 3 | -------------------------------------------------------------------------------- /office365-permission-report/GetUniquePermissions_ClientSideCode.ps1: -------------------------------------------------------------------------------- 1 | # from http://ericjalexander.com/blog/2016/11/20/Full-Permission-Report-PowerShell 2 | Clear-Host 3 | Add-PsSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue 4 | Add-Type -Path ".\SharePoint Assemblies\Microsoft.SharePoint.Client.dll" 5 | Add-Type -Path ".\SharePoint Assemblies\Microsoft.SharePoint.Client.Runtime.dll" 6 | 7 | # from Gary Lapointe https://www.itunity.com/article/loading-specific-values-lambda-expressions-sharepoint-csom-api-windows-powershell-1249 8 | $pLoadCSOMProperties = (Get-Location).ToString() + "\Load-CSOMProperties.ps1" 9 | . $pLoadCSOMProperties 10 | 11 | # Defaults 12 | $properties = @{SiteUrl = ''; SiteTitle = ''; ListTitle = ''; Type = ''; RelativeUrl = ''; ParentGroup = ''; MemberType = ''; MemberName = ''; MemberLoginName = ''; Roles = ''; }; 13 | $RootWeb = ""; 14 | $RootSiteTitle = ""; 15 | $ExportFileDirectory = (Get-Location).ToString(); 16 | 17 | # Prompt Input 18 | $SiteCollectionUrl = Read-Host -Prompt "Enter site collection URL: "; 19 | $Username = Read-Host -Prompt "Enter userName: "; 20 | $password = Read-Host -Prompt "Enter password: " -AsSecureString ; 21 | 22 | Function PermissionObject($_object, $_type, $_relativeUrl, $_siteUrl, $_siteTitle, $_listTitle, $_memberType, $_parentGroup, $_memberName, $_memberLoginName, $_roleDefinitionBindings) { 23 | $permission = New-Object -TypeName PSObject -Property $properties; 24 | $permission.SiteUrl = $_siteUrl; 25 | $permission.SiteTitle = $_siteTitle; 26 | $permission.ListTitle = $_listTitle; 27 | $permission.Type = $_type; 28 | $permission.RelativeUrl = $_relativeUrl; 29 | $permission.MemberType = $_memberType; 30 | $permission.ParentGroup = $_parentGroup; 31 | $permission.MemberName = $_memberName; 32 | $permission.MemberLoginName = $_memberLoginName; 33 | $permission.Roles = $_roleDefinitionBindings -join ","; 34 | 35 | ## Write-Host "Site URL: " $_siteUrl "Site Title" $_siteTitle "List Title" $_istTitle "Member Type" $_memberType "Relative URL" $_RelativeUrl "Member Name" $_memberName "Role Definition" $_roleDefinitionBindings -Foregroundcolor "Green"; 36 | return $permission; 37 | } 38 | 39 | 40 | Function QueryUniquePermissionsByObject($_web, $_object, $_Type, $_RelativeUrl, $_siteUrl, $_siteTitle, $_listTitle) { 41 | $_permissions = @(); 42 | 43 | Load-CSOMProperties -object $_object -propertyNames @("RoleAssignments") ; 44 | 45 | $ctx.ExecuteQuery() ; 46 | 47 | foreach ($roleAssign in $_object.RoleAssignments) { 48 | $RoleDefinitionBindings = @(); 49 | Load-CSOMProperties -object $roleAssign -propertyNames @("RoleDefinitionBindings", "Member"); 50 | $ctx.ExecuteQuery() ; 51 | $roleAssign.RoleDefinitionBindings | ForEach-Object { 52 | Load-CSOMProperties -object $_ -propertyNames @("Name"); 53 | $ctx.ExecuteQuery() ; 54 | $RoleDefinitionBindings += $_.Name; 55 | } 56 | 57 | $MemberType = $roleAssign.Member.GetType().Name; 58 | 59 | $collGroups = ""; 60 | if ($_Type -eq "Site") { 61 | $collGroups = $_web.SiteGroups; 62 | $ctx.Load($collGroups); 63 | $ctx.ExecuteQuery() ; 64 | } 65 | 66 | if ($MemberType -eq "Group" -or $MemberType -eq "User") { 67 | 68 | Load-CSOMProperties -object $roleAssign.Member -propertyNames @("LoginName", "Title"); 69 | $ctx.ExecuteQuery() ; 70 | 71 | $MemberName = $roleAssign.Member.Title; 72 | 73 | $MemberLoginName = $roleAssign.Member.LoginName; 74 | 75 | if ($MemberType -eq "User") { 76 | $ParentGroup = "NA"; 77 | } 78 | else { 79 | $ParentGroup = $MemberName; 80 | } 81 | 82 | $_permissions += (PermissionObject $_object $_Type $_RelativeUrl $_siteUrl $_siteTitle $_listTitle $MemberType $ParentGroup $MemberName $MemberLoginName $RoleDefinitionBindings); 83 | 84 | if ($_Type -eq "Site" -and $MemberType -eq "Group") { 85 | foreach ($group in $collGroups) { 86 | if ($group.Title -eq $MemberName) { 87 | $ctx.Load($group.Users); 88 | $ctx.ExecuteQuery() ; 89 | ##Write-Host "Number of users" $group.Users.Count; 90 | $group.Users| ForEach-Object { 91 | Load-CSOMProperties -object $_ -propertyNames @("LoginName"); 92 | $ctx.ExecuteQuery() ; 93 | 94 | $_permissions += (PermissionObject $_object "Site" $_RelativeUrl $_siteUrl $_siteTitle "" "GroupMember" $group.Title $_.Title $_.LoginName $RoleDefinitionBindings); 95 | ##Write-Host $permissions.Count 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | } 103 | return $_permissions; 104 | 105 | } 106 | 107 | Function QueryUniquePermissions($_web) { 108 | ##query list, files and items unique permissions 109 | $permissions = @(); 110 | Write-Host "Querying web " + $_web.Title ; 111 | $siteUrl = $_web.Url; 112 | 113 | $siteRelativeUrl = $_web.ServerRelativeUrl; 114 | 115 | Write-Host $siteUrl -Foregroundcolor "Red"; 116 | 117 | $siteTitle = $_web.Title; 118 | 119 | Load-CSOMProperties -object $_web -propertyNames @("HasUniqueRoleAssignments"); 120 | $ctx.ExecuteQuery() 121 | ## See more at: https://www.itunity.com/article/loading-specific-values-lambda-expressions-sharepoint-csom-api-windows-powershell-1249#sthash.2ncW42CM.dpuf 122 | #Get Site Level Permissions if it's unique 123 | 124 | if ($_web.HasUniqueRoleAssignments -eq $True) { 125 | $permissions += (QueryUniquePermissionsByObject $_web $_web "Site" $siteRelativeUrl $siteUrl $siteTitle ""); 126 | } 127 | 128 | #Get all lists in web 129 | $ll = $_web.Lists 130 | $ctx.Load($ll); 131 | $ctx.ExecuteQuery() 132 | 133 | Write-Host "Number of lists" + $ll.Count 134 | 135 | foreach ($list in $ll) { 136 | Load-CSOMProperties -object $list -propertyNames @("RootFolder", "Hidden", "HasUniqueRoleAssignments"); 137 | $ctx.ExecuteQuery() 138 | 139 | $listUrl = $list.RootFolder.ServerRelativeUrl; 140 | 141 | #Exclude internal system lists and check if it has unique permissions 142 | 143 | if ($list.Hidden -ne $True) { 144 | Write-Host $list.Title -Foregroundcolor "Yellow"; 145 | $listTitle = $list.Title; 146 | #Check List Permissions 147 | 148 | if ($list.HasUniqueRoleAssignments -eq $True) { 149 | $Type = $list.BaseType.ToString(); 150 | $permissions += (QueryUniquePermissionsByObject $_web $list $Type $listUrl $siteUrl $siteTitle $listTitle); 151 | 152 | if ($list.BaseType -eq "DocumentLibrary") { 153 | #TODO Get permissions on folders 154 | $rootFolder = $list.RootFolder; 155 | $listFolders = $rootFolder.Folders; 156 | $ctx.Load($rootFolder); 157 | $ctx.Load( $listFolders); 158 | 159 | $ctx.ExecuteQuery() ; 160 | 161 | #get all items 162 | 163 | $spQuery = New-Object Microsoft.SharePoint.Client.CamlQuery 164 | $spQuery.ViewXml = " 165 | 2000 166 | " 167 | ## array of items 168 | $collListItem = @(); 169 | 170 | do { 171 | $listItems = $list.GetItems($spQuery); 172 | $ctx.Load($listItems); 173 | $ctx.ExecuteQuery() ; 174 | $spQuery.ListItemCollectionPosition = $listItems.ListItemCollectionPosition 175 | foreach ($item in $listItems) { 176 | $collListItem += $item 177 | } 178 | } 179 | while ($spQuery.ListItemCollectionPosition -ne $null) 180 | 181 | Write-Host $collListItem.Count 182 | 183 | foreach ($item in $collListItem) { 184 | Load-CSOMProperties -object $item -propertyNames @("File", "HasUniqueRoleAssignments"); 185 | $ctx.ExecuteQuery() ; 186 | 187 | Load-CSOMProperties -object $item.File -propertyNames @("ServerRelativeUrl"); 188 | $ctx.ExecuteQuery() ; 189 | 190 | $fileUrl = $item.File.ServerRelativeUrl; 191 | 192 | $file = $item.File; 193 | 194 | if ($item.HasUniqueRoleAssignments -eq $True) { 195 | $Type = $file.GetType().Name; 196 | 197 | $permissions += (QueryUniquePermissionsByObject $_web $item $Type $fileUrl $siteUrl $siteTitle $listTitle); 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | return $permissions; 205 | } 206 | 207 | if (Test-Path $ExportFileDirectory) { 208 | Write-Host $Username 209 | Write-Host $password 210 | 211 | $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($SiteCollectionUrl); 212 | $ctx.Credentials = New-Object System.Net.NetworkCredential($Username, $password); 213 | 214 | 215 | $rootWeb = $ctx.Web 216 | $ctx.Load($rootWeb) 217 | $ctx.Load($rootWeb.Webs) 218 | $ctx.ExecuteQuery() 219 | 220 | #Root Web of the Site Collection 221 | 222 | $RootSiteTitle = $rootWeb.Title; 223 | 224 | $RootWeb = $rootWeb; 225 | #array storing permissions 226 | $Permissions = @(); 227 | 228 | #root web , i.e. site collection level 229 | $Permissions += QueryUniquePermissions($RootWeb); 230 | Write-Host $Permissions.Count; 231 | 232 | Write-Host "Querying Number of webs " $rootWeb.Webs.Count ; 233 | foreach ($web in $rootWeb.Webs) { 234 | $Permissions += (QueryUniquePermissions $web); 235 | Write-Host "Web : " $web.Title "Count" $Permissions.Count 236 | } 237 | 238 | $exportFilePath = Join-Path -Path $ExportFileDirectory -ChildPath $([string]::Concat($RootSiteTitle, "-Permissions.csv")); 239 | 240 | Write-Host $Permissions.Count 241 | 242 | $Permissions | Select-Object SiteUrl, SiteTitle, Type, RelativeUrl, ListTitle, MemberType, MemberName, MemberLoginName, ParentGroup, Roles|Export-CSV -Path $exportFilePath -NoTypeInformation; 243 | } 244 | else { 245 | 246 | Write-Host "Invalid directory path:" $ExportFileDirectory -ForegroundColor "Red"; 247 | 248 | } 249 | -------------------------------------------------------------------------------- /office365-permission-report/Load-CSOMProperties.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Facilitates the loading of specific properties of a Microsoft.SharePoint.Client.ClientObject object or Microsoft.SharePoint.Client.ClientObjectCollection object. 4 | .DESCRIPTION 5 | Replicates what you would do with a lambda expression in C#. 6 | For example, "ctx.Load(list, l => list.Title, l => list.Id)" becomes 7 | "Load-CSOMProperties -object $list -propertyNames @('Title', 'Id')". 8 | .EXAMPLE 9 | Load-CSOMProperties -parentObject $web -collectionObject $web.Fields -propertyNames @("InternalName", "Id") -parentPropertyName "Fields" -executeQuery 10 | $web.Fields | select InternalName, Id 11 | .EXAMPLE 12 | Load-CSOMProperties -object $web -propertyNames @("Title", "Url", "AllProperties") -executeQuery 13 | $web | select Title, Url, AllProperties 14 | #> 15 | function global:Load-CSOMProperties { 16 | [CmdletBinding(DefaultParameterSetName='ClientObject')] 17 | param ( 18 | # The Microsoft.SharePoint.Client.ClientObject to populate. 19 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = "ClientObject")] 20 | [Microsoft.SharePoint.Client.ClientObject] 21 | $object, 22 | 23 | # The Microsoft.SharePoint.Client.ClientObject that contains the collection object. 24 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = "ClientObjectCollection")] 25 | [Microsoft.SharePoint.Client.ClientObject] 26 | $parentObject, 27 | 28 | # The Microsoft.SharePoint.Client.ClientObjectCollection to populate. 29 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1, ParameterSetName = "ClientObjectCollection")] 30 | [Microsoft.SharePoint.Client.ClientObjectCollection] 31 | $collectionObject, 32 | 33 | # The object properties to populate 34 | [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "ClientObject")] 35 | [Parameter(Mandatory = $true, Position = 2, ParameterSetName = "ClientObjectCollection")] 36 | [string[]] 37 | $propertyNames, 38 | 39 | # The parent object's property name corresponding to the collection object to retrieve (this is required to build the correct lamda expression). 40 | [Parameter(Mandatory = $true, Position = 3, ParameterSetName = "ClientObjectCollection")] 41 | [string] 42 | $parentPropertyName, 43 | 44 | # If specified, execute the ClientContext.ExecuteQuery() method. 45 | [Parameter(Mandatory = $false, Position = 4)] 46 | [switch] 47 | $executeQuery 48 | ) 49 | 50 | begin { } 51 | process { 52 | if ($PsCmdlet.ParameterSetName -eq "ClientObject") { 53 | $type = $object.GetType() 54 | } else { 55 | $type = $collectionObject.GetType() 56 | if ($collectionObject -is [Microsoft.SharePoint.Client.ClientObjectCollection]) { 57 | $type = $collectionObject.GetType().BaseType.GenericTypeArguments[0] 58 | } 59 | } 60 | 61 | $exprType = [System.Linq.Expressions.Expression] 62 | $parameterExprType = [System.Linq.Expressions.ParameterExpression].MakeArrayType() 63 | $lambdaMethod = $exprType.GetMethods() | ? { $_.Name -eq "Lambda" -and $_.IsGenericMethod -and $_.GetParameters().Length -eq 2 -and $_.GetParameters()[1].ParameterType -eq $parameterExprType } 64 | $lambdaMethodGeneric = Invoke-Expression "`$lambdaMethod.MakeGenericMethod([System.Func``2[$($type.FullName),System.Object]])" 65 | $expressions = @() 66 | 67 | foreach ($propertyName in $propertyNames) { 68 | $param1 = [System.Linq.Expressions.Expression]::Parameter($type, "p") 69 | try { 70 | $name1 = [System.Linq.Expressions.Expression]::Property($param1, $propertyName) 71 | } catch { 72 | Write-Error "Instance property '$propertyName' is not defined for type $type" 73 | return 74 | } 75 | $body1 = [System.Linq.Expressions.Expression]::Convert($name1, [System.Object]) 76 | $expression1 = $lambdaMethodGeneric.Invoke($null, [System.Object[]] @($body1, [System.Linq.Expressions.ParameterExpression[]] @($param1))) 77 | 78 | if ($collectionObject -ne $null) { 79 | $expression1 = [System.Linq.Expressions.Expression]::Quote($expression1) 80 | } 81 | $expressions += @($expression1) 82 | } 83 | 84 | 85 | if ($PsCmdlet.ParameterSetName -eq "ClientObject") { 86 | $object.Context.Load($object, $expressions) 87 | if ($executeQuery) { $object.Context.ExecuteQuery() } 88 | } else { 89 | $newArrayInitParam1 = Invoke-Expression "[System.Linq.Expressions.Expression``1[System.Func````2[$($type.FullName),System.Object]]]" 90 | $newArrayInit = [System.Linq.Expressions.Expression]::NewArrayInit($newArrayInitParam1, $expressions) 91 | 92 | $collectionParam = [System.Linq.Expressions.Expression]::Parameter($parentObject.GetType(), "cp") 93 | $collectionProperty = [System.Linq.Expressions.Expression]::Property($collectionParam, $parentPropertyName) 94 | 95 | $expressionArray = @($collectionProperty, $newArrayInit) 96 | $includeMethod = [Microsoft.SharePoint.Client.ClientObjectQueryableExtension].GetMethod("Include") 97 | $includeMethodGeneric = Invoke-Expression "`$includeMethod.MakeGenericMethod([$($type.FullName)])" 98 | 99 | $lambdaMethodGeneric2 = Invoke-Expression "`$lambdaMethod.MakeGenericMethod([System.Func``2[$($parentObject.GetType().FullName),System.Object]])" 100 | $callMethod = [System.Linq.Expressions.Expression]::Call($null, $includeMethodGeneric, $expressionArray) 101 | 102 | $expression2 = $lambdaMethodGeneric2.Invoke($null, @($callMethod, [System.Linq.Expressions.ParameterExpression[]] @($collectionParam))) 103 | 104 | $parentObject.Context.Load($parentObject, $expression2) 105 | if ($executeQuery) { $parentObject.Context.ExecuteQuery() } 106 | } 107 | } 108 | end { } 109 | } -------------------------------------------------------------------------------- /office365-permission-report/Loading Specific Values Using Lambda Expressions and the SharePoint CSOM API with Windows PowerShell - IT Unity.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://www.itunity.com/article/loading-specific-values-lambda-expressions-sharepoint-csom-api-windows-powershell-1249 3 | -------------------------------------------------------------------------------- /office365-permission-report/Office SharePoint Unique Permissions Report using CSOM and PowerShell.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://gallery.technet.microsoft.com/office/CSOM-Query-All-Unique-e60f2148 3 | -------------------------------------------------------------------------------- /office365-read-all-calendar/O365-ReadAllCalendar.ps1: -------------------------------------------------------------------------------- 1 | # Config 2 | $clientID = "1f22e467-cb59-4cc7-91a9-a816d5666c75" 3 | $tenantName = "0a9449ca-3619-4fca-8644-bdd67d0c8ca6" 4 | $ClientSecret = "VtKm16o6~G0~U44.JAs~.Sr7eBt7C7ScVS" 5 | 6 | function AuthO365() { 7 | # Auth call 8 | $ReqTokenBody = @{ 9 | Grant_Type = "client_credentials" 10 | client_Id = $clientID 11 | Client_Secret = $clientSecret 12 | Scope = "https://graph.microsoft.com/.default" 13 | } 14 | return Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method "POST" -Body $ReqTokenBody 15 | } 16 | 17 | function readCalendar($token) { 18 | # Loop all users 19 | $api = "https://graph.microsoft.com/v1.0/users" 20 | $users = $null 21 | $users = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.access_token)" } -Uri $api -Method "GET" -ContentType "application/json" 22 | foreach ($u in $users.value) { 23 | 24 | # All events for user 25 | $upn = $u.value[0].userPrincipalName 26 | $api = "https://graph.microsoft.com/v1.0/users/$upn/events?`$top=999&`$filter=start/dateTime ge '2020-01-01' and end/dateTime le '2020-07-29'" 27 | Write-Host $api -Fore Green 28 | $events = $null 29 | $events = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.access_token)" } -Uri $api -Method "GET" -ContentType "application/json" 30 | 31 | do { 32 | foreach ($r in $events.value) { 33 | # Properties 34 | $id = $r.id 35 | $subject = $r.subject 36 | $bodyPreview = $r.bodyPreview 37 | $start = [datetime]$r.start.dateTime 38 | $end = [datetime]$r.end.dateTime 39 | $attendeesCount = $r.attendees.count 40 | $organizername = $r.organizer.emailAddress.name 41 | $organizeraddress = $r.organizer.emailAddress.address 42 | 43 | Write-Host "EVENT: Start=$start End=$end Id=$id" -Fore Yellow 44 | } 45 | if ($events.'@Odata.NextLink') { 46 | $events = Invoke-RestMethod -Headers @{Authorization = "Bearer $($token.access_token)" } -Uri $events.'@Odata.NextLink' -Method "GET" -ContentType "application/json" 47 | } 48 | } while ($events.'@Odata.NextLink') 49 | } 50 | } 51 | 52 | function Main() { 53 | $token = AuthO365 54 | $token 55 | readCalendar $token 56 | } 57 | Main -------------------------------------------------------------------------------- /office365-reduce-sppkg/Reduce-Sppkg.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Reduce SharePoint Framework SPPKG file 4 | .DESCRIPTION 5 | Display 6 | 7 | Comments and suggestions always welcome! spjeff@spjeff.com or @spjeff 8 | .NOTES 9 | File Namespace : Reduce-Sppkg.ps1 10 | Author : Jeff Jones - @spjeff 11 | Version : 0.10 12 | Last Modified : 11-02-2017 13 | .LINK 14 | Source Code 15 | http://www.github.com/spjeff/o365/reduce-sppkg 16 | #> 17 | 18 | [CmdletBinding()] 19 | param ( 20 | [Parameter(Mandatory = $False, ValueFromPipeline = $false, HelpMessage = 'Use -p to provide input package.')] 21 | [Alias("p")] 22 | [string]$package 23 | ) 24 | 25 | # params 26 | $tempFolder = $env:TEMP + "\reduce-sppkg" 27 | $global:keep = @() 28 | 29 | # unzip function 30 | Add-Type -AssemblyName System.IO.Compression.FileSystem 31 | function Unzip { 32 | param([string]$zipfile, [string]$outpath) 33 | Write-Host "Unzip $zipfile to $outpath" -Fore Yellow 34 | Remove-Item $outpath -Recurse -Confirm:$false -ErrorAction SilentlyContinue | Out-Null 35 | mkdir $outpath -ErrorAction SilentlyContinue | Out-Null 36 | try { 37 | [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath) 38 | } 39 | catch {} 40 | } 41 | 42 | function Zip { 43 | param([string]$folderInclude, [string]$outZip) 44 | [System.IO.Compression.CompressionLevel]$compression = "Optimal" 45 | $ziparchive = [System.IO.Compression.ZipFile]::Open( $outZip, "Update" ) 46 | Write-Host "Zip $folderInclude to $outZip" -Fore Yellow 47 | 48 | # loop all child files 49 | $realtiveTempFolder = (Resolve-Path $tempFolder -Relative).TrimStart(".\") 50 | foreach ($file in (Get-ChildItem $folderInclude -Recurse)) { 51 | # skip directories 52 | if ($file.GetType().ToString() -ne "System.IO.DirectoryInfo") { 53 | # relative path 54 | $relpath = "" 55 | if ($file.FullName) { 56 | $relpath = (Resolve-Path $file.FullName -Relative) 57 | } 58 | if (!$relpath) { 59 | $relpath = $file.Name 60 | } 61 | else { 62 | $relpath = $relpath.Replace($realtiveTempFolder, "") 63 | $relpath = $relpath.TrimStart(".\").TrimStart("\\") 64 | } 65 | 66 | # display 67 | Write-Host $relpath 68 | Write-Host $file.FullName 69 | 70 | # add file 71 | [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($ziparchive, $file.FullName, $relpath, $compression) | Out-Null 72 | } 73 | } 74 | $ziparchive.Dispose() 75 | } 76 | 77 | function DisplayFeatures($tempFolder) { 78 | # Gather feature XML 79 | $features = Get-ChildItem "$tempFolder\feat*.xml" 80 | $features = $features |? {$_.Name -notlike "*config*"} 81 | 82 | # Parse Description from XML 83 | $coll = @() 84 | foreach ($f in $features) { 85 | [xml]$xml = Get-Content $f.FullName 86 | $title = $xml.Feature.Title 87 | $id = $xml.Feature.Id 88 | $obj = New-Object -TypeName PSObject -Prop (@{"Title" = $title; "Id" = $id}) 89 | $coll += $obj 90 | } 91 | 92 | $coll | sort Title | ft -a 93 | Write-Host "$($features.length) features found" -Fore Green 94 | } 95 | 96 | function SelectKeepers() { 97 | Write-Host "Type in GUID numbers to keep. Press ENTER for a blank input to finalize." 98 | $id = "GO" 99 | while ($id -ne "") { 100 | $id = Read-Host 101 | if ($id -ne "") { 102 | $global:keep += $id 103 | } 104 | } 105 | Write-Host "Keep Feature IDs" -Fore Yellow 106 | $global:keep | ft -a 107 | } 108 | 109 | function RemoveFeatures() { 110 | # Remove Directory (if not Keep GUID) 111 | foreach ($d in (Get-ChildItem $tempFolder -Directory)) { 112 | $keep = $null 113 | $keep = $global:keep |? {$_ -eq $d.Name} 114 | if (!$keep -and $d.Name -ne "_rels") { 115 | Remove-Item $d.FullName -Recurse -Confirm:$false -Force 116 | } 117 | } 118 | 119 | # Feature Config XML 120 | foreach ($f in (Get-ChildItem "$tempFolder\feature*.config.xml")) { 121 | $keep = $null 122 | $featureId = $f.Name.Replace(".xml.config.xml", "").Replace("feature_", "") 123 | $keep = $global:keep |? {$_ -eq $featureId} 124 | if (!$keep) { 125 | Remove-Item $f.FullName -Confirm:$false -Force 126 | } 127 | } 128 | 129 | # Feature XML 130 | foreach ($f in (Get-ChildItem "$tempFolder\feature*.xml")) { 131 | $keep = $null 132 | $featureId = $f.Name.Replace(".xml", "").Replace("feature_", "") 133 | $keep = $global:keep |? {$_ -eq $featureId} 134 | if (!$keep) { 135 | Remove-Item $f.FullName -Confirm:$false -Force 136 | } 137 | } 138 | 139 | # Feature Rel XML 140 | foreach ($f in (Get-ChildItem "$tempFolder\_rels\feature*.xml.rels")) { 141 | $keep = $null 142 | $featureId = $f.Name.Replace(".xml.rels", "").Replace("feature_", "") 143 | $keep = $global:keep |? {$_ -eq $featureId} 144 | if (!$keep) { 145 | Remove-Item $f.FullName -Confirm:$false -Force 146 | } 147 | } 148 | 149 | # AppManifest.xml 150 | $filePath = "$tempFolder\_rels\AppManifest.xml.rels" 151 | [xml]$xml = Get-Content $filePath 152 | $rels = $xml.Relationships.Relationship 153 | foreach ($r in $rels) { 154 | $keep = $null 155 | $featureId = $r.Target.Replace("/feature_", "").Replace(".xml", "") 156 | $keep = $global:keep |? {$_ -eq $featureId} 157 | if (!$keep) { 158 | # Remove XML Node 159 | $xml.Relationships.RemoveChild($r) | Out-Null 160 | } 161 | } 162 | $xml.Save($filePath) 163 | } 164 | 165 | function Main() { 166 | # UnZIP 167 | Unzip $package $tempFolder 168 | 169 | # Display all features 170 | DisplayFeatures $tempFolder 171 | 172 | # Select keepers 173 | SelectKeepers 174 | 175 | # Remove excess Features 176 | RemoveFeatures 177 | 178 | # ZIP 179 | $timestamp = (get-date).tostring("yyyy-MM-dd-hh-mm-ss") 180 | $newFilename = $package.Replace(".sppkg", "$timestamp.sppkg") 181 | Zip $tempFolder $newFilename 182 | 183 | # Done 184 | Remove-Item $tempFolder -Recurse -Confirm:$false -ErrorAction SilentlyContinue | Out-Null 185 | Write-Host "Created $newFilename" -Fore Yellow 186 | Write-Host "DONE" -Fore Green 187 | } 188 | Main -------------------------------------------------------------------------------- /office365-runbook-spo-storage/SPO-Storage.ps1: -------------------------------------------------------------------------------- 1 | "START" 2 | 3 | # Modules 4 | Import-Module "SharePointPnPPowerShellOnline" 5 | Import-Module "Microsoft.PowerShell.Utility" 6 | 7 | # Config 8 | $url = "https://spjeff-admin.sharepoint.com/" 9 | 10 | # App ID and Secret from Azure Automation secure string "Credential" storage 11 | # from https://stackoverflow.com/questions/28352141/convert-a-secure-string-to-plain-text 12 | # from https://sharepointyankee.com/2018/02/23/azure-automation-credentials/ 13 | $cred = Get-AutomationPSCredential "SPO-Storage-SPOApp" 14 | $cred |ft -a 15 | 16 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.Password) 17 | $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 18 | $clientId = $cred.UserName 19 | $clientSecret = $UnsecurePassword 20 | $clientId 21 | $clientSecret 22 | 23 | # PNP Get Sites 24 | Connect-PnPOnline -Url $url -ClientId $clientId -ClientSecret $clientSecret 25 | $sites = Get-PnPTenantSite 26 | $table = $sites | Select-Object Url,Template,StorageUsage,StorageMaximumLevel,LockState,Owner,OwnerEmail,OwnerLoginName,OwnerName 27 | $table | Format-Table -AutoSize 28 | 29 | # Format HTML 30 | $CSS = @' 31 | 43 | '@ 44 | $csv = "SPO-Storage.csv" 45 | $table | Export-CSV $csv -Force -NoTypeInformation 46 | $html = ($table | ConvertTo-HTML -Property * -Head $CSS) -Join "" 47 | $totalStorageUsage = $table | Measure-Object "StorageUsage" -Sum 48 | $html += "

Count Sites = $($sites.Count)

" 49 | $html += "

Total Storage (MB) = $($totalStorageUsage.Sum)

" 50 | 51 | # Send Email 52 | $cred = Get-AutomationPSCredential "SPO-Storage-EXOUser" 53 | #REM $cred = Get-Credential 54 | $cred |ft -a 55 | $cred.UserName 56 | $cred.Password 57 | $recip = "spjeff@spjeff.com" 58 | $subj = "SPO-Storage" 59 | Send-MailMessage -To $recip -from $recip -Subject $subj -Body $html -BodyAsHtml -smtpserver "smtp.office365.com" -UseSSL -Credential $cred -Port "587" -Attachments $csv 60 | 61 | "FINISH" -------------------------------------------------------------------------------- /office365-site-directory/PnP-Site-Directory-JSON.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | Downloads full listing of SharePoint Online (SPO) site collection into JSON. Expands columns with Azure AD properties (listed below). Finally uploads to SPO document library. 4 | 5 | * Site Owner UPN 6 | * Site Owner Display Name 7 | * Site Owner Email 8 | * Site Owner Department 9 | * Site Owner Manager UPN 10 | * Site Owner Manager Display Name 11 | * Site Owner Manager Email 12 | 13 | Leveage JSON output for future use cases: 14 | 15 | * Site inventory 16 | * Site history report (version history, time machine, restore by URL). 17 | * Site ownership for training 18 | * Site ownership for governance. If owner left company then locate new owner. 19 | * Site directory to find by keyword 20 | 21 | Comments and suggestions always welcome! Please, use the issues panel at the project page. 22 | 23 | .EXAMPLE 24 | .\PnP-Site-Directory-JSON.ps1 25 | 26 | .NOTES 27 | File Name: PnP-Site-Directory-JSON.ps1 28 | Author : Jeff Jones - @spjeff 29 | Version : 1.0 30 | Modified : 2020-01-23 31 | .LINK 32 | https://github.com/spjeff/office365 33 | #> 34 | 35 | 36 | # Import 37 | Import-Module -Name "PNP.PowerShell" -ErrorAction SilentlyContinue | Out-Null 38 | Import-Module -Name "AzureAD" -ErrorAction SilentlyContinue | Out-Null 39 | 40 | # Config 41 | $tenant = "spjeff" 42 | $pfxClientFile = "PnP-PowerShell-$tenant.txt" 43 | $pfxPassword = "password" 44 | $username = "spjeff@spjeff.com" 45 | 46 | $jsonOutput = "PnP-Site-Directory.json" 47 | $jsonFolder = "PnPSiteDirectory" 48 | $dtPeople = New-Object System.Data.DataTable("people") 49 | 50 | # Cache table 51 | function createTable() { 52 | # Schema column 53 | @("mail","upn","displayname","managerupn","managerdisplayname","department") |% { $dtPeople.Columns.Add($_) | Out-Null} 54 | } 55 | 56 | # Connect both AAD and PNP 57 | function connectCloud() { 58 | # Connect AAD 59 | "Connect AAD" 60 | $secpassword = ConvertTo-SecureString -String $password -AsPlainText -Force 61 | $cred = New-Object -Typename "System.Management.Automation.PSCredential" -ArgumentList $username, $secpassword 62 | $out = Connect-AzureAD -Credential $cred 63 | 64 | # Connect PNP 65 | "Connect PNP" 66 | $pfxClientId = Get-Content $pfxClientFile 67 | $pfxSecPassword = $pfxPassword | ConvertTo-SecureString -AsPlainText -Force 68 | $out = Connect-PnPOnline -ClientId $pfxClientId -Url "https://$tenant.sharepoint.com" -Tenant "$tenant.onmicrosoft.com" -CertificatePath "PnP-PowerShell-$tenant.pfx" -CertificatePassword $pfxSecPassword 69 | } 70 | 71 | # Do we have this user? 72 | function findUser($mail) { 73 | # Input validation 74 | if (!$mail) { 75 | "Not found" 76 | return 77 | } 78 | 79 | # DataView rapid filter 80 | $dvPeople = New-Object System.Data.DataView($dtPeople) 81 | $dvPeople.RowFilter = "Mail = '$mail'" 82 | 83 | # Result not found 84 | if ($dvPeople.Count -eq 0) { 85 | # Lookup user 86 | $user = $null 87 | $user = Get-AzureADUser -Filter "mail eq '$mail'" 88 | 89 | # Lookup manager 90 | $mgr = $null 91 | $mgr = Get-AzureADUserManager -ObjectId $user.ObjectId 92 | 93 | # Hash 94 | $hash = @{ 95 | "mail" = $user.Mail 96 | "upn" = $user.UserPrincipalName 97 | "displayname" =$user.DisplayName 98 | "department" =$user.UsageLocation 99 | "managerupn" = $mgr.UserPrincipalName 100 | "managerdisplayname" = $mgr.DisplayName 101 | } 102 | 103 | # Add 104 | $row = $dtPeople.NewRow() 105 | $row['mail'] = $hash['mail'] 106 | $row['upn'] = $hash['upn'] 107 | $row['displayname'] = $hash['displayname'] 108 | $row['department'] = $hash['department'] 109 | $row['managerupn'] = $hash['managerupn'] 110 | $row['managerdisplayname'] = $hash['managerdisplayname'] 111 | $dtpeople.Rows.Add($row) 112 | Write-Host "Add $mail" -ForegroundColor Green 113 | } 114 | 115 | Write-Host "Found $mail" -ForegroundColor Yellow 116 | return $dvPeople 117 | } 118 | 119 | # Collect input PNP sites 120 | function collectSites() { 121 | # Download original 122 | $sites = Get-PnPTenantSite 123 | "Found sites = $($sites.count)" 124 | 125 | # Convert CSV 126 | $sites | Export-Csv "temp.csv" -Force -NoTypeInformation 127 | $rows = Import-csv "temp.csv" 128 | 129 | # Expand columns 130 | foreach ($row in $rows) { 131 | $hash = findUser $s.Owner 132 | $row| Add-Member Noteproperty 'mail' $hash.mail 133 | $row| Add-Member Noteproperty 'upn' $hash.upn 134 | $row| Add-Member Noteproperty 'displayname' $hash.displayname 135 | $row| Add-Member Noteproperty 'department' $hash.department 136 | $row| Add-Member Noteproperty 'managerupn' $hash.managerupn 137 | $row| Add-Member Noteproperty 'managerdisplayname' $hash.managerdisplayname 138 | } 139 | 140 | # Write JSON local 141 | "Write JSON" 142 | $json = $rows | ConvertTo-Json -Depth 9 143 | $json | Out-File $jsonOutput -Force 144 | } 145 | 146 | # Upload JSON to SPO 147 | function uploadJSON() { 148 | "Upload JSON" 149 | $out = Add-PnPFile -Path $jsonOutput -Folder $jsonFolder 150 | $out.ServerRelativeUrl 151 | } 152 | 153 | # main 154 | function main() { 155 | createTable 156 | connectCloud 157 | collectSites 158 | uploadJSON 159 | "Done" 160 | } 161 | main -------------------------------------------------------------------------------- /office365-speed/o365-speed.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | SharePoint Online Speed Test - Measure how fast SPO will connect and provide current Web detail 4 | .DESCRIPTION 5 | Run multiple repetitions of <> to measure O365 connection response times. 6 | 7 | Comments and suggestions always welcome! spjeff@spjeff.com or @spjeff 8 | .NOTES 9 | File Name : o365-speed-test.ps1 10 | Author : Jeff Jones - @spjeff 11 | Version : 0.10 12 | Last Modified : 05-22-2017 13 | .LINK 14 | Source Code 15 | http://www.github.com/spjeff/o365/o365-speed.ps1 16 | 17 | Download PowerShell Plugin 18 | 19 | * PNP - Patterns and Practices 20 | https://github.com/officedev/pnp-powershell 21 | #> 22 | 23 | # CONFIG - CHANGE THESE VALUES 24 | $url = "https://spjeff.sharepoint.com" 25 | 26 | # Prepare environment 27 | Write-Host "Office 365 - Speed Test" 28 | $reps = 1..10 29 | 30 | # Connect to target 31 | $cred = Get-PnPStoredCredential -Name $url 32 | if (!$cred) { 33 | Add-PnPStoredCredential -Name $url 34 | $cred = Get-PnPStoredCredential -Name $url 35 | } 36 | 37 | # Run test repetitions 38 | $coll = @() 39 | $reps |% { 40 | # Core command 41 | $sb = { 42 | Connect-PNPOnline -Url $url 43 | $web = Get-pnpweb -Includes AllProperties 44 | $web | ft 45 | Disconnect-PNPOnline 46 | } 47 | # Measure time 48 | $result = Measure-Command $sb 49 | # Collect times 50 | $coll += $result 51 | } 52 | 53 | # Display result table 54 | $coll | ft -a -------------------------------------------------------------------------------- /office365-spo-banner/configure-page.aspx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Custom Actions Configuration 5 | 6 | 7 | 8 | 299 | 300 | 301 | 315 | 316 | 317 | 318 | 319 | 320 |
321 | 323 |
324 |

User Custom Actions Configuration

325 | This page lists the current user custom actions configured for the current site and site collection. 326 |
327 |
328 | 329 |

Site Collection User Custom Actions

330 | 332 | 333 |
334 | 335 |

Site User Custom Actions

336 | 338 | 339 |
340 | 341 |

Install User Custom Action

342 | 343 |
344 | 345 |
346 | 347 | 348 | 349 |
350 | 351 |
352 | 353 | 354 | 355 |
356 |
357 | 358 | 359 | 360 |
361 | 362 | 363 | 364 |
365 | 366 |
367 |
368 | 369 |
370 | 371 | 372 | 373 | 374 | -------------------------------------------------------------------------------- /office365-spo-banner/spo-banner.js: -------------------------------------------------------------------------------- 1 | // Inject CSS 2 | function addcss(css) { 3 | var head = document.getElementsByTagName('head')[0]; 4 | var s = document.createElement('style'); 5 | s.setAttribute('type', 'text/css'); 6 | if (s.styleSheet) { 7 | // IE 8 | s.styleSheet.cssText = css; 9 | } else { 10 | // the world 11 | s.appendChild(document.createTextNode(css)); 12 | } 13 | head.appendChild(s); 14 | } 15 | 16 | // Set cookie 17 | function setCookie(cname, cvalue, exdays) { 18 | var d = new Date(); 19 | d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); 20 | var expires = 'expires=' + d.toUTCString(); 21 | document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/'; 22 | } 23 | 24 | // Read cookie 25 | function getCookie(cname) { 26 | var name = cname + '='; 27 | var decodedCookie = decodeURIComponent(document.cookie); 28 | var ca = decodedCookie.split(';'); 29 | for (var i = 0; i < ca.length; i++) { 30 | var c = ca[i]; 31 | while (c.charAt(0) == ' ') { 32 | c = c.substring(1); 33 | } 34 | if (c.indexOf(name) == 0) { 35 | return c.substring(name.length, c.length); 36 | } 37 | } 38 | return ''; 39 | } 40 | 41 | // Display banner 42 | function spoBannerLoad() { 43 | // Skip for modal dialogs 44 | if (document.location.href.indexOf("IsDlg=1") > 0) { 45 | return; 46 | } 47 | 48 | // HTTP GET for banner SPList items 49 | var request = new XMLHttpRequest(); 50 | request.open('GET', "/_api/web/lists/getbytitle('GlobalAlert')/items", true); 51 | request.setRequestHeader('Accept', 'application/json; odata=verbose'); 52 | 53 | request.onload = function() { 54 | if (request.status >= 200 && request.status < 400) { 55 | // HTTP 200 - Success! 56 | var data = JSON.parse(request.responseText); 57 | var title = data.d.results[0].Title; 58 | var id = data.d.results[0].Id; 59 | 60 | // Read cookie 61 | var cookie = getCookie('spo-banner'); 62 | if (cookie.indexOf(id) > -1) { 63 | // Not found 64 | return; 65 | } 66 | 67 | // Inject HTML
68 | var body = document.getElementsByTagName('body')[0]; 69 | var d = document.createElement('div'); 70 | d.id = 'spo-banner'; 71 | d.innerHTML = 72 | '' + 73 | title + 74 | '[X]'; 77 | body.appendChild(d); 78 | 79 | // Calculate left offset 80 | var d = document.getElementById('spo-banner'); 81 | var left = window.innerWidth / 2 - 100; 82 | 83 | // Inject CSS 84 | addcss( 85 | '#spo-banner {background-color: yellow;padding:3px;top:0px;left:' + 86 | left + 87 | 'px;z-index: 99;position: fixed;}' 88 | ); 89 | } else { 90 | // We reached our target server, but it returned an error 91 | } 92 | }; 93 | request.onerror = function() { 94 | // There was a connection error of some sort 95 | }; 96 | 97 | request.send(); 98 | } 99 | 100 | // Dismiss banner 101 | function spoBannerDismiss(id) { 102 | // User click [X] to dismiss. Set cookie. 103 | var d = document.getElementById('spo-banner'); 104 | d.style.display = 'none'; 105 | setCookie('spo-banner', id, 7); 106 | } 107 | 108 | // Main 109 | spoBannerLoad(); 110 | -------------------------------------------------------------------------------- /office365-spo-custom-permission/office365-spo-custom-permission.ps1: -------------------------------------------------------------------------------- 1 | # Credentials to connect to office 365 site collection url 2 | $url = "https://tenant.sharepoint.com/sites/team" 3 | $username = "spadmin@tenant.onmicrosoft.com" 4 | $password = "pass@word1" 5 | $secPassword = $password | ConvertTo-SecureString -AsPlainText -Force 6 | 7 | # Load CSOM 8 | Write-Host "Load CSOM libraries" -Foregroundcolor Black -Backgroundcolor Yellow 9 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") 10 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") 11 | Write-Host "CSOM libraries loaded successfully" -Foregroundcolor black -Backgroundcolor Green 12 | 13 | # Connect 14 | Write-Host "Authenticate to SharePoint Online site collection $url and get ClientContext object" -Foregroundcolor black -Backgroundcolor yellow 15 | $context = New-Object Microsoft.SharePoint.Client.ClientContext($url) 16 | $cred = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username, $secPassword) 17 | $Context.Credentials = $cred 18 | $context.RequestTimeOut = 1000 * 60 * 10 19 | $web = $context.Web 20 | $site = $context.Site 21 | $context.Load($web) 22 | $context.Load($site) 23 | try { 24 | $context.ExecuteQuery() 25 | Write-Host "Authenticated to SharePoint Online $url" -Foregroundcolor black -Backgroundcolor Green 26 | } 27 | catch { 28 | Write-Host "Not able to authenticate to SharePoint Online $url - $($_.Exception.Message)" -Foregroundcolor black -Backgroundcolor Red 29 | return 30 | } 31 | 32 | # Microsoft custom permission levels 33 | # from https://msdn.microsoft.com/en-us/library/microsoft.sharepoint.client.permissionkind.aspx 34 | 35 | function CreateRoleDefinitions($permName, $permDescription, $clone, $addPermissionString, $removePermissionString) { 36 | $roleDefinitionCol = $web.RoleDefinitions 37 | $Context.Load($roleDefinitionCol) 38 | $Context.ExecuteQuery() 39 | 40 | # Check if the permission level is exists or not 41 | $permExists = $roleDefinitionCol |? {$_.Name -eq $permName} 42 | $clonePerm = $roleDefinitionCol |? {$_.Name -eq $clone} 43 | 44 | Write-Host Creating Pemission level with the name $permName -Foregroundcolor black -Backgroundcolor Yellow 45 | if (!$permExists) { 46 | try { 47 | $spRoleDef = New-Object Microsoft.SharePoint.Client.RoleDefinitionCreationInformation 48 | $spBasePerm = New-Object Microsoft.SharePoint.Client.BasePermissions 49 | 50 | if ($clonePerm) { 51 | $spBasePerm = $clonePerm.BasePermissions 52 | } 53 | if ($addPermissionString) { 54 | $addPermissionString.split(",") | % { $spBasePerm.Set($_) } 55 | } 56 | if ($removePermissionString) { 57 | $removePermissionString.split(",") | % { $spBasePerm.Clear($_) } 58 | } 59 | $spRoleDef.Name = $permName 60 | $spRoleDef.Description = $permDescription 61 | $spRoleDef.BasePermissions = $spBasePerm 62 | $web.RoleDefinitions.Add($spRoleDef) 63 | 64 | $Context.ExecuteQuery() 65 | Write-Host "Permission level with the name $permName created" -Foregroundcolor black -Backgroundcolor Green 66 | } 67 | catch { 68 | Write-Host "There was an error creating Permission Level $permName : Error details $($_.Exception.Message)" -Foregroundcolor black -backgroundcolor Red 69 | } 70 | } 71 | else { 72 | Write-Host "Permission level with the name $permName already exists" -Foregroundcolor black -Backgroundcolor Red 73 | } 74 | } 75 | 76 | # Create 4 Custom Permission Levels. Defined by removed permission strings. 77 | 78 | CreateRoleDefinitions -permName "NoDelete" -permDescription "Contribute - without Delete" -clone "Contribute" -removePermissionString "DeleteListItems" 79 | CreateRoleDefinitions -permName "AddOnly" -permDescription "Contribute - without Edit or Delete" -clone "Contribute" -removePermissionString "DeleteListItems,EditListItems" 80 | 81 | CreateRoleDefinitions -permName "NoEdit" -permDescription "Contribute - without Edit" -clone "Contribute" -removePermissionString "EditListItems" 82 | CreateRoleDefinitions -permName "EditOnly" -permDescription "Contribute - without Edit" -clone "Contribute" -removePermissionString "AddListItems,DeleteListItems" -------------------------------------------------------------------------------- /office365-spo-export-splist-csv-and-email/office365-spo-export-splist-csv-and-email.ps1: -------------------------------------------------------------------------------- 1 | # from https://www.c-sharpcorner.com/blogs/export-sharepoint-online-list-items-to-csv-using-pnp-powershell 2 | ###### Declare and Initialize Variables ###### 3 | $url="https://spjeff.sharepoint.com/" 4 | $listName="Test" 5 | $currentTime= $(get-date).ToString("yyyyMMddHHmmss") 6 | $logFilePath=".\log-"+$currentTime+".log" 7 | # Fields that has to be retrieved 8 | $Global:selectProperties=@("Title","Custom1","Custom2","Custom3"); 9 | 10 | ## Start the Transcript 11 | Start-Transcript -Path $logFilePath 12 | 13 | 14 | ## Export List to CSV ## 15 | function ExportList 16 | { 17 | try 18 | { 19 | # Get all list items using PnP cmdlet 20 | $listItems=(Get-PnPListItem -List $listName -Fields $Global:selectProperties).FieldValues 21 | $outputFilePath=".\results-"+$currentTime+".csv" 22 | 23 | $hashTable=@() 24 | 25 | # Loop through the list items 26 | foreach($listItem in $listItems) 27 | { 28 | $obj=New-Object PSObject 29 | $listItem.GetEnumerator() | Where-Object { $_.Key -in $Global:selectProperties }| ForEach-Object{ $obj | Add-Member Noteproperty $_.Key $_.Value} 30 | $hashTable+=$obj; 31 | $obj=$null; 32 | } 33 | 34 | $hashtable | export-csv $outputFilePath -NoTypeInformation 35 | } 36 | catch [Exception] 37 | { 38 | $ErrorMessage = $_.Exception.Message 39 | Write-Host "Error: $ErrorMessage" -ForegroundColor Red 40 | } 41 | } 42 | 43 | ## Connect to SharePoint Online site 44 | Connect-PnPOnline -Url $url -UseWebLogin 45 | 46 | ## Call the Function 47 | ExportList 48 | 49 | ## Disconnect the context 50 | Disconnect-PnPOnline 51 | 52 | ## Stop Transcript 53 | Stop-Transcript -------------------------------------------------------------------------------- /office365-spo-modern-column-JSON/attach.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/sp/column-formatting.schema.json", 3 | "elmType": "a", 4 | "attributes": { 5 | "href": "='/ExpenseAttach/' + [$ID]", 6 | "target": "_blank" 7 | }, 8 | "style": { 9 | "border": "none", 10 | "color": "white", 11 | "background-color": "#00457E", 12 | "cursor": "pointer" 13 | }, 14 | "children": [ 15 | { 16 | "elmType": "span", 17 | "style": { 18 | "padding-left": "10px", 19 | "padding-right": "10px" 20 | }, 21 | "color": "white", 22 | "txtContent": "", 23 | "attributes" : {"iconName":"Attach"} 24 | }, 25 | { 26 | "elmType": "span", 27 | "style": { 28 | "padding-left": "10px" 29 | }, 30 | "color": "white", 31 | "txtContent": "ATTACH FILES" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /office365-spo-modern-column-JSON/view.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/sp/column-formatting.schema.json", 3 | "elmType": "a", 4 | "attributes": { 5 | "href": "='/Lists/Expense%20Report/DispForm.aspx?ID=' + [$ID]", 6 | "target": "_blank" 7 | }, 8 | "style": { 9 | "border": "none", 10 | "color": "white", 11 | "background-color": "#00457E", 12 | "cursor": "pointer" 13 | }, 14 | "children": [ 15 | { 16 | "elmType": "span", 17 | "style": { 18 | "padding-left": "10px", 19 | "padding-right": "10px" 20 | }, 21 | "color": "white", 22 | "txtContent": "", 23 | "attributes" : {"iconName":"PreviewLink"} 24 | }, 25 | { 26 | "elmType": "span", 27 | "style": { 28 | "padding-left": "10px" 29 | }, 30 | "color": "white", 31 | "txtContent": "VIEW FORM" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /office365-spo-upload-slices/CoralReef.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/office365-spo-upload-slices/CoralReef.mp4 -------------------------------------------------------------------------------- /office365-spo-upload-slices/UploadFileInSlice.ps1: -------------------------------------------------------------------------------- 1 | # from https://gist.github.com/asadrefai/ecfb32db81acaa80282d 2 | Try{ 3 | Add-Type -Path 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.dll' 4 | Add-Type -Path 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\16\ISAPI\Microsoft.SharePoint.Client.Runtime.dll' 5 | } 6 | catch { 7 | Write-Host $_.Exception.Message 8 | Write-Host "No further parts of the migration will be completed" 9 | } 10 | 11 | Function UploadFileInSlice ($ctx, $libraryName, $fileName, $fileChunkSizeInMB) { 12 | $ctx = Get-PNPContext 13 | $libraryName = "Documents" 14 | $fileName = "C:\TEMP\CoralReef.mp4" 15 | 16 | $fileChunkSizeInMB = 5 17 | 18 | # Each sliced upload requires a unique ID. 19 | $UploadId = [GUID]::NewGuid() 20 | 21 | # Get the name of the file. 22 | $UniqueFileName = [System.IO.Path]::GetFileName($fileName) 23 | 24 | # Get the folder to upload into. 25 | $Docs = $ctx.Web.Lists.GetByTitle($libraryName) 26 | $ctx.Load($Docs) 27 | $ctx.Load($Docs.RootFolder) 28 | $ctx.ExecuteQuery() 29 | 30 | # Get the information about the folder that will hold the file. 31 | $ServerRelativeUrlOfRootFolder = $Docs.RootFolder.ServerRelativeUrl 32 | 33 | # File object. 34 | [Microsoft.SharePoint.Client.File] $upload 35 | 36 | # Calculate block size in bytes. 37 | $BlockSize = $fileChunkSizeInMB * 1024 * 1024 38 | 39 | # Get the size of the file. 40 | $FileSize = (Get-Item $fileName).length 41 | if ($FileSize -le $BlockSize) 42 | { 43 | # Use regular approach. 44 | $FileStream = New-Object IO.FileStream($fileName,[System.IO.FileMode]::Open) 45 | $FileCreationInfo = New-Object Microsoft.SharePoint.Client.FileCreationInformation 46 | $FileCreationInfo.Overwrite = $true 47 | $FileCreationInfo.ContentStream = $FileStream 48 | $FileCreationInfo.URL = $UniqueFileName 49 | $Upload = $Docs.RootFolder.Files.Add($FileCreationInfo) 50 | $ctx.Load($Upload) 51 | $ctx.ExecuteQuery() 52 | return $Upload 53 | } 54 | else 55 | { 56 | # Use large file upload approach. 57 | $BytesUploaded = $null 58 | $Fs = $null 59 | Try { 60 | 61 | $Fs = [System.IO.File]::Open($fileName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) 62 | $br = New-Object System.IO.BinaryReader($Fs) 63 | $buffer = New-Object System.Byte[]($BlockSize) 64 | $lastBuffer = $null 65 | $fileoffset = 0 66 | $totalBytesRead = 0 67 | $bytesRead 68 | $first = $true 69 | $last = $false 70 | 71 | # Read data from file system in blocks. 72 | while(($bytesRead = $br.Read($buffer, 0, $buffer.Length)) -gt 0) { 73 | $totalBytesRead = $totalBytesRead + $bytesRead 74 | # You've reached the end of the file. 75 | if($totalBytesRead -eq $FileSize) { 76 | $last = $true 77 | # Copy to a new buffer that has the correct size. 78 | $lastBuffer = New-Object System.Byte[]($bytesRead) 79 | [array]::Copy($buffer, 0, $lastBuffer, 0, $bytesRead) 80 | } 81 | 82 | If($first) 83 | { 84 | $ContentStream = New-Object System.IO.MemoryStream 85 | # Add an empty file. 86 | $fileInfo = New-Object Microsoft.SharePoint.Client.FileCreationInformation 87 | $fileInfo.ContentStream = $ContentStream 88 | $fileInfo.Url = $UniqueFileName 89 | $fileInfo.Overwrite = $true 90 | $Upload = $Docs.RootFolder.Files.Add($fileInfo) 91 | $ctx.Load($Upload) 92 | 93 | # Start upload by uploading the first slice. 94 | $s = [System.IO.MemoryStream]::new($buffer) 95 | 96 | # Call the start upload method on the first slice. 97 | $BytesUploaded = $Upload.StartUpload($UploadId, $s) 98 | $ctx.ExecuteQuery() 99 | 100 | # fileoffset is the pointer where the next slice will be added. 101 | $fileoffset = $BytesUploaded.Value 102 | 103 | # You can only start the upload once. 104 | $first = $false 105 | } 106 | Else 107 | { 108 | # Get a reference to your file. 109 | $Upload = $ctx.Web.GetFileByServerRelativeUrl($Docs.RootFolder.ServerRelativeUrl + [System.IO.Path]::AltDirectorySeparatorChar + $UniqueFileName); 110 | If($last) { 111 | # Is this the last slice of data? 112 | $s = [System.IO.MemoryStream]::new($lastBuffer) 113 | 114 | # End sliced upload by calling FinishUpload. 115 | $Upload = $Upload.FinishUpload($UploadId, $fileoffset, $s) 116 | $ctx.ExecuteQuery() 117 | 118 | Write-Host "File upload complete" 119 | # Return the file object for the uploaded file. 120 | return $Upload 121 | } 122 | else { 123 | $s = [System.IO.MemoryStream]::new($buffer) 124 | # Continue sliced upload. 125 | $BytesUploaded = $Upload.ContinueUpload($UploadId, $fileoffset, $s) 126 | $ctx.ExecuteQuery() 127 | 128 | # Update fileoffset for the next slice. 129 | $fileoffset = $BytesUploaded.Value 130 | } 131 | } 132 | } #// while ((bytesRead = br.Read(buffer, 0, buffer.Length)) > 0) 133 | } 134 | Catch { 135 | Write-Host $_.Exception.Message -ForegroundColor Red 136 | } 137 | Finally { 138 | if ($Fs -ne $null) 139 | { 140 | $Fs.Dispose() 141 | } 142 | } 143 | } 144 | return $null 145 | } 146 | 147 | $siteURL = "https://spjeff-my.sharepoint.com/personal/spjeff_spjeff_com" 148 | Connect-PNPOnline $siteURL 149 | UploadFileInSlice -------------------------------------------------------------------------------- /office365-stale-webs/Stale_Webs_Email_Site_Final.htm: -------------------------------------------------------------------------------- 1 | Attention: You own a stale SharePoint site!!! 2 | This is the FINAL notification, the Site will automatically be deleted in 7 days unless you update some content on the Site to update the last modified date. 3 |

4 | Site Title: {0}
Site URL: {1} 5 |

6 | To view the Site contents, click here
7 | To delete the Site, click here

8 | Please reference the Stale Site Deletion FAQ article for more information. 9 |

10 | If you have any additional questions please open a Helpdesk ticket for SharePoint Support if you have questions or want to speak to the SharePoint support staff. 11 | -------------------------------------------------------------------------------- /office365-stale-webs/Stale_Webs_Email_Site_Owner.htm: -------------------------------------------------------------------------------- 1 | Attention: You own a stale SharePoint site!!! 2 | The following SharePoint Site has not been modified in the past 90 days. Please review the Site and delete it if it is no longer in use.
If you wish to keep it, visit the site and make a modification in order to update the last modified date.
3 | This notification is sent out weekly, and the Site will be automatically deleted after 4 notifications. 4 |

This is notification {2} of 4.

The Site will automatically be deleted one week after the fourth notification.

5 | Site Title: {0}
Site URL: {1}

6 | To view the Site contents, click here
7 | To delete the Site, click here

8 | Please reference the Stale Site Deletion FAQ article for more information.

9 | If you have any additional questions please open a Helpdesk ticket for SharePoint Support. -------------------------------------------------------------------------------- /office365-stale-webs/Stale_Webs_Email_Summary.htm: -------------------------------------------------------------------------------- 1 | Summary - Stale Webs 2 | The following SharePoint Sites have not been modified in the past 90 days. 3 | 4 |

Scan Summmary:

5 |
6 | {0} 7 |
8 | 9 |

Delete Summmary:

10 | 11 | {1} 12 |
13 | -------------------------------------------------------------------------------- /office365-stale-webs/office365-stale-webs.ps1: -------------------------------------------------------------------------------- 1 | <# === Office365 - Stale Webs === 2 | * leverages 3 libraries (SPO, PNP, CSOM) 3 | * leverages parallel PowerShell 4 | * grant Site Collection Admin for support staff 5 | * apply Site Collection quota 5GB (if none) 6 | * enable Site Collection Auditing 7 | * enable Site Collection Custom Action JS ("office365-gpo.js") 8 | #> 9 | 10 | #Config 11 | $AdminUrl = "https://tenant-admin.sharepoint.com" 12 | $UserName = "admin@tenant.onmicrosoft.com" 13 | $Password = "pass@word1" 14 | $ThresholdDays = 30 15 | $ReminderDays = 6 16 | $MaxReminders = 4 17 | $EmailFrom = "sharepoint_support@tenant.com" 18 | 19 | Function defineMetrics() { 20 | # Global cache 21 | $global:dtWebs = New-Object "System.Data.DataTable" -ArgumentList "Webs" 22 | 23 | # Global counters 24 | $global:countScan = 0 25 | $global:countStale = 0 26 | $global:countDelete = 0 27 | 28 | # Schema 29 | $col = $global:dtWebs.Columns.Add("URL", [String]) 30 | $col = $global:dtWebs.Columns.Add("Title", [String]) 31 | $col = $global:dtWebs.Columns.Add("Last Modified Time Web", [String]) 32 | $col = $global:dtWebs.Columns.Add("Owner", [String]) 33 | $col = $global:dtWebs.Columns.Add("IsRootWeb", [Boolean]) 34 | $col = $global:dtWebs.Columns.Add("StaleWebs_WarningCount", [Int]) 35 | $col = $global:dtWebs.Columns.Add("StaleWebs_EmailSentTime", [DateTime]) 36 | $col = $global:dtWebs.Columns.Add("StaleWebs_EmailSent", [Boolean]) 37 | $col = $global:dtWebs.Columns.Add("Deleted", [Boolean]) 38 | $col = $global:dtWebs.Columns.Add("Script Run Date", [DateTime]) 39 | } 40 | 41 | Function emailReminder($final, $title, $url, $to, $reminderNumber) { 42 | # Final notification 43 | if ($final) { 44 | $file = "Stale_Webs_Email_Site_Final.htm" 45 | } 46 | else { 47 | $file = "Stale_Webs_Email_Site_Owner.htm" 48 | } 49 | 50 | # Site Owner 51 | $html = Get-Content $file 52 | $subject = $html[0] 53 | $body = ($html | Select -Skip 1 | Out-String) -f $title, $url, $reminderNumber 54 | 55 | # Send Email 56 | emailCloud $to, $subject, $body 57 | } 58 | Function emailSummary() { 59 | # SharePoint Admin team 60 | 61 | # Pivot table and count 62 | #TODO 63 | 64 | # Summary 65 | $file = "Stale_Webs_Email_Summary.htm" 66 | $html = Get-Content $file 67 | $subject = $html[0] 68 | $body = ($html | Select -Skip 1 | Out-String) 69 | 70 | # Send Email 71 | emailCloud $EmailSupport, $subject, $body 72 | } 73 | 74 | Function emailCloud($to, $subject, $body) { 75 | # Get the PowerShell credential and prints its properties 76 | # $MyCredential = "O365SMTP" 77 | # $cred = Get-AutomationPSCredential -Name $MyCredential 78 | if ($cred -eq $null) {return} 79 | 80 | Send-MailMessage -To $to -Subject $subject -Body $body -UseSsl -Port 587 -SmtpServer 'smtp.office365.com' -From $EmailFrom -BodyAsHtml -Credential $global:cred 81 | } 82 | 83 | Function processWeb($web) { 84 | Write-Host "Processing web $($web.Url)" 85 | 86 | # Current site - Is stale? 87 | $url = $web.Url 88 | $stale = $false 89 | $lists = Get-PnPList -Web $web -Includes "LastItemModifiedDate" | % {New-Object PSObject -Property @{LastModified = $_.LastModified; }} 90 | $mostRecentList = ($lists | sort LastModified -Desc)[0] 91 | $age = (Get-Date) - ([datetime]$mostRecentList.LastModified) 92 | if ($age.Days -gt $global:ThresholdDays) { 93 | $stale = $true 94 | } 95 | 96 | # Current property bag 97 | $StaleWebs_WarningCount = Get-SPOPropertyBag -Key "StaleWebs_WarningCount" 98 | $StaleWebs_EmailSentTime = Get-SPOPropertyBag -Key "StaleWebs_EmailSentTime" 99 | 100 | # Email recipient 101 | $to = $web.RequestAccessEmail 102 | $to = $EmailFrom 103 | 104 | if ($stale) { 105 | if (!$StaleWebs_WarningCount) { 106 | # First notification 107 | Set-SPOPropertyBag -Key "StaleWebs_WarningCount" -Value 1 108 | Set-SPOPropertyBag -Key "StaleWebs_EmailSentTime" -Value (Get-Date) 109 | 110 | # Add row to table 111 | $newRow = $global:dtWebs.NewRow() 112 | $newRow["URL"] = $url 113 | $newRow["Title"] = $web.Tile 114 | $newRow["Last Modified Time Web"] = $mostRecentList.LastModified 115 | $newRow["Owner"] = $web.RequestAccessEmail 116 | $newRow["IsRootWeb"] = $web.IsRootWeb 117 | $newRow["StaleWebs_WarningCount"] = $StaleWebs_WarningCount 118 | $newRow["StaleWebs_EmailSentTime"] = $StaleWebs_EmailSentTime 119 | $newRow["Deleted"] = 0 120 | $global:dtWebs.Rows.Add($newRow) 121 | 122 | # Email notify site owner 123 | emailReminder $false, $web.Title, $url, $to 124 | } 125 | elseif ($StaleWebs_WarningCount -gt $MaxReminders) { 126 | # Delete 127 | Write-Host "Deleting web $url" 128 | Remove-PnPWeb $url -Force 129 | 130 | # Add row to table 131 | $newRow = $global:dtWebs.NewRow() 132 | $newRow["URL"] = $url 133 | $newRow["Title"] = $web.Tile 134 | $newRow["Last Modified Time Web"] = $mostRecentList.LastModified 135 | $newRow["Owner"] = $web.RequestAccessEmail 136 | $newRow["IsRootWeb"] = $web.IsRootWeb 137 | $newRow["StaleWebs_WarningCount"] = $StaleWebs_WarningCount 138 | $newRow["StaleWebs_EmailSentTime"] = $StaleWebs_EmailSentTime 139 | $newRow["Deleted"] = 1 140 | $global:dtWebs.Rows.Add($newRow) 141 | 142 | # Email notify site owner 143 | emailReminder $true, $web.Title, $url, $to 144 | } 145 | else { 146 | # Reminder 147 | $timeSinceLastReminder = (Get-Date) - $StaleWebs_EmailSentTime 148 | if ($timeSinceLastReminder.Hours -gt $ReminderDays) { 149 | $StaleWebs_WarningCount++ 150 | $StaleWebs_EmailSentTime = Get-Date 151 | Set-SPOPropertyBag -Key "StaleWebs_WarningCount" -Value $StaleWebs_WarningCount 152 | Set-SPOPropertyBag -Key "StaleWebs_EmailSentTime" -Value $StaleWebs_EmailSentTime 153 | 154 | # Add row to table 155 | $newRow = $global:dtWebs.NewRow() 156 | $newRow["URL"] = $url 157 | $newRow["Title"] = $web.Tile 158 | $newRow["Last Modified Time Web"] = $mostRecentList.LastModified 159 | $newRow["Owner"] = $web.RequestAccessEmail 160 | $newRow["IsRootWeb"] = $web.IsRootWeb 161 | $newRow["StaleWebs_WarningCount"] = $StaleWebs_WarningCount 162 | $newRow["StaleWebs_EmailSentTime"] = $StaleWebs_EmailSentTime 163 | $newRow["Deleted"] = 0 164 | $global:dtWebs.Rows.Add($newRow) 165 | 166 | # Email notify site owner 167 | emailReminder $false, $web.Title, $url, $to 168 | } 169 | } 170 | } 171 | } 172 | 173 | Function Main { 174 | # Log 175 | Start-Transcript 176 | $start = Get-Date 177 | 178 | # Metrics 179 | defineMetrics 180 | 181 | # SPO and PNP modules 182 | Import-Module Microsoft.Online.SharePoint.PowerShell -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 183 | Import-Module SharePointPnPPowerShellOnline -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null 184 | 185 | # Credential 186 | $secpw = ConvertTo-SecureString -String $Password -AsPlainText -Force 187 | $global:cred = New-Object System.Management.Automation.PSCredential ($UserName, $secpw) 188 | 189 | # Connect Office 365 190 | Connect-SPOService -URL $AdminUrl -Credential $global:cred 191 | 192 | # Scope 193 | Write-Host "Opening list of sites ..." -Fore Green 194 | $sites = Get-SPOSite 195 | Write-Host $sites.Count 196 | 197 | # Serial loop 198 | Write-Host "Loop sites " 199 | ForEach ($s in $sites) { 200 | Write-Host "." -NoNewLine 201 | 202 | # PNP 203 | Connect-PnPOnline -Url $s.Url -Credentials $global:cred 204 | 205 | # Root 206 | $root = Get-PnPWeb 207 | processWeb $root 208 | 209 | # Child webs 210 | $webs = Get-PnPSubWebs -Recurse 211 | $webs | % {processWeb $_} 212 | } 213 | 214 | # Email Summary 215 | emailSummary 216 | 217 | # Duration 218 | $min = [Math]::Round(((Get-Date) - $start).TotalMinutes, 2) 219 | Write-Host "Duration Min : $min" 220 | Stop-Transcript 221 | } 222 | Main -------------------------------------------------------------------------------- /office365-video/o365-video-channels-info.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | https://docs.microsoft.com/en-us/stream/migration-o365video-prep 3 | 4 | Powershell tool to output information about Office 365 Video, to be used to help prep for O365 Video to Stream migration. 5 | 6 | Change log: 7 | 1/13/2020 by Marc Mroz 8 | - Fix text file/csv encoding to UTF8 to support non-ASCII characters 9 | 10 | 1/9/2020 by Marc Mroz 11 | - Fixed bug where owners/editor/viewer data for a channel wasn't output when the site collection's language wasn't in English 12 | - Add 3 extra columns to the report to export channel's owners/editors/viewers permissions that don't have email addresses 13 | 14 | 11/20/2019 by Marc Mroz 15 | -Added support for multifactor authentication (MFA) 16 | -Added ability to loop over each video in the channel and sum up the view counts over time 17 | #> 18 | 19 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") 20 | [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") 21 | 22 | #Do you want to download the videos from O365 Video to your PC 23 | $DownloadFile = $false 24 | 25 | #--------------Don't make any changes to the script below------------ 26 | 27 | <# 28 | Creates 2 strings, one with a list of email addresses, the other with a list of names/titles 29 | If a user in the permissions list doesn't have an email address (it's a non-mail SG, special SP ACL, etc) then we output the title 30 | of that ACL into a seperate string. 31 | 32 | Parameters: 33 | $permissionListObj (input) - The object listing users that have permission to a chanel as gotten from the O365 Video REST API: 34 | /portals/hub/_api/VideoService/Channels('')/GetPermissionGroup()/Users 35 | $permissionEmailList (output) - Formatted string with ; list of email addresses gotten from $permissionListObj 36 | $permisisonOtherList (output) - Formatted string with ; list of titles gotten from $permissionListObj 37 | #> 38 | function CreatePermissionsListStrings($permissionListObj, [ref]$permissionEmailList, [ref]$permisisonOtherList) 39 | { 40 | $permissionEmailList.Value = "" 41 | $permisisonOtherList.Value = "" 42 | 43 | for ($i=0;$i -lt $permissionListObj.value.Count; $i++) 44 | { 45 | $email = $permissionListObj.value[$i].Email 46 | if ($email.Contains("@")) { 47 | $permissionEmailList.Value = $permissionEmailList.Value + $email + '; ' 48 | } 49 | else { 50 | $title = $permissionListObj.value[$i].Title 51 | $permisisonOtherList.Value = $permisisonOtherList.Value + $title + '; ' 52 | } 53 | } 54 | 55 | } 56 | 57 | 58 | #Use the same folder the script is running in to output the reports 59 | $PathforFiles = $PSScriptRoot + "\output\" 60 | 61 | #Create "output" directory if it doesn't exist 62 | New-Item -ItemType Directory -Force -Path $PathforFiles 63 | 64 | Clear-Host 65 | Write-Host "Reports on Office 365 Video channels, to be used for O365 Video to Stream migration prep." 66 | Write-Host "" 67 | Write-Host "CSV and text files will be output to this folder: " 68 | Write-Host $PathforFiles 69 | Write-Host "" 70 | Write-Host "" 71 | Write-Host "The script will prompt you for a login. You must login with a O365 Global Admin user." 72 | Write-Host "" 73 | Write-Host "Enter the following info to run the script..." 74 | 75 | #Get the user's SPO url 76 | $O365SPOUrl = Read-Host -Prompt "SharePoint Online url (eg https://contoso.sharepoint.com)" 77 | 78 | #Ask if the user wants to output view counts in the report (view counts make the script take forever to run) 79 | $YesOrNo = Read-Host "Report on sum of views for all videos in each channel? This makes the script take much longer to run. (y/n)" 80 | while("y","n" -notcontains $YesOrNo ) 81 | { 82 | $YesOrNo = Read-Host "Report on sum of views for all videos in each channel? This makes the script take much longer to run. (y/n)" 83 | } 84 | $IncludeViewCounts = $false 85 | if ($YesOrNo -eq 'y') {$IncludeViewCounts = $true} 86 | 87 | #Prompt the user for login and PW. They need to login as an O365 Global Admin other wise some API calls won't return data. 88 | #-UseWebLogin will show a normal login window which supports MFA logins 89 | Write-Host "" 90 | Write-Host "You will be prompted to login to your organization. Make sure you login as an Office 365 global admin..." 91 | Connect-PnPOnline -Url $O365SPOUrl -UseWebLogin 92 | 93 | $csvFile = $PathforFiles + "Channels-Info.csv" 94 | $LogFile = $PathforFiles + "Log-Trace.txt" 95 | $FilesToExport = $PathforFiles + "Videos-File-List.txt" 96 | $FileDownloadPath = $PathforFiles + "Downloads\" 97 | 98 | 99 | $O365VideoPortalHubUrl = $O365SPOUrl + "/portals/hub" 100 | $O365VideoRESTUrl = $O365VideoPortalHubUrl + "/_api/VideoService" 101 | 102 | #Prompt for admin login - MFA will be supported as it uses the web login 103 | Connect-PnPOnline -Url $O365SPOUrl -UseWebLogin 104 | 105 | 106 | Write-Host "" 107 | Write-Host "" 108 | Write-Host "Running script" -NoNewline 109 | 110 | #Get channel list from the O365 Video API 111 | $O365VideoChannelsRestAPI = $O365VideoRESTUrl + "/Channels" 112 | Add-Content $LogFile "Connecting to SPO..." 113 | 114 | $Channels = Invoke-PnPSPRestMethod -Url $O365VideoChannelsRestAPI 115 | 116 | 117 | #Get info about each channel 118 | if ($Channels -ne $null) 119 | { 120 | #Create channel object to hold info about a single channel that will be added to CSV 121 | $ChannelObj = New-Object -TypeName psobject 122 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel name' -Value 'Missing data' 123 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel URL' -Value 'Missing data' 124 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel GUID' -Value 'Missing data' 125 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel owners with email addresses' -Value 'Missing data' 126 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel owners without email addresses' -Value 'Missing data' 127 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel editors with email addresses' -Value 'Missing data' 128 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel editors without email addresses' -Value 'Missing data' 129 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel viewers with email addresses' -Value 'Missing data' 130 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Channel viewers without email addresses' -Value 'Missing data' 131 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Count of videos in channel' -Value 'Missing data' 132 | if ($IncludeViewCounts) { 133 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Sum of views (last 3 months) for videos in channel' -Value 'Missing data' 134 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Sum of views (last 6 months) for videos in channel' -Value 'Missing data' 135 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Sum of views (last 12 months) for videos in channel' -Value 'Missing data' 136 | $ChannelObj | Add-Member -MemberType NoteProperty -Name 'Sum of views (last 24 months) for videos in channel' -Value 'Missing data' 137 | } 138 | 139 | 140 | 141 | $TotalChannelMsg = 'There are total of ' + $Channels.value.Count + ' Channels' 142 | Add-Content $LogFile 'Channels retrieved, looping through channels and videos now...' -Encoding UTF8 143 | Add-Content $FilesToExport 'Channels retrieved, looping through channels and videos now...' -Encoding UTF8 144 | Add-Content $FilesToExport $TotalChannelMsg -Encoding UTF8 145 | 146 | for ($i=0;$i -lt $Channels.value.Count; $i++) 147 | { 148 | Write-Host "." -NoNewline 149 | 150 | $ChannelGUID = $Channels.value[$i].Id 151 | $ChannelRestAPIUrl= $O365VideoRESTUrl + "/Channels('" + $ChannelGUID + "')" 152 | 153 | $ChannelURL = $O365SPOUrl + $Channels.value[$i].ServerRelativeUrl 154 | $ChannelURLText = 'Channel Site Collection URL is: ' + $ChannelURL 155 | Add-Content $LogFile $ChannelURLText -Encoding UTF8 156 | 157 | $CreatorsRestAPIUrl = $ChannelRestAPIUrl + "/GetPermissionGroup(" + "2)/Users" 158 | $ContributorsRestAPIUrl = $ChannelRestAPIUrl + "/GetPermissionGroup(" + "0)/Users" 159 | $ViewersRestAPIUrl = $ChannelRestAPIUrl + "/GetPermissionGroup(" + "1)/Users" 160 | 161 | try {$CreatorsList = Invoke-PnPSPRestMethod -Url $CreatorsRestAPIUrl -ErrorAction Stop} 162 | catch { 163 | Add-Content $LogFile "Error calling: $CreatorsRestAPIUrl" -Encoding UTF8 164 | Add-Content $LogFile "+Error msg: $($PSItem.ToString())" -Encoding UTF8 165 | } 166 | 167 | try {$ContributorsList = Invoke-PnPSPRestMethod -Url $ContributorsRestAPIUrl -ErrorAction Stop} 168 | catch { 169 | Add-Content $LogFile "Error calling: $ContributorsRestAPIUrl" -Encoding UTF8 170 | Add-Content $LogFile "+Error msg: $($PSItem.ToString())" -Encoding UTF8 171 | } 172 | 173 | try {$ViewersList = Invoke-PnPSPRestMethod -Url $ViewersRestAPIUrl -ErrorAction Stop} 174 | catch { 175 | Add-Content $LogFile "Error calling: $ViewersRestAPIUrl" -Encoding UTF8 176 | Add-Content $LogFile "+Error msg: $($PSItem.ToString())" -Encoding UTF8 177 | } 178 | 179 | Add-Content $LogFile '==========================================' -Encoding UTF8 180 | Add-Content $LogFile 'Enumerating Channel Owners' -Encoding UTF8 181 | Add-Content $LogFile $CreatorsList.value.Email -Encoding UTF8 182 | Add-Content $LogFile $CreatorsList.value.Title -Encoding UTF8 183 | Add-Content $LogFile '==========================================' -Encoding UTF8 184 | Add-Content $LogFile 'Enumerating Channel Editors' -Encoding UTF8 185 | Add-Content $LogFile $ContributorsList.value.Email -Encoding UTF8 186 | Add-Content $LogFile $ContributorsList.value.Title -Encoding UTF8 187 | Add-Content $LogFile '==========================================' -Encoding UTF8 188 | Add-Content $LogFile 'Enumerating Channel Viewers' -Encoding UTF8 189 | Add-Content $LogFile $ViewersList.value.Email -Encoding UTF8 190 | Add-Content $LogFile $ViewersList.value.Title -Encoding UTF8 191 | Add-Content $LogFile '==========================================' 192 | 193 | Add-Content $FilesToExport $ChannelURLText -Encoding UTF8 194 | $ChannelGUIDString = 'Channel GUID is: ' + $ChannelGUID 195 | Add-Content $LogFile $ChannelGUIDString -Encoding UTF8 196 | Add-Content $FilesToExport $ChannelGUIDString -Encoding UTF8 197 | 198 | #add info to channel object which will be output to the CSV 199 | $ChannelObj.'Channel name' = $Channels.value[$i].Title 200 | $ChannelObj.'Channel URL' = $ChannelURL 201 | $ChannelObj.'Channel GUID' = $ChannelGUID 202 | 203 | #email property for users only populated for 204 | # 1. licensed users - if the user isn't licensed SPO doesn't populate the email property 205 | # 2. Security groups that aren't mail enabled - if not mail enabled obviously no email property 206 | # 3. Special SP ACLs like "Everyone except external users" 207 | # so we are splitting into 2 columns in the report one with email addresses and one where we just show the titles of the permission entites 208 | 209 | $permissionEmailList = "" 210 | $permisisonOtherList = "" 211 | 212 | CreatePermissionsListStrings $CreatorsList ([ref]$permissionEmailList) ([ref]$permisisonOtherList) 213 | $ChannelObj.'Channel owners with email addresses' = $permissionEmailList 214 | $ChannelObj.'Channel owners without email addresses' = $permisisonOtherList 215 | 216 | CreatePermissionsListStrings $ContributorsList ([ref]$permissionEmailList) ([ref]$permisisonOtherList) 217 | $ChannelObj.'Channel editors with email addresses' = $permissionEmailList 218 | $ChannelObj.'Channel editors without email addresses' = $permisisonOtherList 219 | 220 | CreatePermissionsListStrings $ViewersList ([ref]$permissionEmailList) ([ref]$permisisonOtherList) 221 | $ChannelObj.'Channel viewers with email addresses' = $permissionEmailList 222 | $ChannelObj.'Channel viewers without email addresses' = $permisisonOtherList 223 | 224 | #Get all the videos in each channel 225 | $VideoChannelRESTUrl = $ChannelRestAPIUrl + "/Videos" 226 | 227 | try {$VideosInChannel = Invoke-PnPSPRestMethod -Url $VideoChannelRESTUrl -ErrorAction Stop} 228 | catch { 229 | Add-Content $LogFile "Error calling: $VideoChannelRESTUrl" -Encoding UTF8 230 | Add-Content $LogFile "+Error msg: $($PSItem.ToString())" -Encoding UTF8 231 | } 232 | 233 | 234 | #Clear the sums of views on all videos in channel. Using null because we want to know if we were able to get any data or the API itself to get the 235 | #analytics was not returning anything at all. We don't want to confuse 0 views with we weren't able to get any view counts at all for this video because 236 | #it's new (not in search index) or the search analytics counts aren't tabulated or is broken. Will check at bottom if each sum is not null or not. 237 | $SumVideoViews3Months = $null 238 | $SumVideoViews6Months = $null 239 | $SumVideoViews12Months = $null 240 | $SumVideoViews24Months = $null 241 | 242 | 243 | if ($VideosInChannel -ne $null) 244 | { 245 | $videocountMsg = 'Channel has ' + $VideosInChannel.value.Count + ' videos' 246 | Add-Content $LogFile 'Channel is not empty, looping through videos now' -Encoding UTF8 247 | Add-Content $LogFile $videocountMsg -Encoding UTF8 248 | Add-Content $FilesToExport $videocountMsg -Encoding UTF8 249 | 250 | #add count of videos in the channel to object which will be output to CSV 251 | $ChannelObj.'Count of videos in channel' = $VideosInChannel.value.Count 252 | 253 | #Get info about each video in the channel 254 | for ($j=0;$j -lt $VideosInChannel.value.Count; $j++) 255 | { 256 | 257 | if ($IncludeViewCounts) { 258 | #Get view counts for the last 24 months for a video 259 | $VideoViewsOverTimeUrl = $VideoChannelRESTUrl + '(guid''' + $VideosInChannel.value[$j].ID + ''')/GetVideoDetailedViewCount' 260 | Add-Content $LogFile $VideoViewsOverTimeUrl -Encoding UTF8 261 | 262 | $VideoViews = $null 263 | $CurrentVideoViewsLast3Months = $null 264 | $CurrentVideoViewsLast6Months = $null 265 | $CurrentVideoViewsLast12Months = $null 266 | $CurrentVideoViewsLast24Months = $null 267 | 268 | try {$VideoViews = Invoke-PnPSPRestMethod -Url $VideoViewsOverTimeUrl -ErrorAction Stop} 269 | catch { 270 | Add-Content $LogFile "Error calling: $VideoViewsOverTimeUrl" -Encoding UTF8 271 | Add-Content $LogFile "+Error msg: $($PSItem.ToString())" -Encoding UTF8 272 | } 273 | 274 | #$MonthsCnt = $VideoViews.Months.Count 275 | #$MonthsCntMsg = "Vidoe's month node count:"+ $MonthsCnt 276 | #Add-Content $LogFile $MonthsCntMsg -Encoding UTF8 277 | 278 | if ($VideoViews.Months.Count -ne 0) 279 | { 280 | 281 | for ($k=0;$k -lt 24; $k++) 282 | { 283 | $MonthTotalHits = $VideoViews.Months[$k].TotalHits 284 | if ($k -lt 3) {$CurrentVideoViewsLast3Months = $CurrentVideoViewsLast3Months + $MonthTotalHits} 285 | if ($k -lt 6) {$CurrentVideoViewsLast6Months = $CurrentVideoViewsLast6Months + $MonthTotalHits} 286 | if ($k -lt 12) {$CurrentVideoViewsLast12Months = $CurrentVideoViewsLast12Months + $MonthTotalHits} 287 | if ($k -lt 24) {$CurrentVideoViewsLast24Months = $CurrentVideoViewsLast24Months + $MonthTotalHits} 288 | } 289 | 290 | $SumVideoViews3Months = $SumVideoViews3Months + $CurrentVideoViewsLast3Months 291 | $SumVideoViews6Months = $SumVideoViews6Months + $CurrentVideoViewsLast6Months 292 | $SumVideoViews12Months = $SumVideoViews12Months + $CurrentVideoViewsLast12Months 293 | $SumVideoViews24Months = $SumVideoViews24Months + $CurrentVideoViewsLast24Months 294 | 295 | } 296 | } 297 | 298 | 299 | $VideoPath = $O365SPOUrl + $VideosInChannel.value[$j].ServerRelativeUrl 300 | #Add-Content $LogFile $VideoPath -Encoding UTF8 301 | 302 | $VideoFilePathFragments = $VideosInChannel.value[$j].ServerRelativeUrl.Split('/') 303 | $VideoFilePath = "/" + $VideoFilePathFragments[3] + "/" + $VideoFilePathFragments[4] 304 | $VideoFileDownloadPath = $FileDownloadPath + "\" + $VideoFilePathFragments[2] 305 | 306 | if ($DownloadFile) 307 | { 308 | if ( -not(Test-Path $VideoFileDownloadPath)) 309 | { 310 | New-Item -Path $FileDownloadPath -Name $VideoFilePathFragments[2] -ItemType "directory" 311 | } 312 | 313 | $FileURL = $VideosInChannel.value[$j].ServerRelativeUrl 314 | $FileURL = $FileURL.Replace("'", "''") 315 | #$VideosInChannel.value[$j].ServerRelativeUrl 316 | 317 | Download-SPOFile -WebUrl $ChannelURL -UserName $UserName -Password $SecurePassword -FileUrl $FileURL -DownloadPath $VideoFileDownloadPath 318 | 319 | } 320 | else 321 | { 322 | Add-Content $FilesToExport $VideoPath -Encoding UTF8 323 | } 324 | } 325 | Add-Content $LogFile '**********************************************************' -Encoding UTF8 326 | Add-Content $LogFile '**********************************************************' -Encoding UTF8 327 | } 328 | 329 | #If we were able to get view counts all the videos in the channel then output those sums to the CSV 330 | if ($IncludeViewCounts) { 331 | $ChannelObj.'Sum of views (last 3 months) for videos in channel' = $SumVideoViews3Months 332 | $ChannelObj.'Sum of views (last 6 months) for videos in channel' = $SumVideoViews6Months 333 | $ChannelObj.'Sum of views (last 12 months) for videos in channel' = $SumVideoViews12Months 334 | $ChannelObj.'Sum of views (last 24 months) for videos in channel' = $SumVideoViews24Months 335 | } 336 | 337 | #Write out the CSV to the file now 338 | $ChannelObj | Export-Csv -Path $csvFile -Append -NoTypeInformation -Encoding UTF8 339 | 340 | } 341 | Write-Host "Done" 342 | Write-Host "" 343 | Write-Host "CSV and text files in this folder: " $PathforFiles 344 | Write-Host $csvFile 345 | Write-Host " CSV spreadsheet of all channels in O365 Video with which users and mail enabled security groups have access" 346 | Write-Host "" 347 | Write-Host "$FilesToExport" 348 | Write-Host " text file with links to all the SPO URLs for each channel and all videos within each channel" 349 | Write-Host "" 350 | Write-Host "$LogFile" 351 | Write-Host " diagnostic log file for script if needed for debugging" 352 | 353 | } 354 | 355 | 356 | 357 | 358 | 359 | -------------------------------------------------------------------------------- /office365-wait-dom/waitDom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/office365-wait-dom/waitDom.gif -------------------------------------------------------------------------------- /office365-wait-dom/waitDom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /office365-wait-dom/waitDom.js: -------------------------------------------------------------------------------- 1 | //referenced code at http://stackoverflow.com/questions/16149431/make-function-wait-until-element-exists 2 | 3 | function waitDom(id, fn) { 4 | var checkExist = setInterval(function() { 5 | if (document.getElementById(id)) { 6 | console.log("OK!"); 7 | clearInterval(checkExist); 8 | fn(); 9 | } else { 10 | console.log('wait...'); 11 | } 12 | }, 500); 13 | } 14 | 15 | function hello() { 16 | alert('hello'); 17 | } 18 | 19 | function addContainer() { 20 | document.write("
"); 21 | } 22 | 23 | //main 24 | waitDom('container', hello); 25 | window.setTimeout(addContainer, 2000); -------------------------------------------------------------------------------- /outlook-reply-with-OFT-template-attachment-tokens.vb: -------------------------------------------------------------------------------- 1 | ' from https://stackoverflow.com/questions/38200239/outlook-macro-reply-to-sender-with-template 2 | Sub JobApply() 3 | 4 | Dim origEmail As MailItem 5 | Dim replyEmail As MailItem 6 | Dim firstName As String 7 | 8 | Set origEmail = ActiveExplorer.Selection(1) 9 | Set replyEmail = CreateItemFromTemplate("C:\BIN\template.oft") 10 | firstName = Split(origEmail.Reply.To, " ")(0) 11 | 12 | replyEmail.To = origEmail.Reply.To & "<" & origEmail.SenderEmailAddress & ">" 13 | 14 | replyEmail.HTMLBody = Replace(replyEmail.HTMLBody, "{0}", firstName) & origEmail.Reply.HTMLBody 15 | 'replyEmail.SentOnBehalfOfName = "email@domain.com" 16 | replyEmail.Subject = "RE: " & origEmail.Subject 17 | replyEmail.Recipients.ResolveAll 18 | replyEmail.Display 19 | 20 | Set origEmail = Nothing 21 | Set replyEmail = Nothing 22 | 23 | End Sub 24 | -------------------------------------------------------------------------------- /pnp-connect/PNP-Register.ps1: -------------------------------------------------------------------------------- 1 | # PNP Register 2 | # https://pnp.github.io/powershell/articles/connecting.html 3 | # https://pnp.github.io/powershell/articles/authentication.html 4 | # https://docs.microsoft.com/en-us/powershell/module/sharepoint-pnp/register-pnpazureadapp?view=sharepoint-ps 5 | # https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps 6 | # https://mmsharepoint.wordpress.com/2018/12/19/modern-sharepoint-authentication-in-azure-automation-runbook-with-pnp-powershell/ 7 | # Scope 8 | $tenant = "spjeff" 9 | $clientFile = "PnP-PowerShell-$tenant.txt" 10 | # Register 11 | $password = ConvertTo-SecureString -String "password" -AsPlainText -Force 12 | $reg = Register-PnPAzureADApp -ApplicationName "PnP-PowerShell-$tenant" -Tenant "$tenant.onmicrosoft.com" -CertificatePassword $password -Interactive 13 | $reg."AzureAppId/ClientId" | Out-File $clientFile -Force -------------------------------------------------------------------------------- /pnp-connect/PnP-PowerShell-spjeff-Connect-PNPOnline.ps1: -------------------------------------------------------------------------------- 1 | # PnP-PowerShell-spjeff-Connect-PNPOnline.ps1 2 | 3 | # PNP Connect 4 | # https://pnp.github.io/powershell/articles/connecting.html 5 | # https://pnp.github.io/powershell/articles/authentication.html 6 | # https://docs.microsoft.com/en-us/powershell/module/sharepoint-pnp/register-pnpazureadapp?view=sharepoint-ps 7 | # https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps 8 | # https://mmsharepoint.wordpress.com/2018/12/19/modern-sharepoint-authentication-in-azure-automation-runbook-with-pnp-powershell/ 9 | 10 | # Scope 11 | $tenant = "spjeff" 12 | 13 | # Azure Certificate 14 | $password = "password" 15 | $secPassword = $password | ConvertTo-SecureString -AsPlainText -Force 16 | $cert = Get-AutomationCertificate -Name 'PNP-PowerShell' 17 | $pfxCert = $cert.Export("pfx" , $password ) # 3=Pfx 18 | $certPath = "PNP-PowerShell.pfx" 19 | Set-Content -Value $pfxCert -Path $certPath -Force -Encoding Byte 20 | 21 | # Connect 22 | $clientId = "client-id-guid" 23 | Connect-PnPOnline -ClientId $clientId -Url "https://$tenant.sharepoint.com" -Tenant "$tenant.onmicrosoft.com" -CertificatePath $certPath -CertificatePassword $secPassword 24 | Get-PnPTenantSite | Format-Table -AutoSize 25 | -------------------------------------------------------------------------------- /pnp-connect/PnP-PowerShell-spjeff.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/pnp-connect/PnP-PowerShell-spjeff.txt -------------------------------------------------------------------------------- /spo-modern-CEWP-wide-CSS/Modern CEWP wide CSS.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spo-modern-CEWP-wide-CSS/modern-cewp.sppkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spjeff/office365/460c21e738ff6e8f2a2abbc5d4a84757aa698407/spo-modern-CEWP-wide-CSS/modern-cewp.sppkg -------------------------------------------------------------------------------- /xrm-toolbox/XRM-Cmdlet-Query-Dataverse-Rows.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Demo feasiblity and prototype code for download large data set rows from MS Dataverse table into local PowerShell console. 3 | Support pagingation and FetchXML query language for high performance and scale. 4 | 5 | * High scale processing High speed performance 6 | * Line number breakpoint precision debug 7 | * Line by line transcript LOG text run history 8 | * Import any third party module (SharePoint, PNP, SQL, etc.) 9 | 10 | References 11 | https://vishalgrade.com/2023/10/03/how-to-use-powershell-in-dynamics-crm-to-perform-crud-operations/ 12 | 13 | .EXAMPLE 14 | .\XRM-Cmdlet-Query-Dataverse-Rows.ps1 15 | 16 | .NOTES 17 | File Name: XRM-Cmdlet-Query-Dataverse-Rows 18 | Author : Jeff Jones - @spjeff 19 | Modified : 2024-05-11 20 | 21 | .LINK 22 | https://admin.powerplatform.microsoft.com/environments 23 | #> 24 | 25 | # Modules 26 | $ModuleName = "Microsoft.Xrm.Data.PowerShell" 27 | Install-Module $ModuleName -Scope "CurrentUser" 28 | Import-Module $ModuleName -Force 29 | $xrmCommands = Get-Command -Module $ModuleName 30 | $xrmCommands.Count 31 | 32 | # Configuration 33 | $url = "https://org12345678.crm.dynamics.com/" 34 | $fetch = @' 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | '@ 44 | 45 | # Connect 46 | $conn = Get-CrmConnection -InteractiveMode 47 | $conn 48 | 49 | # Download data 50 | $result = Get-CrmRecordsByFetch -conn $conn -Fetch $fetch 51 | $rows = $result.CrmRecords 52 | 53 | # Display data 54 | $rows | Out-GridView 55 | --------------------------------------------------------------------------------