How to correctly implement a custom macro method
I’ve recently been getting a lot of questions about how to add custom functionality in macros. The common answer is to implement your custom macro method. It’s a pretty simple and elegant solution. It’s also really well described in our documentation
as to how to register your custom method. However, it’s not described how to correctly implement the body of your method. Of course it’s up to you as it’s your custom logic ... but there are few things you need to keep in mind in order to make sure your custom method does not introduce a security issue on your website and that it’s also reusable (100% compatible with macro engine features, usable for multilingual websites, etc.). There are 4 very common mistakes I see in implementations of custom macro methods that I’ll address in this article.
No caching implemented
I very often see methods that simply issue a complex query to the DB, get the result (which is rather static), and return it. This macro method is then called, for example, in the transformation in the master page. What this actually does is; for each request on your webpage, the query is issued even though the information retrieved from the DB is the same for each call.
What you need to know is that there is no caching implemented on top of custom macro methods unless you explicitly request it. So if your method gets 1MB of data from the DB per one call of the method, and you have this macro method on your master page, your website visitors will have to download 1MB data from the DB each time they request any page on your website. This is not what you want, is it? There are two ways to handle this:
implement caching inside the body of your method using standard Kentico caching API
force caching of the macro result with the Cache macro method
The benefit of the first approach is that you don’t have to remember to surround call of your macro method with the Cache macro method. This also has the benefit of better autocompletion support when typing the macro as result of your macro method is typed wheras a result of the Cache method is always an object. The first approach is recommended for your custom methods where you have control over the code of the method.
The benefit of the second approach is that you can be 100% sure the macro result will be cached no matter how the body of the method is implemented. This approach is recommended for methods over which you don’t have code control and for those you don’t exactly know how they are implemented. Of course it’s more complicated to write and use your method. You can read more about this in the documentation
Not handling security
The other mistake I often see is introducing a security issue on your website through registering your custom methods. Let’s have a look at this example method which returns a list of users along with their emails:
This custom method is meant to be used in the custom E-shop administration UI which is only accessible to global administrators as it contains sensitive information. (The page is correctly configured so that no one else except global administrators can access it).
You may think everything is fine. There is no SQL injection as this method correctly passes method parameters through ObjectQuery, which handles that. However registering this method became a security issue for the website. Why? Because anybody can access the list of users along with their usernames! But how when the page is correctly secured? The answer is simple. Just call this macro in any place on the website where macros are resolved and you’ll get the list. (For example, a content editor can write the macro into the editable text webpart and select to view the preview mode of the list). You may be asking how is this possible when you know that all the macros are signed by the user who inserted them and the macro in the editable text is signed by the content editor so he does not have access to the list of users ... That’s right, but the macro engine does not know what you’re going to implement inside your method and the output is a simple string, so from the perspective of the general macro engine, the fact it displayed evaluation result is completely correct and allowed for everybody.
It is actually the responsibility of your method to check whether the user who inserted the macro is allowed to see its result. There are two ways to implement your macro method securely.
Implement it so that it returns the result only if the current user is a global administrator. This approach seems fine, but it’s not best practice as you break the rules of how macro security works. Also, it won’t work in situations where you don’t have HttpContext (e.g. asynchronous email sending).
Check who inserted the macro and check view permissions of this user. If a person with permission decided to insert this macro somewhere, he knows what he is doing, so you can display the result. You know that an administrator inserted this macro only to this secured page – he knows what he is doing – so no problem with that. This is the recommended way as all the default macro methods behave like this.
Here is the correct implementation of the above method:
Ignoring culture specifics
Even though you intend to use your macro method on a single language website, it’s good practice to implement the method in a way that works correctly in any culture version. You can then reuse your method for any project and the method is ready to be used inside the Kentico UI, which can have a different localization to your website.
You need to think about cultures whenever you work with culture-sensitive inputs/ outputs like decimal numbers, date/ time, currency, etc. Implementing correct culture support is very easy as most of the .NET and Kentico APIs have overrides that take culture parameter if the results of those are culture-sensitive. You can access the correct culture context of your method evaluation from the context.Culture property and you can simply pass this to other API methods.
Wrong implementation of case sensitivity
If you work with text inputs, always think whether your method should be case sensitive, or not. There is the context.CaseSensitive property (by default the macro engine is case in
sensitive) which tells you how other methods in the macro engine will behave in the same evaluation context—it’s best practice to handle case sensitivity based on the value of this setting. If you ignore this setting, users of your method may be confused by the behavior of it as they expect all methods to behave the same.
The above two are correctly implemented in the following sample Contains method:
To sum up, here are few things I’d like you to remember when implementing your custom macro methods:
Make sure your methods use caching (if you have no control over the method code, you can use the Cache macro method when you insert the macro to force caching).
Ensuring security is the responsibility of the method itself!
For culture sensitive inputs/ outputs, work according to the context.Culture setting
For text inputs/ outputs, decide the case-sensitivity of your method based on the context.CaseSensitive property.