How to hide menu items (reports, dashboard) from the global menu in Optimizely CMS

Reports section is available by default to all users who are members of the WebEditors group as the authorization policy of that MenuItem is set to 

AuthorizationPolicy = CmsPolicyNames.CmsEdit,

So basically Reports section comes always together with the Edit Mode. However, I can imagine that sometimes reports are only useful to only a few specific editors and can be hidden from all others.

Unfortunately it is not possible to configure this anywhere but thanks to Optimizely's great and powerful extensibility it is possible to accomplish that requirement with just a few lines of code:

public class CustomMenuAssembler : MenuAssembler
{
	private readonly IPrincipalAccessor _principalAccessor;

	public CustomMenuAssembler(IEnumerable<IMenuProvider> menuProviders, IHttpContextAccessor httpContextAccessor,
		IPrincipalAccessor principalAccessor) : base(menuProviders, httpContextAccessor)
	{
		_principalAccessor = principalAccessor;
	}

	public override IEnumerable<MenuItem> GetMenuItems(string parentPath, int relativeDepth)
	{
		var menuItems = base.GetMenuItems(parentPath, relativeDepth).ToList();

		if (_principalAccessor.Principal.IsInRole("HasAccessToReportsGroup") == false)
		{
			return menuItems;
		}

		var reportItem =
			menuItems.SingleOrDefault(x => x.Path.Equals("/global/cms/report", StringComparison.Ordinal));

		if (reportItem == null)
		{
			return menuItems;
		}

		// Add user group check here
		menuItems.Remove(reportItem);

		return menuItems;
	}
}

So we create a new MenuAssembler which inherits from the default one and we pass the additional IPrincipalAccessor reference to it.

In my example I created a custom user group called HasAccessToReportsGroup . If an editor does not belong to this group then he/she will not see the Reports menu item.

Last we need to register this custom menu assembler and we need to make sure we do it after the built-in cms initialization modules are finished. Let's add a custom initializable module:

[InitializableModule]
[ModuleDependency(typeof(InitializableModule))]
public class DependencyResolverInitialization : IConfigurableModule
{
	public void ConfigureContainer(ServiceConfigurationContext context)
	{
		context.ConfigurationComplete += (_, _) =>
		{
			context.Services.AddSingleton<MenuAssembler, CustomMenuAssembler>();
		};
	}

	public void Initialize(InitializationEngine context)
	{

	}

	public void Uninitialize(InitializationEngine context)
	{

	}
}

The same approach can be used to conditionally hide all other menu items, you just need to change the path.

Please find the list of other paths:

  • /global/cms/report
  • /global/cms/dashboard
  • /global/cms/visitorgroups

Editing enhancements for the EPiServer.Forms package

Together with Greg we created a little, yet powerful add-on that brings enhancements from EPiServer.Labs.BlockEnhancements to EPiServer.Forms based properties.

Editor is now able to edit every form inline, without the need to switch the editing context. Just double click any Form Element to edit its properties. Then use Inline Publish to public the Form Element.

You can install it from here:

https://nuget.episerver.com/package/?id=Advanced.CMS.Forms

The source code is available here:

https://github.com/advanced-cms/forms

The addon is MIT licensed. Feel free to use/contribute however you want.

Nice improvement for the Inline Editing feature

I have just pushed a nice little improvement that lets the editor to double-click a block and edit its properties straight away.

No context switching, no context menus or finding the rights commands, just double click an item in the Content Area and you're good to go!

More info here:

https://github.com/episerver/EPiServer.Labs.BlockEnhancements#inline-block-editing

v0.4.0 is available here:

https://nuget.episerver.com/package/?id=EPiServer.Labs.BlockEnhancements&v=0.4.0

How to change the delay between subsequent saves in EPiServer TinyMCE

By default we would save every two seconds (of course only if the content was changed in any way, the save is throttled for 2 seconds by default). 

Unfortunately it is not possible to adjust that value using the fluent server side api.

Hopefully, we can easily change the setting by inheriting the default xhtml descriptor and providing our own value.

