Experience Sitecore ! | More than 200 articles about the best DXP by Martin Miles

Experience Sitecore !

More than 200 articles about the best DXP by Martin Miles

SIF certificate error: Cannot process argument transformation on parameter 'Signer'. Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Security.Cryptography.X509Certificates.X509Certificate2"

Just in case you are facing the certificate error while installing Sitecore 9.0 update 2 with SIF 1.2.1 as step CreateSignedCert : NewSignedCertificate, as below:

Install-SitecoreConfiguration : Cannot process argument transformation on parameter 'Signer'. Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Security.Cryptography.X509Certificates.X509Certificate2".


The issue is coming from having the certificate already in a system, so you might manually delete that those from Microsoft Management Console (mmc.exe from Windows start menu).

In my case, I had several certificates named DO_NOT_TRUST_SitecoreRootCert installed under Certificates - Current User - Trusted Root Certification Auhorities- Certificates, deleting them helped progress.


Hope this helps!

Extracting package installation functionality out of Sitecore Commerce 9 SIF

In one of my previous posts, I've explained how Install-SitecoreConfiguration works in SIF and how to use it for package installation.

As time passed, more and more functionality got natively embedded into SIF, that included some useful things such as config transformations, publishing, rebuilding indexes, and of course, packages/modules installation. I decided to inspect what actually went into the platform and extracted this functionality out of Commerce product for those who want to benefit from package installations with SIF bit does not want to bother with XC9.

So, actually, there is not a big change compared with my previous post. On the lower level, there is still a temporal folder being exposed and *.aspx page placed for actual job to be done. The good thing is that plenty of that functionality now supported out of the box

For legal reasons I cannot include downloadable ready to use package, since the code and scripts belong to Sitecore who only can share it. Instead I will tell you how you can make it on your own copy of Sitecore Commerce 9.0 update 2.

You'll need three parts - Configuration, Modules and SiteUtilityPages, all carefully from within SIF.Sitecore.Commerce.1.2.14 folder:

  • Configurations (json files) taken out of Configuration\Commerce\Master_SingleServer.json and Configuration\SitecoreUtilities\InstallModule.json
  • Then you'll need three modules from Modules folder: InstallSitecoreConfiguration, ManageCommerceService, SitecoreUtilityTasks
  • Also you need SiteUtilityPages folder with InstallModules.aspx file within

