View Sitecore xConnect logs in Log2Console

Few days ago I blogged about how to view Commerce logs in Log2Console.

I searched a little bit how to  view  xConnect logs in real time, but I didn’t find any article how to add a custom serilog sink.

I made a small research and here is there solution:

To view xConnect logs in Log2Console you need to follow next steps :

  1. Add Serilog.Sinks.UDP.dll and Serilog.Sinks.PeriodicBatching.dll to your xConnect bin folder . (https://github.com/FantasticFiasco/serilog-sinks-udp-sample-dotnet-framework)
  2.  Add a new Serilog configuration  into :     yourxConnectfolder\App_Data\Config\Sitecore\CoreServices and name it : sc.Serilog.Udp.xml where you set the remoteAddress and remotePort:

3. To view xconnect logs from AutomationEngine, IndexWorker and ProcessEngine you need to copy above file  to the CoreServices folder (example  for IndexWorker : c:\inetpub\wwwroot\xconnectFolder\App_Data\jobs\continuous\IndexWorker\App_Data\config\sitecore\CoreServices\ )

4. Check log2console (I already setup the receivers)

log2consoleXconnect.PNG

Sitecore Commerce logviewing using Log2Console

To view Sitecore logs in Log2Console is pretty easy. Sitecore  Platform is using log4net and you can add an UdpAppender for logging in an external application.

On few blogs is described how to do it:

  1. http://blog.jan.hebnes.dk/2015/12/hackthedot-sitecore-logviewing-made.html
  2. https://community.sitecore.net/developers/f/8/t/136 ( few people described different tools for sitecore logs)
  3. https://publications.soulcode.agency/you-dont-monitor-your-local-sitecore-instance-close-enough/

Sitecore Commerce is a .NET Core application, and is using Serilog for log messages.

We will use an external nuget package to send UDP package over the network: https://github.com/FantasticFiasco/serilog-sinks-udp

To view the commerce logs in Log2Console we need to follow next steps:

  1. Add a settings in commerce config.json to enable/disable sending UDP packages over the network.

writetoudp2.JPG

2. Add reference to Serilog.Sinks.Udp nuget package

nugetpackage.JPG

3. Modify commerce engine to use Serilog.Sink.Udp

The UDP packages are sent on port 7071.

At the end of Startup.cs add next code:

var writeToUdp = false;

//check if writotoUdp is enabled
if (bool.TryParse(Configuration.GetSection(“Serilog:WritetoUDP”).Value, out writeToUdp))
{
if (writeToUdp)
Log.Logger = new LoggerConfiguration()
.WriteTo.Sink((ILogEventSink)Log.Logger)
.WriteTo.Udp(“localhost”, 7071, System.Net.Sockets.AddressFamily.InterNetwork)
.CreateLogger();
}

4. Add a log2console receiver, it will work on port 7071.

log2consoleReceiver.JPG

and commerce logs will appear on Log2console

log2consoleLogs.JPG

Sitecore MultiSite Robots.txt

Sitecore doesn’t have out of the box feature for multisite robots.txt.
If you have a disc file robots.txt for every site definition from web.config we will have same robots.txt file.

To create dinamically robots.txt for every website we need to do :

1. On root site we need to create new field with name Robots (Multi-line text field) where we add content for robots.txt

2.   Add  <handler trigger=”robots.txt” handler=”robots.ashx” />  into section    <customHandlers>

3. Add <add verb=”*” path=”robots.ashx” type=”YourNamespace.RobotsHandler, YourAssembly” name=”Robots” />

into section <handlers>

4. Add new Asp.NET Handler with next content

  /// <summary>
    /// Robots handler
    /// </summary>
    public class RobotsHandler : IHttpHandler
    {
        /// <summary>
        /// The default robots
        /// </summary>
        private string defaultRobots = "User-agent: * \r\n Disallow:";

        /// <summary>
        /// You will need to configure this handler in the Web.config file of your
        /// web and register it with IIS before being able to use it. For more information
        /// see the following link: http://go.microsoft.com/?linkid=8101007
        /// </summary>
        /// <returns>true if the <see cref="T:System.Web.IHttpHandler" /> instance is reusable; otherwise, false.</returns>
        public bool IsReusable
        {
            // Return false in case your Managed Handler cannot be reused for another request.
            // Usually this would be false in case you have some state information preserved per request.
            get { return true; }
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler" /> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext" /> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            string robotsTxt = defaultRobots;
            if ((Sitecore.Context.Site == null) || (Sitecore.Context.Database == null))
            {
                robotsTxt = defaultRobots;
            }
            Item homeItem = Sitecore.Context.Database.GetItem(Sitecore.Context.Site.StartPath);

            if (homeItem != null)
            {
                    //get content from Robots field
                    if (!string.IsNullOrEmpty(homeItem["Robots"]))
                    robotsTxt = homeItem.Fields["Robots"].Value;
            }

            context.Response.ContentType = "text/plain";
            context.Response.Write(robotsTxt);
        }
    }

Here you find what you need to add into Robots field .

Happy coding 🙂

Hide email address to robots

I think every webmaster want to hide email address from their website to robots crawler.
With Sitecore is really easy to do it, from server side we encoded email address and in frontend we decoded using javascript .

To encode email address we need to override GetLinkFieldValue processor.
I create a new configuration files, it will look like :

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sitecore.Pipelines.RenderField.GetLinkFieldValue, Sitecore.Kernel">
          <patch:attribute name="type">Modules.Pipelines.RenderField.GetEncrpytedLinkFieldValue, Modules</patch:attribute>
        </processor>
      </renderField>
    </pipelines>
  </sitecore>
</configuration>
 

GetEncrpytedLinkFieldValue class is very simple we just ovveride CreateRenderer method.


using Sitecore.Data.Items;
using Sitecore.Pipelines.RenderField;
using Sitecore.Xml.Xsl;

namespace Modules.Pipelines.RenderField
{
    /// <summary>
    /// GetEncrpytedLinkFieldValue
    /// </summary>
    public class GetEncrpytedLinkFieldValue : GetLinkFieldValue
    {
        /// <summary>
        /// Creates the renderer.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>Link Renderer</returns>
        protected override LinkRenderer CreateRenderer(Item item)
        {
            return new EncryptedEmailLinkRenderer(item);
        }
    }
}

EncryptedEmailLinkRender is overrinding LinkRenderer.
Just the Render method is overrinding from the base class.


using Sitecore;
using Sitecore.Collections;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Xml.Xsl;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

namespace Modules.Pipelines.RenderField
{
    public class EncryptedEmailLinkRenderer : LinkRenderer
    {
        /// <summary>
        ///
        /// </summary>
        private readonly char[] _delimiter = new char[] { '=', '&amp;' };

        public EncryptedEmailLinkRenderer(Item item) : base(item) { }

        /// <summary>
        /// Renders this instance.
        /// Override base method, for mailto
        /// </summary>
        ///
        public override RenderFieldResult Render()
        {
            string str8;
            SafeDictionary dictionary = new SafeDictionary();
            dictionary.AddRange(this.Parameters);
            if (MainUtil.GetBool(dictionary["endlink"], false))
            {
                return RenderFieldResult.EndLink;
            }
            Set set = Set.Create(new string[] { "field", "select", "text", "haschildren", "before", "after", "enclosingtag", "fieldname" });
            LinkField linkField = this.LinkField;
            if (linkField != null)
            {
                dictionary["title"] = StringUtil.GetString(new string[] { dictionary["title"], linkField.Title });
                dictionary["target"] = StringUtil.GetString(new string[] { dictionary["target"], linkField.Target });
                dictionary["class"] = StringUtil.GetString(new string[] { dictionary["class"], linkField.Class });
            }
            string str = string.Empty;
            string rawParameters = this.RawParameters;
            if (!string.IsNullOrEmpty(rawParameters) &amp;&amp; (rawParameters.IndexOfAny(this._delimiter) &lt; 0))
            {
                str = rawParameters;
            }
            if (string.IsNullOrEmpty(str))
            {
                Item targetItem = this.TargetItem;
                string str3 = (targetItem != null) ? targetItem.DisplayName : string.Empty;
                string str4 = (linkField != null) ? linkField.Text : string.Empty;
                str = StringUtil.GetString(new string[] { str, dictionary[&quot;text&quot;], str4, str3 });
            }
            string url = this.GetUrl(linkField);
            if (((str8 = this.LinkType) != null) &amp;&amp; (str8 == &quot;javascript&quot;))
            {
                dictionary[&quot;href&quot;] = &quot;#&quot;;
                dictionary[&quot;onclick&quot;] = StringUtil.GetString(new string[] { dictionary[&quot;onclick&quot;], url });
            }
            else if (this.LinkType == &quot;mailto&quot;)
            {
                // encrypting if type of link field is mailto

                var encryptedEmailAddress = EncodeEmailAddress(url.Replace(&quot;mailto:&quot;, &quot;&quot;));
                dictionary[&quot;href&quot;] = string.Format(&quot;javascript:SitecoreUtil.sendEmail('{0}')&quot;, encryptedEmailAddress);

                if (IsLinkTextEmailAddress(str))
                {
                    dictionary[&quot;class&quot;] = &quot;encrypted&quot;;
                    str = EncodeEmailAddress(str);
                }
            }
            else
            {
                dictionary[&quot;href&quot;] = HttpUtility.HtmlEncode(StringUtil.GetString(new string[] { dictionary[&quot;href&quot;], url }));
            }
            StringBuilder tag = new StringBuilder(&quot;&lt;a&quot;, 0x2f);
            foreach (KeyValuePair pair in dictionary)
            {
                string key = pair.Key;
                string str7 = pair.Value;
                if (!set.Contains(key.ToLowerInvariant()))
                {
                    FieldRendererBase.AddAttribute(tag, key, str7);
                }
            }
            tag.Append('&gt;');
            if (!MainUtil.GetBool(dictionary["haschildren"], false))
            {
                if (string.IsNullOrEmpty(str))
                {
                    return RenderFieldResult.Empty;
                }
                tag.Append(str);
            }
            RenderFieldResult result = new RenderFieldResult();
            result.FirstPart = tag.ToString();
            result.LastPart = "</a>";
            return result;
        }

        /// <summary>
        /// Encodes the email address.
        /// </summary>
        /// The email.
        ///
        private static string EncodeEmailAddress(string email)
        {
            return BitConverter.ToString(ASCIIEncoding.ASCII.GetBytes(email)).Replace("-", "");
        }

        /// <summary>
        /// Determines whether [is link text email address] [the specified link text].
        /// </summary>
        /// The link text.
        ///
        private static bool IsLinkTextEmailAddress(string linkText)
        {
            const string emailValidPattern = @"^[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.[a-zA-Z][a-zA-Z\.]*[a-zA-Z]$";
            var emailValidator = new Regex(emailValidPattern);

            return emailValidator.IsMatch(linkText);
        }
    }
}

All code can be found on github at : https://github.com/Sitecoreclimber/HideEmailAddress on develop branch.

Indexing PDF with Sitecore 7

For indexing PDF with Sitecore 7 and IFilter you have to follow next steps :

1) Copy all the Adobe iFilter .dll files into the “\System32\Inetsrv” folder. This is the working directory for IIS on Windows Server. The Adobe iFilter .dll files are stored at the “C:\Program Files\Adobe\Adobe PDF iFilter 9 for 64-bit platforms\bin” folder by default. Also you can use the “IFilter Explorer” tool to detect the folder where the .dll files are stored: http://www.citeknet.com/Products/IFilters/IFilterExplorer/tabid/62/Default.aspx For more details please see the screenshot: http://screencast.com/t/xmWukanM+

