Automating item unlocks when deleting Sitecore users

Working with a large team of content editors can present us with difficulties, and one of these problems presents itself when we delete a user. If a user has locked items, deleting the user still leaves their items locked.

Sitecore contains a rule /sitecore/system/Settings/Validation Rules/Item Rules/Security/Locked By Non Existing User and we can use this item rule to give us a visual indicator of which items are locked by none existing users. This can be applied globally by adding it to our Global Rules item found at /sitecore/system/Settings/Validation Rules/Global Rules item.

So we can see which items are locked, and as an individual a content editor can unlock their own files by selecting the ribbon option ‘My Items’ and then using ‘Unlock All Items’ which calls the a method in the Sitecore.Shell.Applications.WebEdit.Commands.UnlockAll class. However, this method executes in the context of the current user rather than our non existing user who has locked the items. So while this method isn’t suitable for what we’re looking for, we can use something similar.

Upon deleting users Sitecore calls the user:deleted event so we need to create a handler for this event. You can see this handler being patched in Helix Base, and the source used by the event but below is the source for the handler:

    public class RemovedUserEventHandler
    {
        public void OnUserDeletedUnlockFiles(object sender, EventArgs args)
        {
            if (EventDisabler.IsActive)
                return;

            var objList = new List<Item>();
            var userName = Event.ExtractParameter<string>(args, 0);

            Assert.IsNotNullOrEmpty(userName, "User name was null or empty");

            var lockedItems = Client.ContentDatabase.SelectItems($"search://*[@__lock='%{userName}%']");

            if (lockedItems == null || !lockedItems.Any())
                return;

            foreach (var lockedItem in lockedItems)
                objList.AddRange(lockedItem.Versions.GetVersions(true).Where(version =>
                    string.Compare(version.Locking.GetOwner(), userName, StringComparison.OrdinalIgnoreCase) == 0));

            ProgressBox.Execute(nameof(RemovedUserEventHandler), "Unlocking items", "Network/16x16/lock.png",
                UnlockAllItems, "lockeditems:refresh", Context.User, objList);

            SheerResponse.Alert($"Successfully unlocked {objList.Count} item(s) checked out by {userName}",
                Array.Empty<string>());
        }

        private static void UnlockAllItems(params object[] parameters)
        {
            Assert.ArgumentNotNull(parameters, nameof(parameters));

            if (!(parameters[0] is List<Item> parameter))
                return;

            var job = Context.Job;

            if (job != null)
                job.Status.Total = parameter.Count;

            foreach (var lockedItem in parameter)
            {
                job?.Status.Messages.Add(Translate.Text("Unlocking {0}", (object)lockedItem.Paths.ContentPath));
                lockedItem.Locking.Unlock();
                if (job != null)
                    ++job.Status.Processed;
            }
        }
    }

Which we then patch in to the event:

  <sitecore>
    <events>
      <event name="user:deleted">
        <handler type="Helixbase.Feature.ItemUnlock.Events.RemovedUserEventHandler, Helixbase.Feature.ItemUnlock" method="OnUserDeletedUnlockFiles" />
      </event>
    </events>
  </sitecore>

Upon deleting users, you should now see their files automatically unlocked.

You can also find this source on GitHub Gist.

Edit… since writing this blog post Michael West has created a useful script for SPE to transfer ownership from one user to another. This can be used in situations where you wish to maintain a record of a previous users locked items.

6 thoughts on “Automating item unlocks when deleting Sitecore users

  1. Just a word of warning about using Xpath – if you have *a lot* of content items/using buckets then this query may take a very long time to execute and lock up the frontend. Unfortunately by default the `Lock` field is excluded from being indexed so you can’t use the ContentSearch API without putting that field back into the index…

    Like

  2. Yes I think so although I’d be interested to run some tests in the XPath builder just to double check no hidden treats are being missed. ‘search:’ is used in the Sitecore UnlockAll class and I practically used the same source but decoupled it from executing in the context of the current user. Refactoring/replacing that class was another option

    Like

    1. Yeah… in this case you might find that `fast:` performs much better (as it is direct to SQL) rather than via Sitecore caches… since the code you are running is essentially a one-off (most of the time) you probably don’t care about putting items in caches. If you try this in the XPath builder then remember to clear caches (test with both, second run of regular xpath will be faster than first because of this)

      Like

  3. Ah I see, I thought ‘fast’ caused issues when used in an expensive way but good point, this will only be executed sparingly on a CE box. I’d be interested to know why Sitecore didn’t opt for ‘fast’, will do some digging…

    Like

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s