Showing posts with label SPE. Show all posts
Showing posts with label SPE. Show all posts

Monday, October 27, 2025

Sitecore Powershell Reporting

Sitecore Powershell Reporting

The Sitecore PowerShell Extensions (SPE) module has a lot of nice features. One of them is creating reports. You get quite some reports out-of-the-box with the module, but you can also create your own. 

I recently noticed however that people are still creating custom pages to create reports for the admins of the customer. Although this gives you a lot of flexibility and perhaps options to include data that resides not in Sitecore this also has some drawbacks compared to creating reports in PowerShell. 

The SPE reports are completely integrated in the Sitecore editing environment. This makes it possible to (amongst others):
  • use context items: the output in the report can be based upon the item it was requested on
  • open items in the editor: directly open the editor from the report
  • more future-proof - especially if you would move to a saas solution
Next to that - the SPE module comes with some very handy features like the Show-ListView which gives your admins a nice toolbox without any effort. 

But enough about the benefits of SPE - let's dive into the example I wanted to show here.

Redirect module

I assume many Sitecore developers have had the request to add some sort of redirect module, enabling editors to create redirects without intervention of IT-people having to change the rewrite instructions.

There used to be several modules floating around in the Sitecore community - but to be honest none of them was ever really perfect. SXA also has it's own implementation of redirects, and that had a different approach than most of the modules. Instead of having a repository of redirect items and using pipeline code to handle them, it is also an option to place the redirects in the tree where you actually want them.

This brings me to the request that led to this post: creating redirect in a non-SXA XM project in a way that they are easily handled by the admins. 

We decided to create a page template with a layout that redirect the page based on some values that can be edited inside the page (eg url, permanent redirect, ... ). You can actually go pretty far in this if you want but as that is not our main focus here let's keep it to that. The focus is here is on answering the question from the Sitecore admins:

"Where are all my redirects?"

Let's answer that question with Sitecore PowerShell. Note that we are using redirects here as an example and this can be done for any kind of data in Sitecore. Also note that out-of-the-box there already is a report to fetch all items based on a template within a folder. But that report is very generic and we wanted a fancy one. Of course, we will copy from this one to give us a head start.

We start with some functions to assist the actual reports.

Sitecore PowerShell Functions

Functions are a way to reuse parts of the code. We will use two functions for our report. The first one is to check whether the item is published to the web database. As many pieces of software, this one is just a copy. But as we in the Sitecore community are honest, we mention our sources. Kudos to Gabriel Streza for this one.

Get-IsPublished
function Get-IsPublished {
    [CmdletBinding()]
    param( 
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [Sitecore.Data.Items.Item]$Item        
    )
   
    $WebDbItem = Get-Item web: -Id $Item.ID

    if ($null -ne $WebDbItem) {
        return $true
    }else{
        return $false
    }
}
I assume this one is fairly easy and doesn't need more explanation. 

The second function will fetch the items and show the report. This is actually the core of the whole thing. 

