1
0
mirror of https://github.com/ONLYOFFICE/DocSpace-server.git synced 2025-04-18 14:24:02 +03:00

Merge hotfix/v3.0.4 into master

This commit is contained in:
Alexey Golubev 2025-02-21 08:50:52 +00:00
commit 641f16447f
20 changed files with 506 additions and 320 deletions

View File

@ -27,7 +27,7 @@
namespace ASC.Common.Caching;
[Singleton]
public class RedisCacheNotify<T>(IRedisClient redisCacheClient) : ICacheNotify<T> where T : new()
public class RedisCacheNotify<T>(IRedisClient redisCacheClient, ILogger<RedisCacheNotify<T>> logger) : ICacheNotify<T> where T : new()
{
private readonly IRedisDatabase _redis = redisCacheClient.GetDefaultDatabase();
private readonly ConcurrentDictionary<CacheNotifyAction, ConcurrentBag<Action<T>>> _invocationList = new();
@ -39,7 +39,14 @@ public class RedisCacheNotify<T>(IRedisClient redisCacheClient) : ICacheNotify<T
foreach (var handler in GetInvocationList(action))
{
handler(obj);
try
{
handler(obj);
}
catch (Exception e)
{
logger.ErrorRedisCacheNotifyPublish(e);
}
}
}
@ -49,7 +56,14 @@ public class RedisCacheNotify<T>(IRedisClient redisCacheClient) : ICacheNotify<T
{
if (i.Id != _instanceId && (i.Action == action || Enum.IsDefined(typeof(CacheNotifyAction), (i.Action & action))))
{
onChange(i.Object);
try
{
onChange(i.Object);
}
catch (Exception e)
{
logger.ErrorRedisCacheNotifySubscribe(e);
}
}
return Task.FromResult(true);

View File

@ -0,0 +1,36 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
namespace ASC.Common.Log;
internal static partial class RedisCacheNotifyLogger
{
[LoggerMessage(LogLevel.Error, "RedisCacheNotify Publish")]
public static partial void ErrorRedisCacheNotifyPublish(this ILogger logger, Exception exception);
[LoggerMessage(LogLevel.Error, "RedisCacheNotify Subscribe")]
public static partial void ErrorRedisCacheNotifySubscribe(this ILogger logger, Exception exception);
}

View File

@ -24,6 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
@ -52,7 +53,17 @@ public class LowercaseDocumentFilter : IDocumentFilter
var paths = new OpenApiPaths();
foreach (var (key, value) in swaggerDoc.Paths)
{
var lowerCaseKey = key.ToLowerInvariant();
var segments = key.Split('/');
for (var i = 0; i < segments.Length; i++)
{
if (!segments[i].StartsWith("{") && !segments[i].EndsWith("}"))
{
segments[i] = segments[i].ToLowerInvariant();
}
}
var lowerCaseKey = string.Join("/", segments);
paths.Add(lowerCaseKey, value);
}
@ -145,11 +156,42 @@ public class TagDescriptionsDocumentFilter : IDocumentFilter
swaggerDoc.Tags = customTags
.Where(tag => _tagDescriptions.ContainsKey(tag))
.Select(tag => new OpenApiTag
.Select(tag =>
{
var tagParts = tag.Split(" / ");
var displayName = tagParts.Length > 1 ? tagParts[1] : tagParts[0];
var openApiTag = new OpenApiTag
{
Name = tag,
Description = _tagDescriptions[tag]
};
openApiTag.Extensions.Add("x-displayName", new OpenApiString(displayName));
return openApiTag;
}).ToList();
var groupTag = customTags
.Where(tag => _tagDescriptions.ContainsKey(tag))
.GroupBy(tag => tag.Split(" / ")[0])
.ToDictionary(group => group.Key, group => group.ToList());
var tagGroups = new OpenApiArray();
foreach (var group in groupTag)
{
Name = tag,
Description = _tagDescriptions[tag]
}).ToList();
var groupObject = new OpenApiObject();
var tagsArray = new OpenApiArray();
foreach(var tag in group.Value)
{
tagsArray.Add(new OpenApiString(tag));
}
groupObject["name"] = new OpenApiString(group.Key);
groupObject["tags"] = tagsArray;
tagGroups.Add(groupObject);
}
swaggerDoc.Extensions["x-tagGroups"] = tagGroups;
}
}

View File

@ -26,21 +26,36 @@
namespace ASC.Core.Billing;
[ProtoContract]
public class PaymentInfo
{
public int ID { get; set; }
public int Status { get; set; }
public int PaymentSystemId { get; set; }
public string CartId { get; set; }
public string FName { get; set; }
public string LName { get; set; }
public string Email { get; set; }
public DateTime PaymentDate { get; set; }
public decimal Price { get; set; }
public int Qty { get; set; }
public string PaymentCurrency { get; set; }
public string PaymentMethod { get; set; }
public int QuotaId { get; set; }
public int ProductRef { get; set; }
public string CustomerId { get; set; }
[ProtoMember(1)] public int ID { get; set; }
[ProtoMember(2)] public int Status { get; set; }
[ProtoMember(3)] public int PaymentSystemId { get; set; }
[ProtoMember(4)] public string CartId { get; set; }
[ProtoMember(5)] public string FName { get; set; }
[ProtoMember(6)] public string LName { get; set; }
[ProtoMember(7)] public string Email { get; set; }
[ProtoMember(8)] public DateTime PaymentDate { get; set; }
[ProtoMember(9)] public decimal Price { get; set; }
[ProtoMember(10)] public int Qty { get; set; }
[ProtoMember(11)] public string PaymentCurrency { get; set; }
[ProtoMember(12)] public string PaymentMethod { get; set; }
[ProtoMember(13)] public int QuotaId { get; set; }
[ProtoMember(14)] public int ProductRef { get; set; }
[ProtoMember(15)] public string CustomerId { get; set; }
}

View File

