diff --git a/.gitignore b/.gitignore
index dfcfd56..1ee5385 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ mono_crash.*
[Rr]eleases/
x64/
x86/
+[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
@@ -61,6 +62,9 @@ project.lock.json
project.fragment.lock.json
artifacts/
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
# StyleCop
StyleCopReport.xml
@@ -137,6 +141,11 @@ _TeamCity*
.axoCover/*
!.axoCover/settings.json
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
# Visual Studio code coverage results
*.coverage
*.coveragexml
@@ -348,3 +357,6 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
diff --git a/MarkdownEditor.sln b/MarkdownEditor.sln
index 058353d..e23fde2 100644
--- a/MarkdownEditor.sln
+++ b/MarkdownEditor.sln
@@ -1,12 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31729.503
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32014.148
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSC.Blazor.Components.MarkdownEditor", "PSC.Blazor.Components.MarkdownEditor\PSC.Blazor.Components.MarkdownEditor.csproj", "{6F56954A-A45C-432C-9F63-699D15042667}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkdownEditorDemo", "MarkdownEditorDemo\MarkdownEditorDemo.csproj", "{4A71D48E-5A73-49F6-B813-2919DB84DC29}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkdownEditorDemo.Api", "MarkdownEditorDemo.Api\MarkdownEditorDemo.Api.csproj", "{9771B329-8344-4839-81D5-78B9F9AA79F4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Other files", "Other files", "{E62D6583-788F-4BDE-833F-6EE650453874}"
+ ProjectSection(SolutionItems) = preProject
+ README.md = README.md
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +28,10 @@ Global
{4A71D48E-5A73-49F6-B813-2919DB84DC29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A71D48E-5A73-49F6-B813-2919DB84DC29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A71D48E-5A73-49F6-B813-2919DB84DC29}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9771B329-8344-4839-81D5-78B9F9AA79F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9771B329-8344-4839-81D5-78B9F9AA79F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9771B329-8344-4839-81D5-78B9F9AA79F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9771B329-8344-4839-81D5-78B9F9AA79F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MarkdownEditorDemo.Api/Controllers/FilesController.cs b/MarkdownEditorDemo.Api/Controllers/FilesController.cs
new file mode 100644
index 0000000..2c76b69
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Controllers/FilesController.cs
@@ -0,0 +1,112 @@
+namespace MarkdownEditorDemo.Api.Controllers;
+
+///
+/// Files controller
+///
+[ApiController]
+[Route("api/[Controller]")]
+public class FilesController : ControllerBase
+{
+ ///
+ /// The logger
+ ///
+ private readonly ILogger _logger;
+ ///
+ /// The HTTP context accessor
+ ///
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ ///
+ /// The file size limit
+ ///
+ private readonly long _fileSizeLimit;
+ ///
+ /// The permitted extensions
+ ///
+ private readonly string[] _permittedExtensions = { ".gif", ".png", ".jpg", ".jpeg" };
+ ///
+ /// The target folder path
+ ///
+ private readonly string _targetFolderPath;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The configuration.
+ public FilesController(ILogger logger, IConfiguration config, IHttpContextAccessor httpContextAccessor)
+ {
+ _logger = logger;
+ _httpContextAccessor = httpContextAccessor;
+
+ _fileSizeLimit = config.GetValue("FileSizeLimit");
+ _targetFolderPath = (string)System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
+ _targetFolderPath = Path.Combine(_targetFolderPath, "Uploads").Replace("file:\\", "");
+
+ Directory.CreateDirectory(_targetFolderPath);
+ }
+
+ ///
+ /// Uploads a file.
+ ///
+ ///
+ [HttpPost]
+ [DisableFormValueModelBinding]
+ public async Task Upload()
+ {
+ if (!Request.ContentType.IsMultipartContentType())
+ {
+ ModelState.AddModelError("File", "The request couldn't be processed (Error 1).");
+ _logger.LogWarning($"The request content type [{Request.ContentType}] is invalid.");
+ return BadRequest(ModelState);
+ }
+
+ var boundary = MediaTypeHeaderValue.Parse(Request.ContentType).GetBoundary(new FormOptions().MultipartBoundaryLengthLimit);
+ var reader = new MultipartReader(boundary, HttpContext.Request.Body);
+ var section = await reader.ReadNextSectionAsync();
+
+ string trustedFileNameForFileStorage = String.Empty;
+
+ while (section != null)
+ {
+ var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition,
+ out var contentDisposition);
+
+ if (hasContentDispositionHeader)
+ {
+ if (contentDisposition.IsFileDisposition())
+ {
+ // Don't trust the file name sent by the client. To display the file name, HTML-encode the value.
+ var trustedFileNameForDisplay = WebUtility.HtmlEncode(contentDisposition.FileName.Value);
+ trustedFileNameForFileStorage = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) +
+ Path.GetExtension(trustedFileNameForDisplay);
+
+ var streamedFileContent = await FileHelpers.ProcessStreamedFile(section, contentDisposition,
+ ModelState, _permittedExtensions, _fileSizeLimit);
+
+ if (!ModelState.IsValid)
+ return BadRequest(ModelState);
+
+ var trustedFilePath = Path.Combine(_targetFolderPath, trustedFileNameForFileStorage);
+ using (var targetStream = System.IO.File.Create(trustedFilePath))
+ {
+ await targetStream.WriteAsync(streamedFileContent);
+ _logger.LogInformation($"Uploaded file '{trustedFileNameForDisplay}' saved to '{_targetFolderPath}' " +
+ $"as {trustedFileNameForFileStorage}");
+ }
+ }
+ }
+
+ // Drain any remaining section body that hasn't been consumed and read the headers for the next section.
+ section = await reader.ReadNextSectionAsync();
+ }
+
+ if (!string.IsNullOrEmpty(trustedFileNameForFileStorage))
+ {
+ string host = $"{_httpContextAccessor.HttpContext.Request.Scheme}://{_httpContextAccessor.HttpContext.Request.Host.Value}";
+ int index = FileHelpers.GetExtensionId(Path.GetExtension(trustedFileNameForFileStorage));
+ return Ok($"{host}/{index}/{Path.GetFileName(trustedFileNameForFileStorage)}");
+ }
+ else
+ return BadRequest("It wasn't possible to upload the file");
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/Controllers/HomeController.cs b/MarkdownEditorDemo.Api/Controllers/HomeController.cs
new file mode 100644
index 0000000..3ff568c
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Controllers/HomeController.cs
@@ -0,0 +1,56 @@
+namespace MarkdownEditorDemo.Api.Controllers
+{
+ public class HomeController : Controller
+ {
+ ///
+ /// The logger
+ ///
+ private readonly ILogger _logger;
+ ///
+ /// The target folder path
+ ///
+ private readonly string _targetFolderPath;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The configuration.
+ public HomeController(ILogger logger, IConfiguration config)
+ {
+ _logger = logger;
+
+ _targetFolderPath = (string)System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
+ _targetFolderPath = Path.Combine(_targetFolderPath, "Uploads");
+ }
+
+ [HttpGet]
+ [Route("{fileType}/{fileName}")]
+ public async Task Download(int fileType, string fileName)
+ {
+ string ext = FileHelpers.GetExtensionById(fileType);
+ if (ext == null)
+ {
+ _logger.LogInformation($"Extension not found.");
+ return BadRequest($"Extension not found.");
+ }
+
+ if(ext != Path.GetExtension(fileName))
+ {
+ _logger.LogInformation($"File not valid.");
+ return BadRequest($"File not valid.");
+ }
+
+ var trustedFilePath = Path.Combine(_targetFolderPath, fileName).Replace("file:\\", "");
+ if (!System.IO.File.Exists(trustedFilePath))
+ {
+ _logger.LogInformation($"File {trustedFilePath} not exists");
+ return NotFound($"File {trustedFilePath} not exists");
+ }
+
+ _logger.LogInformation($"Downloading file [{trustedFilePath}].");
+ var bytes = await System.IO.File.ReadAllBytesAsync(trustedFilePath);
+ return File(bytes, ext.GetMimeTypes(), trustedFilePath);
+ }
+ }
+}
diff --git a/MarkdownEditorDemo.Api/Controllers/ImageController.cs b/MarkdownEditorDemo.Api/Controllers/ImageController.cs
new file mode 100644
index 0000000..3c7ca07
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Controllers/ImageController.cs
@@ -0,0 +1,59 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace MarkdownEditorDemo.Api.Controllers
+{
+ [ApiController]
+ [Route("api/[controller]")]
+ public class ImageController : Controller
+ {
+ [HttpPost]
+ public async Task Upload(IFormFile files)
+ {
+ long size = files.Length;
+
+ var filePath = Path.GetTempFileName();
+
+ using (var stream = System.IO.File.Create(filePath))
+ {
+ await files.CopyToAsync(stream);
+ }
+
+ // Process uploaded files
+ // Don't rely on or trust the FileName property without validation.
+
+ return Ok();
+
+ //// Check if thefile is there
+ //if (file == null)
+ // return BadRequest("File is required");
+
+ //// Get the file name
+ //var fileName = file.FileName;
+
+ //// Get the extension
+ //var extension = Path.GetExtension(fileName);
+
+ //// Validate the extension based on your business needs
+
+ //// Generate a new file to avoid dublicates = (FileName withoutExtension - GUId.extension)
+ //var newFileName = $"{Path.GetFileNameWithoutExtension(fileName)}-{Guid.NewGuid().ToString()}{extension}";
+
+ //// Create the full path of the file including the directory (For this demo we will save the file insidea folder called Data within wwwroot)
+ //var directoryPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "Data");
+ //var fullPath = Path.Combine(directoryPath, newFileName);
+
+ //// Maek sure the directory is ther bycreating it if it's not exist
+ //Directory.CreateDirectory(directoryPath);
+
+ //// Create a new file stream where you want to put your file and copy the content from the current file stream to the new one
+ //using (var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write))
+ //{
+ // // Copy the content to the new stream
+ // await file.CopyToAsync(fileStream);
+ //}
+
+ //// You are done return the new URL which is (yourapplication url/data/newfilename)
+ //return Ok($"https://localhost:44302/Data/{newFileName}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/Extensions/MultipartRequestExtensions.cs b/MarkdownEditorDemo.Api/Extensions/MultipartRequestExtensions.cs
new file mode 100644
index 0000000..6ac440a
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Extensions/MultipartRequestExtensions.cs
@@ -0,0 +1,25 @@
+namespace MarkdownEditorDemo.Api.Extensions;
+
+public static class MultipartRequestExtensions
+{
+ // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
+ // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
+ public static string GetBoundary(this MediaTypeHeaderValue contentType, int lengthLimit)
+ {
+ var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
+
+ if (string.IsNullOrWhiteSpace(boundary))
+ throw new InvalidDataException("Missing content-type boundary.");
+
+ if (boundary.Length > lengthLimit)
+ throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
+
+ return boundary;
+ }
+
+ public static bool IsMultipartContentType(this string contentType)
+ {
+ return !string.IsNullOrEmpty(contentType)
+ && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/Extensions/StringExtensions.cs b/MarkdownEditorDemo.Api/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..3f36d67
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Extensions/StringExtensions.cs
@@ -0,0 +1,31 @@
+namespace MarkdownEditorDemo.Api.Extensions
+{
+ public static class StringExtensions
+ {
+ ///
+ /// Gets the MIME types.
+ ///
+ /// The ext.
+ ///
+ public static string GetMimeTypes(this string ext)
+ {
+ switch (ext)
+ {
+ case ".txt": return "text/plain";
+ case ".csv": return "text/csv";
+ case ".pdf": return "application/pdf";
+ case ".doc": return "application/vnd.ms-word";
+ case ".xls": return "application/vnd.ms-excel";
+ case ".ppt": return "application/vnd.ms-powerpoint";
+ case ".docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+ case ".xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+ case ".pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
+ case ".png": return "image/png";
+ case ".jpg": return "image/jpeg";
+ case ".jpeg": return "image/jpeg";
+ case ".gif": return "image/gif";
+ default: return "application/octet-stream";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/Filters/ModelBinding.cs b/MarkdownEditorDemo.Api/Filters/ModelBinding.cs
new file mode 100644
index 0000000..2d5efb2
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Filters/ModelBinding.cs
@@ -0,0 +1,21 @@
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace MarkdownEditorDemo.Api.Filters
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
+ public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
+ {
+ public void OnResourceExecuting(ResourceExecutingContext context)
+ {
+ var factories = context.ValueProviderFactories;
+ factories.RemoveType();
+ factories.RemoveType();
+ factories.RemoveType();
+ }
+
+ public void OnResourceExecuted(ResourceExecutedContext context)
+ {
+ }
+ }
+}
diff --git a/MarkdownEditorDemo.Api/GlobalUsing.cs b/MarkdownEditorDemo.Api/GlobalUsing.cs
new file mode 100644
index 0000000..37fd88e
--- /dev/null
+++ b/MarkdownEditorDemo.Api/GlobalUsing.cs
@@ -0,0 +1,8 @@
+global using MarkdownEditorDemo.Api.Extensions;
+global using MarkdownEditorDemo.Api.Filters;
+global using MarkdownEditorDemo.Api.Utilities;
+global using Microsoft.AspNetCore.Http.Features;
+global using Microsoft.AspNetCore.Mvc;
+global using Microsoft.AspNetCore.WebUtilities;
+global using Microsoft.Net.Http.Headers;
+global using System.Net;
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/MarkdownEditorDemo.Api.csproj b/MarkdownEditorDemo.Api/MarkdownEditorDemo.Api.csproj
new file mode 100644
index 0000000..e712599
--- /dev/null
+++ b/MarkdownEditorDemo.Api/MarkdownEditorDemo.Api.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/MarkdownEditorDemo.Api/Program.cs b/MarkdownEditorDemo.Api/Program.cs
new file mode 100644
index 0000000..750fd58
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Program.cs
@@ -0,0 +1,43 @@
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+builder.Services.AddApplicationInsightsTelemetry();
+
+builder.Services.AddSingleton();
+
+// Add CORS
+var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
+builder.Services.AddCors(options =>
+{
+ options.AddPolicy(name: MyAllowSpecificOrigins,
+ builder =>
+ {
+ builder.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+});
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.UseCors(MyAllowSpecificOrigins);
+
+app.MapControllers();
+
+app.Run();
diff --git a/MarkdownEditorDemo.Api/Properties/launchSettings.json b/MarkdownEditorDemo.Api/Properties/launchSettings.json
new file mode 100644
index 0000000..00ba441
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:3845",
+ "sslPort": 44304
+ }
+ },
+ "profiles": {
+ "MarkdownEditorDemo.Api": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7013;http://localhost:5013",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/MarkdownEditorDemo.Api/Properties/serviceDependencies.json b/MarkdownEditorDemo.Api/Properties/serviceDependencies.json
new file mode 100644
index 0000000..c264e8c
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Properties/serviceDependencies.json
@@ -0,0 +1,7 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "type": "appInsights"
+ }
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/Properties/serviceDependencies.local.json b/MarkdownEditorDemo.Api/Properties/serviceDependencies.local.json
new file mode 100644
index 0000000..5a956e8
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Properties/serviceDependencies.local.json
@@ -0,0 +1,7 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "type": "appInsights.sdk"
+ }
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/Utilities/FileHelpers.cs b/MarkdownEditorDemo.Api/Utilities/FileHelpers.cs
new file mode 100644
index 0000000..603cc91
--- /dev/null
+++ b/MarkdownEditorDemo.Api/Utilities/FileHelpers.cs
@@ -0,0 +1,194 @@
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace MarkdownEditorDemo.Api.Utilities
+{
+ ///
+ /// FileHelpers
+ ///
+ public static class FileHelpers
+ {
+ // If you require a check on specific characters in the IsValidFileExtensionAndSignature
+ // method, supply the characters in the _allowedChars field.
+ private static readonly byte[] AllowedChars = { };
+
+ // For more file signatures, see the File Signatures Database (https://www.filesignatures.net/)
+ // and the official specifications for the file types you wish to add.
+ private static readonly Dictionary> FileSignature = new Dictionary>
+ {
+ { ".gif", new List {
+ new byte[] { 0x47, 0x49, 0x46, 0x38 } }
+ },
+ { ".png", new List {
+ new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } }
+ },
+ { ".jpeg", new List
+ {
+ new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
+ new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
+ new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
+ }
+ },
+ { ".jpg", new List
+ {
+ new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
+ new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
+ new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 },
+ }
+ },
+ { ".zip", new List
+ {
+ new byte[] { 0x50, 0x4B, 0x03, 0x04 },
+ new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
+ new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
+ new byte[] { 0x50, 0x4B, 0x05, 0x06 },
+ new byte[] { 0x50, 0x4B, 0x07, 0x08 },
+ new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
+ }
+ },
+ };
+
+ ///
+ /// Gets the extension identifier.
+ ///
+ /// The extension.
+ ///
+ public static int GetExtensionId(this string extension)
+ {
+ for (int i = 0; i < FileSignature.Count; ++i)
+ {
+ if (FileSignature.ElementAt(i).Key == extension)
+ return i;
+ }
+ return -1;
+ }
+
+ ///
+ /// Gets the extension by identifier.
+ ///
+ /// The identifier.
+ ///
+ public static string GetExtensionById(this int id)
+ {
+ if (id >= 0 && id <= FileSignature.Count)
+ return FileSignature.ElementAt(id).Key;
+ else
+ return null;
+ }
+
+ ///
+ /// Gets the byte[] from the MemoryStream for a file
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async Task ProcessStreamedFile(
+ MultipartSection section, ContentDispositionHeaderValue contentDisposition,
+ ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit)
+ {
+ try
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ await section.Body.CopyToAsync(memoryStream);
+
+ // Check if the file is empty or exceeds the size limit.
+ if (memoryStream.Length == 0)
+ modelState.AddModelError("File", "The file is empty.");
+ else if (memoryStream.Length > sizeLimit)
+ {
+ var megabyteSizeLimit = sizeLimit / 1048576;
+ modelState.AddModelError("File", $"The file exceeds {megabyteSizeLimit:N1} MB.");
+ }
+ else if (!IsValidFileExtensionAndSignature(contentDisposition.FileName.Value, memoryStream, permittedExtensions))
+ modelState.AddModelError("File", "The file type isn't permitted or the file's signature doesn't match the file's extension.");
+ else
+ return memoryStream.ToArray();
+ }
+ }
+ catch (Exception ex)
+ {
+ modelState.AddModelError("File", $"The upload failed. Please contact the Help Desk for support. Error: {ex.HResult}");
+ }
+
+ return new byte[0];
+ }
+
+ private static bool IsValidFileExtensionAndSignature(string fileName, Stream data, IEnumerable permittedExtensions)
+ {
+ if (string.IsNullOrEmpty(fileName) || data == null || data.Length == 0)
+ {
+ return false;
+ }
+
+ var ext = Path.GetExtension(fileName).ToLowerInvariant();
+
+ if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
+ {
+ return false;
+ }
+
+ data.Position = 0;
+
+ using (var reader = new BinaryReader(data))
+ {
+ if (ext.Equals(".txt") || ext.Equals(".csv") || ext.Equals(".prn"))
+ {
+ if (AllowedChars.Length == 0)
+ {
+ // Limits characters to ASCII encoding.
+ for (var i = 0; i < data.Length; i++)
+ {
+ if (reader.ReadByte() > sbyte.MaxValue)
+ {
+ return false;
+ }
+ }
+ }
+ else
+ {
+ // Limits characters to ASCII encoding and
+ // values of the _allowedChars array.
+ for (var i = 0; i < data.Length; i++)
+ {
+ var b = reader.ReadByte();
+ if (b > sbyte.MaxValue ||
+ !AllowedChars.Contains(b))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // Uncomment the following code block if you must permit
+ // files whose signature isn't provided in the _fileSignature
+ // dictionary. We recommend that you add file signatures
+ // for files (when possible) for all file types you intend
+ // to allow on the system and perform the file signature
+ // check.
+ /*
+ if (!_fileSignature.ContainsKey(ext))
+ {
+ return true;
+ }
+ */
+
+ // File signature check
+ // --------------------
+ // With the file signatures provided in the _fileSignature
+ // dictionary, the following code tests the input content's
+ // file signature.
+ var signatures = FileSignature[ext];
+ var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
+
+ return signatures.Any(signature =>
+ headerBytes.Take(signature.Length).SequenceEqual(signature));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/WeatherForecast.cs b/MarkdownEditorDemo.Api/WeatherForecast.cs
new file mode 100644
index 0000000..b98e38f
--- /dev/null
+++ b/MarkdownEditorDemo.Api/WeatherForecast.cs
@@ -0,0 +1,13 @@
+namespace MarkdownEditorDemo.Api
+{
+ public class WeatherForecast
+ {
+ public DateTime Date { get; set; }
+
+ public int TemperatureC { get; set; }
+
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+
+ public string? Summary { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo.Api/appsettings.Development.json b/MarkdownEditorDemo.Api/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/MarkdownEditorDemo.Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/MarkdownEditorDemo.Api/appsettings.json b/MarkdownEditorDemo.Api/appsettings.json
new file mode 100644
index 0000000..f726d3b
--- /dev/null
+++ b/MarkdownEditorDemo.Api/appsettings.json
@@ -0,0 +1,11 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "StoredFilesPath": "C:\\Projects\\TmpImageMDE",
+ "FileSizeLimit": 2097152
+}
diff --git a/MarkdownEditorDemo/MarkdownEditorDemo.csproj b/MarkdownEditorDemo/MarkdownEditorDemo.csproj
index a6bac62..84eafd2 100644
--- a/MarkdownEditorDemo/MarkdownEditorDemo.csproj
+++ b/MarkdownEditorDemo/MarkdownEditorDemo.csproj
@@ -1,14 +1,14 @@
- net5.0
+ net6.0service-worker-assets.js
-
-
-
+
+
+
diff --git a/MarkdownEditorDemo/Models/FormData.cs b/MarkdownEditorDemo/Models/FormData.cs
index 1a1a8b5..0728e10 100644
--- a/MarkdownEditorDemo/Models/FormData.cs
+++ b/MarkdownEditorDemo/Models/FormData.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MarkdownEditorDemo.Models
+namespace MarkdownEditorDemo.Models
{
public class FormData
{
diff --git a/MarkdownEditorDemo/Pages/Index.razor b/MarkdownEditorDemo/Pages/Index.razor
index c248e32..561f5e2 100644
--- a/MarkdownEditorDemo/Pages/Index.razor
+++ b/MarkdownEditorDemo/Pages/Index.razor
@@ -1,14 +1,38 @@
@page "/"
-
-
-
+
+
-@code {
- FormData form = new FormData();
+
- private void DoSave()
+
Result
+ @((MarkupString)markdownHtml)
+
+
+@code {
+ string markdownValue = "# Markdown Editor for Blazor\nThis component is using [EasyMDE](https://easy-markdown-editor.tk/) " +
+ "to display a nice editor and all functionalities are mapped. See the documentation for more details.\n\n" +
+ "Go ahead, play around with the editor! Be sure to check out **bold**, *italic*, " +
+ "[links](https://google.com) and all the other features. " +
+ "You can type the Markdown syntax, use the toolbar, or use shortcuts like `ctrl-b` or `cmd-b`.";
+ string markdownHtml;
+
+ protected override void OnInitialized()
{
+ markdownHtml = Markdig.Markdown.ToHtml(markdownValue ?? string.Empty);
+ base.OnInitialized();
+ }
+ Task OnMarkdownValueChanged(string value)
+ {
+ return Task.CompletedTask;
+ }
+
+ Task OnMarkdownValueHTMLChanged(string value)
+ {
+ markdownHtml = value;
+ return Task.CompletedTask;
}
-}
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo/Pages/Upload.razor b/MarkdownEditorDemo/Pages/Upload.razor
new file mode 100644
index 0000000..1727746
--- /dev/null
+++ b/MarkdownEditorDemo/Pages/Upload.razor
@@ -0,0 +1,71 @@
+@page "/upload"
+@using PSC.Blazor.Components.MarkdownEditor.Models
+@using System.Net.Http.Headers
+
+
+
+
+
+
+
Result
+ @((MarkupString)markdownHtml)
+
+
+@code {
+ string markdownValue = "# Markdown Editor for Blazor\nThis component is using [EasyMDE](https://easy-markdown-editor.tk/) " +
+ "to display a nice editor and all functionalities are mapped. See the documentation for more details.\n\n" +
+ "Go ahead, play around with the editor! Be sure to check out **bold**, *italic*, " +
+ "[links](https://google.com) and all the other features. " +
+ "You can type the Markdown syntax, use the toolbar, or use shortcuts like `ctrl-b` or `cmd-b`.";
+ string markdownHtml;
+
+ protected override void OnInitialized()
+ {
+ markdownHtml = Markdig.Markdown.ToHtml(markdownValue ?? string.Empty);
+
+ base.OnInitialized();
+ }
+
+ Task OnMarkdownValueChanged(string value)
+ {
+ markdownValue = value;
+ return Task.CompletedTask;
+ }
+
+ async Task OnImageUploadChanged(FileChangedEventArgs e)
+ {
+ this.StateHasChanged();
+ }
+
+ Task OnImageUploadStarted(FileStartedEventArgs e)
+ {
+ Console.WriteLine($"Started Image: {e.File.Name}");
+ return Task.CompletedTask;
+ }
+
+ Task OnImageUploadProgressed(FileProgressedEventArgs e)
+ {
+ Console.WriteLine($"Image: {e.File.Name} Progress: {(int)e.Percentage}");
+ return Task.CompletedTask;
+ }
+
+ Task OnImageUploadEnded(FileEndedEventArgs e)
+ {
+ Console.WriteLine($"Finished Image: {e.File.Name}, Success: {e.Success}");
+ return Task.CompletedTask;
+ }
+
+ Task OnMarkdownValueHTMLChanged(string value)
+ {
+ markdownHtml = value;
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo/Program.cs b/MarkdownEditorDemo/Program.cs
index 53f6e4d..cd76d10 100644
--- a/MarkdownEditorDemo/Program.cs
+++ b/MarkdownEditorDemo/Program.cs
@@ -1,11 +1,7 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
using System;
-using System.Collections.Generic;
using System.Net.Http;
-using System.Text;
using System.Threading.Tasks;
namespace MarkdownEditorDemo
diff --git a/MarkdownEditorDemo/Shared/NavMenu.razor b/MarkdownEditorDemo/Shared/NavMenu.razor
index 6299848..298f4dc 100644
--- a/MarkdownEditorDemo/Shared/NavMenu.razor
+++ b/MarkdownEditorDemo/Shared/NavMenu.razor
@@ -12,6 +12,11 @@
Home
+
+
+ Upload
+
+
diff --git a/MarkdownEditorDemo/_Imports.razor b/MarkdownEditorDemo/_Imports.razor
index 149108d..23b6d8a 100644
--- a/MarkdownEditorDemo/_Imports.razor
+++ b/MarkdownEditorDemo/_Imports.razor
@@ -9,4 +9,5 @@
@using MarkdownEditorDemo
@using MarkdownEditorDemo.Shared
@using MarkdownEditorDemo.Models
-@using PSC.Blazor.Components.MarkdownEditor
\ No newline at end of file
+@using PSC.Blazor.Components.MarkdownEditor
+@using PSC.Blazor.Components.MarkdownEditor.EventsArgs
\ No newline at end of file
diff --git a/MarkdownEditorDemo/libman.json b/MarkdownEditorDemo/libman.json
index 9ff2cff..1d84ba3 100644
--- a/MarkdownEditorDemo/libman.json
+++ b/MarkdownEditorDemo/libman.json
@@ -5,6 +5,10 @@
{
"library": "font-awesome@5.15.4",
"destination": "wwwroot/lib/font-awesome/"
+ },
+ {
+ "library": "jquery@3.6.0",
+ "destination": "wwwroot/lib/query/"
}
]
}
\ No newline at end of file
diff --git a/MarkdownEditorDemo/wwwroot/css/app.css b/MarkdownEditorDemo/wwwroot/css/app.css
index 82fc22a..061e702 100644
--- a/MarkdownEditorDemo/wwwroot/css/app.css
+++ b/MarkdownEditorDemo/wwwroot/css/app.css
@@ -48,3 +48,16 @@ a, .btn-link {
right: 0.75rem;
top: 0.5rem;
}
+
+img {
+ max-width: 100%;
+}
+
+table {
+ margin-bottom: 15px;
+}
+
+table td, table th {
+ border: 1px solid #ddd;
+ padding: 5px;
+}
\ No newline at end of file
diff --git a/MarkdownEditorDemo/wwwroot/index.html b/MarkdownEditorDemo/wwwroot/index.html
index ccec010..3fbe3ba 100644
--- a/MarkdownEditorDemo/wwwroot/index.html
+++ b/MarkdownEditorDemo/wwwroot/index.html
@@ -12,6 +12,8 @@
+
+
@@ -24,6 +26,10 @@
+
+
+
+
-