using System.Xml; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Nop.Core.Domain.Attributes; using Nop.Core.Domain.Catalog; using Nop.Services.Localization; namespace Nop.Services.Attributes; /// /// Attribute parser /// /// Type of the attribute (see ) /// Type of the attribute value (see ) public partial class AttributeParser : IAttributeParser where TAttribute : BaseAttribute where TAttributeValue : BaseAttributeValue { #region Fields protected readonly IAttributeService _attributeService; protected readonly ILocalizationService _localizationService; protected readonly string _attributeName; protected readonly string _attributeValueName; private static readonly char[] _separator = [',']; #endregion #region Ctor public AttributeParser(IAttributeService attributeService, ILocalizationService localizationService) { _attributeName = typeof(TAttribute).Name; _attributeValueName = typeof(TAttributeValue).Name; _attributeService = attributeService; _localizationService = localizationService; } #endregion #region Utilities /// /// Gets attribute values /// /// string value attribute identifiers /// Attribute values protected virtual async IAsyncEnumerable GetValuesAsync(IList valuesStr) { foreach (var valueStr in valuesStr) { if (string.IsNullOrEmpty(valueStr)) continue; if (!int.TryParse(valueStr, out var id)) continue; var value = await _attributeService.GetAttributeValueByIdAsync(id); if (value != null) yield return value; } } #endregion #region Methods /// /// Gets selected attribute identifiers /// /// Attributes in XML format /// Selected attribute identifiers public virtual IEnumerable ParseAttributeIds(string attributesXml) { var ids = new List(); if (string.IsNullOrEmpty(attributesXml)) return ids; try { var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(attributesXml); var nodes = xmlDoc.SelectNodes(@$"//Attributes/{_attributeName}"); if (nodes == null) return Enumerable.Empty(); foreach (XmlNode node in nodes) { if (node.Attributes?["ID"] == null) continue; var str1 = node.Attributes["ID"].InnerText.Trim(); if (int.TryParse(str1, out var id)) ids.Add(id); } } catch { //ignore } return ids; } /// /// Gets selected attributes /// /// Attributes in XML format /// /// A task that represents the asynchronous operation /// The task result contains the selected attributes /// public virtual async Task> ParseAttributesAsync(string attributesXml) { var result = new List(); if (string.IsNullOrEmpty(attributesXml)) return result; var ids = ParseAttributeIds(attributesXml); foreach (var id in ids) { var attribute = await _attributeService.GetAttributeByIdAsync(id); if (attribute != null) result.Add(attribute); } return result; } /// /// Remove an attribute /// /// Attributes in XML format /// Attribute identifier /// Updated result (XML format) public virtual string RemoveAttribute(string attributesXml, int attributeId) { var result = string.Empty; if (string.IsNullOrEmpty(attributesXml)) return string.Empty; try { var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(attributesXml); var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes"); if (rootElement == null) return string.Empty; XmlElement attributeElement = null; //find existing var childNodes = xmlDoc.SelectNodes($@"//Attributes/{_attributeName}"); if (childNodes == null) return string.Empty; var count = childNodes.Count; foreach (XmlElement childNode in childNodes) { if (!int.TryParse(childNode.Attributes["ID"]?.InnerText.Trim(), out var id)) continue; if (id != attributeId) continue; attributeElement = childNode; break; } //found if (attributeElement != null) { rootElement.RemoveChild(attributeElement); count -= 1; } result = count == 0 ? string.Empty : xmlDoc.OuterXml; } catch { //ignore } return result; } /// /// Gets selected attribute value /// /// Attributes in XML format /// Attribute identifier /// Attribute value public virtual IList ParseValues(string attributesXml, int attributeId) { var selectedAddressAttributeValues = new List(); if (string.IsNullOrEmpty(attributesXml)) return selectedAddressAttributeValues; try { var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(attributesXml); var nodeList1 = xmlDoc.SelectNodes(@$"//Attributes/{_attributeName}"); if (nodeList1 == null) return new List(); foreach (XmlNode node1 in nodeList1) { if (node1.Attributes?["ID"] == null) continue; var str1 = node1.Attributes["ID"].InnerText.Trim(); if (!int.TryParse(str1, out var id)) continue; if (id != attributeId) continue; var nodeList2 = node1.SelectNodes(@$"{_attributeValueName}/Value"); if (nodeList2 == null) continue; foreach (XmlNode node2 in nodeList2) { var value = node2.InnerText.Trim(); selectedAddressAttributeValues.Add(value); } } } catch { //ignore } return selectedAddressAttributeValues; } /// /// Get attribute values /// /// Attributes in XML format /// /// A task that represents the asynchronous operation /// The task result contains the attribute values /// public virtual async Task> ParseAttributeValuesAsync(string attributesXml) { var values = new List(); if (string.IsNullOrEmpty(attributesXml)) return values; var attributes = await ParseAttributesAsync(attributesXml); foreach (var attribute in attributes) { if (!attribute.ShouldHaveValues) continue; var valuesStr = ParseValues(attributesXml, attribute.Id); values.AddRange(await GetValuesAsync(valuesStr).ToArrayAsync()); } return values; } /// /// Get attribute values /// /// Attributes in XML format /// Attribute values public virtual async IAsyncEnumerable<(TAttribute attribute, IAsyncEnumerable values)> ParseAttributeValues(string attributesXml) { if (string.IsNullOrEmpty(attributesXml)) yield break; var attributes = await ParseAttributesAsync(attributesXml); foreach (var attribute in attributes) { if (!attribute.ShouldHaveValues) continue; var valuesStr = ParseValues(attributesXml, attribute.Id); yield return (attribute, GetValuesAsync(valuesStr)); } } /// /// Adds an attribute /// /// Attributes in XML format /// Attribute /// Attribute value /// Attributes public virtual string AddAttribute(string attributesXml, TAttribute attribute, string value) { var result = string.Empty; try { var xmlDoc = new XmlDocument(); if (string.IsNullOrEmpty(attributesXml)) xmlDoc.AppendChild(xmlDoc.CreateElement("Attributes")); else xmlDoc.LoadXml(attributesXml); var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes")!; XmlElement attributeElement = null; //find existing var nodeList1 = xmlDoc.SelectNodes(@$"//Attributes/{_attributeName}"); if (nodeList1 != null) foreach (XmlNode node1 in nodeList1) { if (node1.Attributes?["ID"] == null) continue; var str1 = node1.Attributes["ID"].InnerText.Trim(); if (!int.TryParse(str1, out var id)) continue; if (id != attribute.Id) continue; attributeElement = (XmlElement)node1; break; } //create new one if not found if (attributeElement == null) { attributeElement = xmlDoc.CreateElement(_attributeName); attributeElement.SetAttribute("ID", attribute.Id.ToString()); rootElement.AppendChild(attributeElement); } var attributeValueElement = xmlDoc.CreateElement(_attributeValueName); attributeElement.AppendChild(attributeValueElement); var attributeValueValueElement = xmlDoc.CreateElement("Value"); attributeValueValueElement.InnerText = value; attributeValueElement.AppendChild(attributeValueValueElement); result = xmlDoc.OuterXml; } catch { //ignore } return result; } /// /// Validates attributes /// /// Attributes in XML format /// /// A task that represents the asynchronous operation /// The task result contains the warnings /// public virtual async Task> GetAttributeWarningsAsync(string attributesXml) { var warnings = new List(); //ensure it's our attributes var attributes1 = await ParseAttributesAsync(attributesXml); //validate required attributes (whether they're chosen/selected/entered) var attributes2 = await _attributeService.GetAllAttributesAsync(); foreach (var a2 in attributes2) { if (!a2.IsRequired) continue; var found = false; //selected attributes foreach (var a1 in attributes1) { if (a1.Id != a2.Id) continue; var valuesStr = ParseValues(attributesXml, a1.Id); found = valuesStr.Any(str1 => !string.IsNullOrEmpty(str1.Trim())); } if (found) continue; //if not found var notFoundWarning = string.Format(await _localizationService.GetResourceAsync("ShoppingCart.SelectAttribute"), await _localizationService.GetLocalizedAsync(a2, a => a.Name)); warnings.Add(notFoundWarning); } return warnings; } /// /// Get custom attributes from the passed form /// /// Form values /// Name of the attribute control /// /// A task that represents the asynchronous operation /// The task result contains the attributes in XML format /// public virtual async Task ParseCustomAttributesAsync(IFormCollection form, string attributeControlName) { ArgumentNullException.ThrowIfNull(form); var attributesXml = string.Empty; foreach (var attribute in await _attributeService.GetAllAttributesAsync()) { var controlId = string.Format(attributeControlName, attribute.Id); var attributeValues = form[controlId]; switch (attribute.AttributeControlType) { case AttributeControlType.DropdownList: case AttributeControlType.RadioList: if (!StringValues.IsNullOrEmpty(attributeValues) && int.TryParse(attributeValues, out var value) && value > 0) attributesXml = AddAttribute(attributesXml, attribute, value.ToString()); break; case AttributeControlType.Checkboxes: foreach (var attributeValue in attributeValues.ToString().Split(_separator, StringSplitOptions.RemoveEmptyEntries)) { if (!int.TryParse(attributeValue, out value) || value <= 0) continue; attributesXml = AddAttribute(attributesXml, attribute, value.ToString()); } break; case AttributeControlType.ReadonlyCheckboxes: //load read-only (already server-side selected) values var readOnlyAttributeValues = await _attributeService.GetAttributeValuesAsync(attribute.Id); foreach (var readOnlyAttributeValue in readOnlyAttributeValues) { if (!readOnlyAttributeValue.IsPreSelected) continue; attributesXml = AddAttribute(attributesXml, attribute, readOnlyAttributeValue.Id.ToString()); } break; case AttributeControlType.TextBox: case AttributeControlType.MultilineTextbox: if (!StringValues.IsNullOrEmpty(attributeValues)) attributesXml = AddAttribute(attributesXml, attribute, attributeValues.ToString().Trim()); break; case AttributeControlType.Datepicker: case AttributeControlType.ColorSquares: case AttributeControlType.ImageSquares: case AttributeControlType.FileUpload: default: break; } } return attributesXml; } /// /// Check whether condition of some attribute is met (if specified). Return "null" if not condition is specified /// /// Condition attributes (XML format) /// Selected attributes (XML format) /// /// A task that represents the asynchronous operation /// The task result contains the result /// public async Task IsConditionMetAsync(string conditionAttributeXml, string selectedAttributesXml) { if (string.IsNullOrEmpty(conditionAttributeXml)) //no condition return null; //load an attribute this one depends on var dependOnAttribute = (await ParseAttributesAsync(conditionAttributeXml)).FirstOrDefault(); if (dependOnAttribute == null) return true; var valuesThatShouldBeSelected = ParseValues(conditionAttributeXml, dependOnAttribute.Id) //a workaround here: //ConditionAttributeXml can contain "empty" values (nothing is selected) //but in other cases (like below) we do not store empty values //that's why we remove empty values here .Where(x => !string.IsNullOrEmpty(x)) .ToList(); var selectedValues = ParseValues(selectedAttributesXml, dependOnAttribute.Id); if (valuesThatShouldBeSelected.Count != selectedValues.Count) return false; //compare values var allFound = true; foreach (var t1 in valuesThatShouldBeSelected) { var found = false; foreach (var t2 in selectedValues) if (t1 == t2) found = true; if (!found) allFound = false; } return allFound; } #endregion }