@ -27,41 +27,49 @@
namespace ASC.Core.Billing;
[DebuggerDisplay("{State} before {DueDate}")]
[ProtoContract]
public class Tariff
{
/// <summary>
/// ID
/// </summary>
[ProtoMember(1)]
public int Id { get; set; }
/// <summary>
/// Tariff state
/// </summary>
[ProtoMember(2)]
public TariffState State { get; set; }
/// <summary>
/// Due date
/// </summary>
[ProtoMember(3)]
public DateTime DueDate { get; set; }
/// <summary>
/// Delay due date
/// </summary>
[ProtoMember(4)]
public DateTime DelayDueDate { get; set; }
/// <summary>
/// License date
/// </summary>
[ProtoMember(5)]
public DateTime LicenseDate { get; set; }
/// <summary>
/// Customer ID
/// </summary>
[ProtoMember(6)]
public string CustomerId { get; set; }
/// <summary>
/// List of quotas
/// </summary>
[ProtoMember(7)]
public List<Quota> Quotas { get; set; }
public override int GetHashCode()
@ -77,24 +85,38 @@ public class Tariff
public bool EqualsByParams(Tariff t)
{
return t != null
&& t.DueDate == DueDate
&& t.Quotas.Count == Quotas.Count
&& t.Quotas.Exists(Quotas.Contains)
&& t.CustomerId == CustomerId;
&& t.DueDate == DueDate
&& t.Quotas.Count == Quotas.Count
&& t.Quotas.Exists(Quotas.Contains)
&& t.CustomerId == CustomerId;
}
}
public class Quota(int id, int quantity) : IEquatable<Quota>
[ProtoContract]
public class Quota : IEquatable<Quota>
{
/// <summary>
/// ID
/// </summary>
public int Id { get; set; } = id;
[ProtoMember(1)]
public int Id { get; set; }
/// <summary>
/// Quantity
/// </summary>
public int Quantity { get; set; } = quantity;
[ProtoMember(2)]
public int Quantity { get; set; }
public Quota()
{
}
public Quota(int id, int quantity)
{
Id = id;
Quantity = quantity;
}
public bool Equals(Quota other)
{

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using Microsoft.Extensions.Caching.Distributed;
namespace ASC.Core.Billing;
[Singleton]
@ -50,64 +52,6 @@ public class TenantExtraConfig(CoreBaseSettings coreBaseSettings, LicenseReaderC
}
}
[Singleton]
public class TariffServiceStorage
{
private static readonly TimeSpan _defaultCacheExpiration = TimeSpan.FromMinutes(5);
private static readonly TimeSpan _standaloneCacheExpiration = TimeSpan.FromMinutes(15);
private readonly ICache _cache;
private readonly CoreBaseSettings _coreBaseSettings;
private TimeSpan _cacheExpiration;
public TariffServiceStorage(ICacheNotify<TariffCacheItem> notify, ICache cache, CoreBaseSettings coreBaseSettings, IServiceProvider serviceProvider)
{
_cacheExpiration = _defaultCacheExpiration;
_cache = cache;
_coreBaseSettings = coreBaseSettings;
notify.Subscribe(i =>
{
_cache.Insert(TariffService.GetTariffNeedToUpdateCacheKey(i.TenantId), "update", _cacheExpiration);
_cache.Remove(TariffService.GetTariffCacheKey(i.TenantId));
_cache.Remove(TariffService.GetBillingUrlCacheKey(i.TenantId));
_cache.Remove(TariffService.GetBillingPaymentCacheKey(i.TenantId)); // clear all payments
}, CacheNotifyAction.Remove);
notify.Subscribe(i =>
{
using var scope = serviceProvider.CreateScope();
var tariffService = scope.ServiceProvider.GetService<ITariffService>();
var tariff = tariffService.GetBillingInfoAsync(i.TenantId, i.TariffId).Result;
if (tariff != null)
{
InsertToCache(i.TenantId, tariff);
}
}, CacheNotifyAction.Insert);
}
private TimeSpan GetCacheExpiration()
{
if (_coreBaseSettings.Standalone && _cacheExpiration < _standaloneCacheExpiration)
{
_cacheExpiration = _cacheExpiration.Add(TimeSpan.FromSeconds(30));
}
return _cacheExpiration;
}
public void InsertToCache(int tenantId, Tariff tariff)
{
_cache.Insert(TariffService.GetTariffCacheKey(tenantId), tariff, DateTime.UtcNow.Add(GetCacheExpiration()));
}
public void ResetCacheExpiration()
{
if (_coreBaseSettings.Standalone)
{
_cacheExpiration = _defaultCacheExpiration;
}
}
}
[Scope(typeof(ITariffService))]
public class TariffService(
@ -118,14 +62,18 @@ public class TariffService(
IConfiguration configuration,
IDbContextFactory<CoreDbContext> coreDbContextManager,
ICache cache,
ICacheNotify<TariffCacheItem> notify,
TariffServiceStorage tariffServiceStorage,
IDistributedCache distributedCache,
IDistributedLockProvider distributedLockProvider,
ILogger<TariffService> logger,
BillingClient billingClient,
IServiceProvider serviceProvider,
TenantExtraConfig tenantExtraConfig)
: ITariffService
{
private static readonly TimeSpan _defaultCacheExpiration = TimeSpan.FromMinutes(5);
private static readonly TimeSpan _standaloneCacheExpiration = TimeSpan.FromMinutes(15);
private TimeSpan _cacheExpiration = _defaultCacheExpiration;
private const int DefaultTrialPeriod = 30;
//private readonly int _activeUsersMin;
@ -145,133 +93,135 @@ public class TariffService(
tenantId = -1;
}
var tariff = refresh ? null : cache.Get<Tariff>(GetTariffCacheKey(tenantId));
var key = GetTariffCacheKey(tenantId);
var tariff = refresh ? null : await GetFromCache<Tariff>(key);
if (tariff == null)
{
tariff = await GetBillingInfoAsync(tenantId) ?? await CreateDefaultAsync();
tariff = await CalculateTariffAsync(tenantId, tariff);
tariffServiceStorage.InsertToCache(tenantId, tariff);
if (billingClient.Configured && withRequestToPaymentSystem)
await using (await distributedLockProvider.TryAcquireLockAsync($"{key}_lock"))
{
try
tariff = refresh ? null : await GetFromCache<Tariff>(key);
if (tariff != null)
{
var currentPayments = await billingClient.GetCurrentPaymentsAsync(await coreSettings.GetKeyAsync(tenantId), refresh);
if (currentPayments.Length == 0)
{
throw new BillingNotFoundException("Empty PaymentLast");
}
tariff = await CalculateTariffAsync(tenantId, tariff);
return tariff;
}
tariff = await GetBillingInfoAsync(tenantId) ?? await CreateDefaultAsync();
tariff = await CalculateTariffAsync(tenantId, tariff);
await InsertToCache(tenantId, tariff);
var asynctariff = await CreateDefaultAsync(true);
string email = null;
var tenantQuotas = await quotaService.GetTenantQuotasAsync();
foreach (var currentPayment in currentPayments.OrderBy(r => r.EndDate))
if (billingClient.Configured && withRequestToPaymentSystem)
{
try
{
var quota = tenantQuotas.SingleOrDefault(q => q.ProductId == currentPayment.ProductId.ToString());
if (quota == null)
var currentPayments = await billingClient.GetCurrentPaymentsAsync(await coreSettings.GetKeyAsync(tenantId), refresh);
if (currentPayments.Length == 0)
{
throw new InvalidOperationException($"Quota with id {currentPayment.ProductId} not found for portal {await coreSettings.GetKeyAsync(tenantId)}.");
throw new BillingNotFoundException("Empty PaymentLast");
}
asynctariff.Id = currentPayment.PaymentId;
var asynctariff = await CreateDefaultAsync(true);
string email = null;
var tenantQuotas = await quotaService.GetTenantQuotasAsync();
var paymentEndDate = 9999 <= currentPayment.EndDate.Year ? DateTime.MaxValue : currentPayment.EndDate;
asynctariff.DueDate = DateTime.Compare(asynctariff.DueDate, paymentEndDate) < 0 ? asynctariff.DueDate : paymentEndDate;
foreach (var currentPayment in currentPayments.OrderBy(r => r.EndDate))
{
var quota = tenantQuotas.SingleOrDefault(q => q.ProductId == currentPayment.ProductId.ToString());
if (quota == null)
{
throw new InvalidOperationException($"Quota with id {currentPayment.ProductId} not found for portal {await coreSettings.GetKeyAsync(tenantId)}.");
}
asynctariff.Quotas = asynctariff.Quotas.Where(r => r.Id != quota.TenantId).ToList();
asynctariff.Quotas.Add(new Quota(quota.TenantId, currentPayment.Quantity));
email = currentPayment.PaymentEmail;
asynctariff.Id = currentPayment.PaymentId;
var paymentEndDate = 9999 <= currentPayment.EndDate.Year ? DateTime.MaxValue : currentPayment.EndDate;
asynctariff.DueDate = DateTime.Compare(asynctariff.DueDate, paymentEndDate) < 0 ? asynctariff.DueDate : paymentEndDate;
asynctariff.Quotas = asynctariff.Quotas.Where(r => r.Id != quota.TenantId).ToList();
asynctariff.Quotas.Add(new Quota(quota.TenantId, currentPayment.Quantity));
email = currentPayment.PaymentEmail;
}
TenantQuota updatedQuota = null;
foreach (var quota in asynctariff.Quotas)
{
var tenantQuota = tenantQuotas.SingleOrDefault(q => q.TenantId == quota.Id);
tenantQuota *= quota.Quantity;
updatedQuota += tenantQuota;
}
if (updatedQuota != null)
{
await updatedQuota.CheckAsync(serviceProvider);
}
if (!string.IsNullOrEmpty(email))
{
asynctariff.CustomerId = email;
}
if (await SaveBillingInfoAsync(tenantId, asynctariff))
{
asynctariff = await CalculateTariffAsync(tenantId, asynctariff);
tariff = asynctariff;
}
await InsertToCache(tenantId, tariff);
}
catch (Exception error)
{
if (error is not BillingNotFoundException)
{
LogError(error, tenantId.ToString());
}
}
TenantQuota updatedQuota = null;
foreach (var quota in asynctariff.Quotas)
if (tariff.Id == 0)
{
var tenantQuota = tenantQuotas.SingleOrDefault(q => q.TenantId == quota.Id);
var freeTariff = await tariff.Quotas.ToAsyncEnumerable().FirstOrDefaultAwaitAsync(async tariffRow =>
{
var q = await quotaService.GetTenantQuotaAsync(tariffRow.Id);
return q == null
|| (TrialEnabled && q.Trial)
|| q.Free
|| q.NonProfit
|| q.Custom;
});
tenantQuota *= quota.Quantity;
updatedQuota += tenantQuota;
}
var asynctariff = await CreateDefaultAsync();
if (updatedQuota != null)
{
await updatedQuota.CheckAsync(serviceProvider);
}
if (freeTariff == null)
{
asynctariff.DueDate = DateTime.Today.AddDays(-1);
asynctariff.State = TariffState.NotPaid;
}
if (!string.IsNullOrEmpty(email))
{
asynctariff.CustomerId = email;
}
if (await SaveBillingInfoAsync(tenantId, asynctariff))
{
asynctariff = await CalculateTariffAsync(tenantId, asynctariff);
tariff = asynctariff;
}
if (await SaveBillingInfoAsync(tenantId, asynctariff))
{
asynctariff = await CalculateTariffAsync(tenantId, asynctariff);
tariff = asynctariff;
}
await UpdateCacheAsync(tariff.Id);
}
catch (Exception error)
{
if (error is not BillingNotFoundException)
{
LogError(error, tenantId.ToString());
await InsertToCache(tenantId, tariff);
}
}
if (tariff.Id == 0)
else if (tenantExtraConfig.Enterprise && tariff.Id == 0 && tariff.LicenseDate == DateTime.MaxValue)
{
var freeTariff = await tariff.Quotas.ToAsyncEnumerable().FirstOrDefaultAwaitAsync(async tariffRow =>
{
var q = await quotaService.GetTenantQuotaAsync(tariffRow.Id);
return q == null
|| (TrialEnabled && q.Trial)
|| q.Free
|| q.NonProfit
|| q.Custom;
});
var defaultQuota = await quotaService.GetTenantQuotaAsync(Tenant.DefaultTenant);
var asynctariff = await CreateDefaultAsync();
var quota = new TenantQuota(defaultQuota) { Name = "start_trial", Trial = true, TenantId = -1000 };
if (freeTariff == null)
{
asynctariff.DueDate = DateTime.Today.AddDays(-1);
asynctariff.State = TariffState.NotPaid;
}
await quotaService.SaveTenantQuotaAsync(quota);
if (await SaveBillingInfoAsync(tenantId, asynctariff))
{
asynctariff = await CalculateTariffAsync(tenantId, asynctariff);
tariff = asynctariff;
}
tariff = new Tariff { Quotas = [new(quota.TenantId, 1)], DueDate = DateTime.UtcNow.AddDays(DefaultTrialPeriod) };
await UpdateCacheAsync(tariff.Id);
await SetTariffAsync(Tenant.DefaultTenant, tariff, [quota]);
await InsertToCache(tenantId, tariff);
}
}
else if (tenantExtraConfig.Enterprise && tariff.Id == 0 && tariff.LicenseDate == DateTime.MaxValue)
{
var defaultQuota = await quotaService.GetTenantQuotaAsync(Tenant.DefaultTenant);
var quota = new TenantQuota(defaultQuota)
{
Name = "start_trial",
Trial = true,
TenantId = -1000
};
await quotaService.SaveTenantQuotaAsync(quota);
tariff = new Tariff
{
Quotas = [new(quota.TenantId, 1)],
DueDate = DateTime.UtcNow.AddDays(DefaultTrialPeriod)
};
await SetTariffAsync(Tenant.DefaultTenant, tariff, [quota]);
await UpdateCacheAsync(tariff.Id);
}
}
else
{
@ -279,11 +229,6 @@ public class TariffService(
}
return tariff;
async Task UpdateCacheAsync(int tariffId)
{
await notify.PublishAsync(new TariffCacheItem { TenantId = tenantId, TariffId = tariffId }, CacheNotifyAction.Insert);
}
}
public async Task<bool> PaymentChangeAsync(int tenantId, Dictionary<string, int> quantity)
@ -385,16 +330,7 @@ public class TariffService(
{
return $"{tenantId}:tariff";
}
internal static string GetTariffNeedToUpdateCacheKey(int tenantId)
{
return $"{tenantId}:update";
}
internal static string GetBillingUrlCacheKey(int tenantId)
{
return $"{tenantId}:billing:urls";
}
internal static string GetBillingPaymentCacheKey(int tenantId)
{
@ -404,38 +340,50 @@ public class TariffService(
private async Task ClearCacheAsync(int tenantId)
{
await notify.PublishAsync(new TariffCacheItem { TenantId = tenantId, TariffId = -1 }, CacheNotifyAction.Remove);
await distributedCache.RemoveAsync(GetTariffCacheKey(tenantId));
}
public async Task<IEnumerable<PaymentInfo>> GetPaymentsAsync(int tenantId)
{
var key = GetBillingPaymentCacheKey(tenantId);
var payments = cache.Get<List<PaymentInfo>>(key);
var payments = await GetFromCache<List<PaymentInfo>>(key);
if (payments == null)
{
payments = [];
if (billingClient.Configured)
await using (await distributedLockProvider.TryAcquireLockAsync($"{key}_lock"))
{
try
payments = await GetFromCache<List<PaymentInfo>>(key);
if (payments != null)
{
var quotas = await quotaService.GetTenantQuotasAsync();
foreach (var pi in await billingClient.GetPaymentsAsync(await coreSettings.GetKeyAsync(tenantId)))
return payments;
}
payments = [];
if (billingClient.Configured)
{
try
{
var quota = quotas.SingleOrDefault(q => q.ProductId == pi.ProductRef.ToString());
if (quota != null)
var quotas = await quotaService.GetTenantQuotasAsync();
foreach (var pi in await billingClient.GetPaymentsAsync(await coreSettings.GetKeyAsync(tenantId)))
{
pi.QuotaId = quota.TenantId;
var quota = quotas.SingleOrDefault(q => q.ProductId == pi.ProductRef.ToString());
if (quota != null)
{
pi.QuotaId = quota.TenantId;
}
payments.Add(pi);
}
payments.Add(pi);
}
catch (Exception error)
{
LogError(error, tenantId.ToString());
}
}
catch (Exception error)
{
LogError(error, tenantId.ToString());
}
}
cache.Insert(key, payments, DateTime.UtcNow.Add(TimeSpan.FromMinutes(10)));
using var ms = new MemoryStream();
Serializer.Serialize(ms, payments);
await distributedCache.SetAsync(key, ms.ToArray(), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
}
}
return payments;
@ -512,7 +460,7 @@ public class TariffService(
cache.Insert(key, url, DateTime.UtcNow.Add(TimeSpan.FromMinutes(10)));
}
tariffServiceStorage.ResetCacheExpiration();
ResetCacheExpiration();
if (string.IsNullOrEmpty(url))
{
@ -887,4 +835,42 @@ public class TariffService(
{
return billingClient.Configured;
}
private TimeSpan GetCacheExpiration()
{
if (coreBaseSettings.Standalone && _cacheExpiration < _standaloneCacheExpiration)
{
_cacheExpiration = _cacheExpiration.Add(TimeSpan.FromSeconds(30));
}
return _cacheExpiration;
}
private async Task InsertToCache(int tenantId, Tariff tariff)
{
using var ms = new MemoryStream();
Serializer.Serialize(ms, tariff);
await distributedCache.SetAsync(GetTariffCacheKey(tenantId), ms.ToArray(), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = GetCacheExpiration()});
}
private async Task<T> GetFromCache<T>(string key)
{
var serializedObject = await distributedCache.GetAsync(key);
if (serializedObject == null)
{
return default;
}
using var ms = new MemoryStream(serializedObject);
return Serializer.Deserialize<T>(ms);
}
private void ResetCacheExpiration()
{
if (coreBaseSettings.Standalone)
{
_cacheExpiration = _defaultCacheExpiration;
}
}
}

View File

@ -71,7 +71,7 @@ public static class StringExtension
try
{
var punyCode = idn.GetAscii(data);
var punyCode = idn.GetAscii(data.TrimStart('.'));
var domain2 = idn.GetUnicode(punyCode);
if (!string.Equals(punyCode, domain2))
@ -79,8 +79,9 @@ public static class StringExtension
return true;
}
}
catch (ArgumentException)
catch (ArgumentException ex) when(ex.ParamName == "unicode")
{
return true;
}
return false;

View File

@ -45,12 +45,22 @@ public abstract class ActivePassiveBackgroundService<T>(ILogger logger, IService
var registerInstanceService = serviceScope.ServiceProvider.GetService<IRegisterInstanceManager<T>>();
var workerOptions = serviceScope.ServiceProvider.GetService<IOptions<InstanceWorkerOptions<T>>>().Value;
if (!await registerInstanceService.IsActive())
const int millisecondsDelay = 1000;
try
{
logger.TraceActivePassiveBackgroundServiceIsNotActive(serviceName, workerOptions.InstanceId);
await Task.Delay(1000, stoppingToken);
if (!await registerInstanceService.IsActive())
{
logger.TraceActivePassiveBackgroundServiceIsNotActive(serviceName, workerOptions.InstanceId);
await Task.Delay(millisecondsDelay, stoppingToken);
continue;
}
}
catch (Exception e)
{
logger.WarningWithException(e);
await Task.Delay(millisecondsDelay, stoppingToken);
continue;
}
@ -62,6 +72,5 @@ public abstract class ActivePassiveBackgroundService<T>(ILogger logger, IService
await Task.Delay(ExecuteTaskPeriod, stoppingToken);
}
}
}

View File

@ -28,6 +28,7 @@ using Profile = AutoMapper.Profile;
namespace ASC.Core.Tenants;
[ProtoContract]
public class Tenant : IMapFrom<DbTenant>
{
public const int DefaultTenant = -1;
@ -62,36 +63,62 @@ public class Tenant : IMapFrom<DbTenant>
{
Id = id;
}
public void Mapping(Profile profile)
{
profile.CreateMap<DbTenant, Tenant>()
.ForMember(r => r.TrustedDomainsType, opt => opt.MapFrom(src => src.TrustedDomainsEnabled))
.ForMember(r => r.AffiliateId, opt => opt.MapFrom(src => src.Partner.AffiliateId))
.ForMember(r => r.PartnerId, opt => opt.MapFrom(src => src.Partner.PartnerId))
.ForMember(r => r.Campaign, opt => opt.MapFrom(src => src.Partner.Campaign));
profile.CreateMap<TenantUserSecurity, Tenant>()
.IncludeMembers(src => src.DbTenant);
}
[ProtoMember(1)]
public string AffiliateId { get; set; }
[ProtoMember(2)]
public string Alias { get; set; }
[ProtoMember(3)]
public bool Calls { get; set; }
[ProtoMember(4)]
public string Campaign { get; set; }
[ProtoMember(5)]
public DateTime CreationDateTime { get; internal set; }
[ProtoMember(6)]
public string HostedRegion { get; set; }
[ProtoMember(7)]
public int Id { get; internal set; }
[ProtoMember(8)]
public TenantIndustry Industry { get; set; }
[ProtoMember(9)]
public string Language { get; set; }
[ProtoMember(10)]
public DateTime LastModified { get; set; }
[ProtoMember(11)]
public string MappedDomain { get; set; }
[ProtoMember(12)]
public string Name { get; set; }
[ProtoMember(13)]
public Guid OwnerId { get; set; }
[ProtoMember(14)]
public string PartnerId { get; set; }
[ProtoMember(15)]
public string PaymentId { get; set; }
[ProtoMember(16)]
public TenantStatus Status { get; internal set; }
[ProtoMember(17)]
public DateTime StatusChangeDate { get; internal set; }
[ProtoMember(18)]
public string TimeZone { get; set; }
[ProtoMember(19)]
public List<string> TrustedDomains
{
get
@ -104,19 +131,29 @@ public class Tenant : IMapFrom<DbTenant>
return _domains;
}
set => _domains = value;
}
[ProtoMember(20)]
public string TrustedDomainsRaw { get; set; }
[ProtoMember(21)]
public TenantTrustedDomainsType TrustedDomainsType { get; set; }
[ProtoMember(22)]
public int Version { get; set; }
[ProtoMember(23)]
public DateTime VersionChanged { get; set; }
public override bool Equals(object obj)
{
return obj is Tenant t && t.Id == Id;
}
public CultureInfo GetCulture() => !string.IsNullOrEmpty(Language) ? CultureInfo.GetCultureInfo(Language.Trim()) : CultureInfo.CurrentCulture;
public override int GetHashCode()
{
return Id;
@ -143,19 +180,22 @@ public class Tenant : IMapFrom<DbTenant>
result = $"{Alias}.{baseHost}".TrimEnd('.').ToLowerInvariant();
}
if (!string.IsNullOrEmpty(MappedDomain) && allowMappedDomain)
if (string.IsNullOrEmpty(MappedDomain) || !allowMappedDomain)
{
if (MappedDomain.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase))
{
MappedDomain = MappedDomain[7..];
}
if (MappedDomain.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
{
MappedDomain = MappedDomain[8..];
}
result = MappedDomain.ToLowerInvariant();
return result;
}
if (MappedDomain.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase))
{
MappedDomain = MappedDomain[7..];
}
if (MappedDomain.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
{
MappedDomain = MappedDomain[8..];
}
result = MappedDomain.ToLowerInvariant();
return result;
}
@ -169,6 +209,18 @@ public class Tenant : IMapFrom<DbTenant>
{
return Alias;
}
public void Mapping(Profile profile)
{
profile.CreateMap<DbTenant, Tenant>()
.ForMember(r => r.TrustedDomainsType, opt => opt.MapFrom(src => src.TrustedDomainsEnabled))
.ForMember(r => r.AffiliateId, opt => opt.MapFrom(src => src.Partner.AffiliateId))
.ForMember(r => r.PartnerId, opt => opt.MapFrom(src => src.Partner.PartnerId))
.ForMember(r => r.Campaign, opt => opt.MapFrom(src => src.Partner.Campaign));
profile.CreateMap<TenantUserSecurity, Tenant>()
.IncludeMembers(src => src.DbTenant);
}
internal string GetTrustedDomains()
{
@ -192,4 +244,4 @@ public class Tenant : IMapFrom<DbTenant>
TrustedDomains.AddRange(trustedDomains.Split(['|'], StringSplitOptions.RemoveEmptyEntries));
}
}
}
}

View File

@ -1445,7 +1445,7 @@ public class FileStorageService //: IFileStorageService
}
else
{
await entryManager.TrackEditingAsync(fileId, tabId, authContext.CurrentAccount.ID, await tenantManager.GetCurrentTenantIdAsync());
await entryManager.TrackEditingAsync(fileId, tabId, authContext.CurrentAccount.ID, await tenantManager.GetCurrentTenantAsync());
}
return new KeyValuePair<bool, string>(true, string.Empty);
@ -1556,7 +1556,7 @@ public class FileStorageService //: IFileStorageService
throw new InvalidOperationException(FilesCommonResource.ErrorMessage_SecurityException_EditFileTwice);
}
await entryManager.TrackEditingAsync(fileId, Guid.Empty, authContext.CurrentAccount.ID, await tenantManager.GetCurrentTenantIdAsync(), true);
await entryManager.TrackEditingAsync(fileId, Guid.Empty, authContext.CurrentAccount.ID, await tenantManager.GetCurrentTenantAsync(), true);
//without StartTrack, track via old scheme
return await documentServiceHelper.GetDocKeyAsync(fileId, -1, DateTime.MinValue);
@ -1665,7 +1665,6 @@ public class FileStorageService //: IFileStorageService
foreach (var r in history)
{
await entryStatusManager.SetFileStatusAsync(r);
yield return r;
}
}

