Wednesday, December 19, 2018

Sitecore Icon Search Chrome Extension: What's New in v1.2

Back in March of this year, I created and released the Sitecore Icon Search Web Application.  With generally well-received reception - I  decided to expand the idea by building a version of the app as a Google Chrome Extension.  Last Halloween, v1.0 of the Sitecore Icon Search Chrome Extension was born and released.
Like any personal project, I'm inclined to continue making this a better, more useful experience for developers who are actively building templates in Sitecore as time goes by.

In versions 1.0.0 through 1.1.0, I resolved some minor bugs (specifically when utilizing the Template Manager - Sitecore is IFRAME inception) as well as working through a data refactor in order to load the icons table significantly faster than originally released.

So what's new?


The process prior to v1.2 required users to select/focus on the Icon field's text box under the Appearance section of an item prior to opening and selecting an icon from the extension for it to automatically populate the field.  In version 1.2+, the extension can now configure, change, and save icon selections in Sitecore automatically - which should streamline the process even further.

In other words, there's a new user process:
Simply click on an item in Sitecore, open the Sitecore Icon Search Chrome Extension and select the desired icon.

Behind the scenes, the extension automatically:

1) Enables the Standard Fields checkbox under the Views tab in the Ribbon (if it's not already enabled) as a means to access the Appearance section.

2) Sets the Icon field under the Appearance section to the selected icon (user does not need to select the Icon field).  A copy of the selected icon's relative path will still be copied to the clipboard.

3) Saves the item.

I often wonder: Why isn't this built into Sitecore?

