Sitecore Stuff and Such

- by Christian Kay Linkhusen

NAVIGATION - SEARCH

Run Unicorn Sync from Octopus Deploying to Azure With PowerShell Script

In a setup where you deploy your web applications to Azure with Octopus, you’ll find out, when you execute an Azure PowerShell Script the context you execute the script is in the scope of the Octopus Server. This causes trouble when you try to execute a synchronization of Unicorn from PowerShell as described on GitHub Unicorn for Sitecore – PowerShell Remote Scripting.

The reason to this is that, when you execute your PowerShell script in the Azure PowerShell Script Step, you are not able to load the MicroCHAP.dll as it is not present on the Octopus Server. The solution to get access to the local resources on the Azure WebApp is thru the Kudu API.

A setup in Octopus Deploying a Sitecore solution with a final step that executes a Unicorn Sync could look like this in Octopus:
  1. Provision Sitecore to Azure if Sitecore is not present
  2. Stop the Content Editing Site
  3. Cleanup before deploying the application
  4. Deployment of the application itself
  5. Start the Content Editing Site
  6. Hold on a moment for the CM to get alive
  7. Sync your Unicorn stuff
The last step executes a PowerShell made with a strong inspiration from Kam’s example of executing the sync from a remote script.

    function Get-AzureRmWebAppPublishingCredentials($resourceGroupName, $webAppName, $slotName = $null){
	if ([string]::IsNullOrWhiteSpace($slotName)){
		$resourceType = "Microsoft.Web/sites/config"
		$resourceName = "$webAppName/publishingcredentials"
	}
	else{
		$resourceType = "Microsoft.Web/sites/slots/config"
		$resourceName = "$webAppName/$slotName/publishingcredentials"
	}
   
	$publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force
    return $publishingCredentials
}

function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName, $slotName = $null){
    $publishingCredentials = Get-AzureRmWebAppPublishingCredentials $resourceGroupName $webAppName $slotName
    return ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
}
function Get-KuduApiUrl($webAppName, $slotName = "", $kuduPath){
    if ($slotName -eq ""){
        $kuduApiUrl = "https://$webAppName.scm.azurewebsites.net/api/vfs/site/wwwroot/$kuduPath"
    }
    else{
        $kuduApiUrl = "https://$webAppName`-$slotName.scm.azurewebsites.net/api/vfs/site/wwwroot/$kuduPath"
    }
    $virtualPath = $kuduApiUrl.Replace(".scm.azurewebsites.", ".azurewebsites.").Replace("/api/vfs/site/wwwroot", "")

    [hashtable]$Return = @{} 
    $Return.VirtualPath = $virtualPath
    $Return.KuduApiUrl = $kuduApiUrl

    return $Return
}

function Download-FileFromWebApp($resourceGroupName, $webAppName, $slotName = "", $kuduPath, $localPath){
    $kuduApiAuthorisationToken = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName
    $kuduApiObj = Get-KuduApiUrl $webAppName $slotName $kuduPath
   
    Write-Host "Downloading File from WebApp. Source: '$($kuduApiObj.VirtualPath)'. Target: '$localPath'..." -ForegroundColor DarkGray

    Invoke-RestMethod -Uri $kuduApiObj.KuduApiUrl `
                        -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
                        -Method GET `
                        -OutFile $localPath `
                        -ContentType "multipart/form-data" `
                        -ErrorAction SilentlyContinue
}
$ScriptPath = Split-Path $MyInvocation.MyCommand.Path

