Investigating Audit Logs with Sitecore Powershell Reports

Very often Content Administrators seek history of activities performed on an item like edit, publish, workflow approval etc. Though we have options like Sitecore Log Analyzer, it requires developers’ assistance while it is critical to equip Content Administrators to function independently.

Sitecore Powershell comes with a super cool report, ‘Find Audit Trail from logs’ which crawls through the logs for the specified timeframe yielding all the audit logs (without exceptions etc. ;), Sitecore Log Analyzer will need to be used for analyzing exceptions). I haven’t seen much mention of this report and speaking to a few people it seems to have been missed/unnoticed. Here is how you can access the same,

Like any other Sitecore Powershell report, we do have the export and filter options available in this report as well, which makes our life easier.
We experienced one issue which was specifically due to an error in the format of certain log entries,

Few log entries in our instance were having a weird additional spacing before the thread id though the log4net conversionPattern etc. were correct. This appeared to the root cause for the above issue,

Updating the following line to remove empty entries fixed the issue,

By default Sitecore Powershell Reports are enabled only for Administrators. When it is needed for other roles, we will be needing to provide read access to /sitecore/content/Documents and settings/All users/Start menu/Right/Reporting Tools/PowerShell Reports item in core database.

If we are looking to provide access only for this specific report, then deny access will need to be added to other reports under /sitecore/system/Modules/PowerShell/Script Library/SPE/Reporting/Content Reports/Reports item in master database. It is important to note that the Content Author will be able to view history of activities performed on all the items including the ones for which they might not have access.

This Report was introduced in Powershell Extensions 5.0, if you are using an older version, you could use this package.

If you are using Sitecore PAAS with Azure Application Insights, you will be needing Application Insights App ID/Key and will be needing to fetch the logs using Azure Application Insights Queries. The below blog will be helpful for PAAS setup, https://www.sitecoregabe.com/2019/09/azure-application-insights-logs.html

References:
https://www.sitecoregabe.com/2018/08/basic-sitecore-audit-trail-with.html
https://github.com/SitecorePowerShell/Console/issues/1033

Efficient way to retrieve list of created/updated and deleted items during Publish

While we are familiar with publish:itemProcessed event that will be triggered once per processed item during publish, there might be instances where we might be looking to work on the entire list of published items all at once to accomplish the desired operations efficiently.

Sitecore has introduced ProcessedPublishingCandidates property (previously it was ProcessedItems which is obsolete now), which accumulates the processed items within the PublishItem pipeline. The PublishItem pipeline holds the responsibility of publishing a single item from the publish queue (this pipeline can also be intercepted with publish:itemProcessed, publish:itemProcessing events). The ProcessedPublishingCandidates property is accessible within the Publish pipeline as well (this pipeline manages the entire publish operation). This behavior enables us to retrieve the list of published items by intercepting the Publish Pipeline using ProcessedPublishingCandidates property.

Sitecore has also added DeleteCandidates property which
will be helpful if you are just looking to retrieve only the deleted items.

Here is the code snippet,

class CustomPublishProcessor : PublishProcessor
{
    public override void Process(PublishContext context)
    {
        Assert.ArgumentNotNull(context, "context");

        if (context.Aborted)
            return;
	
        //Fetches List of Processed(Created/Updated/Deleted) Items
        var processedItems = context.ProcessedPublishingCandidates.Keys
                .Select(i => context.PublishOptions.TargetDatabase.GetItem(i.ItemId)).Where(j => j != null);

        //Fetches List of Deleted Items
        var deletedItems = context.DeleteCandidates;
    }
}
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <publish>
        <processor patch:after="*[@type='Sitecore.Publishing.Pipelines.Publish.ProcessQueue, Sitecore.Kernel']" type="Assembly.Pipelines.CustomPublishProcessor, Assembly"/>
      </publish>
    </pipelines>
  </sitecore>
</configuration>

Please note that we do have another property ‘CustomData’ which is available within PublishContext, hence it is shared across all the pipeline steps. This allows us to store any custom data specific to a processed item within CustomData property during PublishItem pipeline and can then be utilized across other steps within PublishItem and Publish pipelines if needed.

SOLR Security Vulnerability CVE-2019-0192 (SOLR-13301) – Disabling Config API

Applying fixes for the Security Vulnerabilities is a critical activity needed for preventing any intrusions and for ensuring the security of the system. While it is vital to continuously monitor and apply the Security fixes released by the tools used in the implementation, it is also essential to review and deploy the previously released Security fixes for the versions of the opted tools during initial infrastructure setup.

