Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,26 @@ private static void SerializePortalSettings(XmlWriter writer, PortalInfo portal,
writer.WriteElementString("userquota", portal.UserQuota.ToString(CultureInfo.InvariantCulture));
writer.WriteElementString("pagequota", portal.PageQuota.ToString(CultureInfo.InvariantCulture));

settingsDictionary.TryGetValue("PageHeadText", out setting);
if (!string.IsNullOrEmpty(setting))
{
writer.WriteElementString("pageheadtext", setting);
}
settingsDictionary.TryGetValue("PageHeadText", out setting);
if (!string.IsNullOrEmpty(setting) && !string.Equals(setting, "false", StringComparison.OrdinalIgnoreCase))
{
writer.WriteElementString("pageheadtext", setting);
}

var pageHeaderTags = PageHeaderTagInfo.GetPortalItems(((IPortalInfo)portal).PortalId);
if (pageHeaderTags.Count > 0)
{
writer.WriteStartElement("pageheadertags");
foreach (var item in pageHeaderTags)
{
writer.WriteStartElement("pageheadertag");
writer.WriteAttributeString("name", item.Name);
writer.WriteCData(item.Content ?? string.Empty);
writer.WriteEndElement();
}

writer.WriteEndElement();
}

settingsDictionary.TryGetValue("InjectModuleHyperLink", out setting);
if (!string.IsNullOrEmpty(setting))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -962,10 +962,27 @@ private void ParsePortalSettings(XmlNode nodeSettings, int portalId)
PortalController.UpdatePortalSetting(this.portalController, portalId, "ControlPanelVisibility", XmlUtils.GetNodeValue(nodeSettings, "controlpanelvisibility"));
}

if (!string.IsNullOrEmpty(XmlUtils.GetNodeValue(nodeSettings, "pageheadtext", string.Empty)))
{
PortalController.UpdatePortalSetting(this.portalController, portalId, "PageHeadText", XmlUtils.GetNodeValue(nodeSettings, "pageheadtext", string.Empty));
}
if (!string.IsNullOrEmpty(XmlUtils.GetNodeValue(nodeSettings, "pageheadtext", string.Empty)))
{
PortalController.UpdatePortalSetting(this.portalController, portalId, "PageHeadText", XmlUtils.GetNodeValue(nodeSettings, "pageheadtext", string.Empty));
}

var pageHeaderTagNodes = nodeSettings.SelectNodes("pageheadertags/pageheadertag");
if (pageHeaderTagNodes != null && pageHeaderTagNodes.Count > 0)
{
var items = new List<PageHeaderTagInfo>();
foreach (XmlNode node in pageHeaderTagNodes)
{
items.Add(new PageHeaderTagInfo
{
Name = node.Attributes?["name"]?.Value,
Content = node.InnerText,
});
}

PageHeaderTagInfo.SavePortalItems(portalId, items);
PortalController.UpdatePortalSetting(this.portalController, portalId, "PageHeadText", "false");
}

