using System; using System.IO; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SharedLibraryCore; using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration.Attributes; using SharedLibraryCore.Configuration.Validation; using SharedLibraryCore.Interfaces; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using WebfrontCore.ViewModels; namespace WebfrontCore.Controllers { [Authorize] public class ConfigurationController : BaseController { private readonly ApplicationConfigurationValidator _validator; public ConfigurationController(IManager manager) : base(manager) { _validator = new ApplicationConfigurationValidator(); } /// /// Endpoint to get the current configuration view /// /// public IActionResult Edit() { if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner) { return Unauthorized(); } return RedirectToAction("Files"); } public async Task Files() { if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner) { return Unauthorized(); } try { // todo: move this into a service a some point var model = await Task.WhenAll(System.IO.Directory .GetFiles(System.IO.Path.Join(Utilities.OperatingDirectory, "Configuration")) .Where(file => file.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase)) .Select(async fileName => new ConfigurationFileInfo { FileName = fileName.Split(System.IO.Path.DirectorySeparatorChar).Last(), FileContent = await System.IO.File.ReadAllTextAsync(fileName) })); return View(model); } catch (Exception ex) { return Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); } } [HttpPatch("{Controller}/File/{fileName}")] public async Task PatchFiles([FromRoute] string fileName) { if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner) { return Unauthorized(); } if (!fileName.EndsWith(".json")) { return BadRequest("File must be of json format."); } using var reader = new StreamReader(Request.Body, Encoding.UTF8); var content = await reader.ReadToEndAsync(); if (string.IsNullOrEmpty(content)) { return BadRequest("File content cannot be empty"); } try { var file = JObject.Parse(content); } catch (JsonReaderException ex) { return BadRequest($"{fileName}: {ex.Message}"); } var path = System.IO.Path.Join(Utilities.OperatingDirectory, "Configuration", fileName.Replace($"{System.IO.Path.DirectorySeparatorChar}", "")); // todo: move into a service at some point if (!System.IO.File.Exists(path)) { return BadRequest($"{fileName} does not exist"); } try { await System.IO.File.WriteAllTextAsync(path, content); } catch (Exception ex) { return Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError); } return NoContent(); } /// /// Endpoint for the save action /// /// bound configuration /// [HttpPost] public async Task Save(ApplicationConfiguration newConfiguration) { // todo: make this authorization middleware instead of these checks if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner) { return Unauthorized(); } CleanConfiguration(newConfiguration); var validationResult = _validator.Validate(newConfiguration); if (validationResult.IsValid) { var currentConfiguration = Manager.GetApplicationSettings().Configuration(); CopyConfiguration(newConfiguration, currentConfiguration); await Manager.GetApplicationSettings().Save(); return Ok(new { message = new[] {Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_SAVED"]} }); } else { return BadRequest(new { message = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CONFIGURATION_SAVE_FAILED"], errors = new[] {validationResult.Errors.Select(_error => _error.ErrorMessage)} }); } } /// /// Cleans the configuration by removing empty items from from the array /// /// private void CleanConfiguration(ApplicationConfiguration newConfiguration) { void cleanProperties(object config) { foreach (var property in config.GetType() .GetProperties().Where(_prop => _prop.CanWrite)) { var newPropValue = property.GetValue(config); if (newPropValue is ServerConfiguration[] serverConfig) { foreach (var c in serverConfig) { cleanProperties(c); } } // this clears out any null or empty items in the string array if (newPropValue is string[] configArray) { newPropValue = configArray.Where(_str => !string.IsNullOrWhiteSpace(_str)).ToArray(); } property.SetValue(config, newPropValue); } } cleanProperties(newConfiguration); } /// /// Copies required config fields from new to old /// /// Source config /// Destination config private void CopyConfiguration(ApplicationConfiguration newConfiguration, ApplicationConfiguration oldConfiguration) { foreach (var property in newConfiguration.GetType() .GetProperties().Where(_prop => _prop.CanWrite)) { var newPropValue = property.GetValue(newConfiguration); bool isPropNullArray = property.PropertyType.IsArray && newPropValue == null; // this prevents us from setting a null array as that could screw reading up if (!ShouldIgnoreProperty(property) && !isPropNullArray) { property.SetValue(oldConfiguration, newPropValue); } } } /// /// Generates the partial view for a new list item /// /// name of the property the input element is generated for /// how many items exist already /// if it's a server property, which one /// public IActionResult GetNewListItem(string propertyName, int itemCount, int serverIndex = -1) { if (Client.Level < SharedLibraryCore.Database.Models.EFClient.Permission.Owner) { return Unauthorized(); } // todo: maybe make this cleaner in the future if (propertyName.StartsWith("Servers") && serverIndex < 0) { return PartialView("_ServerItem", new ApplicationConfiguration() { Servers = Enumerable.Repeat(new ServerConfiguration(), itemCount + 1).ToArray() }); } var model = new BindingHelper() { Properties = propertyName.Split("."), ItemIndex = itemCount, ParentItemIndex = serverIndex }; return PartialView("_ListItem", model); } /// /// Indicates if the property should be ignored when cleaning/copying from one config to another /// /// property info of the current property /// private bool ShouldIgnoreProperty(PropertyInfo info) => (info.GetCustomAttributes(false) .Where(_attr => _attr.GetType() == typeof(ConfigurationIgnore)) .FirstOrDefault() as ConfigurationIgnore) != null; } }