This blog covers the options available to mitigate a SOLR Security Vulnerability, CVE-2019-0192 (SOLR-13301) released for versions 5.0–5.5.5, 6.0–6.6.5. When using Sitecore 9.0 or 8.2 with SOLR as Search Platform, the implementation might be running on one of the above mentioned SOLR versions. Options available to overcome this vulnerability can be found in Sitecore/SOLR Documentation,
https://kb.sitecore.net/articles/227897#note6 
https://issues.apache.org/jira/browse/SOLR-13301

While the most recommended option is to upgrade to the recent version of Sitecore and SOLR which will also allow to leverage the latest features, disabling Config API would work well when looking for an immediate solution.

The Config API enables manipulating various aspects of solrconfig.xml using REST-like API calls.
This feature is enabled by default and works similarly in both SolrCloud and standalone mode. Many commonly edited properties (such as cache sizes and commit settings) and request handler definitions can be changed with this API.

Config API can be disabled by adding System Property (disable.configEdit=true) to SOLR_OPTS environment variable defined in solr.in.cmd file located within SOLR bin folder. This can be achieved by adding the following line within solr.in.cmd file,

REM Disabling Config API for mitigating Security Vulnerability https://issues.apache.org/jira/browse/SOLR-13301
set SOLR_OPTS=%SOLR_OPTS% -Ddisable.configEdit=true

SOLR Service must be restarted for the above added System Property to take effect.

If you are using Powershell Script for installing SOLR, adding the following lines to the script should take care of it,

$SolrInCmd_Path = "S:\SOLR\solr-6.6.5\bin\solr.in.cmd"

##Disabling Config API for mitigating Security Vulnerability for v6.6.5 https://issues.apache.org/jira/browse/SOLR-13301
Add-Content -Path $SolrInCmd_Path -Value 'REM Disabling Config API for mitigating Security Vulnerability https://issues.apache.org/jira/browse/SOLR-13301'
Add-Content -Path $SolrInCmd_Path -Value 'set SOLR_OPTS=%SOLR_OPTS% -Ddisable.configEdit=true'

Following Curl Command can be used to ensure that the Config API is disabled. This should result in 403 Forbidden Error,

curl https://localhost:8983/solr/<core_name>/config -H "Accept: application/json" -H "Content-type:application/json" -d "{'set-user-property' : {'variable_name':'some_value'}}"

Alternatively, if Config API is being utilized in the implementation, applying SOLR-13301.patch and re-compiling SOLR or hardening Network Settings to allow only trusted traffic are viable options.

Extend Sitecore Experience to China

China being one of the top economies in the world, huge number of business entities are focusing to expand their services in China. Since websites are the face of the organization, it is highly crucial to have the websites perform better across different intended geographies for better user experience, but performance is a huge concern for websites in China when hosted outside of the country. While adding Content Delivery Network (CDN) usually improves the performance for different countries which are farther from the hosting servers, CDN setup process works little different for China.

There are a couple of major issues that make up to the slowness of the external websites in China,

Limited Peering Capacities – China has very limited internet providers with direct peering being  expensive. This makes the internet traffic exchange with the country highly congested and considered to be key reason for slowness.

The Great Firewall of China (GFW) – The country uses Deep Packet Inspection to monitor all the requests flow. This may also cause package loss, resulting in retransmission which slows the transaction even more.

In order to mitigate the above, to provide consistent load times in China, the easier route is to host the website in China. For hosting websites in China, a ICP (Internet Content Provider) License is mandatory which in turn requires the company to have a China business entity and a website with a Chinese domain registrar. This may also bring in the need of purchasing a new domain specifically for China region from Chinese domain registrar, probably .cn or .com.cn, based on the existing domain setup. Once the application for ICP is submitted and approved by Ministry of Industry and Information Technology (MIIT), an ICP Number is generated which is required to be placed in the website’s footer.

Few tasks including Infrastructure Setup can happen in parallel to the ICP Registration. There are a few options like Aliyun (Alibaba Cloud), Azure, AWS etc. for hosting the sites. Since AWS is one of the top providers in China and the current sites were hosted on AWS(IAAS), AWS China IAAS solution worked better for our situation. The platform selection requires some level of quality analysis based on existing infrastructure. Load Balancer should be considered for High Availability.

Here are few approaches that should be considered while hosting the website in China,