if (!string.IsNullOrEmpty(XmlUtils.GetNodeValue(nodeSettings, "injectmodulehyperlink", string.Empty)))
{
Expand Down
152 changes: 152 additions & 0 deletions DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information

namespace DotNetNuke.Entities.Tabs
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Portals;

public class PageHeaderTagInfo
{
public const string SettingPrefix = "PageHeaderTag_";

public string Name { get; set; }

public string Content { get; set; }

public string SettingName => SettingPrefix + this.Name;

public static IList<PageHeaderTagInfo> FromSettings(IDictionary settings)
{
var items = new List<PageHeaderTagInfo>();
if (settings == null)
{
return items;
}

foreach (DictionaryEntry entry in settings)
{
var key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
if (string.IsNullOrEmpty(key) || !key.StartsWith(SettingPrefix, StringComparison.Ordinal))
{
continue;
}

var name = key.Substring(SettingPrefix.Length);
if (string.IsNullOrWhiteSpace(name))
{
continue;
}

items.Add(new PageHeaderTagInfo
{
Name = name,
Content = Convert.ToString(entry.Value, CultureInfo.InvariantCulture),
});
}

return items.OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase).ToList();
}

public static IList<PageHeaderTagInfo> GetTabItems(int tabId)
{
return FromSettings(TabController.Instance.GetTabSettings(tabId));
}

public static IList<PageHeaderTagInfo> GetPortalItems(int portalId, string cultureCode = null)
{
var settings = string.IsNullOrEmpty(cultureCode)
? PortalController.Instance.GetPortalSettings(portalId)
: PortalController.Instance.GetPortalSettings(portalId, cultureCode);

return FromSettings(new Hashtable(settings));
}

public static string Render(IEnumerable<PageHeaderTagInfo> items)
{
if (items == null)
{
return string.Empty;
}

return string.Join(Environment.NewLine, items.Select(item => item.Content).Where(content => !string.IsNullOrWhiteSpace(content)));
}

public static void SaveTabItems(int tabId, IEnumerable<PageHeaderTagInfo> items)
{
var normalizedItems = Normalize(items);
var targetSettingNames = new HashSet<string>(normalizedItems.Select(item => item.SettingName), StringComparer.OrdinalIgnoreCase);
var existingSettingNames = TabController.Instance.GetTabSettings(tabId)
.Cast<DictionaryEntry>()
.Select(entry => Convert.ToString(entry.Key, CultureInfo.InvariantCulture))
.Where(key => !string.IsNullOrEmpty(key))
.ToList();

foreach (var key in existingSettingNames)
Comment thread
mitchelsellers marked this conversation as resolved.
{
if (key.StartsWith(SettingPrefix, StringComparison.Ordinal) && !targetSettingNames.Contains(key))
{
TabController.Instance.DeleteTabSetting(tabId, key);
}
}

foreach (var item in normalizedItems)
{
TabController.Instance.UpdateTabSetting(tabId, item.SettingName, item.Content);
}
}
Comment on lines +82 to +104

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SaveTabItems/SavePortalItems treat items == null as an empty list (via Normalize), which results in deleting all existing PageHeaderTag_ settings. Several callers pass pageSettings.PageHeaderTags?.Select(...), so omitting the pageHeaderTags field from an update request will unintentionally wipe existing tags. Consider making items == null a no-op (preserve existing), and require an explicit empty list to clear tags.

Copilot uses AI. Check for mistakes.

public static void SavePortalItems(int portalId, IEnumerable<PageHeaderTagInfo> items, string cultureCode = null)
{
var normalizedItems = Normalize(items);
var targetSettingNames = new HashSet<string>(normalizedItems.Select(item => item.SettingName), StringComparer.OrdinalIgnoreCase);
var existingSettingNames = (string.IsNullOrEmpty(cultureCode)
? PortalController.Instance.GetPortalSettings(portalId)
: PortalController.Instance.GetPortalSettings(portalId, cultureCode))
.Keys
.Where(key => !string.IsNullOrEmpty(key))
.ToList();

foreach (var key in existingSettingNames)
Comment thread
mitchelsellers marked this conversation as resolved.
{
if (key.StartsWith(SettingPrefix, StringComparison.Ordinal) && !targetSettingNames.Contains(key))
{
PortalController.DeletePortalSetting(portalId, key);

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SavePortalItems deletes existing PageHeaderTag_ settings using PortalController.DeletePortalSetting(portalId, key) which targets neutral settings only. When cultureCode is provided, this will fail to remove localized settings (and may delete the neutral setting instead). Use the DeletePortalSetting(portalId, settingName, cultureCode) overload (or pass Null.NullString when culture is null) to delete in the same culture scope you are updating.

Suggested change
PortalController.DeletePortalSetting(portalId, key);
PortalController.DeletePortalSetting(portalId, key, cultureCode ?? Null.NullString);

Copilot uses AI. Check for mistakes.
}
}

foreach (var item in normalizedItems)
{
PortalController.Instance.UpdatePortalSetting(portalId, item.SettingName, item.Content, true, cultureCode ?? Null.NullString);
}
}

private static List<PageHeaderTagInfo> Normalize(IEnumerable<PageHeaderTagInfo> items)
Comment thread
mitchelsellers marked this conversation as resolved.
{
if (items == null)
{
return new List<PageHeaderTagInfo>();
}

return items
.Where(item => item != null)
.Select(item => new PageHeaderTagInfo
{
Name = (item.Name ?? string.Empty).Trim(),
Content = item.Content ?? string.Empty,
})
.Where(item => !string.IsNullOrWhiteSpace(item.Name) && !string.IsNullOrWhiteSpace(item.Content))
.GroupBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
.Select(group => group.Last())
.OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ protected virtual void ProcessImportPage(ExportTab otherTab, IList<ExportTab> ex
}

SetTabData(localTab, otherTab);
if (this.Repository.GetRelatedItems<ExportTabSetting>(otherTab.Id).Any(setting => setting.SettingName.StartsWith(PageHeaderTagInfo.SettingPrefix, StringComparison.Ordinal)))
{
localTab.PageHeadText = null;
}

localTab.StateID = this.GetLocalStateId(otherTab.StateID);
var parentId = this.IgnoreParentMatch ? otherTab.ParentId.GetValueOrDefault(Null.NullInteger) : TryFindLocalParentTabId(otherTab, exportedTabs, localTabs);
if (parentId == -1 && otherTab.ParentId > 0)
Expand Down Expand Up @@ -327,6 +332,11 @@ protected virtual void ProcessImportPage(ExportTab otherTab, IList<ExportTab> ex
{
localTab = new TabInfo { PortalID = portalId };
SetTabData(localTab, otherTab);
if (this.Repository.GetRelatedItems<ExportTabSetting>(otherTab.Id).Any(setting => setting.SettingName.StartsWith(PageHeaderTagInfo.SettingPrefix, StringComparison.Ordinal)))
{
localTab.PageHeadText = null;
}

localTab.StateID = this.GetLocalStateId(otherTab.StateID);
var parentId = this.IgnoreParentMatch ? otherTab.ParentId.GetValueOrDefault(Null.NullInteger) : TryFindLocalParentTabId(otherTab, exportedTabs, localTabs);
var checkPartial = false;
Expand Down
15 changes: 15 additions & 0 deletions DNN Platform/Website/Components/Portals/portal.template.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@
<xs:element name="controlpanelsecurity" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="controlpanelvisibility" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="pageheadtext" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="pageheadertags" minOccurs="0" maxOccurs="1">
Comment thread
mitchelsellers marked this conversation as resolved.
<xs:complexType>
<xs:sequence>
<xs:element name="pageheadertag" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="name" type="xs:string" use="required" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="injectmodulehyperlink" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="addcompatiblehttpheader" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="allowuseruiculture" type="xs:string" minOccurs="0" maxOccurs="1" />
Expand Down
56 changes: 40 additions & 16 deletions DNN Platform/Website/Default.aspx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,15 +606,37 @@ private void InitializePage()
this.Page.Header.Controls.AddAt(0, new LiteralControl(this.Comment));
}

if (this.PortalSettings.ActiveTab.PageHeadText != Null.NullString && !Globals.IsAdminControl())
{
this.Page.Header.Controls.Add(new LiteralControl(this.PortalSettings.ActiveTab.PageHeadText));
}

if (!string.IsNullOrEmpty(this.PortalSettings.PageHeadText))
{
this.metaPanel.Controls.Add(new LiteralControl(this.PortalSettings.PageHeadText));
}
if (this.PortalSettings.ActiveTab.PageHeadText != Null.NullString && !Globals.IsAdminControl())
{
var tabHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID));
this.Page.Header.Controls.Add(new LiteralControl(string.IsNullOrEmpty(tabHeaderTags) ? this.PortalSettings.ActiveTab.PageHeadText : tabHeaderTags));
}

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.PortalSettings.ActiveTab.PageHeadText != Null.NullString will evaluate true when PageHeadText is null (since null != ""). After the migration script sets Tabs.PageHeadText = NULL, this branch will run unexpectedly. Use !string.IsNullOrEmpty(this.PortalSettings.ActiveTab.PageHeadText) (or coalesce to empty) to distinguish "has legacy text" vs "no legacy text" reliably.

Copilot uses AI. Check for mistakes.
else if (!Globals.IsAdminControl())
{
var tabHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID));

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageHeaderTagInfo.GetTabItems(...)/Render(...) is executed multiple times during a single request (here and again for the robots meta check). Since GetTabItems hits tab settings storage, consider retrieving/rendering the tab header tags once into a local variable and reusing it for both injection and later HeaderTextRegex checks.