If a user prefers the original workflow (pasting the copied icon's relative path into the Icon field manually), the auto-set and auto-save feature can be disabled by unchecking a checkbox in the extension's options.


When this option is disabled, the extension will skip the auto-set and auto-save step and only copy the relative icon path to the clipboard:

What's even greater is that the extension and process within works across ALL VERSIONS OF SITECORE (tested as far back as 6.5 and as recently as 9.1)!

 I'm looking forward to continually enhancing this project into 2019.  If you have any issues or feedback, or ideas -- please please PLEASE leave a comment below.

If the extension has been useful for you, please give it a rating in the Chrome Webstore 😁

Thursday, November 15, 2018

Which Version of Sitecore Is This Again?

As someone working with multiple clients spanning various Sitecore versions, I often lose track of which versions of Sitecore I have installed 😑   Sometimes, I want to verify if an issue I've come across is a known issue in that version. Other times, I may be providing information about possible upgrade opportunities.

In any case, there are several approaches to identifying which version of Sitecore a site is using.

The Sitecore Shell

If you have access to the Sitecore shell, head to the Content Editor, click the System Menu button and select License Details.



If your Sitecore version is v 7.5 or lower, you can also find the version on the log in screen (update name not included):



Sitecore.Version.XML

Accessing /sitecore/shell/sitecore.version.xml is one of the easiest and quickest ways to find out version details - and it's always available on the CM server (Sitecore Knowledgebase: How to identify the Sitecore version and installed components)


Again, you're left on your own to find the update name yourself.

Sitecore.Kernel.dll

If you have access to the file system, you can navigate to the /bin folder, find the Sitecore.Kernel.dll and open the file's Properties.  Under the Details tab, the Sitecore Product Version will be visible here.



One caveat to this approach is that starting in Sitecore 8.2 (Initial Release), the product version listed in the Sitecore.Kernel.dll start with version 10.0.0.5597 - making it a little more difficult to quickly identify the Sitecore Product Version:

8 = 10.  Math.

Luckily, this Sitecore Stack Overflow Answer has the mappings you'll need.
Sitecore has provided additional information on their Versioning Policy page regarding their approach to product versioning.

Sitecore PowerShell Extensions

If you have access to the CMS and SPE is installed, running the $PSVersionTable command lists the Sitecore build version. Running $PSVersionTable.BuildVersion will display the Major, Minor, Build, and Revision numbers.  


This information is pulled directly from the Sitecore.Kernel.dll.
Referencing the previously mentioned Sitecore Stack Overflow Answer will give you the product's update name.  




Identifying Local Sitecore Versions with PowerShell

Initially, I wanted to come up with a way to identify every version of Sitecore I have installed on my local machine.  I used PowerShell (naturally)  to write a script that lists all installed Sitecore sites registered in IIS and their respective mapped update name based on an inspection of the Sitecore.Kernel.dll.

Here's the script:


Here's the result:

Listing all my local Sitecore instances with their respective 
With the release of the Sitecore Icon Search Chrome Extension, I was super happy with my experience building a Chrome extension - so I decided to give it another go and build the Sitecore Version Detector Chrome Extension.

That's right. The Sitecore Docs site runs on 7.2 Update-4.  Time for an upgrade? 
The extension is simple - built in pure HTML and Javascript - and checks two things:

Is the site using Sitecore? 

We can determine if a site is built on Sitecore by checking for the response on the site's /layouts/System/VisitorIdentification.aspx URL.  This file has been around since the days of DMS/OMS and is central to Sitecore's tracking mechanism. It's also always accessible from delivery servers!

If it's there, we can assume that the site is using Sitecore.

Which version of Sitecore is being used? 

Using the second method listed at the beginning of this post, the extension requests the current tab's /sitecore/shell/sitecore.version.xml URL for an XML response.  This file, while not always accessible on delivery servers, is often left exposed 💀

If it is accessible, we can parse the Major, Minor, Build and Revision values from the XML, then map to the appropriate update version name.

If a Sitecore version is detected, it will display the full revision (with the update name) and link to its respective http://sdn.sitecore.net or http://doc.sitecore.net version page.

The version's Mainstream Support End Date also displays below the version with a link to the Sitecore Product Lifecycle page 😏

Note: While this may often determine versions for live Sitecore sites (especially older versions of Sitecore), not all environments provide the means to determine the version of Sitecore it runs on as an end-user.  

Enjoy!


Thursday, September 6, 2018

Azure PaaS Sitecore Logs using AzureAILogs.html

When hosting Sitecore XP solutions on Azure PaaS – you’ll quickly find out that accessing and viewing Sitecore logs is different than you may be used to.

Sitecore has a great Knowledgebase article available (Access logs and diagnostics data in Sitecore XP on Azure Web Apps) which describes a couple approaches for obtaining the Sitecore logs on a PaaS environment using both Azure Application Insights or FTP.

Grabbing recent log files is pretty straightforward once you’ve configured FTP credentials to access your Sitecore instance.  Simply FTP into the instance (Filezilla is my favorite), navigate to /LogFiles/Application, sort by date, and download the relevant logs.



My preferred approach, however, is utilizing the AzureAILogs.html file that Sitecore provides within the “Using Azure Application Insights REST API” section of the KB article:


Enabling the API Key also requires a few clicks in the Azure portal, but the instructions for enabling this feature are outlined well – and it only takes a couple of minutes to complete.

If you download and open the HTML file locally, the output is pretty sweet:


Once you input the Sitecore app's App Insights App ID and API key, clicking the 'Update Roles' button retrieves the relevant roles for the application (CM, CD, Reporting, and Processing).

Having obtained the roles, you can select one or multiple roles, configure any specific options (recency // checkbox to only show errors // number of entries), and click the 'Get logs from AI' button to make the call.

You’ll end up with filtered and descending log entries you’d typically get from Sitecore logs:

The bonus here is the 'Download logs' button will download the log files – which are also compatible with Sitecore Log Analyzer.

This is super handy as-is, but I also wanted to make it a little easier to access online.  Specifically:

  1. I don’t want to input in the App ID and API key every time I want to use this tool.
  2. I want to access this from our CM environment, similar to any other admin page.
By adding a few old-fashioned lines of Javascript to my copy of the AzureAILogs.html file (just before the requestHandler function), the page was able to accept two URL query parameters (‘appid’ and ‘apikey’) to prefill the two input values.


For example:
AzureAILogs.html?appid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&apikey=0000000000000000000000000000000000000000

In order to access this from our CM Authoring environment, I simply added the HTML page to the /sitecore/admin directory.

Once I was able to hit the page from the server, I tacked on our App ID and API Key to the URL and bookmarked the page.   Voila - simple yet effective.

Of course, this is just one of many ways to view Sitecore logs on a PaaS environment - but I found it to be one of the most useful.

Friday, August 17, 2018

Basic Sitecore Audit Trail with Powershell


Update: This report has been included in the latest version SPE v5.0: 
https://github.com/SitecorePowerShell/Console/issues/1033


Here's a question posted to our internal Sitecore Slack channel yesterday:

The answer was 'no'.

This isn't the first time our clients have requested 'Audit Trail' functionality before, either. And we're talking about the basics here:

  • • When did a user log in?
  • • When did a user log out?
  • • Who published what, and when?
  • • When was this item's workflow executed and by whom?

Unfortunately, there isn't an 'out of the box' solution to easily obtain this data.
The most common "solution" has always been to utilize the Sitecore Log Analyzer's Audit tab - which is great for developers, but not for CMS users.

This tool has saved my sanity so many times. 

Others in the community have shared promising solutions in the past, most of which have become unsupported over time. The Sitecore Audit Trail Marketplace module, for example, was last updated in 2015 and only supports up to version 7.5 (fun fact: mainstream support for this version ended in December 2017!).  Additionally, the setup was heavy - requiring a custom database connection. It also, unfortunately,  had it some known issues associated with it.
While the feature set was fairly extensive, a simpler solution still wasn't available.

The Advanced System Reporter also has an audit feature, but again the last time this module was updated was in 2015 and supports up to version 8.0.

And then it hit me.
POWERSHELL!!!!

You've got to know this by now...my default reaction to any problem is to ask myself: "can this be done using Powershell?".

The idea was to create a Powershell report that consumes the accessible Sitecore log files which already contain the data we need.  The report should include all lines marked as 'AUDIT'' and split into columns.  The user should be able to provide a date range to narrow down the audit.  

Powershell would automatically provide the rest: keyword filtering, sorting, exporting, etc. 

Let's script it.

In order to allow a user to configure a date range, we need to create an interactive dialog

The result:



If the user clicks cancel, we'll want to abort the whole operation.
Otherwise, we'll proceed and create the properties needed for our final ListView.
We'll call a function named Get-Audit where our logic will process.


Within the Get-Audit function, we first obtain the location of the Sitecore log folder (UPDATE: We'll use $SitecoreLogFolder instead. SPE creates a variable $SitecoreLogFolder and resolves the path for you: https://doc.sitecorepowershell.com/working-with-items/variables).  We'll the pull the log files from the resolved log path and filter out everything but the standard log. files:

We'll make sure the user has selected a date range and filter the list of files using those inputs. If a user didn't set a date range, we'll simply use the original file set - which will display the most recent entries. While we're at it, we'll define a 'regex' string used to filter only AUDIT items, and an array object to hold our line objects:


We'll start a loop based on our file count. From there, we can get the contents of the file.


We need a way to include the date for each line object (specifically for our final sorting), so we'll build out a simple date string which we'll use later.


We'll now loop through each line in the file and check if it's marked as 'AUDIT'. If the condition matches, we'll append the simple date string we created previously to the beginning of the line string and sanitize the string by removing double-spaces. In some cases, the audit line will contain ManagedPoolThread #XX instead of an ID. We'll sanitize this as well.

The line now looks like this:
8/17/2018 8324 00:13:47 INFO AUDIT (sitecore\Anonymous): Logout

We'll use the space between each data point to our advantage and split each property into individual objects. The username requires some general sanitation, but more importantly, we'll build out one more DateTime object which we'll use to sort the lines before returning our array. 


Finally, we sort and return our array for the table to process.


Final Script

Putting it all together, our script looks like this:


When the script runs, we get a pretty clear view of who's been doing what:

As always, feel free to use, modify, and build on it as you see fit. This has only been tested on a handful of environments so far, so bugs are still possible.
If you do spot any issues, feel free to report them in the comments - or make the necessary changes and submit a pull request to https://github.com/strezag/sitecore-audit-trail-powershell.

Happy SitecorePowershelling!

Tuesday, July 24, 2018

Custom Sitecore Field: Google Maps Autocomplete

Maps are everywhere.  Most modern web designs often include some kind of map functionality whenever points of interest are involved.  If you've ever built a template that houses location data (stores, events, etc), you'd likely included latitude and longitude data points as Single-Line Text fields on the relevant Sitecore template.

However, this approach still forces the Content Author / Marketer to discover the latitude and longitude points based on an address themselves.

While this isn't too difficult to manage, we wanted to make eliminate this step - and make it easier for the author to ensure they're getting reliable latitude/longitude points every time without the need for any external lat/long discovery.

We've seen other examples (Google Map Location Picker Field or  this older module) exist where maps selectors are used (many of which are pretty old now!), but none that strictly use the Google Maps Autocomplete API.
If you're looking for some code on how to achieve this - you've come to the right place!

GOAL

Create a custom Sitecore Field Type integrated with Google Maps Places Autocomplete API that automatically retrieves and stores Latitude and Longitude points based on an inputted address.

Injecting Scripts to the Content Editor

In order to consume the Google Maps Autocomplete API, our page will need to render a script tag referencing the API in the Content Editor.  Luckily, Sitecore allows us to inject custom styles and scripts to the Content Editor as needed.  In this case, there are two script tags we'll need to add:
    1. The Google Maps Autocomplete API library
    2. A custom Javascript file with methods that initialize, geolocate, and fill in the Latitude and Longitude field

Our patched config entries include a new pipeline:

In our pipeline, we loop through the resource definitions set in the config and process new script tags to be added when the content editor loads:

New Template Field Type Item

Our Autocomplete Address Template Field Type is defined in the Core DB:


The Control field value is set to customfieldtypes:AutocompleteAddress. This means two things:
  1. We'll need to register customfieldtype as a new controlSource
  2. A new AutocompleteAddress class which inherits from the Sitecore.Web.UI.HtmlControls.Control class. 

Template Field Type Code and Config

The config entry to define a new control is pretty straightforward:


The corresponding control code:
In this approach, our Control code actually creates three text boxes on the fly (Address, Latitude, Longitude).  The Address text box control will act as the autocomplete which interacts with the API.

Once an address is selected from the autocomplete selections - the custom JS retrieves the address's latitude and longitude points.  These lat/long values will be mapped to the two additional text boxes, then stored as a NameValueCollection string in the raw value of the Autocomplete field type in the following string format:

Address=2104 North Clark Street, Chicago, IL, USA&Latitude=41.920385&Longitude=-87.637572

To hook the Address textbox to our custom script we're simply adding an onfocus attribute 'javascript:autoCompleteAddress.initAutocomplete' to the Address text box control.

The custom JS handles the rest of the interaction to call the API and populate the Latitude and Longitude textbox controls with the right values.


Utilizing the Field Value

When it's time to use the latitude/longitude points on the front-end, we utilize a StringExtension method to parse the necessary values.

Full source for this implementation can be found here:
https://github.com/strezag/GoogleMapsAutocompleteCustomSitecoreField

While this was built using the latest version of Sitecore (v9 Update-2 at the time of this post), we were able to verify this on a local copy of 8.2.  My guess is that this probably would work for even earlier versions (even if it's just a few tweaks).

Feel free to grab a copy and modify it to your own specs!

Thursday, April 12, 2018

Sitecore 9 Machine Prerequisites Check with PowerShell

Part of setting up Sitecore 9 for the first time requires you to install and confirm the machine's hardware, software, windows features, and SIF prerequisites are all met.

Going through the prerequisite checklist can be a bit daunting when planning to set up Sitecore 9 on an existing workstation (where you may already have some of the prerequisites met) or on a VM/new workstation (where you'll need to install new software) to ensure Sitecore 9 runs smoothly.

To make this process a little easier, I've written a PowerShell script that checks for the prerequisites as defined in the  Sitecore 9 Installation Guide and Sitecore Compatibility Table and reports if you're missing anything. 

As a bonus, it'll apply the IIS_IUSRS Modify permissions to folders defined in section 2.3.1 File System Permissions of the Sitecore 9 Installation Guide wherever applicable, ensure the JAVA_HOME path Environment Variable is set (helps with Solr), and register the Sitecore Powershell Gallery.

The IsThisSitecore9Ready.ps1 script helps verify:

  • - Hardware requirements (are there enough cores and RAM?)
  • - Operating system compatibility (Windows Server 2012 R2 (64-bit) / 2016 (32-bit/64-bit) / Windows 10 (32-bit/64-bit) / Windows 8.1 (32-bit/64-bit)
  • - System Folder Permissions (Sets IIS_IUSRS Modify permissions to folders defined in section 2.3.1 File System Permissions of the Sitecore 9 Installation Guide)
  • - IIS version (8.5+)
  • - NET Framework (4.6.2+).
  • - SQL Server 2014 SP2 or 2016 SP1
  • - JavaRuntime (and confirms JAVA_HOME path Windows EnvironmentVariable is set)
  • - Checks and registers SIF (SitecoreInstallFramework and SitecoreFundamentals)

When you run the script, it's run through a series of checks




Please note that this does not cover the following sections from the Sitecore 9 Installation Guide, so you'll want to ensure these loose ends are wrapped up after:

  • - 2.3.2 Prerequisites for the Sitecore Installation Framework
  • - 2.3.3 Enable Contained Database Authentication 
  • - 2.3.4 Installing Solr

You can grab a copy of the script here: https://github.com/strezag/IsThisSitecore9Ready


As always, feel free to use and modify the script to fit your needs.
Leave a comment if you have any suggestions or recommendations, too!


Good luck!

Friday, March 30, 2018

Quick Tip: xPath Builder Chrome Bookmark

Whenever I need to access Sitecore's xPath Builder, I typically do a Google search to find any random blog post that provides the URL path (no, I don't plan to memorize that URL path any time soon ðŸ˜Š).

However, in order to circumvent this, I now use a Google Chrome Bookmark that uses JavaScript to automatically take me to the xPath Builder.

Simply add this to the content of a new bookmark:

javascript:(rel=>{location=rel.startsWith('/')?`${location.protocol}//${location.host}${rel}`:`${location.protocol}//${location.host}${location.pathname}/${rel}`})('/sitecore/shell/default.aspx?xmlcontrol=IDE.XPath.Builder')
While on any Sitecore site - hit the bookmark and you're there!

Enjoy!

Monday, March 5, 2018

Sitecore Icon Search Web App

While the #SCHackathon was fully underway last weekend (holy cow, just following the Twitter diaries was intense), I was inspired to also stay up all night building something I hope will be useful for Sitecore developers moving forward.

I'll preface this with a quote from Phil Wicklund and Jason Wilkerson's book - Professional Sitecore 8 Development:

"It's often said that the hardest part about Sitecore projects is choosing the right icon."
As a best-practice, Sitecore recommends developers "use icons wherever applicable to facilitate visual differentiation."

But finding the right icon for your Sitecore templates ain't easy.
It unfortunately never has been.

Why?

  1. Opening the Icon selector for the first time takes a while

  2. There are 8,758 icons split across 25 categories - with no out-of-the-box search capabilities  -spotting the right icon can end up being time-consuming.

  3. We've gotten some great community traction in the Marketplace - however, the available modules often compatible with specific versions of Sitecore.  (Kudos to the following module developers - these are not irrelevant).


Introducing: Sitecore Icon Search


Sitecore Icon Search was built to help Sitecore developers quickly identify the right icon for their Sitecore templates - allowing them to focus on creating great visual experiences for Sitecore CMS users.  

There are no package installations or versions to keep up with - simply visit this tool from your web browser, find the best icon, then copy and paste the relative path into the template's Icon field.

demo

This tool isn't currently mapped to any custom domain as it's using a free Azure AppService tier.  Given a positive community response, I'd be happy to upgrade the tier and map a custom one for additional ease of access.

What do you think it should it be?

Feel free to leave your suggestions (or general thoughts) in the comments below!

Happy Sitecoring!



Thursday, February 22, 2018

Real-time, Filterable Trailing/Rolling Sitecore Logs with PowerShell

Sitecore troubleshooting usually goes something like this:

1) Do something on the site
2) Navigate to the /logs directory
3) Sort the files by date
4) Open the latest log file
5) Scroll to the bottom to find the most recent entries
6) Repeat as needed