Setting up a New Website Infrastructure – Few businesses would prefer to run China websites separately from the existing infrastructure due to differences in website appearance, behaviour or content. This will introduce the need of setting up all required Application/Storage Roles and Environments.

Extending the Existing Website Infrastructure – Most of the businesses would like to retain their branding etc. which eliminates the need of setting up Content Management Role, Staging, UAT etc. unless it is required. Content Delivery and xDB/xConnect Application/Storage Roles will suffice the need.

Setting up CDN in China – Using China CDNs is another option which works well but introduces lot of challenges in leveraging different Experience Management capabilities of Sitecore. Please note that setting up CDNs in China will still require ICP Registration.

 

It is highly crucial to review the China Data Protection Regulations (CDPR) when there is a collection of personal information happening in any part of the website. Though there are various draft measures and sector-specific regulations on transfer of personal data outside borders of China, it is unclear at this point to what extent these rules apply and it is expected to be clearly defined this year. There is also a requirement to appoint a Data Protection Officer in China for large organizations depending on the quantity of data being collected/processed.

Though Google Chrome is the widely used Browser in China, there are few additional Browsers including Tencent’s QQ browser, Alibaba’s UC browser, Baidu, Sogou etc. which is being used by huge number of users. Hence it is important to make sure our websites are compatible with these additional browsers across different devices.

There are few Social Media including Facebook, Google+ etc. which are blocked in China, hence related integrations must be reviewed.

Though Google Analytics works in China, Baidu is highly preferred considering the following reasons,

  • Latency and Data loss issues when using Google Analytics
  • Google Analytics Web Interface being blocked in China

And there is no guarantee that Google Analytics won’t be blocked in future.

Despite all the above challenges and process, it is still worth doing. We did have our website performing about 70-80% faster in different regions of the country after hosting the sites in China.

Integrating Sitefinity with Azure AD B2C for Front End User Authentication

Sitefinity CMS supports out-of-the-box integration with multiple external identity providers like Windows Authentication, ADFS, Facebook and Google. But the support for the emerging cloud-based identity management solution, Azure Active Directory B2C, is not included out-of-the-box. Reason being is the policy framework of Azure B2C which is not supported by Sitefinity.
Whenever a request is sent to Azure B2C, it looks for the policy (eg: sign-in, sign-up, edit profile etc.) in order to redirect the user to the appropriate page. Sitefinity’s OWIN Middleware does not support changing policies. Sitefinity reverts it back to the default policy (in our case it is sign-in policy) if changed to a different policy (like edit profile, reset password etc.), making it hard to reach edit profile or reset password pages. We can achieve this by having the Sitefinity’s OWIN Middleware for Sign In requests (so that front end users can have access to the backend content) and switching to custom middleware for other requests like Edit Profile, Reset Password etc. (for which Sitefinity backend access is not required).
A custom external Identity Provider should be registered for Azure B2C in Sitefinity, which can be done by creating a new AuthenticationProviderElement by navigating to Advanced Settings -> Authentication -> STS -> AuthenticationProviderElement -> Create New,

Once the AuthenticationProviderElement is created, parameters required to connect and authenticate via Azure B2C can be created under that. But the parameters created under AuthenticationProviderElement will be initialized only after our OWIN Middleware is initialized, hence we might not be able to use them in code. So the preferred way would be having the parameters in Application Settings Config.

<add key="ida:Issuer" value="https://login.microsoftonline.com/NTTGEHA.onmicrosoft.com/v2.0"/>
<add key="ida:Tenant" value="NTTGEHA.onmicrosoft.com"/>
<add key="ida:ClientId" value="b74bbd8b-d377-4f69-af51-597f787e2dd1"/>
<add key="ida:AadInstance" value="https://login.microsoftonline.com/tfp/{0}/{1}/v2.0/.well-known/openid-configuration"/>
<add key="ida:RedirectUri" value="http://localhost:60876/Sitefinity/Authenticate/OpenID/signin-custom"/>
<add key="ida:ProfileRedirectUri" value="http://localhost:60876/Home/EditProfileRedirect"/>
<add key="ida:PostLoginRedirectUri" value="http://localhost:60876/"/>
<add key="ida:PostLogoutRedirectUri" value="http://localhost:60876/"/>
<add key="ida:SignUpSignInPolicyId" value="B2C_1_SignUpIn"/>
<add key="ida:EditProfilePolicyId" value="B2C_1_SignProfileEdit"/>
<add key="ida:ResetPasswordPolicyId" value="B2C_1_SignPWReset"/>
<add key="ida:ResponseType" value="id_token"/>
<add key="ida:Scope" value="openid profile email"/>

