using System.Globalization; using System.Xml; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Data; using Nop.Services.Directory; using Nop.Services.Localization; using Nop.Services.Media; namespace Nop.Services.Catalog; /// /// Product attribute parser /// public partial class ProductAttributeParser : IProductAttributeParser { #region Fields protected readonly ICurrencyService _currencyService; protected readonly IDownloadService _downloadService; protected readonly ILocalizationService _localizationService; protected readonly IProductAttributeService _productAttributeService; protected readonly IRepository _productAttributeValueRepository; protected readonly IWorkContext _workContext; private static readonly char[] _separator = [',']; #endregion #region Ctor public ProductAttributeParser(ICurrencyService currencyService, IDownloadService downloadService, ILocalizationService localizationService, IProductAttributeService productAttributeService, IRepository productAttributeValueRepository, IWorkContext workContext) { _currencyService = currencyService; _downloadService = downloadService; _productAttributeService = productAttributeService; _productAttributeValueRepository = productAttributeValueRepository; _workContext = workContext; _localizationService = localizationService; } #endregion #region Utilities /// /// Returns a list which contains all possible combinations of elements /// /// Type of element /// Elements to make combinations /// All possible combinations of elements protected virtual IList> CreateCombination(IList elements) { var rez = new List>(); for (var i = 1; i < Math.Pow(2, elements.Count); i++) { var current = new List(); var index = -1; //transform int to binary string var binaryMask = Convert.ToString(i, 2).PadLeft(elements.Count, '0'); foreach (var flag in binaryMask) { index++; if (flag == '0') continue; //add element if binary mask in the position of element has 1 current.Add(elements[index]); } rez.Add(current); } return rez; } /// /// Gets selected product attribute values with the quantity entered by the customer /// /// Attributes in XML format /// Product attribute mapping identifier /// Collections of pairs of product attribute values and their quantity protected IList> ParseValuesWithQuantity(string attributesXml, int productAttributeMappingId) { var selectedValues = new List>(); if (string.IsNullOrEmpty(attributesXml)) return selectedValues; try { var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(attributesXml); foreach (XmlNode attributeNode in xmlDoc.SelectNodes(@"//Attributes/ProductAttribute")) { if (attributeNode.Attributes?["ID"] == null) continue; if (!int.TryParse(attributeNode.Attributes["ID"].InnerText.Trim(), out var attributeId) || attributeId != productAttributeMappingId) continue; foreach (XmlNode attributeValue in attributeNode.SelectNodes("ProductAttributeValue")) { var value = attributeValue.SelectSingleNode("Value").InnerText.Trim(); var quantityNode = attributeValue.SelectSingleNode("Quantity"); selectedValues.Add(new Tuple(value, quantityNode != null ? quantityNode.InnerText.Trim() : string.Empty)); } } } catch { // ignored } return selectedValues; } /// /// Adds gift cards attributes in XML format /// /// Product /// Form /// Attributes in XML format protected virtual void AddGiftCardsAttributesXml(Product product, IFormCollection form, ref string attributesXml) { if (!product.IsGiftCard) return; var recipientName = ""; var recipientEmail = ""; var senderName = ""; var senderEmail = ""; var giftCardMessage = ""; foreach (var formKey in form.Keys) { if (formKey.Equals($"giftcard_{product.Id}.RecipientName", StringComparison.InvariantCultureIgnoreCase)) { recipientName = form[formKey]; continue; } if (formKey.Equals($"giftcard_{product.Id}.RecipientEmail", StringComparison.InvariantCultureIgnoreCase)) { recipientEmail = form[formKey]; continue; } if (formKey.Equals($"giftcard_{product.Id}.SenderName", StringComparison.InvariantCultureIgnoreCase)) { senderName = form[formKey]; continue; } if (formKey.Equals($"giftcard_{product.Id}.SenderEmail", StringComparison.InvariantCultureIgnoreCase)) { senderEmail = form[formKey]; continue; } if (formKey.Equals($"giftcard_{product.Id}.Message", StringComparison.InvariantCultureIgnoreCase)) { giftCardMessage = form[formKey]; } } attributesXml = AddGiftCardAttribute(attributesXml, recipientName, recipientEmail, senderName, senderEmail, giftCardMessage); } /// /// Gets product attributes in XML format /// /// Product /// Form /// Errors /// /// A task that represents the asynchronous operation /// The task result contains the attributes in XML format /// protected virtual async Task GetProductAttributesXmlAsync(Product product, IFormCollection form, List errors) { var attributesXml = string.Empty; var productAttributes = await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id); foreach (var attribute in productAttributes) { var controlId = $"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}"; switch (attribute.AttributeControlType) { case AttributeControlType.DropdownList: case AttributeControlType.RadioList: case AttributeControlType.ColorSquares: case AttributeControlType.ImageSquares: { var ctrlAttributes = form[controlId]; if (!StringValues.IsNullOrEmpty(ctrlAttributes)) { var selectedAttributeId = int.Parse(ctrlAttributes); if (selectedAttributeId > 0) { //get quantity entered by customer var quantity = 1; var quantityStr = form[$"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}_{selectedAttributeId}_qty"]; if (!StringValues.IsNullOrEmpty(quantityStr) && (!int.TryParse(quantityStr, out quantity) || quantity < 1)) errors.Add(await _localizationService.GetResourceAsync("Products.QuantityShouldBePositive")); attributesXml = AddProductAttribute(attributesXml, attribute, selectedAttributeId.ToString(), quantity > 1 ? (int?)quantity : null); } } } break; case AttributeControlType.Checkboxes: { var ctrlAttributes = form[controlId]; if (!StringValues.IsNullOrEmpty(ctrlAttributes)) { foreach (var item in ctrlAttributes.ToString() .Split(_separator, StringSplitOptions.RemoveEmptyEntries)) { var selectedAttributeId = int.Parse(item); if (selectedAttributeId > 0) { //get quantity entered by customer var quantity = 1; var quantityStr = form[$"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}_{item}_qty"]; if (!StringValues.IsNullOrEmpty(quantityStr) && (!int.TryParse(quantityStr, out quantity) || quantity < 1)) errors.Add(await _localizationService.GetResourceAsync("Products.QuantityShouldBePositive")); attributesXml = AddProductAttribute(attributesXml, attribute, selectedAttributeId.ToString(), quantity > 1 ? (int?)quantity : null); } } } } break; case AttributeControlType.ReadonlyCheckboxes: { //load read-only (already server-side selected) values var attributeValues = await _productAttributeService.GetProductAttributeValuesAsync(attribute.Id); foreach (var selectedAttributeId in attributeValues .Where(v => v.IsPreSelected) .Select(v => v.Id) .ToList()) { //get quantity entered by customer var quantity = 1; var quantityStr = form[$"{NopCatalogDefaults.ProductAttributePrefix}{attribute.Id}_{selectedAttributeId}_qty"]; if (!StringValues.IsNullOrEmpty(quantityStr) && (!int.TryParse(quantityStr, out quantity) || quantity < 1)) errors.Add(await _localizationService.GetResourceAsync("Products.QuantityShouldBePositive")); attributesXml = AddProductAttribute(attributesXml, attribute, selectedAttributeId.ToString(), quantity > 1 ? (int?)quantity : null); } } break; case AttributeControlType.TextBox: case AttributeControlType.MultilineTextbox: { var ctrlAttributes = form[controlId]; if (!StringValues.IsNullOrEmpty(ctrlAttributes)) { var enteredText = ctrlAttributes.ToString().Trim(); attributesXml = AddProductAttribute(attributesXml, attribute, enteredText); } } break; case AttributeControlType.Datepicker: { var day = form[controlId + "_day"]; var month = form[controlId + "_month"]; var year = form[controlId + "_year"]; DateTime? selectedDate = null; try { selectedDate = new DateTime(int.Parse(year), int.Parse(month), int.Parse(day)); } catch { // ignored } if (selectedDate.HasValue) attributesXml = AddProductAttribute(attributesXml, attribute, selectedDate.Value.ToString("D")); } break; case AttributeControlType.FileUpload: { _ = Guid.TryParse(form[controlId], out var downloadGuid); var download = await _downloadService.GetDownloadByGuidAsync(downloadGuid); if (download != null) attributesXml = AddProductAttribute(attributesXml, attribute, download.DownloadGuid.ToString()); } break; default: break; } } //validate conditional attributes (if specified) foreach (var attribute in productAttributes) { var conditionMet = await IsConditionMetAsync(attribute, attributesXml); if (conditionMet.HasValue && !conditionMet.Value) { attributesXml = RemoveProductAttribute(attributesXml, attribute); } } return attributesXml; } /// /// Remove an attribute /// /// Attributes in XML format /// Attribute value id /// Updated result (XML format) protected virtual string RemoveAttribute(string attributesXml, int attributeValueId) { 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/{ChildElementName}"); 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 != attributeValueId) 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 identifiers /// /// Attributes in XML format /// Selected attribute identifiers protected virtual IList ParseAttributeIds(string attributesXml) { var ids = new List(); if (string.IsNullOrEmpty(attributesXml)) return ids; try { var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(attributesXml); var elements = xmlDoc.SelectNodes(@$"//Attributes/{ChildElementName}"); if (elements == null) return Array.Empty(); foreach (XmlNode node in elements) { if (node.Attributes?["ID"] == null) continue; var attributeValue = node.Attributes["ID"].InnerText.Trim(); if (int.TryParse(attributeValue, out var id)) ids.Add(id); } } catch { //ignore } return ids; } #endregion #region Product attributes /// /// Gets selected product attribute mappings /// /// Attributes in XML format /// /// A task that represents the asynchronous operation /// The task result contains the selected product attribute mappings /// public virtual async Task> ParseProductAttributeMappingsAsync(string attributesXml) { var result = new List(); if (string.IsNullOrEmpty(attributesXml)) return result; var ids = ParseAttributeIds(attributesXml); foreach (var id in ids) { var attribute = await _productAttributeService.GetProductAttributeMappingByIdAsync(id); if (attribute != null) result.Add(attribute); } return result; } /// /// /// Get product attribute values /// /// Attributes in XML format /// Product attribute mapping identifier; pass 0 to load all values /// /// A task that represents the asynchronous operation /// The task result contains the product attribute values /// public virtual async Task> ParseProductAttributeValuesAsync(string attributesXml, int productAttributeMappingId = 0) { var values = new List(); if (string.IsNullOrEmpty(attributesXml)) return values; var attributes = await ParseProductAttributeMappingsAsync(attributesXml); //to load values only for the passed product attribute mapping if (productAttributeMappingId > 0) attributes = attributes.Where(attribute => attribute.Id == productAttributeMappingId).ToList(); foreach (var attribute in attributes) { if (!attribute.ShouldHaveValues()) continue; foreach (var attributeValue in ParseValuesWithQuantity(attributesXml, attribute.Id)) { if (string.IsNullOrEmpty(attributeValue.Item1) || !int.TryParse(attributeValue.Item1, out var attributeValueId)) continue; var value = await _productAttributeService.GetProductAttributeValueByIdAsync(attributeValueId); if (value == null) continue; if (!string.IsNullOrEmpty(attributeValue.Item2) && int.TryParse(attributeValue.Item2, out var quantity) && quantity != value.Quantity) { //if customer enters quantity, use new entity with new quantity var oldValue = await _productAttributeValueRepository.LoadOriginalCopyAsync(value); oldValue.ProductAttributeMappingId = attribute.Id; oldValue.Quantity = quantity; values.Add(oldValue); } else values.Add(value); } } return values; } /// /// Gets selected product attribute values /// /// Attributes in XML format /// Product attribute mapping identifier /// Product attribute values public virtual IList ParseValues(string attributesXml, int productAttributeMappingId) { var selectedValues = new List(); if (string.IsNullOrEmpty(attributesXml)) return selectedValues; try { var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(attributesXml); var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/ProductAttribute"); 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 != productAttributeMappingId) continue; var nodeList2 = node1.SelectNodes(@"ProductAttributeValue/Value"); foreach (XmlNode node2 in nodeList2) { var value = node2.InnerText.Trim(); selectedValues.Add(value); } } } catch { //ignore } return selectedValues; } /// /// Adds an attribute /// /// Attributes in XML format /// Product attribute mapping /// Value /// Quantity (used with AttributeValueType.AssociatedToProduct to specify the quantity entered by the customer) /// Updated result (XML format) public virtual string AddProductAttribute(string attributesXml, ProductAttributeMapping productAttributeMapping, string value, int? quantity = null) { var result = string.Empty; try { var xmlDoc = new XmlDocument(); if (string.IsNullOrEmpty(attributesXml)) { var element1 = xmlDoc.CreateElement("Attributes"); xmlDoc.AppendChild(element1); } else { xmlDoc.LoadXml(attributesXml); } var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes"); XmlElement attributeElement = null; //find existing var nodeList1 = xmlDoc.SelectNodes(@"//Attributes/ProductAttribute"); 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 != productAttributeMapping.Id) continue; attributeElement = (XmlElement)node1; break; } //create new one if not found if (attributeElement == null) { attributeElement = xmlDoc.CreateElement("ProductAttribute"); attributeElement.SetAttribute("ID", productAttributeMapping.Id.ToString()); rootElement.AppendChild(attributeElement); } var attributeValueElement = xmlDoc.CreateElement("ProductAttributeValue"); attributeElement.AppendChild(attributeValueElement); var attributeValueValueElement = xmlDoc.CreateElement("Value"); attributeValueValueElement.InnerText = value; attributeValueElement.AppendChild(attributeValueValueElement); //the quantity entered by the customer if (quantity.HasValue) { var attributeValueQuantity = xmlDoc.CreateElement("Quantity"); attributeValueQuantity.InnerText = quantity.ToString(); attributeValueElement.AppendChild(attributeValueQuantity); } result = xmlDoc.OuterXml; } catch { //ignore } return result; } /// /// Remove an attribute /// /// Attributes in XML format /// Product attribute mapping /// Updated result (XML format) public virtual string RemoveProductAttribute(string attributesXml, ProductAttributeMapping productAttributeMapping) { return RemoveAttribute(attributesXml, productAttributeMapping.Id); } /// /// Are attributes equal /// /// The attributes of the first product /// The attributes of the second product /// A value indicating whether we should ignore non-combinable attributes /// A value indicating whether we should ignore the quantity of attribute value entered by the customer /// /// A task that represents the asynchronous operation /// The task result contains the result /// public virtual async Task AreProductAttributesEqualAsync(string attributesXml1, string attributesXml2, bool ignoreNonCombinableAttributes, bool ignoreQuantity = true) { var attributes1 = await ParseProductAttributeMappingsAsync(attributesXml1); if (ignoreNonCombinableAttributes) attributes1 = attributes1.Where(x => !x.IsNonCombinable()).ToList(); var attributes2 = await ParseProductAttributeMappingsAsync(attributesXml2); if (ignoreNonCombinableAttributes) attributes2 = attributes2.Where(x => !x.IsNonCombinable()).ToList(); if (attributes1.Count != attributes2.Count) return false; var attributesEqual = true; foreach (var a1 in attributes1) { var hasAttribute = false; foreach (var a2 in attributes2) { if (a1.Id != a2.Id) continue; hasAttribute = true; var values1Str = ParseValuesWithQuantity(attributesXml1, a1.Id); var values2Str = ParseValuesWithQuantity(attributesXml2, a2.Id); if (values1Str.Count == values2Str.Count) { foreach (var str1 in values1Str) { var hasValue = false; foreach (var str2 in values2Str) { //case insensitive? //if (str1.Trim().ToLowerInvariant() == str2.Trim().ToLowerInvariant()) if (str1.Item1.Trim() != str2.Item1.Trim()) continue; hasValue = ignoreQuantity || str1.Item2.Trim() == str2.Item2.Trim(); break; } if (hasValue) continue; attributesEqual = false; break; } } else { attributesEqual = false; break; } } if (hasAttribute) continue; attributesEqual = false; break; } return attributesEqual; } /// /// Check whether condition of some attribute is met (if specified). Return "null" if not condition is specified /// /// Product attribute /// Selected attributes (XML format) /// /// A task that represents the asynchronous operation /// The task result contains the result /// public virtual async Task IsConditionMetAsync(ProductAttributeMapping pam, string selectedAttributesXml) { ArgumentNullException.ThrowIfNull(pam); var conditionAttributeXml = pam.ConditionAttributeXml; if (string.IsNullOrEmpty(conditionAttributeXml)) //no condition return null; //load an attribute this one depends on var dependOnAttribute = (await ParseProductAttributeMappingsAsync(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; } /// /// Finds a product attribute combination by attributes stored in XML /// /// Product /// Attributes in XML format /// A value indicating whether we should ignore non-combinable attributes /// /// A task that represents the asynchronous operation /// The task result contains the found product attribute combination /// public virtual async Task FindProductAttributeCombinationAsync(Product product, string attributesXml, bool ignoreNonCombinableAttributes = true) { ArgumentNullException.ThrowIfNull(product); //anyway combination cannot contains non combinable attributes if (string.IsNullOrEmpty(attributesXml)) return null; var combinations = await _productAttributeService.GetAllProductAttributeCombinationsAsync(product.Id); return await combinations.FirstOrDefaultAwaitAsync(async x => await AreProductAttributesEqualAsync(x.AttributesXml, attributesXml, ignoreNonCombinableAttributes)); } /// /// Generate all combinations /// /// Product /// A value indicating whether we should ignore non-combinable attributes /// List of allowed attribute identifiers. If null or empty then all attributes would be used. /// /// A task that represents the asynchronous operation /// The task result contains the attribute combinations in XML format /// public virtual async Task> GenerateAllCombinationsAsync(Product product, bool ignoreNonCombinableAttributes = false, IList allowedAttributeIds = null) { ArgumentNullException.ThrowIfNull(product); var allProductAttributeMappings = await _productAttributeService.GetProductAttributeMappingsByProductIdAsync(product.Id); if (ignoreNonCombinableAttributes) allProductAttributeMappings = allProductAttributeMappings.Where(x => !x.IsNonCombinable()).ToList(); //get all possible attribute combinations var allPossibleAttributeCombinations = CreateCombination(allProductAttributeMappings); var allAttributesXml = new List(); foreach (var combination in allPossibleAttributeCombinations) { var attributesXml = new List(); foreach (var productAttributeMapping in combination) { if (!productAttributeMapping.ShouldHaveValues()) continue; //get product attribute values var attributeValues = await _productAttributeService.GetProductAttributeValuesAsync(productAttributeMapping.Id); //filter product attribute values if (allowedAttributeIds?.Any() ?? false) attributeValues = attributeValues.Where(attributeValue => allowedAttributeIds.Contains(attributeValue.Id)).ToList(); if (!attributeValues.Any()) continue; var isCheckbox = productAttributeMapping.AttributeControlType == AttributeControlType.Checkboxes || productAttributeMapping.AttributeControlType == AttributeControlType.ReadonlyCheckboxes; var currentAttributesXml = new List(); if (isCheckbox) { //add several values attribute types (checkboxes) //checkboxes could have several values ticked foreach (var oldXml in attributesXml.Any() ? attributesXml : [string.Empty]) { foreach (var checkboxCombination in CreateCombination(attributeValues)) { var newXml = oldXml; foreach (var checkboxValue in checkboxCombination) newXml = AddProductAttribute(newXml, productAttributeMapping, checkboxValue.Id.ToString()); if (!string.IsNullOrEmpty(newXml)) currentAttributesXml.Add(newXml); } } } else { //add one value attribute types (dropdownlist, radiobutton, color squares) foreach (var oldXml in attributesXml.Any() ? attributesXml : [string.Empty]) { currentAttributesXml.AddRange(attributeValues.Select(attributeValue => AddProductAttribute(oldXml, productAttributeMapping, attributeValue.Id.ToString()))); } } attributesXml.Clear(); attributesXml.AddRange(currentAttributesXml); } allAttributesXml.AddRange(attributesXml); } //validate conditional attributes (if specified) //minor workaround: //once it's done (validation), then we could have some duplicated combinations in result //we don't remove them here (for performance optimization) because anyway it'll be done in the "GenerateAllAttributeCombinations" method of ProductController for (var i = 0; i < allAttributesXml.Count; i++) { var attributesXml = allAttributesXml[i]; foreach (var attribute in allProductAttributeMappings) { var conditionMet = await IsConditionMetAsync(attribute, attributesXml); if (conditionMet.HasValue && !conditionMet.Value) allAttributesXml[i] = RemoveProductAttribute(attributesXml, attribute); } } return allAttributesXml; } /// /// Parse a customer entered price of the product /// /// Product /// Form /// /// A task that represents the asynchronous operation /// The task result contains the customer entered price of the product /// public virtual async Task ParseCustomerEnteredPriceAsync(Product product, IFormCollection form) { ArgumentNullException.ThrowIfNull(product); ArgumentNullException.ThrowIfNull(form); var customerEnteredPriceConverted = decimal.Zero; if (product.CustomerEntersPrice) foreach (var formKey in form.Keys) { if (formKey.Equals($"addtocart_{product.Id}.CustomerEnteredPrice", StringComparison.InvariantCultureIgnoreCase)) { if (decimal.TryParse(form[formKey], out var customerEnteredPrice)) customerEnteredPriceConverted = await _currencyService.ConvertToPrimaryStoreCurrencyAsync(customerEnteredPrice, await _workContext.GetWorkingCurrencyAsync()); break; } } return customerEnteredPriceConverted; } /// /// Parse a entered quantity of the product /// /// Product /// Form /// Customer entered price of the product public virtual int ParseEnteredQuantity(Product product, IFormCollection form) { ArgumentNullException.ThrowIfNull(product); ArgumentNullException.ThrowIfNull(form); var quantity = 1; foreach (var formKey in form.Keys) if (formKey.Equals($"addtocart_{product.Id}.EnteredQuantity", StringComparison.InvariantCultureIgnoreCase)) { _ = int.TryParse(form[formKey], out quantity); break; } return quantity; } /// /// Parse product rental dates on the product details page /// /// Product /// Form /// Start date /// End date public virtual void ParseRentalDates(Product product, IFormCollection form, out DateTime? startDate, out DateTime? endDate) { ArgumentNullException.ThrowIfNull(product); ArgumentNullException.ThrowIfNull(form); startDate = null; endDate = null; if (product.IsRental) { var ctrlStartDate = form[$"rental_start_date_{product.Id}"]; var ctrlEndDate = form[$"rental_end_date_{product.Id}"]; try { startDate = DateTime.ParseExact(ctrlStartDate, CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern, CultureInfo.InvariantCulture); endDate = DateTime.ParseExact(ctrlEndDate, CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern, CultureInfo.InvariantCulture); } catch { // ignored } } } /// /// Get product attributes from the passed form /// /// Product /// Form values /// Errors /// /// A task that represents the asynchronous operation /// The task result contains the attributes in XML format /// public virtual async Task ParseProductAttributesAsync(Product product, IFormCollection form, List errors) { ArgumentNullException.ThrowIfNull(product); ArgumentNullException.ThrowIfNull(form); //product attributes var attributesXml = await GetProductAttributesXmlAsync(product, form, errors); //gift cards AddGiftCardsAttributesXml(product, form, ref attributesXml); return attributesXml; } #endregion #region Gift card attributes /// /// Add gift card attributes /// /// Attributes in XML format /// Recipient name /// Recipient email /// Sender name /// Sender email /// Message /// Attributes public string AddGiftCardAttribute(string attributesXml, string recipientName, string recipientEmail, string senderName, string senderEmail, string giftCardMessage) { var result = string.Empty; try { recipientName = recipientName.Trim(); recipientEmail = recipientEmail.Trim(); senderName = senderName.Trim(); senderEmail = senderEmail.Trim(); var xmlDoc = new XmlDocument(); if (string.IsNullOrEmpty(attributesXml)) { var element1 = xmlDoc.CreateElement("Attributes"); xmlDoc.AppendChild(element1); } else xmlDoc.LoadXml(attributesXml); var rootElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes"); var giftCardElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo"); if (giftCardElement == null) { giftCardElement = xmlDoc.CreateElement("GiftCardInfo"); rootElement.AppendChild(giftCardElement); } var recipientNameElement = xmlDoc.CreateElement("RecipientName"); recipientNameElement.InnerText = recipientName; giftCardElement.AppendChild(recipientNameElement); var recipientEmailElement = xmlDoc.CreateElement("RecipientEmail"); recipientEmailElement.InnerText = recipientEmail; giftCardElement.AppendChild(recipientEmailElement); var senderNameElement = xmlDoc.CreateElement("SenderName"); senderNameElement.InnerText = senderName; giftCardElement.AppendChild(senderNameElement); var senderEmailElement = xmlDoc.CreateElement("SenderEmail"); senderEmailElement.InnerText = senderEmail; giftCardElement.AppendChild(senderEmailElement); var messageElement = xmlDoc.CreateElement("Message"); messageElement.InnerText = giftCardMessage; giftCardElement.AppendChild(messageElement); result = xmlDoc.OuterXml; } catch { //ignore } return result; } /// /// Get gift card attributes /// /// Attributes /// Recipient name /// Recipient email /// Sender name /// Sender email /// Message public void GetGiftCardAttribute(string attributesXml, out string recipientName, out string recipientEmail, out string senderName, out string senderEmail, out string giftCardMessage) { recipientName = string.Empty; recipientEmail = string.Empty; senderName = string.Empty; senderEmail = string.Empty; giftCardMessage = string.Empty; try { var xmlDoc = new XmlDocument(); xmlDoc.LoadXml(attributesXml); var recipientNameElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo/RecipientName"); var recipientEmailElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo/RecipientEmail"); var senderNameElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo/SenderName"); var senderEmailElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo/SenderEmail"); var messageElement = (XmlElement)xmlDoc.SelectSingleNode(@"//Attributes/GiftCardInfo/Message"); if (recipientNameElement != null) recipientName = recipientNameElement.InnerText; if (recipientEmailElement != null) recipientEmail = recipientEmailElement.InnerText; if (senderNameElement != null) senderName = senderNameElement.InnerText; if (senderEmailElement != null) senderEmail = senderEmailElement.InnerText; if (messageElement != null) giftCardMessage = messageElement.InnerText; } catch { //ignore } } #endregion #region Properties protected string ChildElementName { get; set; } = "ProductAttribute"; #endregion }