Sometimes seeing what's being written to the logs in real-time makes a lot more sense during development and troubleshooting sessions.

While there are plenty of note editors out there that support log tailing (Notepad++, SnakeTail, or even just the Command Prompt) there are always caveats (lack of filtering, finding the latest log file) - I wanted a simpler experience, with slightly more advanced options that can be used across any of my local Sitecore instances - without it being too complicated to use.  More specifically, you may not always need all the INFO or WARN messages during a real-time monitoring session.

And since I've become a PowerShell addict throughout the last couple of years...

I ended up with a couple of pretty handy PowerShell script I've been using in my day-to-day development that'd I'd love to share with you.

Actually, there are two scripts!

RollingSitecoreLogs.ps1

The first script can be placed anywhere on your machine.  When you run it, you'll be prompted with a Directory Selection dialog.  Simply navigate to the /logs folder for any Sitecore instance.


Once you've selected the directory, the script prompts you with a second dialog form which allows you to filter in/out INFO, WARN, ERROR entries.


Hit OK, and the log starts rolling in!


RollingSitecoreLogs-RelativeDirectory.ps1

The second script is far less involved and runs from your /logs directory (just drop it right in).


It determines the current directory (based on where the script itself is located), finds the latest log.* file, gives you the option to choose which messages should filter in, and begins a rolling session.