For implementing the custom external authentication provider, a custom AuthenticationProvidersInitializer has to be created where the external provider can be configured and then the initializer can be registered in the ObjectFactory.
Once a user logs via SSO with the STS in the relying party instance, in case there is no user previously authenticated with the same email, a new local user account is automatically created. The profile fields of the account are populated with the information provided by the STS in the claims that are returned. Profile fields of the local account (in the relying party instance) are updated only when they are empty and only from the claims received by the STS. Thus, if you edit your first name in the relying party instance, the change is not synced with the first name on the STS. Once the account is created locally, it is bound to the identity authenticated via email by the STS. If the email is modified either in the STS, or in the local profile in the relying party instance, a new account is once again created for the external user when they log in. If this is the case, all local profile information and local application roles are lost.

public class AuthenticationProvidersInitializerExtender : AuthenticationProvidersInitializer
{
    // App config settings
    public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
    public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
    public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
    public static string Issuer = ConfigurationManager.AppSettings["ida:Issuer"];
    public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
    public static string PostLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
    public static string ProfileRedirectUri = ConfigurationManager.AppSettings["ida:ProfileRedirectUri"];
    public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
    public static string ResponseType = ConfigurationManager.AppSettings["ida:ResponseType"];
    public static string Scope = ConfigurationManager.AppSettings["ida:Scope"];

    public override Dictionary<string, Action<IAppBuilder, string, AuthenticationProviderElement>> GetAdditionalIdentityProviders()
    {
        var providers = base.GetAdditionalIdentityProviders();

        string providerName = "AzureB2C";
        providers.Add(providerName, (IAppBuilder app, string signInAsType, AuthenticationProviderElement providerConfig) =>
        {
            var options = new OpenIdConnectAuthenticationOptions()
            {
                ClientId = ClientId,
                Authority = Issuer + "/",
                AuthenticationType = "OpenIDConnect",
                SignInAsAuthenticationType = signInAsType,
                RedirectUri = RedirectUri,
                ResponseType = ResponseType,
                PostLogoutRedirectUri = PostLogoutRedirectUri,
                Scope = Scope,
                MetadataAddress = string.Format(AadInstance, Tenant, SignUpSignInPolicyId),
                Notifications = new OpenIdConnectAuthenticationNotifications()
                {
                    SecurityTokenValidated = n => this.SecurityTokenValidatedInternal(n),
                    AuthenticationFailed = n => this.OnAuthenticationFailed(n),
                }
            };

            app.UseOpenIdConnectAuthentication(options);
        });

        return providers;
    }

    private Task SecurityTokenValidatedInternal(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
    {
        var identity = notification.AuthenticationTicket.Identity;

        var externalUserEmail = identity.FindFirst("emails");
        if (externalUserEmail != null)
            identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserEmail, externalUserEmail.Value));

        var externalUserId = identity.FindFirst("sub");
        if (externalUserId != null)
            identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserId, externalUserId.Value));

        var externalUserFirstName = identity.FindFirst("given_name") != null ? identity.FindFirst("given_name").Value : string.Empty;
        var externalUserFamilyName = identity.FindFirst("family_name") != null ? identity.FindFirst("family_name").Value : string.Empty;
        var externalUserFullName = externalUserFirstName + " " + externalUserFamilyName;
        identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserName, externalUserFullName));

        var externalUserNickName = identity.FindFirst("nickname") != null ? identity.FindFirst("nickname").Value : string.Empty;
        identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserNickName, externalUserNickName));

        var externalUserPicture = identity.FindFirst("picture");
        if (externalUserPicture != null)
            identity.AddClaim(new Claim(SitefinityClaimTypes.ExternalUserPictureUrl, externalUserPicture.Value));

        return Task.FromResult(0);
    }

    private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
    {
        notification.HandleResponse();

        // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
        // because password reset is not supported by a "sign-up or sign-in policy"
        if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
        {
            // If the user clicked the reset password link, redirect to the reset password route
            notification.Response.Redirect("/Account/ResetPassword");
        }
        else if (notification.Exception.Message == "access_denied")
        {
            notification.Response.Redirect("/");
        }
        else
        {
            notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
        }

        return Task.FromResult(0);
    }
}

Initializer has to be registered in Global.asax,

