From 315963d55c8269b4d569ab136b45632d34530e68 Mon Sep 17 00:00:00 2001 From: washala <1106699+washala@user.noreply.gitee.com> Date: Thu, 30 Apr 2026 14:29:26 +0800 Subject: [PATCH 1/8] feat(SEO): Add page header tag management functionality - Add PageHeaderTagInfo class for managing page header tags - Add tag management interface in Page Settings - Support header tag settings at Portal and Tag levels - Migrate legacy PageHeadText to the new tag system - Update template files to support new tag format - Add XSS sanitization and tag rendering functionality - Integrate tag management into Default Site Settings Closes #7184 --- .../Templates/PortalTemplateExporter.cs | 25 +- .../Templates/PortalTemplateImporter.cs | 25 +- .../Entities/Tabs/PageHeaderTagInfo.cs | 152 ++++++++++++ .../Components/Services/PagesExportService.cs | 10 + .../Components/Portals/portal.template.xsd | 15 ++ DNN Platform/Website/Default.aspx.cs | 56 +++-- .../Portals/_default/Blank Website.template | 5 +- .../Portals/_default/Default Website.template | 5 +- .../SqlDataProvider/10.03.02.SqlDataProvider | 42 ++++ .../PageHeaderTags/PageHeaderTags.jsx | 227 ++++++++++++++++++ .../src/components/PageHeaderTags/style.less | 144 +++++++++++ .../Pages.Web/src/components/Seo/Seo.jsx | 70 +++--- .../Pages.Web/src/services/pageService.js | 2 + .../PageHeaderTags/PageHeaderTags.jsx | 227 ++++++++++++++++++ .../src/components/PageHeaderTags/style.less | 148 ++++++++++++ .../components/defaultPagesSettings/index.jsx | 22 +- .../Components/Pages/BulkPagesController.cs | 23 +- .../Components/Pages/Converters.cs | 13 +- .../Components/Pages/PagesControllerImpl.cs | 15 +- .../Components/Pages/XssCleaner.cs | 7 + .../Services/DTO/PageHeaderTagItem.cs | 18 ++ .../Services/DTO/PageSettings.cs | 3 + .../UpdateDefaultPagesSettingsRequest.cs | 5 + .../Services/SiteSettingsController.cs | 26 +- 24 files changed, 1185 insertions(+), 100 deletions(-) create mode 100644 DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs create mode 100644 Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx create mode 100644 Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/style.less create mode 100644 Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/PageHeaderTags.jsx create mode 100644 Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/style.less create mode 100644 Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageHeaderTagItem.cs diff --git a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs index 9ecc9b19a23..7d4f640cfb3 100644 --- a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs +++ b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs @@ -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)) diff --git a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs index bcfb2b415fc..a7d1fa593ed 100644 --- a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs +++ b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs @@ -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(); + 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))) { diff --git a/DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs b/DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs new file mode 100644 index 00000000000..a93a2547367 --- /dev/null +++ b/DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs @@ -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 FromSettings(IDictionary settings) + { + var items = new List(); + 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 GetTabItems(int tabId) + { + return FromSettings(TabController.Instance.GetTabSettings(tabId)); + } + + public static IList 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 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 items) + { + var normalizedItems = Normalize(items); + var targetSettingNames = new HashSet(normalizedItems.Select(item => item.SettingName), StringComparer.OrdinalIgnoreCase); + var existingSettingNames = TabController.Instance.GetTabSettings(tabId) + .Cast() + .Select(entry => Convert.ToString(entry.Key, CultureInfo.InvariantCulture)) + .Where(key => !string.IsNullOrEmpty(key)) + .ToList(); + + foreach (var key in existingSettingNames) + { + 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); + } + } + + public static void SavePortalItems(int portalId, IEnumerable items, string cultureCode = null) + { + var normalizedItems = Normalize(items); + var targetSettingNames = new HashSet(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) + { + if (key.StartsWith(SettingPrefix, StringComparison.Ordinal) && !targetSettingNames.Contains(key)) + { + PortalController.DeletePortalSetting(portalId, key); + } + } + + foreach (var item in normalizedItems) + { + PortalController.Instance.UpdatePortalSetting(portalId, item.SettingName, item.Content, true, cultureCode ?? Null.NullString); + } + } + + private static List Normalize(IEnumerable items) + { + if (items == null) + { + return new List(); + } + + 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(); + } + } +} diff --git a/DNN Platform/Modules/DnnExportImport/Components/Services/PagesExportService.cs b/DNN Platform/Modules/DnnExportImport/Components/Services/PagesExportService.cs index 3e2e763f7c5..f017e2378d4 100644 --- a/DNN Platform/Modules/DnnExportImport/Components/Services/PagesExportService.cs +++ b/DNN Platform/Modules/DnnExportImport/Components/Services/PagesExportService.cs @@ -261,6 +261,11 @@ protected virtual void ProcessImportPage(ExportTab otherTab, IList ex } SetTabData(localTab, otherTab); + if (this.Repository.GetRelatedItems(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) @@ -327,6 +332,11 @@ protected virtual void ProcessImportPage(ExportTab otherTab, IList ex { localTab = new TabInfo { PortalID = portalId }; SetTabData(localTab, otherTab); + if (this.Repository.GetRelatedItems(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; diff --git a/DNN Platform/Website/Components/Portals/portal.template.xsd b/DNN Platform/Website/Components/Portals/portal.template.xsd index dac53219754..cc27e8acc31 100644 --- a/DNN Platform/Website/Components/Portals/portal.template.xsd +++ b/DNN Platform/Website/Components/Portals/portal.template.xsd @@ -41,6 +41,21 @@ + + + + + + + + + + + + + + + diff --git a/DNN Platform/Website/Default.aspx.cs b/DNN Platform/Website/Default.aspx.cs index 53bafe4e5be..50e402a0ae4 100644 --- a/DNN Platform/Website/Default.aspx.cs +++ b/DNN Platform/Website/Default.aspx.cs @@ -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)); + } + else if (!Globals.IsAdminControl()) + { + var tabHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID)); + 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)); + if (!string.IsNullOrEmpty(portalHeaderTags)) + { + this.metaPanel.Controls.Add(new LiteralControl(portalHeaderTags)); + } + } // set page title if (UrlUtils.InPopUp()) @@ -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) || + HeaderTextRegex.IsMatch(PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode)) + portalPageHeadText))) { this.MetaRobots.Visible = true; var allowIndex = true; diff --git a/DNN Platform/Website/Portals/_default/Blank Website.template b/DNN Platform/Website/Portals/_default/Blank Website.template index b32cdd752a4..492b5e76e62 100644 --- a/DNN Platform/Website/Portals/_default/Blank Website.template +++ b/DNN Platform/Website/Portals/_default/Blank Website.template @@ -24,7 +24,10 @@ 0 0 0 - <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> + + + ]]> + True IE=edge 1 diff --git a/DNN Platform/Website/Portals/_default/Default Website.template b/DNN Platform/Website/Portals/_default/Default Website.template index 031d5e3ba03..7fcdf6aa74c 100644 --- a/DNN Platform/Website/Portals/_default/Default Website.template +++ b/DNN Platform/Website/Portals/_default/Default Website.template @@ -24,7 +24,10 @@ 0 0 0 - <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> + + + ]]> + True IE=edge 1 diff --git a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider index 33a259d05fa..a1d54fd6863 100644 --- a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider +++ b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider @@ -7,3 +7,45 @@ /***** for {databaseOwner} and {objectQualifier} *****/ /***** *****/ /************************************************************/ + + +-- Migrate Portal-level PageHeadText to PageHeaderTag_Default setting +INSERT INTO {databaseOwner}[{objectQualifier}PortalSettings] + (PortalID, SettingName, SettingValue, CreatedByUserID, CreatedOnDate, LastModifiedByUserID, LastModifiedOnDate, CultureCode) +SELECT ps.PortalID, 'PageHeaderTag_Default', ps.SettingValue, ps.CreatedByUserID, GETDATE(), ps.LastModifiedByUserID, GETDATE(), ps.CultureCode +FROM {databaseOwner}[{objectQualifier}PortalSettings] ps +WHERE ps.SettingName = 'PageHeadText' + AND ISNULL(ps.SettingValue, '') NOT IN ('', 'false') + AND NOT EXISTS ( + SELECT 1 + FROM {databaseOwner}[{objectQualifier}PortalSettings] psExisting + WHERE psExisting.PortalID = ps.PortalID + AND ISNULL(psExisting.CultureCode, '') = ISNULL(ps.CultureCode, '') + AND psExisting.SettingName = 'PageHeaderTag_Default'); +GO + +-- Reset legacy PageHeadText to 'false' after migration +UPDATE {databaseOwner}[{objectQualifier}PortalSettings] +SET SettingValue = 'false' +WHERE SettingName = 'PageHeadText' + AND ISNULL(SettingValue, '') NOT IN ('', 'false'); +GO + +-- Migrate Tab-level PageHeadText to TabSettings +INSERT INTO {databaseOwner}[{objectQualifier}TabSettings] + (TabID, SettingName, SettingValue, CreatedByUserID, CreatedOnDate, LastModifiedByUserID, LastModifiedOnDate) +SELECT t.TabID, 'PageHeaderTag_Default', t.PageHeadText, t.CreatedByUserID, GETDATE(), t.LastModifiedByUserID, GETDATE() +FROM {databaseOwner}[{objectQualifier}Tabs] t +WHERE ISNULL(t.PageHeadText, '') <> '' + AND NOT EXISTS ( + SELECT 1 + FROM {databaseOwner}[{objectQualifier}TabSettings] tsExisting + WHERE tsExisting.TabID = t.TabID + AND tsExisting.SettingName = 'PageHeaderTag_Default'); +GO + +-- Clear legacy PageHeadText column after migration +UPDATE {databaseOwner}{objectQualifier}Tabs +SET PageHeadText = NULL +WHERE ISNULL(PageHeadText, '') <> ''; +GO diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx new file mode 100644 index 00000000000..935275c08cd --- /dev/null +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx @@ -0,0 +1,227 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Button, Collapsible, GridCell, MultiLineInputWithError, SvgIcons, SingleLineInputWithError } from "@dnnsoftware/dnn-react-common"; +import utils from "../../utils"; +import "./style.less"; + +class PageHeaderTags extends Component { + constructor(props) { + super(props); + this.state = { + addingNew: false, + editingIndex: -1, + draft: { name: "", content: "" }, + triedToSubmit: false + }; + } + + onOpenNewForm() { + this.setState({ + addingNew: true, + editingIndex: -1, + draft: { name: "", content: "" }, + triedToSubmit: false + }); + } + + onOpenEditForm(index) { + const { value } = this.props; + const opened = this.state.editingIndex === index; + + if (opened) { + this.onCloseForm(); + return; + } + + const item = (value || [])[index] || { name: "", content: "" }; + this.setState({ + addingNew: false, + editingIndex: index, + draft: { name: item.name || "", content: item.content || "" }, + triedToSubmit: false + }); + } + + onCloseForm() { + this.setState({ + addingNew: false, + editingIndex: -1, + draft: { name: "", content: "" }, + triedToSubmit: false + }); + } + + onDelete(index) { + const { onChange, value } = this.props; + utils.confirm("Delete this tag?", "Delete", "Cancel", () => { + onChange((value || []).filter((item, itemIndex) => itemIndex !== index)); + if (this.state.editingIndex === index) { + this.onCloseForm(); + } + }); + } + + onChangeField(key, event) { + this.setState({ + draft: { + ...this.state.draft, + [key]: event.target.value + } + }); + } + + hasDuplicateName() { + const { value } = this.props; + const { draft, editingIndex } = this.state; + const normalizedName = (draft.name || "").trim().toLowerCase(); + + if (!normalizedName) { + return false; + } + + return (value || []).some((item, index) => index !== editingIndex && ((item.name || "").trim().toLowerCase() === normalizedName)); + } + + hasNameTooLong() { + return ((this.state.draft.name || "").trim().length > 30); + } + + onSave() { + const { onChange, value } = this.props; + const { addingNew, editingIndex, draft } = this.state; + const nextDraft = { + name: (draft.name || "").trim(), + content: draft.content || "" + }; + + this.setState({ triedToSubmit: true, draft: nextDraft }); + + if (!nextDraft.name || !nextDraft.content.trim() || this.hasDuplicateName() || this.hasNameTooLong()) { + return; + } + + const nextValue = [...(value || [])]; + if (addingNew) { + nextValue.push(nextDraft); + } + else if (editingIndex >= 0) { + nextValue[editingIndex] = nextDraft; + } + + onChange(nextValue); + this.onCloseForm(); + } + + renderEditor(isOpened) { + const { draft, triedToSubmit } = this.state; + const duplicateName = this.hasDuplicateName(); + const nameTooLong = this.hasNameTooLong(); + + return +
+ + + + + + + + +
+ + +
+
+
+ ; + } + + renderAddRow() { + const { addingNew } = this.state; + + if (!addingNew) { + return null; + } + + return
+ - + +
+ + {this.renderEditor(true)} +
; + } + + renderRows() { + const { value } = this.props; + const { editingIndex } = this.state; + + if (!value || value.length === 0) { + return null; + } + + return value.map((item, index) => { + const opened = editingIndex === index; + + return
+ + {item.name} + + + {!opened &&
} + {!opened &&
} +
+ {opened && this.renderEditor(true)} +
; + }); + } + + render() { + const { label } = this.props; + const { addingNew } = this.state; + + return
+
+
{label}
+
+
+ Add Tag +
+
+
+
+ Name + +
+ {this.renderAddRow()} + {this.renderRows()} +
+
; + } +} + +PageHeaderTags.propTypes = { + label: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.array +}; + +export default PageHeaderTags; diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/style.less b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/style.less new file mode 100644 index 00000000000..e53b2dc467b --- /dev/null +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/style.less @@ -0,0 +1,144 @@ +@import "~@dnnsoftware/dnn-react-common/styles/index.less"; + +.page-header-tags { + width: 100%; + + .tag-table { + margin-bottom: 30px; + width: 100%; + float: left; + border-left: 1px solid @alto; + border-right: 1px solid @alto; + } + + .header-row { + display: table; + width: 100%; + border-bottom: 1px solid @alto; + border-top: 1px solid @alto; + float: left; + position: relative; + padding: 15px 20px; + box-sizing: border-box; + text-transform: uppercase; + } + + .tagRow { + display: table; + border-bottom: 1px solid @alto; + color: #666; + width: 100%; + position: relative; + padding: 15px 20px; + box-sizing: border-box; + cursor: auto; + + &.row-opened { + margin-top: -3px; + margin-bottom: -1px; + border-top: 2px solid #0d6efd; + border-bottom: 2px solid #0d6efd !important; + } + } + + .tag-name { + word-break: break-all; + } + + .extension-action, + .extension-action-hidden { + width: 16px; + height: 16px; + float: right; + margin-right: 5px; + } + + .extension-action { + cursor: pointer; + color: #8a8a8a; + + &:hover { + color: #222; + } + + svg { + width: 16px; + height: 16px; + } + } + + .extension-action-hidden { + visibility: hidden; + } + + .editTag { + width: 100%; + float: left; + padding: 15px 20px 0 20px; + + .editTag-body { + .left-column { + padding-right: 15px; + } + + .right-column { + padding-left: 15px; + } + + .dnn-single-line-input-with-error, + .dnn-multi-line-input-with-error { + width: 100%; + } + } + } + + .addItemRow { + width: 100%; + margin: 0 0 15px 0; + font-weight: bolder; + border-bottom: 1px solid @alto; + overflow: hidden; + + .sectionTitle { + font-weight: bolder; + float: left; + line-height: 25px; + } + + .AddItemBox { + width: auto; + float: right; + color: @thunder; + cursor: pointer; + line-height: 25px; + + &.active { + color: @curiousBlue; + cursor: inherit; + } + + .add-icon { + margin-left: 20px; + margin-right: 5px; + margin-top: 4px; + float: left; + cursor: pointer; + + svg { + width: 16px; + float: left; + height: 16px; + fill: @thunder; + } + + &.active { + svg { + fill: @curiousBlue; + } + + cursor: inherit; + } + } + } + } +} diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/Seo/Seo.jsx b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/Seo/Seo.jsx index 50a3bd13a04..fc2c652ce59 100644 --- a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/Seo/Seo.jsx +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/Seo/Seo.jsx @@ -1,9 +1,10 @@ import React, {Component} from "react"; import PropTypes from "prop-types"; import styles from "./style.module.less"; -import { GridSystem, GridCell, Label, Dropdown, MultiLineInputWithError, Switch } from "@dnnsoftware/dnn-react-common"; +import { GridCell, Label, Dropdown, Switch } from "@dnnsoftware/dnn-react-common"; import Localization from "../../localization"; import PageUrls from "./PageUrls/PageUrls"; +import PageHeaderTags from "../PageHeaderTags/PageHeaderTags"; class Seo extends Component { @@ -40,40 +41,37 @@ class Seo extends Component { pageHasParent={page.hasParent} primaryAliasId={page.primaryAliasId} />} - - - - - - - - - - - + + onChangeField("pageHeaderTags", value)} /> + + + + + + +
); } @@ -85,4 +83,4 @@ Seo.propTypes = { }; -export default Seo; \ No newline at end of file +export default Seo; diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js b/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js index 4565633c985..39e99a0b32a 100644 --- a/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js @@ -93,6 +93,7 @@ const PageService = function () { page.iconFile = null; page.iconFileLarge = null; page.sitemapPriority = 0.5; + page.pageHeaderTags = page.pageHeaderTags || []; return page; }); }; @@ -132,6 +133,7 @@ const PageService = function () { ...page, startDate: page.schedulingEnabled ? page.startDate : null, endDate: page.schedulingEnabled ? page.endDate : null, + pageHeadText: undefined, schedulingEnabled: undefined }; }; diff --git a/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/PageHeaderTags.jsx b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/PageHeaderTags.jsx new file mode 100644 index 00000000000..aa0a3285d89 --- /dev/null +++ b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/PageHeaderTags.jsx @@ -0,0 +1,227 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Button, Collapsible, GridCell, MultiLineInputWithError, SvgIcons, SingleLineInputWithError } from "@dnnsoftware/dnn-react-common"; +import utils from "../../utils"; +import "./style.less"; + +class PageHeaderTags extends Component { + constructor(props) { + super(props); + this.state = { + addingNew: false, + editingIndex: -1, + draft: { name: "", content: "" }, + triedToSubmit: false + }; + } + + onOpenNewForm() { + this.setState({ + addingNew: true, + editingIndex: -1, + draft: { name: "", content: "" }, + triedToSubmit: false + }); + } + + onOpenEditForm(index) { + const { value } = this.props; + const opened = this.state.editingIndex === index; + + if (opened) { + this.onCloseForm(); + return; + } + + const item = (value || [])[index] || { name: "", content: "" }; + this.setState({ + addingNew: false, + editingIndex: index, + draft: { name: item.name || "", content: item.content || "" }, + triedToSubmit: false + }); + } + + onCloseForm() { + this.setState({ + addingNew: false, + editingIndex: -1, + draft: { name: "", content: "" }, + triedToSubmit: false + }); + } + + onDelete(index) { + const { onChange, value } = this.props; + utils.utilities.confirm("Delete this tag?", "Delete", "Cancel", () => { + onChange((value || []).filter((item, itemIndex) => itemIndex !== index)); + if (this.state.editingIndex === index) { + this.onCloseForm(); + } + }); + } + + onChangeField(key, event) { + this.setState({ + draft: { + ...this.state.draft, + [key]: event.target.value + } + }); + } + + hasDuplicateName() { + const { value } = this.props; + const { draft, editingIndex } = this.state; + const normalizedName = (draft.name || "").trim().toLowerCase(); + + if (!normalizedName) { + return false; + } + + return (value || []).some((item, index) => index !== editingIndex && ((item.name || "").trim().toLowerCase() === normalizedName)); + } + + hasNameTooLong() { + return ((this.state.draft.name || "").trim().length > 30); + } + + onSave() { + const { onChange, value } = this.props; + const { addingNew, editingIndex, draft } = this.state; + const nextDraft = { + name: (draft.name || "").trim(), + content: draft.content || "" + }; + + this.setState({ triedToSubmit: true, draft: nextDraft }); + + if (!nextDraft.name || !nextDraft.content.trim() || this.hasDuplicateName() || this.hasNameTooLong()) { + return; + } + + const nextValue = [...(value || [])]; + if (addingNew) { + nextValue.push(nextDraft); + } + else if (editingIndex >= 0) { + nextValue[editingIndex] = nextDraft; + } + + onChange(nextValue); + this.onCloseForm(); + } + + renderEditor(isOpened) { + const { draft, triedToSubmit } = this.state; + const duplicateName = this.hasDuplicateName(); + const nameTooLong = this.hasNameTooLong(); + + return +
+ + + + + + + + +
+ + +
+
+
+ ; + } + + renderAddRow() { + const { addingNew } = this.state; + + if (!addingNew) { + return null; + } + + return
+ - + +
+ + {this.renderEditor(true)} +
; + } + + renderRows() { + const { value } = this.props; + const { editingIndex } = this.state; + + if (!value || value.length === 0) { + return null; + } + + return value.map((item, index) => { + const opened = editingIndex === index; + + return
+ + {item.name} + + + {!opened &&
} + {!opened &&
} +
+ {opened && this.renderEditor(true)} +
; + }); + } + + render() { + const { label } = this.props; + const { addingNew } = this.state; + + return
+
+
{label}
+
+
+ Add Tag +
+
+
+
+ Name + +
+ {this.renderAddRow()} + {this.renderRows()} +
+
; + } +} + +PageHeaderTags.propTypes = { + label: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + value: PropTypes.array +}; + +export default PageHeaderTags; diff --git a/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/style.less b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/style.less new file mode 100644 index 00000000000..0b6aa7af15b --- /dev/null +++ b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/PageHeaderTags/style.less @@ -0,0 +1,148 @@ +@import "~@dnnsoftware/dnn-react-common/styles/index.less"; + +.page-header-tags { + width: 100%; + + .tag-table { + margin-bottom: 30px; + width: 100%; + float: left; + border-left: 1px solid @alto; + border-right: 1px solid @alto; + } + + .header-row { + display: table; + width: 100%; + border-bottom: 1px solid @alto; + border-top: 1px solid @alto; + float: left; + position: relative; + padding: 15px 20px; + box-sizing: border-box; + text-transform: uppercase; + } + + .tagRow { + display: table; + border-bottom: 1px solid @alto; + color: #666; + width: 100%; + position: relative; + padding: 15px 20px; + box-sizing: border-box; + cursor: auto; + + &.row-opened { + margin-top: -3px; + margin-bottom: -1px; + border-top: 2px solid #0d6efd; + border-bottom: 2px solid #0d6efd !important; + } + } + + .tag-name { + word-break: break-all; + } + + .extension-action, + .extension-action-hidden { + width: 16px; + height: 16px; + float: right; + margin-right: 5px; + } + + .extension-action { + cursor: pointer; + color: #8a8a8a; + + &:hover { + color: #222; + } + + svg { + width: 16px; + height: 16px; + } + } + + .extension-action-hidden { + visibility: hidden; + } + + .editTag { + width: 100%; + float: left; + padding: 15px 20px 0 20px; + + .editTag-body { + .left-column { + padding-right: 15px; + } + + .right-column { + padding-left: 15px; + } + + .dnn-single-line-input-with-error, + .dnn-multi-line-input-with-error { + width: 100%; + } + } + } + + .addItemRow { + width: 100%; + margin: 0 0 15px 0; + font-weight: bolder; + border-bottom: 1px solid @alto; + overflow: hidden; + + .sectionTitle { + font-weight: bolder; + float: left; + line-height: 25px; + width: auto; + margin: 0; + padding: 0; + border: none; + } + + .AddItemBox { + width: auto; + float: right; + color: @thunder; + cursor: pointer; + line-height: 25px; + + &.active { + color: @curiousBlue; + cursor: inherit; + } + + .add-icon { + margin-left: 20px; + margin-right: 5px; + margin-top: 4px; + float: left; + cursor: pointer; + + svg { + width: 16px; + float: left; + height: 16px; + fill: @thunder; + } + + &.active { + svg { + fill: @curiousBlue; + } + + cursor: inherit; + } + } + } + } +} diff --git a/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/defaultPagesSettings/index.jsx b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/defaultPagesSettings/index.jsx index 6f4116dade8..aaee3687ef6 100644 --- a/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/defaultPagesSettings/index.jsx +++ b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/defaultPagesSettings/index.jsx @@ -4,10 +4,11 @@ import { connect } from "react-redux"; import { siteBehavior as SiteBehaviorActions } from "../../actions"; -import { InputGroup, MultiLineInputWithError, PagePicker, GridSystem, Label, Button } from "@dnnsoftware/dnn-react-common"; +import { InputGroup, PagePicker, GridSystem, Label, Button } from "@dnnsoftware/dnn-react-common"; import util from "../../utils"; import resx from "../../resources"; import styles from "./style.module.less"; +import PageHeaderTags from "../PageHeaderTags/PageHeaderTags"; let isHost = false; @@ -69,8 +70,12 @@ class DefaultPagesSettingsPanelBody extends Component { return; } } + else if (key === "PageHeaderTags") { + // PageHeaderTags passes the value directly, not an event + defaultPagesSettings[key] = event; + } else { - defaultPagesSettings[key] = typeof (event) === "object" ? event.target.value : event; + defaultPagesSettings[key] = typeof (event) === "object" && event.target ? event.target.value : event; } this.setState({ @@ -342,16 +347,13 @@ class DefaultPagesSettingsPanelBody extends Component { {isHost &&
{resx.get("PageOutputSettings")}
} {isHost && - - +
}
diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/BulkPagesController.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/BulkPagesController.cs index 2d0a4e52203..9a0d9b81056 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/BulkPagesController.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/BulkPagesController.cs @@ -382,21 +382,26 @@ private int CreateTabFromParent(IPortalSettings portalSettings, TabInfo objRoot, // TODO: To be retrieved once the parent tab is selected? ////tab.Terms.Clear(); ////tab.StartDate = objRoot.StartDate; - ////tab.EndDate = objRoot.EndDate; - tab.RefreshInterval = objRoot.RefreshInterval; - tab.SiteMapPriority = objRoot.SiteMapPriority; - tab.PageHeadText = objRoot.PageHeadText; - tab.IsSecure = objRoot.IsSecure; - tab.PermanentRedirect = objRoot.PermanentRedirect; - } + ////tab.EndDate = objRoot.EndDate; + tab.RefreshInterval = objRoot.RefreshInterval; + tab.SiteMapPriority = objRoot.SiteMapPriority; + tab.PageHeadText = Null.NullString; + tab.IsSecure = objRoot.IsSecure; + tab.PermanentRedirect = objRoot.PermanentRedirect; + } if (validateOnly) { return -1; } - tab.TabID = TabController.Instance.AddTab(tab); - this.ApplyDefaultTabTemplate(tab); + tab.TabID = TabController.Instance.AddTab(tab); + if (objRoot != null) + { + PageHeaderTagInfo.SaveTabItems(tab.TabID, PageHeaderTagInfo.GetTabItems(objRoot.TabID)); + } + + this.ApplyDefaultTabTemplate(tab); // create localized tabs if content localization is enabled if (portalSettings.ContentLocalizationEnabled) diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/Converters.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/Converters.cs index 98075b5e053..baffdc4ffd3 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/Converters.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/Converters.cs @@ -122,11 +122,14 @@ public static T ConvertToPageSettings(TabInfo tab) CacheProvider = (string)tab.TabSettings["CacheProvider"], CacheDuration = CacheDuration(tab), CacheIncludeExclude = CacheIncludeExclude(tab), - CacheIncludeVaryBy = (string)tab.TabSettings["IncludeVaryBy"], - CacheExcludeVaryBy = (string)tab.TabSettings["ExcludeVaryBy"], - CacheMaxVaryByCount = MaxVaryByCount(tab), - PageHeadText = tab.PageHeadText, - SiteMapPriority = tab.SiteMapPriority, + CacheIncludeVaryBy = (string)tab.TabSettings["IncludeVaryBy"], + CacheExcludeVaryBy = (string)tab.TabSettings["ExcludeVaryBy"], + CacheMaxVaryByCount = MaxVaryByCount(tab), + PageHeadText = tab.PageHeadText, + PageHeaderTags = PageHeaderTagInfo.GetTabItems(tab.TabID) + .Select(item => new PageHeaderTagItem { Name = item.Name, Content = item.Content }) + .ToList(), + SiteMapPriority = tab.SiteMapPriority, PermanentRedirect = tab.PermanentRedirect, LinkNewWindow = LinkNewWindow(tab), PageStyleSheet = (string)tab.TabSettings["CustomStylesheet"], diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/PagesControllerImpl.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/PagesControllerImpl.cs index d2c7e5bc661..10b74190e76 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/PagesControllerImpl.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/PagesControllerImpl.cs @@ -600,6 +600,7 @@ public virtual int AddTab(PortalSettings settings, PageSettings pageSettings) this.SavePagePermissions(tab, pageSettings.Permissions); var tabId = this.tabController.AddTab(tab); + PageHeaderTagInfo.SaveTabItems(tabId, pageSettings.PageHeaderTags?.Select(this.ToPageHeaderTagInfo)); this.CreateOrUpdateContentItem(tab); @@ -926,6 +927,7 @@ public int UpdateTab(TabInfo tab, PageSettings pageSettings) this.SavePagePermissions(tab, pageSettings.Permissions); this.tabController.UpdateTab(tab); + PageHeaderTagInfo.SaveTabItems(tab.TabID, pageSettings.PageHeaderTags?.Select(this.ToPageHeaderTagInfo)); this.CreateOrUpdateContentItem(tab); @@ -1233,7 +1235,7 @@ protected virtual void UpdateTabInfoFromPageSettings(TabInfo tab, PageSettings p tab.TabSettings["AllowIndex"] = pageSettings.AllowIndex; tab.SiteMapPriority = pageSettings.SiteMapPriority; - tab.PageHeadText = pageSettings.PageHeadText; + tab.PageHeadText = Null.NullString; tab.PermanentRedirect = pageSettings.PermanentRedirect; tab.Url = GetInternalUrl(pageSettings); @@ -1606,7 +1608,7 @@ private void CopySourceTabProperties(TabInfo tab, TabInfo sourceTab) { tab.IconFile = sourceTab.IconFile; tab.IconFileLarge = sourceTab.IconFileLarge; - tab.PageHeadText = sourceTab.PageHeadText; + tab.PageHeadText = Null.NullString; tab.RefreshInterval = sourceTab.RefreshInterval; this.tabController.UpdateTab(tab); @@ -1618,6 +1620,8 @@ private void CopySourceTabProperties(TabInfo tab, TabInfo sourceTab) this.tabController.UpdateTabSetting(tab.TabID, key, Convert.ToString(sourceTab.TabSettings[key], CultureInfo.InvariantCulture)); } } + + PageHeaderTagInfo.SaveTabItems(tab.TabID, PageHeaderTagInfo.GetTabItems(sourceTab.TabID)); } private void CopyModulesFromSourceTab(TabInfo tab, TabInfo sourceTab, IEnumerable includedModules) @@ -1713,5 +1717,12 @@ private void CopyModulesFromSourceTab(TabInfo tab, TabInfo sourceTab, IEnumerabl } } } + + private PageHeaderTagInfo ToPageHeaderTagInfo(PageHeaderTagItem item) + { + return item == null + ? null + : new PageHeaderTagInfo { Name = item.Name, Content = item.Content }; + } } } diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs index 1c5c7ecabf8..7969f4ceabc 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs @@ -21,6 +21,13 @@ public static void Clean(this PageSettings input) input.Alias = Clean(input.Alias); input.LocalizedName = Clean(input.LocalizedName); input.PageStyleSheet = Clean(input.PageStyleSheet); + if (input.PageHeaderTags != null) + { + foreach (var item in input.PageHeaderTags) + { + item.Name = Clean(item.Name); + } + } } public static void Clean(this BulkPage input) diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageHeaderTagItem.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageHeaderTagItem.cs new file mode 100644 index 00000000000..7836decccb0 --- /dev/null +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageHeaderTagItem.cs @@ -0,0 +1,18 @@ +// 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 Dnn.PersonaBar.Pages.Services.Dto +{ + using System.Runtime.Serialization; + + [DataContract] + public class PageHeaderTagItem + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "content")] + public string Content { get; set; } + } +} diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageSettings.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageSettings.cs index 8c24e027be5..b4c410c8561 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageSettings.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/PageSettings.cs @@ -118,6 +118,9 @@ public class PageSettings [DataMember(Name = "pageHeadText")] public string PageHeadText { get; set; } + [DataMember(Name = "pageHeaderTags")] + public IList PageHeaderTags { get; set; } + [DataMember(Name = "sitemapPriority")] public float SiteMapPriority { get; set; } diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/SiteSettings/UpdateDefaultPagesSettingsRequest.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/SiteSettings/UpdateDefaultPagesSettingsRequest.cs index a330c46ddb8..f5be40a6fca 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/SiteSettings/UpdateDefaultPagesSettingsRequest.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/DTO/SiteSettings/UpdateDefaultPagesSettingsRequest.cs @@ -4,6 +4,9 @@ namespace Dnn.PersonaBar.SiteSettings.Services.Dto { + using System.Collections.Generic; + + using Dnn.PersonaBar.Pages.Services.Dto; using Dnn.PersonaBar.Security.Attributes; public class UpdateDefaultPagesSettingsRequest @@ -34,6 +37,8 @@ public class UpdateDefaultPagesSettingsRequest public string PageHeadText { get; set; } + public IList PageHeaderTags { get; set; } + [TabExist] public int RedirectAfterLoginTabId { get; set; } diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/SiteSettingsController.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/SiteSettingsController.cs index 3409e706cbd..cae4192734a 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/SiteSettingsController.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Services/SiteSettingsController.cs @@ -388,13 +388,16 @@ public HttpResponseMessage GetDefaultPagesSettings(int? portalId, string culture PrivacyTabName = this.TabSanitizer(portal.PrivacyTabId, pid)?.TabName, RedirectAfterLoginTabId = this.TabSanitizer(redirectAfterLoginTabId, pid)?.TabID, RedirectAfterLoginTabName = this.TabSanitizer(redirectAfterLoginTabId, pid)?.TabName, - RedirectAfterLogoutTabId = this.TabSanitizer(redirectAfterLogoutTabId, pid)?.TabID, - RedirectAfterLogoutTabName = this.TabSanitizer(redirectAfterLogoutTabId, pid)?.TabName, - RedirectAfterRegistrationTabId = this.TabSanitizer(redirectAfterRegistrationTabId, pid)?.TabID, - RedirectAfterRegistrationTabName = this.TabSanitizer(redirectAfterRegistrationTabId, pid)?.TabName, - PageHeadText = localizedPortalSettings["PageHeadText"], - }, - }); + RedirectAfterLogoutTabId = this.TabSanitizer(redirectAfterLogoutTabId, pid)?.TabID, + RedirectAfterLogoutTabName = this.TabSanitizer(redirectAfterLogoutTabId, pid)?.TabName, + RedirectAfterRegistrationTabId = this.TabSanitizer(redirectAfterRegistrationTabId, pid)?.TabID, + RedirectAfterRegistrationTabName = this.TabSanitizer(redirectAfterRegistrationTabId, pid)?.TabName, + PageHeadText = localizedPortalSettings["PageHeadText"], + PageHeaderTags = PageHeaderTagInfo.GetPortalItems(pid, cultureCode) + .Select(item => new Dnn.PersonaBar.Pages.Services.Dto.PageHeaderTagItem { Name = item.Name, Content = item.Content }) + .ToList(), + }, + }); } catch (Exception exc) { @@ -442,10 +445,11 @@ public HttpResponseMessage UpdateDefaultPagesSettings(UpdateDefaultPagesSettings portalInfo.PrivacyTabId = this.ValidateTabId(request.PrivacyTabId, pid); PortalController.Instance.UpdatePortalInfo(portalInfo); - PortalController.UpdatePortalSetting(this.portalController, pid, "Redirect_AfterLogin", this.ValidateTabId(request.RedirectAfterLoginTabId, pid).ToString(CultureInfo.InvariantCulture), false, cultureCode); - PortalController.UpdatePortalSetting(this.portalController, pid, "Redirect_AfterLogout", this.ValidateTabId(request.RedirectAfterLogoutTabId, pid).ToString(CultureInfo.InvariantCulture), false, cultureCode); - PortalController.UpdatePortalSetting(this.portalController, pid, "Redirect_AfterRegistration", this.ValidateTabId(request.RedirectAfterRegistrationTabId, pid).ToString(CultureInfo.InvariantCulture), false, cultureCode); - PortalController.UpdatePortalSetting(this.portalController, pid, "PageHeadText", string.IsNullOrEmpty(request.PageHeadText) ? "false" : request.PageHeadText); + PortalController.UpdatePortalSetting(this.portalController, pid, "Redirect_AfterLogin", this.ValidateTabId(request.RedirectAfterLoginTabId, pid).ToString(CultureInfo.InvariantCulture), false, cultureCode); + PortalController.UpdatePortalSetting(this.portalController, pid, "Redirect_AfterLogout", this.ValidateTabId(request.RedirectAfterLogoutTabId, pid).ToString(CultureInfo.InvariantCulture), false, cultureCode); + PortalController.UpdatePortalSetting(this.portalController, pid, "Redirect_AfterRegistration", this.ValidateTabId(request.RedirectAfterRegistrationTabId, pid).ToString(CultureInfo.InvariantCulture), false, cultureCode); + PageHeaderTagInfo.SavePortalItems(pid, request.PageHeaderTags?.Select(item => new PageHeaderTagInfo { Name = item.Name, Content = item.Content }), cultureCode); + PortalController.UpdatePortalSetting(this.portalController, pid, "PageHeadText", "false", false, cultureCode); return this.Request.CreateResponse(HttpStatusCode.OK, new { Success = true, }); } From e52cb88085f889dfbb695399c7bf6440a7300f67 Mon Sep 17 00:00:00 2001 From: zaizai Date: Thu, 30 Apr 2026 15:40:12 +0800 Subject: [PATCH 2/8] Update DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../DataProviders/SqlDataProvider/10.03.02.SqlDataProvider | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider index a1d54fd6863..0dd7ad450c4 100644 --- a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider +++ b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider @@ -45,7 +45,7 @@ WHERE ISNULL(t.PageHeadText, '') <> '' GO -- Clear legacy PageHeadText column after migration -UPDATE {databaseOwner}{objectQualifier}Tabs +UPDATE {databaseOwner}[{objectQualifier}Tabs] SET PageHeadText = NULL WHERE ISNULL(PageHeadText, '') <> ''; GO From 7208846a4bc2166163ce110e2c0ef2a8f8a0f88d Mon Sep 17 00:00:00 2001 From: washala <1106699+washala@user.noreply.gitee.com> Date: Wed, 6 May 2026 17:16:46 +0800 Subject: [PATCH 3/8] refactor(PageHeaderTags): Adjust grid column width layout Modify the `columnSize` parameter in GridCell: adjust the left column from 50 to 60 and the right column from 50 to 95 to optimize the page layout. #7184 Pull Request #7248 --- .../src/components/PageHeaderTags/PageHeaderTags.jsx | 4 ++-- .../src/components/PageHeaderTags/PageHeaderTags.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx index 935275c08cd..18c26a82440 100644 --- a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/PageHeaderTags/PageHeaderTags.jsx @@ -120,7 +120,7 @@ class PageHeaderTags extends Component { return
- + - +
- + - + Date: Sat, 9 May 2026 14:44:30 +0800 Subject: [PATCH 4/8] fix: resolve issue where culture code was not passed when deleting portal settings --- DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs b/DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs index a93a2547367..d4505af7fee 100644 --- a/DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs +++ b/DNN Platform/Library/Entities/Tabs/PageHeaderTagInfo.cs @@ -118,7 +118,7 @@ public static void SavePortalItems(int portalId, IEnumerable { if (key.StartsWith(SettingPrefix, StringComparison.Ordinal) && !targetSettingNames.Contains(key)) { - PortalController.DeletePortalSetting(portalId, key); + PortalController.DeletePortalSetting(portalId, key, cultureCode ?? Null.NullString); } } From 2b62ccf984c0a06cf13686754ce35e0218f34f02 Mon Sep 17 00:00:00 2001 From: washala <1106699+washala@user.noreply.gitee.com> Date: Sat, 9 May 2026 15:41:41 +0800 Subject: [PATCH 5/8] refactor: Simplify the processing logic of page header tags and metadata Remove redundant conditional checks, merge similar code blocks, and optimize the generation logic of page header tags and metadata --- DNN Platform/Website/Default.aspx.cs | 62 ++++++++++------------------ 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/DNN Platform/Website/Default.aspx.cs b/DNN Platform/Website/Default.aspx.cs index 50e402a0ae4..94dfea8830e 100644 --- a/DNN Platform/Website/Default.aspx.cs +++ b/DNN Platform/Website/Default.aspx.cs @@ -606,37 +606,21 @@ private void InitializePage() this.Page.Header.Controls.AddAt(0, new LiteralControl(this.Comment)); } - 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)); - 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)); - if (!string.IsNullOrEmpty(portalHeaderTags)) - { - this.metaPanel.Controls.Add(new LiteralControl(portalHeaderTags)); - } - } + var tabHeaderTags = string.Empty; + if (!Globals.IsAdminControl()) + { + tabHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetTabItems(this.PortalSettings.ActiveTab.TabID)); + if (!string.IsNullOrEmpty(tabHeaderTags)) + { + this.Page.Header.Controls.Add(new LiteralControl(tabHeaderTags)); + } + } + + var portalHeaderTags = PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode)); + if (!string.IsNullOrEmpty(portalHeaderTags)) + { + this.metaPanel.Controls.Add(new LiteralControl(portalHeaderTags)); + } // set page title if (UrlUtils.InPopUp()) @@ -751,15 +735,13 @@ private void InitializePage() this.Copyright = string.Concat("Copyright (c) ", DateTime.Now.Year, " by ", this.PortalSettings.PortalName); } - // 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) || - HeaderTextRegex.IsMatch(PageHeaderTagInfo.Render(PageHeaderTagInfo.GetPortalItems(this.PortalSettings.PortalId, this.PortalSettings.CultureCode)) + portalPageHeadText))) + // META generator + this.Generator = string.Empty; + + // META Robots - hide it inside popups and if header tags already contain a robots meta tag + if (!UrlUtils.InPopUp() && + !(HeaderTextRegex.IsMatch(tabHeaderTags) || + HeaderTextRegex.IsMatch(portalHeaderTags))) { this.MetaRobots.Visible = true; var allowIndex = true; From 88d7b53f2b8309d7b5e6135f4bba8ba65bcb4ce6 Mon Sep 17 00:00:00 2001 From: washala <1106699+washala@user.noreply.gitee.com> Date: Wed, 20 May 2026 16:39:20 +0800 Subject: [PATCH 6/8] chore: complete PageHeadText to PageHeaderTag migration and deprecation 1. Remove legacy pageheadtext nodes from XSD schema and template files 2. Deprecate PageHeadText property and migrate data to PageHeaderTag settings 3. Update export/import service to support the new page header tag settings 4. Add 10.03.03 SQL data migration script and remove the old 10.03.02 script --- DNN Platform/Library/Entities/Tabs/TabInfo.cs | 3 +-- .../Components/Services/PortalExportService.cs | 3 ++- .../Components/Portals/portal.template.xsd | 2 -- .../Portals/_default/Blank Website.template | 11 ----------- .../Portals/_default/Default Website.template | 15 --------------- ...2.SqlDataProvider => 10.03.03.SqlDataProvider} | 8 +++----- SolutionInfo.cs | 4 ++-- 7 files changed, 8 insertions(+), 38 deletions(-) rename DNN Platform/Website/Providers/DataProviders/SqlDataProvider/{10.03.02.SqlDataProvider => 10.03.03.SqlDataProvider} (91%) diff --git a/DNN Platform/Library/Entities/Tabs/TabInfo.cs b/DNN Platform/Library/Entities/Tabs/TabInfo.cs index 72bd06b0763..03afc5dfbca 100644 --- a/DNN Platform/Library/Entities/Tabs/TabInfo.cs +++ b/DNN Platform/Library/Entities/Tabs/TabInfo.cs @@ -544,6 +544,7 @@ public ArrayList Modules /// Gets or sets the page head text, i.e. content to render in the <head> of the page. [XmlElement("pageheadtext")] + [Obsolete("Deprecated in DotNetNuke 10.3.2. Use PageHeaderTagInfo instead. Scheduled removal in v12.0.0.")] public string PageHeadText { get; set; } /// Gets a list of the names of the panes available to the page. @@ -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, @@ -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"]); diff --git a/DNN Platform/Modules/DnnExportImport/Components/Services/PortalExportService.cs b/DNN Platform/Modules/DnnExportImport/Components/Services/PortalExportService.cs index 1b783dd18a0..9b54bba2d47 100644 --- a/DNN Platform/Modules/DnnExportImport/Components/Services/PortalExportService.cs +++ b/DNN Platform/Modules/DnnExportImport/Components/Services/PortalExportService.cs @@ -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; @@ -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; diff --git a/DNN Platform/Website/Components/Portals/portal.template.xsd b/DNN Platform/Website/Components/Portals/portal.template.xsd index cc27e8acc31..22a09605c3d 100644 --- a/DNN Platform/Website/Components/Portals/portal.template.xsd +++ b/DNN Platform/Website/Components/Portals/portal.template.xsd @@ -40,7 +40,6 @@ - @@ -332,7 +331,6 @@ - diff --git a/DNN Platform/Website/Portals/_default/Blank Website.template b/DNN Platform/Website/Portals/_default/Blank Website.template index 492b5e76e62..6460c6900ef 100644 --- a/DNN Platform/Website/Portals/_default/Blank Website.template +++ b/DNN Platform/Website/Portals/_default/Blank Website.template @@ -24,7 +24,6 @@ 0 0 0 - ]]> @@ -439,7 +438,6 @@ true false - false -1 0.5 @@ -568,7 +566,6 @@ false false - false -1 0.5 @@ -1054,7 +1051,6 @@ false false - false -1 0.5 @@ -1154,7 +1150,6 @@ false false - false -1 0 @@ -1300,7 +1295,6 @@ true false - false -1 0.5 @@ -1798,7 +1792,6 @@ true false - false -1 0.5 @@ -2295,7 +2288,6 @@ true false - false -1 0.5 @@ -2763,7 +2755,6 @@ false false - false -1 0.5 @@ -2884,7 +2875,6 @@ true false - false -1 0.5 @@ -2973,7 +2963,6 @@ false false - false -1 0.5 diff --git a/DNN Platform/Website/Portals/_default/Default Website.template b/DNN Platform/Website/Portals/_default/Default Website.template index 7fcdf6aa74c..b8b7aee8b2b 100644 --- a/DNN Platform/Website/Portals/_default/Default Website.template +++ b/DNN Platform/Website/Portals/_default/Default Website.template @@ -24,7 +24,6 @@ 0 0 0 - ]]> @@ -439,7 +438,6 @@ false false - false -1 0.5 @@ -762,7 +760,6 @@ true false - false -1 0.5 @@ -816,7 +813,6 @@ true false - false -1 0.5 @@ -870,7 +866,6 @@ true false - false -1 0.5 @@ -924,7 +919,6 @@ true false - false -1 0.5 @@ -978,7 +972,6 @@ false false - false -1 0.5 @@ -1464,7 +1457,6 @@ false false - false -1 0.5 @@ -1564,7 +1556,6 @@ false false - false -1 0 @@ -1710,7 +1701,6 @@ true false - false -1 0.5 @@ -2208,7 +2198,6 @@ true false - false -1 0.5 @@ -2705,7 +2694,6 @@ true false - false -1 0.5 @@ -3173,7 +3161,6 @@ false false - false -1 0.5 @@ -3294,7 +3281,6 @@ true false - false -1 0.5 @@ -3383,7 +3369,6 @@ false false - false -1 0.5 diff --git a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.03.SqlDataProvider similarity index 91% rename from DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider rename to DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.03.SqlDataProvider index 0dd7ad450c4..96bee3ae122 100644 --- a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.02.SqlDataProvider +++ b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.03.03.SqlDataProvider @@ -24,11 +24,9 @@ WHERE ps.SettingName = 'PageHeadText' AND psExisting.SettingName = 'PageHeaderTag_Default'); GO --- Reset legacy PageHeadText to 'false' after migration -UPDATE {databaseOwner}[{objectQualifier}PortalSettings] -SET SettingValue = 'false' +-- Delete legacy PageHeadText records after migration +DELETE FROM {databaseOwner}[{objectQualifier}PortalSettings] WHERE SettingName = 'PageHeadText' - AND ISNULL(SettingValue, '') NOT IN ('', 'false'); GO -- Migrate Tab-level PageHeadText to TabSettings @@ -48,4 +46,4 @@ GO UPDATE {databaseOwner}[{objectQualifier}Tabs] SET PageHeadText = NULL WHERE ISNULL(PageHeadText, '') <> ''; -GO +GO \ No newline at end of file diff --git a/SolutionInfo.cs b/SolutionInfo.cs index 48ea2304ee2..4ae9d7d3019 100644 --- a/SolutionInfo.cs +++ b/SolutionInfo.cs @@ -14,5 +14,5 @@ [assembly: AssemblyCopyright("DNN Platform is copyright 2002-2026 by .NET Foundation. All Rights Reserved.")] [assembly: AssemblyTrademark("DNN")] [assembly: AssemblyVersion("10.3.3")] -[assembly: AssemblyFileVersion("10.3.3.1")] -[assembly: AssemblyInformationalVersion("10.3.3-rc.1+Branch.release-10.3.2.Sha.fd7b020117a15228a497db4ff973a8feef609a25")] +[assembly: AssemblyFileVersion("10.3.3.0")] +[assembly: AssemblyInformationalVersion("10.3.3 Custom build")] From 6c5bcb7597fc78cc81e33a2207cb1227792eecf2 Mon Sep 17 00:00:00 2001 From: washala <1106699+washala@user.noreply.gitee.com> Date: Thu, 18 Jun 2026 08:28:07 +0800 Subject: [PATCH 7/8] refactor(portal template): Simplify the handling of header tags for portal template export and import --- .../Templates/PortalTemplateExporter.cs | 34 +++++++---------- .../Templates/PortalTemplateImporter.cs | 37 ++++++++----------- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs index 7d4f640cfb3..c519397e6c2 100644 --- a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs +++ b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateExporter.cs @@ -231,26 +231,20 @@ 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) && !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(); - } + 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)) diff --git a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs index a7d1fa593ed..e36c7903d4f 100644 --- a/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs +++ b/DNN Platform/Library/Entities/Portals/Templates/PortalTemplateImporter.cs @@ -962,27 +962,22 @@ 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)); - } - - var pageHeaderTagNodes = nodeSettings.SelectNodes("pageheadertags/pageheadertag"); - if (pageHeaderTagNodes != null && pageHeaderTagNodes.Count > 0) - { - var items = new List(); - 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"); - } + var pageHeaderTagNodes = nodeSettings.SelectNodes("pageheadertags/pageheadertag"); + if (pageHeaderTagNodes != null && pageHeaderTagNodes.Count > 0) + { + var items = new List(); + 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))) { From 8f542ef5b53bf2427a72ac3dd163af9af797ef3e Mon Sep 17 00:00:00 2001 From: washala <1106699+washala@user.noreply.gitee.com> Date: Thu, 18 Jun 2026 09:08:50 +0800 Subject: [PATCH 8/8] refactor(Dnn.PersonaBar.Extensions): Adjust the cleaning logic of XssCleaner Move the common cleaning logic for batch page requests into the Clean method of the corresponding entity to reduce duplicate code --- .../Components/Pages/XssCleaner.cs | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs index 2ad6c00cc63..2a65adb0f3f 100644 --- a/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs +++ b/Dnn.AdminExperience/Dnn.PersonaBar.Extensions/Components/Pages/XssCleaner.cs @@ -25,6 +25,13 @@ public static void Clean(this PageSettings input) input.Alias = Clean(input.Alias); input.LocalizedName = Clean(input.LocalizedName); input.PageStyleSheet = Clean(input.PageStyleSheet); + if (input.PageHeaderTags != null) + { + foreach (var item in input.PageHeaderTags) + { + item.Name = Clean(item.Name); + } + } } /// Cleans input. @@ -50,23 +57,6 @@ public static void Clean(this DnnPagesRequest input) { foreach (var locale in input.Locales) { - input.Title = Clean(input.Title); - input.Description = Clean(input.Description); - input.Name = Clean(input.Name); - input.Keywords = Clean(input.Keywords); - input.Tags = Clean(input.Tags); - input.Url = Clean(input.Url); - input.PageType = Clean(input.PageType); - input.Alias = Clean(input.Alias); - input.LocalizedName = Clean(input.LocalizedName); - input.PageStyleSheet = Clean(input.PageStyleSheet); - if (input.PageHeaderTags != null) - { - foreach (var item in input.PageHeaderTags) - { - item.Name = Clean(item.Name); - } - } locale.Clean(); }