The idea is - since you're probably navigating to your logs directory to pick up on the latest log anyway - once you have this script added to our /logs directory you can simply run the script and have it start a real-time log monitoring session.  Simple as that.

Please note - this only considers the primary log.* files - so it's not built for Crawling.log, Search.log, etc. Perhaps additional options can be added at some point in the future to chose or maybe even merge them somehow.

Feel free to grab whichever script suites your needs here:
https://github.com/strezag/sitecore-rolling-logs-powershell

As always, let me know what you think in the comments.

Happy log-sifting



Monday, January 22, 2018

Keep Performance Counters Enabled After Installing Sitecore 9 Using PowerShell

After figuring out the Sitecore Installation Framework and successfully installing Sitecore 9 - I was finally getting to enjoy that fresh CMS smell.

I decided to take a peek at the logs (which to my surprise were now located inside the Website root's App_Data folder) and was greeted an error:


 ManagedPoolThread #0 22:19:56 WARN Failed to create counter 'Sitecore.System\Events | Events Raised / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #1 22:19:56 WARN Failed to create counter 'Sitecore.System\IO | File Watcher Events / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #2 22:19:56 WARN Failed to create counter 'Sitecore.System\Logging | Errors Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #3 22:19:56 WARN Failed to create counter 'Sitecore.System\Logging | Fatals Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #4 22:19:56 WARN Failed to create counter 'Sitecore.System\Logging | Informations Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #5 22:19:57 WARN Failed to create counter 'Sitecore.System\Logging | Warnings Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #6 22:19:57 WARN Failed to create counter 'Sitecore.System\Logging | Audits Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #7 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Methods Invoked / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #8 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Objects Created / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #9 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Objects Not Created / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #10 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Types Resolved / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #11 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Types Not Resolved / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #12 22:19:58 WARN Failed to create counter 'Sitecore.System\Threading | Background Threads Started / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #13 22:19:58 WARN Failed to create counter 'Sitecore.System\Xml | Packets Created / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 3692 22:19:58 INFO HttpModule is being initialized  
 ManagedPoolThread #13 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Jobs | Jobs Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #12 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Pipelines | Pipelines Aborted / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #11 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Pipelines | Pipelines Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #10 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Pipelines | Processors Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #9 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Publishing | Items Queued / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #8 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Publishing | Replacements / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #7 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | File Cleanups / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #6 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Html Cache Clearings / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #5 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Publishings / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #4 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Reminders Sent / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #3 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Tasks Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  

Given that I was installing Sitecore 9 on a Windows 10 VM, I must have missed a requirement/prerequisite somewhere along the line - but this looked really familiar.

I've seen this before.

Oh yeah...

From https://kb.sitecore.net/articles/404548:
Possible solution:   A Sitecore application pool user has to be a member of the system “Performance Monitor Users” group to have access to the performance counters.
Adding the user to this group and restarting IIS should resolve the problem.

That's right - Windows Performance Counters!


Turns out, this is actually mentioned in the appendix of the Sitecore Experience Platform 9.0 Sitecore Experience Platform Installation Guide under Windows Performance Counters section, too (1 page before the end of the document!):


Sitecore XP contains a built-in functionality that reads and updates the Windows performance counters that you can use to monitor and troubleshoot the Sitecore application. This functionality requires access to Windows registry keys. 

So the obvious two options -

1) Set Counters.Enabled setting to false in \App_Config\Sitecore.config
Booorrring