[EditorDescriptorRegistration(TargetType = typeof(XhtmlString), 
                              EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
public class CustomXhtmlStringEditorDescriptor : XhtmlStringEditorDescriptor
{
	public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
	{
		base.ModifyMetadata(metadata, attributes);
		if (!metadata.EditorConfiguration.ContainsKey("dirtyCheckInterval"))
		{
			metadata.EditorConfiguration.Add("dirtyCheckInterval", 3000);
		}
	}

	public CustomXhtmlStringEditorDescriptor(ServiceAccessor<TinyMceConfiguration> tinyMceConfiguration) 
	           : base(tinyMceConfiguration)
	{
	}
}

 

How to support a locale with a specific regioncode, for example en-gb, en-nz or en-au

Apparently dojo has issues with their localization modules that makes it impossible to determine the correct culture info when you have two languages from the same locale registered at the same time. 

For example if your editors are from let's say New Zealand the because of this dojo bug https://bugs.dojotoolkit.org/ticket/17155 the language formatting would always fallback to the _ROOT_ nls for them (which is "en" in this case).

It's a very annoying issue as for example number and datetime formatting are different for English editors from US and from GB/NZ/AU.

One could try to solve it by changing the globalization of the whole site or by turning languages in admin mode on and off but in fact the Edit Mode is the only place where we need the change to be visible in.

If you add this simple initializable module to your site it would just affect dojo i18n module without touching the asp.net runtime.

using System;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.Shell.UI;
 
namespace AlloyMvcTemplates
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    internal class DateFormatModifier : IInitializableModule
    {
        private void DojoConfigOnSerializing(object sender, EventArgs eventArgs)
        {
            var config = (DojoConfig) sender;
            if (config.Locale.StartsWith("en"))
            {
                config.Locale = "en-gb";
            }
        }
 
        public void Initialize(InitializationEngine context)
        {
            DojoConfig.Serializing += DojoConfigOnSerializing;
        }
 
        public void Uninitialize(InitializationEngine context)
        {
            DojoConfig.Serializing -= DojoConfigOnSerializing;
        }
    }
}

The example above will make sure that all your English editors will always use the "en-gb" language tag so for example date/times would no longer be en-us formatted (default English formatting).

You won't have to change anything about the available locales in the My Settings screen, users can just select English and it would automatically use en-gb instead of the default en-us.

 

If you use the Forms addon then you will also have to do a little trick as the dgrid datetime formatter is set to a fixed pattern 'yyyy-MM-dd'

In order to change it please add the following module:

define([
    "dojo/_base/declare",
    "dojo/date/locale",
    "epi-forms/dgrid/Formatters"
], function (
    declare,
    locale,
    Formatters
) {
    return declare([], {
        initialize: function () {
            Formatters.dateTimeFormatter = function (value) {
                return locale.format(new Date(value), { datePattern: "dd-MM-yyyy", timePattern: "HH:mm:ss" });
            };
        }
    });
});

And register it in your module.config

<clientModule initializer="alloy/formatters-initializer">
  <moduleDependencies>
	<add dependency="EPiServer.Forms.UI" type="RunAfter" />
  </moduleDependencies>
</clientModule>

That way you will also get a custom date formatting in the "Form submisstions" dgrid:

How to create a custom login page in EPiServer MVC with OWIN

 

It's very easy to create a custom login page using the new AspNetIdentityPackage however the biggest problem I encountered was the lack of support for RedirectUrl, user would always be redirected back to the start page.

However, after a bit of digging I made it to work. Please take a look: Custom Login Page

The diff to the current Alloy MVC is here: Custom Login Page Diff

You can just clone my repository and follow the Readme to see it in action.

The problem was to override our default behavior of handling ReturnUrl which we always read from QueryString, however in case of a custom login screen that value is passed in form data. It was easy to do by overriding the default UISignInManager with a custom implementation with an overridden SignIn method that would skip the redirect and let your login controller to do the work.

public class CustomApplicationUISignInManager<T> : ApplicationUISignInManager<T>
  where T : IdentityUser, IUIUser, new()
{
  private readonly ServiceAccessor<ApplicationSignInManager<T>> _signInManager;

  public CustomApplicationUISignInManager(ServiceAccessor<ApplicationSignInManager<T>> signInManager)
    : base(signInManager)
  {
    _signInManager = signInManager;
  }

  public override bool SignIn(string providerName, string userName, string password)
  {
    return _signInManager().SignIn(userName, password, string.Empty);
  }
}

and registration in Startup.cs

// Add CMS integration for ASP.NET Identity
app.AddCmsAspNetIdentity<ApplicationUser>();
app.CreatePerOwinContext<UISignInManager>((options, context) =>
    new CustomApplicationUISignInManager<ApplicationUser>(context
        .Get<ApplicationSignInManager<ApplicationUser>>));

 

 

 

How to run EPiServer from console

Have you ever added admin mode plugins with a single button to run 1-time jobs like import / cleanup / data migration?

Or have you ever added a scheduled job just to run a long running task and see the progress.

Well...I did that plenty of times.

Wouldn't it be easier to just run those kind of 1-time jobs from the console and just forget about stuff like sessions, timeouts, web.config etc.?

Of course, I'm not saying that scheduled jobs or plugins are a bad thing, sometimes there is just no other way. But in most cases, it's easier to use a good old terminal.

A while ago, while visiting the HQ in Stockholm, me & Greg spent a few hours to create a simple tool that would let you do just that.

The idea is simple, you run a console app with a set of parameters:

  • Application Path ( -p ) - path to the folder that contains your webapplication
  • Task Assembly ( -a ) - path to the assembly that contains your custom tasks
  • Tasks ( -t ) - an ordered list of tasks from the Task Assembly

Here it is in action:

As you can see it's easy to use it. You can also report progress back from your tasks if necessary.

The tasks are simple objects with an Execute method. We use duck typing and would only execute those that contain that method.

A basic example of a task is as following:

public class SampleTask1
{
    private readonly IContentRepository _contentRepository;

    public event EventHandler<ProgressChangedEventArgs> Progress;

    public SampleTask1(IContentRepository contentRepository)
    {
        _contentRepository = contentRepository;
    }

    public void Execute()
    {        
        var totalPages = 20;

        for (int i = 1; i < totalPages; i++)
        {                
            var articlePage = _contentRepository.GetDefault<ArticlePage>(ContentReference.StartPage);
            articlePage.Name = "First sample page " + i;
            _contentRepository.Save(articlePage, SaveAction.Publish, AccessLevel.NoAccess);
            Progress?.Invoke(this, new ProgressChangedEventArgs((100 * i)/ totalPages, null));
        }

        Progress?.Invoke(this, new ProgressChangedEventArgs(100, null));
    }
}

It can either be a part of your web application project or it can also be placed in a separate project if you for example don't need to use the model classes.

The code that finds task classes is very simple:

public class TasksLoader
{
    private const string ExecuteMethodName = "Execute";
    private const string ProgressEventName = "Progress";
    private readonly IServiceLocator _serviceLocator;

    public TasksLoader(IServiceLocator serviceLocator)
    {
        _serviceLocator = serviceLocator;
    }

    public IEnumerable<EpiTask> Load(Assembly assembly, IEnumerable<string> tasks)
    {
        var types = assembly.ExportedTypes.Where(t => tasks.Contains(t.Name, StringComparer.InvariantCultureIgnoreCase)).ToList();
        if (types.Count != tasks.Count())
        {
            var missingTaskClasses = tasks.Except(types.Select(t => t.Name));
            throw new ArgumentException($"Invalid task class {string.Join(",", missingTaskClasses)} provided.");
        }
            
        foreach (var task in tasks)
        {
            var type = types.First(t => t.Name == task);
            var executeMethod = type.GetMethod(ExecuteMethodName, BindingFlags.Public | BindingFlags.Instance);
            if (executeMethod == null)
            {
                throw new ArgumentException($"The task {type} is missing the {ExecuteMethodName} method.");
            }

            yield return new EpiTask
            {
                Type = type,
                Instance = _serviceLocator.GetInstance(type),
                ExecuteMethod = executeMethod,
                ProgressEvent = type.GetEvent(ProgressEventName)
            };                
        }            
    }
}

Each of EpiTask instances is then passed to the TaskRunner and executed:

public class TaskRunner
{
    private readonly IServiceLocator _serviceLocator;
    private ProgressBar _pbar;

    public int? tick;

    public TaskRunner(ProgressBar pBar)
    {
        this._pbar = pBar;
    }

    public void Run(EpiTask epiTask)
    {
        if (epiTask.ProgressEvent != null)
        {
            var showProgressMethod = this.GetType()
                .GetMethod("ShowProgress", BindingFlags.Instance | BindingFlags.Public);
            var tDelegate = epiTask.ProgressEvent.EventHandlerType;
            var handler = Delegate.CreateDelegate(tDelegate, this, showProgressMethod);
            epiTask.ProgressEvent.AddEventHandler(epiTask.Instance, handler);
        }
            
        epiTask.ExecuteMethod.Invoke(epiTask.Instance, null);
    }

    public void ShowProgress(object sender, ProgressChangedEventArgs eventArgs)
    {
        if (!tick.HasValue)
        {
            tick = (int)(100 / (eventArgs.ProgressPercentage));
            this._pbar.UpdateMaxTicks(tick.Value);
        }

        this._pbar.Tick();
    }
}

The only tricky part was to initialize the framework. We don't have the web.config in the console app so we have to first point it to the site's web.config and then force it to load the correct episerver.framework section in order to initialize the base framework modules.

HostingEnvironmentMutator.Mutate(AppDomain.CurrentDomain);
Console.WriteLine("Initializing EPiServer environment");

var fileMap = new ExeConfigurationFileMap
{
    ExeConfigFilename = Path.Combine(commandLineParams.AppPath, "web.config")
};
var config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
var section = config.GetSection("episerver.framework") as EPiServerFrameworkSection;
ConfigurationSource.Instance = new FileConfigurationSource(config);
InitializationModule.FrameworkInitialization(HostType.Service);

var assembly = Assembly.LoadFrom(commandLineParams.TaskAssembly);
var assemblyName = AssemblyName.GetAssemblyName(commandLineParams.TaskAssembly);
AppDomain.CurrentDomain.Load(assemblyName);

If you use localdb then there's also a little trick that you need to do because the 'DataDirectory' variable is resolved by runtime and we are running our app from the console so we have a problem ;)

Fortunately it's also easy to fix using an instance of SqlConnectionStringBuilder and replacing the connectionString with a real path (without |DataDirectory| variable).

var connectionString = config.ConnectionStrings.ConnectionStrings["EPiServerDB"];
var builder = new SqlConnectionStringBuilder(connectionString.ConnectionString);

if (!string.IsNullOrWhiteSpace(builder.AttachDBFilename))
{
    builder.AttachDBFilename = builder.AttachDBFilename.Replace("|DataDirectory|",
        Path.Combine(commandLineParams.AppPath, "App_Data") + "\\");
}
connectionString.ConnectionString = builder.ConnectionString;

The whole code is available on https://github.com/barteksekula/epiconsole

Customizing TinyMCE Styles Dropdown programatically

I have recently received a question from one of the developers on how to provide a different set of TinyMCE Styles in a multi site environment. 

The expected result was to be able to change both the applied styles in the html editor and the dropdown that is used to choose a particular style.

There is a great post by Arve Systad about Setting editor stylesheets programmatically that is a great starting point for our problem.

We would create an EditorDescriptor where we would place the logic that returns different stylesheets per site.

using System;
using System.Collections.Generic;
using System.Linq;
using EPiServer.Cms.Shell.UI.ObjectEditing.EditorDescriptors;
using EPiServer.Core;
using EPiServer.Editor.TinyMCE;
using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
using EPiServer.Web;
using EPiServer.Web.Hosting;

namespace AlloyTemplates.Business.EditorDescriptors
{
    [EditorDescriptorRegistration(TargetType = typeof(XhtmlString), EditorDescriptorBehavior = EditorDescriptorBehavior.OverrideDefault)]
    public class MyEditorDescriptor : XhtmlStringEditorDescriptor
    {
        public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
        {
            base.ModifyMetadata(metadata, attributes);

            var tinyMceSettings = metadata.EditorConfiguration.FirstOrDefault(x => x.Key == "settings");
            var dictionary = tinyMceSettings.Value as Dictionary<string, object>;
            if (dictionary == null) return;

            var formatter = new TinyMCEStyleFormatter();

            var staticCssEditorCss = SiteDefinition.Current.Name.Equals("Foo") ? "/Static/css/editor.css" : "/Static/css/editor2.css";

            dictionary["content_css"] = staticCssEditorCss;
            using (var stream = GenericHostingEnvironment.VirtualPathProvider.GetFile(staticCssEditorCss).Open())
            {
                var styleFormats = formatter.CreateStyleFormats(stream);
                dictionary["style_formats"] = styleFormats;
            }
        }
    }
}

content_css property is responsible for styling the editor whereas style_formats is responsible for populating the styles dropdown.

Let's imagine we have two separate stylesheets:

  • editor1 - which should be used in the "Foo" site
h2 {EditMenuName:Header 2;color:red;}
h3 {EditMenuName:Header 3;}

h2 {
    color:red;
}
  • editor2 - which should be used in all other sites
h2 {EditMenuName:Alternative Header 2;color:blue;}
h3 {EditMenuName:Alternative Header 3;}
h4 {EditMenuName:Alternative Header 4;}

h2 {
    color:blue;
}

The end result would look as following:

 

And

Ambiguous match found - thrown from ContentData Metadata Provider

I’m in the middle of a big upgrade from CMS6R2 to 7.19.2 and I faced an interesting problem.

It seemed that EPiServer was not able to match interface literal to a valid type. I decided to dig in to EPiServer core to find out what the problem is:

propertyWithAttributes is then passed to a special method that tries to extract the typename and propertyname.

private static PropertyInfo GetPropertyInfoFromInterface(Type modelType, string propertyWithAttributesName)
{
 string[] strArray = propertyWithAttributesName.Split(new char[] { '_' });
 if ((strArray.Length == 2) && (modelType.GetInterface(strArray[0], true) != null))
 ...

As you can see the interface is not fully qualified (it should be episerver.core.icontent_name) and unfortunately we had an interface IContent in our codebase (used for different purposes) which effectively prevented it from working...
After we changed our internal IContent to a different name, everything started to work.