protected void Application_Start(object sender, EventArgs e)
        {
            SystemManager.ApplicationStart += SystemManager_ApplicationStart;
        }

        private void SystemManager_ApplicationStart(object sender, EventArgs e)
        {
            // Register the backend logic for the new external provider
            ObjectFactory.Container.RegisterType<AuthenticationProvidersInitializer, AuthenticationProvidersInitializerExtender>(new ContainerControlledLifetimeManager());
        }

Azure B2C comes with different policies for Sign In, Sign Up, Edit Profile and Reset Password. The policy has to be set appropriately before making request to Azure B2C. Sitefinity uses OWIN for authentication which does not support changing policies before making request. To solve this, another OWIN middleware has to be created which will be used when changing policies (which will not work with Sitefinity’s OWIN middleware). A OWIN Startup class (Startup.cs) has be added under project,

public partial class Startup
{
    // The OWIN middleware will invoke this method when the app starts
    public void Configuration(IAppBuilder app)
    {
        // ConfigureAuth defined in other part of the class
        ConfigureAuth(app);
    }
}

Below Startup.Auth.cs has to be added under App_Start,

[assembly: OwinStartup(typeof(SitefinityWebApp.Startup))]

namespace SitefinityWebApp
{
    public partial class Startup
    {
        // App config settings
        public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
        public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
        public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
        public static string PostLoginRedirectUri = ConfigurationManager.AppSettings["ida:PostLoginRedirectUri"];
        public static string ProfileRedirectUri = ConfigurationManager.AppSettings["ida:ProfileRedirectUri"];
        public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
        public static string ResponseType = ConfigurationManager.AppSettings["ida:ResponseType"];
        public static string Scope = ConfigurationManager.AppSettings["ida:Scope"];

        // Configure the OWIN middleware
        public void ConfigureAuth(IAppBuilder app)
        {
            //Use Sitefinity's OWIN Middleware for all requests including Sign Up & Sign In
            app.MapWhen(context => !IsUpdateProfileRequest(context), appBuilder =>
            {
                appBuilder.UseSitefinityMiddleware();
            });

            //Use Custom OWIN Middleware for Edit Profile & Reset Password Request
            app.MapWhen(context => IsUpdateProfileRequest(context), appBuilder =>
            {
                appBuilder.UseCookieAuthentication(new CookieAuthenticationOptions());
                appBuilder.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    //Generate the metadata address using the tenant and policy information
                    MetadataAddress = String.Format(AadInstance, Tenant, SignUpSignInPolicyId),

                    //These are standard OpenID Connect parameters, with values pulled from web.config
                    ClientId = ClientId,
                    RedirectUri = PostLoginRedirectUri,

                    Notifications = new OpenIdConnectAuthenticationNotifications()
                    {
                        RedirectToIdentityProvider = n => this.OnRedirectToIdentityProvider(n),
                    },

                    //Specify the claims to validate
                    ResponseType = ResponseType,
                    AuthenticationType = "OpenIdConnect",
                    SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,

                    //Specify the scope by appending all of the scopes requested into one string (seperated by a blank space)
                    Scope = Scope
                });
            });
        }

        private static bool IsUpdateProfileRequest(IOwinContext context)
        {
            return (context.Request.Path.ToString().Contains("/EditProfile") || context.Request.Path.ToString().Contains("/ResetPassword"));
        }

        private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
        {
            var policy = notification.OwinContext.Get<string>("Policy");

            if (!string.IsNullOrEmpty(policy) && !policy.Equals(SignUpSignInPolicyId))
            {
                notification.ProtocolMessage.Scope = OpenIdConnectScopes.OpenId;
                notification.ProtocolMessage.ResponseType = OpenIdConnectResponseTypes.IdToken;
                notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.Replace(SignUpSignInPolicyId.ToLower(), policy.ToLower());

                //Redirect to a Controller Action (after Profile Update) to avoid being redirected to Sitefinity Login Page through Sitefinity's OWIN middleware
                notification.ProtocolMessage.RedirectUri = ProfileRedirectUri;
            }

            return Task.FromResult(0);
        }
    }
}

Sitefinity’s OWIN Middleware has to be used for Sign Up and Sign In, for the user to access the content inside Sitefinity.
Since Sitefinity comes with a OWIN Startup, the above Startup can be set as preferred in AppSettings, in order to avoid multiple Startup conflicts

<add key="owin:appStartup" value="SitefinityWebApp.Startup" />