2) Grant access by making the application pool identity a member of the built-in Performance Monitor Users group.

Instead of simply just disabling the counters, I wanted to see exactly what it takes to keep them on.

This should be as easy as adding the Application Pool Identity of my Sitecore 9 instance as a Performance Monitor Users group.  Unfortunately for me and my VM - the Local Users and Groups option wasn't available for me:
WHERE IS IT?!


Rather than figuring out why Windows 10 isn't showing that option - I figured, why can't this just be automated?

So in the spirit of scripting everything...

I put together the following post-installation script that assigns the environment's AppPoolIdentity account to the Performance Monitor Users group, then resets IIS for the changes to take effect.

All that needs to be configured is the $accountName variable - then simply run the script as in an elevated PowerShell window:



Write-Host 

# Configure this to the same value as the Sitecore Site Name (eg. sc90.local). 
# If you're using Network Service account, set to 'NT AUTHORITY\Network Service'
$accountName = "sc90.local"  
# Get the Performance Monitor Users group policy
$group = [ADSI]"WinNT://$Env:ComputerName/Performance Monitor Users,group"

# Create Account Object
$ntAccount = New-Object System.Security.Principal.NTAccount($accountName)
Write-Host "Account to add to Performance Monitor Users: $ntAccount"

# Translates the account name represented by the NTAccount object into another IdentityReference-derived type.
$strSID = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])