function Sync-Unicorn {
	param(
		[Parameter(Mandatory=$TRUE)]
		[string]$ControlPanelUrl,
		[Parameter(Mandatory=$TRUE)]
		[string]$SharedSecret,
		[string[]]$Configurations,
		[string]$Verb = 'Sync',
		[switch]$SkipTransparentConfigs,
		[switch]$DebugSecurity
	)

	# PARSE THE URL TO REQUEST
	$parsedConfigurations = '' # blank/default = all
	
	if($Configurations) {
		$parsedConfigurations = ($Configurations) -join "^"
	}

	$skipValue = 0
	if($SkipTransparentConfigs) {
		$skipValue = 1
	}

	$url = "{0}?verb={1}&configuration={2}&skipTransparentConfigs={3}" -f $ControlPanelUrl, $Verb, $parsedConfigurations, $skipValue 

	if($DebugSecurity) {
		Write-Host "Sync-Unicorn: Preparing authorization for $url"
	}

	# GET AN AUTH CHALLENGE
	$challenge = Get-Challenge -ControlPanelUrl $ControlPanelUrl

	if($DebugSecurity) {
		Write-Host "Sync-Unicorn: Received challenge from remote server: $challenge"
	}

	# CREATE A SIGNATURE WITH THE SHARED SECRET AND CHALLENGE
	$signatureService = New-Object MicroCHAP.SignatureService -ArgumentList $SharedSecret

	$signature = $signatureService.CreateSignature($challenge, $url, $null)

	if($DebugSecurity) {
		Write-Host "Sync-Unicorn: MAC '$($signature.SignatureSource)'"
		Write-Host "Sync-Unicorn: HMAC '$($signature.SignatureHash)'"
		Write-Host "Sync-Unicorn: If you get authorization failures compare the values above to the Sitecore logs."
	}

	Write-Host "Sync-Unicorn: Executing $Verb..."

	# USING THE SIGNATURE, EXECUTE UNICORN
	[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
	$result = Invoke-StreamingWebRequest -Uri $url -Mac $signature.SignatureHash -Nonce $challenge

	if($result.TrimEnd().EndsWith('****ERROR OCCURRED****')) {
		throw "Unicorn $Verb to $url returned an error. See the preceding log for details."
	}

	# Uncomment this if you want the console results to be returned by the function
	# $result
}

function Get-Challenge {
	param(
		[Parameter(Mandatory=$TRUE)]
		[string]$ControlPanelUrl
	)

	$url = "$($ControlPanelUrl)?verb=Challenge"

	[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
	$result = Invoke-WebRequest -Uri $url -TimeoutSec 360 -UseBasicParsing

	$result.Content
}

function Invoke-StreamingWebRequest($Uri, $MAC, $Nonce) {
	$responseText = new-object -TypeName "System.Text.StringBuilder"

	$request = [System.Net.WebRequest]::Create($Uri)
	$request.Headers["X-MC-MAC"] = $MAC
	$request.Headers["X-MC-Nonce"] = $Nonce
	$request.Timeout = 10800000

	$response = $request.GetResponse()
	$responseStream = $response.GetResponseStream()
	$responseStreamReader = new-object System.IO.StreamReader $responseStream
	
	while(-not $responseStreamReader.EndOfStream) {
		$line = $responseStreamReader.ReadLine()

		if($line.StartsWith('Error:')) {
			Write-Host $line.Substring(7) -ForegroundColor Red
		}
		elseif($line.StartsWith('Warning:')) {
			Write-Host $line.Substring(9) -ForegroundColor Yellow
		}
		elseif($line.StartsWith('Debug:')) {
			Write-Host $line.Substring(7) -ForegroundColor Gray
		}
		elseif($line.StartsWith('Info:')) {
			Write-Host $line.Substring(6) -ForegroundColor White
		}
		else {
			Write-Host $line -ForegroundColor White
		}
		[void]$responseText.AppendLine($line)
	}
	return $responseText.ToString()
}

. .\AuthenticateWithServicePrincipal.ps1
Authenticate
Set-AzureRMContext -SubscriptionId $subscriptionId

Download-FileFromWebApp $resourceGroup $OctopusParameters["Octopus.Action.Azure.WebAppName"] "" "bin/MicroCHAP.dll" "$($OctopusParameters["env:OctopusCalamariWorkingDirectory"])\MicroCHAP.dll"
Add-Type -Path "$($OctopusParameters["env:OctopusCalamariWorkingDirectory"])\MicroCHAP.dll"

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Sync-Unicorn -ControlPanelUrl "https://$($OctopusParameters["cmSiteHostName"])/unicorn.aspx" -SharedSecret $OctopusParameters["Unicorn.SharedSecret"]

Publish Item from the Context Menu

A nice feature in Sitecore when you are editing Items from the Content Editor, is the Context Menu. Here are almost all functions you'll need when working with Items. Create new, Copy, Delete, Rename Item and more. But one feature that is missing is the possibility to publish your Items.
But don't cry yourself to sleep anymore over this, because this little tip will fix this small issue for you :)

In Sitecore open the Desktop and switch to the Core Database. Here you'll find how Sitecore is build up inside Sitecore, so be carefull not to mess it all up inside here! 

Next you wil have to navigate to the Menu Item for Publish Item. This you will find on the path: "/sitecore/content/Applications/Content Editor/Menues/Publish/Publish Item"

Then you just have to copy the Publish Item menu item to the Default Context menu. This is done by selecting Copying -> Copy to and select the destination: "/sitecore/content/Applications/Content Editor/Context Menues/Default"

Last thing you just have to do is to sort the new Menu Item in the Context menu, making it appear in a logical order in the menu. Go to the Default Context menu in the Content editor: "/sitecore/content/Applications/Content Editor/Context Menues/Default", and just sort the Publishing Item to the position you find best.

That's all - shift back to the Master database and start publishing your Sitecore Items from the Context Menu. 

Update Items without changing theirs statistics

I have a small tip to you, when you have to update items in your Sitecore solution from a batch job or a script correcting data on several items in the solution. It is possible to accomplice this without changing the fields "Updated by" and "Updated", there by your script will not overwrite the original data about, who updated the item and when.
 
This is simply done by setting the updateStatistics flag to false when editing the Item.

    using (new SecurityDisabler())
    {
        var items = Sitecore.Context.Item.Children;
        foreach (var scItem in items)
        {
            using (new EditContext(scItem, false, false))
            {
                //update whatever data on your childitem you have to correct
                //without changing the statistics fields
            }
        }
    }

Thats all folks - hapii coding ;)


Receiving the same reminder twice from Sitecore

When using the reminder function in Sitecore the editor will receive the same reminder for every database the Item exists. In a normal setup with a master and a web database this will cause the editor to get the same reminder twice.

If you only want the editors to receive one email (eg. for the master database) it is possible to disable the sending of reminders for other databases by overriding the Sitecore.Tasks.TaskDatabaseAgent in Sitecore.

After reflecting the TaskDatabaseAgent I came up with this implementation, which only execute a EmailReminderTask if it's from the Master database. If it's from another database the task is marked as done, but without executing the Task. 


public class CustomTaskDatabaseAgent : TaskDatabaseAgent
    {
        public new void Run()
        {
            Task[] pendingTasks = Globals.TaskDatabase.GetPendingTasks();
            this.LogInfo("Processing tasks (count: " + (object)pendingTasks.Length + ")");
            foreach (Task task in pendingTasks)
            {
                try
                {
                    task.LogActivity = this.LogActivity;
                    //Test if the Task is the EmailReminderTask and the current database is not master database
                    if (task.GetType() == typeof (EmailReminderTask) && !task.DatabaseName.ToLower().Equals("master"))
                    {
                        //If its not the master database
                        //Only mark the Task as done, dont execute the remider
                        Globals.TaskDatabase.MarkDone(task);
                    }
                    else
                    {
                        task.Execute();    
                    }
                    TaskCounters.TasksExecuted.Increment();
                }
                catch (Exception ex)
                {
                    Log.Error("Exception in task", ex, task.GetType());
                }
            }
        }

        private void LogInfo(string message)
        {
            if (!this.LogActivity)
                return;
            Log.Info(message, (object)this);
        }
    }


The custom DatabaseAgent is enabled by changing the Sitecore.Tasks.DatabaseAgent in the web.config file.


//Default TaskDatabaseAgent
<agent type="Sitecore.Tasks.TaskDatabaseAgent" method="Run" interval="00:10:00"/>

//Custom TaskDatabaseAgent
<agent type="Netmester.SitecoreExtensions.CustomTaskDatabaseAgent" method="Run" interval="00:10:00"/>
	

Now the editor will only receive one email pr. reminder, which is the one from the Master database.

Problems with Reminders not being sent in Sitecore CM/CD setup

If you set up the reminder function in Sitecore, and you have a Multi server setup with Content Manager and Content Delivery instances, you may run into problems with reminders not being sent.

The problem is that the TaskDatabaseAgent are running on both the CM- and the CD instance in your Sitecore setup, and if the Master database is disabled on the CD instance, the sending of the reminder will fail. In the Sitecore logfiles on the CD instance you will find log entries like this one:


ManagedPoolThread #13 14:34:25 ERROR Exception in task
Exception: System.InvalidOperationException
Message: Could not find configuration node: databases/database[@id='master']
Source: Sitecore.Kernel
   at Sitecore.Diagnostics.Assert.IsTrue(Boolean condition, String message)
   at Sitecore.Configuration.Factory.GetConfigNode(String xpath, Boolean assert)
   at Sitecore.Configuration.Factory.CreateObject(String configPath, String[] parameters, Boolean assert)
   at Sitecore.Configuration.Factory.GetDatabase(String name, Boolean assert)
   at Sitecore.Tasks.EmailReminderTask.SendReminder()
   at Sitecore.Tasks.TaskDatabaseAgent.Run()

ManagedPoolThread #13 14:34:25 INFO  Executing email reminder task
ManagedPoolThread #13 14:34:25 INFO  Parameters: <r><to>xyz@domain.net</to><txt>Påmindelse kl 13:41 - ckl</txt></r>
ManagedPoolThread #13 14:34:25 ERROR Exception in task
Exception: System.InvalidOperationException
Message: Could not find configuration node: databases/database[@id='master']
Source: Sitecore.Kernel
   at Sitecore.Diagnostics.Assert.IsTrue(Boolean condition, String message)
   at Sitecore.Configuration.Factory.GetConfigNode(String xpath, Boolean assert)
   at Sitecore.Configuration.Factory.CreateObject(String configPath, String[] parameters, Boolean assert)
   at Sitecore.Configuration.Factory.GetDatabase(String name, Boolean assert)
   at Sitecore.Tasks.EmailReminderTask.SendReminder()
   at Sitecore.Tasks.TaskDatabaseAgent.Run()

ManagedPoolThread #13 14:34:25 INFO  Job ended: Sitecore.Tasks.TaskDatabaseAgent (units processed: )

The solution to the problem is to disable the TaskDatabaseAgent on the CD instance. This is done by setting the interval to 00:00:00 on the TaskDatabaseAgent in the web.config file.

	
<agent type="Sitecore.Tasks.TaskDatabaseAgent" method="Run" interval="00:00:00"/>


Once you have solved the problem, you might receive a reminder mail from each database for every item where a reminder was created in Sitecore. How to solve this problem you can read about in this  blog: Receiving the same reminder twice :)

