Quick Self-Signed Certs Sitecore 9.x

When you download and install Sitecore XP1, you need to install all of the certificates first. If you need to do this often like I do, this can be quite tedious.  Stealing some code from the Single-Developer.ps1, this simple foreach wrapper is a huge time saver.

This assumes you already have the Sitecore Installation Framework installed(SIF) and can execute PowerShell scripts. Please use real certificates in production.

Enjoy!

###############################
# CreateAllSelfSignedCerts

# Prefix for sites
$Prefix = "sitecore"
# Folder with the install files in it (specifically the createcert.json)
$SCInstallRoot = "C:\Sitecore\9.1.1\XP1\"

$ContentDeliverySiteName = "$prefix.cd"
$ContentManagementSiteName = "$prefix.cm"
$ReportingSiteName = "$prefix.rep"
$ProcessingSiteName = "$prefix.prc"
$ReferenceDateSiteName = "$prefix.refdata"
$IdentityServerSiteName = "$prefix.identityserver"
$XP1MarketingAutomationSiteName = "$prefix.ma"
$XP1MarketingAutomationReportingSiteName = "$prefix.mareporting"
$XP1ClientCertificateName = "$prefix.xconnect_client"
$XP1CollectionSitename = "$prefix.collection"
$XP1CollectionSearchSitename = "$prefix.search"
$XP1CortexProcessingSitename = "$prefix.processingEngine"
$XP1CortexReportingSitename = "$prefix.reporting"

$siteNames =
$ContentDeliverySiteName,
$ContentManagementSiteName,
$ReportingSiteName,
$ProcessingSiteName,
$ReferenceDateSiteName,
$IdentityServerSiteName,
$XP1MarketingAutomationSiteName,
$XP1MarketingAutomationReportingSiteName,
$XP1ClientCertificateName,
$XP1CollectionSitename,
$XP1CollectionSearchSitename,
$XP1CortexProcessingSitename,
$XP1CortexReportingSitename

function InstallCertificates {
Foreach ($site in $siteNames) {
$certParams = @{
Path = "$SCInstallRoot\createcert.json"
CertificateName = $site
}
Install-SitecoreConfiguration @certParams -Verbose *&>1 | Tee-Object ".\CertInstall.${site}.log"
}
}
InstallCertificates

Querying Sitecore

Often times we run into situations where we need to pull information from Sitecore.  Two of my favorite tools to do this are Sitecore Rocks and Sitecore PowerShell Extensions (SPE).

For those of you who are unfamiliar with these tools, Sitecore Rocks is a Visual Studio extension that makes developing and interacting with Sitecore easy.  SPE is an installed module into Sitecore that gives you a scripting environment and a command line interface.  We will simply be executing queries, but I highly recommend reading their respective documentation sites to learn about their full set of features.

I don’t claim to be a PowerShell guru, so what I’ve done is create simple queries you can run in both Sitecore Rocks and SPE.  You can write pure PowerShell queries, but I find the ones I’ve created to feel overly complex.  So for any query below, you can execute this in SPE with the following:

Get-Item -Path master: -Query "/sitecore/templates/Feature//*[@@templatename = 'Template']"

Or like this so you can easily define the columns into a table:

$items = Get-Item -Path master: -Query "/sitecore/templates/Feature//*[@@templatename = 'Template']"

$items | Format-Table Name, @{ Label = 'Path'; Expression={ $_.Paths.Path } },  @{ Label = 'Std Val'; Expression={ $_["__Standard values"] } }

You can get more information about Working with Items and getting item by XPath on the SPE doc site.  If you have built indexes and aren’t working off of restored databases, a performant way to do this would be finding items using the content search API.

The examples provided below are intended to be run in the Sitecore Rocks Query Analyzer.  I highly recommend reading 28 Days of Sitecore Rocks: Query Analyzer to get started.

Standard Values of Templates
Are there Standard Values on all the templates?

select @@Name as Name, @@Path as Path, @#__Standard values# as #Standard Values# from /sitecore/templates/Feature//*[@@templatename = 'Template']

Types of Fields
Are there fields of type Multilist and what is their source?

select @@Name as Name, @Type, @Source, @@Path as Path from /sitecore/templates/Feature//*[@Type = 'Multilist']

Are there fields of type Treelist and what is their source?

select @@Name as Name, @Type, @Source, @@Path as Path from /sitecore/templates/Feature//*[@Type = 'Treelist']

What are all the fields, their types, and their sources regardless of their template? Order them by Type and Name.

select @@Name as Name, @Type, @@Path as Path from /sitecore/templates/Project//*[@@templatename = 'Template field'] order by Type, Name