View File

@ -294,7 +294,7 @@ public class DocumentServiceTrackerHelper(SecurityContext securityContext,
try
{
file = await entryManager.TrackEditingAsync(fileId, userId, userId, await tenantManager.GetCurrentTenantIdAsync());
file = await entryManager.TrackEditingAsync(fileId, userId, userId, await tenantManager.GetCurrentTenantAsync());
}
catch (Exception e)
{

View File

@ -1689,25 +1689,27 @@ public class EntryManager(IDaoFactory daoFactory,
return file;
}
public async Task<File<T>> TrackEditingAsync<T>(T fileId, Guid tabId, Guid userId, int tenantId, bool editingAlone = false)
public async Task<File<T>> TrackEditingAsync<T>(T fileId, Guid tabId, Guid userId, Tenant tenant, bool editingAlone = false)
{
var token = externalShare.GetKey();
var file = await daoFactory.GetFileDao<T>().GetFileStableAsync(fileId);
if (file == null)
{
throw new FileNotFoundException(FilesCommonResource.ErrorMessage_FileNotFound);
}
var docKey = await documentServiceHelper.GetDocKeyAsync(file);
bool checkRight;
if ((await fileTracker.GetEditingByAsync(fileId)).Contains(userId))
{
checkRight = await fileTracker.ProlongEditingAsync(fileId, tabId, userId, tenantId, commonLinkUtility.ServerRootPath, editingAlone, token);
checkRight = await fileTracker.ProlongEditingAsync(fileId, tabId, userId, tenant, commonLinkUtility.ServerRootPath, docKey, editingAlone, token);
if (!checkRight)
{
return null;
}
}
var file = await daoFactory.GetFileDao<T>().GetFileAsync(fileId);
if (file == null)
{
throw new FileNotFoundException(FilesCommonResource.ErrorMessage_FileNotFound);
}
if (!await CanEditAsync(userId, file))
{
@ -1724,7 +1726,7 @@ public class EntryManager(IDaoFactory daoFactory,
throw new Exception(FilesCommonResource.ErrorMessage_ViewTrashItem);
}
checkRight = await fileTracker.ProlongEditingAsync(fileId, tabId, userId, tenantId, commonLinkUtility.ServerRootPath, editingAlone, token);
checkRight = await fileTracker.ProlongEditingAsync(fileId, tabId, userId, tenant, commonLinkUtility.ServerRootPath, docKey, editingAlone, token);
if (checkRight)
{
await fileTracker.ChangeRight(fileId, userId, false);

View File

@ -78,7 +78,7 @@ public class FileTrackerHelper
_logger.Debug("FileTracker subscribed");
}
public async Task<bool> ProlongEditingAsync<T>(T fileId, Guid tabId, Guid userId, int tenantId, string baseUri, bool editingAlone = false, string token = null)
public async Task<bool> ProlongEditingAsync<T>(T fileId, Guid tabId, Guid userId, Tenant tenant, string baseUri, string docKey, bool editingAlone = false, string token = null)
{
var checkRight = true;
var tracker = GetTracker(fileId);
@ -97,15 +97,13 @@ public class FileTrackerHelper
UserId = userId,
NewScheme = tabId == userId,
EditingAlone = editingAlone,
TenantId = tenantId,
BaseUri = baseUri,
Token = token
});
}
}
else
{
tracker = new FileTracker(tabId, userId, tabId == userId, editingAlone, tenantId, baseUri, token);
tracker = new FileTracker(tabId, userId, tabId == userId, editingAlone, tenant, baseUri, docKey, token);
}
await SetTrackerAsync(fileId, tracker);
@ -233,33 +231,29 @@ public class FileTrackerHelper
private Action<object, object, EvictionReason, object> EvictionCallback()
{
return (cacheFileId, fileTracker, reason, _) =>
return (cacheFileId, fileTracker, reason, state) =>
{
if (reason != EvictionReason.Expired || cacheFileId == null)
{
return;
}
var fId = cacheFileId.ToString()?.Substring(Tracker.Length);
var fId = cacheFileId.ToString()?[Tracker.Length..];
var t = int.TryParse(fId, out var internalFileId) ?
_ = int.TryParse(fId, out var internalFileId) ?
Callback(internalFileId, fileTracker as FileTracker).ConfigureAwait(false) :
Callback(fId, fileTracker as FileTracker).ConfigureAwait(false);
t.GetAwaiter().GetResult();
};
async Task Callback<T>(T fileId, FileTracker fileTracker)
{
try
{
if (fileTracker.EditingBy == null || fileTracker.EditingBy.Count == 0)
if (fileTracker.EditingBy == null || fileTracker.EditingBy.IsEmpty)
{
return;
}
var editedBy = fileTracker.EditingBy.FirstOrDefault();
var token = fileTracker.EditingBy
.OrderByDescending(x => x.Value.TrackTime)
.Where(x => !string.IsNullOrEmpty(x.Value.Token))
@ -268,25 +262,16 @@ public class FileTrackerHelper
await using var scope = _serviceProvider.CreateAsyncScope();
var tenantManager = scope.ServiceProvider.GetRequiredService<TenantManager>();
await tenantManager.SetCurrentTenantAsync(editedBy.Value.TenantId);
tenantManager.SetCurrentTenant(fileTracker.Tenant);
var commonLinkUtility = scope.ServiceProvider.GetRequiredService<BaseCommonLinkUtility>();
commonLinkUtility.ServerUri = editedBy.Value.BaseUri;
commonLinkUtility.ServerUri = fileTracker.BaseUri;
var helper = scope.ServiceProvider.GetRequiredService<DocumentServiceHelper>();
var tracker = scope.ServiceProvider.GetRequiredService<DocumentServiceTrackerHelper>();
var daoFactory = scope.ServiceProvider.GetRequiredService<IDaoFactory>();
var file = await daoFactory.GetFileDao<T>().GetFileStableAsync(fileId);
if (file == null)
{
return;
}
var docKey = await helper.GetDocKeyAsync(file);
using (_logger.BeginScope(new[] { new KeyValuePair<string, object>("DocumentServiceConnector", $"{fileId}") }))
{
if (await tracker.StartTrackAsync(fileId.ToString(), docKey, token))
if (await tracker.StartTrackAsync(fileId.ToString(), fileTracker.DocKey, token))
{
await SetTrackerAsync(fileId, fileTracker);
}
@ -303,7 +288,7 @@ public class FileTrackerHelper
}
}
private string GetCacheKey<T>(T fileId)
private static string GetCacheKey<T>(T fileId)
{
return Tracker + fileId;
}
@ -324,19 +309,29 @@ public record FileTracker
{
[ProtoMember(1)]
public ConcurrentDictionary<Guid, TrackInfo> EditingBy { get; set; }
[ProtoMember(2)]
public Tenant Tenant { get; set; }
[ProtoMember(3)]
public string BaseUri { get; set; }
[ProtoMember(4)]
public string DocKey { get; set; }
public FileTracker() { }
internal FileTracker(Guid tabId, Guid userId, bool newScheme, bool editingAlone, int tenantId, string baseUri, string token = null)
internal FileTracker(Guid tabId, Guid userId, bool newScheme, bool editingAlone, Tenant tenant, string baseUri, string docKey, string token = null)
{
DocKey = docKey;
Tenant = tenant;
BaseUri = baseUri;
EditingBy = new ConcurrentDictionary<Guid, TrackInfo>();
EditingBy.TryAdd(tabId, new TrackInfo
{
UserId = userId,
NewScheme = newScheme,
EditingAlone = editingAlone,
TenantId = tenantId,
BaseUri = baseUri,
Token = token
});
}
@ -354,18 +349,12 @@ public record FileTracker
public required Guid UserId { get; init; }
[ProtoMember(4)]
public required int TenantId { get; init; }
[ProtoMember(5)]
public required string BaseUri { get; init; }
[ProtoMember(6)]
public required bool NewScheme { get; init; }
[ProtoMember(7)]
[ProtoMember(5)]
public required bool EditingAlone { get; init; }
[ProtoMember(8)]
[ProtoMember(6)]
public string Token { get; init; }
}
}

View File

@ -61,7 +61,7 @@ public class FilesControllerInternal(
[SwaggerResponse(403, "You don't have enough permission to perform the operation")]
[SwaggerResponse(404, "The required file was not found")]
[HttpGet("file/{fileId:int}/log")]
public IAsyncEnumerable<HistoryDto> GetHistoryAsync(HistoryRequestDto inDto)
public IAsyncEnumerable<HistoryDto> GetFileHistoryAsync(HistoryRequestDto inDto)
{
return historyApiHelper.GetFileHistoryAsync(inDto.FileId, inDto.FromDate, inDto.ToDate);
}
@ -471,7 +471,7 @@ public abstract class FilesController<T>(FilesControllerHelper filesControllerHe
[Tags("Files / Files")]
[SwaggerResponse(200, "Order is set")]
[HttpPut("order")]
public async Task SetOrder(OrdersRequestDto<T> inDto)
public async Task SetFilesOrder(OrdersRequestDto<T> inDto)
{
await fileStorageService.SetOrderAsync(inDto.Items);
}

View File

@ -61,7 +61,7 @@ public class FoldersControllerInternal(
[SwaggerResponse(403, "You don't have enough permission to perform the operation")]
[SwaggerResponse(404, "The required folder was not found")]
[HttpGet("folder/{folderId:int}/log")]
public IAsyncEnumerable<HistoryDto> GetHistoryAsync(HistoryFolderRequestDto inDto)
public IAsyncEnumerable<HistoryDto> GetFolderHistoryAsync(HistoryFolderRequestDto inDto)
{
return historyApiHelper.GetFolderHistoryAsync(inDto.FolderId, inDto.FromDate, inDto.ToDate);
}

View File

@ -596,7 +596,7 @@ public abstract class VirtualRoomsController<T>(
[Tags("Files / Rooms")]
[SwaggerResponse(200, "List of file entry information", typeof(IAsyncEnumerable<NewItemsDto<FileEntryDto>>))]
[HttpGet("{id}/news")]
public async Task<List<NewItemsDto<FileEntryDto>>> GetNewItemsAsync(RoomIdRequestDto<T> inDto)
public async Task<List<NewItemsDto<FileEntryDto>>> GetNewRoomItemsAsync(RoomIdRequestDto<T> inDto)
{
var newItems = await _fileStorageService.GetNewRoomFilesAsync(inDto.Id);
var result = new List<NewItemsDto<FileEntryDto>>();

View File

@ -24,6 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
using ASC.Core.Common;
using ASC.Files.Service.Services.Thumbnail;
namespace ASC.Files.Service.IntegrationEvents.EventHandling;
@ -36,6 +37,7 @@ public class ThumbnailRequestedIntegrationEventHandler : IIntegrationEventHandle
private readonly ITariffService _tariffService;
private readonly TenantManager _tenantManager;
private readonly IDbContextFactory<FilesDbContext> _dbContextFactory;
private readonly BaseCommonLinkUtility _baseCommonLinkUtility;
private ThumbnailRequestedIntegrationEventHandler()
{
@ -47,13 +49,15 @@ public class ThumbnailRequestedIntegrationEventHandler : IIntegrationEventHandle
IDbContextFactory<FilesDbContext> dbContextFactory,
ITariffService tariffService,
TenantManager tenantManager,
ChannelWriter<FileData<int>> channelWriter)
ChannelWriter<FileData<int>> channelWriter,
BaseCommonLinkUtility baseCommonLinkUtility)
{
_logger = logger;
_channelWriter = channelWriter;
_tariffService = tariffService;
_tenantManager = tenantManager;
_dbContextFactory = dbContextFactory;
_baseCommonLinkUtility = baseCommonLinkUtility;
}
private async Task<IEnumerable<FileData<int>>> GetFreezingThumbnailsAsync()
@ -79,7 +83,9 @@ public class ThumbnailRequestedIntegrationEventHandler : IIntegrationEventHandle
{
_ = await _tenantManager.SetCurrentTenantAsync(r.TenantId);
var tariff = await _tariffService.GetTariffAsync(r.TenantId);
var fileData = new FileData<int>(r.TenantId, r.ModifiedBy, r.Id, "", tariff.State);
var baseUrl = _baseCommonLinkUtility.GetFullAbsolutePath(string.Empty);
var fileData = new FileData<int>(r.TenantId, r.ModifiedBy, r.Id, baseUrl, tariff.State);
return fileData;
}).ToListAsync();

View File

@ -53,7 +53,7 @@ public class Builder<T>(ThumbnailSettings settings,
private IDataStore _dataStore;
private readonly List<string> _imageFormatsCanBeCrop =
[".bmp", ".gif", ".jpeg", ".jpg", ".pbm", ".png", ".tiff", ".tga", ".webp"];
[".bmp", ".jpeg", ".jpg", ".pbm", ".png", ".tiff", ".tga", ".webp"];
internal async Task BuildThumbnail(FileData<T> fileData)
{

View File

@ -91,14 +91,21 @@ public class ThumbnailBuilderService(IServiceScopeFactory serviceScopeFactory,
{
await foreach (var fileData in reader.ReadAllAsync(stoppingToken))
{
await using var scope = serviceScopeFactory.CreateAsyncScope();
try
{
await using var scope = serviceScopeFactory.CreateAsyncScope();
var commonLinkUtility = scope.ServiceProvider.GetService<CommonLinkUtility>();
commonLinkUtility.ServerUri = fileData.BaseUri;
var commonLinkUtility = scope.ServiceProvider.GetService<CommonLinkUtility>();
commonLinkUtility.ServerUri = fileData.BaseUri;
var builder = scope.ServiceProvider.GetService<Builder<int>>();
var builder = scope.ServiceProvider.GetService<Builder<int>>();
await builder.BuildThumbnail(fileData);
await builder.BuildThumbnail(fileData);
}
catch (Exception e)
{
logger.ErrorWithException(e);
}
}
}, stoppingToken));
}

View File

@ -256,20 +256,26 @@ public class PortalController(
var result = new TariffDto
{
Id = source.Id,
State = source.State,
DueDate = source.DueDate,
DelayDueDate = source.DelayDueDate,
LicenseDate = source.LicenseDate,
CustomerId = source.CustomerId,
Quotas = source.Quotas,
};
var currentUserType = await userManager.GetUserTypeAsync(securityContext.CurrentAccount.ID);
if (currentUserType is EmployeeType.RoomAdmin or EmployeeType.DocSpaceAdmin)
{
result.DueDate = source.DueDate;
result.DelayDueDate = source.DelayDueDate;
}
if (await permissionContext.CheckPermissionsAsync(SecurityConstants.EditPortalSettings))
{
result.Id = source.Id;
result.OpenSource = tenantExtra.Opensource;
result.Enterprise = tenantExtra.Enterprise;
result.Developer = tenantExtra.Developer;
result.CustomerId = source.CustomerId;
result.LicenseDate = source.LicenseDate;
result.Quotas = source.Quotas;
}
return result;