The app is now properly configured to communicate with Azure AD B2C by using the OpenID Connect authentication protocol. OWIN manages the details of crafting authentication messages, validating tokens from Azure AD B2C, and maintaining user session. All that remains is to initiate each user’s flow.

[ControllerToolboxItem(Name = "SignIn", Title = "SignIn", SectionName = "MVC")]
public class AccountController : Controller
{
    // App config settings
    public static string PostLoginRedirectUri = ConfigurationManager.AppSettings["ida:PostLoginRedirectUri"];
    public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
    public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];

    public ActionResult Index()
    {
        return View("SignIn");
    }

    public void SignUpSignIn()
    {
        if (!Request.IsAuthenticated)
        {
            var challengeProperties = ChallengeProperties.ForExternalUser("OpenIDConnect", PostLoginRedirectUri);
            HttpContext.GetOwinContext().Authentication.Challenge(challengeProperties, ClaimsManager.CurrentAuthenticationModule.STSAuthenticationType);
            return;
        }
        Response.Redirect("/");
    }

    public void EditProfile()
    {
        HttpContext.GetOwinContext().Set("Policy", EditProfilePolicyId);
        HttpContext.GetOwinContext().Authentication.Challenge();
    }

    public void ResetPassword()
    {
        HttpContext.GetOwinContext().Set("Policy", ResetPasswordPolicyId);
        HttpContext.GetOwinContext().Authentication.Challenge();
    }

    public void SignOut()
    {
        if (Request.IsAuthenticated)
        {
            IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
            HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
        }
    }

    public void EditProfileRedirect()
    {
        Response.Redirect("/");
    }
}

Sitecore API for Adding & Retrieving Query Strings

Whenever we start any new project we tend to write some Utilities class with frequently used methods like GetItem, EditItem, etc. and also, adding & retrieving query strings using UriBuilder/HttpUtility.ParseQueryString API.

Sitecore uses query strings like sc_mode, sc_lang etc. This got me a thinking.

Sitecore should have come up with its own methods for adding & retrieving query strings.

Luckily we have such methods in Sitecore.Web.WebUtil class, syntax below,

Sitecore.Web.WebUtil.AddQueryString(<em>url</em>, <em>key1</em>, <em>value1</em>, <em>key2, value2</em>)
Sitecore.Web.WebUtil.GetQueryString(<em>key</em>)

AddQueryString method will encode the query string values (in case of special characters), so url encoding using HttpUtility.UrlEncode is not required. In case of decoding, ExtractUrlParm & ParseQueryString methods will help.

Hope this will save you some lines of code and time.

Originally Posted at:  https://www.nttdatasitecore.com/Blog/2017/March/Sitecore-API-for-Adding-Retrieving-Query-Strings

Rendering Sitecore Content Tree

Once we got a requirement to display certain media library items in our page. We have certain controls like DataTreeView in Sitecore which can be extended based on our requirement.

<DataContext ID="TreeviewDataContext" Root=”{3D6658D8-A0BF-4E75-B3E2-D050FABCF4E1}” DataViewName=”Master” />
<DataTreeview ID="Treeview" DataContext="TreeviewDataContext" Root="true"> 
</DataTreeview>

But we faced some challenges in extending DataTreeView.

Another simple and easily extendable solution would be Telerik’s RadTreeView and Sitecore.Resources.ImageBuilder API.

<telerik:RadTreeView ID="contentTree" runat="server" OnNodeDataBound="contentTree_NodeDataBound" ItemType="Sitecore.Data.Items.Item">
</telerik:RadTreeView>
protected void contentTree_NodeDataBound(object sender, RadTreeNodeEventArgs e)
{
    UpdateTree(e.Node);
}

private void UpdateTree(RadTreeNode treeNode)
{
    Item currentItem = (Item)treeNode.DataItem;
        
    //Binding Icon and Name of Sitecore Item
    HtmlGenericControl nodeContainer = new HtmlGenericControl("div");
    ImageBuilder imageBuilder = new ImageBuilder() { Height = 16, Src = currentItem.Appearance.Icon, Width = 16 };
    nodeContainer.InnerHtml = imageBuilder.ToString() + currentItem.Name;
    treeNode.Controls.Add(nodeContainer);
        
    //Binding Child Items 
    foreach (Item childItem in currentItem.Children)
    {
        RadTreeNode currentNode = new RadTreeNode() { DataItem = childItem };
        UpdateTree(currentNode);
        treeNode.Nodes.Add(currentNode);
    }
    treeNode.DataBind();
}