2) Delete all the files under the “Website/App_Data/MediaCache” folder;

3) Rebuild the Sitecore Search Indexes (Sitecore -> Control Panel -> Indexing -> Indexing Manager);

4) Clear the Sitecore cache (the http://{hostname}/sitecore/admin/cache.aspx tool);

5) Restart the IIS;

Disable Sitecore users if they are idle for a period

To disable users I create a Sitecore Scheduler Task that disable users
Please follow next steps :
1. You need to create class DisableUsers :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Security;
using Sitecore.Security;
using Sitecore.SecurityModel;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Data.Items;
using Sitecore.Tasks;

namespace Scheduler
{
 public class DisableUsers
 {

Database master = Sitecore.Configuration.Factory.GetDatabase("master");
 public void ExecuteDisableUsers(Item[] itemArray, CommandItem commandItem, ScheduleItem scheduledItem)
 {
 if (scheduledItem != null)
 {
 // logs to see if scheduler begin and end
 Log.Info("Schedule Disable Users: Execute Begin", this);
 Run();
 Log.Info("Scheduler Disable Users: Execute End", this);
 }
 }
 public void Run()
 {
 try
 {
 MembershipUserCollection memberCollection = Membership.GetAllUsers();
 //This is an item that contains name of ExceptUser
 string exceptuser = master.GetItem(Constants.ExceptUser)["ExceptUserFields"];
 DateTime dateDelimiterInactiveUsers = GetDateDelimiterForInactiveUsers();

foreach (System.Web.Security.MembershipUser member in memberCollection)
 {
 try
 {
 string usernameLower = member.UserName.ToLower();
 //
 if (!exceptuser.ToLower().Contains(usernameLower) && member.IsApproved && !usernameLower.Contains("anonymous"))
 {
 //We disable user IF:
 //user last activity date is older then date delimiter

if ((member.LastActivityDate < dateDelimiterInactiveUsers))
 {
 MakeUserInactive(member);
 UpdateUserDate(member);
 }
 //user disable date is setup and it's older then current date
 else if (UserIsSetupToBeDisableAfterADate(member))
 {
 MakeUserInactive(member);
 }
 }
 }
 catch (Exception exception)
 {
 Sitecore.Diagnostics.Log.Info(string.Format("Schedule Disable Users: Error: User: {0}, Problem: {1} {2}", member.UserName, exception.Message, exception.StackTrace), "");
 }
 }
 }
 catch (Exception exception)
 {
 Sitecore.Diagnostics.Log.Info(string.Format("Schedule Disable Users: Error: General Problem: {0} {1}", exception.Message, exception.StackTrace), "");
 }
 }
 /// <summary>
 /// Make and user inactive
 /// </summary>
 /// <param name="member"></param>
 public void MakeUserInactive(MembershipUser member)
 {
 //Make user inactive
 using (new SecurityDisabler())
 {
 member.IsApproved = false;
 Membership.UpdateUser(member);
 }
 Sitecore.Diagnostics.Log.Audit(string.Format("User {0} was disabled at {1}", member.UserName, String.Format("{0:dd-MM-yyyy}", DateTime.Now)), this);

}

/// <summary>
 /// Setup date delimiter from Current date and substract period setup in sitecore
 /// </summary>
 /// <returns></returns>
 private DateTime GetDateDelimiterForInactiveUsers()
 {
 DateTime dateDelimiterInactiveUsers = new DateTime();
 int months = -1, days = -1, hours = -1, minutes = -1;
 //MonthlyIdleTimeout will be something like : 06:05:04:03 : in this example 06 represent number of month , 05 is number of days, 04 is number of minutes , 03 number of seconds
 string[] time = master.GetItem(Constants.MonthlyIdleTimeout)["Title"].Split(':');

//Initialize months days hours and minutes with nevative values to be able to extract them from current date
 if (int.TryParse("-" + time[0], out months) && int.TryParse("-" + time[1], out days) && int.TryParse("-" + time[2], out hours) && int.TryParse("-" + time[3], out minutes))
 {
 dateDelimiterInactiveUsers = DateTime.Now.AddMonths(months).AddDays(days).AddHours(hours).AddMinutes(minutes);
 }
 return dateDelimiterInactiveUsers;
 }
 /// <summary>
 /// Update user disable date
 /// </summary>
 /// <param name="user"></param>
 /// <returns></returns>
 private void UpdateUserDate(MembershipUser member)
 {
 //Get User for Editing
 Sitecore.Security.Accounts.User user = Sitecore.Security.Accounts.User.FromName(member.UserName, true);

using (new SecurityDisabler())
 {
 Sitecore.Security.UserProfile userProfile = user.Profile;
 userProfile.SetCustomProperty("Date", String.Format("{0:dd-MM-yyyy}", DateTime.Now));
 userProfile.Save();
 }

Sitecore.Diagnostics.Log.Audit(string.Format("User {0} date was updated at {1}", member.UserName, String.Format("{0:dd-MM-yyyy}", DateTime.Now)), this);

}
 /// <summary>
 /// True if user disable date is setup and it's older then current date
 /// </summary>
 /// <param name="user"></param>
 /// <returns></returns>
 private bool UserIsSetupToBeDisableAfterADate(MembershipUser member)
 {

//Get User (we are using false so user Last Activity doesn't updates)
 Sitecore.Security.Accounts.User user = Sitecore.Security.Accounts.User.FromName(member.UserName, false);

//Get user Profile
 Sitecore.Security.UserProfile userProfile = user.Profile;

bool disableUser = false;

if (userProfile != null)
 {
 //If profile has disable data on user profile and the date is lower then the current date, then the user will be disabled
 string disableDateCustom = userProfile.GetCustomProperty("Date");
 if (!string.IsNullOrEmpty(disableDateCustom))
 {
 string[] dateComponents = disableDateCustom.Split('-');
 int day = -1, month = -1, year = -1;

if (int.TryParse(dateComponents[0], out day) && int.TryParse(dateComponents[1], out month) && int.TryParse(dateComponents[2], out year))
 {
 DateTime disableDate = new DateTime(year, month, day);
 if (disableDate < DateTime.Now)
 {
 disableUser = true;
 }
 }
 }
 }
 return disableUser;
 }
 }
}

