diff --git a/src/Business/Grand.Business.Checkout/Services/ExportImpot/OrderSchemaProperty.cs b/src/Business/Grand.Business.Checkout/Services/ExportImport/OrderSchemaProperty.cs similarity index 99% rename from src/Business/Grand.Business.Checkout/Services/ExportImpot/OrderSchemaProperty.cs rename to src/Business/Grand.Business.Checkout/Services/ExportImport/OrderSchemaProperty.cs index ac57291cae..bcf4655ae9 100644 --- a/src/Business/Grand.Business.Checkout/Services/ExportImpot/OrderSchemaProperty.cs +++ b/src/Business/Grand.Business.Checkout/Services/ExportImport/OrderSchemaProperty.cs @@ -3,7 +3,7 @@ using Grand.Business.Core.Utilities.ExportImport; using Grand.Domain.Orders; -namespace Grand.Business.Checkout.Services.ExportImpot; +namespace Grand.Business.Checkout.Services.ExportImport; public class OrderSchemaProperty : ISchemaProperty { diff --git a/src/Business/Grand.Business.Checkout/Startup/StartupApplication.cs b/src/Business/Grand.Business.Checkout/Startup/StartupApplication.cs index e96e584014..84c5b9659f 100644 --- a/src/Business/Grand.Business.Checkout/Startup/StartupApplication.cs +++ b/src/Business/Grand.Business.Checkout/Startup/StartupApplication.cs @@ -1,5 +1,5 @@ using Grand.Business.Checkout.Services.CheckoutAttributes; -using Grand.Business.Checkout.Services.ExportImpot; +using Grand.Business.Checkout.Services.ExportImport; using Grand.Business.Checkout.Services.GiftVouchers; using Grand.Business.Checkout.Services.Orders; using Grand.Business.Checkout.Services.Payments; diff --git a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs index b98d8880ca..86aa0c73ba 100644 --- a/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs +++ b/src/Business/Grand.Business.Marketing/Services/Contacts/ContactAttributeParser.cs @@ -318,16 +318,11 @@ public async Task> GetParseContactAttributes(IList x.Key == attribute.Id + "_day")?.Value; var month = model.FirstOrDefault(x => x.Key == attribute.Id + "_month")?.Value; var year = model.FirstOrDefault(x => x.Key == attribute.Id + "_year")?.Value; - DateTime? selectedDate = null; - try - { - selectedDate = new DateTime(int.Parse(year), int.Parse(month), int.Parse(date)); - } - catch { } - - if (selectedDate.HasValue) + if (int.TryParse(year, out var y) && y is >= 1 and <= 9999 && + int.TryParse(month, out var m) && m is >= 1 and <= 12 && + int.TryParse(date, out var d) && d >= 1 && d <= DateTime.DaysInMonth(y, m)) customAttributes = AddContactAttribute(customAttributes, - attribute, selectedDate.Value.ToString("D")).ToList(); + attribute, new DateTime(y, m, d).ToString("D")).ToList(); } break; case AttributeControlType.FileUpload: diff --git a/src/Tests/Grand.Business.Checkout.Tests/Services/ExportImport/OrderSchemaPropertyTests.cs b/src/Tests/Grand.Business.Checkout.Tests/Services/ExportImport/OrderSchemaPropertyTests.cs index 64af094e72..91157718d7 100644 --- a/src/Tests/Grand.Business.Checkout.Tests/Services/ExportImport/OrderSchemaPropertyTests.cs +++ b/src/Tests/Grand.Business.Checkout.Tests/Services/ExportImport/OrderSchemaPropertyTests.cs @@ -1,4 +1,4 @@ -using Grand.Business.Checkout.Services.ExportImpot; +using Grand.Business.Checkout.Services.ExportImport; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Domain.Common; using Grand.Domain.Directory; diff --git a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs index 6630be0520..06851aef37 100644 --- a/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs +++ b/src/Tests/Grand.Business.Marketing.Tests/Services/Contacts/ContactAttributeParserTests.cs @@ -154,6 +154,38 @@ public async Task GetParseContactAttributes_ReadsTheAttributesOfTheGivenAccess() Assert.AreEqual("text", result[0].Value); } + [TestMethod] + [DataRow("2024", "2", "29", true)] + [DataRow("2023", "2", "29", false)] + [DataRow("2024", "4", "30", true)] + [DataRow("2024", "4", "31", false)] + [DataRow("2024", "1", "0", false)] + [DataRow("2024", "0", "1", false)] + [DataRow("2024", "13", "1", false)] + [DataRow("1", "1", "1", true)] + [DataRow("0", "1", "1", false)] + [DataRow("9999", "12", "31", true)] + [DataRow("10000", "1", "1", false)] + [DataRow("abc", "1", "1", false)] + [DataRow(null, "1", "1", false)] + public async Task GetParseContactAttributes_Datepicker_AddsOnlyAValidDate(string year, string month, string day, + bool added) + { + var access = CatalogAccess.For(new Customer(), "store1"); + _contactAttributeServiceMock.Setup(c => c.GetAllContactAttributes(access)) + .ReturnsAsync([new ContactAttribute { Id = "date", AttributeControlType = AttributeControlType.Datepicker }]); + var model = new List { + new() { Key = "date_month", Value = month }, + new() { Key = "date_day", Value = day } + }; + if (year != null) + model.Add(new CustomAttribute { Key = "date_year", Value = year }); + + var result = await _parser.GetParseContactAttributes(model, access); + + Assert.AreEqual(added, result.Count == 1); + } + private async Task FormatFileUpload(DomainHost host, Guid? downloadGuid = null) { var attribute = new ContactAttribute { diff --git a/src/Tests/Grand.Web.Tests/Models/DateParsingModelsTests.cs b/src/Tests/Grand.Web.Tests/Models/DateParsingModelsTests.cs new file mode 100644 index 0000000000..2727ce47c5 --- /dev/null +++ b/src/Tests/Grand.Web.Tests/Models/DateParsingModelsTests.cs @@ -0,0 +1,47 @@ +using Grand.Web.Models.Blogs; +using Grand.Web.Models.Customer; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Tests.Models; + +[TestClass] +public class DateParsingModelsTests +{ + [TestMethod] + [DataRow(2024, 2, 29, true)] + [DataRow(2023, 2, 29, false)] + [DataRow(2024, 13, 1, false)] + [DataRow(2024, 4, 31, false)] + [DataRow(0, 1, 1, false)] + public void ParseDateOfBirth_ReturnsADateOnlyWhenItExists(int year, int month, int day, bool valid) + { + var register = new RegisterModel { DateOfBirthYear = year, DateOfBirthMonth = month, DateOfBirthDay = day }; + var info = new CustomerInfoModel { DateOfBirthYear = year, DateOfBirthMonth = month, DateOfBirthDay = day }; + + var expected = valid ? new DateTime(year, month, day) : (DateTime?)null; + Assert.AreEqual(expected, register.ParseDateOfBirth()); + Assert.AreEqual(expected, info.ParseDateOfBirth()); + } + + [TestMethod] + public void ParseDateOfBirth_MissingPart_ReturnsNull() + { + Assert.IsNull(new RegisterModel { DateOfBirthYear = 2000, DateOfBirthMonth = 1 }.ParseDateOfBirth()); + Assert.IsNull(new CustomerInfoModel { DateOfBirthYear = 2000, DateOfBirthDay = 1 }.ParseDateOfBirth()); + } + + [TestMethod] + [DataRow("2024-5", 2024, 5)] + [DataRow("2024-13", 0, 0)] + [DataRow("2024-0", 0, 0)] + [DataRow("abc-5", 0, 0)] + [DataRow("2024", 0, 0)] + [DataRow("", 0, 0)] + public void GetParsedMonth_ReturnsTheFirstDayOfAValidMonth(string month, int year, int expectedMonth) + { + var model = new BlogPagingFilteringModel { Month = month }; + + var expected = year > 0 ? new DateTime(year, expectedMonth, 1) : (DateTime?)null; + Assert.AreEqual(expected, model.GetParsedMonth()); + } +} diff --git a/src/Web/Grand.Web.Admin/Controllers/PluginController.cs b/src/Web/Grand.Web.Admin/Controllers/PluginController.cs index 43722646be..55e11536a3 100644 --- a/src/Web/Grand.Web.Admin/Controllers/PluginController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/PluginController.cs @@ -284,10 +284,15 @@ public IActionResult UploadPlugin(IFormFile zippedFile) var message = translationService.GetResource("Admin.Plugins.Uploaded"); Success(message); } + catch (Exception exc) + { + Error(exc); + return RedirectToAction("List"); + } finally { // Delete temporary file - DeleteDirectory(tempDirectory); + DeleteTempDirectory(tempDirectory); } logger.LogInformation("The plugin has been uploaded by the user {CurrentCustomerEmail}", @@ -299,6 +304,20 @@ public IActionResult UploadPlugin(IFormFile zippedFile) return RedirectToAction("List"); } + private void DeleteTempDirectory(string tempDirectory) + { + try + { + DeleteDirectory(tempDirectory); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + //the previous plugin version moved here can hold an assembly this process has loaded; + //it is not loaded from here on the next start and the next upload removes it + logger.LogWarning(ex, "The temporary upload folder {TempDirectory} could not be deleted", tempDirectory); + } + } + private void Upload(string archivePath) { var pluginsPath = Path.Combine(webHostEnvironment.ContentRootPath, CommonPath.Plugins); @@ -374,13 +393,27 @@ private void Upload(string archivePath) var pathToUpload = Path.Combine(pluginsPath, uploadedItemDirectoryName); - try + if (!Directory.Exists(pathToUpload)) { - DeleteDirectory(pathToUpload); + ZipFile.ExtractToDirectory(archivePath, pluginsPath); + return; } - catch { } - ZipFile.ExtractToDirectory(archivePath, pluginsPath); + //move the previous version into the temp upload folder instead of deleting it in place: + //a locked assembly fails the move before anything is changed, and a failed extraction puts it back + var previousVersionPath = Path.Combine(Path.GetDirectoryName(archivePath)!, Guid.NewGuid().ToString()); + Directory.Move(pathToUpload, previousVersionPath); + try + { + ZipFile.ExtractToDirectory(archivePath, pluginsPath); + } + catch + { + if (Directory.Exists(pathToUpload)) + DeleteDirectory(pathToUpload); + Directory.Move(previousVersionPath, pathToUpload); + throw; + } } #endregion diff --git a/src/Web/Grand.Web.Admin/Controllers/SystemController.cs b/src/Web/Grand.Web.Admin/Controllers/SystemController.cs index 99a78c15a1..03730f1c5d 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SystemController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SystemController.cs @@ -83,20 +83,10 @@ public async Task SystemInfo() var model = new SystemInfoModel { GrandVersion = GrandVersion.FullVersion, GitBranch = GrandVersion.GitBranch, - GitCommit = GrandVersion.GitCommit + GitCommit = GrandVersion.GitCommit, + OperatingSystem = RuntimeInformation.OSDescription, + AspNetInfo = RuntimeInformation.FrameworkDescription }; - try - { - model.OperatingSystem = RuntimeInformation.OSDescription; - } - catch (Exception) { } - - try - { - model.AspNetInfo = RuntimeEnvironment.GetSystemVersion(); - } - catch (Exception) { } - model.MachineName = Environment.MachineName; model.WebRootPath = _webHostEnvironment.WebRootPath; model.ContentRootPath = _webHostEnvironment.ContentRootPath; diff --git a/src/Web/Grand.Web.Models/Blogs/BlogPagingFilteringModel.cs b/src/Web/Grand.Web.Models/Blogs/BlogPagingFilteringModel.cs index 539a756080..31b4f840f1 100644 --- a/src/Web/Grand.Web.Models/Blogs/BlogPagingFilteringModel.cs +++ b/src/Web/Grand.Web.Models/Blogs/BlogPagingFilteringModel.cs @@ -8,19 +8,13 @@ public class BlogPagingFilteringModel : BasePageableModel public virtual DateTime? GetParsedMonth() { - DateTime? result = null; if (string.IsNullOrEmpty(Month)) return null; var tempDate = Month.Split(['-']); if (tempDate.Length != 2) return null; - int.TryParse(tempDate[0], out var year); - int.TryParse(tempDate[1], out var month); - try - { - result = new DateTime(year, month, 1); - } - catch { } - - return result; + if (!int.TryParse(tempDate[0], out var year) || year is < 1 or > 9999) return null; + if (!int.TryParse(tempDate[1], out var month) || month is < 1 or > 12) return null; + + return new DateTime(year, month, 1); } public virtual DateTime? GetFromMonth() diff --git a/src/Web/Grand.Web.Models/Customer/CustomerInfoModel.cs b/src/Web/Grand.Web.Models/Customer/CustomerInfoModel.cs index 7f12517277..12e9faba00 100644 --- a/src/Web/Grand.Web.Models/Customer/CustomerInfoModel.cs +++ b/src/Web/Grand.Web.Models/Customer/CustomerInfoModel.cs @@ -162,14 +162,11 @@ public class CustomerInfoModel : BaseModel if (!DateOfBirthYear.HasValue || !DateOfBirthMonth.HasValue || !DateOfBirthDay.HasValue) return null; - DateTime? dateOfBirth = null; - try - { - dateOfBirth = new DateTime(DateOfBirthYear.Value, DateOfBirthMonth.Value, DateOfBirthDay.Value); - } - catch { } - - return dateOfBirth; + int year = DateOfBirthYear.Value, month = DateOfBirthMonth.Value, day = DateOfBirthDay.Value; + if (year is < 1 or > 9999 || month is < 1 or > 12 || day < 1 || day > DateTime.DaysInMonth(year, month)) + return null; + + return new DateTime(year, month, day); } diff --git a/src/Web/Grand.Web.Models/Customer/RegisterModel.cs b/src/Web/Grand.Web.Models/Customer/RegisterModel.cs index 5b082b170d..b4bac6b7b5 100644 --- a/src/Web/Grand.Web.Models/Customer/RegisterModel.cs +++ b/src/Web/Grand.Web.Models/Customer/RegisterModel.cs @@ -165,13 +165,10 @@ public class RegisterModel : BaseModel if (!DateOfBirthYear.HasValue || !DateOfBirthMonth.HasValue || !DateOfBirthDay.HasValue) return null; - DateTime? dateOfBirth = null; - try - { - dateOfBirth = new DateTime(DateOfBirthYear.Value, DateOfBirthMonth.Value, DateOfBirthDay.Value); - } - catch { } - - return dateOfBirth; + int year = DateOfBirthYear.Value, month = DateOfBirthMonth.Value, day = DateOfBirthDay.Value; + if (year is < 1 or > 9999 || month is < 1 or > 12 || day < 1 || day > DateTime.DaysInMonth(year, month)) + return null; + + return new DateTime(year, month, day); } } \ No newline at end of file diff --git a/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs b/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs index b3359e6f37..1fecb19217 100644 --- a/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs +++ b/src/Web/Grand.Web/Commands/Handler/Contact/ContactAttributeChangeCommandHandler.cs @@ -97,19 +97,11 @@ private async Task> ParseContactAttributes(ContactAttribu var date = request.Attributes.FirstOrDefault(x => x.Key == attribute.Id + "_day")?.Value; var month = request.Attributes.FirstOrDefault(x => x.Key == attribute.Id + "_month")?.Value; var year = request.Attributes.FirstOrDefault(x => x.Key == attribute.Id + "_year")?.Value; - DateTime? selectedDate = null; - try - { - selectedDate = new DateTime(int.Parse(year), int.Parse(month), int.Parse(date)); - } - catch - { - // ignored - } - - if (selectedDate.HasValue) + if (int.TryParse(year, out var y) && y is >= 1 and <= 9999 && + int.TryParse(month, out var m) && m is >= 1 and <= 12 && + int.TryParse(date, out var d) && d >= 1 && d <= DateTime.DaysInMonth(y, m)) customAttributes = _contactAttributeParser.AddContactAttribute(customAttributes, - attribute, selectedDate.Value.ToString("D")).ToList(); + attribute, new DateTime(y, m, d).ToString("D")).ToList(); } break; }