Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Copy link

Copilot AI Apr 30, 2026

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, cultureCode ?? Null.NullString);
}
}

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();
}
}
}
3 changes: 1 addition & 2 deletions DNN Platform/Library/Entities/Tabs/TabInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ public ArrayList Modules

/// <summary>Gets or sets the page head text, i.e. content to render in the <c>&lt;head&gt;</c> of the page.</summary>
[XmlElement("pageheadtext")]
[Obsolete("Deprecated in DotNetNuke 10.3.2. Use PageHeaderTagInfo instead. Scheduled removal in v12.0.0.")]
public string PageHeadText { get; set; }

/// <summary>Gets a list of the names of the panes available to the page.</summary>
Expand Down Expand Up @@ -915,7 +916,6 @@ public TabInfo Clone()
ContainerPath = this.ContainerPath,
IsSuperTab = this.IsSuperTab,
RefreshInterval = this.RefreshInterval,
PageHeadText = this.PageHeadText,
IsSecure = this.IsSecure,
PermanentRedirect = this.PermanentRedirect,
IsSystem = this.IsSystem,
Expand Down Expand Up @@ -978,7 +978,6 @@ public override void Fill(IDataReader dr)
this.EndDate = Null.SetNullDateTime(dr["EndDate"], DateTimeKind.Utc);
this.HasChildren = Null.SetNullBoolean(dr["HasChildren"]);
this.RefreshInterval = Null.SetNullInteger(dr["RefreshInterval"]);
this.PageHeadText = Null.SetNullString(dr["PageHeadText"]);
this.IsSecure = Null.SetNullBoolean(dr["IsSecure"]);
this.PermanentRedirect = Null.SetNullBoolean(dr["PermanentRedirect"]);
this.SiteMapPriority = Null.SetNullSingle(dr["SiteMapPriority"]);
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace Dnn.ExportImport.Components.Services
using DotNetNuke.Abstractions.Logging;
using DotNetNuke.Common;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Tabs;
using DotNetNuke.Services.Localization;

using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -73,7 +74,7 @@ public override void ExportData(ExportImportJob exportJob, ExportDto exportDto)

// Migrate only allowed portal settings.
portalSettings =
portalSettings.Where(x => settingToMigrate.Any(setting => setting.Trim().Equals(x.SettingName, StringComparison.OrdinalIgnoreCase))).ToList();
portalSettings.Where(x => settingToMigrate.Any(setting => setting.Trim().Equals(x.SettingName, StringComparison.OrdinalIgnoreCase)) || x.SettingName.StartsWith(PageHeaderTagInfo.SettingPrefix, StringComparison.OrdinalIgnoreCase)).ToList();

// Update the total items count in the check points. This should be updated only once.
this.CheckPoint.TotalItems = this.CheckPoint.TotalItems <= 0 ? portalSettings.Count : this.CheckPoint.TotalItems;
Expand Down
17 changes: 15 additions & 2 deletions DNN Platform/Website/Components/Portals/portal.template.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,21 @@
<xs:element name="controlpanelmode" type="xs:string" minOccurs="0" maxOccurs="1" />
<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 Expand Up @@ -317,7 +331,6 @@
</xs:complexType>
</xs:element>
<xs:element name="refreshinterval" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="pageheadtext" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="permanentredirect" type="xs:boolean" minOccurs="0" maxOccurs="1" />
<xs:element name="panes" minOccurs="0" maxOccurs="1">
<xs:complexType>
Expand Down
20 changes: 13 additions & 7 deletions DNN Platform/Website/Default.aspx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,14 +606,20 @@ private void InitializePage()
this.Page.Header.Controls.AddAt(0, new LiteralControl(this.Comment));
}

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

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

// set page title
Expand Down Expand Up @@ -732,10 +738,10 @@ private void InitializePage()
// META generator
this.Generator = string.Empty;

// META Robots - hide it inside popups and if PageHeadText of current tab already contains a robots meta tag
// META Robots - hide it inside popups and if header tags already contain a robots meta tag
if (!UrlUtils.InPopUp() &&
!(HeaderTextRegex.IsMatch(this.PortalSettings.ActiveTab.PageHeadText) ||
HeaderTextRegex.IsMatch(this.PortalSettings.PageHeadText)))
!(HeaderTextRegex.IsMatch(tabHeaderTags) ||
HeaderTextRegex.IsMatch(portalHeaderTags)))
{
this.MetaRobots.Visible = true;
var allowIndex = true;
Expand Down
Loading
Loading