List of Templates
Just give me a list of all the templates.

select @@Name as Name, @@Path as Path from /sitecore/templates/Project//*[@@templatename = 'Template']

List of Content Items
Where are all my content items based on the Article template in the Draft workflow state?  The GUID in this example is of the Draft item in a the Sample Workflow.

select @@Name as Name, @@Path as Path from /sitecore/content//*[@@templatename = 'Article' and contains(@#__Workflow state#, "{190B1C84-F1BE-47ED-AA41-F42193D9C8FC}")] order by Path

Rich Text Editor fields with HTML editor profiles defined
What are all the rich-text fields and their sources? (originally from Sitecore John himself)

select @@path, @Source from /sitecore/templates//*[@@templatekey = 'template field' and @type = 'Rich Text'] order by Source

Search for Tokens in Fields
Where are all the single-line text fields with a $name token?

select @@Name as Name, @Type, @@Path as Path from /sitecore/templates//*[@type = 'Single-Line Text' and contains(@title, '$name')]

Explorer Sitecore Rocks Tools
Right next to the Execute Command in the ribbon, there are some really useful tools!Sitecore Rocks Query Analyzer Tools

Insert Fields gives you a list of every field available out of Sitecore.  As it is connected to your Sitecore instance, this means it will pull the fields off of the items specific to your environment!

Sitecore Rocks Query Analyzer Insert Fields

Exporting Data with Sitecore Rocks

One of my favorite features is the ability easily export the results.  Simply right click on the Results tab.  This is invaluable for providing an ad hoc report of items.  For example, we could export the results of the “Article” query we performed.

Sitecore Rocks Query Analyzer Export

For repeatable queries/reports you want Sitecore users to access, consider a different approach with Sitecore PowerShell Extensions Dynamic Reports.

The Big Gotcha

When using a Sitecore Query, it’s important to be cognizant of the configured Query.MaxItems value in the Sitecore configs.  Depending on your version of Sitecore, it may be set to 100 or 260 by default.  I typically set this to 9999 on my local, but I wouldn’t do this on a production environment.

Disclaimer

These queries are intended for developers to gather information.  They are not optimized for performance and should not be used in a production environment.

Sitecore Workflow – Multiple Users Approving an Item

Today we are going to create a workflow that allows multiple users to approve an item.

What we won’t cover in this post is how to setup security on fields and workflow commands.  For that, I recommend John’s post on Security Access Rights.

Based on the business requirements, there are a few things I need to do:

  • Checks to make sure that at least one user is required for the approval
  • Make it easy for editors to determine who needs to approve the content

Workflow

I’ve gone ahead and built out my workflow with the needed commands and actions and selected some amazing icons.

MultiApprove Workflow
MultiApprove Workflow

User List Field Type

First, we need to build a custom Field Type that allows us to select the users we want to require.  I’ve called mine User List.  John blogged about a users droplist to accomplish this, so we’re going to build it as a multilist using another blog showing how to create a custom multilist field.  I’ve created a txt of Lookup.cs as it’s a bit long.

Now, actually setup the Field Type in Sitecore.  In the core database, I’ve created mine here:
/sitecore/system/Field types/List Types/User List using the template Template Field Type

Set the Assembly and Class appropriately.  Mine were:
Assembly: sandbox.BusinessLogic
Class: sandbox.BusinessLogic.Common.UserLookup

Workflow Template

With the new user list created, we can create our template like so.  I’ve also removed editing permissions to Everyone on the Approved By and the Awaiting Approval From fields as we don’t want editors to be able to change these.

MultiApproveTemplate
MultiApprove Template

Once you’ve created this, make sure you inherit it to either your site’s Standard Template  or Page Template.  In my case, my News Page inherits it.

MultiApprove Template Inheritance
MultiApprove Template Inheritance

 

Users Assigned Check Action

In my case, we want to prevent items from progressing in the workflow if they have not selected any users to approve it.  So, we want to create an Action under the Draft’s Submit command.  I’ve named mine UsersAssignedCheck.

User Assigned Check Action
User Assigned Check Action

In the Type String field, fill in the classNamespace.Classname, AssemblyName. For this example, we’ll be using:
sandbox.BusinessLogic.Workflows.MultiApprove.UsersAssignedCheck, sandbox.BusinessLogic

UserAssignedCheck Code:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;
using System;
using sandbox.BusinessLogic.Helpers;
using Sitecore.Workflows.Simple;

namespace sandbox.BusinessLogic.Workflows.Multiapprove
{
    public class UsersAssignedCheck
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            ProcessorItem processorItem = args.ProcessorItem;
            if (processorItem == null)
                return;
            var item = args.DataItem;


