Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
Experience Sitecore! | All posts tagged 'PowerShell'

Experience Sitecore!

Martin Miles on Sitecore

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!


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 this context it deals with PowerShell module. Instead, extract archive content into folder<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 bare minimum, I borrowed the very simple script from Adam's gists. That is what we got into PS.ps1:

Set-ExecutionPolicy RemoteSigned
Import-Module -Name SPE

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

Invoke-RemoteScript -ScriptBlock {
    Get-Item -Path "master:\content\Careers\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.


Some useful batch and PowerShell snippents helping Sitecore automation

From time to time working with Sitecore I have to rely on automation (especially when working with CI / CD) so just decided to store some snippets for myself that use occasionally. This list will update with time.

  1. Run MsBuild from a console
  2. Config transform
  3. Test SQL connectivity from PowerShell
  4. Archive a folder and place it into a specific location using PowerShell only
  5. Unzip a folder from an archive using PowerShell only
  6. Install a NuGet package using CLI
  7. Run xUnit tests
  8. Push a NuGet package into a repository on example of Ocopus with API secret
  9. Upload a file to FTP using WebClient via PowerShell
  10. Transform of webconfig setting Sitecore 9 "role" and "localenv" variables
  11. Deserialize Unicorn from a PowerShell
  12. Create new IIS hostname winding for existing website

1. Run MsBuild from a console
Building solution outside of Visual Studio or alternative IDE requires a manual call of MsBuild. In the very simple call you need to pass just two parameters - a solution itself and target, that can be Clean, Build etc.:
"c:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\MSBuild.exe" c:\Projects\Platform\Platform.sln  /t:Build

2. Config transform
Calling config transform described in more details by this link, so there is just a snippet below: 
"C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\amd64\MSBuild.exe" 
/nologo /maxcpucount 
/nodeReuse:False 
/property:Configuration=Debug 
/property:Platform="Any CPU" 
/property:WebConfigToTransform=C:\inetpub\wwwroot\Platform.dev.local\ 
/property:TransformFile=C:\Projects\Platform\src\Project\YourWebsite\code\web.config.xdt 
/property:FileToTransform=web.config 
/target:ApplyTransform 
/toolsversion:15.0 
/verbosity:minimal 
C:\Projects\Platform\scripts\applytransform.targets

3. Test SQL connectivity from PowerShell
The easiest way to test connectivity between a custom machine running PowerShell and the desired SQL Server instance:
Invoke-Sqlcmd -ServerInstance 'hostname-and-instance-and-optionally-port' `
                      -Username 'sa' `
                      -Password 'Pa55W0rd!' `
                      -Query 'SELECT GETDATE() AS TimeOfQuery'

4. Archive a folder a place it into a specific location using PowerShell only
When you need to archive a folder you need to rely on an external tool such as zip, which brings another dependency into your pipeline. But that is not a case anymore when using PowerShell as it has entire power of .NET and that in turn has zip support within the namespaceSystem.Net.Compression. So why not to rely on PowerShell and .NET to do the entire job?
IF EXIST output.zip DEL /F output.zip
powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('c:\Projects\Platform\build\output\', 'output.zip'); }"

5. Unzip a folder from an archive using PowerShell only
The reverse procedure of unzipping an archive into a folder can also be performed with .NET and PowerShell in the same manner:
powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('c:\Projects\SomeArchive.zip', 'c:\inetpub\wwwroot\TargetFolder'); }"

6. Install a NuGet package using CLI
When you need to use NuGet from a console, you will be likely using NuGet CL. An example below shows installing a NuGet package into a folder passed as a parameter. Please keep in mind that package repositories should be mentioned in the accompanying configuration for nuget.exe:
nuget install xunit.runner.console -OutputDirectory c:\Projects\!\NuGet\

7 Run xUnit tests
After installing xUnit running unit tests from a console, the rest is easy as than simply passing a library containing tests as a parameter:
c:\Projects\!\NuGet\xunit.runner.console.2.3.1\tools\net452\xunit.console.exe c:\Projects\Platform\src\Foundation\Dictionary\tests\bin\Debug\Sitecore.Foundation.Dictionary.Tests.dll

8. Push a NuGet package into a repository on an example of Octopus with API secret key
When you create a versioned package and may want to push that into NuGet repository, you will rely on nuget push command. A snippet shown below demonstrates that on the example of Octopus Deploy, passing it API key. A code below use to substitute current site context, pretty easy:
NuGet.exe push Platform.68.0.0.nupkg -ApiKey API-UZYKODSIIRJZQF25QP2T7WFWG -Source http://winbuildserver.local:8080/nuget/packages

9. Upload a file to FTP using WebClient via PowerShell
One more trick to avoid using system dependencies by calling .NET commands via PowerShell. This time it is for sending a file over FTP to the remote server. Quite a disadvantage is storing the details open-text, including a password. That should be parametrised, of course:
$File = "c:\Projects\archive.zip"
$ftp = "ftp://hostname-and-port\username:Pa55w0rd@domain/path/more-folder/archive.zip"

"ftp url: $ftp"

$webclient = New-Object System.Net.WebClient
#$uri = New-Object System.Uri($ftp)

$uri = [uri]::EscapeUriString($ftp)

"Uploading $File..."

$webclient.UploadFile($uri, $File)

10. Transform of web.config settings for "role" and "localenv" variables
In Sitecore 9, one can set up an instance into a specific role that also takes predefined configurations. Further ahead, you may keep your numerous custom configurations next to each other targeting different 'roles' - that avoids clumsy config pathing and keeps settings functionally together in order to simplify maintenance. There is also localenv setting that helps you to distinguish various groups of servers from the same role, but residing in the different environments. 
I have a separate blog post dedicated to this task.

11. Deserialize Unicorn from a PowerShell
If you are using Unicorn in a Continuous Delivery pipeline, you need to make unicorn deserialise (sync) items into Sitecore from a console. Luckily, Unicorn has support for doing that by calling sync.ps1 that uses MicroCHAP.dll and supporting script Unicorn.psm1, passing Unicorn URL and a secret key as a parameter. That secret key can be configured at Unicorn.SharedSecret.config. Make sure there is unrestricted execution policy. Usage is pretty easy: 
sync.ps1 -secret 749CABBC85EAD20CE55E2C6066F1BE375D2115696C8A8B24DB6ED1FD60613086 -url http://platform.dev.local/unicorn.aspx

12. Create new IIS hostname winding for existing website
When installing Sitecore with SIF, it makes sense also to add all your additional custom domain names bindings into IIS website, that has been just created by SIF, ideally should be done for both HTTP on port 80 and HTTPS on 443. The last one also requires creating a self-signed certificate for given hostname. So you may create a step having this command at the very end of installation PowerShell script:
	$Hostname = "YourSiteCustomHostname.dev.local" 
	$SiteNameHere = "$SolutionPrefix.$SitePostFix" # "Platform.dev.local"
	
        write-host "Adding IIS Hostname Binding for website (HTTP and HTTPS)"
        write-host "Site name: $SiteNameHere"
        write-host "Hostname: $Hostname"

	$cert=(Get-ChildItem cert:\LocalMachine\My | where-object { $_.Subject -match "CN=$Hostname" } | Select-Object -First 1) 
	if ($cert  -eq $null) { 
		$cert = New-SelfSignedCertificate -DnsName $Hostname -CertStoreLocation "Cert:\LocalMachine\My" 
	} 
	$binding = (Get-WebBinding -Name $SiteNameHere | where-object {$_.protocol -eq "https"})
	if($binding -ne $null) {
		try{
	     	Remove-WebBinding -Name $SiteNameHere -Port 80 -Protocol "http" -HostHeader $Hostname
	     	Remove-WebBinding -Name $SiteNameHere -Port 443 -Protocol "https" -HostHeader $Hostname
		}
		catch{
		     	write-host "$SiteNameHere yet does not have a binding for $Hostname"
		}
	} 
	
	New-WebBinding -Name $SiteNameHere -IPAddress "*" -Port 80 -HostHeader $Hostname
New-WebBinding -Name $SiteNameHere -Port 443 -Protocol https -HostHeader $Hostname (Get-WebBinding -Name $SiteNameHere -Port 443 -Protocol "https" -HostHeader $Hostname).AddSslCertificate($cert.Thumbprint, "my")

Configuring role and localenv variables in Sitecore 9 - PowerShell way

In Sitecore 9, one can set up an instance into a specific 'role' that also takes predefined configurations. Further ahead, you may keep your numerous custom configurations next to each other targeting different 'roles' - that avoids clumsy config pathing and keeps settings functionally together in order to simplify maintenance. There is also 'localenv' setting that helps you to distinguish various groups of servers from the same role, but residing in the different environments. Not a nice way of changing role and adding environment by simply replacing a string occurrence:
(Get-Content Web.config).replace('    <add key="role:define" value="Standalone" />', '<add key="role:define" value="ContentManagement" /><add key="localenv:define" value="UAT" />') | Set-Content Web.config

Just a string replacement? Errrghh.... Not a nice solution! Let's make it better, by relying on XML namespace (thanks, Neil):
#$webConfigPath = "С:\path\to\your\Web.config"
#$localEnvName = "UAT"

$RptKeyFound=0;
$xml = (get-content $webConfigPath) -as [Xml];              # Create the XML Object and open the web.config file 
$root = $xml.get_DocumentElement();                         # Get the root element of the file

foreach($item in $root.appSettings.add)                     # loop through the child items in appsettings 
{ 
  if($item.key -eq "localenv:define")                       # If the desired element already exists 
    { 
      $RptKeyFound=1;                                       # Set the found flag 
    } 
}

if($RptKeyFound -eq 0)                                      # If the desired element does not exist 
{ 
    $newEl=$xml.CreateElement("add");                       # Create a new Element 
    $nameAtt1=$xml.CreateAttribute("key");                  # Create a new attribute "key" 
    $nameAtt1.psbase.value="localenv:define";               # Set the value of "key" attribute 
    $newEl.SetAttributeNode($nameAtt1);                     # Attach the "key" attribute 
    $nameAtt2=$xml.CreateAttribute("value");                # Create "value" attribute  
    $nameAtt2.psbase.value="$localEnvName";                 # Set the value of "value" attribute 
    $newEl.SetAttributeNode($nameAtt2);                     # Attach the "value" attribute 
    $xml.configuration["appSettings"].AppendChild($newEl);  # Add the newly created element to the right position
}

$xml.Save($webConfigPath)                                   # Save the web.config file
This same approach can be taken for any other XML-based transforms and replacements.

Fixing: Sitecore 9 installer unable to connect to Solr server, while it is available

Recently tried to reinstall Sitecore 9.0 update 1 and got the following message:

"Request failed: Unable to connect to remote server" (and the URL which is default https://localhost:8389/solr)


Opening Solr in browser went well, and there were no existing cores that could prevent the installation. Weird...


After experiments, I found out that was an antivirus preventing such requests. Disabling it allowed cores to be installed well and thу rest of install script succeeded.

Sitecore 9 is out there


So, finally, what we all have been waiting for, has happened - Sitecore 9 has been announced and released. To be honest, as an MVP, I already have had an access to a beta of the version 9 of Sitecore since late July. but after an official release, we now can publically talk about that.

So, what has been new:

Runtime for Sitecore 9 is .NET Framework 4.6.2 and most of Sitecore 9 NuGet libraries are built against that version of the framework.

The installation process is the first you meet when trying to play with the platform. And it's worth saying that installation process has been totally reworked. So, welcome Sitecore Install Framework - Powershell modules for install and uninstall and a set of JSON config files that store steps for installing Sitecore. Each of these JSON files relies on a part of installation: xConnect, Solr and Sitecore itself. No SIM support (yet) available, unfortunately.

I mentioned xConnect - that is a new component and probably the largest architectural change for the platform - a framework that allows you to read and write analytics data to xDB and search providers, keeping collections, processing, search (Solr) and reporting behind itself. Previously we have had 4 different APIs used to deal with xDB contact, depending on its state, so that entire method full of if /else clauses looked monstrous. Since now we do not do direct xDB calls and operate using XConnectClient. That allows updating contacts from any channel at any time and automatic indexing of data. Sitecore XP introduces client certificate authentication in order to communicate with xConnect server, that is a Windows service. Also, a good thing is that this framework is well documented.

SQL Server 2016 becomes the only storage for all Sitecore databases - content data, xDB, marketing, processing, reporting etc. If you are desperate to keep Mongo - nothing to worry about it is still possible to do, or you may store in the cloud with Microsoft Azure SQL or Azure DocumentDB.

Solr is now search provider by default, and it is also required for installation by Sitecore Installation Framework. Sitecore 9.0 supports version 6.6.2 and sorry not for having version 7.+ of Solr. It is commonly used to install Solr as a windows service, using nssm as the easiest.

Sitecore Services Client (SSC) has got the ability to create oData REST service and API key management for securing access to APIs, that would be good for interacting with VUE or React JS.

Those who struggled with Web Forms for Marketers module previously will enjoy new Sitecore Forms, featuring completely redesigned UX with drag and drop, easy styling, built analytics. Also, there is a multipage wizard for creating complex "split" forms. One can even create a form template and instantiate forms from it! Also, there is form and field metrics showing the corresponding performance.

One more long waited feature is Dynamic Placeholders, now built into the platform, so that it may be called as @Html.Sitecore().DynamicPlaceholder("name"/*, some optional parameters*/). In general purpose, dynamic placeholders allow the use of the same placeholder key multiple times in the same rendering and across multiple renderings in the same placeholder. We have evidenced multiple implementations, so now good to have a universal standard.

Marketing Automation tool is much better reworked. It provides a new way to create automated online campaigns in Sitecore. With a user-friendly drag-and-drop interface, Marketing Automation provides an extensible set of tools to processes contact enrollment in campaigns and activities. The Marketing Automation Engine is a stand-alone service that is responsible for enrolling contacts in plans and activities

Express Migration Tool is already familiar to us. Since version 9 that it supports upgrades to 9 from almost any legacy version of Sitecore.

Welcome, SPEAK 3, built on Angular 4, now using proper architecture. It is now reworked according to best industry standards, pulls data from Sitecore Services Client and unlike previous versions, it is item unaware. That means no more struggle with Rocks and lack of documentation. There is already a demo project you may download and rework.

What previously caused pain for both developers, administrators, DevOps was way too much complicated configuration. Sitecore is a truly flexible platform, but the flexibility has hidden cost of every flexible component being registered via configuration, boosting compiled config up to hundreds of thousands of lines. That was taken into consideration and we were given configuration Roles and Layers. Now it becomes possible to configure whether a particular setting or config node should be patched for a specific server role using role:require attribute. An instance can be in one of the following roles: ContentDelivery, ContentManagement, Processing, Reporting, Standalone. Apart from roles, there is also a different level of configuration - the also layers: Sitecore, modules, custom - simple, flexible, allows less patching. With layers one can switch off an entire layer, it can be handy for Sitecore support providing a diagnostics, to disable anything apart from Sitecore to make ensure platform itself is working well.

What is else new regarding Sitecore 9?
JSS Framework is staging. Also coming soon Sitecore Commerce 9 with a proper inventory and support for federated authentication, SXA and Azure PaaS, CRM connectors for SalesForce and Dynamics for xConnect.

PowerShell Module issue - got a NullReferenceException when trying to open PowerShell ISE

Today I downloaded the latest (v.3.1 for Sitecore 8_001) PowerShell module and decided to play around one concept I hoped to push forward. But unfortunately, as soon as I opened ISE window, I got a NullReferenceException.


I thought that could possibly be related to my Sitecore instance. Thus I have installed a new copy of Sitecore 8.0.4 using Sitecore Instance Manager. Then tried to install package at that instance and magically worked there. So, two instances with the same Sitecore version, same PowerShell module, but different results... hm-m...

Then I assumed that ISE is trying to run some default script and probably that fact for some reason causes the exception. So, what are difference between two instances? I thought that initial ISE script might be rely on home item, and the one may be hardcoded, while my first sitecore instance has several website roots, but none of them is called Home. Worth of checking that idea. Just as the quickest, I have duplicated one of my webroots within /sitecore/content node and set a copy with the name Home. And voila - it magically worked for me!



Mass update of field value for all existing items of certain template - two ways of achieving result

Given: I have multiple already existing items of a specific template across my content tree. Template has some field that I want to update with a new value for all existing items, within some node I can specify.

Use cases for that scenario are really universal - you may use that for various purposes. What comes into my head, at a first glance, is the situation when you would like to assign some template to a workflow but already have multiple existing items of that template (just to remind - setting a field value at Standard Values will apply for all new, but not existing items of that particular template). In that case you need assign a workflow to each existing item individually.

Another good example is when you have a folder with multiple items, growing in amount with time flow. Then you may decide to make a folder bucketable, but those existing items require to set a Bucketable checkbox for each existing item individually. I have described that case in one of my previous blog posts - "Understanding Buckets: adding new items to buckets correctly". So let's pick up second case and try to solve it.

Solution: So, what are possible ways of achieving that goal? Not so much of them, there are just two options.

1. Straightforward option - use C# code. With C# task may be achieved by a recursive method, that accepts an item, verifies if the one is of desired template and if yes - updates the field and does the same for all item's children if any. Here is an example of such method and example of call :

private void UpdateAllFieldsRecursively(Item parentItem, string templateName, string fieldName, string newValue)
{
    if (parentItem != null)
    {
        using (new SecurityDisabler())
        {
            foreach (Item childItem in parentItem.Children)
            {
                if (childItem.Fields[fieldName] != null && childItem.TemplateName == templateName)
                {
                    using (new EditContext(childItem))
                    {
                        childItem[fieldName] = newValue;
                    }
                }

                if (childItem.HasChildren)
                {
                    UpdateAllFieldsRecursively(childItem, templateName, fieldName, newValue);
                }
            }
        }
    }
}

// and below there is the call to recursive mehod
const string parentNode = "/sitecore/content";
var database = Sitecore.Context.Database;
var parentItem = database.GetItem(parentNode);

UpdateAllFieldsRecursively(parentItem, "Article", "Bucketable", "1");

The code above works out perfectly and does exactly what we want from it, however there are certain disadvantages: first of all you need to run the code in some context - call if from some existing class, for example. Second - as that is C# code - it must be re-compiled and re-deployed. And if you may need to amend small change into the code - will have to re-compile and re-deploy again. But, luckily, there is a much better and faster alternative:

2. PowerShell Module. That module integrates into the platform and offers unprecedentedly ultimate possibilities and power over Sitecore instance and databases. After installation there is PowerShell console and more advanced PowerShell ISE, where you may create and test scripts. They are executed immediately without any of prerequisites like rebuilt DLL with a code at your bin folder. So, here is a script that does exactly the same as previous C# code:

cd 'master:/sitecore/content'
Get-ChildItem -Recurse . | Where-Object { $_.TemplateName -match "Article" -and $_.Fields["Bucketable"] -ne $null } | ForEach-Object {
   
    $_.Editing.BeginEdit()
    $_.Fields["Bucketable"].Value = "1";
    $_.Editing.EndEdit()
    ""
}

PowerShell script above does exactly the same - iterates Sitecore tree for "Article" templates, starting recursively from /sitecore/content node and if anything found - updates the filed value.

Hope this code helps!