Overriding of the Sitecore.Context.Item

In a solution I'm working on, we have a newslist presentation where News are created in Sitecore as Items in different parts of the content tree, and they are all shown on the same list. When you follow a link on the list, the Newsitem is shown in the context of the list and not from the address reflecting where the Item is created in Sitecore.

Address of NewsItems that are presented in the context of the newslist would be on the form:
www.host.net/list/item?id={guid}

 
When you implement your webcontrols for presentation of the Item, it is not possible to reference Sitecore.Context.Item because the context will be the containeritem used as an "Presentation container Item" and not the newsitem you want to show. A solution could be to create a local variable "currentItem" where you get the item you want to render by caling something like Sitecore.Context.Database.GetItem(Request.QueryString["id"]); in the Page_Load() or similar. But this solution, will not work if you have Items with different layouts, eg. if you are using Web Forms For Marketiers and have created a webform for one of the Items you are trying to render in the context of the list.

A solution for the problem could be to override the Context Item by creating a httpRequestBegin processor and execute right after Sitecores Itemresolver.
More...







Single Sign On to the Sitecore desktop

When you use Sitecore Active Directory module it is possible to set up Single Sign On by protecting the file LDAPLogin.aspx with Windows Authentication. Then it is possible to login to Sitecore without entering username and password, when you enter the following URL: http://[yoursite]/sitecore/admin/LDAPLogin.aspx.
This will login the user to the Content Editor or the StartURL entered on the users User Profile in Sitecore.

But what if you want to make it possible for the users to decide for them self, if they would like to login to the Desktop or the Content Editor?

Syntax highlighting with highlight.js

Highlight.js supplies a javascript library for syntax highlighting your code snippets on the web. The solution is hosted and is setup with inserting 3 lines of code. 

Include the Javascript, stylesheet and initialize the highlighting, by inserting the following 3 lines to the head section of your page:

<link rel="stylesheet" href="http://yandex.st/highlightjs/8.0/styles/vs.min.css"> <script src="http://yandex.st/highlightjs/8.0/highlight.min.js"></script> <script type="text/javascript">hljs.initHighlightingOnLoad();</script>

All you have to do now, is to mark up your code snippet in the content with <pre><code>[content]</code</pre> and you are up and running.