RE 3
This commit is contained in:
@@ -1,146 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Api
|
||||
{
|
||||
public class Safenames
|
||||
{
|
||||
public enum Scope
|
||||
{
|
||||
Url,
|
||||
File
|
||||
}
|
||||
|
||||
const char HYPHEN = '-';
|
||||
|
||||
const char DOT = '.';
|
||||
|
||||
const char PLUS = '+';
|
||||
|
||||
const char AMPERSAND = '&';
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts an untrusted to a safe filename
|
||||
/// </summary>
|
||||
public static string File(string value)
|
||||
{
|
||||
return Generate(Path.GetFileName(value), Scope.File);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts a term to a safe alias (suitable for URLs)
|
||||
/// </summary>
|
||||
public static string Alias(string value)
|
||||
{
|
||||
return Generate(value, Scope.Url);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts a term to a safe alias (suitable for URLs)
|
||||
/// </summary>
|
||||
public static string Alias(object value)
|
||||
{
|
||||
return Generate(value?.ToString(), Scope.Url);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
static string Generate(string value, Scope scope)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
char previous = default;
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
// get character in lower case
|
||||
char character = char.ToLower(value[i]);
|
||||
char target;
|
||||
|
||||
// do not handle surrogates
|
||||
if (char.IsSurrogate(character))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// special replacements accents + umlauts
|
||||
if (character.TryReplaceAccent(out char[] replacement))
|
||||
{
|
||||
if (replacement.Length > 1)
|
||||
{
|
||||
output.Append(replacement);
|
||||
output.Remove(output.Length - 1, 1);
|
||||
}
|
||||
target = replacement[replacement.Length - 1];
|
||||
}
|
||||
// append character a-z, 0-9
|
||||
else if (character.IsAZor09())
|
||||
{
|
||||
target = character;
|
||||
}
|
||||
// + sign for + and &
|
||||
else if (character == PLUS || character == AMPERSAND)
|
||||
{
|
||||
target = PLUS;
|
||||
}
|
||||
else if (scope == Scope.File && character == DOT)
|
||||
{
|
||||
target = DOT;
|
||||
}
|
||||
// add hyphen for all other characters
|
||||
else
|
||||
{
|
||||
target = HYPHEN;
|
||||
}
|
||||
|
||||
// add default characters
|
||||
if (target != HYPHEN && target != PLUS)
|
||||
{
|
||||
output.Append(target);
|
||||
}
|
||||
// add hyphen if it isn't first and previous char is not + or -
|
||||
else if (target == HYPHEN && previous != default && previous != PLUS && previous != HYPHEN)
|
||||
{
|
||||
output.Append(target);
|
||||
}
|
||||
// add plus. do remove hyphen it is the previous character
|
||||
else if (target == PLUS)
|
||||
{
|
||||
if (previous == HYPHEN)
|
||||
{
|
||||
output.Remove(output.Length - 1, 1);
|
||||
}
|
||||
output.Append(target);
|
||||
}
|
||||
|
||||
if (output.Length > 0)
|
||||
{
|
||||
previous = output[output.Length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (output.Length > 0 && !output[output.Length - 1].IsAZor09())
|
||||
{
|
||||
output.Remove(output.Length - 1, 1);
|
||||
}
|
||||
|
||||
if (output.Length == 0)
|
||||
{
|
||||
output.Append(HYPHEN);
|
||||
}
|
||||
|
||||
return output.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
//using Raven.Client.Documents;
|
||||
//using Raven.Client.Documents.Session;
|
||||
//using System;
|
||||
//using System.Linq;
|
||||
//using System.Threading.Tasks;
|
||||
//using zero.Core.Database;
|
||||
//using zero.Core.Entities;
|
||||
//using zero.Core.Extensions;
|
||||
//using zero.Core.Options;
|
||||
|
||||
//namespace zero.Core.Api
|
||||
//{
|
||||
// public class Token : IToken
|
||||
// {
|
||||
// protected IZeroStore Store { get; private set; }
|
||||
|
||||
// protected IZeroOptions Options { get; private set; }
|
||||
|
||||
// private const string PREFIX = "changeTokens";
|
||||
|
||||
|
||||
// public Token(IZeroStore store, IZeroOptions options)
|
||||
// {
|
||||
// Store = store;
|
||||
// Options = options;
|
||||
// }
|
||||
|
||||
|
||||
// /// <inheritdoc />
|
||||
// public bool Verify(ZeroIdEntity entity, string token)
|
||||
// {
|
||||
// return Verify(entity?.Id, token);
|
||||
// }
|
||||
|
||||
|
||||
// /// <inheritdoc />
|
||||
// public bool Verify(string entityId, string token)
|
||||
// {
|
||||
// if (token.IsNullOrWhiteSpace() && entityId.IsNullOrEmpty())
|
||||
// {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// if (token.IsNullOrWhiteSpace() || entityId.IsNullOrEmpty())
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// using (IDocumentSession session = Store.OpenSession())
|
||||
// {
|
||||
// return session.Query<ChangeToken>().Any(x => x.Id == token && x.ReferenceId == entityId);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// /// <inheritdoc />
|
||||
// public string Get(ZeroIdEntity entity)
|
||||
// {
|
||||
// return Get(entity?.Id);
|
||||
// }
|
||||
|
||||
|
||||
// /// <inheritdoc />
|
||||
// public string Get(string entityId)
|
||||
// {
|
||||
// if (entityId.IsNullOrEmpty())
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// ChangeToken token = new ChangeToken()
|
||||
// {
|
||||
// Id = Options.Raven.CollectionPrefix.EnsureEndsWith(Store.Raven.Conventions.IdentityPartsSeparator) + PREFIX.EnsureEndsWith(Store.Raven.Conventions.IdentityPartsSeparator) + Guid.NewGuid(),
|
||||
// ReferenceId = entityId
|
||||
// };
|
||||
|
||||
// using (IDocumentSession session = Store.OpenSession())
|
||||
// {
|
||||
// session.Store(token);
|
||||
// session.Advanced.GetMetadataFor(token)[Constants.Database.Expires] = DateTime.UtcNow.AddMinutes(Options.TokenExpiration);
|
||||
// session.SaveChanges();
|
||||
// }
|
||||
|
||||
// return token.Id;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// public interface IToken
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// Verifies if the change token is valid for the entity
|
||||
// /// </summary>
|
||||
// bool Verify(ZeroIdEntity entity, string token);
|
||||
|
||||
// /// <summary>
|
||||
// /// Verifies if the change token is valid for the entity
|
||||
// /// </summary>
|
||||
// bool Verify(string entityId, string token);
|
||||
|
||||
// /// <summary>
|
||||
// /// Get a new change token for the entity
|
||||
// /// </summary>
|
||||
// string Get(ZeroIdEntity entity);
|
||||
|
||||
// /// <summary>
|
||||
// /// Get a new change token for the entity
|
||||
// /// </summary>
|
||||
// string Get(string entityId);
|
||||
// }
|
||||
//}
|
||||
@@ -1,240 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Handlers;
|
||||
using zero.Core.Identity;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero.Core
|
||||
{
|
||||
public class ApplicationResolver : IApplicationResolver
|
||||
{
|
||||
protected IZeroStore Store { get; private set; }
|
||||
|
||||
protected IZeroOptions Options { get; private set; }
|
||||
|
||||
protected ILogger<ApplicationResolver> Logger { get; private set; }
|
||||
|
||||
protected IHandlerHolder Handler { get; private set; }
|
||||
|
||||
private IList<Application> Apps { get; set; }
|
||||
|
||||
|
||||
|
||||
public ApplicationResolver(IZeroStore store, IZeroOptions options, ILogger<ApplicationResolver> logger, IHandlerHolder handler = null)
|
||||
{
|
||||
Store = store;
|
||||
Options = options;
|
||||
Logger = logger;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Application> Resolve(HttpContext context, ClaimsPrincipal user)
|
||||
{
|
||||
if (context?.Request == null)
|
||||
{
|
||||
Logger.LogWarning("Could not resolve application as HTTP request is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
Application app;
|
||||
|
||||
if (context.IsBackofficeRequest(Options.BackofficePath))
|
||||
{
|
||||
app = await ResolveFromUser(user);
|
||||
}
|
||||
else
|
||||
{
|
||||
app = await ResolveFromRequest(context);
|
||||
}
|
||||
|
||||
if (app == null)
|
||||
{
|
||||
//Logger.LogWarning("Could not resolve application for host {host}", context.Request.Host);
|
||||
IList<Application> apps = await GetApplications();
|
||||
app = apps.FirstOrDefault();
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Application> ResolveFromUser(ClaimsPrincipal user)
|
||||
{
|
||||
BackofficeUser userEntity = await GetBackofficeUser(user);
|
||||
return await ResolveFromUser(userEntity);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Application> ResolveFromUser(BackofficeUser user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string appId;
|
||||
string[] allowedAppIds = user.GetAllowedAppIds();
|
||||
|
||||
if (!user.CurrentAppId.IsNullOrEmpty())
|
||||
{
|
||||
if (user.IsSuper || allowedAppIds.Contains(user.CurrentAppId, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
appId = user.CurrentAppId;
|
||||
}
|
||||
else
|
||||
{
|
||||
appId = user.AppId;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
appId = user.AppId;
|
||||
}
|
||||
|
||||
if (appId.IsNullOrEmpty())
|
||||
{
|
||||
throw new Exception($"User entity ${user.Id} needs a valid AppId");
|
||||
}
|
||||
|
||||
IAsyncDocumentSession session = Store.Session(global: true);
|
||||
return await session.LoadAsync<Application>(appId);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Application> ResolveFromRequest(HttpContext context)
|
||||
{
|
||||
IApplicationResolverHandler handler = Handler.Get<IApplicationResolverHandler>();
|
||||
Application app = handler?.Resolve(context.Request, await GetApplications());
|
||||
|
||||
if (app == null)
|
||||
{
|
||||
app = await ResolveFromUri(context.Request.GetEncodedUrl());
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Application> ResolveFromUri(string uriString)
|
||||
{
|
||||
return ResolveFromUriInternal(new Uri(uriString, UriKind.Absolute), await GetApplications());
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Application> ResolveFromUri(Uri uri)
|
||||
{
|
||||
return ResolveFromUriInternal(uri, await GetApplications());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get matching application from an URI
|
||||
/// </summary>
|
||||
Application ResolveFromUriInternal(Uri uri, IList<Application> apps)
|
||||
{
|
||||
foreach (Application app in apps)
|
||||
{
|
||||
if (app.Domains?.Length < 1)
|
||||
{
|
||||
Logger.LogWarning("No domains specified for app {app}", app.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Uri domain in app.Domains)
|
||||
{
|
||||
int compareResult = Uri.Compare(uri, domain, UriComponents.HostAndPort, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase);
|
||||
if (compareResult == 0)
|
||||
{
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get all applications to choose from
|
||||
/// </summary>
|
||||
async Task<IList<Application>> GetApplications()
|
||||
{
|
||||
if (Apps != null)
|
||||
{
|
||||
return Apps;
|
||||
}
|
||||
|
||||
IAsyncDocumentSession session = Store.Session(global: true);
|
||||
Apps = await session.Query<Application>().ToListAsync();
|
||||
return Apps;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get backoffice user from claims principal
|
||||
/// </summary>
|
||||
async Task<BackofficeUser> GetBackofficeUser(ClaimsPrincipal user)
|
||||
{
|
||||
string userId = user.FindFirstValue(Constants.Auth.Claims.UserId);
|
||||
|
||||
IAsyncDocumentSession session = Store.Session(global: true);
|
||||
return await session.LoadAsync<BackofficeUser>(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IApplicationResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the current application from either the backoffice user (in case it is backoffice request)
|
||||
/// or the domain (in case it is frontend request).
|
||||
/// </summary>
|
||||
Task<Application> Resolve(HttpContext context, ClaimsPrincipal user);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the current application from the request path
|
||||
/// </summary>
|
||||
Task<Application> ResolveFromRequest(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Get matching application from an URI string
|
||||
/// </summary>
|
||||
Task<Application> ResolveFromUri(string uriString);
|
||||
|
||||
/// <summary>
|
||||
/// Get matching application from an URI
|
||||
/// </summary>
|
||||
Task<Application> ResolveFromUri(Uri uri);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the current application from the logged-in backoffice user.
|
||||
/// This method won't return apps the user has no access to.
|
||||
/// </summary>
|
||||
Task<Application> ResolveFromUser(ClaimsPrincipal user);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the current application from a user.
|
||||
/// This method won't return apps the user has no access to.
|
||||
/// </summary>
|
||||
Task<Application> ResolveFromUser(BackofficeUser user);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace zero.Core.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically generate ID with the specified length and insert it into this property on entity save
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class GenerateIdAttribute : Attribute
|
||||
{
|
||||
public int? Length = null;
|
||||
|
||||
public GenerateIdAttribute(int length)
|
||||
{
|
||||
Length = length;
|
||||
}
|
||||
|
||||
public GenerateIdAttribute() { }
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace zero.Core.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, AllowMultiple = false)]
|
||||
public class LocalizeAttribute : Attribute
|
||||
{
|
||||
public string Key;
|
||||
|
||||
public LocalizeAttribute(string key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace zero.Core.Attributes
|
||||
{
|
||||
public class OperationCancelledExceptionFilterAttribute : ExceptionFilterAttribute
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
||||
public OperationCancelledExceptionFilterAttribute(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<OperationCancelledExceptionFilterAttribute>();
|
||||
}
|
||||
|
||||
|
||||
public override void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Request was cancelled");
|
||||
context.ExceptionHandled = true;
|
||||
context.Result = new StatusCodeResult(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Utils;
|
||||
|
||||
namespace zero.Core.Blueprints
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class Blueprint<T> : Blueprint where T : ZeroEntity, new()
|
||||
{
|
||||
public List<BlueprintField<T>> UnlockedFields { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// These fields are not copied by the ObjectCloner
|
||||
/// as they are either locked or copied manually by ApplyDefaults()
|
||||
/// </summary>
|
||||
protected static string[] DefaultUncopyFields { get; set; } = new[] { "id", "url", "name", "alias", "key", "sort", "isActive", "hash", "languageId", "createdById", "createdDate", "lastModifiedById", "lastModifiedDate", "blueprint" };
|
||||
|
||||
|
||||
public Blueprint() : base(typeof(T)) { }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Merges blueprint data into a model based on the configuration
|
||||
/// </summary>
|
||||
public virtual T Apply(T blueprint, T model)
|
||||
{
|
||||
// reset blueprint options for model in case no blueprint was found
|
||||
if (blueprint == null || model.Blueprint == null)
|
||||
{
|
||||
model.Blueprint = null;
|
||||
return model;
|
||||
}
|
||||
|
||||
// copy all properties which are synced from the blueprint to the model
|
||||
ApplyDefaults(blueprint, model);
|
||||
ObjectCloner.CopyProperties(blueprint, model, DefaultUncopyFields.Union(model.Blueprint.Desync).ToArray());
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model) => Apply(blueprint as T, model as T);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Update default entity data
|
||||
/// </summary>
|
||||
protected virtual void ApplyDefaults(T blueprint, T model)
|
||||
{
|
||||
if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.Name), StringComparer))
|
||||
{
|
||||
model.Name = blueprint.Name;
|
||||
model.Alias = blueprint.Alias;
|
||||
}
|
||||
if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.Key), StringComparer))
|
||||
{
|
||||
model.Key = blueprint.Key;
|
||||
}
|
||||
if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.Sort), StringComparer))
|
||||
{
|
||||
model.Sort = blueprint.Sort;
|
||||
}
|
||||
if (!model.Blueprint.Desync.Contains(nameof(ZeroEntity.IsActive), StringComparer))
|
||||
{
|
||||
model.IsActive = blueprint.IsActive;
|
||||
}
|
||||
|
||||
// these values can't be overridden
|
||||
model.Hash = blueprint.Hash;
|
||||
model.LanguageId = blueprint.LanguageId;
|
||||
model.CreatedById = blueprint.CreatedById;
|
||||
model.CreatedDate = blueprint.CreatedDate;
|
||||
model.LastModifiedById = blueprint.LastModifiedById;
|
||||
model.LastModifiedDate = blueprint.LastModifiedDate;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Lock a field so it always get synchronized no and cannot be changed
|
||||
/// </summary>
|
||||
public void LockAll()
|
||||
{
|
||||
UnlockedFields = new();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Lock a field so it always get synchronized no and cannot be changed
|
||||
/// </summary>
|
||||
public void Lock(Expression<Func<T, object>> selector)
|
||||
{
|
||||
RemoveField(selector);
|
||||
}
|
||||
|
||||
|
||||
public void Unlock(Expression<Func<T, object>> selector)
|
||||
{
|
||||
AddField(selector);
|
||||
}
|
||||
|
||||
|
||||
public void UnlockDefaults()
|
||||
{
|
||||
AddField(x => x.Name);
|
||||
AddField(x => x.Alias);
|
||||
AddField(x => x.Sort);
|
||||
AddField(x => x.Key);
|
||||
AddField(x => x.IsActive);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<string> GetUnlockedFieldNames()
|
||||
{
|
||||
foreach (BlueprintField<T> field in UnlockedFields)
|
||||
{
|
||||
yield return field.FieldName.ToCamelCaseId();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get existing field or create a new one
|
||||
/// </summary>
|
||||
protected BlueprintField<T> AddField(Expression<Func<T, object>> selector)
|
||||
{
|
||||
BlueprintField<T> tempField = new(selector);
|
||||
BlueprintField<T> storedField = UnlockedFields.FirstOrDefault(x => x.FieldName.Equals(tempField.FieldName, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (storedField != null)
|
||||
{
|
||||
return storedField;
|
||||
}
|
||||
|
||||
UnlockedFields.Add(tempField);
|
||||
return tempField;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Removes a field
|
||||
/// </summary>
|
||||
protected void RemoveField(Expression<Func<T, object>> selector)
|
||||
{
|
||||
BlueprintField<T> tempField = new(selector);
|
||||
|
||||
if (tempField != null)
|
||||
{
|
||||
UnlockedFields.RemoveAll(x => x.FieldName.Equals(tempField.FieldName, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public abstract class Blueprint
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the associated entity
|
||||
/// </summary>
|
||||
public Type ContentType { get; private set; }
|
||||
|
||||
public string Alias { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// String comparer for property name comparison
|
||||
/// </summary>
|
||||
protected readonly StringComparer StringComparer = StringComparer.InvariantCultureIgnoreCase;
|
||||
|
||||
|
||||
public Blueprint(Type type)
|
||||
{
|
||||
ContentType = type;
|
||||
Alias = type.Name.ToCamelCaseId();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges blueprint data into a model based on the configuration
|
||||
/// </summary>
|
||||
public abstract ZeroEntity Apply(ZeroEntity blueprint, ZeroEntity model);
|
||||
|
||||
/// <summary>
|
||||
/// Get all fields which have been unlocked
|
||||
/// </summary>
|
||||
public abstract IEnumerable<string> GetUnlockedFieldNames();
|
||||
}
|
||||
|
||||
|
||||
public class DefaultBlueprint<T> : Blueprint<T> where T : ZeroEntity, new()
|
||||
{
|
||||
public DefaultBlueprint(Action<Blueprint<T>> expression = null) : base()
|
||||
{
|
||||
expression?.Invoke(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Collections;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Blueprints
|
||||
{
|
||||
public class BlueprintChildInterceptor : CollectionInterceptor
|
||||
{
|
||||
protected IZeroContext Context { get; set; }
|
||||
|
||||
protected IZeroStore Store { get; set; }
|
||||
|
||||
protected ILogger<BlueprintChildInterceptor> Logger { get; set; }
|
||||
|
||||
protected IBlueprintService BlueprintService { get; set; }
|
||||
|
||||
|
||||
public BlueprintChildInterceptor(IZeroContext context, IZeroStore store, ILogger<BlueprintChildInterceptor> logger, IBlueprintService blueprintService)
|
||||
{
|
||||
Context = context;
|
||||
Store = store;
|
||||
Logger = logger;
|
||||
BlueprintService = blueprintService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(InterceptorParameters args, ZeroIdEntity model)
|
||||
{
|
||||
// this interceptor does only work for child entities
|
||||
return args.Context.Store.ResolvedDatabase != Context.Options.Raven.Database;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<InterceptorResult<ZeroIdEntity>> Deleting(InterceptorParameters args, ZeroIdEntity model)
|
||||
{
|
||||
if (model is not ZeroEntity || (model as ZeroEntity).Blueprint != null)
|
||||
{
|
||||
InterceptorResult<ZeroIdEntity> result = new();
|
||||
result.Result = EntityResult<ZeroIdEntity>.Fail("@blueprint.errors.cannotDeleteChild");
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
return base.Deleting(args, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Blueprints
|
||||
{
|
||||
public class BlueprintField<T> where T : ZeroEntity
|
||||
{
|
||||
public Expression<Func<T, object>> Expression { get; private set; }
|
||||
|
||||
public PropertyInfo PropertyInfo { get; private set; }
|
||||
|
||||
public string FieldName { get; private set; }
|
||||
|
||||
Action<T, T> _applyHook { get; set; }
|
||||
|
||||
Func<T, object> _selectorFunc = null;
|
||||
|
||||
|
||||
internal BlueprintField(Expression<Func<T, object>> selector)
|
||||
{
|
||||
Expression = selector;
|
||||
FieldName = selector.GetPropertyPath(out MemberExpression member);
|
||||
PropertyInfo = member?.Member as PropertyInfo;
|
||||
|
||||
if (PropertyInfo == null)
|
||||
{
|
||||
throw new ArgumentException("selector parameter has to be a property selector");
|
||||
}
|
||||
|
||||
if (FieldName.IsNullOrEmpty())
|
||||
{
|
||||
throw new Exception("Could not find a matching field name");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public virtual void Apply(T blueprint, T model)
|
||||
{
|
||||
if (_applyHook != null)
|
||||
{
|
||||
_applyHook(blueprint, model);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectorFunc ??= Expression.Compile();
|
||||
PropertyInfo.SetValue(model, _selectorFunc(blueprint));
|
||||
}
|
||||
|
||||
|
||||
public virtual void OnUpdate(Action<T, T> onUpdate)
|
||||
{
|
||||
_applyHook = onUpdate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Collections;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Blueprints
|
||||
{
|
||||
public class BlueprintInterceptor : CollectionInterceptor
|
||||
{
|
||||
protected IZeroContext Context { get; set; }
|
||||
|
||||
protected IZeroStore Store { get; set; }
|
||||
|
||||
protected ILogger<BlueprintInterceptor> Logger { get; set; }
|
||||
|
||||
protected IBlueprintService BlueprintService { get; set; }
|
||||
|
||||
IList<Application> Apps { get; set; }
|
||||
|
||||
|
||||
public BlueprintInterceptor(IZeroContext context, IZeroStore store, ILogger<BlueprintInterceptor> logger, IBlueprintService blueprintService)
|
||||
{
|
||||
Context = context;
|
||||
Store = store;
|
||||
Logger = logger;
|
||||
BlueprintService = blueprintService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(InterceptorParameters args, ZeroIdEntity model)
|
||||
{
|
||||
// we do only update children if we operate on the shared database
|
||||
return args.Context.Store.ResolvedDatabase == Context.Options.Raven.Database;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Saved(InterceptorParameters args, ZeroIdEntity model)
|
||||
{
|
||||
if (model is not ZeroEntity || !BlueprintService.TryGetBlueprint(model, out Blueprint blueprint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ZeroEntity entity = model as ZeroEntity;
|
||||
int count = 0;
|
||||
|
||||
foreach (Application app in await GetApplications())
|
||||
{
|
||||
using ZeroContextScope scope = Context.CreateScope(app);
|
||||
IZeroDocumentSession session = scope.Store.Session(scope.Database);
|
||||
|
||||
ZeroEntity child = await session.LoadAsync<ZeroEntity>(model.Id);
|
||||
|
||||
if (child == null)
|
||||
{
|
||||
child = entity.Clone();
|
||||
child.Blueprint = new() { Id = model.Id };
|
||||
}
|
||||
else
|
||||
{
|
||||
blueprint.Apply(entity, child);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
// now we have to store the child
|
||||
// but this will not work with the session as we need to run the scoped interceptors,
|
||||
// therefore we need access to the collection
|
||||
|
||||
await session.StoreAsync(child);
|
||||
await session.SaveChangesAsync();
|
||||
}
|
||||
|
||||
Logger.LogDebug("Blueprint: Synced {count} children for {name} ({id})", count, entity.Name, model.Id);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Deleted(InterceptorParameters args, ZeroIdEntity model)
|
||||
{
|
||||
if (model is not ZeroEntity || !BlueprintService.TryGetBlueprint(model, out Blueprint blueprint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ZeroEntity entity = model as ZeroEntity;
|
||||
int count = 0;
|
||||
|
||||
foreach (Application app in await GetApplications())
|
||||
{
|
||||
using ZeroContextScope scope = Context.CreateScope(app);
|
||||
IZeroDocumentSession session = scope.Store.Session(scope.Database);
|
||||
|
||||
count += 1;
|
||||
|
||||
// now we have to delete the child
|
||||
// but this will not work with the session as we need to run the scoped interceptors,
|
||||
// therefore we need access to the collection
|
||||
session.Delete(entity.Id);
|
||||
await session.SaveChangesAsync();
|
||||
}
|
||||
|
||||
Logger.LogDebug("Blueprint: Deleted {count} children for {name} ({id})", count, entity.Name, entity.Id);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get all applications to choose from
|
||||
/// </summary>
|
||||
async Task<IList<Application>> GetApplications()
|
||||
{
|
||||
if (Apps != null)
|
||||
{
|
||||
return Apps;
|
||||
}
|
||||
|
||||
IAsyncDocumentSession session = Store.Session(global: true);
|
||||
Apps = await session.Query<Application>().ToListAsync();
|
||||
return Apps;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
//public override async Task Saved(SaveParameters args)
|
||||
//{
|
||||
|
||||
// //Logger.LogInformation("Route updates completed (+{added}/~{updated}/-{removed}) for {model} (id: {id})", countRoutes - countUpdatedRoutes, countUpdatedRoutes, obsoleteRoutes.Count, args.Model.Name, args.Model.Id);
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero.Core.Blueprints
|
||||
{
|
||||
public class BlueprintService : IBlueprintService
|
||||
{
|
||||
protected IZeroOptions Options { get; set; }
|
||||
|
||||
protected IReadOnlyCollection<Blueprint> Blueprints { get; set; }
|
||||
|
||||
|
||||
public BlueprintService(IZeroOptions options)
|
||||
{
|
||||
Options = options;
|
||||
Blueprints = Options.Blueprints.GetAllItems();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled<T>(T model) => IsEnabled(model.GetType());
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled<T>()=> IsEnabled(typeof(T));
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled(Type type) => Blueprints.Any(x => x.ContentType.IsAssignableFrom(type));
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetBlueprint<T>(T model, out Blueprint blueprint) => TryGetBlueprint(model.GetType(), out blueprint);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetBlueprint<T>(out Blueprint blueprint) => TryGetBlueprint(typeof(T), out blueprint);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetBlueprint(Type type, out Blueprint blueprint)
|
||||
{
|
||||
blueprint = Blueprints.FirstOrDefault(x => x.ContentType.IsAssignableFrom(type));
|
||||
return blueprint != null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IBlueprintService
|
||||
{
|
||||
/// <summary>
|
||||
/// Check whether blueprinting functionality is enabled for a certain entity
|
||||
/// </summary>
|
||||
bool IsEnabled<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Check whether blueprinting functionality is enabled for a certain entity
|
||||
/// </summary>
|
||||
bool IsEnabled<T>(T model);
|
||||
|
||||
bool IsEnabled(Type type);
|
||||
|
||||
bool TryGetBlueprint<T>(T model, out Blueprint blueprint);
|
||||
|
||||
bool TryGetBlueprint<T>(out Blueprint blueprint);
|
||||
|
||||
bool TryGetBlueprint(Type type, out Blueprint blueprint);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Collections
|
||||
{
|
||||
public abstract partial class CollectionInterceptor<T> : ICollectionInterceptor<T> where T : ZeroIdEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual bool CanRun(InterceptorParameters args, T model) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<T>> Creating(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<T>> Updating(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<T>> Saving(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task<InterceptorResult<T>> Deleting(InterceptorParameters args, T model) => Task.FromResult<InterceptorResult<T>>(default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Created(InterceptorParameters args, T model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Updated(InterceptorParameters args, T model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Saved(InterceptorParameters args, T model) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Task Deleted(InterceptorParameters args, T model) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
public abstract partial class CollectionInterceptor : CollectionInterceptor<ZeroIdEntity>, ICollectionInterceptor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(InterceptorParameters args, ZeroIdEntity model) => base.CanRun(args, model);
|
||||
}
|
||||
|
||||
public interface ICollectionInterceptor : ICollectionInterceptor<ZeroIdEntity> { }
|
||||
|
||||
public interface ICollectionInterceptor<T> where T : ZeroIdEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether any of the interceptor methods is allowed to run based on the parameters
|
||||
/// </summary>
|
||||
bool CanRun(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been stored but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Created(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is stored and validated
|
||||
/// </summary>
|
||||
Task<InterceptorResult<T>> Creating(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been deleted but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Deleted(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is deleted
|
||||
/// </summary>
|
||||
Task<InterceptorResult<T>> Deleting(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been updated but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Updated(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is stored and validated
|
||||
/// </summary>
|
||||
Task<InterceptorResult<T>> Updating(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called after an entity has been saved (created or updated) but before the session has saved its changes
|
||||
/// </summary>
|
||||
Task Saved(InterceptorParameters args, T model);
|
||||
|
||||
/// <summary>
|
||||
/// Called before an entity is stored and validated
|
||||
/// </summary>
|
||||
Task<InterceptorResult<T>> Saving(InterceptorParameters args, T model);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Collections
|
||||
{
|
||||
public sealed class CollectionInterceptorShim<T> : CollectionInterceptor<T> where T : ZeroIdEntity
|
||||
{
|
||||
ICollectionInterceptor _base;
|
||||
|
||||
public CollectionInterceptorShim(ICollectionInterceptor baseInterceptor)
|
||||
{
|
||||
_base = baseInterceptor;
|
||||
}
|
||||
|
||||
InterceptorResult<T> Result(InterceptorResult<ZeroIdEntity> result)
|
||||
{
|
||||
return result == null ? null : new InterceptorResult<T>()
|
||||
{
|
||||
InterceptorHash = result.InterceptorHash,
|
||||
Parameters = result.Parameters,
|
||||
Prevent = result.Prevent,
|
||||
Result = result.Result != null ? EntityResult<T>.From(result.Result, result.Result.Model as T) : null
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(InterceptorParameters args, T model) => _base.CanRun(args, model);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<InterceptorResult<T>> Creating(InterceptorParameters args, T model) => Result(await _base.Creating(args, model));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<InterceptorResult<T>> Updating(InterceptorParameters args, T model) => Result(await _base.Updating(args, model));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<InterceptorResult<T>> Saving(InterceptorParameters args, T model) => Result(await _base.Saving(args, model));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<InterceptorResult<T>> Deleting(InterceptorParameters args, T model) => Result(await _base.Deleting(args, model));
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Created(InterceptorParameters args, T model) => _base.Created(args, model);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Updated(InterceptorParameters args, T model) => _base.Updated(args, model);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Saved(InterceptorParameters args, T model) => _base.Saved(args, model);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task Deleted(InterceptorParameters args, T model) => _base.Deleted(args, model);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Collections
|
||||
{
|
||||
public class InterceptorResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Autoset. Hash used to match interceptors in order to correctly pass parameters
|
||||
/// </summary>
|
||||
internal string InterceptorHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prevent further interceptors from running for this operation (only valid for the current interception type, e.g. Update/Created/Purge/...)
|
||||
/// </summary>
|
||||
public bool Prevent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a result. This will prevent further execution of the operation and cancels all subsequent interceptors
|
||||
/// </summary>
|
||||
public EntityResult<T> Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional parameters which can be passed to the interceptor after the operation was completed
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Parameters { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
namespace zero.Core
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const string ErrorFieldNone = "__zero_no_field";
|
||||
|
||||
public static class Tabs
|
||||
{
|
||||
public const string General = "general";
|
||||
}
|
||||
|
||||
public static class Auth
|
||||
{
|
||||
public const string SystemUser = "system";
|
||||
public const string DefaultScheme = "zeroScheme";
|
||||
public const string BackofficeDisplayName = "Zero Bacckoffice";
|
||||
public const string BackofficeScheme = "zeroBackoffice";
|
||||
public const string BackofficeCookieName = "zero.be.session";
|
||||
public const string DefaultCookieName = "zero.session";
|
||||
|
||||
public static class Claims
|
||||
{
|
||||
public const string IsZero = "zero.claim.iszero";
|
||||
public const string IsSuper = "zero.claim.issuper";
|
||||
public const string UserId = "zero.claim.userid";
|
||||
public const string UserName = "zero.claim.username";
|
||||
public const string Role = "zero.claim.rolealias";
|
||||
public const string SecurityStamp = "zero.claim.securitystamp";
|
||||
public const string Email = "zero.claim.email";
|
||||
public const string Permission = "zero.claim.permission";
|
||||
public const string DefaultAppId = "zero.claim.defaultAppId";
|
||||
public const string AppId = "zero.claim.appId";
|
||||
public const string TicketExpires = "zero.claim.ticketExpires";
|
||||
}
|
||||
}
|
||||
|
||||
public static class Database
|
||||
{
|
||||
public const string ReservationPrefix = "zero.";
|
||||
public const string CoreIdPrefix = "core.";
|
||||
public const string Expires = Raven.Client.Constants.Documents.Metadata.Expires;
|
||||
}
|
||||
|
||||
public static class Sections
|
||||
{
|
||||
public const string Dashboard = "dashboard";
|
||||
public const string Pages = "pages";
|
||||
public const string Spaces = "spaces";
|
||||
public const string Media = "media";
|
||||
public const string Settings = "settings";
|
||||
}
|
||||
|
||||
public static class Settings
|
||||
{
|
||||
public const string Updates = "updates";
|
||||
public const string Applications = "applications";
|
||||
public const string Users = "users";
|
||||
public const string Languages = "languages";
|
||||
public const string Translations = "translations";
|
||||
public const string Countries = "countries";
|
||||
public const string Mails = "mailTemplates";
|
||||
public const string Integrations = "integrations";
|
||||
public const string Logging = "logs";
|
||||
public const string Plugins = "plugins";
|
||||
public const string CreatePlugin = "createplugin";
|
||||
}
|
||||
|
||||
public static class PermissionCollections
|
||||
{
|
||||
public const string Sections = "permissionCollectionSections";
|
||||
public const string Spaces = "permissionCollectionSpaces";
|
||||
public const string Settings = "permisssionCollectionSettings";
|
||||
public const string Modules = "permissionCollectionModules";
|
||||
}
|
||||
|
||||
public static class Pages
|
||||
{
|
||||
public const string FolderAlias = "zero.folder";
|
||||
public const string DefaultRootPageTypeAlias = "root";
|
||||
public const string PageRouteProviderAlias = "zero.pages";
|
||||
}
|
||||
|
||||
public static class Routing
|
||||
{
|
||||
public const string InternalRoutePrefix = "/__zero/";
|
||||
|
||||
public const string ErrorRoute = InternalRoutePrefix + "error";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Cultures
|
||||
{
|
||||
public class CultureResolver : ICultureResolver
|
||||
{
|
||||
protected ILogger<CultureResolver> Logger { get; private set; }
|
||||
|
||||
|
||||
public CultureResolver(ILogger<CultureResolver> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CultureInfo> Resolve(IZeroContext context)
|
||||
{
|
||||
// TODO this is just fake, we need to correctly resolve culture here
|
||||
|
||||
if (context.IsBackofficeRequest)
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
var session = context.Store.Session();
|
||||
Language language = await session.Query<Language>().FirstOrDefaultAsync();
|
||||
|
||||
if (language == null)
|
||||
{
|
||||
Logger.LogWarning("Could not set request culture as there is no available Language stored");
|
||||
return CultureInfo.CurrentCulture;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CultureInfo culture = CultureInfo.CreateSpecificCulture(language.Code);
|
||||
|
||||
if (culture.ThreeLetterISOLanguageName.IsNullOrEmpty())
|
||||
{
|
||||
throw new Exception("ThreeLetterISOLanguageName is empty");
|
||||
}
|
||||
|
||||
CultureInfo.CurrentCulture = culture;
|
||||
CultureInfo.CurrentUICulture = culture;
|
||||
ValidatorOptions.Global.LanguageManager.Culture = culture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not create culture from Language code {code}", language.Code);
|
||||
return CultureInfo.CurrentCulture;
|
||||
}
|
||||
}
|
||||
|
||||
return CultureInfo.CurrentCulture;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface ICultureResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the current application from either the backoffice user (in case it is backoffice request)
|
||||
/// or the domain (in case it is frontend request).
|
||||
/// </summary>
|
||||
Task<CultureInfo> Resolve(IZeroContext context);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class zero_Countries : ZeroIndex<Country>
|
||||
{
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items.Select(item => new
|
||||
{
|
||||
Name = item.Name,
|
||||
IsPreferred = item.IsPreferred
|
||||
});
|
||||
|
||||
Index(x => x.Name, FieldIndexing.Search);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class MediaFolder_ByHierarchy : ZeroIndex<MediaFolder, MediaFolder_ByHierarchy.Result>
|
||||
{
|
||||
public class Result : ZeroIdEntity, IZeroDbConventions
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<PathResult> Path { get; set; } = new List<PathResult>();
|
||||
}
|
||||
|
||||
|
||||
public class PathResult
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items.Select(item => new Result
|
||||
{
|
||||
Id = item.Id,
|
||||
Name = item.Name,
|
||||
Path = Recurse(item, x => LoadDocument<MediaFolder>(x.ParentId))
|
||||
.Where(x => x != null && x.Id != null && x.Id != item.Id)
|
||||
.Reverse()
|
||||
.Select(current => new PathResult()
|
||||
{
|
||||
Id = current.Id,
|
||||
Name = current.Name
|
||||
})
|
||||
.ToList()
|
||||
});
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
//Index(x => x.ChannelId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class MediaFolders_WithChildren : ZeroIndex<MediaFolder, MediaFolders_WithChildren.Result>
|
||||
{
|
||||
public class Result : ZeroIdEntity
|
||||
{
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string[] ChildrenIds { get; set; }
|
||||
}
|
||||
|
||||
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items.Where(x => x.ParentId != null).Select(item => new Result
|
||||
{
|
||||
Id = item.Id,
|
||||
ParentId = item.ParentId,
|
||||
Name = item.Name,
|
||||
ChildrenIds = new string[] { }
|
||||
});
|
||||
|
||||
Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result()
|
||||
{
|
||||
Id = group.Key.ParentId,
|
||||
ParentId = group.Key.ParentId,
|
||||
Name = String.Empty,
|
||||
ChildrenIds = group.Select(x => x.Id).ToArray()
|
||||
});
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
//Index(x => x.ChannelId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Media_ByChildren : ZeroMultiMapIndex<Media_ByChildren.Result>
|
||||
{
|
||||
public class Result : ZeroIdEntity, IZeroDbConventions
|
||||
{
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public int ChildrenCount { get; set; }
|
||||
|
||||
public string[] ChildrenIds { get; set; }
|
||||
}
|
||||
|
||||
|
||||
protected override void Create()
|
||||
{
|
||||
AddMap<Media>(items => items.Select(item => new Result()
|
||||
{
|
||||
Id = item.Id,
|
||||
ParentId = item.FolderId,
|
||||
ChildrenCount = 1,
|
||||
ChildrenIds = new string[] { }
|
||||
}));
|
||||
|
||||
AddMap<MediaFolder>(items => items.Select(item => new Result()
|
||||
{
|
||||
Id = item.Id,
|
||||
ParentId = item.ParentId,
|
||||
ChildrenCount = 1,
|
||||
ChildrenIds = new string[] { }
|
||||
}));
|
||||
|
||||
Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result()
|
||||
{
|
||||
Id = null,
|
||||
ParentId = group.Key.ParentId,
|
||||
ChildrenCount = group.Sum(x => x.ChildrenCount),
|
||||
ChildrenIds = group.Select(x => x.Id).ToArray()
|
||||
});
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
Index(x => x.ParentId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Media_ByParent : ZeroMultiMapIndex<MediaListItem>
|
||||
{
|
||||
protected override void Create()
|
||||
{
|
||||
AddMap<MediaFolder>(items => items.Select(item => new MediaListItem
|
||||
{
|
||||
Id = item.Id,
|
||||
ParentId = item.ParentId,
|
||||
CreatedDate = item.CreatedDate,
|
||||
IsFolder = true,
|
||||
Name = item.Name,
|
||||
Image = null,
|
||||
Children = 0,
|
||||
Size = 0,
|
||||
IsShared = item.Blueprint != null,
|
||||
AspectRatio = 0
|
||||
}));
|
||||
|
||||
AddMap<Media>(items => items.Select(item => new MediaListItem
|
||||
{
|
||||
Id = item.Id,
|
||||
ParentId = item.FolderId,
|
||||
CreatedDate = item.CreatedDate,
|
||||
IsFolder = false,
|
||||
Name = item.Name,
|
||||
Image = item.PreviewSource,
|
||||
Children = 0,
|
||||
Size = item.Size,
|
||||
IsShared = item.Blueprint != null,
|
||||
AspectRatio = item.ImageMeta != null ? (float)item.ImageMeta.Width / item.ImageMeta.Height : 0
|
||||
}));
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
Index(x => x.ParentId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Pages_AsHistory : ZeroIndex<Page, Pages_AsHistory.Result>
|
||||
{
|
||||
public class Result : ZeroIdEntity, IZeroDbConventions
|
||||
{
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
|
||||
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items
|
||||
.Select(x => new Result()
|
||||
{
|
||||
Id = x.Id,
|
||||
LastModified = x.LastModifiedDate
|
||||
});
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
//Index(x => x.ChannelId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Pages_ByHierarchy : ZeroIndex<Page, Pages_ByHierarchy.Result>
|
||||
{
|
||||
public class Result : ZeroIdEntity, IZeroDbConventions
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<PathResult> Path { get; set; } = new List<PathResult>();
|
||||
|
||||
public string[] PathIds { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
|
||||
public class PathResult
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items
|
||||
.Select(item => new
|
||||
{
|
||||
Item = item,
|
||||
Path = Recurse(item, x => LoadDocument<Page>(x.ParentId)).Where(x => x != null && x.Id != null && x.Id != item.Id).Reverse()
|
||||
})
|
||||
.Select(item => new Result
|
||||
{
|
||||
Id = item.Item.Id,
|
||||
Name = item.Item.Name,
|
||||
Path = item.Path.Select(current => new PathResult() { Id = current.Id, Name = current.Name }).ToList(),
|
||||
PathIds = item.Path.Select(current => current.Id).ToArray()
|
||||
});
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
Index("PathIds", FieldIndexing.Exact);
|
||||
//Index(x => x.ChannelId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Pages_ByType : ZeroIndex<Page>
|
||||
{
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items.Select(item => new
|
||||
{
|
||||
PageTypeAlias = item.PageTypeAlias
|
||||
});
|
||||
|
||||
Index(x => x.PageTypeAlias, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Raven.Client.Documents.Indexes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Pages_WithChildren : ZeroIndex<Page, Pages_WithChildren.Result>
|
||||
{
|
||||
public class Result : ZeroIdEntity, IZeroDbConventions
|
||||
{
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string[] ChildrenIds { get; set; }
|
||||
}
|
||||
|
||||
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items.Where(x => x.ParentId != null).Select(item => new Result
|
||||
{
|
||||
Id = item.Id,
|
||||
ParentId = item.ParentId,
|
||||
Name = item.Name,
|
||||
ChildrenIds = new string[] { }
|
||||
});
|
||||
|
||||
Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result()
|
||||
{
|
||||
Id = group.Key.ParentId,
|
||||
ParentId = group.Key.ParentId,
|
||||
Name = String.Empty,
|
||||
ChildrenIds = group.Select(x => x.Id).ToArray()
|
||||
});
|
||||
|
||||
StoreAllFields(FieldStorage.Yes);
|
||||
//Index(x => x.ChannelId, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Linq;
|
||||
using zero.Core.Routing;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class RouteRedirects_ByUrl : ZeroIndex<RouteRedirect>
|
||||
{
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items
|
||||
.Select(x => new
|
||||
{
|
||||
IsAutomated = x.IsAutomated,
|
||||
SourceUrl = x.SourceUrl,
|
||||
TargetUrl = x.TargetUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Linq;
|
||||
using zero.Core.Routing;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Routes_ByDependencies : ZeroIndex<Route>
|
||||
{
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items
|
||||
.Select(x => new
|
||||
{
|
||||
Dependencies = x.Dependencies
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Linq;
|
||||
using zero.Core.Routing;
|
||||
|
||||
namespace zero.Core.Database.Indexes
|
||||
{
|
||||
public class Routes_ForResolver : ZeroIndex<Route>
|
||||
{
|
||||
protected override void Create()
|
||||
{
|
||||
Map = items => items
|
||||
.Select(x => new
|
||||
{
|
||||
Url = x.Url,
|
||||
AllowSuffix = x.AllowSuffix
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Attributes;
|
||||
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// An application is a website. zero can host multiple websites at once which share common assets
|
||||
/// </summary>
|
||||
[Collection("Applications")]
|
||||
public class Application : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Raven database name for application data
|
||||
/// </summary>
|
||||
public string Database { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full company or product name
|
||||
/// </summary>
|
||||
public string FullName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generic contact email. Can be used in various locations
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Image of the application
|
||||
/// </summary>
|
||||
public string ImageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Simple image of the application (can be used as favicon)
|
||||
/// </summary>
|
||||
public string IconId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// All assigned domains for this application
|
||||
/// </summary>
|
||||
public Uri[] Domains { get; set; } = Array.Empty<Uri>();
|
||||
|
||||
/// <summary>
|
||||
/// Features which are enabled for this application.
|
||||
/// Can be user-defined and affect both backoffice and frontend
|
||||
/// </summary>
|
||||
public List<string> Features { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class Country : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Preferred countries are displayed on top in lists
|
||||
/// </summary>
|
||||
public bool IsPreferred { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Country code (ISO 3166-1)
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class Culture
|
||||
{
|
||||
public string Code { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using zero.Core.Attributes;
|
||||
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
[Collection("Languages")]
|
||||
public class Language : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Language code (ISO 3166-1)
|
||||
/// </summary>
|
||||
public string Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is the default language
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this language is optional and does not have to be filled out
|
||||
/// </summary>
|
||||
public bool IsOptional { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If this language is inherited it gets all missing properties from its parent
|
||||
/// </summary>
|
||||
public string InheritedLanguageId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using zero.Core.Attributes;
|
||||
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
[Collection("MailTemplates")]
|
||||
public class MailTemplate : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Email address of the sender (overrides email from application)
|
||||
/// </summary>
|
||||
public string SenderEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the sender (overrides name from application)
|
||||
/// </summary>
|
||||
public string SenderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Email address of the recipient. This is only necessary for templates which do not have a dynamic recipient (e.g. reports).
|
||||
/// </summary>
|
||||
public string RecipientEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional comma-separated emails to send a copy to
|
||||
/// </summary>
|
||||
public string Cc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional comma-separated emails to send a hidden copy to
|
||||
/// </summary>
|
||||
public string Bcc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Email subject (can contain placeholders)
|
||||
/// </summary>
|
||||
public string Subject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Email body (can contain placeholders)
|
||||
/// </summary>
|
||||
public string Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preheader which is displayed in the preview pane (can contain placeholders)
|
||||
/// </summary>
|
||||
public string Preheader { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using zero.Core.Attributes;
|
||||
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// A media file (can contain an image or other media like videos and documents)
|
||||
/// </summary>
|
||||
[Collection("Media")]
|
||||
public class Media : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Id/name of the phyiscal folder which is stored on disk/cloud
|
||||
/// </summary>
|
||||
public string FileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the media folder
|
||||
/// </summary>
|
||||
public string FolderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alternative text which is used when the image can't be loaded
|
||||
/// </summary>
|
||||
public string AlternativeText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional caption text
|
||||
/// </summary>
|
||||
public string Caption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path of the media item
|
||||
/// </summary>
|
||||
public string Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For images this is the source for a 100x100px thumbnail
|
||||
/// </summary>
|
||||
public string ThumbnailSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For images this is the source for a [proportional]x210px thumbnail
|
||||
/// </summary>
|
||||
public string PreviewSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filesize in bytes
|
||||
/// </summary>
|
||||
public long Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Meta data for images
|
||||
/// </summary>
|
||||
public MediaImageMeta ImageMeta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional focal point for an image
|
||||
/// </summary>
|
||||
public MediaFocalPoint FocalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the media
|
||||
/// </summary>
|
||||
public MediaType Type { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public enum MediaSourceSize
|
||||
{
|
||||
Original = 0,
|
||||
Preview = 1,
|
||||
Thumbnail = 2
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// The focal point sets the point of interest in an image with x/y coordinates from 0-1
|
||||
/// </summary>
|
||||
public class MediaFocalPoint
|
||||
{
|
||||
public decimal Left { get; set; }
|
||||
|
||||
public decimal Top { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using zero.Core.Attributes;
|
||||
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// A media folder contains media and other folders
|
||||
/// </summary>
|
||||
[Collection("MediaFolders")]
|
||||
public class MediaFolder : ZeroEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Parent folder id
|
||||
/// </summary>
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata for images
|
||||
/// </summary>
|
||||
public class MediaImageMeta
|
||||
{
|
||||
/// <summary>
|
||||
/// Width in pixels
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Height in pixels
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution factor
|
||||
/// </summary>
|
||||
public double DPI { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Date the image was taken
|
||||
/// </summary>
|
||||
public DateTimeOffset? CreatedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original color space of the image
|
||||
/// </summary>
|
||||
public string ColorSpace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this image contains transparent pixels
|
||||
/// </summary>
|
||||
public bool HasTransparency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How many frames contains this image (for animation)
|
||||
/// </summary>
|
||||
public int Frames { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class MediaListItem : ZeroIdEntity
|
||||
{
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public DateTimeOffset CreatedDate { get; set; }
|
||||
|
||||
public bool IsFolder { get; set; }
|
||||
|
||||
public string Image { get; set; }
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
public int Children { get; set; }
|
||||
|
||||
public bool HasTransparency { get; set; }
|
||||
|
||||
public float AspectRatio { get; set; }
|
||||
|
||||
public bool IsShared { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class MediaListQuery : ListQuery<Media>
|
||||
{
|
||||
public string FolderId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class MediaListItemQuery : ListQuery<MediaListItem>
|
||||
{
|
||||
public string FolderId { get; set; }
|
||||
|
||||
public bool SearchIsGlobal { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public enum MediaType
|
||||
{
|
||||
File = 0,
|
||||
Image = 1
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class MediaRef : Ref
|
||||
{
|
||||
public bool IsCore { get; set; }
|
||||
|
||||
public MediaRef() : base() { }
|
||||
public MediaRef(string id, bool isCore) : base(id)
|
||||
{
|
||||
IsCore = isCore;
|
||||
}
|
||||
|
||||
public static implicit operator string(MediaRef reference) => reference.Id;
|
||||
|
||||
public static implicit operator MediaRef(string id) => new MediaRef(id, false);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class Ref<T> : Ref where T : ZeroIdEntity
|
||||
{
|
||||
public Ref() : base() { }
|
||||
public Ref(string id) : base(id) { }
|
||||
|
||||
public static implicit operator Ref<T>(string id) => new Ref<T>(id);
|
||||
|
||||
public static implicit operator string(Ref<T> reference) => reference.Id;
|
||||
}
|
||||
|
||||
|
||||
public class Ref
|
||||
{
|
||||
public Ref() { }
|
||||
|
||||
public Ref(string id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Id;
|
||||
}
|
||||
|
||||
|
||||
public static implicit operator Ref(string id) => new Ref(id);
|
||||
|
||||
public static implicit operator string(Ref reference) => reference.Id;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class ValueRef<T> : ValueRef<T, string> where T : ZeroIdEntity
|
||||
{
|
||||
public ValueRef() : base() { }
|
||||
public ValueRef(string id, string value) : base(id, value) { }
|
||||
|
||||
public static implicit operator string(ValueRef<T> reference) => reference.Id;
|
||||
}
|
||||
|
||||
|
||||
public class ValueRef<TEntity, TValue> : Ref where TEntity : ZeroIdEntity
|
||||
{
|
||||
public ValueRef() : base() { }
|
||||
public ValueRef(string id, TValue value) : base(id)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public TValue Value { get; set; }
|
||||
|
||||
public static implicit operator string(ValueRef<TEntity, TValue> reference) => reference.Id;
|
||||
}
|
||||
|
||||
//public class ValueRef : Ref
|
||||
//{
|
||||
// public ValueRef() : base() { }
|
||||
|
||||
// public ValueRef(string id, string value) : base(id)
|
||||
// {
|
||||
// Value = value;
|
||||
// }
|
||||
|
||||
// public string Value { get; set; }
|
||||
|
||||
|
||||
// public override string ToString()
|
||||
// {
|
||||
// return (Id, Value).ToString();
|
||||
// }
|
||||
//}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
namespace zero.Core.Entities.Setup
|
||||
{
|
||||
public sealed class SetupModel
|
||||
{
|
||||
public string AppName { get; set; }
|
||||
|
||||
public SetupUserModel User { get; set; }
|
||||
|
||||
public SetupDatabaseModel Database { get; set; }
|
||||
|
||||
public string ContentRootPath { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public sealed class SetupUserModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SetupDatabaseModel
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class Translation : ZeroEntity
|
||||
{
|
||||
public Translation()
|
||||
{
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Value of the translation
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display + input type
|
||||
/// </summary>
|
||||
public TranslationDisplay Display { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public enum TranslationDisplay
|
||||
{
|
||||
Text = 0,
|
||||
HTML = 1
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public class Video
|
||||
{
|
||||
public VideoProvider Provider { get; set; }
|
||||
|
||||
public string VideoId { get; set; }
|
||||
|
||||
public string VideoUrl { get; set; }
|
||||
|
||||
public string VideoPreviewImageUrl { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string PreviewImageId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public enum VideoProvider
|
||||
{
|
||||
Html,
|
||||
Youtube,
|
||||
Vimeo
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Identity;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class BackofficeUserExtensions
|
||||
{
|
||||
public static string[] GetAllowedAppIds(this BackofficeUser user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return new string[0] { };
|
||||
}
|
||||
|
||||
IEnumerable<Permission> permissions = user.Claims
|
||||
.Where(claim => claim.Type == Constants.Auth.Claims.Permission && claim.Value.StartsWith(Permissions.Applications))
|
||||
.Select(x => Permission.FromClaim(x.ToClaim(), Permissions.Applications));
|
||||
|
||||
return permissions.Where(x => x.IsTrue).Select(x => x.NormalizedKey).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using zero.Core.Collections;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class CollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared scope for the current collection instance
|
||||
/// </summary>
|
||||
public static CollectionScope SharedScope<T>(this T collection) where T : ICollectionBase
|
||||
{
|
||||
return new CollectionScope(collection, "shared");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class ColorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get color object from a hex string
|
||||
/// </summary>
|
||||
public static Color FromHex(this string color)
|
||||
{
|
||||
if (color.StartsWith("#"))
|
||||
{
|
||||
color = color[1..];
|
||||
}
|
||||
|
||||
if (color.Length != 6)
|
||||
{
|
||||
throw new Exception("Hex color has to be in format #RRGGBB");
|
||||
}
|
||||
|
||||
return Color.FromArgb(
|
||||
int.Parse(color.Substring(0, 2), System.Globalization.NumberStyles.HexNumber),
|
||||
int.Parse(color.Substring(2, 2), System.Globalization.NumberStyles.HexNumber),
|
||||
int.Parse(color.Substring(4, 2), System.Globalization.NumberStyles.HexNumber)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get contrast ratio between two color values
|
||||
/// <see cref="https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js#L95" />
|
||||
/// </summary>
|
||||
public static double GetContrastRatio(this Color color1, Color color2)
|
||||
{
|
||||
double lum1 = color1.GetLuminance();
|
||||
double lum2 = color2.GetLuminance();
|
||||
double brightest = Math.Max(lum1, lum2);
|
||||
double darkest = Math.Min(lum1, lum2);
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get luminance value of a color based on WCAG-conform JS implementation
|
||||
/// <see cref="https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js#L42" />
|
||||
/// </summary>
|
||||
public static double GetLuminance(this Color color)
|
||||
{
|
||||
double[] a = new double[3] { color.R, color.G, color.B }.Select(v =>
|
||||
{
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.Pow((v + 0.055) / 1.055, 2.4);
|
||||
}).ToArray();
|
||||
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static ListResult<T> ToQueriedList<T>(this IEnumerable<T> items, ListQuery<T> query) where T : ZeroEntity
|
||||
{
|
||||
//queryable = queryable.Statistics(out QueryStatistics stats);
|
||||
|
||||
if (query != null)
|
||||
{
|
||||
if (!query.IncludeInactive)
|
||||
{
|
||||
items = items.Where(x => x.IsActive);
|
||||
}
|
||||
|
||||
if (!query.Search.IsNullOrEmpty())
|
||||
{
|
||||
items = items.Where(x => x.Name.Contains(query.Search, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
//if (!query.Search.IsNullOrEmpty() && query.SearchSelector != null)
|
||||
//{
|
||||
// items = items.SearchIf(query.SearchSelector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
|
||||
//}
|
||||
//if (!query.Search.IsNullOrEmpty() && query.SearchSelectors.Length > 0)
|
||||
//{
|
||||
// foreach (var selector in query.SearchSelectors)
|
||||
// {
|
||||
// items = items.SearchIf(selector, query.Search, "*", "*", Raven.Client.Documents.Queries.SearchOperator.And);
|
||||
// }
|
||||
//}
|
||||
|
||||
//if (!query.OrderBy.IsNullOrEmpty())
|
||||
//{
|
||||
// items = items.OrderBy(query.OrderBy, query.OrderIsDescending);
|
||||
//}
|
||||
|
||||
if (query.PageSize > 0)
|
||||
{
|
||||
items = items.Paging(query.Page, query.PageSize);
|
||||
}
|
||||
}
|
||||
|
||||
List<T> result = items.ToList();
|
||||
|
||||
return new ListResult<T>(result, result.Count, query.Page, query.PageSize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static IEnumerable<T> Paging<T>(this IEnumerable<T> source, int pageNumber, int pageSize)
|
||||
{
|
||||
pageNumber = pageNumber.Limit(1, 10_000_000);
|
||||
pageSize = pageSize.Limit(1, 1_000);
|
||||
|
||||
if (pageNumber <= 0 || pageSize <= 0)
|
||||
{
|
||||
throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero");
|
||||
}
|
||||
|
||||
return source.Skip((pageNumber - 1) * pageSize).Take(pageSize);
|
||||
}
|
||||
|
||||
|
||||
public static bool TryGet<T>(this IEnumerable<T> source, Func<T, bool> predicate, out T model)
|
||||
{
|
||||
model = source.FirstOrDefault(predicate);
|
||||
return model != null;
|
||||
}
|
||||
|
||||
|
||||
public static bool TryAdd<T>(this IList<T> source, T model) where T : class
|
||||
{
|
||||
if (model == default)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.Add(model);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the current request is a backoffice request
|
||||
/// </summary>
|
||||
public static bool IsBackofficeRequest(this HttpContext context, string backofficePath)
|
||||
{
|
||||
string path = backofficePath.EnsureStartsWith('/').TrimEnd('/');
|
||||
return context.Request.Path.ToString().StartsWith(path);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current request is an AJAX request
|
||||
/// </summary>
|
||||
public static bool IsAjaxRequest(this HttpContext context)
|
||||
{
|
||||
if (context?.Request?.Headers == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current request only accepts application/json
|
||||
/// </summary>
|
||||
public static bool IsJsonRequest(this HttpContext context)
|
||||
{
|
||||
if (context?.Request?.Headers == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return context.Request.Headers["Accept"] == "application/json";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get IP Address of the client
|
||||
/// </summary>
|
||||
public static string GetClientIpAddress(this HttpContext context)
|
||||
{
|
||||
return context.Connection.RemoteIpAddress?.ToString();
|
||||
//string ipAddress = context.GetServerVariable("HTTP_X_FORWARDED_FOR");
|
||||
//return !ipAddress.IsNullOrEmpty() ? ipAddress.Split(',')[0] : context.GetServerVariable("REMOTE_ADDR");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Mail;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class MailExtensions
|
||||
{
|
||||
//public static string Add(this AttachmentCollection attachments, byte[] content, )
|
||||
//{
|
||||
// if (input < min)
|
||||
// {
|
||||
// return min;
|
||||
// }
|
||||
// if (input > max)
|
||||
// {
|
||||
// return max;
|
||||
// }
|
||||
// return input;
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class NumberExtensions
|
||||
{
|
||||
static Dictionary<FileSizeNotation, string[]> FileSizeUnits = new()
|
||||
{
|
||||
{ FileSizeNotation.SI, new string[] { "B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" } },
|
||||
{ FileSizeNotation.IEC, new string[] { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" } },
|
||||
{ FileSizeNotation.JEDEC, new string[] { "B", "KB", "MB", "GB" } }
|
||||
};
|
||||
|
||||
public static int Limit(this int input, int min, int max)
|
||||
{
|
||||
if (input < min)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
if (input > max)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
public static string GetFileSize(this long sizeInBytes, FileSizeNotation notation = FileSizeNotation.JEDEC)
|
||||
{
|
||||
if (!FileSizeUnits.ContainsKey(notation))
|
||||
{
|
||||
throw new NotImplementedException($"The notation {notation} has no implementation for generating human-readable file sizes");
|
||||
}
|
||||
|
||||
int power = notation == FileSizeNotation.SI ? 1000 : 1024;
|
||||
|
||||
string[] units = FileSizeUnits[notation];
|
||||
|
||||
int order = 0;
|
||||
|
||||
while (sizeInBytes >= power && order + 1 < units.Length)
|
||||
{
|
||||
order++;
|
||||
sizeInBytes = sizeInBytes / power;
|
||||
}
|
||||
|
||||
return String.Format("{0:0.##} {1}", sizeInBytes, units[order]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Attributes;
|
||||
using zero.Core.Utils;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class ObjectExtensions
|
||||
{
|
||||
public static bool Is<T>(this Type type)
|
||||
{
|
||||
return type.IsAssignableFrom(typeof(T));
|
||||
}
|
||||
|
||||
|
||||
public static bool Is<T>(this object obj)
|
||||
{
|
||||
return obj.GetType().IsAssignableFrom(typeof(T));
|
||||
}
|
||||
|
||||
|
||||
public static T Clone<T>(this T obj)
|
||||
{
|
||||
Type type = obj.GetType();
|
||||
return (T)JsonConvert.DeserializeObject(JsonConvert.SerializeObject(obj, new RefJsonConverter()), type, new RefJsonConverter());
|
||||
}
|
||||
|
||||
public static T CloneLax<T>(this object obj)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(obj, new RefJsonConverter()), new RefJsonConverter());
|
||||
}
|
||||
|
||||
public static T AutoSetIds<T>(this T obj)
|
||||
{
|
||||
// find all Raven Ids
|
||||
List<ObjectTraverser.Result<GenerateIdAttribute>> ravenIds = ObjectTraverser.FindAttribute<GenerateIdAttribute>(obj);
|
||||
|
||||
// set unset Raven Ids
|
||||
foreach (ObjectTraverser.Result<GenerateIdAttribute> item in ravenIds)
|
||||
{
|
||||
string id = item.Property.GetValue(item.Parent, null) as string;
|
||||
if (String.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
item.Property.SetValue(item.Parent, item.Item.Length.HasValue ? IdGenerator.Create(item.Item.Length.Value) : IdGenerator.Create());
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Linq;
|
||||
using Raven.Client.Documents.Queries;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string path, bool isDescending, OrderingType type = OrderingType.String)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(path))
|
||||
{
|
||||
char[] a = path.ToCharArray();
|
||||
a[0] = char.ToUpper(a[0]);
|
||||
path = new string(a);
|
||||
}
|
||||
|
||||
if (isDescending)
|
||||
{
|
||||
return source.OrderByDescending(path, type);
|
||||
}
|
||||
return source.OrderBy(path, type);
|
||||
}
|
||||
|
||||
|
||||
public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string path, bool isDescending, OrderingType type = OrderingType.String)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(path))
|
||||
{
|
||||
char[] a = path.ToCharArray();
|
||||
a[0] = char.ToUpper(a[0]);
|
||||
path = new string(a);
|
||||
}
|
||||
|
||||
if (isDescending)
|
||||
{
|
||||
return source.ThenByDescending(path, type);
|
||||
}
|
||||
return source.ThenBy(path, type);
|
||||
}
|
||||
|
||||
|
||||
public static IQueryable<T> Paging<T>(this IQueryable<T> source, int pageNumber, int pageSize)
|
||||
{
|
||||
pageNumber = pageNumber.Limit(1, 10_000_000);
|
||||
pageSize = pageSize.Limit(1, 1_000);
|
||||
|
||||
if (pageNumber <= 0 || pageSize <= 0)
|
||||
{
|
||||
throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero");
|
||||
}
|
||||
|
||||
return source.Skip((pageNumber - 1) * pageSize).Take(pageSize);
|
||||
}
|
||||
|
||||
|
||||
public static IRavenQueryable<T> WhereIf<T>(this IRavenQueryable<T> source, Expression<Func<T, bool>> predicate, bool condition, Expression<Func<T, bool>> elsePredicate = null)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
if (elsePredicate != null)
|
||||
{
|
||||
return source.Where(elsePredicate);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
return source.Where(predicate);
|
||||
}
|
||||
|
||||
|
||||
public static IQueryable<T> SearchIf<T>(this IQueryable<T> source, Expression<Func<T, object>> fieldSelector, string searchTerms, string suffix = null, string prefix = null, SearchOperator @operator = SearchOperator.Or)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(searchTerms))
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
searchTerms = searchTerms.Trim();
|
||||
|
||||
string[] searchParts = searchTerms.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries).Select(x =>
|
||||
{
|
||||
if (suffix != null)
|
||||
{
|
||||
x += suffix;
|
||||
}
|
||||
if (prefix != null)
|
||||
{
|
||||
x = prefix + x;
|
||||
}
|
||||
return x;
|
||||
}).ToArray();
|
||||
|
||||
if (searchTerms.StartsWith('"') && searchTerms.EndsWith('"'))
|
||||
{
|
||||
searchParts = new[] { searchTerms };
|
||||
}
|
||||
|
||||
return source.Search(fieldSelector, searchParts, @operator: @operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using zero.Core.Assemblies;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all found implementations based on the service type and assembly discovery rules
|
||||
/// </summary>
|
||||
public static void AddAll<TService>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient, bool asSelf = false) => services.AddAll(typeof(TService), lifetime, asSelf);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all found implementations based on the service type and assembly discovery rules
|
||||
/// </summary>
|
||||
public static void AddAll(this IServiceCollection services, Type serviceType, ServiceLifetime lifetime = ServiceLifetime.Transient, bool asSelf = false)
|
||||
{
|
||||
if (AssemblyDiscovery.Current == null)
|
||||
{
|
||||
throw new Exception("services.AddAll() can only be run after mvcBuilder.AddZero()");
|
||||
}
|
||||
|
||||
// add implementations with generic service types
|
||||
if (serviceType.GetTypeInfo().IsGenericTypeDefinition)
|
||||
{
|
||||
IEnumerable<(Type, TypeInfo)> matches = AssemblyDiscovery.Current.GetAllClassTypes().SelectMany(type =>
|
||||
{
|
||||
var interfaces = type.GetInterfaces().Select(x => x.GetTypeInfo());
|
||||
IEnumerable<Type> genericTypes = interfaces.Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == serviceType);
|
||||
if (genericTypes.Any())
|
||||
{
|
||||
var genericTypeDefinitions = genericTypes.First();
|
||||
}
|
||||
return genericTypes.Select(x => (x, type));
|
||||
});
|
||||
|
||||
foreach ((Type, Type) match in matches)
|
||||
{
|
||||
services.Add(new ServiceDescriptor(asSelf ? match.Item2 : match.Item1, match.Item2, lifetime));
|
||||
}
|
||||
}
|
||||
// add implementations with specific service types
|
||||
else
|
||||
{
|
||||
foreach (Type type in AssemblyDiscovery.Current.GetTypes(serviceType))
|
||||
{
|
||||
services.Add(new ServiceDescriptor(asSelf ? type : serviceType, type, lifetime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds or overrides an implementation
|
||||
/// </summary>
|
||||
public static void Replace<TService, TImplementation>(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
where TService : class
|
||||
where TImplementation : class, TService
|
||||
{
|
||||
services.RemoveAll<TService>();
|
||||
services.Add(new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds or overrides an implementation
|
||||
/// </summary>
|
||||
public static void Replace<TService, TImplementation>(this IServiceCollection services, Func<IServiceProvider, TImplementation> implementationFactory, ServiceLifetime lifetime = ServiceLifetime.Transient)
|
||||
where TService : class
|
||||
where TImplementation : class, TService
|
||||
{
|
||||
services.RemoveAll<TService>();
|
||||
services.Add(new ServiceDescriptor(typeof(TService), implementationFactory, lifetime));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
using FluentValidation;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using zero.Core.Api;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Extensions
|
||||
{
|
||||
public static class ValidatorExtensions
|
||||
{
|
||||
private const char DOT = '.';
|
||||
|
||||
private const char KLAMMERAFFE = '@';
|
||||
|
||||
private static string HEX_REGEX = "#[0-9a-fA-F]{3,8}";
|
||||
|
||||
/// <summary>
|
||||
/// Validate a color input as HEX (#aabbccdd or #aabbcc or #abc)
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Hex<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.Matches(HEX_REGEX).WithMessage("@errors.forms.hex_format");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validate an email
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Url<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.Must((root, value, context) =>
|
||||
{
|
||||
return value.IsNullOrWhiteSpace() || Uri.IsWellFormedUriString(value, UriKind.Absolute);
|
||||
}).WithMessage("@errors.forms.url_format");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validate an email
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Email<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.Must((root, value, context) =>
|
||||
{
|
||||
if (value.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
int index = value.IndexOf(KLAMMERAFFE);
|
||||
|
||||
if (index < 0 || index == value.Length - 1 || index != value.LastIndexOf(KLAMMERAFFE))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).WithMessage("@errors.forms.email_invalid");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validate one or multiple emails
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Emails<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.Must((root, value, context) =>
|
||||
{
|
||||
if (value.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
string[] mails = value.Split(',', ';').Select(x => x.Trim()).Where(x => !x.IsNullOrWhiteSpace()).ToArray();
|
||||
|
||||
if (!mails.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (string mail in mails)
|
||||
{
|
||||
int index = value.IndexOf(KLAMMERAFFE);
|
||||
|
||||
if (index < 0 || index == value.Length - 1 || index != value.LastIndexOf(KLAMMERAFFE))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}).WithMessage("@errors.forms.emails_invalid");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this value is unique within a collection
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, TProperty> Unique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroStore store) where T : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
|
||||
{
|
||||
bool any = await store.Session().Advanced.AsyncDocumentQuery<T>()
|
||||
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
|
||||
.WhereEquals(context.PropertyName.ToPascalCaseId(), value)
|
||||
.AnyAsync(cancellation);
|
||||
|
||||
return !any;
|
||||
}).WithMessage("@errors.forms.not_unique");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this value is at least set once to the expected value within a collection
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, TProperty> ExpectAnyUnique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroStore store, TProperty expectedValue) where T : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
|
||||
{
|
||||
return await store.Session().Advanced.AsyncDocumentQuery<T>()
|
||||
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
|
||||
.WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue)
|
||||
.AnyAsync(cancellation);
|
||||
}).WithMessage("@errors.forms.not_unique_alone");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Exists<T>(this IRuleBuilder<T, string> ruleBuilder, IZeroStore store) where T : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.Exists<T, T>(store);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Exists<T, TReference>(this IRuleBuilder<T, string> ruleBuilder, IZeroStore store) where T : ZeroEntity where TReference : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.MustAsync(async (entity, id, context, cancellation) =>
|
||||
{
|
||||
if (id.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TReference model = await store.Session().LoadAsync<TReference>(id);
|
||||
return model != null;
|
||||
}).WithMessage("@errors.forms.reference_notfound");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validates a culture identifier
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Culture<T>(this IRuleBuilder<T, string> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.Must((root, value, context) =>
|
||||
{
|
||||
if (value.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CultureInfo info = CultureInfo.GetCultureInfo(value);
|
||||
return info != null && !info.EnglishName.Equals(value, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}).WithMessage("@errors.forms.culture");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this value is unique within a collection
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, TProperty> Unique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
|
||||
{
|
||||
bool any = await session.Advanced.AsyncDocumentQuery<T>()
|
||||
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
|
||||
.WhereEquals(context.PropertyName.ToPascalCaseId(), value)
|
||||
.AnyAsync(cancellation);
|
||||
|
||||
return !any;
|
||||
}).WithMessage("@errors.forms.not_unique");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this value is at least set once to the expected value within a collection
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, TProperty> ExpectAnyUnique<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, IZeroDocumentSession session, TProperty expectedValue) where T : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.MustAsync(async (entity, value, context, cancellation) =>
|
||||
{
|
||||
return await session.Advanced.AsyncDocumentQuery<T>()
|
||||
.WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id)
|
||||
.WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue)
|
||||
.AnyAsync(cancellation);
|
||||
}).WithMessage("@errors.forms.not_unique_alone");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Exists<T>(this IRuleBuilder<T, string> ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.Exists<T, T>(session);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current)
|
||||
/// </summary>
|
||||
public static IRuleBuilderOptions<T, string> Exists<T, TReference>(this IRuleBuilder<T, string> ruleBuilder, IZeroDocumentSession session) where T : ZeroEntity where TReference : ZeroEntity
|
||||
{
|
||||
return ruleBuilder.MustAsync(async (entity, id, context, cancellation) =>
|
||||
{
|
||||
if (id.IsNullOrWhiteSpace())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
TReference model = await session.LoadAsync<TReference>(id);
|
||||
return model != null;
|
||||
}).WithMessage("@errors.forms.reference_notfound");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Handlers
|
||||
{
|
||||
public interface IApplicationResolverHandler : IHandler
|
||||
{
|
||||
Application Resolve(HttpRequest request, IEnumerable<Application> applications);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Handlers
|
||||
{
|
||||
public interface IHandler
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace zero.Core.Handlers
|
||||
{
|
||||
public class HandlerHolder : IHandlerHolder
|
||||
{
|
||||
IServiceProvider Services { get; }
|
||||
|
||||
public HandlerHolder(IServiceProvider services)
|
||||
{
|
||||
Services = services;
|
||||
}
|
||||
|
||||
|
||||
public T Get<T>() where T : IHandler
|
||||
{
|
||||
return Services.GetService<T>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IHandlerHolder
|
||||
{
|
||||
T Get<T>() where T : IHandler;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Handlers
|
||||
{
|
||||
public interface IModuleTypeHandler : IHandler
|
||||
{
|
||||
IEnumerable<ModuleType> GetAllowedModuleTypes(Application application, IEnumerable<ModuleType> registeredTypes, Page page = default, string[] tags = default);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Raven.Client.Documents.Session;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Handlers
|
||||
{
|
||||
public interface IPageTypeHandler : IHandler
|
||||
{
|
||||
Task<IEnumerable<PageType>> GetAllowedPageTypes(Application application, IEnumerable<PageType> registeredTypes, IEnumerable<Page> parents);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using zero.Core.Attributes;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Integrations
|
||||
{
|
||||
/// <summary>
|
||||
/// An integration is an application part which has a public configuration per app.
|
||||
/// It's up to the user to provide functionality.
|
||||
/// </summary>
|
||||
[Collection("Integrations")]
|
||||
public class Integration : ZeroEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string TypeAlias { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using zero.Core.Collections;
|
||||
|
||||
namespace zero.Core.Integrations
|
||||
{
|
||||
public class IntegrationService : IntegrationsCollection, IIntegrationService
|
||||
{
|
||||
public IntegrationService(ICollectionContext<Integration> context, ILogger<IntegrationsCollection> logger) : base(context, logger)
|
||||
{
|
||||
Options = new(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IIntegrationService : IIntegrationsCollection { }
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero.Core.Integrations
|
||||
{
|
||||
/// <summary>
|
||||
/// An integration is an application part which has a public configuration per app.
|
||||
/// It's up to the user to provide functionality.
|
||||
/// </summary>
|
||||
public class IntegrationType<T> : IntegrationType where T : Integration, new()
|
||||
{
|
||||
public IntegrationType() : base(typeof(T)) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An integration is an application part which has a public configuration per app.
|
||||
/// It's up to the user to provide functionality.
|
||||
/// </summary>
|
||||
public class IntegrationType : OptionsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Group integrations by tags
|
||||
/// </summary>
|
||||
public List<string> Tags { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Optional description
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Image of the integration
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; }
|
||||
|
||||
|
||||
public IntegrationType(Type type)
|
||||
{
|
||||
ContentType = type;
|
||||
}
|
||||
|
||||
public static IntegrationType Convert<T>(IntegrationType<T> model) where T : Integration, new()
|
||||
{
|
||||
return new(model.ContentType)
|
||||
{
|
||||
Alias = model.Alias,
|
||||
Name = model.Name,
|
||||
Description = model.Description,
|
||||
ImagePath = model.ImagePath,
|
||||
Tags = model.Tags,
|
||||
Validator = model.Validator
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace zero.Core.Integrations
|
||||
{
|
||||
public class IntegrationTypeWithStatus
|
||||
{
|
||||
public IntegrationType Type { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
public bool IsConfigured { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Mail;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Api;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of an IMailSender which sends the mail a flat file
|
||||
/// and therefore not using the SMTP channel.
|
||||
/// Implementing real mail sending is up to the consuming application.
|
||||
/// </summary>
|
||||
public class FileMailDispatcher : IMailDispatcher
|
||||
{
|
||||
protected Queue<Mail> Queue { get; private set; } = new Queue<Mail>();
|
||||
|
||||
protected IPaths PathResolver { get; private set; }
|
||||
|
||||
string MailDirectory;
|
||||
|
||||
|
||||
public FileMailDispatcher(IPaths pathResolver)
|
||||
{
|
||||
PathResolver = pathResolver;
|
||||
MailDirectory = "mails";
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Enqueue(Mail message)
|
||||
{
|
||||
Queue.Enqueue(message);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Send(CancellationToken token = default)
|
||||
{
|
||||
if (Queue.Count < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string folder = PathResolver.Map(MailDirectory);
|
||||
PathResolver.Create(folder);
|
||||
|
||||
while (Queue.Count > 0)
|
||||
{
|
||||
Mail message = Queue.Dequeue();
|
||||
string content = JsonConvert.SerializeObject(message, new JsonSerializerSettings()
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
TypeNameHandling = TypeNameHandling.None,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||
});
|
||||
|
||||
string filename = Safenames.File(DateTimeOffset.Now.ToString("yyyy-MM-dd-HH-mm-ss") + "_" + message.To[0].Address + ".txt");
|
||||
string path = PathResolver.Map(folder, filename);
|
||||
|
||||
await File.WriteAllTextAsync(path, content, token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creats the file content from a mail message
|
||||
/// </summary>
|
||||
string CreateContent(Mail message)
|
||||
{
|
||||
StringBuilder text = new();
|
||||
text.AppendLine("To: " + message.To);
|
||||
text.AppendLine("CC: " + message.CC);
|
||||
text.AppendLine("Bcc: " + message.Bcc);
|
||||
text.AppendLine("From: " + message.From);
|
||||
text.AppendLine("Subject: " + message.Subject);
|
||||
text.Append("Date: " + DateTimeOffset.UtcNow.ToString());
|
||||
|
||||
text.AppendLine();
|
||||
text.AppendLine("=============== BODY ===============");
|
||||
text.AppendLine();
|
||||
|
||||
text.AppendLine(message.Body);
|
||||
|
||||
return text.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
public interface IMailDispatcher : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new mail message to the outgoing queue
|
||||
/// </summary>
|
||||
void Enqueue(Mail message);
|
||||
|
||||
/// <summary>
|
||||
/// Sends all mails which have been added to the queue previously
|
||||
/// </summary>
|
||||
Task Send(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a certain sender signature is supported by this dispatcher
|
||||
/// </summary>
|
||||
Task<bool> IsSenderSupported(string email) => Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Mail;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of an IMailSender which sends the mail to the attached logger
|
||||
/// and therefore not using the SMTP channel.
|
||||
/// Implementing real mail sending is up to the consuming application.
|
||||
/// </summary>
|
||||
public class LoggerMailDispatcher : IMailDispatcher
|
||||
{
|
||||
protected Queue<Mail> Queue { get; private set; } = new Queue<Mail>();
|
||||
|
||||
protected ILogger<LoggerMailDispatcher> Logger { get; set; }
|
||||
|
||||
|
||||
public LoggerMailDispatcher(ILogger<LoggerMailDispatcher> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Enqueue(Mail message)
|
||||
{
|
||||
Queue.Enqueue(message);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Send(CancellationToken token = default)
|
||||
{
|
||||
while (Queue.Count > 0)
|
||||
{
|
||||
Mail message = Queue.Dequeue();
|
||||
Logger.LogInformation("Mail to {to}. Subject: {subject}", message.To[0].Address, message.Subject);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Net.Mail;
|
||||
using zero.Core.Entities;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
public class Mail<T> : Mail where T : class
|
||||
{
|
||||
public T Model { get; set; }
|
||||
}
|
||||
|
||||
public class Mail : MailMessage
|
||||
{
|
||||
public bool IsDeactivated { get; set; }
|
||||
|
||||
public string ViewPath { get; set; }
|
||||
|
||||
public bool HasView { get; set; } = true;
|
||||
|
||||
public bool IsRendered { get; set; }
|
||||
|
||||
public string Preheader { get; set; }
|
||||
|
||||
public MailTemplate Template { get; set; }
|
||||
|
||||
public MailPlaceholders Placeholders { get; set; } = new();
|
||||
|
||||
public MailMetadata Metadata { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
public class MailMetadata : Dictionary<string, string>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
public class MailPlaceholders : Dictionary<string, string>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Collections;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Renderer;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
public class MailProvider : IMailProvider
|
||||
{
|
||||
protected IMailTemplatesCollection Collection { get; set; }
|
||||
|
||||
protected ILogger<IMailProvider> Logger { get; set; }
|
||||
|
||||
protected IZeroContext Zero { get; set; }
|
||||
|
||||
protected IMailDispatcher MailSender { get; set; }
|
||||
|
||||
protected IRazorRenderer Renderer { get; set; }
|
||||
|
||||
protected Regex PlaceholderRegex { get; set; }
|
||||
|
||||
private Encoding encoding = Encoding.UTF8;
|
||||
|
||||
|
||||
public MailProvider(IZeroContext zero, IMailTemplatesCollection collection, ILogger<IMailProvider> logger, IMailDispatcher mailSender, IRazorRenderer renderer)
|
||||
{
|
||||
Zero = zero;
|
||||
Collection = collection;
|
||||
Logger = logger;
|
||||
MailSender = mailSender;
|
||||
Renderer = renderer;
|
||||
PlaceholderRegex = new Regex("{([\\w-_.]+)}", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<T> Create<T>(string mailTemplateKey, Action<MailTemplate> onCreate = null) where T : Mail, new()
|
||||
{
|
||||
MailTemplate template = await GetMailTemplate(mailTemplateKey);
|
||||
|
||||
if (template == null)
|
||||
{
|
||||
Logger.LogError("Could not find a mail template with the key {key}", mailTemplateKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
onCreate?.Invoke(template);
|
||||
|
||||
return Merge(new T(), template);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Mail> Create(string mailTemplateKey, Action<MailTemplate> onCreate = null)
|
||||
{
|
||||
MailTemplate template = await GetMailTemplate(mailTemplateKey);
|
||||
|
||||
if (template == null)
|
||||
{
|
||||
Logger.LogError("Could not find a mail template with the key {key}", mailTemplateKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
onCreate?.Invoke(template);
|
||||
|
||||
return Merge(new Mail(), template);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task Send(Mail message, CancellationToken token = default)
|
||||
{
|
||||
await Send(message, MailSender, token);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default)
|
||||
{
|
||||
if (message.IsDeactivated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Render(message);
|
||||
dispatcher.Enqueue(message);
|
||||
await dispatcher.Send(token);
|
||||
Logger.LogInformation("Dispatched email (template: {template}) to {recipient}", message.Template?.Alias ?? "none", message.To);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to send mail message with template @{template}", message.Template.Key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected virtual async Task<MailTemplate> GetMailTemplate(string key)
|
||||
{
|
||||
return await Collection.GetByKey(key);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<string> Render(Mail message)
|
||||
{
|
||||
message.Subject = TokenReplacement.Apply(message.Subject, message.Placeholders);
|
||||
message.Body = TokenReplacement.Apply(message.Body, message.Placeholders);
|
||||
message.Preheader = TokenReplacement.Apply(message.Preheader, message.Placeholders);
|
||||
|
||||
if (!message.HasView)
|
||||
{
|
||||
message.IsRendered = true;
|
||||
return message.Body;
|
||||
}
|
||||
|
||||
string viewPath = message.ViewPath;
|
||||
|
||||
if (viewPath.IsNullOrEmpty())
|
||||
{
|
||||
viewPath = $"~/Views/Mails/{message.Template.Key.Replace('.', '/')}.cshtml";
|
||||
}
|
||||
|
||||
message.Body = await Renderer.ViewAsync(viewPath, message);
|
||||
message.IsBodyHtml = true;
|
||||
message.IsRendered = true;
|
||||
return message.Body;
|
||||
}
|
||||
|
||||
|
||||
protected virtual T Merge<T>(T mail, MailTemplate template) where T : Mail
|
||||
{
|
||||
mail.Template = template;
|
||||
mail.IsDeactivated = !mail.Template.IsActive;
|
||||
|
||||
// get sender from template or fall back to application
|
||||
mail.From = new MailAddress(template.SenderEmail.Or(Zero.Application.Email), template.SenderName.Or(Zero.Application.FullName), encoding);
|
||||
mail.Sender = mail.From;
|
||||
mail.ReplyToList.Add(mail.From);
|
||||
|
||||
// cc + bcc (multiple addresses are separated with commas
|
||||
if (!template.Cc.IsNullOrWhiteSpace())
|
||||
{
|
||||
mail.CC.Add(template.Cc);
|
||||
}
|
||||
if (!template.Bcc.IsNullOrWhiteSpace())
|
||||
{
|
||||
mail.Bcc.Add(template.Bcc);
|
||||
}
|
||||
|
||||
// recipient
|
||||
if (!template.RecipientEmail.IsNullOrWhiteSpace())
|
||||
{
|
||||
mail.To.Add(template.RecipientEmail);
|
||||
}
|
||||
|
||||
// subject
|
||||
mail.Subject = template.Subject;
|
||||
mail.SubjectEncoding = encoding;
|
||||
|
||||
// body
|
||||
mail.Body = mail.Template.Body;
|
||||
mail.IsBodyHtml = true;
|
||||
mail.BodyEncoding = encoding;
|
||||
mail.Preheader = mail.Template.Preheader;
|
||||
|
||||
return mail;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IMailProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a maling from a template
|
||||
/// </summary>
|
||||
Task<Mail> Create(string mailTemplateKey, Action<MailTemplate> onCreate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a maling from a template
|
||||
/// </summary>
|
||||
Task<T> Create<T>(string mailTemplateKey, Action<MailTemplate> onCreate = null) where T : Mail, new();
|
||||
|
||||
/// <summary>
|
||||
/// Renders the message body.
|
||||
/// This is automatically called when sending messages.
|
||||
/// </summary>
|
||||
Task<string> Render(Mail message);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message with the default dispatcher
|
||||
/// </summary>
|
||||
Task Send(Mail message, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message with the specified dispatcher
|
||||
/// </summary>
|
||||
Task Send(Mail message, IMailDispatcher dispatcher, CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace zero.Core.Mails
|
||||
{
|
||||
public class MailSendResult
|
||||
{
|
||||
public string Alias { get; set; }
|
||||
|
||||
public DateTimeOffset LastRunDate { get; set; }
|
||||
|
||||
public MailSendResult() { }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace zero.Core.Messages
|
||||
{
|
||||
public interface IMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates a handler that can perform an action when a message of
|
||||
/// a certain type is received. (could be an interface rather than concrete type)
|
||||
/// </summary>
|
||||
public interface IMessageHandler<TMessage> where TMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Method to invoke when a message of type TMessage is published
|
||||
/// </summary>
|
||||
Task Handle(TMessage message);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a handler that can perform an action when a message of
|
||||
/// a certain type is received. (could be an interface rather than concrete type)
|
||||
/// </summary>
|
||||
public interface IBatchMessageHandler<TMessage> : IMessageHandler<TMessage> where TMessage : IMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Method to invoke when a batch of messages of type TMessage are published
|
||||
/// </summary>
|
||||
Task HandleBatchAsync(IReadOnlyCollection<TMessage> message);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Messages
|
||||
{
|
||||
public class MessageAggregator : IMessageAggregator
|
||||
{
|
||||
readonly ConcurrentBag<IMessageSubscription> Subscription = new ConcurrentBag<IMessageSubscription>();
|
||||
readonly IServiceProvider ServiceProvider;
|
||||
|
||||
|
||||
public MessageAggregator(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage
|
||||
{
|
||||
IEnumerable<IMessageSubscription> subscriptions = Subscription.Where(s => s.CanDeliver<TMessage>());
|
||||
|
||||
foreach (IMessageSubscription subscription in subscriptions)
|
||||
{
|
||||
await subscription.Deliver(ServiceProvider, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Subscribe<TMessage, TMessageHandler>()
|
||||
where TMessage : class, IMessage
|
||||
where TMessageHandler : IMessageHandler<TMessage>
|
||||
{
|
||||
Subscription.Add(new MessageSubscription<TMessage, TMessageHandler>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IMessageAggregator
|
||||
{
|
||||
/// <summary>
|
||||
/// Publishes the specified message, invoking any handlers subscribed to the message.
|
||||
/// </summary>
|
||||
Task Publish<TMessage>(TMessage message) where TMessage : class, IMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes the specified handler to the spified message type
|
||||
/// </summary>
|
||||
void Subscribe<TMessage, TMessageHandler>()
|
||||
where TMessage : class, IMessage
|
||||
where TMessageHandler : IMessageHandler<TMessage>;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Messages
|
||||
{
|
||||
internal class MessageSubscription<TMessage, TMessageHandler> : IMessageSubscription
|
||||
where TMessage : class, IMessage
|
||||
where TMessageHandler : IMessageHandler<TMessage>
|
||||
{
|
||||
public bool CanDeliver<T>()
|
||||
{
|
||||
return typeof(TMessage).GetTypeInfo().IsAssignableFrom(typeof(T));
|
||||
}
|
||||
|
||||
public async Task Deliver(IServiceProvider serviceProvider, object message)
|
||||
{
|
||||
if (message == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
}
|
||||
if (!(message is TMessage))
|
||||
{
|
||||
throw new ArgumentException($"{ nameof(message) } must be of type '{typeof(TMessage).FullName}'");
|
||||
}
|
||||
|
||||
var handler = serviceProvider.GetRequiredService<TMessageHandler>();
|
||||
await handler.Handle((TMessage)message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IMessageSubscription
|
||||
{
|
||||
bool CanDeliver<TMessage>();
|
||||
|
||||
Task Deliver(IServiceProvider serviceProvider, object message);
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace zero.Core
|
||||
{
|
||||
public class Paths : IPaths
|
||||
{
|
||||
public string WebRoot { get; set; }
|
||||
|
||||
public string ContentRoot { get; set; }
|
||||
|
||||
public string SecureRoot { get; set; }
|
||||
|
||||
public string Media { get; set; }
|
||||
|
||||
protected IWebHostEnvironment Env { get; set; }
|
||||
|
||||
private bool IsDebug { get; set; }
|
||||
|
||||
public const string MEDIA_FOLDER = "Uploads";
|
||||
|
||||
public const char PATH_SEPARATOR = '/';
|
||||
|
||||
private static char[] InvalidFilenameChars = null;
|
||||
|
||||
private const char REPLACEMENT_CHAR = '-';
|
||||
|
||||
private FileExtensionContentTypeProvider FileExtensionContentTypeProvider { get; set; }
|
||||
|
||||
private static Dictionary<char, string> Replacements = new Dictionary<char, string>()
|
||||
{
|
||||
{ 'ä', "ae" },
|
||||
{ 'ü', "ue" },
|
||||
{ 'ö', "oe" },
|
||||
{ 'ß', "ss" }
|
||||
};
|
||||
|
||||
|
||||
public Paths(IWebHostEnvironment env, bool isDebug)
|
||||
{
|
||||
Env = env;
|
||||
IsDebug = isDebug;
|
||||
WebRoot = env.WebRootPath;
|
||||
ContentRoot = env.ContentRootPath;
|
||||
SecureRoot = Path.Combine(ContentRoot, "wwwroot.secure");
|
||||
Media = Path.Combine(WebRoot, MEDIA_FOLDER);
|
||||
FileExtensionContentTypeProvider = new();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Combine a path
|
||||
/// </summary>
|
||||
public string Combine(params string[] paths)
|
||||
{
|
||||
return Path.Combine(paths);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
public string Map(string path)
|
||||
{
|
||||
return Path.Combine(WebRoot, path);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
public string MapSecure(string path)
|
||||
{
|
||||
return Path.Combine(SecureRoot, path);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
public string Map(params string[] paths)
|
||||
{
|
||||
return Path.Combine(WebRoot, Path.Combine(paths));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
public string MapSecure(params string[] paths)
|
||||
{
|
||||
return Path.Combine(SecureRoot, Path.Combine(paths));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Create a directory if it does not exist yet
|
||||
/// </summary>
|
||||
public void Create(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get content type for a filename
|
||||
/// </summary>
|
||||
public string GetContentType(string filename, string fallback = "application/octet-stream")
|
||||
{
|
||||
if (filename == null || !FileExtensionContentTypeProvider.TryGetContentType(Path.GetFileName(filename), out string contentType))
|
||||
{
|
||||
contentType = fallback;
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a filename and removes invalid chars
|
||||
/// </summary>
|
||||
public string ToFilename(string value)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// lowercase for string
|
||||
value = value.ToLower();
|
||||
|
||||
StringBuilder sb = new StringBuilder(value.Length);
|
||||
|
||||
var invalids = InvalidFilenameChars ?? (InvalidFilenameChars = Path.GetInvalidFileNameChars());
|
||||
bool changed = false;
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
char c = value[i];
|
||||
|
||||
if (Replacements.ContainsKey(c))
|
||||
{
|
||||
changed = true;
|
||||
sb.Append(Replacements[c]);
|
||||
}
|
||||
else if (invalids.Contains(c))
|
||||
{
|
||||
changed = true;
|
||||
sb.Append(REPLACEMENT_CHAR);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length == 0)
|
||||
{
|
||||
return "_";
|
||||
}
|
||||
|
||||
return changed ? sb.ToString() : value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IPaths
|
||||
{
|
||||
string ContentRoot { get; set; }
|
||||
|
||||
string WebRoot { get; set; }
|
||||
|
||||
string SecureRoot { get; set; }
|
||||
|
||||
string Media { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Combine a path
|
||||
/// </summary>
|
||||
string Combine(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
string Map(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
string MapSecure(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Map a path
|
||||
/// </summary>
|
||||
string Map(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// Map a secure path
|
||||
/// </summary>
|
||||
string MapSecure(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// Create a directory if it does not exist yet
|
||||
/// </summary>
|
||||
void Create(string directory);
|
||||
|
||||
/// <summary>
|
||||
/// Get content type for a filename
|
||||
/// </summary>
|
||||
string GetContentType(string filename, string fallback = "application/octet-stream");
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a filename and removes invalid chars
|
||||
/// </summary>
|
||||
string ToFilename(string value);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero.Core.Plugins
|
||||
{
|
||||
internal interface IZeroBuiltInPlugin
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using zero.Core.Options;
|
||||
|
||||
namespace zero.Core.Plugins
|
||||
{
|
||||
public abstract class ZeroPlugin : IZeroPlugin
|
||||
{
|
||||
public IZeroPluginOptions Options { get; } = new ZeroPluginOptions();
|
||||
|
||||
public virtual void ConfigureServices(IServiceCollection services, IConfiguration configuration) { }
|
||||
|
||||
public virtual void Configure(IZeroOptions zero) { }
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroPlugin
|
||||
{
|
||||
IZeroPluginOptions Options { get; }
|
||||
|
||||
void ConfigureServices(IServiceCollection services, IConfiguration configuration);
|
||||
|
||||
void Configure(IZeroOptions zero);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Plugins
|
||||
{
|
||||
public class ZeroPluginOptions : IZeroPluginOptions
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<string> LocalizationPaths { get; private set; } = new List<string>();
|
||||
|
||||
public string PluginPath { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroPluginOptions
|
||||
{
|
||||
string Name { get; set; }
|
||||
|
||||
string Description { get; set; }
|
||||
|
||||
List<string> LocalizationPaths { get; }
|
||||
|
||||
string PluginPath { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public interface IZeroPluginStartup
|
||||
{
|
||||
Task Startup();
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace zero.Core.Renderer
|
||||
{
|
||||
public class RazorRenderer : IRazorRenderer, IDisposable
|
||||
{
|
||||
protected IRazorViewEngine ViewEngine { get; set; }
|
||||
|
||||
protected ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; }
|
||||
|
||||
protected IModelMetadataProvider ModelMetadataProvider { get; set; }
|
||||
|
||||
protected IHttpContextAccessor HttpContextAccessor { get; set; }
|
||||
|
||||
protected IServiceScope ServiceScope { get; set; }
|
||||
|
||||
protected HtmlHelperOptions HtmlHelperOptions { get; set; }
|
||||
|
||||
|
||||
public RazorRenderer(IRazorViewEngine viewEngine, IHttpContextAccessor httpContextAccessor, ITempDataDictionaryFactory tempDataDictionaryFactory,
|
||||
IModelMetadataProvider modelMetadataProvider, IServiceProvider serviceProvider, IOptions<MvcViewOptions> mvcHelperOptions)
|
||||
{
|
||||
ViewEngine = viewEngine;
|
||||
HttpContextAccessor = httpContextAccessor;
|
||||
TempDataDictionaryFactory = tempDataDictionaryFactory;
|
||||
ModelMetadataProvider = modelMetadataProvider;
|
||||
ServiceScope = serviceProvider.CreateScope();
|
||||
HtmlHelperOptions = mvcHelperOptions.Value.HtmlHelperOptions;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync<T>(object args = null) where T : ViewComponent
|
||||
{
|
||||
return await ComponentAsync(typeof(T), args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(Type componentType, object args = null)
|
||||
{
|
||||
return await ComponentAsync(componentType, BuildActionContext(), args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(string componentName, object args = null)
|
||||
{
|
||||
return await ComponentAsync(componentName, BuildActionContext(), args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync<T>(ActionContext context, object args = null) where T : ViewComponent
|
||||
{
|
||||
return await ComponentAsync(typeof(T), context, args);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(Type componentType, ActionContext context, object args = null)
|
||||
{
|
||||
IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
|
||||
|
||||
using StringWriter stringWriter = new();
|
||||
|
||||
ViewContext viewContext = BuildViewContext(context, stringWriter);
|
||||
|
||||
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
|
||||
IHtmlContent result = await viewComponentHelper.InvokeAsync(componentType, args);
|
||||
|
||||
result.WriteTo(stringWriter, HtmlEncoder.Default);
|
||||
await stringWriter.FlushAsync();
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
public async Task<string> ComponentAsync(string componentName, ActionContext context, object args = null)
|
||||
{
|
||||
IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService<IViewComponentHelper>();
|
||||
|
||||
using StringWriter stringWriter = new();
|
||||
|
||||
ViewContext viewContext = BuildViewContext(context, stringWriter);
|
||||
|
||||
(viewComponentHelper as IViewContextAware)?.Contextualize(viewContext);
|
||||
IHtmlContent result = await viewComponentHelper.InvokeAsync(componentName, args);
|
||||
|
||||
result.WriteTo(stringWriter, HtmlEncoder.Default);
|
||||
await stringWriter.FlushAsync();
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
public async Task<string> ViewAsync(string view, object model = null)
|
||||
{
|
||||
return await ViewAsync(BuildActionContext(), view, model);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
public async Task<string> ViewAsync(ActionContext context, string view, object model = null)
|
||||
{
|
||||
IView viewResult = FindView(context, view);
|
||||
|
||||
using StringWriter stringWriter = new();
|
||||
|
||||
ViewContext viewContext = BuildViewContext(context, stringWriter, viewResult);
|
||||
viewContext.RouteData = context.RouteData;
|
||||
viewContext.ViewData.Model = model;
|
||||
|
||||
await viewResult.RenderAsync(viewContext);
|
||||
await stringWriter.FlushAsync();
|
||||
|
||||
return stringWriter.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
ServiceScope?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Build the view context
|
||||
/// </summary>
|
||||
protected virtual ViewContext BuildViewContext(ActionContext context, StringWriter writer, IView view = null)
|
||||
{
|
||||
ViewDataDictionary viewData = new(ModelMetadataProvider, context.ModelState); // result.ViewData ?? new ViewData...;
|
||||
ITempDataDictionary tempData = TempDataDictionaryFactory.GetTempData(context.HttpContext); // result.TempData ?? TempData...;
|
||||
|
||||
return new ViewContext(context, view ?? NullView.Instance, viewData, tempData, writer, HtmlHelperOptions);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Builds a new view context
|
||||
/// </summary>
|
||||
protected virtual ActionContext BuildActionContext()
|
||||
{
|
||||
HttpContext context = GetHttpContext();
|
||||
RouteData routeData = context.GetRouteData();
|
||||
return new ActionContext(context, routeData, new ActionDescriptor());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get HTTP context or mock one
|
||||
/// </summary>
|
||||
protected virtual HttpContext GetHttpContext()
|
||||
{
|
||||
HttpContext context = HttpContextAccessor.HttpContext;
|
||||
context ??= new DefaultHttpContext()
|
||||
{
|
||||
RequestServices = ServiceScope.ServiceProvider
|
||||
};
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find a view
|
||||
/// </summary>
|
||||
protected virtual IView FindView(ActionContext actionContext, string viewName)
|
||||
{
|
||||
ViewEngineResult getViewResult = ViewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false);
|
||||
if (getViewResult.Success)
|
||||
{
|
||||
return getViewResult.View;
|
||||
}
|
||||
|
||||
ViewEngineResult findViewResult = ViewEngine.FindView(actionContext, viewName, isMainPage: false);
|
||||
if (findViewResult.Success)
|
||||
{
|
||||
return findViewResult.View;
|
||||
}
|
||||
|
||||
IEnumerable<string> searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
|
||||
string errorMessage = String.Join(Environment.NewLine, new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));
|
||||
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IRazorRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync<T>(object args = null) where T : ViewComponent;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(Type componentType, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(string componentName, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync<T>(ActionContext context, object args = null) where T : ViewComponent;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(Type componentType, ActionContext context, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor component to a string
|
||||
/// </summary>
|
||||
Task<string> ComponentAsync(string componentName, ActionContext context, object args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
Task<string> ViewAsync(string view, object model = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a razor view to a string
|
||||
/// </summary>
|
||||
Task<string> ViewAsync(ActionContext context, string view, object model = null);
|
||||
}
|
||||
|
||||
|
||||
class GenericController : ControllerBase { }
|
||||
|
||||
|
||||
class NullView : IView
|
||||
{
|
||||
public static readonly NullView Instance = new NullView();
|
||||
|
||||
public string Path => string.Empty;
|
||||
|
||||
public Task RenderAsync(ViewContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Renderer
|
||||
{
|
||||
public class TokenReplacement
|
||||
{
|
||||
static Regex TokenRegex;
|
||||
|
||||
|
||||
static TokenReplacement()
|
||||
{
|
||||
TokenRegex = new Regex("{([\\w-_.]+)}", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
|
||||
public static string Apply(string text, Dictionary<string, string> tokens)
|
||||
{
|
||||
if (text.IsNullOrWhiteSpace())
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
MatchCollection matches = TokenRegex.Matches(text);
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string original = match.Value;
|
||||
string token = match.Groups[1].Value;
|
||||
|
||||
if (tokens.ContainsKey(token))
|
||||
{
|
||||
text = text.Replace(original, tokens[token]);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
|
||||
//foreach ((string key, string value) in tokens)
|
||||
//{
|
||||
// string tokenKey = key.EnsureStartsWith(BEGIN).EnsureEndsWith(END);
|
||||
// string tokenValue = value; // TODO escape for HTML?
|
||||
|
||||
// text = text.Replace(tokenKey, value);
|
||||
//}
|
||||
|
||||
//return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using zero.Core.Attributes;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Renderer;
|
||||
|
||||
namespace zero.Core.Services
|
||||
{
|
||||
public class Localizer : ILocalizer
|
||||
{
|
||||
protected Dictionary<string, string> Cache { get; private set; } = new();
|
||||
|
||||
protected IZeroStore Store { get; private set; }
|
||||
|
||||
|
||||
public Localizer(IZeroStore store)
|
||||
{
|
||||
Store = store;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text(string key) => Text(key, null);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text(string key, Dictionary<string, string> tokens)
|
||||
{
|
||||
if (key.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Cache.TryGetValue(key, out string value))
|
||||
{
|
||||
Translation translation = LoadTranslation(key);
|
||||
|
||||
if (translation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
value = translation.Value;
|
||||
|
||||
lock (Cache) // TOOD use concurrent dictionary
|
||||
{
|
||||
Cache[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (tokens != null)
|
||||
{
|
||||
value = TokenReplacement.Apply(value, tokens);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text<T>(T enumValue) where T : Enum => Text(enumValue, null);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text<T>(T enumValue, Dictionary<string, string> tokens) where T : Enum
|
||||
{
|
||||
Type type = enumValue.GetType();
|
||||
MemberInfo memInfo = type.GetMember(enumValue.ToString())[0];
|
||||
return Text(memInfo.GetCustomAttribute<LocalizeAttribute>()?.Key, tokens);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Maybe(string key) => Maybe(key, null);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Maybe(string key, Dictionary<string, string> tokens)
|
||||
{
|
||||
return key.IsNullOrEmpty() || !key.StartsWith("@") ? key : Text(key.Substring(1), tokens);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get translation from database or any other source
|
||||
/// </summary>
|
||||
protected virtual Translation LoadTranslation(string key)
|
||||
{
|
||||
return Store.Session().Query<Translation>().FirstOrDefault(x => x.Key == key); // TODO I guess this throws ^^
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILocalizer
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
string Text(string key);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
string Text(string key, Dictionary<string, string> tokens);
|
||||
|
||||
/// <summary>
|
||||
/// Get a text string from a [Localize] attribute
|
||||
/// </summary>
|
||||
string Text<T>(T enumValue) where T : Enum;
|
||||
|
||||
/// <summary>
|
||||
/// Get a text string from a [Localize] attribute
|
||||
/// </summary>
|
||||
string Text<T>(T enumValue, Dictionary<string, string> tokens) where T : Enum;
|
||||
|
||||
/// <summary>
|
||||
/// Only tries to resolve the key when it is prefixed with an @
|
||||
/// </summary>
|
||||
string Maybe(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Only tries to resolve the key when it is prefixed with an @
|
||||
/// </summary>
|
||||
string Maybe(string key, Dictionary<string, string> tokens);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using FluentValidation;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Validation
|
||||
{
|
||||
public class ApplicationValidator : ZeroValidator<Application>
|
||||
{
|
||||
public ApplicationValidator()
|
||||
{
|
||||
//RuleFor(x => x.Code).Length(2);
|
||||
RuleFor(x => x.Name).NotEmpty().Length(2, 50);
|
||||
RuleFor(x => x.FullName).MaximumLength(120);
|
||||
RuleFor(x => x.Email).Email().NotEmpty().MaximumLength(120);
|
||||
RuleFor(x => x.Domains).NotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using FluentValidation;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Validation
|
||||
{
|
||||
public class BackofficeUserValidator : ZeroValidator<BackofficeUser>
|
||||
{
|
||||
public BackofficeUserValidator(bool isCreate = false)
|
||||
{
|
||||
RuleFor(x => x.Name).Length(2, 80);
|
||||
RuleFor(x => x.Email).NotEmpty().Email().MaximumLength(120);
|
||||
RuleFor(x => x.PasswordHash).NotEmpty();
|
||||
RuleFor(x => x.LanguageId).NotEmpty();
|
||||
RuleFor(x => x.RoleIds).NotEmpty();
|
||||
|
||||
if (isCreate)
|
||||
{
|
||||
RuleFor(x => x.IsSuper).Equals(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using FluentValidation;
|
||||
using zero.Core.Entities.Setup;
|
||||
using zero.Core.Extensions;
|
||||
|
||||
namespace zero.Core.Validation
|
||||
{
|
||||
public class SetupModelValidator : ZeroValidator<SetupModel>
|
||||
{
|
||||
public SetupModelValidator()
|
||||
{
|
||||
RuleFor(x => x.User).NotNull();
|
||||
RuleFor(x => x.User.Email).NotEmpty().Email();
|
||||
RuleFor(x => x.User.Name).MaximumLength(40).NotEmpty();
|
||||
RuleFor(x => x.User.Password).MaximumLength(1024); // TODO password policy
|
||||
|
||||
RuleFor(x => x.AppName).MaximumLength(40).NotEmpty();
|
||||
|
||||
RuleFor(x => x.Database.Url).NotEmpty().Url();
|
||||
RuleFor(x => x.Database.Name).NotEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using FluentValidation;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using System.Linq;
|
||||
using System;
|
||||
|
||||
namespace zero.Core.Validation
|
||||
{
|
||||
public class UserRoleValidator : ZeroValidator<BackofficeUserRole>
|
||||
{
|
||||
const string SECTION_CLAIM = "section.";
|
||||
|
||||
const string TRUE_CLAIM_VALUE = ":true";
|
||||
|
||||
public UserRoleValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).Length(2, 80);
|
||||
RuleFor(x => x.Description).MaximumLength(200);
|
||||
RuleFor(x => x.Icon).NotEmpty();
|
||||
|
||||
RuleFor(x => x.Claims).NotEmpty().Must((role, claims, context) =>
|
||||
{
|
||||
foreach (UserClaim claim in claims)
|
||||
{
|
||||
if (claim.Value.StartsWith(SECTION_CLAIM, StringComparison.InvariantCultureIgnoreCase) && claim.Value.EndsWith(TRUE_CLAIM_VALUE, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}).WithMessage("@errors.role.nosection");
|
||||
|
||||
RuleForEach(x => x.Claims).Must((role, claim, context) =>
|
||||
{
|
||||
return !claim.Type.IsNullOrEmpty() && !claim.Value.IsNullOrEmpty();
|
||||
}).WithMessage("@errors.role.emptyclaim");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Raven.Client.Documents.Session;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Cultures;
|
||||
using zero.Core.Database;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Handlers;
|
||||
using zero.Core.Options;
|
||||
using zero.Core.Routing;
|
||||
using zero.Core.Utils;
|
||||
|
||||
namespace zero.Core
|
||||
{
|
||||
public class ZeroContext : IZeroContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Application Application { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string AppId { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ClaimsPrincipal BackofficeUser { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsBackofficeRequest { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLoggedIntoBackoffice { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroOptions Options { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Route Route => ResolvedRoute?.Route;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRouteModel ResolvedRoute => HttpContextAccessor?.HttpContext?.Features.Get<IRouteModel>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IZeroStore Store { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IServiceProvider Services { get; private set; }
|
||||
|
||||
|
||||
protected IApplicationResolver AppResolver { get; private set; }
|
||||
|
||||
protected ICultureResolver CultureResolver { get; private set; }
|
||||
|
||||
protected ILogger<ZeroContext> Logger { get; private set; }
|
||||
|
||||
protected IHandlerHolder Handler { get; private set; }
|
||||
|
||||
protected IHttpContextAccessor HttpContextAccessor { get; private set; }
|
||||
|
||||
protected IPrimitiveTypeCollection ValueCollection { get; private set; }
|
||||
|
||||
|
||||
bool _resolved = false;
|
||||
|
||||
|
||||
public ZeroContext(IZeroOptions options, IHttpContextAccessor httpContextAccessor, IApplicationResolver appResolver, ICultureResolver cultureResolver,
|
||||
ILogger<ZeroContext> logger, IZeroStore store, IHandlerHolder handler, IServiceProvider services)
|
||||
{
|
||||
Options = options;
|
||||
AppResolver = appResolver;
|
||||
CultureResolver = cultureResolver;
|
||||
Logger = logger;
|
||||
Store = store;
|
||||
Handler = handler;
|
||||
ValueCollection = new PrimitiveTypeCollection();
|
||||
HttpContextAccessor = httpContextAccessor;
|
||||
Services = services;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async virtual Task Resolve(HttpContext context)
|
||||
{
|
||||
if (_resolved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context?.Request is null)
|
||||
{
|
||||
Store.ResolvedDatabase = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Options.SetupCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_resolved = true;
|
||||
|
||||
// check if the current request is a backoffice request
|
||||
IsBackofficeRequest = context.IsBackofficeRequest(Options.BackofficePath);
|
||||
|
||||
// get the currently logged-in backoffice user
|
||||
BackofficeUser = new ClaimsPrincipal();
|
||||
IsLoggedIntoBackoffice = false;
|
||||
AuthenticateResult authResult = await context.AuthenticateAsync(Constants.Auth.BackofficeScheme);
|
||||
if (authResult?.Principal is not null)
|
||||
{
|
||||
BackofficeUser = authResult.Principal;
|
||||
IsLoggedIntoBackoffice = true;
|
||||
}
|
||||
|
||||
// resolve current application
|
||||
Application = await AppResolver.Resolve(context, BackofficeUser);
|
||||
AppId = Application.Id;
|
||||
|
||||
// set default database for document store
|
||||
Store.ResolvedDatabase = Application.Database;
|
||||
|
||||
// set current culture
|
||||
await CultureResolver.Resolve(this);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Override(Application app)
|
||||
{
|
||||
Application = app;
|
||||
AppId = app?.Id;
|
||||
Store.ResolvedDatabase = app?.Database;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public ZeroContextScope CreateScope(Application app)
|
||||
{
|
||||
return new(this, app, Application);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public T Get<T>() => ValueCollection.Get<T>();
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Set<T>(T value) => ValueCollection.Set(value);
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Remove<T>() => ValueCollection.Remove<T>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class ZeroContextScope : IDisposable
|
||||
{
|
||||
public IZeroContext Context { get; }
|
||||
|
||||
public Application App { get; set; }
|
||||
|
||||
public string Database { get; set; }
|
||||
|
||||
public IZeroStore Store { get; set; }
|
||||
|
||||
Application _originalApp = null;
|
||||
|
||||
|
||||
internal ZeroContextScope(IZeroContext context, Application app, Application originalApp)
|
||||
{
|
||||
Context = context;
|
||||
App = app;
|
||||
Database = app.Database;
|
||||
Store = context.Store;
|
||||
_originalApp = originalApp;
|
||||
Context.Override(app);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Context.Override(_originalApp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public interface IZeroContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently loaded application
|
||||
/// </summary>
|
||||
Application Application { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current loaded application Id
|
||||
/// </summary>
|
||||
string AppId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolved backoffice user principal
|
||||
/// </summary>
|
||||
ClaimsPrincipal BackofficeUser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current request is a backoffice request or not
|
||||
/// </summary>
|
||||
bool IsBackofficeRequest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user is logged into the backoffice
|
||||
/// </summary>
|
||||
bool IsLoggedIntoBackoffice { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Global zero options
|
||||
/// </summary>
|
||||
IZeroOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Document store
|
||||
/// </summary>
|
||||
IZeroStore Store { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Service container
|
||||
/// </summary>
|
||||
IServiceProvider Services { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Matching (frontend) path route
|
||||
/// </summary>
|
||||
Route Route { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Matching (frontend) resolved route
|
||||
/// </summary>
|
||||
IRouteModel ResolvedRoute { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the current application (for backoffice + frontend requests) and
|
||||
/// the currently active backoffice user, as users are not signed in with the default scheme and do therefore not populate HttpContext.User
|
||||
/// </summary>
|
||||
Task Resolve(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the resolved application for this context instance
|
||||
/// </summary>
|
||||
void Override(Application app);
|
||||
|
||||
/// <summary>
|
||||
/// SCOPE
|
||||
/// </summary>
|
||||
ZeroContextScope CreateScope(Application app);
|
||||
|
||||
/// <summary>
|
||||
/// Get a custom property from this scoped context
|
||||
/// </summary>
|
||||
T Get<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Add a custom property to this scoped context
|
||||
/// </summary>
|
||||
void Set<T>(T value);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a custom property from this scoped context
|
||||
/// </summary>
|
||||
void Remove<T>();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Raven.Client.Documents;
|
||||
using Raven.Client.Documents.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Collections;
|
||||
using zero.Core.Database.Indexes;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Extensions;
|
||||
using zero.Core.Identity;
|
||||
|
||||
namespace zero.Web.Controllers
|
||||
{
|
||||
[ZeroAuthorize(Permissions.Settings.Countries, PermissionsValue.Read)]
|
||||
public class CountriesController : ZeroBackofficeCollectionController<Country, ICountriesCollection>
|
||||
{
|
||||
public CountriesController(ICountriesCollection collection) : base(collection)
|
||||
{
|
||||
PreviewTransform = (item, model) => model.Icon = "flag-" + item.Code.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public override Task<ListResult<Country>> GetByQuery([FromQuery] ListBackofficeQuery<Country> query)
|
||||
{
|
||||
query.OrderQuery = q => q.OrderByDescending(x => x.IsPreferred).ThenBy(x => x.Name);
|
||||
return Collection.Load<zero_Countries>(query);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
using System;
|
||||
using zero.Core.Assemblies;
|
||||
|
||||
namespace zero.Web.Defaults
|
||||
{
|
||||
public class ZeroAssemblyDiscoveryRule : IAssemblyDiscoveryRule
|
||||
{
|
||||
const string ZERO_PREFIX = "zero.";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsValid(RuntimeLibrary library, AssemblyDiscoveryContext context)
|
||||
{
|
||||
StringComparison casing = StringComparison.OrdinalIgnoreCase;
|
||||
// TODO we need to auto-add assemblies and discover their types which have implementations of IZeroPlugin
|
||||
return library.Name.StartsWith(ZERO_PREFIX, casing) || (context.HasEntryAssembly && library.Name.Contains(context.EntryAssemblyName, casing));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,6 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using zero.Core;
|
||||
using zero.Core.Api;
|
||||
using zero.Core.Blueprints;
|
||||
using zero.Core.Collections;
|
||||
using zero.Core.Entities;
|
||||
using zero.Core.Integrations;
|
||||
using zero.Core.Mails;
|
||||
using zero.Core.Messages;
|
||||
using zero.Core.Options;
|
||||
using zero.Core.Plugins;
|
||||
using zero.Core.Renderer;
|
||||
using zero.Core.Routing;
|
||||
using zero.Core.Services;
|
||||
using zero.Core.Tokens;
|
||||
using zero.Core.Validation;
|
||||
using zero.Web.Sections;
|
||||
using zero.Web.ViewHelpers;
|
||||
|
||||
@@ -61,22 +44,6 @@ namespace zero.Web.Defaults
|
||||
{
|
||||
//services.AddAll(typeof(IValidator<>), ServiceLifetime.Scoped);
|
||||
//services.AddAll(typeof(IValidator), ServiceLifetime.Scoped);
|
||||
services.AddSingleton<IMessageAggregator, MessageAggregator>();
|
||||
|
||||
services.AddScoped<IRazorRenderer, RazorRenderer>();
|
||||
|
||||
services.AddTransient<Application, Application>();
|
||||
services.AddTransient<Translation, Translation>();
|
||||
services.AddTransient<Page, Page>();
|
||||
services.AddTransient<RecycledEntity, RecycledEntity>();
|
||||
services.AddTransient<Preview, Preview>();
|
||||
services.AddTransient<Core.Routing.Route, Core.Routing.Route>();
|
||||
|
||||
services.AddTransient<IValidator<Application>, ApplicationValidator>();
|
||||
services.AddTransient<IValidator<Translation>, TranslationValidator>();
|
||||
services.AddTransient<IValidator<Page>, PageValidator>();
|
||||
services.AddTransient<IValidator<BackofficeUserRole>, UserRoleValidator>();
|
||||
services.AddTransient<IValidator<BackofficeUser>, BackofficeUserValidator>();
|
||||
|
||||
services.AddTransient<ICountriesCollection, CountriesCollection>();
|
||||
services.AddTransient<ILanguagesCollection, LanguagesCollection>();
|
||||
@@ -98,23 +65,7 @@ namespace zero.Web.Defaults
|
||||
services.AddTransient<ISpacesApi, SpacesApi>();
|
||||
services.AddTransient<IPermissionsApi, PermissionsApi>();
|
||||
services.AddTransient<IModulesApi, ModulesApi>();
|
||||
services.AddTransient<IRecycleBinApi, RecycleBinApi>();
|
||||
|
||||
services.AddScoped<IRequestUrlResolver, RequestUrlResolver>();
|
||||
services.AddScoped<IRoutes, Routes>();
|
||||
services.AddScoped<IRouteResolver, RouteResolver>();
|
||||
services.AddScoped<IRedirectAutomation, RedirectAutomation>();
|
||||
services.AddScoped<IRouteRedirectCollection, RouteRedirectCollection>();
|
||||
services.AddScoped<IPageUrlBuilder, PageUrlBuilder>();
|
||||
services.AddScoped<IRouteProvider, PageRouteProvider>();
|
||||
services.AddScoped<ILinks, Links>();
|
||||
services.AddScoped<ILinkProvider, PageLinkProvider>();
|
||||
services.AddScoped<ILinkProvider, RawLinkProvider>();
|
||||
services.AddScoped<ZeroRoutesTransformer>();
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, NotFoundSelectorPolicy>());
|
||||
|
||||
services.AddScoped<IMailProvider, MailProvider>();
|
||||
services.AddScoped<IMailDispatcher, FileMailDispatcher>();
|
||||
services.AddTransient<IRecycleBinApi, RecycleBinApi>();
|
||||
|
||||
services.AddScoped<IZeroMediaHelper, ZeroMediaHelper>();
|
||||
|
||||
@@ -125,16 +76,7 @@ namespace zero.Web.Defaults
|
||||
services.AddScoped(typeof(ICollectionContext<>), typeof(CollectionContext<>));
|
||||
services.AddScoped(typeof(IInterceptorRunner<>), typeof(InterceptorRunner<>));
|
||||
|
||||
services.AddScoped<ILocalizer, Localizer>();
|
||||
services.AddScoped<IZeroTokenProvider, ZeroTokenProvider>();
|
||||
|
||||
services.AddScoped<IBackofficeSearchService, BackofficeSearchService>();
|
||||
|
||||
services.AddScoped<IBlueprintService, BlueprintService>();
|
||||
services.AddScoped<BlueprintInterceptor>();
|
||||
services.AddScoped<BlueprintChildInterceptor>();
|
||||
|
||||
services.AddScoped<ZeroEntityRouteInterceptor>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
// Copyright (c) .NET Foundation Contributors. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Original Source: https://github.com/aspnet/JavaScriptServices
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace Zero.Web.DevServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="StreamReader"/> to expose an evented API, issuing notifications
|
||||
/// when the stream emits partial lines, completed lines, or finally closes.
|
||||
/// </summary>
|
||||
internal class EventedStreamReader
|
||||
{
|
||||
public delegate void OnReceivedChunkHandler(ArraySegment<char> chunk);
|
||||
public delegate void OnReceivedLineHandler(string line);
|
||||
public delegate void OnStreamClosedHandler();
|
||||
|
||||
public event OnReceivedChunkHandler OnReceivedChunk;
|
||||
public event OnReceivedLineHandler OnReceivedLine;
|
||||
public event OnStreamClosedHandler OnStreamClosed;
|
||||
|
||||
private readonly StreamReader _streamReader;
|
||||
private readonly StringBuilder _linesBuffer;
|
||||
|
||||
public EventedStreamReader(StreamReader streamReader)
|
||||
{
|
||||
_streamReader = streamReader ?? throw new ArgumentNullException(nameof(streamReader));
|
||||
_linesBuffer = new StringBuilder();
|
||||
Task.Factory.StartNew(Run);
|
||||
}
|
||||
|
||||
public Task<Match> WaitForMatch(Regex regex)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Match>();
|
||||
var completionLock = new object();
|
||||
|
||||
OnReceivedLineHandler onReceivedLineHandler = null;
|
||||
OnStreamClosedHandler onStreamClosedHandler = null;
|
||||
|
||||
void ResolveIfStillPending(Action applyResolution)
|
||||
{
|
||||
lock (completionLock)
|
||||
{
|
||||
if (!tcs.Task.IsCompleted)
|
||||
{
|
||||
OnReceivedLine -= onReceivedLineHandler;
|
||||
OnStreamClosed -= onStreamClosedHandler;
|
||||
applyResolution();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReceivedLineHandler = line =>
|
||||
{
|
||||
var match = regex.Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
ResolveIfStillPending(() => tcs.SetResult(match));
|
||||
}
|
||||
};
|
||||
|
||||
onStreamClosedHandler = () =>
|
||||
{
|
||||
ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException()));
|
||||
};
|
||||
|
||||
OnReceivedLine += onReceivedLineHandler;
|
||||
OnStreamClosed += onStreamClosedHandler;
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
|
||||
public Task WaitForFinish()
|
||||
{
|
||||
var tcs = new TaskCompletionSource();
|
||||
var completionLock = new object();
|
||||
|
||||
OnStreamClosedHandler onStreamClosedHandler = null;
|
||||
|
||||
void ResolveIfStillPending(Action applyResolution)
|
||||
{
|
||||
lock (completionLock)
|
||||
{
|
||||
if (!tcs.Task.IsCompleted)
|
||||
{
|
||||
OnStreamClosed -= onStreamClosedHandler;
|
||||
applyResolution();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onStreamClosedHandler = () =>
|
||||
{
|
||||
ResolveIfStillPending(() => tcs.SetResult());
|
||||
};
|
||||
|
||||
OnStreamClosed += onStreamClosedHandler;
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
|
||||
private async Task Run()
|
||||
{
|
||||
var buf = new char[8 * 1024];
|
||||
while (true)
|
||||
{
|
||||
var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
|
||||
if (chunkLength == 0)
|
||||
{
|
||||
OnClosed();
|
||||
break;
|
||||
}
|
||||
|
||||
OnChunk(new ArraySegment<char>(buf, 0, chunkLength));
|
||||
|
||||
int lineBreakPos = -1;
|
||||
int startPos = 0;
|
||||
|
||||
// get all the newlines
|
||||
while ((lineBreakPos = Array.IndexOf(buf, '\n', startPos, chunkLength - startPos)) >= 0 && startPos < chunkLength)
|
||||
{
|
||||
var length = lineBreakPos - startPos;
|
||||
_linesBuffer.Append(buf, startPos, length);
|
||||
OnCompleteLine(_linesBuffer.ToString());
|
||||
_linesBuffer.Clear();
|
||||
startPos = lineBreakPos + 1;
|
||||
}
|
||||
|
||||
// get the rest
|
||||
if (lineBreakPos < 0 && startPos < chunkLength)
|
||||
{
|
||||
_linesBuffer.Append(buf, startPos, chunkLength - startPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChunk(ArraySegment<char> chunk)
|
||||
{
|
||||
var dlg = OnReceivedChunk;
|
||||
dlg?.Invoke(chunk);
|
||||
}
|
||||
|
||||
private void OnCompleteLine(string line)
|
||||
{
|
||||
var dlg = OnReceivedLine;
|
||||
dlg?.Invoke(line);
|
||||
}
|
||||
|
||||
private void OnClosed()
|
||||
{
|
||||
var dlg = OnStreamClosed;
|
||||
dlg?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the completed-line notifications from a <see cref="EventedStreamReader"/>,
|
||||
/// combining the data into a single <see cref="string"/>.
|
||||
/// </summary>
|
||||
internal class EventedStreamStringReader : IDisposable
|
||||
{
|
||||
private EventedStreamReader _eventedStreamReader;
|
||||
private bool _isDisposed;
|
||||
private StringBuilder _stringBuilder = new StringBuilder();
|
||||
|
||||
public EventedStreamStringReader(EventedStreamReader eventedStreamReader)
|
||||
{
|
||||
_eventedStreamReader = eventedStreamReader
|
||||
?? throw new ArgumentNullException(nameof(eventedStreamReader));
|
||||
_eventedStreamReader.OnReceivedLine += OnReceivedLine;
|
||||
}
|
||||
|
||||
public string ReadAsString() => _stringBuilder.ToString();
|
||||
|
||||
private void OnReceivedLine(string line) => _stringBuilder.AppendLine(line);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_eventedStreamReader.OnReceivedLine -= OnReceivedLine;
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Zero.Web.DevServer
|
||||
{
|
||||
public static class PidUtils
|
||||
{
|
||||
const string ssPidRegex = @"(?:^|"",|"",pid=)(\d+)";
|
||||
const string portRegex = @"[^]*[.:](\\d+)$";
|
||||
|
||||
public static int GetPortPid(ushort port)
|
||||
{
|
||||
int pidOut = -1;
|
||||
|
||||
int portColumn = 1; // windows
|
||||
int pidColumn = 4; // windows
|
||||
string pidRegex = null;
|
||||
|
||||
List<string[]> results = null;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
results = RunProcessReturnOutputSplit("netstat", "-anv -p tcp");
|
||||
results.AddRange(RunProcessReturnOutputSplit("netstat", "-anv -p udp"));
|
||||
portColumn = 3;
|
||||
pidColumn = 8;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
results = RunProcessReturnOutputSplit("ss", "-tunlp");
|
||||
portColumn = 4;
|
||||
pidColumn = 6;
|
||||
pidRegex = ssPidRegex;
|
||||
}
|
||||
else
|
||||
{
|
||||
results = RunProcessReturnOutputSplit("netstat", "-ano");
|
||||
}
|
||||
|
||||
|
||||
foreach (var line in results)
|
||||
{
|
||||
if (line.Length <= portColumn || line.Length <= pidColumn) continue;
|
||||
try
|
||||
{
|
||||
// split lines to words
|
||||
var portMatch = Regex.Match(line[portColumn], $"[.:]({port})");
|
||||
if (portMatch.Success)
|
||||
{
|
||||
int portValue = int.Parse(portMatch.Groups[1].Value);
|
||||
|
||||
if (pidRegex == null)
|
||||
{
|
||||
pidOut = int.Parse(line[pidColumn]);
|
||||
return pidOut;
|
||||
}
|
||||
else
|
||||
{
|
||||
var pidMatch = Regex.Match(line[pidColumn], pidRegex);
|
||||
if (pidMatch.Success)
|
||||
{
|
||||
pidOut = int.Parse(pidMatch.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignore line error
|
||||
}
|
||||
}
|
||||
|
||||
return pidOut;
|
||||
}
|
||||
|
||||
private static List<string[]> RunProcessReturnOutputSplit(string fileName, string arguments)
|
||||
{
|
||||
string result = RunProcessReturnOutput(fileName, arguments);
|
||||
if (result == null) return new List<string[]>();
|
||||
|
||||
string[] lines = result.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lineWords = new List<string[]>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
lineWords.Add(line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
return lineWords;
|
||||
}
|
||||
|
||||
private static string RunProcessReturnOutput(string fileName, string arguments)
|
||||
{
|
||||
Process process = null;
|
||||
try
|
||||
{
|
||||
var si = new ProcessStartInfo(fileName, arguments)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
process = Process.Start(si);
|
||||
var stdOutT = process.StandardOutput.ReadToEndAsync();
|
||||
var stdErrorT = process.StandardError.ReadToEndAsync();
|
||||
if (!process.WaitForExit(10000))
|
||||
{
|
||||
try { process?.Kill(); } catch { }
|
||||
}
|
||||
|
||||
if (Task.WaitAll(new Task[] { stdOutT, stdErrorT }, 10000))
|
||||
{
|
||||
// if success, return data
|
||||
return (stdOutT.Result + Environment.NewLine + stdErrorT.Result).Trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
process?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Kill(string process, bool ignoreCase = true, bool force = false, bool tree = true)
|
||||
{
|
||||
var args = new List<string>();
|
||||
try
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
if (force) { args.Add("-9"); }
|
||||
if (ignoreCase) { args.Add("-i"); }
|
||||
args.Add(process);
|
||||
RunProcessReturnOutput("pkill", string.Join(" ", args));
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
if (force) { args.Add("-9"); }
|
||||
if (ignoreCase) { args.Add("-I"); }
|
||||
args.Add(process);
|
||||
RunProcessReturnOutput("killall", string.Join(" ", args));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (force) { args.Add("/f"); }
|
||||
if (tree) { args.Add("/T"); }
|
||||
args.Add("/im");
|
||||
args.Add(process);
|
||||
return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool Kill(int pid, bool force = false, bool tree = true)
|
||||
{
|
||||
if (pid == -1) { return false; }
|
||||
|
||||
var args = new List<string>();
|
||||
try
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
if (force) { args.Add("-9"); }
|
||||
args.Add(pid.ToString());
|
||||
RunProcessReturnOutput("kill", string.Join(" ", args));
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
if (force) { args.Add("-9"); }
|
||||
args.Add(pid.ToString());
|
||||
RunProcessReturnOutput("kill", string.Join(" ", args));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (force) { args.Add("/f"); }
|
||||
if (tree) { args.Add("/T"); }
|
||||
args.Add("/PID");
|
||||
args.Add(pid.ToString());
|
||||
return RunProcessReturnOutput("taskkill", string.Join(" ", args))?.StartsWith("SUCCESS") ?? false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool KillPort(ushort port, bool force = false, bool tree = true) => Kill(GetPortPid(port), force: force, tree: tree);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Plugins;
|
||||
|
||||
namespace Zero.Web.DevServer
|
||||
{
|
||||
public class PluginResolver
|
||||
{
|
||||
public IEnumerable<IZeroPlugin> Plugins { get; set; }
|
||||
|
||||
|
||||
public PluginResolver(IEnumerable<IZeroPlugin> plugins)
|
||||
{
|
||||
Plugins = plugins;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Zero.Web.DevServer
|
||||
{
|
||||
public class ProcessProxy
|
||||
{
|
||||
string workingDirectory = null;
|
||||
string script = null;
|
||||
Action<ProcessStartInfo> onProcessConfigure = null;
|
||||
Action<string, bool> captureLog = null;
|
||||
bool isWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
Dictionary<string, string> envVars = new();
|
||||
HashSet<string> arguments = new();
|
||||
bool forwardLog = false;
|
||||
|
||||
Process process = null;
|
||||
EventedStreamReader stdOut = null;
|
||||
EventedStreamReader stdErr = null;
|
||||
|
||||
|
||||
|
||||
|
||||
public ProcessProxy(string workingDirectory, string script, bool forwardLog = false)
|
||||
{
|
||||
this.workingDirectory = workingDirectory;
|
||||
this.script = script;
|
||||
this.forwardLog = forwardLog;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds an environment variable
|
||||
/// </summary>
|
||||
public ProcessProxy EnvVar(string key, string value)
|
||||
{
|
||||
envVars.Add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds an argument
|
||||
/// </summary>
|
||||
public ProcessProxy Argument(string argument)
|
||||
{
|
||||
arguments.Add(argument);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Configures the process start info
|
||||
/// </summary>
|
||||
public ProcessProxy Configure(Action<ProcessStartInfo> onProcessConfigure)
|
||||
{
|
||||
this.onProcessConfigure = onProcessConfigure;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Capture the log instead of outputting it to the console
|
||||
/// </summary>
|
||||
public ProcessProxy Capture(Action<string, bool> action)
|
||||
{
|
||||
this.captureLog = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Run the script and wait for completion.
|
||||
/// This is only recommended for scripts which finish automatically and have no user interaction.
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(TimeSpan timeout = default)
|
||||
{
|
||||
StartProcess();
|
||||
|
||||
using var stdErrReader = new EventedStreamStringReader(stdErr);
|
||||
try
|
||||
{
|
||||
// TODO implement timeout
|
||||
// https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource
|
||||
await stdOut.WaitForFinish();
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Run the script and return as soon as a condition is met.
|
||||
/// The script will not cancel and will continue to run as a sub-process as long as it does not auto-close.
|
||||
/// </summary>
|
||||
public async Task RunAsync(string startupCondition, TimeSpan startupTimeout = default)
|
||||
{
|
||||
StartProcess();
|
||||
|
||||
using var stdErrReader = new EventedStreamStringReader(stdErr);
|
||||
try
|
||||
{
|
||||
await stdOut.WaitForMatch(new Regex(startupCondition, RegexOptions.IgnoreCase, startupTimeout == default ? TimeSpan.FromMinutes(5) : startupTimeout));
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"The script '{script}' exited without indicating that the server was listening for requests.\nThe error output was: " + $"{stdErrReader.ReadAsString()}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
try { process?.Kill(); } catch { }
|
||||
try { process?.WaitForExit(); } catch { }
|
||||
|
||||
AppDomain.CurrentDomain.DomainUnload -= UnloadHandler;
|
||||
AppDomain.CurrentDomain.ProcessExit -= UnloadHandler;
|
||||
AppDomain.CurrentDomain.UnhandledException -= UnloadHandler;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Run
|
||||
/// </summary>
|
||||
Process StartProcess()
|
||||
{
|
||||
string executable = script;
|
||||
|
||||
StringBuilder command = new();
|
||||
command.Append(script);
|
||||
command.Append(' ');
|
||||
foreach (string arg in arguments)
|
||||
{
|
||||
command.Append(arg);
|
||||
}
|
||||
string argumentList = command.ToString();
|
||||
|
||||
if (isWindows)
|
||||
{
|
||||
argumentList = $"/c {argumentList}";
|
||||
executable = "cmd";
|
||||
}
|
||||
|
||||
ProcessStartInfo startInfo = new(executable)
|
||||
{
|
||||
Arguments = argumentList,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
WorkingDirectory = workingDirectory
|
||||
};
|
||||
|
||||
foreach (var envVar in envVars)
|
||||
{
|
||||
startInfo.Environment[envVar.Key] = envVar.Value;
|
||||
}
|
||||
|
||||
onProcessConfigure?.Invoke(startInfo);
|
||||
|
||||
try
|
||||
{
|
||||
process = Process.Start(startInfo);
|
||||
process.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = $"Failed to start '{startInfo.FileName}'. To resolve this:.\n\n"
|
||||
+ $"[1] Ensure that '{startInfo.FileName}' is installed and can be found in one of the PATH directories.\n"
|
||||
+ $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
|
||||
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n"
|
||||
+ "[2] See the InnerException for further details of the cause.";
|
||||
throw new InvalidOperationException(message, ex);
|
||||
}
|
||||
|
||||
AttachLogger();
|
||||
|
||||
AppDomain.CurrentDomain.DomainUnload += UnloadHandler;
|
||||
AppDomain.CurrentDomain.ProcessExit += UnloadHandler;
|
||||
AppDomain.CurrentDomain.UnhandledException += UnloadHandler;
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
|
||||
void UnloadHandler(object sender, EventArgs e)
|
||||
{
|
||||
Exit();
|
||||
}
|
||||
|
||||
|
||||
void AttachLogger(bool isStream = false)
|
||||
{
|
||||
stdOut = new EventedStreamReader(process.StandardOutput);
|
||||
stdErr = new EventedStreamReader(process.StandardError);
|
||||
|
||||
stdOut.OnReceivedLine += line => WriteToLog(line);
|
||||
stdOut.OnReceivedChunk += chunk => WriteToLog(chunk);
|
||||
stdErr.OnReceivedLine += line => WriteToLog(line, true);
|
||||
stdErr.OnReceivedChunk += chunk => WriteToLog(chunk, true);
|
||||
}
|
||||
|
||||
|
||||
void WriteToLog(string line, bool isError = false)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
line = line.StartsWith("<s>") ? line.Substring(3) : line;
|
||||
|
||||
if (captureLog != null)
|
||||
{
|
||||
captureLog(line, isError);
|
||||
}
|
||||
if (forwardLog)
|
||||
{
|
||||
(isError ? Console.Error : Console.Out).WriteLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void WriteToLog(ArraySegment<char> chunk, bool isError = false)
|
||||
{
|
||||
bool containsNewline = Array.IndexOf(chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;
|
||||
|
||||
if (containsNewline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (captureLog != null)
|
||||
{
|
||||
captureLog(new String(chunk.Array, chunk.Offset, chunk.Count), isError);
|
||||
}
|
||||
if (forwardLog)
|
||||
{
|
||||
(isError ? Console.Error : Console.Out).Write(chunk.Array, chunk.Offset, chunk.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Zero.Web.DevServer
|
||||
{
|
||||
public class ZeroDevOptions
|
||||
{
|
||||
public int Port { get; set; } = 3399;
|
||||
|
||||
public bool ForwardLog { get; set; } = false;
|
||||
|
||||
public string WorkingDirectory { get; set; }
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using zero.Core.Plugins;
|
||||
|
||||
namespace Zero.Web.DevServer
|
||||
{
|
||||
public class ZeroDevService : IHostedService
|
||||
{
|
||||
IWebHostEnvironment env;
|
||||
IOptions<ZeroDevOptions> options;
|
||||
ProcessProxy viteProcess;
|
||||
ILogger<ZeroDevService> logger;
|
||||
PluginResolver pluginResolver;
|
||||
string workingDirectory;
|
||||
bool isRunning = false;
|
||||
|
||||
|
||||
public ZeroDevService(IWebHostEnvironment env, IOptions<ZeroDevOptions> options, ILogger<ZeroDevService> logger, IEnumerable<IZeroPlugin> plugins)
|
||||
{
|
||||
//foreach (IZeroPlugin plugin in plugins)
|
||||
//{
|
||||
// string location = Assembly.GetAssembly(plugin.GetType()). ;
|
||||
//}
|
||||
this.pluginResolver = new PluginResolver(plugins);
|
||||
this.env = env;
|
||||
this.options = options;
|
||||
this.workingDirectory = options.Value.WorkingDirectory;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// this is a development-time service,
|
||||
// therefore no way to enable it in production
|
||||
if (!env.IsDevelopment() || !options.Value.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// locate npm version and throw if it is not installed
|
||||
Version npmVersion = await FindNpmVersion();
|
||||
|
||||
if (npmVersion == null)
|
||||
{
|
||||
throw new Exception("Please install node+npm to use the zero dev service (https://www.npmjs.com/)");
|
||||
}
|
||||
|
||||
// start vite server
|
||||
viteProcess = await StartDevServer(options.Value.Port);
|
||||
logger.LogInformation("vite listening on: http://localhost:{port}", options.Value.Port);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the node version from the system
|
||||
/// </summary>
|
||||
async Task<Version> FindNpmVersion()
|
||||
{
|
||||
Version version = null;
|
||||
|
||||
ProcessProxy process = new ProcessProxy(workingDirectory, "npm").Argument("-v").Capture((value, err) =>
|
||||
{
|
||||
if (version == null && !value.Contains("not recognized") && Version.TryParse(value, out Version _version))
|
||||
{
|
||||
version = _version;
|
||||
}
|
||||
});
|
||||
|
||||
await process.ExecuteAsync();
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Starts the vite dev server which also support HMR
|
||||
/// </summary>
|
||||
async Task<ProcessProxy> StartDevServer(int port)
|
||||
{
|
||||
// if the port we want to use is occupied, terminate the process utilizing that port.
|
||||
// this occurs when "stop" is used from the debugger and the middleware does not have the opportunity to kill the process
|
||||
PidUtils.KillPort((ushort)port, true);
|
||||
|
||||
// get all plugins which need to be passed to vite
|
||||
List<string> plugins = new();
|
||||
|
||||
foreach (var plugin in pluginResolver.Plugins)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(plugin.Options.PluginPath))
|
||||
{
|
||||
plugins.Add(plugin.Options.PluginPath);
|
||||
}
|
||||
}
|
||||
|
||||
// create and run the vite script
|
||||
ProcessProxy process = new ProcessProxy(workingDirectory, "npm", options.Value.ForwardLog)
|
||||
.Argument("run dev")
|
||||
.EnvVar("PORT", port.ToString())
|
||||
.EnvVar("ZERO_PLUGINS", JsonConvert.SerializeObject(plugins))
|
||||
.Capture(CaptureLog);
|
||||
|
||||
await process.RunAsync("localhost:", TimeSpan.FromMinutes(1));
|
||||
|
||||
isRunning = true;
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
|
||||
void CaptureLog(string line, bool isError)
|
||||
{
|
||||
if (!isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (isError)
|
||||
{
|
||||
logger.LogWarning(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using System.Collections.Generic;
|
||||
using zero.Core.Services;
|
||||
|
||||
namespace zero.Web.Extensions
|
||||
{
|
||||
public static class LocalizerExtensions
|
||||
{
|
||||
public static IHtmlContent Html(this ILocalizer localizer, string key)
|
||||
{
|
||||
string value = localizer.Text(key);
|
||||
|
||||
HtmlContentBuilder builder = new();
|
||||
builder.SetHtmlContent(value);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
public static IHtmlContent Html(this ILocalizer localizer, string key, Dictionary<string, string> tokens)
|
||||
{
|
||||
string value = localizer.Text(key, tokens);
|
||||
|
||||
HtmlContentBuilder builder = new();
|
||||
builder.SetHtmlContent(value);
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user