            // if no users selected, throw warning
            if (String.IsNullOrEmpty(item.Fields[MultiApproveConstants.Sitecore_UsersFieldName].Value)))
            {
                SheerResponse.Alert("Assign at least 1 Sitecore User to the MultiApprove Sitecore Users.", false, "Error");

                // abort to prevent Next State
                args.AbortPipeline();
            }
        }
    }
}

Screenshot:

Assign One User Error
Assign One User Error

Approve Action

The approve action will:

  • Prevent the next state if all users have not approved
  • Update the Approved By field
  • Update the Awaiting Approval From field
  • Clear Approved By and Awaiting Approval From  fields if all users have approved

The String Type for this action is:
sandbox.BusinessLogic.Workflows.MultiApprove.Approve, sandbox.BusinessLogic

Approve Action Code:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using System;
using System.IO;
using sandbox.BusinessLogic.Helpers;
using Sitecore.Workflows.Simple;
using Sitecore.Security.Authentication;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using sandbox.BusinessLogic.Models.sitecore.templates.Common.Workflows;
using Sitecore.Web.UI.Sheer;

namespace sandbox.BusinessLogic.Workflows.Multiapprove
{
    public class Approve
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            ProcessorItem processorItem = args.ProcessorItem;
            if (processorItem == null)
                return;

            // this is the context item
            var item = args.DataItem;

            // the active user
            string activeUserName = AuthenticationManager.GetActiveUser().Name.ToLower();

            // required Approver List
            List<string> requiredApprovers = new List<string>(item.Fields[IMultiApproveConstants.Sitecore_UsersFieldName].Value.ToLower().Split('|'));

            // update fields if this user is an approver
            if (requiredApprovers.Any(str => str.Contains(activeUserName)))
            {
                AddToApprovedBy(item, activeUserName);
                UpdateAwaitingApprovalFrom(item, requiredApprovers);
            }

            // if we're still waiting for approval, display a message telling the user they're not the last approver
            if (!String.IsNullOrEmpty(item.Fields[IMultiApproveConstants.Awaiting_Approval_FromFieldName].Value))
            {
                // not all users approved
                SheerResponse.Alert(
                    "You have approved this item, however, the following users still need to approve this content:\n"  
                    item.Fields[IMultiApproveConstants.Awaiting_Approval_FromFieldName].Value, false, "Approval");
                
                // abort the pipeline, because we don't want to reach the "Next State" execution
                args.AbortPipeline();
            }
            else
            {
                // clear Approved By and Awaiting Approval From fields
                // as we should clear it before the item is edited again
                ClearApprovals(item);
            }

        }

        private void AddToApprovedBy(Item item, string user)
        {
            if (ContainsUser(item.Fields[IMultiApproveConstants.Approved_ByFieldName].Value, user)) return;
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Approved_ByFieldName] = AddUserToMultiline(item[IMultiApproveConstants.Approved_ByFieldName], user);
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }
        }

        private void UpdateAwaitingApprovalFrom(Item item, List<string> requiredApprovers)
        {
            StringBuilder awaitingApprovalUsers = new StringBuilder();
            List<string> approvedBy = new List<string>(item.Fields[ IMultiApproveConstants.Approved_ByFieldName].Value.Split('\n'));

            foreach (string approver in requiredApprovers)
            {
                if (!approvedBy.Any(str => str.Contains(approver)))
                {
                    awaitingApprovalUsers.AppendLine(approver);
                }
            }
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Awaiting_Approval_FromFieldName] = awaitingApprovalUsers.ToString();
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }
        }

        private string AddUserToMultiline(string multiline, string user)
        {
            StringBuilder sb = new StringBuilder(multiline);
            sb.AppendLine(user);
            return sb.ToString();
        }

        private Boolean ContainsUser(string multiline, string user)
        {
            using (StringReader reader = new StringReader(multiline))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    if (line == user)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        private void ClearApprovals(Item item)
        {
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Approved_ByFieldName] = "";
                item[IMultiApproveConstants.Awaiting_Approval_FromFieldName] = "";
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }

        }
    }

}

Screenshots:

This is the notification you will receive if you’re not the last required approver.

Approval Notification
Approval Notification

The item is updated, so all users can easily see what approvals are still required.  You can see that I have approved it.  Note: I am an admin during this test.  Normal content editors would not have the option to edit these fields at this point.

Test News Page
Test News Page

We’re done! Or are we?

This is a working proof of concept.  Ideally you won’t have hard-coded strings or display unnecessary users.  Maybe we want to hide the commands to those users who have already approved, that’s for another post!  Enjoy!

