So, I have a rather complex issue to detail out here. the TL;DR is that Link Manager is returning inconsistent results in the GetServerUrl() method when given what appears to be the same parameters.
To set this up, I am using Sitecore 8.2 Update 2 with EXM 3.4 Update 1. I have, for all practical purposes, a vanilla install and setup of both Sitecore and EXM, with the exception of an additional site entry, which is the site that we are working with, and a content Home node.
Symptom of Issue
Dispatching an Email (Scenario 1)
When I dispatch a very simple email from EXM, that contains one link, which is to the Home page of the site that is configured as the EXM Site, the link that is created looks like this:
http://brandname.local/sitecore/RedirectUrlPage.aspx?ec_contact_id=1814C7BC2B434DC096F74115AB45104A&ec_message_id=2E6A28769FBB45F2AF36F956FA3B8021&ec_url=%2f%3a%2f%2fbrandname.local%2f
The parameter ec_url is the parameter that I am paying attention to. When EXM resolves the Click action, it redirects the user to http://brandname.local/://brandname.local/. That link generates a protection error because you can't have colons in the url past the protocol portion.
Site Scheme
I am aware that the reason why ://brandname.local/ is created is because the site definition doesn't have the scheme attribute set to http. While that makes this a valid link for the purposes of the redirect, the proper link should be relative, not absolute (if I understand correctly).
Expected Result
Looking at the Email Message in EXM (Scenario 2)
When I look at the email message in EXM (Third Tab[Message] when looking at a dispatch), all of the same pipelines run that generate these links, except that now the link is generated correctly. I also found out that when looking at the email from here, PreviewMode is false, so it creates the same url as if dispatching. By correctly, I mean, because SiteResolving returns the brandname site, and EXM is configured to use the brandname site, the links generated look like this:
When that is clicked on, it resolves/redirects to http://brandname.local/ which is the expected behavior.
Enter LinkManager
EXM uses LinkManager to resolve these links.
In the HandleInternalLink
processor for the <modifyHyperlink>
pipeline, you find the following code:
DynamicLink result;
if (args.Url.IndexOf("~/link.aspx?", StringComparison.InvariantCulture) >= 0 &&
DynamicLink.TryParse(args.Url, out result))
{
var defaultUrlOptions = LinkManager.GetDefaultUrlOptions();
defaultUrlOptions.SiteResolving = true;
defaultUrlOptions.Site = SiteContext.GetSite(args.WebsiteConfigurationName);
args.Url = LinkManager.GetItemUrl(new ItemUtilExt().GetItem(result.ItemId), defaultUrlOptions);
}
Now, becasuse my link in the Rich Text box is a dynamic link that points to a Home item, this processor uses LinkManager.GetItemUrl() to fetch the Url.
WebsiteConfigurationName
Note that args.WebsiteConfigurationName equals the site name that is configured on the EXM Manager Root. In both Scenario 1 and 2 (because the same Manager Root is being used), this is set to brandname. This maps back to Sitecore's site definition.
SiteResolving
I am also aware that SiteResolving works best when there is a targetHostName defined on the site definition.
LinkManager GetServerUrlElement()
Way down the line of LinkManager.GetItemUrl()
, LinkManager executes the GetServerUrlElement()
method, that does the heavy lifting of creating the server element portion of this.
protected virtual string GetServerUrlElement(SiteInfo siteInfo)
{
SiteContext site = Context.Site;
string str1 = site != null ? site.Name : string.Empty;
string hostName = this.GetHostName();
string str2 = this.AlwaysIncludeServerUrl ? WebUtil.GetServerUrl() : string.Empty;
if (siteInfo == null)
return str2;
string str3;
if (string.IsNullOrEmpty(siteInfo.HostName) || string.IsNullOrEmpty(hostName) || !siteInfo.Matches(hostName))
str3 = StringUtil.GetString(new string[2]
{
this.GetTargetHostName(siteInfo),
hostName
});
else
str3 = hostName;
string str4 = str3;
string str5 = StringUtil.GetString(new string[2]{ siteInfo.Scheme, this.GetScheme() });
int defaultValue = MainUtil.GetInt(siteInfo.Port, WebUtil.GetPort());
int port = WebUtil.GetPort();
int num = MainUtil.GetInt(siteInfo.ExternalPort, defaultValue);
if (num != defaultValue)
{
if (this.AlwaysIncludeServerUrl)
str2 = num == 80 ? string.Format("{0}://{1}", (object) str5, (object) this.GetHostName()) : string.Format("{0}://{1}:{2}", (object) str5, (object) this.GetHostName(), (object) num);
defaultValue = num;
}
if (!this.AlwaysIncludeServerUrl && siteInfo.Name.Equals(str1, StringComparison.OrdinalIgnoreCase) && hostName.Equals(str4, StringComparison.OrdinalIgnoreCase) || (string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0))
return str2;
string scheme = this.GetScheme();
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase;
if (str4.Equals(hostName, comparisonType) && defaultValue == port && str5.Equals(scheme, comparisonType))
return str2;
string str6 = str5 + "://" + str4;
if (defaultValue > 0 && defaultValue != 80)
str6 = str6 + ":" + (object) defaultValue;
return str6;
}
Debugging (using dotPeek Symbol Server) reveals the following portions of the above code are the exits points for each scenario.
Scenario 1 Returns "://brandname.local" At
string str6 = str5 + "://" + str4;
if (defaultValue > 0 && defaultValue != 80)
str6 = str6 + ":" + (object) defaultValue;
return str6;
Scenario 2 Returns ""(String.Empty) At
if (str4.Equals(hostName, comparisonType) && defaultValue == port && str5.Equals(scheme, comparisonType))
return str2;
Configuration
Site Config
<site name="brandname" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content/BrandName" startItem="/Home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="1000MB" renderingParametersCacheSize="200MB" xslCacheSize="100MB" filteredItemsCacheSize="200MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" hostName="brandname.local" notFoundPage="/sitecore/content/BrandName/Home/404" itemRedirectPath="/sitecore/content/BrandName/Redirects/ItemBased" legacyRedirectPath="/sitecore/content/BrandName/Redirects/Legacy" patch:source="BrandName.SiteDefintion.config"/>
Site, Site Info, defaultUrlOptions
The following is the raw output of my debugging session, looking at the three pieces that are used in HandleInternalLink
processor:
- defaultUrlOptions.Site
- defaultUrlOptions
- defaultUrlOptions.Site.SiteInfo
I have indicated the pieces that are different according to BeyondCompare.
Scenario 1 - When the link doesn't generate right
defaultUrlOptions.Site"brandname"
AllowDebug: true
BrowserTitle: "BrandName - Sitecore"
CacheHtml: true <--- Different
CacheMedia: true <--- Different
Caches: {Sitecore.Sites.SiteCaches}
ContentDatabase: null
"": ""
ContentStartItem: ""
ContentStartPath: "/sitecore/content/BrandName"
Database: {web} <--- Different
DefaultDevice: ""
Device: ""
DictionaryDomain: ""
DisableBrowserCaching: true
DisableClientData: false
DisableFiltering: false
DisableWebEditEditing: false
DisableWebEditSelecting: false
DisableXmlControls: false
DisplayDate: {3/10/2017 2:40:34 AM} <--- Different
DisplayMode: Normal <--- Different
Domain: extranet <--- Different
DrawDots: false <--- Different
EnableDebugger: true
EnableLinkedItems: true
EnablePreview: true
EnableWebEdit: true
EnableWorkflow: false <--- Different
FilterItems: false <--- Different
HostName: "brandname.local"
Language: "en"
LoginPage: ""
MasterDatabase: null
MediaCachePath: "/App_Data/MediaCache/brandname"
Name: "brandname"
Notifications: {Sitecore.Events.NotificationContext}
Page: {Sitecore.Layouts.PageContext}
PhysicalFolder: "/"
Properties: {System.Collections.Specialized.NameValueCollection}
Request: {Sitecore.Sites.SiteRequest}
RequireLogin: false
Resources: {Sitecore.Resources.ResourceContext}
Response: {Sitecore.Sites.SiteResponse}
RootPath: "/sitecore/content/BrandName"
SiteInfo: "brandname" ("web"#"")
StartItem: "/Home"
StartPath: "/sitecore/content/BrandName/Home"
TargetHostName: "brandname.local"
VirtualFolder: "/"
XmlControlPage: "/layouts/xmlcontrol.aspx"
defaultUrlOptions
{Sitecore.Links.UrlOptions}
AddAspxExtension: false
AlwaysIncludeServerUrl: false
EncodeNames: true
"en": "en"
LanguageEmbedding: Never
LanguageLocation: FilePath
LowercaseUrls: true
ShortenUrls: true
Site: "brandname"
SiteResolving: true
UseDisplayName: false
defaultUrlOptions.Site.SiteInfo
"brandname" ("web"#"")
AllowDebug: true
BrowserTitle: "BrandName - Sitecore"
CacheHtml: true
CacheMedia: true
ContentLanguage: ""
ContentStartItem: ""
Database: "web"
DefaultDevice: ""
Device: ""
DictionaryDomain: ""
DisableBrowserCaching: true
DisableClientData: false
DisableXmlControls: false
Domain: "extranet"
EnableDebugger: true
EnableFieldLanguageFallback: false
EnableItemLanguageFallback: false
EnableLinkedItems: false
EnablePreview: true
EnableWebEdit: true
EnableWorkflow: false
EnforceVersionPresence: false
ExternalPort: 0
FilterItems: false
FilteredItemsCache: {Sitecore.Caching.FilteredItemsCache}
HostName: "brandname.local"
HtmlCache: {Sitecore.Caching.HtmlCache}
HtmlCacheClearLatency: ""
IsActive: true
Language: ""
LoginPage: ""
MasterDatabase: ""
MediaCachePath: ""
Name: "brandname"
PhysicalFolder: "/"
Port: 0
Properties: {System.Collections.Specialized.NameValueCollection}
RegistryCache: {Sitecore.Caching.RegistryCache}
RenderingParametersCache: null
RequireLogin: false
RootPath: "/sitecore/content/BrandName"
Scheme: ""
StartItem: "/Home"
TargetHostName: ""
ViewStateCache: {Sitecore.Caching.ViewStateCache}
VirtualFolder: "/"
XmlControlPage: ""
XslCache: {Sitecore.Caching.XslCache}
Scenario 2 - When the link generates correctly
defaultUrlOptions.Site"brandname"
AllowDebug: true
BrowserTitle: "BrandName - Sitecore"
CacheHtml: false <--- Different
CacheMedia: false <--- Different
Caches: {Sitecore.Sites.SiteCaches}
ContentDatabase: null
"": ""
ContentStartItem: ""
ContentStartPath: "/sitecore/content/BrandName"
Database: {master} <--- Different
DefaultDevice: ""
Device: ""
DictionaryDomain: ""
DisableBrowserCaching: true
DisableClientData: false
DisableFiltering: false
DisableWebEditEditing: false
DisableWebEditSelecting: false
DisableXmlControls: false
DisplayDate: {3/10/2017 2:44:46 AM} <--- Different
DisplayMode: Edit <--- Different
Domain: sitecore <--- Different
DrawDots: true <--- Different
EnableDebugger: true
EnableLinkedItems: true
EnablePreview: true
EnableWebEdit: true
EnableWorkflow: true <--- Different
FilterItems: true <--- Different
HostName: "brandname.local"
Language: "en"
LoginPage: ""
MasterDatabase: null
MediaCachePath: "/App_Data/MediaCache/brandname"
Name: "brandname"
Notifications: {Sitecore.Events.NotificationContext}
Page: {Sitecore.Layouts.PageContext}
PhysicalFolder: "/"
Properties: {System.Collections.Specialized.NameValueCollection}
Request: {Sitecore.Sites.SiteRequest}
RequireLogin: false
Resources: {Sitecore.Resources.ResourceContext}
Response: {Sitecore.Sites.SiteResponse}
RootPath: "/sitecore/content/BrandName"
SiteInfo: "brandname" ("web"#"")
StartItem: "/Home"
StartPath: "/sitecore/content/BrandName/Home"
TargetHostName: "brandname.local"
VirtualFolder: "/"
XmlControlPage: "/layouts/xmlcontrol.aspx"
defaultUrlOptions
{Sitecore.Links.UrlOptions}
AddAspxExtension: false
AlwaysIncludeServerUrl: false
EncodeNames: true
"en": "en"
LanguageEmbedding: Never
LanguageLocation: FilePath
LowercaseUrls: true
ShortenUrls: true
Site: "brandname"
SiteResolving: true
UseDisplayName: false
defaultUrlOptions.Site.SiteInfo
"brandname" ("web"#"")
AllowDebug: true
BrowserTitle: "BrandName - Sitecore"
CacheHtml: true
CacheMedia: true
ContentLanguage: ""
ContentStartItem: ""
Database: "web"
DefaultDevice: ""
Device: ""
DictionaryDomain: ""
DisableBrowserCaching: true
DisableClientData: false
DisableXmlControls: false
Domain: "extranet"
EnableDebugger: true
EnableFieldLanguageFallback: false
EnableItemLanguageFallback: false
EnableLinkedItems: false
EnablePreview: true
EnableWebEdit: true
EnableWorkflow: false
EnforceVersionPresence: false
ExternalPort: 0
FilterItems: false
FilteredItemsCache: {Sitecore.Caching.FilteredItemsCache}
HostName: "brandname.local"
HtmlCache: {Sitecore.Caching.HtmlCache}
HtmlCacheClearLatency: ""
IsActive: true
Language: ""
LoginPage: ""
MasterDatabase: ""
MediaCachePath: ""
Name: "brandname"
PhysicalFolder: "/"
Port: 0
Properties: {System.Collections.Specialized.NameValueCollection}
RegistryCache: {Sitecore.Caching.RegistryCache}
RenderingParametersCache: null
RequireLogin: false
RootPath: "/sitecore/content/BrandName"
Scheme: ""
StartItem: "/Home"
TargetHostName: ""
ViewStateCache: {Sitecore.Caching.ViewStateCache}
VirtualFolder: "/"
XmlControlPage: ""
XslCache: {Sitecore.Caching.XslCache}
Question (Finally)
What in the world am I missing that would cause LinkManager to operate so differently between these two scenarios? My understanding is that EXM, if the site configured on the Manager Root and the Site resolved by the item are the same site, ec_url should be a relative URL. But for some reason, in one scenario, I am getting an absolute URL.