Hope this will save you some time, if you are looking out for more customization. Otherwise DataTreeView will help.

Originally Posted at:  https://www.nttdatasitecore.com/Blog/2017/February/Rendering-Sitecore-Content-Tree

Faceted Autocomplete for a Sitecore Multilingual site using SOLR Suggester API

There are lot of options for SOLR Autocomplete and the popular ones are NGrams, Terms Component and Suggester. Suggester is the most recent, fastest and most recommended one.

Though faceting/filtering of results based on template or any other field may seem to be difficult with Suggester, it is possible with a minor tweak!!

Create Field for Building Suggestions

The field from which suggestions have to be built can be a Sitecore field or a custom computed field. In my case it involved few Sitecore fields like Title, Subtitle, Description etc.. So I created a copy field with these as below.

<field name="_suggest" type="text_general" indexed="true" stored="true"/>
<copyfield source="title_t" dest="_suggest"/>
<copyfield source="subtitle_t" dest="_suggest"/>
<copyfield source="description_t" dest="_suggest"/>

Creating Field Type for Suggester

This fieldtype will be used by suggester for analyzing and building the suggestions from our field (_suggest). We can either use existing field types like text_general etc. or create a fieldtype like the one below (in the schema.xml file) in case any customization may later be required.

<fieldtype name="text_suggester" class="solr.TextField" positionincrementgap="100">
    <analyzer>
        <tokenizer class="solr.WhitespaceTokenizerFactory"/>
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.RemoveDuplicatesTokenFilterFactory"/>
        <charfilter class="solr.PatternReplaceCharFilterFactory" pattern="[^a-zA-Z0-9]" replacement=" "/>
    </analyzer>
</fieldtype>

Creating The Suggest Component:

Though we have many Lookup Implementations for creating Suggest Component, but Context Filtering and Payload Fields are supported only by AnalyzingInfixLookupFactory and BlendedInfixLookupFactory.

Out of these, AnalyzingInfixLookupFactory doesn’t remove duplicate suggestion values. So I preferred BlendedInfixLookupFactory, which returns only unique values though the search term is present in multiple indexed documents.

Since we need to get the suggestions only for the current language (by Context Filtering) we should have contextField set as _language (this SOLR field of indexed document corresponds to Sitecore Item’s Language).

SOLR has come up with a Payload field for purposes like boosting, tagging etc. but it can be used for other purposes too. Here we can use it for faceting results!! In my case I wished to group results based on a sitecore field, Facet Category.

<searchcomponent name="suggest" class="solr.SuggestComponent">
    <lst name="suggester">
        <str name="name">suggester</str>
        <str name="lookupImpl">BlendedInfixLookupFactory</str>
        <str name="dictionaryImpl">DocumentDictionaryFactory</str>
        <str name="field">_suggest</str>
        <str name="contextField">_language</str>
        <str name="payloadField">facet_category_t</str>
        <str name="suggestAnalyzerFieldType">text_suggester</str>
        <str name="buildOnStartup">false</str>
        <str name="buildOnCommit">false</str>
    </lst>
</searchcomponent>

Creating Request Handler:

We need to create a request handler pointing to our suggest component, that will help in building and retrieving suggestions.

<requesthandler name="/suggest" class="solr.SearchHandler">
    <lst name="defaults">
        <str name="suggest">true</str>
        <str name="suggest.dictionary">suggester</str>
        <str name="suggest.onlyMorePopular">true</str>
        <str name="suggest.count">10</str>
        <str name="suggest.collate">true</str>
    </lst>
    <arr name="components">
        <str>suggest</str>
    </arr>
</requesthandler>

Once we are done with this, we can query and check for results in xml format,
http://localhost:8983/solr/sitecore_web_index/suggest?suggest.q=apple&suggest.cfq=fr-CA

Building jQuery Autocomplete:

We can use getJSON API to retrieve the suggestions in json format and display after adding querystring ‘wt=json’.
An Example below,

