This commit is contained in:
2021-11-20 13:52:28 +01:00
parent be9ad9d335
commit 2d6d798771
367 changed files with 4840 additions and 7501 deletions
-146
View File
@@ -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();
}
}
}
-111
View File
@@ -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);
// }
//}
-240
View File
@@ -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);
}
}
}
}
-205
View File
@@ -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();
}
}
-90
View File
@@ -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";
}
}
}
-78
View File
@@ -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();
}
}
-15
View File
@@ -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; }
}
}
-9
View File
@@ -1,9 +0,0 @@
namespace zero.Core.Entities
{
public class Culture
{
public string Code { get; set; }
public string Name { get; set; }
}
}
-28
View File
@@ -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; }
}
}
-48
View File
@@ -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; }
}
}
-74
View File
@@ -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
}
}
-17
View File
@@ -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);
}
}
-36
View File
@@ -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;
}
}
-42
View File
@@ -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; }
}
}
-27
View File
@@ -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
}
}
-25
View File
@@ -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);
}
}
-12
View File
@@ -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
{
}
}
-27
View File
@@ -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);
}
}
-17
View File
@@ -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; }
}
}
-97
View File
@@ -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();
}
}
}
-25
View File
@@ -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() { }
}
}
-29
View File
@@ -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();
}
}
-9
View File
@@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace zero.Core.Mails
{
public class MailMetadata : Dictionary<string, string>
{
}
}
-9
View File
@@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace zero.Core.Mails
{
public class MailPlaceholders : Dictionary<string, string>
{
}
}
-209
View File
@@ -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);
}
}
-15
View File
@@ -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() { }
}
}
-6
View File
@@ -1,6 +0,0 @@
namespace zero.Core.Messages
{
public interface IMessage
{
}
}
-30
View File
@@ -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);
}
}
-224
View File
@@ -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
{
}
}
-25
View File
@@ -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();
}
}
-287
View File
@@ -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;
}
}
}
-129
View File
@@ -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");
}
}
}
-275
View File
@@ -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));
}
}
}
+2 -60
View File
@@ -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;
}
}
}
}
-205
View File
@@ -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);
}
}
-16
View File
@@ -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;
}
}
}
-252
View File
@@ -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);
}
}
}
}
-13
View File
@@ -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;
}
}
-142
View File
@@ -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