Suggested change
if (this.PortalSettings.ActiveTab.PageHeadText != Null.NullString && !Globals.IsAdminControl())
{
var tabHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID));
this.Page.Header.Controls.Add(new LiteralControl(string.IsNullOrEmpty(tabHeaderTags) ? this.PortalSettings.ActiveTab.PageHeadText : tabHeaderTags));
}
else if (!Globals.IsAdminControl())
{
var tabHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID));
var tabHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID));
if (this.PortalSettings.ActiveTab.PageHeadText != Null.NullString && !Globals.IsAdminControl())
{
this.Page.Header.Controls.Add(new LiteralControl(string.IsNullOrEmpty(tabHeaderTags) ? this.PortalSettings.ActiveTab.PageHeadText : tabHeaderTags));
}
else if (!Globals.IsAdminControl())
{

Copilot uses AI. Check for mistakes.
if (!string.IsNullOrEmpty(tabHeaderTags))
{
this.Page.Header.Controls.Add(new LiteralControl(tabHeaderTags));
}
}

var portalPageHeadText = string.Equals(this.PortalSettings.PageHeadText, "false", StringComparison.OrdinalIgnoreCase)
? string.Empty
: this.PortalSettings.PageHeadText;

if (!string.IsNullOrEmpty(portalPageHeadText))
{
var portalHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode));
this.metaPanel.Controls.Add(new LiteralControl(string.IsNullOrEmpty(portalHeaderTags) ? portalPageHeadText : portalHeaderTags));
}
else
{
var portalHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode));

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Portal header tags are fetched/rendered multiple times in this method (both branches here and again in the robots meta check). To reduce repeated portal-settings access and string concatenations, compute portalHeaderTags once (and potentially the combined legacy+new string used for HeaderTextRegex) and reuse it.