#Create the user
$user = [ADSI]"WinNT://$strSID"

try
{
        # Set user to group policy
 $group.Add($user.Path)
 Write-Host -ForegroundColor Green "$accountName has been successfully added as a member of the Performance Monitor Users group."
 
 # Reset IIS for changes to take effect
 iisreset
}
catch
{
     Write-Host -ForegroundColor Green "$accountName is already a member of Performance Monitor Users."
}


Write-Host "Process complete."
Write-Host 


After running this script, the logs no longer contained "Access to the registry key 'Global' is denied. / Sitecore has no necessary permissions for reading/creating counters" errors - indicating that Sitecore could now read the permissions.

Feel free to include this in your own set of post-installation steps and modify to fit your needs.

Happy scripting!

Wednesday, January 3, 2018

Sitecore Powershell: Valid Page URLs Report

Preface: I love the Sitecore Powershell Extensions Module, and I opt to use it every chance I get.


One of my clients had a simple request:
"Please provide a list in Excel of every single valid URL on the live global site, please." 

After running ScreamingFrog and obtaining a report with missing URLs (the final list returned was faulty – likely due to the software’s inability to hit specific links only available via AJAX rendered components) - we had a couple options on the table:

  1. Similar to functionality found in a Sitemap component, we need a simple ASPX page that loops through the content that filters out everything but the global English version, generate the URLs, trigger a web request to determine the URL's web status, and display it on the page (or create a Download button to get the list).  Code this, deploy it, etc.
  2. Do all of the above - but with Powershell - which happened to already be installed on the CMS