<input id="search-box" type="text" />
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20type%3D%22text%2Fjavascript%22%3E%0A%24('%23search-box').autocomplete(%7B%0A%20%20%20%20source%3A%20function%20(request%2C%20response)%20%7B%0A%20%20%20%20%20%20%20%20var%20currentLanguage%20%3D%20'en'%3B%0A%20%20%20%20%20%20%20%20var%20url%20%3D%20%22http%3A%2F%2Flocalhost%3A8983%2Fsolr%2Fsitecore_master_index%2Fsuggest%3Fsuggest.q%3D%22%20%2B%20request.term%20%2B%20%22%26suggest.cfq%3D%22%20%2B%20currentLanguage%20%2B%20%22%26wt%3Djson%22%3B%0A%20%20%20%20%20%20%20%20%24.getJSON(url%2C%20function(data)%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20response(%24.map(data.suggest.suggester%5Brequest.term%5D.suggestions%2C%20function(value)%20%7B%20return%20value.term%20%7D))%3B%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%7D%0A%7D)%3B%0A%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" />

In case if you face any Cross Domain issues, you need to add the filter component as mentioned in the below url to your web.xml http://marianoguerra.org/posts/enable-cors-in-apache-solr.html Don’t forget to change param-value if your SOLR is hosted in a different server.

 

Happy Searching!!

References:
https://cwiki.apache.org/confluence/display/solr/Suggester
https://blog.horizontalintegration.com/2015/07/20/configuring-solr-to-provide-search-suggestions/

Originally Posted at:  https://www.nttdatasitecore.com/Blog/2017/January/Faceted-Autocomplete-for-a-Sitecore-Multilingual-Site-using-SOLR-Suggester-API

Reading Update Package – Implementing Version Commands

As we know we have three version commands, AddVersionCommand, DeleteVersionCommand and ChangeVersionCommand. Version Commands will exist inside ChangeItemCommand. Version commands can be determined using the CommandPrefix property.

AddVersionCommand:

Version to be added and the target Item can be retrieved from the AddVersionCommand using ID, Language and Version. If the target Version already exists and if the collision behavior is set to Skip for the AddVersionCommand, version addition can be skipped. Version can be overwritten if collision behavior is set to force. Field values will be retrieved from Command using AddedVersion.Fields property and added to the newly created version.

if (command.CommandPrefix == (new AddVersionCommand()).CommandPrefix)
{
    AddVersionCommand addVersionCommand = (AddVersionCommand)command;
    Item itemVersionToAdd = master.GetItem(new Sitecore.Data.ID(addVersionCommand.ItemID), Sitecore.Globalization.Language.Parse(addVersionCommand.Language), Sitecore.Data.Version.Parse(addVersionCommand.Version));
    if (!(command.CollisionBehavior == CollisionBehavior.Skip && itemVersionToAdd != null))
    {
        //Adding Versions & Field Values to Item
        foreach (SyncField syncFieldToBeAdded in addVersionCommand.AddedVersion.Fields)
        {
            itemVersionToAdd.Fields[new Sitecore.Data.ID(syncFieldToBeAdded.FieldID)].Value = syncFieldToBeAdded.FieldValue;
        }
    }
}

DeleteVersionCommand:

Version to be deleted can be retrieved from the DeleteFieldCommand using ID, Language and Version,

if (command.CommandPrefix == (new DeleteVersionCommand()).CommandPrefix)
{
    DeleteVersionCommand deleteVersionCommand = (DeleteVersionCommand)command;
    Item itemVersionToDelete = master.GetItem(new Sitecore.Data.ID(deleteVersionCommand.ItemID), Sitecore.Globalization.Language.Parse(deleteVersionCommand.Language), Sitecore.Data.Version.Parse(deleteVersionCommand.Version));
    if (itemVersionToDelete != null)
    {
        //Removing Version from Item
        itemVersionToDelete.Versions.RemoveVersion();
    }
}

ChangeVersionCommand:

Version to be changed can be retrieved from ChangeVersionCommand. Fields of the Version to be changed can be retrieved from the Changes property of ChangeVersionCommand.

if (command.CommandPrefix == (new ChangeVersionCommand()).CommandPrefix)
{
    ChangeVersionCommand changeVersionCommand = (ChangeVersionCommand)command;
    Item itemVersionToChange = master.GetItem(new Sitecore.Data.ID(changeVersionCommand.ItemID), Sitecore.Globalization.Language.Parse(changeVersionCommand.Language), Sitecore.Data.Version.Parse(changeVersionCommand.Version));
    itemVersionToChange.Editing.BeginEdit();
    foreach (ICommand change in changeVersionCommand.Changes)
    {
        //Executing FieldCommands of Item Fields
        ExecuteFieldCommands(change);
    }
    itemVersionToChange.Editing.EndEdit();
}

We would be discussing about Field Commands and Property Commands in detail in the upcoming blogs.

Originally Posted at:  https://www.nttdatasitecore.com/Blog/2016/December/Reading-Update-Package-Implementing-Version-Commands