Suggested change
if (!string.IsNullOrEmpty(portalPageHeadText))
{
var portalHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode));
this.metaPanel.Controls.Add(new LiteralControl(string.IsNullOrEmpty(portalHeaderTags) ? portalPageHeadText : portalHeaderTags));
}
else
{
var portalHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode));
var portalHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode));
if (!string.IsNullOrEmpty(portalPageHeadText))
{
this.metaPanel.Controls.Add(new LiteralControl(string.IsNullOrEmpty(portalHeaderTags) ? portalPageHeadText : portalHeaderTags));
}
else
{

Copilot uses AI. Check for mistakes.
if (!string.IsNullOrEmpty(portalHeaderTags))
{
this.metaPanel.Controls.Add(new LiteralControl(portalHeaderTags));
}
}

// set page title
if (UrlUtils.InPopUp())
Expand Down Expand Up @@ -729,13 +751,15 @@ private void InitializePage()
this.Copyright = string.Concat("Copyright (c) ", DateTime.Now.Year, " by ", this.PortalSettings.PortalName);
}

// META generator
this.Generator = string.Empty;

// META Robots - hide it inside popups and if PageHeadText of current tab already contains a robots meta tag
if (!UrlUtils.InPopUp() &&
!(HeaderTextRegex.IsMatch(this.PortalSettings.ActiveTab.PageHeadText) ||
HeaderTextRegex.IsMatch(this.PortalSettings.PageHeadText)))
// META generator
this.Generator = string.Empty;

var tabLegacyPageHeadText = this.PortalSettings.ActiveTab.PageHeadText ?? string.Empty;

// META Robots - hide it inside popups and if PageHeadText of current tab already contains a robots meta tag
if (!UrlUtils.InPopUp() &&
!(HeaderTextRegex.IsMatch(PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID)) + tabLegacyPageHeadText) ||
Comment thread
mitchelsellers marked this conversation as resolved.
Outdated
HeaderTextRegex.IsMatch(PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode)) + portalPageHeadText)))
{
this.MetaRobots.Visible = true;
var allowIndex = true;
Expand Down
5 changes: 4 additions & 1 deletion DNN Platform/Website/Portals/_default/Blank Website.template
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
<hostspace>0</hostspace>
<userquota>0</userquota>
<pagequota>0</pagequota>
<pageheadtext>&lt;meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /&gt;</pageheadtext>
<pageheadtext></pageheadtext>
Comment thread
mitchelsellers marked this conversation as resolved.
Outdated
<pageheadertags>
<pageheadertag name="Default"><![CDATA[<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />]]></pageheadertag>
</pageheadertags>
<injectmodulehyperlink>True</injectmodulehyperlink>
<addcompatiblehttpheader>IE=edge</addcompatiblehttpheader>
<sitemapcachedays>1</sitemapcachedays>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
<hostspace>0</hostspace>
<userquota>0</userquota>
<pagequota>0</pagequota>
<pageheadtext>&lt;meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /&gt;</pageheadtext>
<pageheadtext></pageheadtext>
<pageheadertags>
<pageheadertag name="Default"><![CDATA[<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />]]></pageheadertag>
</pageheadertags>
<injectmodulehyperlink>True</injectmodulehyperlink>
<addcompatiblehttpheader>IE=edge</addcompatiblehttpheader>
<sitemapcachedays>1</sitemapcachedays>
Expand Down
Loading
Loading