GUESS which I opted for? :)
Yeah!...you guessed it!

Let's get right into it.

Using the Get-ChildItem command and targetting a specific part of the content tree (explicitly using the Web DB), we get the initial list of English versioned items.
 $itemsWithMatchingCondition = Get-ChildItem 
          -Path web:'/sitecore/content/WebsiteName/Home' 
          -Language 'en' 
          -Version * 
          -Recurse 

With this specific implementation, I was lucky enough to have a stable template naming convention where all items using a template that ended with "Page" were always going to be...well...pages.
(Without this luck, I may have had to check if the item contained at least a main layout within the renderings).

To filter this, we'll use a simple IF statement with a LIKE operator against the initial item list's item:
 iif ($item.Template.Name -like $script:pageString)

Now that we have a list of page items we want to process, we need to generate the item's URL.

This handy function that sets the site context, configures the UrlOptions, and gets the URL via the LinkManager does just that:
function Get-ItemUrl($itemToProcess){
     [Sitecore.Context]::SetActiveSite("website")
     $urlop = New-Object ([Sitecore.Links.UrlOptions]::DefaultOptions)
     $urlop.AddAspxExtension = $false
     $urlop.AlwaysIncludeServerUrl = $true
     $linkUrl = [Sitecore.Links.LinkManager]::GetItemUrl($itemToProcess,$urlop)
     $linkUrl
}

Here's the fun part!