&nbsp;

2. You need to create a command item ; Please check next image :

Commands Disable Scheduler

3. Please create scheduler item that will have above command atached :

disable users 2

Custom Tree List

Default TreeList Field on Sitecore doesn’t support dynamically source like on Multilist Field .

To make it dynammcally you need to follow next steps :

1 .  Create CustomTreeList class :

namespace    YourNamespace

{

public class CustomTreeList : TreeList
 {
 public new string Source
 {
 get { return base.Source; }
 set
 {
 string dataSourceStr = StringUtil
 .ExtractParameter("DataSource", value).Trim();
 if (dataSourceStr.StartsWith("query:"))
 {
 base.Source = value.Replace(dataSourceStr, ResolveDataSourceQuery(dataSourceStr));
 }
 else
 {
 base.Source = value.StartsWith("query:") ? ResolveDataSourceQuery(value) : value;
 }
 }
 }

private string ResolveDataSourceQuery(string sQuery)
 {
 sQuery = sQuery.Substring("query:".Length);
 Item contextItem = Sitecore.Context.ContentDatabase.Items[ItemID];
 Item queryItem = contextItem.Axes.SelectSingleItem(sQuery);
 if (queryItem != null)
 {
 return queryItem.Paths.FullPath;
 }
 return string.Empty;
 }

}

}

2. Add on web.config

<source mode=”on” namespace=”YouNameSpace” assembly=”YourAssembly” prefix=”contentExtension”/>

on <controlSources> section

3.    <fieldType name=”CustomTreeList” type=”YourNamespace.CustomTreeList,YourAssembly” />

  add on   App_Config\FieldTypes.config on <configuration> section .
4  . On Core database duplicate :
      /sitecore/system/Field types/List Types/TreeList , rename item to CustomTreeList .
     Rename Control field value like on below picture  .
     Custom Tree List Querable Sitecore
   Happy coding 🙂 .