Show-RedirectReport
function Show-RedirectReport{ 
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [string]$templateId,
        [Parameter(Mandatory=$true, Position=1)]
        [string]$path
    )
        
	$items = Find-Item -Index sitecore_master_index `
	   -Criteria @{Filter = "Equals"; Field = "_template"; Value = "$templateId"},
	   @{Filter = "Equals"; Field = "_language"; Value = "en"},
	   @{Filter = "StartsWith"; Field = "_fullpath"; Value = "$path" }  | Initialize-Item

        Import-Function Get-IsPublished

	if($items.Count -eq 0) {
		Show-Alert "There are no redirects here."
	} else {
	    $props = @{
		Title = "Redirect Report"
		PageSize = 25
	    }
		
	    $items |
		Show-ListView @props -Property @{Label="Name"; Expression={$_.DisplayName} },
	    	    @{Label="Updated"; Expression={$_.__Updated} },
		    @{Label="Updated by"; Expression={[Sitecore.Security.Accounts.User]::FromName($_."__Updated by", $false).Profile.FullName}},
		    @{Label="Path"; Expression={$_.ItemPath} },
		    @{Label="Target - Url"; Expression={$link = $_._.RedirectUrl
		        if ($link.IsInternal) { $link.TargetItem.Paths.Path } else { $link.Url }} },
		    @{Label="Permanent"; Expression={$_._.Permanent.Checked} },
		    @{Label="Published"; Expression={Get-IsPublished -Item $_ } }
	}
}
We will go a little deeper into this one. It has two parts.
First of all we are fetching items through the master index. The function expects a templateID and a path and we will use those to fetch all items of that template in that path (in English).

If we found some items, we are using Show-ListView to display the report. Most of the fields we are showing are quite common but a few deserve some attenion:
  • Updated by: we are using the security account here to fetch the full name of the profile instead of the Sitecore login name as that might be much more readable. Kudos to the one and only Adam Najmanowicz for this one.
  • Target: this can be an internal or external link so we use the "._" notation (short for .PSFields) to access the typed field and check what we need to show
  • Permanent: again using ._ to handle this as a checkbox
  • Published: using our Get-Published function here


The report

Once we have our two functions, the report itself is very easy. We need to collect the parameters to pass to the report function and we should be ok.
$templatePath = "master:\templates\xxx\Shared\Redirect"
$baseTemplate = Get-Item -Path $templatePath
$templateId = $baseTemplate.ID
$path = "/sitecore/content/Sites"

Import-Function Show-RedirectReport
Show-RedirectReport -templateId $templateId -path $path

Close-Window
As this report is intended to be used for one specific template we can hardcode that. We did the same for the base path for now - it's the root path for all content in this case.

Location
In our Sitecore PowerShell module we create (or go to) a folder Reports: "/sitecore/system/Modules/PowerShell/Script Library/xxx/Reports". Here we create a PowerShell Script Library that will be the folder in the reporting tools section. In that folder we place our PowerShell Script - "Redirects". That will result as shown here:




This is nice - we have our report together with the other reports. But we mentioned before that we can make it context aware. Of course, we could ask for a start item in a dialog box. But we can also use a very similar report in the context menu. 

The context menu

To add the report to the context menu and use the context item as the start path, we simply add a new script to our library.
$templatePath = "master:\templates\xxx\Shared\Redirect"
$baseTemplate = Get-Item -Path $templatePath
$templateId = $baseTemplate.ID
$contextItem = Get-Item .
$path = $contextItem.Paths.FullPath

Import-Function Show-RedirectReport
Show-RedirectReport -templateId $templateId -path $path

Close-Window

Not many differences compared to what we already had - just the path is now coming from the context item ".".

Location














We are placing the new script in the folder "Content Editor/Context Menu" in our SPE module. But note that in this case we don't only take care of the location as this location will add the script everywhere but we want to limit it a little bit. We can use the Show Rule to define where the script will be shown in the context menu. In our case, we limit it to items of 2 specific templates but you can use any rule here.

This will look like this:



The final result

I want to show a screenshot of the final result as well - especially for those people who are not so familiar with Sitecore PowerShell yet. For others it will look very familiar - but that doesn't make it less nice :) 


In conclusion I would remind you once more that if you need to bring reports to your editors or admins, think about Sitecore PowerShell. It is a really cool tool that will bring you the result you need. And on top of that you can find a lot of resources already from Sitecore community people that have shared scripts and/or snippets to give you ideas and help you write your own scripts.  And maybe this one extra post helps someone as well.  





One small final note: use this script in the desktop mode...

Friday, June 9, 2023

Sitecore Powershell Extensions - Elevated session state issues

 SPE - Elevated session state issues


Some time ago I faced a very weird issue on a local setup of Sitecore 10.2 which includes the wonderful Sitecore Powershell Extensions (SPE). I wanted to run a script and SPE requested elevated session state. As you probably know is you have been using SPE already this happens (quite often) and requires you to provide your password again.



The problem was that in my case my password didn't work. Although I was sure it was correct. I could use the same password to login in Sitecore. I had this issue only on my local environment, not on any other test or production environment.

So I started my investigation that actually surfaced a few things so it might be worth putting this in a blog post.

SPE identityserver config

First of all, make sure the necessary config file is present and not disabled: https://sitecore.stackexchange.com/a/28255/237. In "\App_Config\Include\Spe" rename Spe.IdentityServer.config.disabled to Spe.IdentityServer.config. And restart.


This didn't fix my issue though and then I bumped into some configuration that could disable the request for elevated session state. Normally I would never consider this, but the problem was only local so it might be worth doing - but my curious mind wanted to fix the issue instead of putting it aside. 

Disable elevation

If you do want to tamper with the elevation requirements, check the "App_Config\Include\Spe\Spe.config" file and find the following entries: 

<token name="Default" expiration="00:00:00" elevationAction="..."/>
<token name="Console" expiration="00:05:00" elevationAction="..."/>
<token name="ISE" expiration="00:05:00" elevationAction="..."/>
<token name="ItemSave" expiration="00:05:00" elevationAction="..."/>

To disable the requirement, put "Allow" in the elevationAction.

But as mentioned we continue to solve the issue properly. I can't remember where I actually got the idea to verify the SHA settings... but that was next on the list.

SHA - the hash algorithm

I hope you do know you can (and should/must) change the hashing algorithm on your site. This is explained in https://doc.sitecore.com/xp/en/developers/latest/platform-administration-and-architecture/change-the-hash-algorithm-for-password-encryption.html.  

To do so, you need to update two files. The web.config file in the website needs an updated hashAlgorithmType setting (eg to SHA512). And if you use Sitecore Identity, you must also change the algorithm on the Sitecore Identity server

And apparently that was my issue. On the local environment I had no automated publish towards the Identity Server and so the hash algorithm was not changed there. I still think it's pretty weird that I could login to Sitecore with those settings but well.. I did. It's only when using SPE that this turned into a problem. Settings the hash to the same value as in the web.config fixed it all.

Conclusion

If you run into issues with elevated session state login:
  • check the SPE config folder if you have all necessary configs enabled
  • check whether the hash algorithm on the site and on the Identity Server are the same
  • check if your user is not locked (after a few failed attempts that might happen ☺) 
And if needed, you can also disable the elevation although I would not recommend doing so.




Monday, June 28, 2021

Transfer Sitecore security settings with Powershell

Sitecore security settings

You can set security on each item in Sitecore for any number of roles (or users, but you shouldn't do that). A while ago I had to set the security for a particular role in a way that I actually changed a few dozen items. This was on a pre-production environment and related to a new site that was prepared in that environment. That new site is moved to the production environment with a package, and that will include the security settings on all the items in the site. But what with all the ones that are not in the site and that are also touched by adding a new security rule for this role? I couldn't move all those items and I don't want to do it manually...  so I came up with a script in Powershell. I'm posting it here for my future reference, but maybe someone else can use it as well...

Sitecore Powershell Extensions (SPE) to the rescue

I created a script that created a Powershell script. Executing the created script will set all the security rules that were found for a particular role in a give path.

function GetAssignedItems($path)
{
    Write-Host "Start: " $path
     
    $items = Get-ChildItem -Path $path -Recurse
    $rootItem = Get-Item -Path $path
    $items = $items + $rootItem
 
    foreach ($item in $items)
    {
        $acl = Get-ItemAcl -Identity "MYROLE" -Item $item
        if ($acl)
        {
            foreach ($rule in $acl)
            {
                "Add-ItemAcl -PropagationType " + $rule.PropagationType + " -SecurityPermission " + $rule.SecurityPermission + " -AccessRight '" + $rule.AccessRight +"' -Identity 'MYROLE' -Path '"+ $item.Paths.FullPath + "'"
            }
        }
    }
    Write-Host "Done."
}
 
GetAssignedItems "/sitecore/media library"

Just replace "myrole" with the name of your role and that's it. The call to the function takes the root you want to check.

This will generate output like this:
Add-ItemAcl -PropagationType Entity -SecurityPermission AllowAccess -AccessRight 'item:read' -Identity 'MYROLE' -Path '/sitecore/media library/Project'
Add-ItemAcl -PropagationType Entity -SecurityPermission DenyInheritance -AccessRight '*' -Identity 'MYROLE' -Path '/sitecore/media library/Project'

You can do this for any database - just note that the database is not added to the result output. 

This actually exports the rights - meaning it will generate a script that is able to set them again on any other environment. 

To import the rights into an environment, run the resulting script in a Powershell console in your Sitecore admin. This will set the security as required. Just make sure you run the script in the correct database ;)


Tuesday, October 13, 2020

Cloning a SXA site (in another folder)

Cloning a Sitecore SXA site

Part 7 already in my series of blog posts related to a multisite project where we wanted to make it as easy as possible to create a new site - with the premise that all the sites can share almost all settings. As you might have read in the previous posts we are really doing this - and keeping it all flexible. It's not because we share (all) things, we cannot configure or overrule some parts if needed.

Anyway - we do have a setup with a shared site and a base "master" site within a tenant (as recommended by SXA). As I did not want these sites to be amongst all the other created sites, I placed those two in a Sites Folder within the tenant. We have (at least) one other Sites Folder where we want to place the actual sites - and we might even want to split those sites over different folders.

We use SXA site cloning to create a new site. This is a very easy and rapid process. The "Clone Site" Powershell (SPE) script that is used can be found by a right click on a site item in the Scripts section. It will ask the name of the new destination site and copy everything from the master site. Note that:
  • it will also copy media items within the sites media folder - really useful if you have a standard folder setup there
  • it will not copy any forms - a new empty folder is created though within Forms
  • it will change all links towards the newly created items

The script works fine and it is a very fast way to create a site. But.. it will create a site next to the master site. Of course, we can move all items after the creation - but it would be faster to just ask a preferred location and created the site there. So that's what I did...

The solution provided here is only tested with one level of site folders - it probably could work with a tree of those folders with a few more adjustments though. 

The Powershell scripts to clone a site

Show-CloneSiteDialog

First script to take care of is the dialog script. We want the user that clones a site to be able to choose the site folder where the destination site should be created. I created a copy of the ootb script "/sitecore/system/Modules/PowerShell/Script Library/SXA/SXA - Multisite/Functions/Site cloning/Show-CloneSiteDialog" and adjusted that one (you could also opt to change the original one). The changes to the original script are not that big so I will not put the entire script here to keep focus.  

In the beginning of the script process the dialogParameters are defined. We add one more parameter there (siteLocation):
$dialogParameters.Add(@{ Name = "siteLocation"; 
  Title = [Sitecore.Globalization.Translate]::Text([Sitecore.XA.Foundation.Multisite.Texts]::Path); 
  Editor = "droplist"; Source="query:$($SourceSite.Parent.Paths.Path)/ancestor-or-self::*[@@templatename='Tenant']/*[@@templatename='Site Folder']"; 
  Tab = [Sitecore.Globalization.Translate]::Text([Sitecore.XA.Foundation.Multisite.Texts]::CloneSiteDialogGeneralTab); Mandatory=$true })  | Out-Null  
I am using a droplist here with a datasource query that finds my tenant and takes the children of type Site Folder. (note we are only supporting one level here - this customization would need to be changed to support a tree).
To keep it simple I used an existing text as label. The selection is marked as mandatory to make sure a choice is made.

The next (and final) change to this script is all the way at the bottom where the results are captured:
@{
    siteName              = $siteName
    siteDefinitionmapping = $mapping
    parent                = $siteLocation
}
We add a "parent" here that will contain the chosen location of the new site.

Clone-Site

Next script to adjust is the main one: "/sitecore/system/Modules/PowerShell/Script Library/SXA/SXA - Multisite/Content Editor/Context Menu/Clone Site". This is the script that gets called from the context menu and we need to adjust it so we can use the parent from our dialog:
$ctx = Get-Item .
    
Import-Function Copy-Site
Import-Function Show-CloneSiteInParentDialog
    
$dialogResult = Show-CloneSiteInParentDialog $ctx
$cloneSiteName = $dialogResult.siteName
$mapping = $dialogResult.siteDefinitionmapping

#custom    
 $parent = $dialogResult.parent
 if (!$parent) {
     $parent = $ctx.Parent
  }
#endcustom

$destinationSite = Copy-Site $ctx $parent $cloneSiteName $mapping
$destinationSiteID = $destinationSite.ID.ToString()
$host.PrivateData.CloseMessages.Add("item:load(id=$destinationSiteID)")

Run-SiteManager
I am just showing the main part here (within the "try") to keep it clear. So we changed the call to Show-CloneSite to the one we created, we fetch the parent (with a fallback scenario although that is probably not needed) and pass it to the Copy-Site function.

And that's it. Well, that's what we thought so we tested this. The site was indeed created in the correct folder. So was the forms folder. But the media folder was still next to our master. 

Copy-Site

Let's take a look at the "/sitecore/system/Modules/PowerShell/Script Library/SXA/SXA - Multisite/Functions/Site cloning/Copy-Site" script. The site creation was fine with our new parent. We noticed that creating the Forms folder was done as it would be for a new site and that script uses the paths to determine where to create a new (empty) folder - this works fine in our version as well.

So we need to take a look at the copy process for the media folder. Somewhere in the middle of the Copy-Site script you'll find the media part and let's customize that a bit:
if ($Site.SiteMediaLibrary) {
  $SiteMediaLibrary = $Site.Database.GetItem($Site.SiteMediaLibrary) | Wrap-Item

#custom
  if ($SiteMediaLibrary.Parent.Name -ne $destinationSite.parent.Name) {
    $SiteMediaRoot = $ctx.Database.GetItem($SiteMediaLibrary.Parent.Paths.Path.Replace($($SiteMediaLibrary.Parent.Name),$($destinationSite.parent.Name)))
    if (!$SiteMediaRoot) {
      $SiteMediaRoot = $ctx.Database.GetItem($ctx.SiteMediaLibrary).Parent
    }
  }
  else {
    $SiteMediaRoot = $SiteMediaLibrary.Parent
  }
#end custom

  $NewSiteMediaLibrary = Copy-RootAndFixReference $SiteMediaLibrary $SiteMediaRoot $destinationSite.Name

  Set-NewLinkReference $Site $destinationSite $SiteMediaLibrary.Paths.Path $NewSiteMediaLibrary.Paths.Path
  $destinationSite.SiteMediaLibrary = $NewSiteMediaLibrary.ID
}
Instead of copying the media tree to the parent of the master media tree, we use string replacement and the paths to determine the location we want. Note that we do need a folder structure corresponding to the Site Folders structure, otherwise this part will fail. We could expand the script to create the necessary folders if needed.


Conclusion

With that last change we managed it. Our new site cloning script will ask us where to create the destination site - of course within the same tenant and in our one-level Site Folder structure. Combined with all the stuff from my previous posts - and the ootb features - creating a site is becoming really fast.