If you do not keep a structure as from Commerce Installer (as didn't I) - please make adjust paths of dependencies correspondingly.

Last but not the least, you need to modify main Deploy-Sitecore-Commerce.ps1 PowerShell script, removing everything apart from package installation out of it. I renamed it into Install.ps1 and here is what I end up with:

param(
		[string]$SiteName = "habitat91.dev.local",	           # your sitecore instance name
		[string]$ModulePath = "p:\PowerShell Extensions-5.0.zip"   # path to the module to be installed
)

$global:DEPLOYMENT_DIRECTORY=Split-Path $MyInvocation.MyCommand.Path
$modulesPath=( Join-Path -Path $DEPLOYMENT_DIRECTORY -ChildPath "Modules" )
if ($env:PSModulePath -notlike "*$modulesPath*")
{
    $p = $env:PSModulePath + ";" + $modulesPath
    [Environment]::SetEnvironmentVariable("PSModulePath",$p)
}

$params = @{
		Path = Resolve-Path '.\Configuration\Master.json'	
		SiteName = $SiteName
		InstallDir = "$($Env:SYSTEMDRIVE)\inetpub\wwwroot\$SiteName"		
		SiteUtilitiesSrc = ( Join-Path -Path $DEPLOYMENT_DIRECTORY -ChildPath "SiteUtilityPages" )	

		ModuleFullPath = Resolve-Path -Path  $ModulePath
    }

Install-SitecoreConfiguration @params

and below there is my tree structure:


There are two parameters to be configured for Install.ps1 script to run:


In case you are working outside of inetpub\wwwroot folder, please also adjust the path for InstallDir variable.

Hope this helps your DevOps!


Passed! Sitecore Commerce 9 certification training and exam

Today I am proud to be one of the first folks globally who managed to pass Sitecore Commerce 9 exam after the eLearning course.

There are 50 questions in total anв you need to get 80% of correct answers (I managed to get 88%) in order to pass. All these questions are split into these categories (but not only):

  • Sales
  • BusinessTools
  • Sitecore Commerce
  • Features
  • Catalog
  • Pricing
  • App-Price-Cards
  • Inventory
  • Modify-Sellable
  • Customers
  • Profiles
  • Conv-Cart
  • Mang-Order
  • App-Coup
  • Conf-Coup
  • Create-Coup
  • Commerce EngRole
  • CommerceEngine
  • CommerceEngPlugin
  • Security
  • SDK
  • Nuget
  • SXA
  • Search
  • Navigation
  • Cart
  • Entitlement
  • Fulfilment
  • Payment
  • Orders
  • Plugins
  • RulesI

For preparation, I was guided by the materials from this StackExchange answer: https://sitecore.stackexchange.com/questions/12771/how-to-start-development-with-sitecore-commerce-9-sitecore-commerce-9-learning.


Happy learning!

Troubleshooting Sitecore Commerce 9 installation

While installing Sitecore Commerce I came across several blockers I had to sort out in:

  1. SIF
  2. Business Tools
  3. Storefront

1. You are getting 500.19 error during SIF. Other people who came across advice that you are to install ASP.NET Core 2. That's correct, as Commerce micro-services utilize .NET core, but not everything. There was a strong assumption of something missing from IIS or being mis-configured, so in my case it was this component unchecked:


Also, keep in mind you must have URL-Rewrite module installed (the easiest way is: choco install urlrewrite).

Another non-critical error I am getting from time to time is "The service cannot accept control messages at this time". IIS does respond at the moment - you may run iisreset and try warm it up by hitting URL manually in browser. Then repeat SIF deploy.

If SIF is complaining about duplicate certificates, you may need to delete those from Microsoft Management Console (MMC). In order to do that, click Start button, then type in mmc and hit enter. Once there, File - Add/Remove Snap-ins and select Certificates, as on screenshot below:


One more error from SIF: "Error: Unable to add user SITECOREDEV\WindowsUserName. Details: Database 'sc902com_Core' does not exist. Make sure that the name is entered correctly." In that case, ensure you're referencing correct database name for SitecoreCoreDbName variable (line 29 of default deploy script)


2. Running Business Tools may bring an error. In some previous versions Business Tools desktop icon was referencing invalid port, so make sure it is 4200 (by default). You can do that by changing Link field for Business Tools launchpad icon, as you usually do in corу database (item path: /sitecore/client/Applications/Launchpad/PageSettings/Buttons/Commerce/BusinessTools)

While running Business Tools, if you may see to following error:


The error means your browser needs a security exception for self-signed certificate, no worries - that's only for localhost and needs to be done once. Just open a new browser tab and follow to localhost:5000, confirming security exception, as prompted.

3. Once you have successfully installed Sitecore Commerce, you try to access storefront. So you hit URL in browser (sxa.storefront.com) and ... see default Sitecore home page. Why?

The first assumption coming into one's mind is that since storefront is running on top of SXA - you should configure the hostname at HostName field of /sitecore/content/Sitecore/Storefront/Settings/Site Grouping/Storefront item. But wait! sxa.storefront.com is the default value and already presents there. Does it work now? Nope ...

Firstly, try open storefront in Experience Editor (from master database). If you see the right storefront - just re-publish site, and update indexes - it will work from web database then. But if not - the answer is different: it appeared that my current partner's license does not include SXA. Not obvious, but when you're out of license - storefront default to Home page rather than notify you re. that error.

Remember, that you may apply for a developer trial license valid for two month that will include all the features of Sitecore, including SXA and JSS.

An exciting updgrade of Sitecore.Link project to JSS with Vue and GraphQL on Azure PaaS

It is 2.5 years already since I launched Sitecore.Link project and it made a great input to the community in terms of accumulating and finding technical information. It served well as a starting point for those willing quickly to obtain all available knowledge about some specific aspects of Sitecore as all the links are split into 150+ categories from 15+ top-level areas (ie. SXA, Helix, Security, Media etc.)


From the back-end prospective Sitecore.Link is one of the greatest things I've ever done, with all automation, crawlers and bit of artificial intelligence and machine learning, finding the new materials and taking off all the monotonous work from me, except the one and only thing I should do - taking the decision (on adding that particular link to the collection after reading it).

Speaking about front-end and UX I can now say that things were also great 2 years ago when the project gained 1-2K of links, not 16K+ as for today. It offered real-time predictive input, powerful filtering and few more. But many of these features came out of PoC (as well as the project itself entirely) and have been added more or less spontaneously in the limited timeframes (yes, I have a full-time job plus 3 hours of commute each day), that's why front-end and UX did not expect such a growth of data. I bravely optimized everything as much as could until hitting the performance ceiling of rendering. So it was just a matter of time when to perform reUX and rewrite, rather than whether to.

Finally, the time has come! I am doing the entire rework of Sitecore.Link project.


When attending SUGCON 2018 I was very enthusiastic to find out how perfectly GraphQL stands for querying the data. Since front-end of Sitecore.Link is, in fact, a single page application - JSS would suit the best, while the data itself becomes bucketable implementing new taxonomy. So, as for the moment, I came to the following changes:

Most of back-end and crawlers will remain as they are. From the new features, apart from changing data storage, I plan to add a page where blog authors will be easily able to test and adjust their resources for Sitecore.Link parser them further automatically, and even see how crawlers process their blogs in real-time.

Front-end will entirely rework UI to fit visitors' day-to-day needs to be as quick as possible. UI will be re-written to Vue.js backed by Sitecore Javascript Services (JSS)

At the moment, only blog names are searchable, but after changes will take place all articles' content also becomes tokenized for relevancy in order to provide more precise results and order. Such a nice Sitecore Google will be born, lol)

Once completed, the project will be delivered to Azure PaaS for hosting.


Outcomes

One of the most important things - when done, the source code becomes opensource (MIT) on GitHub - this will bring to the community a real-world example of JSS-running website (but not only - plenty of Sitecore features will be there as implementation example for others). Since the project is community-based and is fed by the community-driven data, it will serve the community in full, including the source code to be hared and ideally pull-requested with new features or fixes.

Another community support back to the project will become a browser extension for submitting currently open URL (of a blog post) into the links database by specifying relevant categories as well as other information, after a quick moderation of course.

Numerous blog posts from myself, describing the changes implemented and sharing the experience of JSS and other features done will accompany this transformation.

In order to make all mentioned above happen, I finish my current contract and do not pick anything next in the following 2 months, to be able fully to concentrate on this amazing challenges. I am also looking for any support from Sitecore Technical Marketing team on technical issues

Amazing Sitecore PowerShell Extension Remoting - quick start to make it work


Introduction. Most of you are already familiar with Sitecore PowerShell Extensions (SPE). This is a genius module written by Adam Najmanowicz and Michael West, that not just brings the power of console into Sitecore, allowing to maintain and automate almost everything, but also integrates that into Sitecore UI so that one may have pretty nice reports on virtually anything in their system. But today I am going to tell you about one underestimated part of SPE - Remoting,

What is Remoting? Old school developers remember this term from the first version of .NET where it used to name the technology of building distributed applications with purely means of the framework in pre-RESTful years. Not a nice term for not the best technology! But in SPE Remoting got more adequate meaning - executing scripts remotely. The idea is to allow running SPE script from the standarв Windows PowerShell console - in that case, a script is serialized and send for the remote execution to Sitecore server. That means it will be executed in the standard Sitecore context, same as normal SPE script you run in Sitecore PowerShell Console within your Sitecore instance. Then the results will be returned back to original you original system PowerShell window as if you ran them locally.

Why do I need that? Well, you may not realise you need that straight away, but being able to expose the power of SPE outside gives you an ultimate power of maintaining your instances internally. What firstly comes into my head is Continuous Integration - now you have the tool for managing content and precise fine-tuning everything within your instances. Not limited to - you also have access to servers filesystem, can download and upload files, same for media library, manipulate users and permissions. For example, you may automate regular downloading logs from all of your servers or whatever you may decide.

Great, I'm interested. How do I make it work? This article is all about setting up SPE Remoting for your instance and getting first results. So, here we go.


Firstly download Sitecore Powershell Extensions module itself (if you don't yet have) and install as you normally install the packages. The module is relatively large and it may take a while until it gets done. Then SPE becomes present in your Sitecore start menu:


SPE module comes already with remoting built-in however it requires few more steps to enable and configure it.

There is a new security role that becomes available upon SPE installation: sitecore\PowerShell Extensions Remoting. You must make your credentials user for external remoting connection (applies to users and/or other roles) to be a member of this role, even (and especially) if that is sitecore\admin. If you don't do that you will see following error:



Secondly, download SPE Remoting module for the same version you may find it there along with regular SPE on Marketplace:


Note - that is not a Sitecore module and it's not for Package Installer. The term "module" is too meaningful here and in current context it deals with PowerShell module. Instead, extract archive content into you PowerShell modules root folder - for example <YOUR_USER_FOLDER>\Documents\WindowsPowerShell\Modules\SPE folder


You may need to import that module first in order to use it from within Windows PowerShell console. Make sure you set the execution policy to:

Set-ExecutionPolicy RemoteSigned

and the import command:

Import-Module -Name SPE

Before writing a basic actual script, we also need to adjust configuration - module authors treat security seriously. The configuration file is located at: <INSTANCE_ROOT>\App_Config\Include\Cognifide.PowerShell.config and the article on how-to can be found here.

Since you are likely experimenting on your local machine and are more relaxed regarding security - you may get things sorted easier by applying a patch file to weaken security. Make sure you use if only for local dev and never in higher environments. As a bonus - the patch won't be overwritten when reinstalling the SPE module.

Finally, we can create and execute the script. As the very bare minimum, I suggest the very simple script for you to test. Create PS.ps1 and copy the below content into it:

Set-ExecutionPolicy RemoteSigned
Import-Module -Name SPE

$session = New-ScriptSession -Username admin -Password b -ConnectionUri https://platform.dev.local

Invoke-RemoteScript -ScriptBlock {
    Get-Item -Path "master:\content\Home"
} -Session $session 

Now execute that:


And voila! Although visually it is not as impressive, what actually happens on this screenshot is getting a simple query sent to Sitecore instance, executed remotely with the results returned back to Windows PowerShell, all transparently for the caller.

The reason for me to write the article was the number of efforts I spent on getting things understood, downloaded and setup, configured and executed. Some pieces of documentation are not available, while others not a descriptive. Once again, thanks to Michael West for his support, and hope this article will encourage you at least to try that!

Sitecore PowerShell Extension snippets collection

A collection of Sitecore PowerShell snippets from various sources to have them in one place easy to copy, adjust and execute. Credits to Adam Najmanowicz and Michael West - you guys did a great job!

  1. Write to CSV
  2. Using link fields
  3. Update item presentation final layout
  4. Traverse tree with query
  5. Read from CSV
  6. Output all object properties
  7. Move item
  8. List cms users
  9. Get and edit an item
  10. Generate google sitemap XML
  11. Exclude template
  12. Create users from CSV
  13. Create an item
  14. Convert datetime field value to DateTime object
  15. Change item template
  16. Add base template
  17. Parse a Rich Text to replace elements
  18. Generate a hash of a media file
  19. Exporting the username and custom properties
  20. Show List View with all users' properties
  21. Set workflow for all items
  22. Unlock items of a template recursively
  23. Zip and download sitecore media library folder
  24. Zip and download logs
  25. Updating renderings properties to enable VaryByData
  26. New media item
  27. Update media item
  28. List Roles for User
  29. Remove unused media items
  30. Get items modified in last 7 days

1. Write to csv
More description - normal font
$everythingUnderHome = Get-Item master: -Query "/sitecore/content/Home//*"
$outputFilePath = "C:\temp\my-csv-file.csv"
$results = @();

$everythingUnderHome | ForEach-Object {

  $properties = @{
        Name = $_.Name
        Template = $_.TemplateName
        Path = $_.ItemPath
  }

  $results += New-Object psobject -Property $properties
}

$Results | Select-Object Name,Template,Path | Export-Csv -notypeinformation -Path $outputFilePath

2. Using link fields
More description - normal font
#Get an alias
$alias = = Get-Item -Path "master:/sitecore/system/Aliases/test"

#Get the link field
[Sitecore.Data.Fields.LinkField]$field = $_.Fields["Linked item"]

# the path to the target item
$field.InternalPath

# The guid if internal. Otherwise this is an Empty GUID
$field.TargetID

# 'internal' or 'external'
$field.LinkType

# Other proporties include MediaPath, QueryString and Target

3. Update item presentation final layout
$items = Get-Item master:/content/home//* | Where-Object { $_.Fields["__Final Renderings"] -like "*col-huge*" }

$items | ForEach-Object {    
    Write-Host $_.Fields["__Final Renderings"]
    
    $_.Editing.BeginEdit()
    
    $_["__Final Renderings"] = $_.Fields["__Final Renderings"].toString().replace("col-huge", "col-wide-1");
    
    $_.Editing.EndEdit();
    
    Write-Host $_.Fields["__Final Renderings"]

4. Traverse tree with query
$everythingUnderHome = Get-Item master: -Query "/sitecore/content/Home//*"

$everythingUnderHome | ForEach-Object {

  Write-Host "Item name: " + $_.Name
}

5. Read from CSV
$csv = Import-Csv "C:\temp\my-csv-file.csv"

foreach($row in $csv)
{
    if ($row -eq $csv[0])
    {
        #Skip the first row as it contains the column headings in your CSV
        continue;
    }
    
    #Output value for Column1 and Column2
    Write-Host $row."Column1";
    Write-Host $row."Column2";
}

6. Output all object properties
[Sitecore.Data.Fields.LinkField]$field = $_.Fields["Linked item"]
Write-Host ($field | Format-Table | Out-String)

7. Move item
$item = Get-Item master:/content/home/old/item

#Will move and place this item under the target new
Move-Item -Path $item.ItemPath "master:\sitecore\content\Home\new";

8. List cms users
$allSitecoreUsers = Get-User -Filter "sitecore\*" 

$allSitecoreUsers | ForEach-Object {
    Write-Host $_.Name
}

9. Get and edit an item
$item = Get-Item master:/content/home

$item.Editing.BeginEdit();

$item["Title"] = "New title for the home item!";

$item.Editing.EndEdit();

10. Generate google sitemap XML
$xmlWriter = New-Object System.XMl.XmlTextWriter('c:\temp\sitemap.xml',$Null)
$xmlWriter.Formatting = 'Indented'
$xmlWriter.Indentation = 1
$XmlWriter.IndentChar = "`t"

$xmlWriter.WriteStartDocument()
$xmlWriter.WriteStartElement('urlset')
$XmlWriter.WriteAttributeString('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')

$everythingUnderHome = Get-Item master: -Query "/sitecore/content/Home//*"
$baseUrl = "https://example.com"

$everythingUnderHome | ForEach-Object {

    $url = $baseUrl + [Sitecore.Links.LinkManager]::GetItemUrl($_)

    $xmlWriter.WriteStartElement('url')
    $xmlWriter.WriteElementString('loc',$url)
    $xmlWriter.WriteEndElement()
}

$xmlWriter.WriteEndElement()
$xmlWriter.WriteEndDocument()
$xmlWriter.Flush()
$xmlWriter.Close()

11. Exclude template
$item = Get-Item -Path master: -Query "/sitecore/content//*[@@templatename !='Sample Item']"

#Or

$item = Get-Item -Path master: -Query "/sitecore/content//*" | Where-Object { $_.TemplateName -ne "Sample Item" -and $_.TemplateName -ne "Local Datasource

12. Create users from CSV
$csv = Import-Csv "C:\temp\my-csv-file.csv"

foreach($row in $csv)
{
    if ($row -eq $csv[0])
    {
        #Skip the first row as it contains the column headings in your CSV
        continue;
    }
    
    #New-User docs: https://doc.sitecorepowershell.com/appendix/commands/New-User.html
    #Include domain in username to specify the target domain (e.g extranet\adam)
    New-User -Identity $row."Username" -Enabled -Password $row."Password" -Email $row."Email" -FullName $row."FullName"
}

13. Create an item
$item = New-Item "master:/content/home/my new sample item" -type "Sample/Sample Item"

14. Convert datetime field value to DateTime object
$dateTime = ([sitecore.dateutil]::IsoDateToDateTime($item["My Date Field"])

Write-Host $dateTime.ToString("dd MMMM yyyy")

#Output 10 September 2016

15. Change item template
$item = Get-Item master:/content/home

$newTemplate = [Sitecore.Configuration.Factory]::GetDatabase("master").Templates["Sample/Sample Item"];

$item.ChangeTemplate($newTemplate)

16. Add base template
$targetTemplate    = Get-item 'master:/sitecore/templates/User Defined/Common/Data';
$templateFilter    = Get-Item "master:/sitecore/templates/System/Templates/Template";
$templateToBeAddAsBaseTemplate     = Get-Item 'master:/sitecore/templates/User Defined/Common/Data/Carousel'
 
 
Get-ChildItem $targetTemplate.FullPath -recurse | Where-Object { $_.TemplateName -eq $templateFilter.Name -and $_.Name -eq "promo"} | ForEach-Object {
    if(-not ($_."__Base template" -match "$($templateToBeAddAsBaseTemplate.Id)")){
           #If not exist then add
         $_."__Base template" = "$( $_."__Base template")|$($templateToBeAddAsBaseTemplate.Id)"
    }
}

17. Parse a Rich Text to replace elements
The easiest solution is probably to find and replace characters rather than parse all of the html:
# Home Item
$rootId = "{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}"

# Get only the immediate children
$items = @((Get-ChildItem -Path "master:" -ID $rootId))
foreach($item in $items) {
    $html = $item.Text
    if([String]::IsNullOrEmpty($html)) { continue }
    $newText = $html.Replace("
    ", "
      ").Replace("
", "") $item.Text = $newText }

18. Generate a hash of a media file
#Generate MD5 Hash of item
function To-Byte-Array
        {
            param (
            [CmdletBinding()]
            [Parameter(Mandatory = $true)]
            [AllowEmptyString()]
            [AllowEmptyCollection()]
            [AllowNull()]
            [Object]
            $InputStream)
             
            $InputStream.Position = 0
            $buffer = New-Object byte[] $InputStream.Length
            $i = 0
            For ($totalBytesCopied=0; $totalBytesCopied -lt $InputStream.Length; $i++) {
                $totalBytesCopied += $InputStream.Read($buffer, $totalBytesCopied, $InputStream.Length - $totalBytesCopied)
            }
             
            $buffer
        }
 
$item = Get-Item master: -ID "{20AA9D26-F1D6-43DB-B8A8-21EC04A5A4CB}" -Language de-DE
    $mediaItem = New-Object Sitecore.Data.Items.MediaItem($item)
    $media = [Sitecore.Resources.Media.MediaManager]::GetMedia($mediaItem)
    $stream = $media.GetStream().Stream
    try {
        $bytes = To-Byte-Array $stream
    }
    finally
    {
        if ($null -ne $stream -and $stream -is [System.IDisposable])
        {
            $stream.Dispose()
        }
    }
     
    $md5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
    $hash = [System.BitConverter]::ToString($md5.ComputeHash($bytes))
     
    Write-Host $hash

19. Exporting the username and custom properties
$property = @(
"Name",
@{Name='Email';Expression={ $PSItem.Profile.GetCustomProperty('Email') }},
@{Name='FirstName';Expression={ $PSItem.Profile.GetCustomProperty('FirstName') }},
@{Name='LastName';Expression={ $PSItem.Profile.GetCustomProperty('LastName') }},
@{Name='Title';Expression={ $PSItem.Profile.GetCustomProperty('Title') }},
@{Name='Company';Expression={ $PSItem.Profile.GetCustomProperty('Company') }},
@{Name='Country';Expression={ $PSItem.Profile.GetCustomProperty('Country') }},
@{Name='ZipCode';Expression={ $PSItem.Profile.GetCustomProperty('ZipCode') }},
@{Name='Department';Expression={ $PSItem.Profile.GetCustomProperty('Department') }},
@{Name='Street';Expression={ $PSItem.Profile.GetCustomProperty('Street') }},
@{Name='City';Expression={ $PSItem.Profile.GetCustomProperty('City') }},
@{Name='Phone';Expression={ $PSItem.Profile.GetCustomProperty('Phone') }},
@{Name='Username';Expression={ $PSItem.Profile.GetCustomProperty('Username') }},
)

# Gets not disabled extranet users, next select all customer properties and save all properties to CSV file
Get-User -Filter 'extranet\*'  `
    | Where-Object { $_.Profile.State -ne 'Disabled' } `
        |  Select-Object -Property $property `
            | Export-CSV -Path "$apppath\extranet-enabled-uc.csv" -notype -encoding "unicode"
        
Download-File  "$apppath\extranet-enabled-uc.csv"

20. Show List View with all users' properties
[System.Web.Security.Membership]::GetAllUsers() |

Show-ListView -Property @{Label="User"; Expression={ $_.UserName} },
    @{Label="Is Online"; Expression={ $_.IsOnline} },
    @{Label="Creation Date"; Expression={ $_.CreationDate} },
    @{Label="Last Login Date"; Expression={ $_.LastLoginDate} },
    @{Label="Last Activity Date"; Expression={ $_.LastActivityDate } }

21. Set workflow for all items
## 1. Set default workflow state in template’s standard value ##
## 2. Before running script, must set correct Context Item ##
##################################################################

function SetWorkflow($item)
{
## Update only items assigned __Default workflow
if ($item.”__Default workflow” -eq “{A5BC37E7-ED96-4C1E-8590-A26E64DB55EA}”) {
$item.__Workflow = “{A5BC37E7-ED96-4C1E-8590-A26E64DB55EA}”;
$item.”__Workflow state” = “{190B1C84-F1BE-47ED-AA41-F42193D9C8FC}”;
}
}

## Update correct workflow information.
## Uncomment below two lines if you are ready to go for updating
#get-item . -Language * | foreach-object { SetWorkFlow($_) }
#get-childitem . -recurse -Language * | foreach-object { SetWorkFlow($_) }

## Show Updated Result
get-item . -Language * | Format-Table Id, Name, Language, __Workflow, “__Workflow state”, “__Default workflow”
get-childitem . -recurse -Language * | Format-Table Id, Name, Language, __Workflow, “__Workflow state”, “__Default workflow”

22. Unlock items of a template recursively
Get-ChildItem -Path master:\content\home -Recurse | 
    Where-Object { $_.TemplateId -eq "{ENTER_YOUR_TEMPLATE_GUID}"} | 
    Unlock-Item -PassThru

23. Zip and download sitecore media library folder
  
#
# The ZipFiles function is based on noam's answer
# on the following Stack Overflow's page: http://bit.ly/PsZip
#
function ZipItems( $zipArchive, $sourcedir )
{
  Set-Location $sourcedir
  [System.Reflection.Assembly]::Load("WindowsBase,Version=3.0.0.0, `
      Culture=neutral, PublicKeyToken=31bf3856ad364e35") | Out-Null
  $ZipPackage=[System.IO.Packaging.ZipPackage]::Open($zipArchive, `
      [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite)
  $items = gci -recurse $sourceDir
  [byte[]]$buff = new-object byte[] 40960
  $i = 0;
  ForEach ($item In $items) {
    $i++
    if([Sitecore.Resources.Media.MediaManager]::HasMediaContent($item)){
      $mediaItem = New-Object "Sitecore.Data.Items.MediaItem" $item;
      $mediaStream = $mediaItem.GetMediaStream();
      $fileName = Resolve-Path -Path $item.ProviderPath -Relative
      $fileName = "$fileName.$($item.Extension)" `
        -replace "\\","/" -replace "./", "/"
      # Print out the file - the list will show up once the file is downloaded
      "Added: $fileName"
      # Show progress for the operation
      Write-Progress -Activity "Zipping Files " `
        -CurrentOperation "Adding $fileName" `
        -Status "$i out of $($items.Length)" `
        -PercentComplete ($i *100 / $items.Length)
      $partUri = New-Object System.Uri($fileName, [System.UriKind]::Relative)
      $partUri = [System.IO.Packaging.PackUriHelper]::CreatePartUri($partUri);
      $part = $ZipPackage.CreatePart($partUri, `
        "application/zip",  `
        [System.IO.Packaging.CompressionOption]::Maximum)
      $stream=$part.GetStream();
      do {
        $count = $mediaStream.Read($buff, 0, $buff.Length)
        $stream.Write($buff, 0, $count)
      } while ($count -gt 0)
      $stream.Close()
      $mediaStream.Close()
    }
  }
  $ZipPackage.Close()
}
 
#the location will be set by PowerShell automatically based on which item was clicked
$location = get-location
$dateTime = Get-Date -format "yyyy-MM-d_hhmmss"
$zipName = Split-Path -leaf $location | % { $_ -replace " ", ""}
$dataFolder = [Sitecore.Configuration.Settings]::DataFolder
$zipPath = "$dataFolder\$zipName-$datetime.zip"
 
# Call the Zipping function
ZipItems $zipPath $location
 
#Send user the file, add -NoDialog if you want to skip the download dialogue 
Download-File -FullName $zipPath | Out-Null
 
# Clean up after yourself
Remove-Item $zipPath
 
# Close the results window - we don't really need to see the results
Close-Window

24. Zip and download logs
#
# The ZipFiles function is based on noam's answer
# on the following Stack Overflow's page: http://bit.ly/PsZip
#
function ZipFiles( $zipArchive, $sourcedir )
{
    [System.Reflection.Assembly]::Load("WindowsBase,Version=3.0.0.0, `
        Culture=neutral, PublicKeyToken=31bf3856ad364e35") | Out-Null
    $ZipPackage=[System.IO.Packaging.ZipPackage]::Open($zipArchive, `
        [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite)
    $in = gci $sourceDir | select -expand fullName
    [array]$files = $in -replace "C:","" -replace "\\","/"
    ForEach ($file In $files) {
        $fileName = [System.IO.Path]::GetFileName($file);
            $partName=New-Object System.Uri($file, [System.UriKind]::Relative)
            $part=$ZipPackage.CreatePart("/$fileName", "application/zip", `
                [System.IO.Packaging.CompressionOption]::Maximum)
            Try{
                $bytes=[System.IO.File]::ReadAllBytes($file)
            }Catch{
                $_.Exception.ErrorRecord.Exception
            }
            $stream=$part.GetStream()
            $stream.Write($bytes, 0, $bytes.Length)
            $stream.Close()
    }
    $ZipPackage.Close()
}
 
# Get Sitecore folders and format the zip file name
$dateTime = Get-Date -format "yyyy-MM-d_hhmmss"
$dataFolder = [Sitecore.Configuration.Settings]::DataFolder
$logsFolder = [Sitecore.Configuration.Settings]::LogFolder
$myZipFile = "$dataFolder\logs-$datetime.zip"
 
# Warn that the user log files will fail to zip
Write-Host -f Yellow "Zipping files locked by Sitecore will fail." -n
Write-Host -f Yellow "Files listed below were used."
 
# Zip the log files
ZipFiles $myZipFile $LogsFolder
 
#Download the zipped logs
Get-File -FullName $myZipFile | Out-Null
 
#Delete the zipped logs from t

25. Updating renderings properties to enable VaryByData
$items = Get-ChildItem -Recurse | Where-Object {$_.TemplateID -match “{79A0F7AB-17C8-422C-B927-82A1EC666ABC}”} | ForEach-Object {

$renderingInstance = Get-Rendering -Item $_ -Rendering $rendering
if($renderingInstance){
$renderingInstance.VaryByData = 1
Set-Rendering -Item $_ -Instance $renderingInstance
}
}

26. New media item
function New-MediaItem{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$filePath,

        [Parameter(Position=1, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$mediaPath)

$mco = New-Object Sitecore.Resources.Media.MediaCreatorOptions
$mco.Database = [Sitecore.Configuration.Factory]::GetDatabase("master");
$mco.Language = [Sitecore.Globalization.Language]::Parse("en");
$mco.Versioned = [Sitecore.Configuration.Settings+Media]::UploadAsVersionableByDefault;
$mco.Destination = "$($mediaPath)/$([System.IO.Path]::GetFileNameWithoutExtension($filePath))";

$mc = New-Object Sitecore.Resources.Media.MediaCreator
$mc.CreateFromFile($filepath, $mco);
}

# Usage example
New-MediaItem "C:\Users\Adam\Desktop\Accelerators.jpg" "$([Sitecore.Constants]::MediaLibraryPath)/Images"

27. Update media item
function Update-MediaItem{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$filePath,

        [Parameter(Position=1, Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$mediaPath)

[Sitecore.Data.Items.MediaItem]$item = gi $mediaPath
[Sitecore.Resources.Media.Media] $media = [Sitecore.Resources.Media.MediaManager]::GetMedia($item);
$extension = [System.IO.Path]::GetExtension($filePath);
$stream = New-Object -TypeName System.IO.FileStream -ArgumentList $filePath, "Open", "Read"
$media.SetStream($stream, $extension);
$stream.Close();
}

# Usage example - overwrite the image created with previous cmdlet with new image
Update-MediaItem "$SitecoreDataFolder\Tchotchkeys.jpg" "$([Sitecore.Constants]::MediaLibraryPath)/Images/Accelerators"

28. List Roles for User
function Get-UserRoles{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [string]$Identity
    )
  $user = Get-User $Identity
  return Get-Role -Filter * | Where-Object { $_.IsMember($user,$true,$true) } 
}

Write-Host "Roles for ServicesAPI" -ForegroundColor Green
Get-UserRoles "sitecore\ServicesAPI"

Write-Host "Roles for Admin" -ForegroundColor Green
Get-UserRoles "sitecore\admin"

29. Remove unused media items
#Include all paths you do not want to analyse below 
$protectedPaths = @(
    "/sitecore/media library/System/", 
    "/sitecore/media library/Experience Explorer"
    "/sitecore/media library/Images/Social"
    );
    
#Include all item templates you want to ignore in the array below
$protectedTemplates = @(
    [Sitecore.TemplateIDs]::MediaFolder
    );

$itemsToDelete = 
    Get-ChildItem -Path "master:\sitecore\media library" -Recurse |

        # filter out items of templates you do not want to touch
        Where-Object { $_.TemplateID -notin $protectedTemplates } |

        # do not allow items in the protected paths
        Where-Object { 
            $item = $_; 
            $protected = $protectedPaths | Where-Object { ($item.Paths.Path) -match $_ };
            $protected.Count -lt 1;
        } | 

        # and only items that are not used
        Where-Object { [Sitecore.Globals]::LinkDatabase.GetReferrerCount($_) -eq 0 }

#List the items
$itemsToDelete | ft ProviderPath

#If the list above looks like what you want to delete you can uncomment the following line
#$itemsToDelete | Remove-Item

30. Get items modified in last 7 days
$days = 7;
Read-Variable -Parameters @{ Name="days"; Title="Number of days"} -Title "Pointy haired report" | Out-Null

Get-ChildItem master:\content\home -Recurse | 
    ? { $_."__updated" -gt (Get-Date).AddDays(-$days) } | 
    Show-ListView -Property ID, Name, "__updated", "__updated by"

How to use Install-SitecoreConfiguration from SIF with your custom configuration on example of installing SPE and SXA along with Sitecore

This is an exercise resulting from a blog post by Rob Ahnemann and I will cover Install-SitecoreConfiguration in more details on an example of installing Sitecore modules SPE and SXA as a part of a Sitecore installation by SIF.

Install-SitecoreConfiguration is one of the most what actually SIF is. In my Sitecore installation script I have created a step called Install-Packages. Let's look at how it works:

function Install-Packages {
    Unblock-File .\build\PostInstall\Invoke-InstallPackageTask.psm1 
    Install-SitecoreConfiguration -Path .\build\PostInstall\install-sitecore-package.json -SiteName "$SolutionPrefix.$SitePostFix"
}

This installs a configuration called install-sitecore-package.json Opening it looks like below:

{
    "Parameters": {
         "SiteName": {
            "Type": "string",
            "DefaultValue": "Sitecore",
            "Description": "The name of the site to be deployed."
        },
        "InstallDirectory": {
            "Type": "string",
            "DefaultValue": "c:\\inetpub\\wwwroot",
            "Description": "Base folder to where website is deployed."
        }
    },
    "Variables": {
        // The sites full path on disk
        "Site.PhysicalPath": "[joinpath(parameter('InstallDirectory'), parameter('SiteName'))]",
		"Site.Url": "[concat('http://', parameter('SiteName'))]"

    },
    "Tasks": {
		"InstallPackages":{
			"Type": "InstallPackage",
            "Params": [
                {
                    "SiteFolder": "[variable('Site.PhysicalPath')]",
                    "SiteUrl": "[variable('Site.Url')]",
                    "PackagePath": ".\\build\\assets\\Modules\\Sitecore PowerShell Extensions-4.7.2 for Sitecore 8.zip"
                },
                {
                    "SiteFolder": "[variable('Site.PhysicalPath')]",
                    "SiteUrl": "[variable('Site.Url')]",
                    "PackagePath": ".\\build\\assets\\Modules\\Sitecore Experience Accelerator 1.6 rev. 180103 for 9.0.zip"
                }
            ]
		}
    },
	"Modules":[
		".\\build\\PostInstall\\Invoke-InstallPackageTask.psm1"
	]
}

Parameters are values that may be passed when Install-SitecoreConfiguration is called. Parameters must declare a Type and may declare a DefaultValue and Description. Parameters with no DefaultValue are required when Install-SitecoreConfiguration is called.

Variables are values calculated in a configuration. They can reference Parameters, other Variables, and config functions.

Tasks are separate units of work in a configuration. Each task is an action that will be completed when Install-SitecoreConfiguration is called. By default, tasks are applied in the order they are declared. Tasks may reference Parameters, Variables, and config functions.

Finally, the last line is actually referencing the actual task - a PowerShell module script (Invoke-InstallPackageTask.psm1) that will be run with given parameters:

Set-StrictMode -Version 2.0

Function Invoke-InstallPackageTask {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [string]$SiteFolder,
		[Parameter(Mandatory=$true)]
        [string]$SiteUrl,
		[Parameter(Mandatory=$true)]
        [string]$PackagePath
    )

    Write-TaskInfo "Installing Package $PackagePath" -Tag 'PackageInstall'

    #Generate a random 10 digit folder name. For security 
	$folderKey = -join ((97..122) | Get-Random -Count 10 | % {[char]$_})
	
	#Generate a Access Key (hi there TDS)
	$accessKey = New-Guid
	
	Write-TaskInfo "Folder Key = $folderKey" -Tag 'PackageInstall'
	Write-TaskInfo "Access Guid = $accessKey" -Tag 'PackageInstall'

	#The path to the source Agent.  Should be in the same folder as I'm running
	$sourceAgentPath = Resolve-Path "PackageInstaller.asmx"
	
	#The folder on the Server where the Sitecore PackageInstaller folder is to be created
	$packageInstallPath = [IO.Path]::Combine($SiteFolder, 'sitecore', 'PackageInstaller')
	
	#The folder where the actuall install happens
	$destPath = [IO.Path]::Combine($SiteFolder, 'sitecore', 'PackageInstaller', $folderKey)

	#Full path including the installer name
	$fullFileDestPath = Join-Path $destPath "PackageInstaller.asmx"
	
	Write-TaskInfo "Source Agent [$sourceAgentPath]" -Tag 'PackageInstall'
	Write-TaskInfo "Dest AgentPath [$destPath]" -Tag 'PackageInstall'

	#Forcibly cread the folder 
	New-Item -ItemType Directory -Force -Path $destPath

	#Read contents of the file, and embed the security token
	(Get-Content $sourceAgentPath).replace('[TOKEN]', $accessKey) | Set-Content $fullFileDestPath

	#How do we get to Sitecore? This URL!
	$webURI= "$siteURL/sitecore/PackageInstaller/$folderKey/packageinstaller.asmx?WSDL"
	 
	Write-TaskInfo "Url $webURI" -Tag 'PackageInstall'
	
	#Do the install here
	$proxy = New-WebServiceProxy -uri $webURI
	$proxy.Timeout = 1800000

	#Invoke our proxy
	$proxy.InstallZipPackage($PackagePath, $accessKey)

	#Remove the folderKey
	Remove-Item $packageInstallPath -Recurse
}
Register-SitecoreInstallExtension -Command Invoke-InstallPackageTask -As InstallPackage -Type Task

What this task does is locates ASMX file, which is an actual handler and copies it into temp random folder within your Sitecore instance allowing you to execute package installer APIs that itself does require Sitecore context. 

Obviously, before running .\install-xp0.ps1 please make sure you have both installers for modules by their paths as per configuration parameters in PackagePath (from example above, they are):
    .\build\assets\Modules\Sitecore PowerShell Extensions-4.7.2 for Sitecore 8.zip
    .\build\assets\Modules\Sitecore Experience Accelerator 1.6 rev. 180103 for 9.0.zip

So, that's how we added new SitecoreConfiguration in order to achieve automated package installation of SPE and SXA by SIF. Since now new locally installed Sitecore instance already has both SPE and SXA pre-installed and ready to use. This approach also allows installing any other modules as you may need them pre-installed.

Finally, would highly recommend watching a great video by Thomas Eldblom about using SIF configurations:

UPDATE: there is another way of doing this by new built-in SIF functionality, please read the blog post here.


One of the first Sitecore 9 solution released to production

Proud to be among those very first to be able to deliver a Sitecore 9, Azure PaaS and Helix powered website into a production! To my greatest regret, I am not allowed disclose the client's name, that's why instead better share the spec.

Technologically, the solution contains many of things discovered previously in my blog and is the following:

  • Sitecore 9.0 update 1
  • Helix principles
  • Glass mapper with T4 model interface code generation
  • VSTS for everything - CI / CD tool, code repository, productivity tracking solution
  • WebDeploy for deploying the assets to Azure PaaS
  • Unicorn for serialization
  • Three XP-Single, four XP-Scaled as well as DEV Environments
  • Relies on PowerShell for automation and management
It is a good time to steam out for a while and then keep a focus on my projects such as Sitecore.Link, Sitecore Discussion Club, Onero and of course - blogging here.