Per the requirement, we'll need to validate that the URLs Sitecore was generating were actually functioning.  Any non-functioning URLs (if any) shouldn't be included in the final report (only status code 200).

Powershell lets us make web requests - which we could then check the status of.
All we need to do here is pass in the URL we generated and expect a true or false value in return:

function IsValidPageStatus($urStr){
    $return = $false;
    $HTTP_Request = [System.Net.WebRequest]::Create($urStr)
    $HTTP_Response = $HTTP_Request.GetResponse()
    $HTTP_Status = [int]$HTTP_Response.StatusCode
    if ($HTTP_Status -eq 200) {
        $return = $true
    }
    else {
        Write-Host $urStr
        Write-Host "Response: " $HTTP_Status
        $return = $false
    }
    $HTTP_Response.Close()
    return $return
}

(Note: Any page URL that fails will be listed in the console after the script completes.)

After every URL goes through this check, we add the item to the array list:

if($isValidUrl){
      $script:itemIDsWithPassedCriteria.Add($item) > $null 
}

Finally, build out the report - which can then be exported via the Powershell ISE in CSV/Excel format:

if ($script:itemIDsWithPassedCriteria.Count -eq 0)
{
    Write-Warning "No page items found."
}else{
$props = @{
 InfoTitle = "Live Page Urls"
 InfoDescription = "Provides a list of all valid page URLs "
 PageSize = 100
}
    $script:itemIDsWithPassedCriteria|Show-ListView @props -Property 
       @{ Label = "Url"; Expression = { Get-ItemUrl ($_) } }
    Close-Window 
}


Here's the full script:

<#
.SYNOPSIS
  Provides a list report of all valid page URLs  
.AUTHOR
Written by Gabe Streza
#>
# Variables
$script:pageString = "* Page" #page string
function GetItemsWhichUsePageTemplate()
{
    $itemsWithMatchingCondition = Get-ChildItem -Path web:'/sitecore/content/WebsiteName/Home' 
                                                        -Language 'en' -Version * -Recurse 
    { 
        if ($item.Template.Name -like $script:pageString)
        {
            $linkUrl = Get-ItemUrl($item)
            $isValidUrl = IsValidPageStatus($linkUrl)
            if($isValidUrl){
                $script:itemIDsWithPassedCriteria.Add($item) > $null # The output of the Add is ignored
            }
        }
    }
}
function Get-ItemUrl($itemToProcess){
     [Sitecore.Context]::SetActiveSite("website")
     $urlop = New-Object ([Sitecore.Links.UrlOptions]::DefaultOptions)
     $urlop.AddAspxExtension = $false
     $urlop.AlwaysIncludeServerUrl = $true
     $linkUrl = [Sitecore.Links.LinkManager]::GetItemUrl($itemToProcess,$urlop)
     $linkUrl
}
function IsValidPageStatus($urStr){
    $return = $false;
    $HTTP_Request = [System.Net.WebRequest]::Create($urStr)
    $HTTP_Response = $HTTP_Request.GetResponse()
    $HTTP_Status = [int]$HTTP_Response.StatusCode
    if ($HTTP_Status -eq 200) {
        $return = $true
    }
    else {
        Write-Host $urStr
        Write-Host "Response: " $HTTP_Status
        $return = $false
    }
    $HTTP_Response.Close()
    return $return
}

$script:itemIDsWithPassedCriteria = New-Object System.Collections.ArrayList
GetItemsWhichUsePageTemplate

if ($script:itemIDsWithPassedCriteria.Count -eq 0)
{
    Write-Warning "No page items found."
}else{
$props = @{
 InfoTitle = "Live Page Urls"
 InfoDescription = "Provides a list of all valid page URLs "
 PageSize = 100
}
    $script:itemIDsWithPassedCriteria|Show-ListView @props 
                                                            -Property @{ Label = "Url"; Expression = { Get-ItemUrl ($_) } }
    Close-Window 
}
Write-Host "Done."

This took about 8 minutes to process a 2000 page site - which is good for a one-time run - but there are certainly some optimizations we should make if this was a report the client would use repeatedly in order to make it a bit snappier.  For this purpose, we're all set!

Feel free to grab this, tinker with it, and make it your own!

Let me know in the comments if this has helped - or if you have any additional recommendations.