├── .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 | 
10 | 
11 |
12 | ## Video
13 | [](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 | 
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 | ID (usually leave blank)
347 | Url (e.g. SiteAssets/hello.js)
348 | Sequence (e.g. 1000)
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 | Install Site Collection
358 | Uninstall Site Collection
359 |
360 |
361 |
362 | Install Current Web
363 | Uninstall Current Web
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 |
8 |
9 |
Delete Summmary:
10 |
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 |
--------------------------------------------------------------------------------