Built on Sitecore 8.0 Update 4

Sitecore Workflow – Suppressing Comments

When creating your own workflows, you may not always want to require your users to comment.  Sitecore provides an option to Suppress Comment on the actions you create, however checking this box didn’t seem to have any effect .

So out of curiosity, I opened up the Sitecore Kernel with trusty DotPeek.

Looking for the field “Suppress Comment”, I found that it was being checked in Sitecore.Workflows.BasicWorkflowCommandAppearanceEvaluator

Solution

Suppress Comment

Applying this Appearance Evaluator Type to the Command allows this check to take place.

  1. Make sure you’ve selected the Suppress Comment checkbox
  2. On you Command, locate the Field Appearance Evaluator Type
  3. Insert: Sitecore.Workflows.BasicWorkflowCommandAppearanceEvaluator, Sitecore.Kernel

Using the LinkedIn JavaScript API

I was recently charged with a project where I needed to get user information from LinkedIn and I decided to use their JavaScript API.  Unfortunately, most of their examples were not very useful for what I was trying to do.

What Am I doing?

The goal was to pre-fill a long registration form and then display willing registrants on an attendees page.  This was convenient since I could take the user’s LinkedIn photo URL.  In this post, I will only go over how to get the values from LinkedIn.

Create the Application

The first step is to create an application.  Head over to https://www.linkedin.com/secure/developer. (LinkedIn account required!)  I’m not going to walk you through the form process, but I’d like to point out a few important spots.

  • The Application Name will be displayed to the user, so name it accordingly
  • Make sure to check the appropriate scopes according to which data you will need. For this example, I have selected r_fullprofile, r_emailaddress, and r_contactinfo. They provide a well-documented section for the profile fields, which should help you define your scope.
  • JavaScript API Domains – make sure you include your development domain, e.g. localhost.

Once you hit save, you’ll be presented with an API Key.  Save that, you’ll need it later.  If you forget, it will be listed on the application page.

The Fun Stuff

Initialize the LinkedIn JavaScript API, and paste in your API key.  Since we defined the scope at the application level, you do not need to define it here, but you can if you want.

<script type="text/javascript" src="http://platform.linkedin.com/in.js">
api_key: yourApiKey
onLoad: onLinkedInLoad
authorize: true
</script>

Now within another script tag, add the authentication event:

<script type="text/javascript">
function onLinkedInLoad() {
IN.Event.on(IN, "auth", onLinkedInAuth);
}

Now we make the API call, listing the fields we’d like to pull back.  Again, refer to the profile fields to see all the available fields and which fields are collections.

function onLinkedInAuth() {
IN.API.Profile("me")
.fields(["firstName", "lastName", "positions:(title,company)", "pictureUrl", "publicProfileUrl", "emailAddress"])
.result(function (result) {
displayProfileData(result);
});
}

In this display function, I’m using jQuery to set the values I’ve pulled back.  You will notice I am setting the positions field to 0, as I only care about their current job. In my case, I do not care or need to iterate through their past positions and companies.

function displayProfileData(profile) {
var profile = profile.values[0]; $('#firstName').html(profile.firstName); $('#lastName').html(profile.lastName); $('#title').html(profile.positions.values[0].title); $('#company').html(profile.positions.values[0].company.name);
$('#email').html(profile.emailAddress); $('#pic').attr('href', profile.publicProfileUrl);
 $('#pic img').attr('src', profile.pictureUrl);
$('#pic img').attr('alt', profile.firstName + " " + profile.lastName);
}

Toss this in the body of your HTML.  This is the button you’ll use to initiate the LinkedIn popup authentication form.  I have seen people add their own CSS styles to it to customize it, but I personally will not do that.  On a LinkedIn forum post, one of the developers states that LinkedIn would like to have a consistent experience for their users across all sites.  My only other concern is if they decide to change their styles one day, you will have a broken button.

<script type="IN/Login"></script>

This is the HTML we are going to shove the data into.  In my own project, I put these values into the input fields, but for the purpose of this post, we are going to do it this way.

<div id="firstName"></div>
<div id="lastName"></div>
<div id="title"></div>
<div id="company"></div>
<div id="email"></div>
<a id="pic" href="#"><img src="" alt="" /></a>

It’s Alive!

And that’s it!  If you did everything correctly, you should see a login button.

button

When clicked, LinkedIn will notify the user to which data you’ve requested access.  So, make sure you only select the scope’s you will need.

login

Once you allow access, this should be the result: (obviously with your beautiful mug instead of mine).

2013-10-10_0005