Merge branch 'develop' into feature/translations
This commit is contained in:
commit
95dbfb8c5c
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[submodule "products/ASC.Files/Server/DocStore"]
|
||||
path = products/ASC.Files/Server/DocStore
|
||||
url = https://github.com/ONLYOFFICE/document-templates
|
||||
branch = main/community-server
|
@ -85,6 +85,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.Mail", "products\ASC.Ma
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.Calendar", "products\ASC.Calendar\Server\ASC.Calendar.csproj", "{F39933F8-7598-492F-9DD3-E25780D68288}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASC.SsoAuth.Svc", "common\services\ASC.SsoAuth.Svc\ASC.SsoAuth.Svc.csproj", "{6AD828EA-FBA2-4D30-B969-756B3BE78E4E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -239,6 +241,10 @@ Global
|
||||
{F39933F8-7598-492F-9DD3-E25780D68288}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F39933F8-7598-492F-9DD3-E25780D68288}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F39933F8-7598-492F-9DD3-E25780D68288}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6AD828EA-FBA2-4D30-B969-756B3BE78E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6AD828EA-FBA2-4D30-B969-756B3BE78E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6AD828EA-FBA2-4D30-B969-756B3BE78E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6AD828EA-FBA2-4D30-B969-756B3BE78E4E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
6
build/Jenkinsfile
vendored
6
build/Jenkinsfile
vendored
@ -62,7 +62,7 @@ pipeline {
|
||||
}
|
||||
stage('Files') {
|
||||
steps {
|
||||
sh "dotnet build ASC.Web.sln && cd ${env.WORKSPACE}/products/ASC.Files/Tests/ && dotnet test ASC.Files.Tests.csproj -r linux-x64 -l \"console;verbosity=detailed\""
|
||||
sh "git submodule update --progress --init -- products/ASC.Files/Server/DocStore && dotnet build ASC.Web.sln && cd ${env.WORKSPACE}/products/ASC.Files/Tests/ && dotnet test ASC.Files.Tests.csproj -r linux-x64 -l \"console;verbosity=detailed\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,7 +90,7 @@ pipeline {
|
||||
}
|
||||
stage('Files') {
|
||||
steps {
|
||||
bat "dotnet build ASC.Web.sln && cd ${env.WORKSPACE}\\products\\ASC.Files\\Tests\\ && dotnet test ASC.Files.Tests.csproj"
|
||||
bat "git submodule update --progress --init -- products\\ASC.Files\\Server\\DocStore && dotnet build ASC.Web.sln && cd ${env.WORKSPACE}\\products\\ASC.Files\\Tests\\ && dotnet test ASC.Files.Tests.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -98,7 +98,7 @@ pipeline {
|
||||
}
|
||||
}
|
||||
stage('Notify') {
|
||||
when { expression { return env.CHANGE_ID != '' && env.BUILD_NUMBER == '1' } }
|
||||
when { expression { return env.CHANGE_ID != null && env.BUILD_NUMBER == '1' } }
|
||||
agent { label 'net-core' }
|
||||
options { skipDefaultCheckout() }
|
||||
environment {
|
||||
|
2
build/run/SsoAuth.bat
Normal file
2
build/run/SsoAuth.bat
Normal file
@ -0,0 +1,2 @@
|
||||
echo "RUN ASC.SsoAuth.Svc"
|
||||
call dotnet run --project ..\..\common\services\ASC.SsoAuth.Svc\ASC.SsoAuth.Svc.csproj --no-build --$STORAGE_ROOT=..\..\Data --log__dir=..\..\Logs --log__name=ssoauth
|
@ -31,10 +31,10 @@ namespace ASC.Api.Core.Middleware
|
||||
{
|
||||
public CommonApiError Error { get; set; }
|
||||
|
||||
protected internal ErrorApiResponse(HttpStatusCode statusCode, Exception error) : base(statusCode)
|
||||
protected internal ErrorApiResponse(HttpStatusCode statusCode, Exception error, string message = null) : base(statusCode)
|
||||
{
|
||||
Status = 1;
|
||||
Error = CommonApiError.FromException(error);
|
||||
Error = CommonApiError.FromException(error, message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,11 +88,11 @@ namespace ASC.Api.Core.Middleware
|
||||
|
||||
public int Hresult { get; set; }
|
||||
|
||||
public static CommonApiError FromException(Exception exception)
|
||||
public static CommonApiError FromException(Exception exception, string message = null)
|
||||
{
|
||||
return new CommonApiError()
|
||||
{
|
||||
Message = exception.Message,
|
||||
Message = message ?? exception.Message,
|
||||
Type = exception.GetType().ToString(),
|
||||
Stack = exception.StackTrace,
|
||||
Hresult = exception.HResult
|
||||
|
@ -1,4 +1,8 @@
|
||||
using System.Net;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Security;
|
||||
|
||||
using ASC.Common.Web;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
@ -10,12 +14,33 @@ namespace ASC.Api.Core.Middleware
|
||||
public override void OnException(ExceptionContext context)
|
||||
{
|
||||
var status = (HttpStatusCode)context.HttpContext.Response.StatusCode;
|
||||
string message = null;
|
||||
|
||||
if (status == HttpStatusCode.OK)
|
||||
{
|
||||
status = HttpStatusCode.InternalServerError;
|
||||
}
|
||||
|
||||
var result = new ObjectResult(new ErrorApiResponse(status, context.Exception))
|
||||
switch (context.Exception)
|
||||
{
|
||||
case ItemNotFoundException:
|
||||
status = HttpStatusCode.NotFound;
|
||||
message = "The record could not be found";
|
||||
break;
|
||||
case ArgumentException:
|
||||
status = HttpStatusCode.BadRequest;
|
||||
message = "Invalid arguments";
|
||||
break;
|
||||
case SecurityException:
|
||||
status = HttpStatusCode.Forbidden;
|
||||
message = "Access denied";
|
||||
break;
|
||||
case InvalidOperationException:
|
||||
status = HttpStatusCode.Forbidden;
|
||||
break;
|
||||
}
|
||||
|
||||
var result = new ObjectResult(new ErrorApiResponse(status, context.Exception, message))
|
||||
{
|
||||
StatusCode = (int)status
|
||||
};
|
||||
|
@ -25,39 +25,24 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
|
||||
using ASC.Data.Storage;
|
||||
|
||||
public static class StreamExtension
|
||||
{
|
||||
// public const int BufferSize = 2048; //NOTE: set to 2048 to fit in minimum tcp window
|
||||
public const int BufferSize = 2048; //NOTE: set to 2048 to fit in minimum tcp window
|
||||
|
||||
public static void StreamCopyTo(this Stream srcStream, Stream dstStream, int length)
|
||||
{
|
||||
if (srcStream == null) throw new ArgumentNullException("srcStream");
|
||||
if (dstStream == null) throw new ArgumentNullException("dstStream");
|
||||
|
||||
public static Stream GetBuffered(this Stream srcStream)
|
||||
{
|
||||
if (srcStream == null) throw new ArgumentNullException(nameof(srcStream));
|
||||
if (!srcStream.CanSeek || srcStream.CanTimeout)
|
||||
{
|
||||
//Buffer it
|
||||
var memStream = TempStream.Create();
|
||||
srcStream.CopyTo(memStream);
|
||||
memStream.Position = 0;
|
||||
return memStream;
|
||||
}
|
||||
return srcStream;
|
||||
}
|
||||
|
||||
public static byte[] GetCorrectBuffer(this Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
using var mem = stream.GetBuffered();
|
||||
var buffer = new byte[mem.Length];
|
||||
mem.Position = 0;
|
||||
mem.Read(buffer, 0, buffer.Length);
|
||||
return buffer;
|
||||
var buffer = new byte[BufferSize];
|
||||
int totalRead = 0;
|
||||
int readed;
|
||||
while ((readed = srcStream.Read(buffer, 0, length - totalRead > BufferSize ? BufferSize : length - totalRead)) > 0 && totalRead < length)
|
||||
{
|
||||
dstStream.Write(buffer, 0, readed);
|
||||
totalRead += readed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
90
common/ASC.Common/Data/TempPath.cs
Normal file
90
common/ASC.Common/Data/TempPath.cs
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System.Reflection;
|
||||
|
||||
using ASC.Common;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace System.IO
|
||||
{
|
||||
[Singletone]
|
||||
public class TempPath
|
||||
{
|
||||
readonly string tempFolder;
|
||||
|
||||
public TempPath(IConfiguration configuration)
|
||||
{
|
||||
string rootFolder = AppContext.BaseDirectory;
|
||||
|
||||
if (string.IsNullOrEmpty(rootFolder))
|
||||
{
|
||||
rootFolder = Assembly.GetEntryAssembly().Location;
|
||||
}
|
||||
|
||||
tempFolder = configuration["temp"] ?? Path.Combine("..", "Data", "temp");
|
||||
|
||||
if (!Path.IsPathRooted(tempFolder))
|
||||
{
|
||||
tempFolder = Path.GetFullPath(Path.Combine(rootFolder, tempFolder));
|
||||
}
|
||||
|
||||
if (!Directory.Exists(tempFolder))
|
||||
{
|
||||
Directory.CreateDirectory(tempFolder);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetTempPath()
|
||||
{
|
||||
return tempFolder;
|
||||
}
|
||||
|
||||
public string GetTempFileName()
|
||||
{
|
||||
FileStream f = null;
|
||||
string path;
|
||||
var count = 0;
|
||||
|
||||
do
|
||||
{
|
||||
path = Path.Combine(tempFolder, Path.GetRandomFileName());
|
||||
|
||||
try
|
||||
{
|
||||
using (f = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
if (ex.HResult != -2147024816 || count++ > 65536)
|
||||
throw;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
if (count++ > 65536)
|
||||
throw new IOException(ex.Message, ex);
|
||||
}
|
||||
} while (f == null);
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +1,52 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace ASC.Data.Storage
|
||||
{
|
||||
public static class TempStream
|
||||
{
|
||||
public static Stream Create()
|
||||
{
|
||||
//Return temporary stream
|
||||
return new FileStream(Path.GetTempFileName(), FileMode.Open, FileAccess.ReadWrite, FileShare.Read, 2048, FileOptions.DeleteOnClose);
|
||||
}
|
||||
}
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace ASC.Common
|
||||
{
|
||||
[Singletone]
|
||||
public class TempStream
|
||||
{
|
||||
private TempPath TempPath { get; }
|
||||
|
||||
public TempStream(TempPath tempPath)
|
||||
{
|
||||
TempPath = tempPath;
|
||||
}
|
||||
|
||||
public Stream GetBuffered(Stream srcStream)
|
||||
{
|
||||
if (srcStream == null) throw new ArgumentNullException("srcStream");
|
||||
if (!srcStream.CanSeek || srcStream.CanTimeout)
|
||||
{
|
||||
//Buffer it
|
||||
var memStream = Create();
|
||||
srcStream.CopyTo(memStream);
|
||||
memStream.Position = 0;
|
||||
return memStream;
|
||||
}
|
||||
return srcStream;
|
||||
}
|
||||
|
||||
public Stream Create()
|
||||
{
|
||||
return new FileStream(TempPath.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.DeleteOnClose);
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ using System.Reflection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
using HttpContext = Microsoft.AspNetCore.Http.HttpContext;
|
||||
|
||||
@ -163,15 +164,17 @@ namespace System.Web
|
||||
}
|
||||
|
||||
public static bool DesktopApp(this HttpRequest request)
|
||||
{
|
||||
return request != null && !string.IsNullOrEmpty(request.Headers["desktop"]);
|
||||
{
|
||||
return request != null
|
||||
&& (!string.IsNullOrEmpty(request.Query["desktop"])
|
||||
|| !string.IsNullOrEmpty(request.Headers[HeaderNames.UserAgent]) && request.Headers[HeaderNames.UserAgent].ToString().Contains("AscDesktopEditor"));
|
||||
}
|
||||
|
||||
public static bool SailfishApp(this HttpRequest request)
|
||||
{
|
||||
return request != null
|
||||
&& (!string.IsNullOrEmpty(request.Headers["sailfish"])
|
||||
|| !string.IsNullOrEmpty(request.Headers["User-Agent"]) && request.Headers["User-Agent"].ToString().Contains("SailfishOS"));
|
||||
|| !string.IsNullOrEmpty(request.Headers[HeaderNames.UserAgent]) && request.Headers[HeaderNames.UserAgent].ToString().Contains("SailfishOS"));
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,7 +38,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ASC.Core.Common
|
||||
{
|
||||
[Singletone]
|
||||
[Scope]
|
||||
public class CommonLinkUtilitySettings
|
||||
{
|
||||
public string ServerUri { get; set; }
|
||||
@ -112,7 +112,7 @@ namespace ASC.Core.Common
|
||||
|
||||
protected CoreBaseSettings CoreBaseSettings { get; }
|
||||
private CoreSettings CoreSettings { get; }
|
||||
private TenantManager TenantManager { get; }
|
||||
protected TenantManager TenantManager { get; }
|
||||
|
||||
private string serverRootPath;
|
||||
public string ServerRootPath
|
||||
@ -238,10 +238,10 @@ namespace ASC.Core.Common
|
||||
return baseUri.ToString().TrimEnd('/');
|
||||
}
|
||||
|
||||
public void Initialize(string serverUri)
|
||||
public void Initialize(string serverUri, bool localhost = true)
|
||||
{
|
||||
var uri = new Uri(serverUri.Replace('*', 'x').Replace('+', 'x'));
|
||||
_serverRoot = new UriBuilder(uri.Scheme, LOCALHOST, uri.Port);
|
||||
_serverRoot = new UriBuilder(uri.Scheme, localhost ? LOCALHOST : uri.Host, uri.Port);
|
||||
_vpath = "/" + uri.AbsolutePath.Trim('/');
|
||||
}
|
||||
}
|
||||
|
@ -26,249 +26,271 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Security.Cryptography;
|
||||
using System.ServiceModel;
|
||||
using System.Web;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
|
||||
using ASC.Common.Logging;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using ASC.Common;
|
||||
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ASC.Core.Billing
|
||||
{
|
||||
public class BillingClient : ClientBase<IService>, IDisposable
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly bool test;
|
||||
|
||||
private string Security { get; set; }
|
||||
private string PartnersProduct { get; set; }
|
||||
|
||||
public BillingClient(IConfiguration configuration, IOptionsMonitor<ILog> option)
|
||||
: this(false, configuration, option)
|
||||
{
|
||||
}
|
||||
|
||||
public BillingClient(bool test, IConfiguration configuration, IOptionsMonitor<ILog> option)
|
||||
{
|
||||
this.test = test;
|
||||
Security = configuration["core:payment:security"];
|
||||
PartnersProduct = configuration["core:payment:partners-product"];
|
||||
log = option.CurrentValue;
|
||||
}
|
||||
|
||||
|
||||
public PaymentLast GetLastPayment(string portalId)
|
||||
{
|
||||
var result = Request("GetLatestActiveResourceEx", portalId);
|
||||
var xelement = ToXElement("<root>" + result + "</root>");
|
||||
var dedicated = xelement.Element("dedicated-resource");
|
||||
var payment = xelement.Element("payment");
|
||||
|
||||
if (!test && GetValueString(payment.Element("status")) == "4")
|
||||
{
|
||||
throw new BillingException("Can not accept test payment.", new { PortalId = portalId });
|
||||
}
|
||||
|
||||
var autorenewal = string.Empty;
|
||||
try
|
||||
{
|
||||
autorenewal = Request("GetLatestAvangateLicenseRecurringStatus", portalId);
|
||||
}
|
||||
catch (BillingException err)
|
||||
{
|
||||
log.Debug(err); // ignore
|
||||
}
|
||||
|
||||
return new PaymentLast
|
||||
{
|
||||
[Singletone]
|
||||
public class BillingClient
|
||||
{
|
||||
public readonly bool Configured = false;
|
||||
private readonly string _billingDomain;
|
||||
private readonly string _billingKey;
|
||||
private readonly string _billingSecret;
|
||||
private readonly bool _test;
|
||||
|
||||
private const int AvangatePaymentSystemId = 1;
|
||||
|
||||
|
||||
public BillingClient(IConfiguration configuration)
|
||||
: this(false, configuration)
|
||||
{
|
||||
}
|
||||
|
||||
public BillingClient(bool test, IConfiguration configuration)
|
||||
{
|
||||
_test = test;
|
||||
|
||||
var billingDomain = configuration["core:payment-url"];
|
||||
|
||||
_billingDomain = (billingDomain ?? "").Trim().TrimEnd('/');
|
||||
if (!string.IsNullOrEmpty(_billingDomain))
|
||||
{
|
||||
ProductId = GetValueString(dedicated.Element("product-id")),
|
||||
EndDate = GetValueDateTime(dedicated.Element("end-date")),
|
||||
Autorenewal = "enabled".Equals(autorenewal, StringComparison.InvariantCultureIgnoreCase),
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<PaymentInfo> GetPayments(string portalId, DateTime from, DateTime to)
|
||||
{
|
||||
string result;
|
||||
if (from == DateTime.MinValue && to == DateTime.MaxValue)
|
||||
{
|
||||
result = Request("GetPayments", portalId);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Request("GetListOfPaymentsByTimeSpan", portalId, Tuple.Create("StartDate", from.ToString("yyyy-MM-dd HH:mm:ss")), Tuple.Create("EndDate", to.ToString("yyyy-MM-dd HH:mm:ss")));
|
||||
}
|
||||
var xelement = ToXElement(result);
|
||||
return xelement.Elements("payment").Select(ToPaymentInfo);
|
||||
}
|
||||
|
||||
public IDictionary<string, Tuple<Uri, Uri>> GetPaymentUrls(string portalId, string[] products, string affiliateId = null, string campaign = null, string currency = null, string language = null, string customerId = null)
|
||||
{
|
||||
var urls = new Dictionary<string, Tuple<Uri, Uri>>();
|
||||
|
||||
var additionalParameters = new List<Tuple<string, string>>(2) { Tuple.Create("PaymentSystemId", "1") };
|
||||
if (!string.IsNullOrEmpty(affiliateId))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("AffiliateId", affiliateId));
|
||||
_billingDomain += "/billing/";
|
||||
|
||||
_billingKey = configuration["core:payment-key"];
|
||||
_billingSecret = configuration["core:payment-secret"];
|
||||
|
||||
Configured = true;
|
||||
}
|
||||
}
|
||||
|
||||
public PaymentLast GetLastPayment(string portalId)
|
||||
{
|
||||
var result = Request("GetActiveResource", portalId);
|
||||
var paymentLast = JsonSerializer.Deserialize<PaymentLast>(result);
|
||||
|
||||
if (!_test && paymentLast.PaymentStatus == 4)
|
||||
{
|
||||
throw new BillingException("Can not accept test payment.", new { PortalId = portalId });
|
||||
}
|
||||
|
||||
return paymentLast;
|
||||
}
|
||||
|
||||
public IEnumerable<PaymentInfo> GetPayments(string portalId)
|
||||
{
|
||||
string result = Request("GetPayments", portalId);
|
||||
var payments = JsonSerializer.Deserialize<List<PaymentInfo>>(result);
|
||||
|
||||
return payments;
|
||||
}
|
||||
|
||||
public IDictionary<string, Tuple<Uri, Uri>> GetPaymentUrls(string portalId, string[] products, string affiliateId = null, string campaign = null, string currency = null, string language = null, string customerId = null, string quantity = null)
|
||||
{
|
||||
var urls = new Dictionary<string, Tuple<Uri, Uri>>();
|
||||
|
||||
var additionalParameters = new List<Tuple<string, string>>() { Tuple.Create("PaymentSystemId", AvangatePaymentSystemId.ToString()) };
|
||||
if (!string.IsNullOrEmpty(affiliateId))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("AffiliateId", affiliateId));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(campaign))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("campaign", campaign));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(currency))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("Currency", currency));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(language))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("Language", language));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(customerId))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("CustomerID", customerId));
|
||||
}
|
||||
|
||||
var parameters = products
|
||||
.Distinct()
|
||||
.Select(p => Tuple.Create("ProductId", p))
|
||||
.Concat(additionalParameters)
|
||||
.ToArray();
|
||||
|
||||
//max 100 products
|
||||
var paymentUrls = ToXElement(Request("GetBatchPaymentSystemUrl", portalId, parameters))
|
||||
.Elements()
|
||||
.ToDictionary(e => e.Attribute("id").Value, e => ToUrl(e.Attribute("value").Value));
|
||||
|
||||
var upgradeUrls = new Dictionary<string, string>();
|
||||
if (!string.IsNullOrEmpty(portalId))
|
||||
{
|
||||
try
|
||||
{
|
||||
//max 100 products
|
||||
upgradeUrls = ToXElement(Request("GetBatchPaymentSystemUpgradeUrl", portalId, parameters))
|
||||
.Elements()
|
||||
.ToDictionary(e => e.Attribute("id").Value, e => ToUrl(e.Attribute("value").Value));
|
||||
}
|
||||
catch (BillingException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var p in products)
|
||||
{
|
||||
var paymentUrl = (Uri)null;
|
||||
var upgradeUrl = (Uri)null;
|
||||
if (paymentUrls.TryGetValue(p, out var url) && !string.IsNullOrEmpty(url))
|
||||
{
|
||||
paymentUrl = new Uri(url);
|
||||
}
|
||||
if (upgradeUrls.TryGetValue(p, out url) && !string.IsNullOrEmpty(url))
|
||||
{
|
||||
upgradeUrl = new Uri(url);
|
||||
}
|
||||
urls[p] = Tuple.Create(paymentUrl, upgradeUrl);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
public Invoice GetInvoice(string paymentId)
|
||||
{
|
||||
var result = Request("GetInvoice", null, Tuple.Create("PaymentId", paymentId));
|
||||
var xelement = ToXElement(result);
|
||||
return new Invoice
|
||||
{
|
||||
Sale = GetValueString(xelement.Element("sale")),
|
||||
Refund = GetValueString(xelement.Element("refund")),
|
||||
};
|
||||
}
|
||||
if (!string.IsNullOrEmpty(currency))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("Currency", currency));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(language))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("Language", language));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(customerId))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("CustomerID", customerId));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(quantity))
|
||||
{
|
||||
additionalParameters.Add(Tuple.Create("Quantity", quantity));
|
||||
}
|
||||
|
||||
var parameters = products
|
||||
.Distinct()
|
||||
.Select(p => Tuple.Create("ProductId", p))
|
||||
.Concat(additionalParameters)
|
||||
.ToArray();
|
||||
|
||||
//max 100 products
|
||||
var result = Request("GetPaymentUrl", portalId, parameters);
|
||||
var paymentUrls = JsonSerializer.Deserialize<Dictionary<string, string>>(result);
|
||||
|
||||
var upgradeUrls = new Dictionary<string, string>();
|
||||
if (!string.IsNullOrEmpty(portalId)
|
||||
//TODO: remove
|
||||
&& false)
|
||||
{
|
||||
try
|
||||
{
|
||||
//max 100 products
|
||||
result = Request("GetPaymentUpgradeUrl", portalId, parameters);
|
||||
upgradeUrls = JsonSerializer.Deserialize<Dictionary<string, string>>(result);
|
||||
}
|
||||
catch (BillingNotFoundException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var p in products)
|
||||
{
|
||||
string url;
|
||||
var paymentUrl = (Uri)null;
|
||||
var upgradeUrl = (Uri)null;
|
||||
if (paymentUrls.TryGetValue(p, out url) && !string.IsNullOrEmpty(url = ToUrl(url)))
|
||||
{
|
||||
paymentUrl = new Uri(url);
|
||||
}
|
||||
if (upgradeUrls.TryGetValue(p, out url) && !string.IsNullOrEmpty(url = ToUrl(url)))
|
||||
{
|
||||
upgradeUrl = new Uri(url);
|
||||
}
|
||||
urls[p] = Tuple.Create(paymentUrl, upgradeUrl);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
public IDictionary<string, IEnumerable<Tuple<string, decimal>>> GetProductPriceInfo(params string[] productIds)
|
||||
public IDictionary<string, Dictionary<string, decimal>> GetProductPriceInfo(params string[] productIds)
|
||||
{
|
||||
if (productIds == null)
|
||||
{
|
||||
throw new ArgumentNullException("productIds");
|
||||
}
|
||||
|
||||
var responce = Request("GetBatchAvangateProductPriceInfo", null, productIds.Select(pid => Tuple.Create("ProductId", pid)).ToArray());
|
||||
var xelement = ToXElement(responce);
|
||||
return productIds
|
||||
.Select(p =>
|
||||
var parameters = productIds.Select(pid => Tuple.Create("ProductId", pid)).ToList();
|
||||
parameters.Add(Tuple.Create("PaymentSystemId", AvangatePaymentSystemId.ToString()));
|
||||
|
||||
var result = Request("GetProductsPrices", null, parameters.ToArray());
|
||||
var prices = JsonSerializer.Deserialize<Dictionary<int, Dictionary<string, Dictionary<string, decimal>>>>(result);
|
||||
|
||||
if (prices.ContainsKey(AvangatePaymentSystemId))
|
||||
{
|
||||
var pricesPaymentSystem = prices[AvangatePaymentSystemId];
|
||||
|
||||
return productIds.Select(productId =>
|
||||
{
|
||||
var prices = Enumerable.Empty<Tuple<string, decimal>>();
|
||||
var product = xelement.XPathSelectElement(string.Format("/avangate-product/internal-id[text()=\"{0}\"]", p));
|
||||
if (product != null)
|
||||
if (pricesPaymentSystem.ContainsKey(productId))
|
||||
{
|
||||
prices = product.Parent.Element("prices").Elements("price-item")
|
||||
.Select(e => Tuple.Create(e.Element("currency").Value, decimal.Parse(e.Element("amount").Value)));
|
||||
return new { ProductId = productId, Prices = pricesPaymentSystem[productId] };
|
||||
}
|
||||
return new { ProductId = p, Prices = prices, };
|
||||
return new { ProductId = productId, Prices = new Dictionary<string, decimal>() };
|
||||
})
|
||||
.ToDictionary(e => e.ProductId, e => e.Prices);
|
||||
.ToDictionary(e => e.ProductId, e => e.Prices);
|
||||
}
|
||||
|
||||
return new Dictionary<string, Dictionary<string, decimal>>();
|
||||
}
|
||||
|
||||
|
||||
private string CreateAuthToken(string pkey, string machinekey)
|
||||
{
|
||||
using (var hasher = new HMACSHA1(Encoding.UTF8.GetBytes(machinekey)))
|
||||
{
|
||||
var now = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
|
||||
var hash = WebEncoders.Base64UrlEncode(hasher.ComputeHash(Encoding.UTF8.GetBytes(string.Join("\n", now, pkey))));
|
||||
return string.Format("ASC {0}:{1}:{2}", pkey, now, hash);
|
||||
}
|
||||
}
|
||||
|
||||
private string Request(string method, string portalId, params Tuple<string, string>[] parameters)
|
||||
{
|
||||
var request = new XElement(method);
|
||||
var url = _billingDomain + method;
|
||||
|
||||
var request = WebRequest.Create(url);
|
||||
request.Method = "POST";
|
||||
request.Timeout = 60000;
|
||||
request.ContentType = "application/json";
|
||||
|
||||
if (!string.IsNullOrEmpty(_billingKey))
|
||||
{
|
||||
request.Headers.Add("Authorization", CreateAuthToken(_billingKey, _billingSecret));
|
||||
}
|
||||
|
||||
var data = new Dictionary<string, List<string>>();
|
||||
if (!string.IsNullOrEmpty(portalId))
|
||||
{
|
||||
request.Add(new XElement("PortalId", portalId));
|
||||
data.Add("PortalId", new List<string>() { portalId });
|
||||
}
|
||||
request.Add(parameters.Select(p => new XElement(p.Item1, p.Item2)).ToArray());
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if (!data.ContainsKey(parameter.Item1))
|
||||
{
|
||||
data.Add(parameter.Item1, new List<string>() { parameter.Item2 });
|
||||
}
|
||||
else
|
||||
{
|
||||
data[parameter.Item1].Add(parameter.Item2);
|
||||
}
|
||||
}
|
||||
var body = JsonSerializer.Serialize(data);
|
||||
|
||||
var responce = Channel.Request(new Message { Type = MessageType.Data, Content = request.ToString(SaveOptions.DisableFormatting), });
|
||||
if (responce.Content == null)
|
||||
var bytes = Encoding.UTF8.GetBytes(body ?? "");
|
||||
request.ContentLength = bytes.Length;
|
||||
using (var stream = request.GetRequestStream())
|
||||
{
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
string result;
|
||||
try
|
||||
{
|
||||
using (var response = request.GetResponse())
|
||||
using (var stream = response.GetResponseStream())
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new BillingNotConfiguredException("Billing response is null");
|
||||
}
|
||||
using (var readStream = new StreamReader(stream))
|
||||
{
|
||||
result = readStream.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
request.Abort();
|
||||
throw;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
throw new BillingNotConfiguredException("Billing response is null");
|
||||
}
|
||||
if (responce.Type == MessageType.Data)
|
||||
if (!result.StartsWith("{\"Message\":\"error"))
|
||||
{
|
||||
var result = responce.Content;
|
||||
var invalidChar = ((char)65279).ToString();
|
||||
return result.Contains(invalidChar) ? result.Replace(invalidChar, string.Empty) : result;
|
||||
return result;
|
||||
}
|
||||
|
||||
var @params = (parameters ?? Enumerable.Empty<Tuple<string, string>>()).Select(p => string.Format("{0}: {1}", p.Item1, p.Item2));
|
||||
var info = new { Method = method, PortalId = portalId, Params = string.Join(", ", @params) };
|
||||
if (responce.Content.Contains("error: cannot find "))
|
||||
if (result.Contains("{\"Message\":\"error: cannot find "))
|
||||
{
|
||||
throw new BillingNotFoundException(responce.Content, info);
|
||||
throw new BillingNotFoundException(result, info);
|
||||
}
|
||||
throw new BillingException(responce.Content, info);
|
||||
}
|
||||
|
||||
private static XElement ToXElement(string xml)
|
||||
{
|
||||
return XElement.Parse(xml);
|
||||
}
|
||||
|
||||
private static PaymentInfo ToPaymentInfo(XElement x)
|
||||
{
|
||||
return new PaymentInfo
|
||||
{
|
||||
ID = (int)GetValueDecimal(x.Element("id")),
|
||||
Status = (int)GetValueDecimal(x.Element("status")),
|
||||
PaymentType = GetValueString(x.Element("reserved-str-2")),
|
||||
ExchangeRate = (double)GetValueDecimal(x.Element("exch-rate")),
|
||||
GrossSum = (double)GetValueDecimal(x.Element("gross-sum")),
|
||||
Name = (GetValueString(x.Element("fname")) + " " + GetValueString(x.Element("lname"))).Trim(),
|
||||
Email = GetValueString(x.Element("email")),
|
||||
Date = GetValueDateTime(x.Element("payment-date")),
|
||||
Price = GetValueDecimal(x.Element("price")),
|
||||
Currency = GetValueString(x.Element("payment-currency")),
|
||||
Method = GetValueString(x.Element("payment-method")),
|
||||
CartId = GetValueString(x.Element("cart-id")),
|
||||
ProductId = GetValueString(x.Element("product-ref")),
|
||||
TenantID = GetValueString(x.Element("customer-id")),
|
||||
Country = GetValueString(x.Element("country")),
|
||||
DiscountSum = GetValueDecimal(x.Element("discount-sum"))
|
||||
};
|
||||
throw new BillingException(result, info);
|
||||
}
|
||||
|
||||
private string ToUrl(string s)
|
||||
@ -278,55 +300,12 @@ namespace ASC.Core.Billing
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (test && !s.Contains("&DOTEST = 1"))
|
||||
if (_test && !s.Contains("&DOTEST = 1"))
|
||||
{
|
||||
s += "&DOTEST=1";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static string GetValueString(XElement xelement)
|
||||
{
|
||||
return xelement != null ? HttpUtility.HtmlDecode(xelement.Value) : default;
|
||||
}
|
||||
|
||||
private static DateTime GetValueDateTime(XElement xelement)
|
||||
{
|
||||
return xelement != null ?
|
||||
DateTime.ParseExact(xelement.Value, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) :
|
||||
default;
|
||||
}
|
||||
|
||||
private static decimal GetValueDecimal(XElement xelement)
|
||||
{
|
||||
if (xelement == null || string.IsNullOrEmpty(xelement.Value))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
var sep = CultureInfo.InvariantCulture.NumberFormat.CurrencyDecimalSeparator;
|
||||
return decimal.TryParse(xelement.Value.Replace(".", sep).Replace(",", sep), NumberStyles.Currency, CultureInfo.InvariantCulture, out var value) ? value : default;
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
Close();
|
||||
}
|
||||
catch (CommunicationException)
|
||||
{
|
||||
Abort();
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Abort();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Abort();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -34,24 +34,22 @@ namespace ASC.Core.Billing
|
||||
[Scope(typeof(ConfigureTariffService))]
|
||||
public interface ITariffService
|
||||
{
|
||||
Tariff GetTariff(int tenantId, bool withRequestToPaymentSystem = true);
|
||||
|
||||
void SetTariff(int tenantId, Tariff tariff);
|
||||
|
||||
void DeleteDefaultBillingInfo();
|
||||
|
||||
void ClearCache(int tenantId);
|
||||
|
||||
IEnumerable<PaymentInfo> GetPayments(int tenantId, DateTime from, DateTime to);
|
||||
|
||||
Uri GetShoppingUri(int? tenant, int quotaId, string affiliateId, string currency = null, string language = null, string customerId = null);
|
||||
|
||||
IDictionary<string, IEnumerable<Tuple<string, decimal>>> GetProductPriceInfo(params string[] productIds);
|
||||
|
||||
Invoice GetInvoice(string paymentId);
|
||||
|
||||
string GetButton(int tariffId, string partnerId);
|
||||
|
||||
Tariff GetTariff(int tenantId, bool withRequestToPaymentSystem = true);
|
||||
|
||||
void SetTariff(int tenantId, Tariff tariff);
|
||||
|
||||
void DeleteDefaultBillingInfo();
|
||||
|
||||
void ClearCache(int tenantId);
|
||||
|
||||
IEnumerable<PaymentInfo> GetPayments(int tenantId);
|
||||
|
||||
Uri GetShoppingUri(int? tenant, int quotaId, string affiliateId, string currency = null, string language = null, string customerId = null, string quantity = null);
|
||||
|
||||
IDictionary<string, Dictionary<string, decimal>> GetProductPriceInfo(params string[] productIds);
|
||||
|
||||
string GetButton(int tariffId, string partnerId);
|
||||
|
||||
void SaveButton(int tariffId, string partnerId, string buttonUrl);
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ServiceModel;
|
||||
|
||||
using ASC.Core.Tenants;
|
||||
|
||||
namespace ASC.Core.Billing
|
||||
{
|
||||
[ServiceContract]
|
||||
public interface ITariffSyncService
|
||||
{
|
||||
[OperationContract]
|
||||
IEnumerable<TenantQuota> GetTariffs(int version, string key);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
namespace ASC.Core.Billing
|
||||
{
|
||||
public class Invoice
|
||||
{
|
||||
public string Sale
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string Refund
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
@ -229,6 +229,8 @@ namespace ASC.Core.Billing
|
||||
CountPortals = license.PortalCount,
|
||||
DiscEncryption = true,
|
||||
PrivacyRoom = true,
|
||||
Restore = true,
|
||||
ContentSearch = true
|
||||
};
|
||||
|
||||
if (defaultQuota.Name != "overdue" && !defaultQuota.Trial)
|
||||
|
@ -48,5 +48,17 @@ namespace ASC.Core.Billing
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int PaymentStatus
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public int Quantity
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,34 +35,28 @@ namespace ASC.Core.Billing
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
public string PaymentType { get; set; }
|
||||
|
||||
public double ExchangeRate { get; set; }
|
||||
|
||||
public double GrossSum { get; set; }
|
||||
public int PaymentSystemId { get; set; }
|
||||
|
||||
public string CartId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string FName { get; set; }
|
||||
|
||||
public string LName { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public DateTime Date { get; set; }
|
||||
public DateTime PaymentDate { get; set; }
|
||||
|
||||
public decimal Price { get; set; }
|
||||
public Decimal Price { get; set; }
|
||||
|
||||
public string Currency { get; set; }
|
||||
public string PaymentCurrency { get; set; }
|
||||
|
||||
public string Method { get; set; }
|
||||
public string PaymentMethod { get; set; }
|
||||
|
||||
public int QuotaId { get; set; }
|
||||
|
||||
public string ProductId { get; set; }
|
||||
public string ProductRef { get; set; }
|
||||
|
||||
public string TenantID { get; set; }
|
||||
|
||||
public string Country { get; set; }
|
||||
|
||||
public decimal DiscountSum { get; set; }
|
||||
public string CustomerId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -47,8 +47,9 @@ namespace ASC.Core.Billing
|
||||
|
||||
public bool Autorenewal { get; set; }
|
||||
|
||||
public bool Prolongable { get; set; }
|
||||
|
||||
public bool Prolongable { get; set; }
|
||||
|
||||
public int Quantity { get; set; }
|
||||
|
||||
public static Tariff CreateDefault()
|
||||
{
|
||||
@ -59,6 +60,7 @@ namespace ASC.Core.Billing
|
||||
DueDate = DateTime.MaxValue,
|
||||
DelayDueDate = DateTime.MaxValue,
|
||||
LicenseDate = DateTime.MaxValue,
|
||||
Quantity = 1
|
||||
};
|
||||
}
|
||||
|
||||
@ -71,6 +73,14 @@ namespace ASC.Core.Billing
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Tariff t && t.QuotaId == QuotaId;
|
||||
}
|
||||
|
||||
public bool EqualsByParams(Tariff t)
|
||||
{
|
||||
return t != null
|
||||
&& t.QuotaId == QuotaId
|
||||
&& t.DueDate == DueDate
|
||||
&& t.Quantity == Quantity;
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using ASC.Common;
|
||||
using ASC.Common.Caching;
|
||||
@ -37,7 +38,8 @@ using ASC.Common.Logging;
|
||||
using ASC.Core.Caching;
|
||||
using ASC.Core.Common.EF;
|
||||
using ASC.Core.Tenants;
|
||||
|
||||
using ASC.Core.Users;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@ -57,7 +59,7 @@ namespace ASC.Core.Billing
|
||||
{
|
||||
Cache.Remove(TariffService.GetTariffCacheKey(i.TenantId));
|
||||
Cache.Remove(TariffService.GetBillingUrlCacheKey(i.TenantId));
|
||||
Cache.Remove(TariffService.GetBillingPaymentCacheKey(i.TenantId, DateTime.MinValue, DateTime.MaxValue)); // clear all payments
|
||||
Cache.Remove(TariffService.GetBillingPaymentCacheKey(i.TenantId)); // clear all payments
|
||||
}, CacheNotifyAction.Remove);
|
||||
|
||||
//TODO: Change code of WCF -> not supported in .NET standard/.Net Core
|
||||
@ -143,8 +145,6 @@ namespace ASC.Core.Billing
|
||||
private static readonly TimeSpan DEFAULT_CACHE_EXPIRATION = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan STANDALONE_CACHE_EXPIRATION = TimeSpan.FromMinutes(15);
|
||||
|
||||
private readonly static bool billingConfigured = false;
|
||||
|
||||
internal ICache Cache { get; set; }
|
||||
internal ICacheNotify<TariffCacheItem> Notify { get; set; }
|
||||
internal ILog Log { get; set; }
|
||||
@ -161,6 +161,10 @@ namespace ASC.Core.Billing
|
||||
internal Lazy<CoreDbContext> LazyCoreDbContext { get; set; }
|
||||
internal TariffServiceStorage TariffServiceStorage { get; set; }
|
||||
internal IOptionsMonitor<ILog> Options { get; set; }
|
||||
public BillingClient BillingClient { get; }
|
||||
|
||||
public readonly int ACTIVE_USERS_MIN;
|
||||
public readonly int ACTIVE_USERS_MAX;
|
||||
|
||||
public TariffService()
|
||||
{
|
||||
@ -175,7 +179,9 @@ namespace ASC.Core.Billing
|
||||
IConfiguration configuration,
|
||||
DbContextManager<CoreDbContext> coreDbContextManager,
|
||||
TariffServiceStorage tariffServiceStorage,
|
||||
IOptionsMonitor<ILog> options)
|
||||
IOptionsMonitor<ILog> options,
|
||||
Constants constants,
|
||||
BillingClient billingClient)
|
||||
: this()
|
||||
|
||||
{
|
||||
@ -185,7 +191,8 @@ namespace ASC.Core.Billing
|
||||
CoreSettings = coreSettings;
|
||||
Configuration = configuration;
|
||||
TariffServiceStorage = tariffServiceStorage;
|
||||
Options = options;
|
||||
Options = options;
|
||||
BillingClient = billingClient;
|
||||
CoreBaseSettings = coreBaseSettings;
|
||||
Test = configuration["core:payment:test"] == "true";
|
||||
int.TryParse(configuration["core:payment:delay"], out var paymentDelay);
|
||||
@ -194,7 +201,16 @@ namespace ASC.Core.Billing
|
||||
|
||||
Cache = TariffServiceStorage.Cache;
|
||||
Notify = TariffServiceStorage.Notify;
|
||||
LazyCoreDbContext = new Lazy<CoreDbContext>(() => coreDbContextManager.Value);
|
||||
LazyCoreDbContext = new Lazy<CoreDbContext>(() => coreDbContextManager.Value);
|
||||
var range = (Configuration["core.payment-user-range"] ?? "").Split('-');
|
||||
if (!int.TryParse(range[0], out ACTIVE_USERS_MIN))
|
||||
{
|
||||
ACTIVE_USERS_MIN = 0;
|
||||
}
|
||||
if (range.Length < 2 || !int.TryParse(range[1], out ACTIVE_USERS_MAX))
|
||||
{
|
||||
ACTIVE_USERS_MAX = constants.MaxEveryoneCount;
|
||||
}
|
||||
}
|
||||
|
||||
public Tariff GetTariff(int tenantId, bool withRequestToPaymentSystem = true)
|
||||
@ -207,46 +223,50 @@ namespace ASC.Core.Billing
|
||||
var tariff = Cache.Get<Tariff>(key);
|
||||
if (tariff == null)
|
||||
{
|
||||
tariff = Tariff.CreateDefault();
|
||||
|
||||
var cached = GetBillingInfo(tenantId);
|
||||
if (cached != null)
|
||||
{
|
||||
tariff.QuotaId = cached.Item1;
|
||||
tariff.DueDate = cached.Item2;
|
||||
}
|
||||
|
||||
tariff = GetBillingInfo(tenantId);
|
||||
tariff = CalculateTariff(tenantId, tariff);
|
||||
Cache.Insert(key, tariff, DateTime.UtcNow.Add(GetCacheExpiration()));
|
||||
|
||||
if (billingConfigured && withRequestToPaymentSystem)
|
||||
if (BillingClient.Configured && withRequestToPaymentSystem)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = GetBillingClient();
|
||||
var p = client.GetLastPayment(GetPortalId(tenantId));
|
||||
var quota = QuotaService.GetTenantQuotas().SingleOrDefault(q => q.AvangateId == p.ProductId);
|
||||
var client = GetBillingClient();
|
||||
var lastPayment = client.GetLastPayment(GetPortalId(tenantId));
|
||||
|
||||
var quota = QuotaService.GetTenantQuotas().SingleOrDefault(q => q.AvangateId == lastPayment.ProductId);
|
||||
if (quota == null)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format("Quota with id {0} not found for portal {1}.", p.ProductId, GetPortalId(tenantId)));
|
||||
throw new InvalidOperationException(string.Format("Quota with id {0} not found for portal {1}.", lastPayment.ProductId, GetPortalId(tenantId)));
|
||||
}
|
||||
|
||||
var asynctariff = Tariff.CreateDefault();
|
||||
asynctariff.QuotaId = quota.Id;
|
||||
asynctariff.Autorenewal = p.Autorenewal;
|
||||
asynctariff.DueDate = 9999 <= p.EndDate.Year ? DateTime.MaxValue : p.EndDate;
|
||||
asynctariff.Autorenewal = lastPayment.Autorenewal;
|
||||
asynctariff.DueDate = 9999 <= lastPayment.EndDate.Year ? DateTime.MaxValue : lastPayment.EndDate;
|
||||
|
||||
if (SaveBillingInfo(tenantId, Tuple.Create(asynctariff.QuotaId, asynctariff.DueDate), false))
|
||||
if (quota.ActiveUsers == -1
|
||||
&& lastPayment.Quantity < ACTIVE_USERS_MIN)
|
||||
{
|
||||
throw new BillingException(string.Format("The portal {0} is paid for {1} users", tenantId, lastPayment.Quantity));
|
||||
}
|
||||
asynctariff.Quantity = lastPayment.Quantity;
|
||||
|
||||
if (SaveBillingInfo(tenantId, asynctariff, false))
|
||||
{
|
||||
asynctariff = CalculateTariff(tenantId, asynctariff);
|
||||
ClearCache(tenantId);
|
||||
Cache.Insert(key, asynctariff, DateTime.UtcNow.Add(GetCacheExpiration()));
|
||||
}
|
||||
}
|
||||
catch (BillingNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
LogError(error);
|
||||
LogError(error, tenantId.ToString());
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -264,7 +284,7 @@ namespace ASC.Core.Billing
|
||||
|
||||
var q = QuotaService.GetTenantQuota(tariff.QuotaId);
|
||||
if (q == null) return;
|
||||
SaveBillingInfo(tenantId, Tuple.Create(tariff.QuotaId, tariff.DueDate));
|
||||
SaveBillingInfo(tenantId, tariff);
|
||||
if (q.Trial)
|
||||
{
|
||||
// reset trial date
|
||||
@ -289,9 +309,9 @@ namespace ASC.Core.Billing
|
||||
return string.Format("{0}:{1}", tenantId, "billing:urls");
|
||||
}
|
||||
|
||||
internal static string GetBillingPaymentCacheKey(int tenantId, DateTime from, DateTime to)
|
||||
{
|
||||
return string.Format("{0}:{1}:{2}-{3}", tenantId, "billing:payments", from.ToString("yyyyMMddHHmmss"), to.ToString("yyyyMMddHHmmss"));
|
||||
internal static string GetBillingPaymentCacheKey(int tenantId)
|
||||
{
|
||||
return string.Format("{0}:{1}", tenantId, "billing:payments");
|
||||
}
|
||||
|
||||
|
||||
@ -299,45 +319,43 @@ namespace ASC.Core.Billing
|
||||
{
|
||||
Notify.Publish(new TariffCacheItem { TenantId = tenantId }, CacheNotifyAction.Remove);
|
||||
}
|
||||
|
||||
public IEnumerable<PaymentInfo> GetPayments(int tenantId)
|
||||
{
|
||||
var key = GetBillingPaymentCacheKey(tenantId);
|
||||
var payments = Cache.Get<List<PaymentInfo>>(key);
|
||||
if (payments == null)
|
||||
{
|
||||
payments = new List<PaymentInfo>();
|
||||
if (BillingClient.Configured)
|
||||
{
|
||||
try
|
||||
{
|
||||
var quotas = QuotaService.GetTenantQuotas();
|
||||
var client = GetBillingClient();
|
||||
foreach (var pi in client.GetPayments(GetPortalId(tenantId)))
|
||||
{
|
||||
var quota = quotas.SingleOrDefault(q => q.AvangateId == pi.ProductRef);
|
||||
if (quota != null)
|
||||
{
|
||||
pi.QuotaId = quota.Id;
|
||||
}
|
||||
payments.Add(pi);
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
LogError(error, tenantId.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
Cache.Insert(key, payments, DateTime.UtcNow.Add(TimeSpan.FromMinutes(10)));
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
|
||||
public IEnumerable<PaymentInfo> GetPayments(int tenantId, DateTime from, DateTime to)
|
||||
{
|
||||
from = from.Date;
|
||||
to = to.Date.AddTicks(TimeSpan.TicksPerDay - 1);
|
||||
var key = GetBillingPaymentCacheKey(tenantId, from, to);
|
||||
var payments = Cache.Get<List<PaymentInfo>>(key);
|
||||
if (payments == null)
|
||||
{
|
||||
payments = new List<PaymentInfo>();
|
||||
if (billingConfigured)
|
||||
{
|
||||
try
|
||||
{
|
||||
var quotas = QuotaService.GetTenantQuotas();
|
||||
using var client = GetBillingClient();
|
||||
foreach (var pi in client.GetPayments(GetPortalId(tenantId), from, to))
|
||||
{
|
||||
var quota = quotas.SingleOrDefault(q => q.AvangateId == pi.ProductId);
|
||||
if (quota != null)
|
||||
{
|
||||
pi.QuotaId = quota.Id;
|
||||
}
|
||||
payments.Add(pi);
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
LogError(error);
|
||||
}
|
||||
}
|
||||
|
||||
Cache.Insert(key, payments, DateTime.UtcNow.Add(TimeSpan.FromMinutes(10)));
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
|
||||
public Uri GetShoppingUri(int? tenant, int quotaId, string affiliateId, string currency = null, string language = null, string customerId = null)
|
||||
public Uri GetShoppingUri(int? tenant, int quotaId, string affiliateId, string currency = null, string language = null, string customerId = null, string quantity = null)
|
||||
{
|
||||
var quota = QuotaService.GetTenantQuota(quotaId);
|
||||
if (quota == null) return null;
|
||||
@ -349,19 +367,27 @@ namespace ASC.Core.Billing
|
||||
if (!(Cache.Get<Dictionary<string, Tuple<Uri, Uri>>>(key) is IDictionary<string, Tuple<Uri, Uri>> urls))
|
||||
{
|
||||
urls = new Dictionary<string, Tuple<Uri, Uri>>();
|
||||
if (billingConfigured)
|
||||
if (BillingClient.Configured)
|
||||
{
|
||||
try
|
||||
{
|
||||
var products = QuotaService.GetTenantQuotas()
|
||||
.Where(q => !string.IsNullOrEmpty(q.AvangateId) && q.Visible == quota.Visible)
|
||||
.Select(q => q.AvangateId)
|
||||
.ToArray();
|
||||
|
||||
using var client = GetBillingClient();
|
||||
urls = tenant.HasValue ?
|
||||
client.GetPaymentUrls(GetPortalId(tenant.Value), products, GetAffiliateId(tenant.Value), GetCampaign(tenant.Value), "__Currency__", "__Language__", "__CustomerID__") :
|
||||
client.GetPaymentUrls(null, products, !string.IsNullOrEmpty(affiliateId) ? affiliateId : null, null, "__Currency__", "__Language__", "__CustomerID__");
|
||||
.ToArray();
|
||||
|
||||
var client = GetBillingClient();
|
||||
urls =
|
||||
client.GetPaymentUrls(
|
||||
tenant.HasValue ? GetPortalId(tenant.Value) : null,
|
||||
products,
|
||||
tenant.HasValue ? GetAffiliateId(tenant.Value) : affiliateId,
|
||||
tenant.HasValue ? GetCampaign(tenant.Value) : null,
|
||||
!string.IsNullOrEmpty(currency) ? "__Currency__" : null,
|
||||
!string.IsNullOrEmpty(language) ? "__Language__" : null,
|
||||
!string.IsNullOrEmpty(customerId) ? "__CustomerID__" : null,
|
||||
!string.IsNullOrEmpty(quantity) ? "__Quantity__" : null
|
||||
);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
@ -373,71 +399,60 @@ namespace ASC.Core.Billing
|
||||
|
||||
ResetCacheExpiration();
|
||||
|
||||
if (!string.IsNullOrEmpty(quota.AvangateId) && urls.TryGetValue(quota.AvangateId, out var tuple))
|
||||
{
|
||||
var result = tuple.Item2;
|
||||
if (!string.IsNullOrEmpty(quota.AvangateId) && urls.TryGetValue(quota.AvangateId, out var tuple))
|
||||
{
|
||||
var result = tuple.Item2;
|
||||
|
||||
var tariff = tenant.HasValue ? GetTariff(tenant.Value) : null;
|
||||
if (result == null || tariff == null || tariff.QuotaId == quotaId || tariff.State >= TariffState.Delay)
|
||||
{
|
||||
result = tuple.Item1;
|
||||
}
|
||||
|
||||
result = new Uri(result.ToString()
|
||||
.Replace("__Currency__", currency ?? "")
|
||||
.Replace("__Language__", (language ?? "").ToLower())
|
||||
.Replace("__CustomerID__", customerId ?? ""));
|
||||
return result;
|
||||
if (result == null)
|
||||
{
|
||||
result = tuple.Item1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var tariff = tenant.HasValue ? GetTariff(tenant.Value) : null;
|
||||
if (tariff == null || tariff.QuotaId == quotaId || tariff.State >= TariffState.Delay)
|
||||
{
|
||||
result = tuple.Item1;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null) return null;
|
||||
|
||||
result = new Uri(result.ToString()
|
||||
.Replace("__Currency__", HttpUtility.UrlEncode(currency ?? ""))
|
||||
.Replace("__Language__", HttpUtility.UrlEncode((language ?? "").ToLower()))
|
||||
.Replace("__CustomerID__", HttpUtility.UrlEncode(customerId ?? ""))
|
||||
.Replace("__Quantity__", HttpUtility.UrlEncode(quantity ?? "")));
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public IDictionary<string, IEnumerable<Tuple<string, decimal>>> GetProductPriceInfo(params string[] productIds)
|
||||
{
|
||||
if (productIds == null)
|
||||
{
|
||||
throw new ArgumentNullException("productIds");
|
||||
}
|
||||
try
|
||||
{
|
||||
var key = "biling-prices" + string.Join(",", productIds);
|
||||
var result = Cache.Get<IDictionary<string, IEnumerable<Tuple<string, decimal>>>>(key);
|
||||
if (result == null)
|
||||
{
|
||||
using (var client = GetBillingClient())
|
||||
{
|
||||
result = client.GetProductPriceInfo(productIds);
|
||||
}
|
||||
Cache.Insert(key, result, DateTime.Now.AddHours(1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
LogError(error);
|
||||
return productIds
|
||||
.Select(p => new { ProductId = p, Prices = Enumerable.Empty<Tuple<string, decimal>>() })
|
||||
.ToDictionary(e => e.ProductId, e => e.Prices);
|
||||
}
|
||||
}
|
||||
|
||||
public Invoice GetInvoice(string paymentId)
|
||||
{
|
||||
var result = new Invoice();
|
||||
|
||||
if (billingConfigured)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = GetBillingClient();
|
||||
result = client.GetInvoice(paymentId);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
LogError(error);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
public IDictionary<string, Dictionary<string, decimal>> GetProductPriceInfo(params string[] productIds)
|
||||
{
|
||||
if (productIds == null)
|
||||
{
|
||||
throw new ArgumentNullException("productIds");
|
||||
}
|
||||
try
|
||||
{
|
||||
var key = "biling-prices" + string.Join(",", productIds);
|
||||
var result = Cache.Get<IDictionary<string, Dictionary<string, decimal>>>(key);
|
||||
if (result == null)
|
||||
{
|
||||
var client = GetBillingClient();
|
||||
result = client.GetProductPriceInfo(productIds);
|
||||
Cache.Insert(key, result, DateTime.Now.AddHours(1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
LogError(error);
|
||||
return productIds
|
||||
.Select(p => new { ProductId = p, Prices = new Dictionary<string, decimal>() })
|
||||
.ToDictionary(e => e.ProductId, e => e.Prices);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -463,34 +478,42 @@ namespace ASC.Core.Billing
|
||||
}
|
||||
|
||||
|
||||
private Tuple<int, DateTime> GetBillingInfo(int tenant)
|
||||
private Tariff GetBillingInfo(int tenant)
|
||||
{
|
||||
var r = CoreDbContext.Tariffs
|
||||
.Where(r => r.Tenant == tenant)
|
||||
.OrderByDescending(r => r.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
return r != null ? Tuple.Create(r.Tariff, r.Stamp.Year < 9999 ? r.Stamp : DateTime.MaxValue) : null;
|
||||
.SingleOrDefault();
|
||||
|
||||
if (r == null) return Tariff.CreateDefault();
|
||||
|
||||
var tariff = Tariff.CreateDefault();
|
||||
tariff.QuotaId = r.Tariff;
|
||||
tariff.DueDate = r.Stamp.Year < 9999 ? r.Stamp : DateTime.MaxValue;
|
||||
tariff.Quantity = r.Quantity;
|
||||
return tariff;
|
||||
}
|
||||
|
||||
private bool SaveBillingInfo(int tenant, Tuple<int, DateTime> bi, bool renewal = true)
|
||||
private bool SaveBillingInfo(int tenant, Tariff tariffInfo, bool renewal = true)
|
||||
{
|
||||
var inserted = false;
|
||||
if (!Equals(bi, GetBillingInfo(tenant)))
|
||||
var inserted = false;
|
||||
var currentTariff = GetBillingInfo(tenant);
|
||||
if (!tariffInfo.EqualsByParams(currentTariff))
|
||||
{
|
||||
using var tx = CoreDbContext.Database.BeginTransaction();
|
||||
|
||||
// last record is not the same
|
||||
var count = CoreDbContext.Tariffs
|
||||
.Count(r => r.Tenant == tenant && r.Tariff == bi.Item1 && r.Stamp == bi.Item2);
|
||||
|
||||
if (bi.Item2 == DateTime.MaxValue || renewal || count == 0)
|
||||
.Count(r => r.Tenant == tenant && r.Tariff == tariffInfo.QuotaId && r.Stamp == tariffInfo.DueDate && r.Quantity == tariffInfo.Quantity);
|
||||
|
||||
if (tariffInfo.DueDate == DateTime.MaxValue || renewal || count == 0)
|
||||
{
|
||||
var efTariff = new DbTariff
|
||||
{
|
||||
Tenant = tenant,
|
||||
Tariff = bi.Item1,
|
||||
Stamp = bi.Item2,
|
||||
Tariff = tariffInfo.QuotaId,
|
||||
Stamp = tariffInfo.DueDate,
|
||||
Quantity = tariffInfo.Quantity,
|
||||
CreateOn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
@ -617,7 +640,7 @@ namespace ASC.Core.Billing
|
||||
{
|
||||
try
|
||||
{
|
||||
return new BillingClient(Test, Configuration, Options);
|
||||
return new BillingClient(Test, Configuration);
|
||||
}
|
||||
catch (InvalidOperationException ioe)
|
||||
{
|
||||
@ -665,27 +688,28 @@ namespace ASC.Core.Billing
|
||||
}
|
||||
}
|
||||
|
||||
private void LogError(Exception error)
|
||||
{
|
||||
if (error is BillingNotFoundException)
|
||||
{
|
||||
Log.DebugFormat("Payment not found: {0}", error.Message);
|
||||
}
|
||||
else if (error is BillingNotConfiguredException)
|
||||
{
|
||||
Log.DebugFormat("Billing not configured: {0}", error.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Log.IsDebugEnabled)
|
||||
{
|
||||
Log.Error(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error(error.Message);
|
||||
}
|
||||
}
|
||||
private void LogError(Exception error, string tenantId = null)
|
||||
{
|
||||
if (error is BillingNotFoundException)
|
||||
{
|
||||
Log.DebugFormat("Payment tenant {0} not found: {1}", tenantId, error.Message);
|
||||
}
|
||||
else if (error is BillingNotConfiguredException)
|
||||
{
|
||||
Log.DebugFormat("Billing tenant {0} not configured: {1}", tenantId, error.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Log.IsDebugEnabled)
|
||||
{
|
||||
Log.Error("Billing tenant " + tenantId);
|
||||
Log.Error(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.ErrorFormat("Billing tenant {0}: {1}", tenantId, error.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
using ASC.Common.Module;
|
||||
using ASC.Core.Tenants;
|
||||
|
||||
namespace ASC.Core.Billing
|
||||
{
|
||||
public class TariffSyncClient : BaseWcfClient<ITariffSyncService>, ITariffSyncService
|
||||
{
|
||||
public IEnumerable<TenantQuota> GetTariffs(int version, string key)
|
||||
{
|
||||
return Channel.GetTariffs(version, key);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Common.Module;
|
||||
using ASC.Common.Utils;
|
||||
using ASC.Core.Data;
|
||||
using ASC.Core.Tenants;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ASC.Core.Billing
|
||||
{
|
||||
class TariffSyncService : ITariffSyncService, IServiceController
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly TariffSyncServiceSection config;
|
||||
private readonly IDictionary<int, IEnumerable<TenantQuota>> quotaServices = new Dictionary<int, IEnumerable<TenantQuota>>();
|
||||
private Timer timer;
|
||||
|
||||
|
||||
public TariffSyncService(
|
||||
IServiceProvider serviceProvider,
|
||||
ConfigurationExtension configuration,
|
||||
DbQuotaService dbQuotaService,
|
||||
IOptionsMonitor<ILog> options)
|
||||
{
|
||||
config = TariffSyncServiceSection.GetSection();
|
||||
ServiceProvider = serviceProvider;
|
||||
Configuration = configuration;
|
||||
DbQuotaService = dbQuotaService;
|
||||
log = options.CurrentValue;
|
||||
}
|
||||
|
||||
|
||||
// server part of service
|
||||
public IEnumerable<TenantQuota> GetTariffs(int version, string key)
|
||||
{
|
||||
lock (quotaServices)
|
||||
{
|
||||
if (!quotaServices.ContainsKey(version))
|
||||
{
|
||||
var cs = Configuration.GetConnectionStrings(config.ConnectionStringName + version) ??
|
||||
Configuration.GetConnectionStrings(config.ConnectionStringName);
|
||||
quotaServices[version] = DbQuotaService.GetTenantQuotas();
|
||||
}
|
||||
return quotaServices[version];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// client part of service
|
||||
public string ServiceName
|
||||
{
|
||||
get { return "Tariffs synchronizer"; }
|
||||
}
|
||||
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private ConfigurationExtension Configuration { get; }
|
||||
private DbQuotaService DbQuotaService { get; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (timer == null)
|
||||
{
|
||||
timer = new Timer(Sync, null, TimeSpan.Zero, config.Period);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (timer != null)
|
||||
{
|
||||
timer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
timer.Dispose();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void Sync(object _)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
var tariffSync = scope.ServiceProvider.GetService<TariffSync>();
|
||||
tariffSync.Sync();
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
log.Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TariffSync
|
||||
{
|
||||
public TariffSync(TenantManager tenantManager, CoreSettings coreSettings, DbQuotaService dbQuotaService)
|
||||
{
|
||||
TenantManager = tenantManager;
|
||||
CoreSettings = coreSettings;
|
||||
DbQuotaService = dbQuotaService;
|
||||
}
|
||||
|
||||
private TenantManager TenantManager { get; }
|
||||
private CoreSettings CoreSettings { get; }
|
||||
private DbQuotaService DbQuotaService { get; }
|
||||
|
||||
public void Sync()
|
||||
{
|
||||
var tenant = TenantManager.GetTenants(false).OrderByDescending(t => t.Version).FirstOrDefault();
|
||||
if (tenant != null)
|
||||
{
|
||||
using var wcfClient = new TariffSyncClient();
|
||||
var quotaService = DbQuotaService;
|
||||
|
||||
var oldtariffs = quotaService.GetTenantQuotas().ToDictionary(t => t.Id);
|
||||
// save new
|
||||
foreach (var tariff in wcfClient.GetTariffs(tenant.Version, CoreSettings.GetKey(tenant.TenantId)))
|
||||
{
|
||||
quotaService.SaveTenantQuota(tariff);
|
||||
oldtariffs.Remove(tariff.Id);
|
||||
}
|
||||
|
||||
// remove old
|
||||
foreach (var tariff in oldtariffs.Values)
|
||||
{
|
||||
tariff.Visible = false;
|
||||
quotaService.SaveTenantQuota(tariff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using System.Configuration;
|
||||
|
||||
namespace ASC.Core.Billing
|
||||
{
|
||||
class TariffSyncServiceSection : ConfigurationSection
|
||||
{
|
||||
[ConfigurationProperty("period", DefaultValue = "4:0:0")]
|
||||
public TimeSpan Period
|
||||
{
|
||||
get { return (TimeSpan)this["period"]; }
|
||||
set { this["period"] = value; }
|
||||
}
|
||||
|
||||
[ConfigurationProperty("connectionStringName", DefaultValue = "core")]
|
||||
public string ConnectionStringName
|
||||
{
|
||||
get { return (string)this["connectionStringName"]; }
|
||||
set { this["connectionStringName"] = value; }
|
||||
}
|
||||
|
||||
public static TariffSyncServiceSection GetSection()
|
||||
{
|
||||
return (TariffSyncServiceSection)ConfigurationManager.GetSection("tariffs") ?? new TariffSyncServiceSection();
|
||||
}
|
||||
}
|
||||
}
|
@ -82,37 +82,23 @@ namespace ASC.Core
|
||||
|
||||
public IEnumerable<PaymentInfo> GetTariffPayments(int tenant)
|
||||
{
|
||||
return GetTariffPayments(tenant, DateTime.MinValue, DateTime.MaxValue);
|
||||
}
|
||||
|
||||
public IEnumerable<PaymentInfo> GetTariffPayments(int tenant, DateTime from, DateTime to)
|
||||
{
|
||||
return tariffService.GetPayments(tenant, from, to);
|
||||
}
|
||||
|
||||
public Invoice GetPaymentInvoice(string paymentId)
|
||||
{
|
||||
return tariffService.GetInvoice(paymentId);
|
||||
return tariffService.GetPayments(tenant);
|
||||
}
|
||||
|
||||
public IDictionary<string, IEnumerable<Tuple<string, decimal>>> GetProductPriceInfo(params string[] productIds)
|
||||
public IDictionary<string, Dictionary<string, decimal>> GetProductPriceInfo(params string[] productIds)
|
||||
{
|
||||
return tariffService.GetProductPriceInfo(productIds);
|
||||
}
|
||||
|
||||
public Uri GetShoppingUri(int tenant, int quotaId, string currency = null, string language = null, string customerId = null)
|
||||
|
||||
public Uri GetShoppingUri(int quotaId, bool forCurrentTenant = true, string affiliateId = null, string currency = null, string language = null, string customerId = null, string quantity = null)
|
||||
{
|
||||
return tariffService.GetShoppingUri(tenant, quotaId, null, currency, language, customerId);
|
||||
return tariffService.GetShoppingUri(forCurrentTenant ? TenantManager.GetCurrentTenant().TenantId : (int?)null, quotaId, affiliateId, currency, language, customerId, quantity);
|
||||
}
|
||||
|
||||
public Uri GetShoppingUri(int quotaId, bool forCurrentTenant = true, string affiliateId = null, string currency = null, string language = null, string customerId = null)
|
||||
public Uri GetShoppingUri(int quotaId, string affiliateId, string currency = null, string language = null, string customerId = null, string quantity = null)
|
||||
{
|
||||
return tariffService.GetShoppingUri(forCurrentTenant ? TenantManager.GetCurrentTenant().TenantId : (int?)null, quotaId, affiliateId, currency, language, customerId);
|
||||
}
|
||||
|
||||
public Uri GetShoppingUri(int quotaId, string affiliateId, string currency = null, string language = null, string customerId = null)
|
||||
{
|
||||
return tariffService.GetShoppingUri(null, quotaId, affiliateId, currency, language, customerId);
|
||||
return tariffService.GetShoppingUri(null, quotaId, affiliateId, currency, language, customerId, quantity);
|
||||
}
|
||||
|
||||
public void ActivateKey(string key)
|
||||
|
@ -312,23 +312,31 @@ namespace ASC.Core
|
||||
{
|
||||
return QuotaService.GetTenantQuotas().Where(q => q.Id < 0 && (all || q.Visible)).OrderByDescending(q => q.Id).ToList();
|
||||
}
|
||||
|
||||
public TenantQuota GetTenantQuota(int tenant)
|
||||
{
|
||||
var defaultQuota = QuotaService.GetTenantQuota(tenant) ?? QuotaService.GetTenantQuota(Tenant.DEFAULT_TENANT) ?? TenantQuota.Default;
|
||||
if (defaultQuota.Id != tenant && TariffService != null)
|
||||
{
|
||||
var tariff = TariffService.GetTariff(tenant);
|
||||
var currentQuota = QuotaService.GetTenantQuota(tariff.QuotaId);
|
||||
if (currentQuota != null)
|
||||
{
|
||||
currentQuota = (TenantQuota)currentQuota.Clone();
|
||||
|
||||
if (currentQuota.ActiveUsers == -1)
|
||||
{
|
||||
currentQuota.ActiveUsers = tariff.Quantity;
|
||||
currentQuota.MaxTotalSize *= currentQuota.ActiveUsers;
|
||||
}
|
||||
|
||||
return currentQuota;
|
||||
}
|
||||
}
|
||||
return defaultQuota;
|
||||
}
|
||||
|
||||
public TenantQuota GetTenantQuota(int tenant)
|
||||
{
|
||||
// если в tenants_quota есть строка, с данным идентификатором портала, то в качестве квоты берется именно она
|
||||
var q = QuotaService.GetTenantQuota(tenant) ?? QuotaService.GetTenantQuota(Tenant.DEFAULT_TENANT) ?? TenantQuota.Default;
|
||||
if (q.Id != tenant && TariffService != null)
|
||||
{
|
||||
var tariffQuota = QuotaService.GetTenantQuota(TariffService.GetTariff(tenant).QuotaId);
|
||||
if (tariffQuota != null)
|
||||
{
|
||||
return tariffQuota;
|
||||
}
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
public IDictionary<string, IEnumerable<Tuple<string, decimal>>> GetProductPriceInfo(bool all = true)
|
||||
public IDictionary<string, Dictionary<string, decimal>> GetProductPriceInfo(bool all = true)
|
||||
{
|
||||
var productIds = GetTenantQuotas(all)
|
||||
.Select(p => p.AvangateId)
|
||||
|
@ -66,7 +66,7 @@ namespace ASC.Core
|
||||
private TenantManager TenantManager { get; }
|
||||
private PermissionContext PermissionContext { get; }
|
||||
private UserManagerConstants UserManagerConstants { get; }
|
||||
public CoreBaseSettings CoreBaseSettings { get; }
|
||||
private CoreBaseSettings CoreBaseSettings { get; }
|
||||
private Constants Constants { get; }
|
||||
|
||||
private Tenant tenant;
|
||||
@ -310,7 +310,52 @@ namespace ASC.Core
|
||||
var newUser = UserService.SaveUser(Tenant.TenantId, u);
|
||||
|
||||
return newUser;
|
||||
}
|
||||
}
|
||||
|
||||
public UserInfo SaveUserInfo(UserInfo u, bool isVisitor = false)
|
||||
{
|
||||
if (IsSystemUser(u.ID)) return SystemUsers[u.ID];
|
||||
if (u.ID == Guid.Empty) PermissionContext.DemandPermissions(Constants.Action_AddRemoveUser);
|
||||
else PermissionContext.DemandPermissions(new UserSecurityProvider(u.ID), Constants.Action_EditUser);
|
||||
|
||||
if (!CoreBaseSettings.Personal)
|
||||
{
|
||||
if (Constants.MaxEveryoneCount <= GetUsersByGroup(Constants.GroupEveryone.ID).Length)
|
||||
{
|
||||
throw new TenantQuotaException("Maximum number of users exceeded");
|
||||
}
|
||||
|
||||
if (u.Status == EmployeeStatus.Active)
|
||||
{
|
||||
if (isVisitor)
|
||||
{
|
||||
var maxUsers = TenantManager.GetTenantQuota(TenantManager.GetCurrentTenant().TenantId).ActiveUsers;
|
||||
var visitors = TenantManager.GetTenantQuota(TenantManager.GetCurrentTenant().TenantId).Free ? 0 : Constants.CoefficientOfVisitors;
|
||||
if (!CoreBaseSettings.Standalone && GetUsersByGroup(Constants.GroupVisitor.ID).Length > visitors * maxUsers)
|
||||
{
|
||||
throw new TenantQuotaException("Maximum number of visitors exceeded");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var q = TenantManager.GetTenantQuota(TenantManager.GetCurrentTenant().TenantId);
|
||||
if (q.ActiveUsers < GetUsersByGroup(Constants.GroupUser.ID).Length)
|
||||
{
|
||||
throw new TenantQuotaException(string.Format("Exceeds the maximum active users ({0})", q.ActiveUsers));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (u.Status == EmployeeStatus.Terminated && u.ID == TenantManager.GetCurrentTenant().OwnerId)
|
||||
{
|
||||
throw new InvalidOperationException("Can not disable tenant owner.");
|
||||
}
|
||||
|
||||
var newUser = UserService.SaveUser(TenantManager.GetCurrentTenant().TenantId, u);
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
public void DeleteUser(Guid id)
|
||||
{
|
||||
|
@ -224,7 +224,7 @@ namespace ASC.Core.Data
|
||||
var passwordHashs = usersQuery.Select(r => GetPasswordHash(r, passwordHash)).ToList();
|
||||
|
||||
q = query()
|
||||
.Where(r => passwordHashs.Any(p => r.UserSecurity.PwdHash == p));
|
||||
.Where(r => passwordHashs.Any(p => r.UserSecurity.PwdHash == p) && r.DbTenant.Status == TenantStatus.Active);
|
||||
|
||||
//new password
|
||||
result = result.Concat(q.Select(FromTenantUserToTenant)).ToList();
|
||||
|
@ -54,6 +54,11 @@ namespace ASC.Core.Common.EF.Model
|
||||
new FilesConverts { Input = ".epub", Output = ".pdf" },
|
||||
new FilesConverts { Input = ".epub", Output = ".rtf" },
|
||||
new FilesConverts { Input = ".epub", Output = ".txt" },
|
||||
new FilesConverts { Input = ".fb2", Output = ".docx" },
|
||||
new FilesConverts { Input = ".fb2", Output = ".odt" },
|
||||
new FilesConverts { Input = ".fb2", Output = ".pdf" },
|
||||
new FilesConverts { Input = ".fb2", Output = ".rtf" },
|
||||
new FilesConverts { Input = ".fb2", Output = ".txt" },
|
||||
new FilesConverts { Input = ".fodp", Output = ".odp" },
|
||||
new FilesConverts { Input = ".fodp", Output = ".pdf" },
|
||||
new FilesConverts { Input = ".fodp", Output = ".pptx" },
|
||||
|
@ -1,4 +1,5 @@
|
||||
using ASC.Core.Common.EF.Model;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ASC.Core.Common.EF
|
||||
@ -29,7 +30,7 @@ namespace ASC.Core.Common.EF
|
||||
.Add(MySqlAddDbQuota, Provider.MySql)
|
||||
.Add(PgSqlAddDbQuota, Provider.Postgre)
|
||||
.HasData(
|
||||
new DbQuota { Tenant = -1, Name = "default", Description = null, MaxFileSize = 102400, MaxTotalSize = 10995116277760, ActiveUsers = 10000, Features = "docs,domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption", Price = decimal.Parse("0,00"), Price2 = decimal.Parse("0,00"), AvangateId = "0", Visible = false }
|
||||
new DbQuota { Tenant = -1, Name = "default", Description = null, MaxFileSize = 102400, MaxTotalSize = 10995116277760, ActiveUsers = 10000, Features = "domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption,privacyroom,restore", Price = decimal.Parse("0,00"), Price2 = decimal.Parse("0,00"), AvangateId = "0", Visible = false }
|
||||
);
|
||||
|
||||
return modelBuilder;
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
using ASC.Core.Common.EF.Model;
|
||||
|
||||
@ -13,6 +12,7 @@ namespace ASC.Core.Common.EF
|
||||
public int Tenant { get; set; }
|
||||
public int Tariff { get; set; }
|
||||
public DateTime Stamp { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
public string Comment { get; set; }
|
||||
public DateTime CreateOn { get; set; }
|
||||
}
|
||||
@ -48,6 +48,10 @@ namespace ASC.Core.Common.EF
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP")
|
||||
.ValueGeneratedOnAddOrUpdate();
|
||||
|
||||
entity.Property(e => e.Quantity)
|
||||
.HasColumnName("quantity")
|
||||
.HasColumnType("int");
|
||||
|
||||
entity.Property(e => e.Stamp)
|
||||
.HasColumnName("stamp")
|
||||
.HasColumnType("datetime");
|
||||
@ -79,6 +83,8 @@ namespace ASC.Core.Common.EF
|
||||
|
||||
entity.Property(e => e.Stamp).HasColumnName("stamp");
|
||||
|
||||
entity.Property(e => e.Quantity).HasColumnName("quantity");
|
||||
|
||||
entity.Property(e => e.Tariff).HasColumnName("tariff");
|
||||
|
||||
entity.Property(e => e.Tenant).HasColumnName("tenant");
|
||||
|
@ -676,7 +676,7 @@ namespace ASC.Core.Common.Migrations.MySql.CoreDbContextMySql
|
||||
Tenant = -1,
|
||||
ActiveUsers = 10000,
|
||||
AvangateId = "0",
|
||||
Features = "docs,domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption",
|
||||
Features = "domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption,privacyroom,restore",
|
||||
MaxFileSize = 102400L,
|
||||
MaxTotalSize = 10995116277760L,
|
||||
Name = "default",
|
||||
@ -744,13 +744,16 @@ namespace ASC.Core.Common.Migrations.MySql.CoreDbContextMySql
|
||||
|
||||
b.Property<DateTime>("Stamp")
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("stamp");
|
||||
.HasColumnName("stamp");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("quantity");
|
||||
|
||||
b.Property<int>("Tariff")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("tariff");
|
||||
|
||||
|
||||
b.Property<int>("Tenant")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("tenant");
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@ -92,7 +93,8 @@ namespace ASC.Core.Common.Migrations.MySql.CoreDbContextMySql
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
tenant = table.Column<int>(type: "int", nullable: false),
|
||||
tariff = table.Column<int>(type: "int", nullable: false),
|
||||
stamp = table.Column<DateTime>(type: "datetime", nullable: false),
|
||||
stamp = table.Column<DateTime>(type: "datetime", nullable: false),
|
||||
quantity = table.Column<int>(type: "int", nullable: false),
|
||||
tariff_key = table.Column<string>(type: "varchar(64)", nullable: true, collation: "utf8_general_ci")
|
||||
.Annotation("MySql:CharSet", "utf8"),
|
||||
comment = table.Column<string>(type: "varchar(255)", nullable: true, collation: "utf8_general_ci")
|
||||
@ -180,7 +182,7 @@ namespace ASC.Core.Common.Migrations.MySql.CoreDbContextMySql
|
||||
migrationBuilder.InsertData(
|
||||
table: "tenants_quota",
|
||||
columns: new[] { "tenant", "active_users", "avangate_id", "description", "features", "max_file_size", "max_total_size", "name", "price", "price2", "visible" },
|
||||
values: new object[] { -1, 10000, "0", null, "docs,domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption", 102400L, 10995116277760L, "default", 0.00m, 0.00m, false });
|
||||
values: new object[] { -1, 10000, "0", null, "domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption,privacyroom,restore", 102400L, 10995116277760L, "default", 0.00m, 0.00m, false });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "last_modified",
|
||||
|
@ -674,7 +674,7 @@ namespace ASC.Core.Common.Migrations.MySql.CoreDbContextMySql
|
||||
Tenant = -1,
|
||||
ActiveUsers = 10000,
|
||||
AvangateId = "0",
|
||||
Features = "docs,domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption",
|
||||
Features = "domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption,privacyroom,restore",
|
||||
MaxFileSize = 102400L,
|
||||
MaxTotalSize = 10995116277760L,
|
||||
Name = "default",
|
||||
@ -742,7 +742,11 @@ namespace ASC.Core.Common.Migrations.MySql.CoreDbContextMySql
|
||||
|
||||
b.Property<DateTime>("Stamp")
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("stamp");
|
||||
.HasColumnName("stamp");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("quantity");
|
||||
|
||||
b.Property<int>("Tariff")
|
||||
.HasColumnType("int")
|
||||
|
@ -229,6 +229,31 @@ namespace ASC.Core.Common.Migrations.MySql.FilesDbContextMySql
|
||||
Output = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".docx"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".odt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".pdf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".rtf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fodp",
|
||||
Output = ".odp"
|
||||
|
@ -167,7 +167,12 @@ namespace ASC.Core.Common.Migrations.MySql.FilesDbContextMySql
|
||||
{ ".fodt", ".txt" },
|
||||
{ ".html", ".docx" },
|
||||
{ ".fods", ".xlsx" },
|
||||
{ ".xps", ".pdf" }
|
||||
{ ".xps", ".pdf" },
|
||||
{ ".fb2", ".docx" },
|
||||
{ ".fb2", ".odt" },
|
||||
{ ".fb2", ".pdf" },
|
||||
{ ".fb2", ".rtf" },
|
||||
{ ".fb2", ".txt" }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -227,6 +227,31 @@ namespace ASC.Core.Common.Migrations.MySql.FilesDbContextMySql
|
||||
Output = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".docx"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".odt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".pdf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".rtf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Output = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fodp",
|
||||
Output = ".odp"
|
||||
|
@ -730,7 +730,11 @@ namespace ASC.Core.Common.Migrations.Npgsql.CoreDbContextNpgsql
|
||||
|
||||
b.Property<DateTime>("Stamp")
|
||||
.HasColumnName("stamp")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("quantity");
|
||||
|
||||
b.Property<int>("Tariff")
|
||||
.HasColumnName("tariff")
|
||||
|
@ -69,7 +69,7 @@ namespace ASC.Core.Common.Migrations.Npgsql.CoreDbContextNpgsql
|
||||
schema: "onlyoffice",
|
||||
table: "tenants_quota",
|
||||
columns: new[] { "tenant", "active_users", "avangate_id", "description", "features", "max_file_size", "max_total_size", "name", "visible" },
|
||||
values: new object[] { -1, 10000, "0", null, "docs,domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption", 102400L, 10995116277760L, "default", false });
|
||||
values: new object[] { -1, 10000, "0", null, "domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption,privacyroom,restore", 102400L, 10995116277760L, "default", false });
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tenants_quotarow",
|
||||
@ -96,7 +96,8 @@ namespace ASC.Core.Common.Migrations.Npgsql.CoreDbContextNpgsql
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
tenant = table.Column<int>(nullable: false),
|
||||
tariff = table.Column<int>(nullable: false),
|
||||
stamp = table.Column<DateTime>(nullable: false),
|
||||
stamp = table.Column<DateTime>(nullable: false),
|
||||
quantity = table.Column<int>(type: "int", nullable: false),
|
||||
comment = table.Column<string>(maxLength: 255, nullable: true, defaultValueSql: "NULL"),
|
||||
create_on = table.Column<DateTime>(nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
|
@ -671,7 +671,7 @@ namespace ASC.Core.Common.Migrations.Npgsql.CoreDbContextNpgsql
|
||||
Tenant = -1,
|
||||
ActiveUsers = 10000,
|
||||
AvangateId = "0",
|
||||
Features = "docs,domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption",
|
||||
Features = "domain,audit,controlpanel,healthcheck,ldap,sso,whitelabel,branding,ssbranding,update,support,portals:10000,discencryption,privacyroom,restore",
|
||||
MaxFileSize = 102400L,
|
||||
MaxTotalSize = 10995116277760L,
|
||||
Name = "default",
|
||||
@ -743,7 +743,11 @@ namespace ASC.Core.Common.Migrations.Npgsql.CoreDbContextNpgsql
|
||||
|
||||
b.Property<DateTime>("Stamp")
|
||||
.HasColumnName("stamp")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("int")
|
||||
.HasColumnName("quantity");
|
||||
|
||||
b.Property<int>("Tariff")
|
||||
.HasColumnName("tariff")
|
||||
|
@ -228,6 +228,31 @@ namespace ASC.Core.Common.Migrations.Npgsql.FilesDbContextNpgsql
|
||||
Ouput = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".docx"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".odt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".pdf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".rtf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fodp",
|
||||
Ouput = ".odp"
|
||||
|
@ -170,7 +170,12 @@ namespace ASC.Core.Common.Migrations.Npgsql.FilesDbContextNpgsql
|
||||
{ ".fodt", ".txt" },
|
||||
{ ".html", ".docx" },
|
||||
{ ".fods", ".xlsx" },
|
||||
{ ".xps", ".pdf" }
|
||||
{ ".xps", ".pdf" },
|
||||
{ ".fb2", ".docx" },
|
||||
{ ".fb2", ".odt" },
|
||||
{ ".fb2", ".pdf" },
|
||||
{ ".fb2", ".rtf" },
|
||||
{ ".fb2", ".txt" }
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -226,6 +226,31 @@ namespace ASC.Core.Common.Migrations.Npgsql.FilesDbContextNpgsql
|
||||
Ouput = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".docx"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".odt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".pdf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".rtf"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fb2",
|
||||
Ouput = ".txt"
|
||||
},
|
||||
new
|
||||
{
|
||||
Input = ".fodp",
|
||||
Ouput = ".odp"
|
||||
|
@ -33,9 +33,8 @@ using ASC.Common;
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Common.Utils;
|
||||
using ASC.Notify.Messages;
|
||||
using ASC.Notify.Patterns;
|
||||
using ASC.Notify.Patterns;
|
||||
|
||||
using MailKit;
|
||||
using MailKit.Security;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -287,10 +286,11 @@ namespace ASC.Core.Notify.Senders
|
||||
|
||||
var smtpClient = new MailKit.Net.Smtp.SmtpClient
|
||||
{
|
||||
ServerCertificateValidationCallback = (sender, certificate, chain, errors) =>
|
||||
sslCertificatePermit || MailService.DefaultServerCertificateValidationCallback(sender, certificate, chain, errors),
|
||||
Timeout = NETWORK_TIMEOUT
|
||||
};
|
||||
};
|
||||
|
||||
if (sslCertificatePermit)
|
||||
smtpClient.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
|
||||
|
||||
return smtpClient;
|
||||
}
|
||||
|
@ -191,7 +191,65 @@ namespace ASC.Core.Tenants
|
||||
{
|
||||
get { return GetFeature("privacyroom"); }
|
||||
set { SetFeature("privacyroom", value); }
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableMailServer
|
||||
{
|
||||
get { return GetFeature("mailserver"); }
|
||||
set { SetFeature("mailserver", value); }
|
||||
}
|
||||
|
||||
public int CountAdmin
|
||||
{
|
||||
get
|
||||
{
|
||||
var features = (Features ?? string.Empty).Split(' ', ',', ';').ToList();
|
||||
var admin = features.FirstOrDefault(f => f.StartsWith("admin:"));
|
||||
int countAdmin;
|
||||
if (admin == null || !int.TryParse(admin.Replace("admin:", ""), out countAdmin))
|
||||
{
|
||||
countAdmin = int.MaxValue;
|
||||
}
|
||||
return countAdmin;
|
||||
}
|
||||
set
|
||||
{
|
||||
var features = (Features ?? string.Empty).Split(' ', ',', ';').ToList();
|
||||
var admin = features.FirstOrDefault(f => f.StartsWith("admin:"));
|
||||
features.Remove(admin);
|
||||
if (value > 0)
|
||||
{
|
||||
features.Add("admin:" + value);
|
||||
}
|
||||
Features = string.Join(",", features.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public bool Restore
|
||||
{
|
||||
get { return GetFeature("restore"); }
|
||||
set { SetFeature("restore", value); }
|
||||
}
|
||||
|
||||
public bool AutoBackup
|
||||
{
|
||||
get { return GetFeature("autobackup"); }
|
||||
set { SetFeature("autobackup", value); }
|
||||
}
|
||||
|
||||
public bool Oauth
|
||||
{
|
||||
get { return GetFeature("oauth"); }
|
||||
set { SetFeature("oauth", value); }
|
||||
}
|
||||
|
||||
public bool ContentSearch
|
||||
{
|
||||
get { return GetFeature("contentsearch"); }
|
||||
set { SetFeature("contentsearch", value); }
|
||||
}
|
||||
|
||||
|
||||
public int CountPortals
|
||||
{
|
||||
get
|
||||
@ -215,8 +273,13 @@ namespace ASC.Core.Tenants
|
||||
}
|
||||
Features = string.Join(",", features.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public bool ThirdParty
|
||||
{
|
||||
get { return GetFeature("thirdparty"); }
|
||||
set { SetFeature("thirdparty", value); }
|
||||
}
|
||||
|
||||
public TenantQuota()
|
||||
{
|
||||
|
@ -1,143 +1,143 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
///*
|
||||
// *
|
||||
// * (c) Copyright Ascensio System Limited 2010-2018
|
||||
// *
|
||||
// * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
// * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
// * In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
// *
|
||||
// * You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
// *
|
||||
// * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
// * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
// *
|
||||
// * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
// * relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
// * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
// * in every copy of the program you distribute.
|
||||
// * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
// *
|
||||
//*/
|
||||
|
||||
|
||||
#if DEBUG
|
||||
namespace ASC.Core.Common.Tests
|
||||
{
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
//#if DEBUG
|
||||
//namespace ASC.Core.Common.Tests
|
||||
//{
|
||||
// using System.Linq;
|
||||
// using System.Text.Json;
|
||||
|
||||
using ASC.Core.Billing;
|
||||
using ASC.Core.Data;
|
||||
using ASC.Core.Tenants;
|
||||
// using ASC.Core.Billing;
|
||||
// using ASC.Core.Data;
|
||||
// using ASC.Core.Tenants;
|
||||
|
||||
using NUnit.Framework;
|
||||
// using NUnit.Framework;
|
||||
|
||||
[TestFixture]
|
||||
class DbQuotaServiceTest : DbBaseTest<DbQuotaService>
|
||||
{
|
||||
public DbQuotaServiceTest()
|
||||
{
|
||||
}
|
||||
// [TestFixture]
|
||||
// class DbQuotaServiceTest : DbBaseTest<DbQuotaService>
|
||||
// {
|
||||
// public DbQuotaServiceTest()
|
||||
// {
|
||||
// }
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void ClearData()
|
||||
{
|
||||
Service.RemoveTenantQuota(Tenant);
|
||||
foreach (var row in Service.FindTenantQuotaRows(Tenant))
|
||||
{
|
||||
//DeleteQuotaRow(row);
|
||||
}
|
||||
}
|
||||
// [OneTimeSetUp]
|
||||
// public void ClearData()
|
||||
// {
|
||||
// Service.RemoveTenantQuota(Tenant);
|
||||
// foreach (var row in Service.FindTenantQuotaRows(Tenant))
|
||||
// {
|
||||
// //DeleteQuotaRow(row);
|
||||
// }
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void QuotaMethod()
|
||||
{
|
||||
var quota1 = new TenantQuota(Tenant)
|
||||
{
|
||||
MaxFileSize = 3,
|
||||
MaxTotalSize = 4,
|
||||
ActiveUsers = 30,
|
||||
};
|
||||
Service.SaveTenantQuota(quota1);
|
||||
CompareQuotas(quota1, Service.GetTenantQuota(quota1.Id));
|
||||
// [Test]
|
||||
// public void QuotaMethod()
|
||||
// {
|
||||
// var quota1 = new TenantQuota(Tenant)
|
||||
// {
|
||||
// MaxFileSize = 3,
|
||||
// MaxTotalSize = 4,
|
||||
// ActiveUsers = 30,
|
||||
// };
|
||||
// Service.SaveTenantQuota(quota1);
|
||||
// CompareQuotas(quota1, Service.GetTenantQuota(quota1.Id));
|
||||
|
||||
Service.RemoveTenantQuota(Tenant);
|
||||
Assert.IsNull(Service.GetTenantQuota(quota1.Id));
|
||||
// Service.RemoveTenantQuota(Tenant);
|
||||
// Assert.IsNull(Service.GetTenantQuota(quota1.Id));
|
||||
|
||||
var row = new TenantQuotaRow { Tenant = this.Tenant, Path = "path", Counter = 1000, Tag = "tag" };
|
||||
Service.SetTenantQuotaRow(row, false);
|
||||
// var row = new TenantQuotaRow { Tenant = this.Tenant, Path = "path", Counter = 1000, Tag = "tag" };
|
||||
// Service.SetTenantQuotaRow(row, false);
|
||||
|
||||
var rows = Service.FindTenantQuotaRows(Tenant).ToList();
|
||||
CompareQuotaRows(row, rows.Find(r => r.Tenant == row.Tenant && r.Tag == row.Tag));
|
||||
// var rows = Service.FindTenantQuotaRows(Tenant).ToList();
|
||||
// CompareQuotaRows(row, rows.Find(r => r.Tenant == row.Tenant && r.Tag == row.Tag));
|
||||
|
||||
Service.SetTenantQuotaRow(row, true);
|
||||
row.Counter += 1000;
|
||||
rows = Service.FindTenantQuotaRows(Tenant).ToList();
|
||||
CompareQuotaRows(row, rows.Find(r => r.Tenant == row.Tenant && r.Tag == row.Tag));
|
||||
// Service.SetTenantQuotaRow(row, true);
|
||||
// row.Counter += 1000;
|
||||
// rows = Service.FindTenantQuotaRows(Tenant).ToList();
|
||||
// CompareQuotaRows(row, rows.Find(r => r.Tenant == row.Tenant && r.Tag == row.Tag));
|
||||
|
||||
//DeleteQuotaRow(row);
|
||||
}
|
||||
// //DeleteQuotaRow(row);
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void SerializeTest()
|
||||
{
|
||||
var quota1 = new TenantQuota(Tenant)
|
||||
{
|
||||
AvangateId = "1",
|
||||
Features = "trial,year",
|
||||
Name = "quota1",
|
||||
Price = 12.5m,
|
||||
Price2 = 45.23m,
|
||||
Visible = true,
|
||||
MaxFileSize = 3,
|
||||
MaxTotalSize = 4,
|
||||
ActiveUsers = 30,
|
||||
};
|
||||
// [Test]
|
||||
// public void SerializeTest()
|
||||
// {
|
||||
// var quota1 = new TenantQuota(Tenant)
|
||||
// {
|
||||
// AvangateId = "1",
|
||||
// Features = "trial,year",
|
||||
// Name = "quota1",
|
||||
// Price = 12.5m,
|
||||
// Price2 = 45.23m,
|
||||
// Visible = true,
|
||||
// MaxFileSize = 3,
|
||||
// MaxTotalSize = 4,
|
||||
// ActiveUsers = 30,
|
||||
// };
|
||||
|
||||
var json = JsonSerializer.Serialize(quota1);
|
||||
Assert.AreEqual("{\"Id\":1024,\"Name\":\"quota1\",\"MaxFileSize\":3,\"MaxTotalSize\":4,\"ActiveUsers\":30,\"Features\":\"trial,year\",\"Price\":12.5,\"Price2\":45.23,\"AvangateId\":\"1\",\"Visible\":true}", json);
|
||||
}
|
||||
// var json = JsonSerializer.Serialize(quota1);
|
||||
// Assert.AreEqual("{\"Id\":1024,\"Name\":\"quota1\",\"MaxFileSize\":3,\"MaxTotalSize\":4,\"ActiveUsers\":30,\"Features\":\"trial,year\",\"Price\":12.5,\"Price2\":45.23,\"AvangateId\":\"1\",\"Visible\":true}", json);
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void SyncTest()
|
||||
{
|
||||
using var client = new TariffSyncClient();
|
||||
var quotas = client.GetTariffs(1, "key");
|
||||
Assert.AreNotEqual(0, quotas.Count());
|
||||
}
|
||||
// [Test]
|
||||
// public void SyncTest()
|
||||
// {
|
||||
// using var client = new TariffSyncClient();
|
||||
// var quotas = client.GetTariffs(1, "key");
|
||||
// Assert.AreNotEqual(0, quotas.Count());
|
||||
// }
|
||||
|
||||
private void CompareQuotas(TenantQuota q1, TenantQuota q2)
|
||||
{
|
||||
Assert.AreEqual(q1.Id, q2.Id);
|
||||
Assert.AreEqual(q1.Name, q2.Name);
|
||||
Assert.AreEqual(q1.MaxFileSize, q2.MaxFileSize);
|
||||
Assert.AreEqual(q1.MaxTotalSize, q2.MaxTotalSize);
|
||||
Assert.AreEqual(q1.ActiveUsers, q2.ActiveUsers);
|
||||
Assert.AreEqual(q1.Features, q2.Features);
|
||||
Assert.AreEqual(q1.Price, q2.Price);
|
||||
Assert.AreEqual(q1.Price2, q2.Price2);
|
||||
Assert.AreEqual(q1.AvangateId, q2.AvangateId);
|
||||
Assert.AreEqual(q1.Visible, q2.Visible);
|
||||
}
|
||||
// private void CompareQuotas(TenantQuota q1, TenantQuota q2)
|
||||
// {
|
||||
// Assert.AreEqual(q1.Id, q2.Id);
|
||||
// Assert.AreEqual(q1.Name, q2.Name);
|
||||
// Assert.AreEqual(q1.MaxFileSize, q2.MaxFileSize);
|
||||
// Assert.AreEqual(q1.MaxTotalSize, q2.MaxTotalSize);
|
||||
// Assert.AreEqual(q1.ActiveUsers, q2.ActiveUsers);
|
||||
// Assert.AreEqual(q1.Features, q2.Features);
|
||||
// Assert.AreEqual(q1.Price, q2.Price);
|
||||
// Assert.AreEqual(q1.Price2, q2.Price2);
|
||||
// Assert.AreEqual(q1.AvangateId, q2.AvangateId);
|
||||
// Assert.AreEqual(q1.Visible, q2.Visible);
|
||||
// }
|
||||
|
||||
private void CompareQuotaRows(TenantQuotaRow r1, TenantQuotaRow r2)
|
||||
{
|
||||
Assert.AreEqual(r1.Path, r2.Path);
|
||||
Assert.AreEqual(r1.Tag, r2.Tag);
|
||||
Assert.AreEqual(r1.Tenant, r2.Tenant);
|
||||
Assert.AreEqual(r1.Counter, r2.Counter);
|
||||
}
|
||||
// private void CompareQuotaRows(TenantQuotaRow r1, TenantQuotaRow r2)
|
||||
// {
|
||||
// Assert.AreEqual(r1.Path, r2.Path);
|
||||
// Assert.AreEqual(r1.Tag, r2.Tag);
|
||||
// Assert.AreEqual(r1.Tenant, r2.Tenant);
|
||||
// Assert.AreEqual(r1.Counter, r2.Counter);
|
||||
// }
|
||||
|
||||
//private void DeleteQuotaRow(TenantQuotaRow row)
|
||||
//{
|
||||
// var d = new SqlDelete(DbQuotaService.tenants_quotarow).Where("tenant", row.Tenant).Where("path", row.Path);
|
||||
// var dbManager = DbOptionsManager.Value;
|
||||
// dbManager.ExecuteNonQuery(d);
|
||||
//}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// //private void DeleteQuotaRow(TenantQuotaRow row)
|
||||
// //{
|
||||
// // var d = new SqlDelete(DbQuotaService.tenants_quotarow).Where("tenant", row.Tenant).Where("path", row.Path);
|
||||
// // var dbManager = DbOptionsManager.Value;
|
||||
// // dbManager.ExecuteNonQuery(d);
|
||||
// //}
|
||||
// }
|
||||
//}
|
||||
//#endif
|
||||
|
@ -1,92 +1,92 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
///*
|
||||
// *
|
||||
// * (c) Copyright Ascensio System Limited 2010-2018
|
||||
// *
|
||||
// * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
// * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
// * In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
// *
|
||||
// * You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
// *
|
||||
// * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
// * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
// *
|
||||
// * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
// * relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
// * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
// * in every copy of the program you distribute.
|
||||
// * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
// *
|
||||
//*/
|
||||
|
||||
|
||||
#if DEBUG
|
||||
namespace ASC.Core.Common.Tests
|
||||
{
|
||||
using System;
|
||||
//#if DEBUG
|
||||
//namespace ASC.Core.Common.Tests
|
||||
//{
|
||||
// using System;
|
||||
|
||||
using ASC.Core.Billing;
|
||||
// using ASC.Core.Billing;
|
||||
|
||||
using NUnit.Framework;
|
||||
// using NUnit.Framework;
|
||||
|
||||
[TestFixture]
|
||||
public class TariffServiceTest
|
||||
{
|
||||
private readonly ITariffService tariffService;
|
||||
// [TestFixture]
|
||||
// public class TariffServiceTest
|
||||
// {
|
||||
// private readonly ITariffService tariffService;
|
||||
|
||||
|
||||
public TariffServiceTest()
|
||||
{
|
||||
tariffService = new TariffService();
|
||||
}
|
||||
// public TariffServiceTest()
|
||||
// {
|
||||
// tariffService = new TariffService();
|
||||
// }
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestShoppingUriBatch()
|
||||
{
|
||||
using var bc = new BillingClient(true, null, null);
|
||||
var result = bc.GetPaymentUrls("0", new[] { "12", "13", "14", "0", "-2" });
|
||||
Assert.AreEqual(5, result.Count);
|
||||
}
|
||||
// [Test]
|
||||
// public void TestShoppingUriBatch()
|
||||
// {
|
||||
// using var bc = new BillingClient(true, null, null);
|
||||
// var result = bc.GetPaymentUrls("0", new[] { "12", "13", "14", "0", "-2" });
|
||||
// Assert.AreEqual(5, result.Count);
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void TestPaymentInfo()
|
||||
{
|
||||
var payments = tariffService.GetPayments(918, DateTime.MinValue, DateTime.MaxValue);
|
||||
Assert.IsNotNull(payments);
|
||||
}
|
||||
// [Test]
|
||||
// public void TestPaymentInfo()
|
||||
// {
|
||||
// var payments = tariffService.GetPayments(918, DateTime.MinValue, DateTime.MaxValue);
|
||||
// Assert.IsNotNull(payments);
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void TestTariff()
|
||||
{
|
||||
var tariff = tariffService.GetTariff(918);
|
||||
Assert.IsNotNull(tariff);
|
||||
}
|
||||
// [Test]
|
||||
// public void TestTariff()
|
||||
// {
|
||||
// var tariff = tariffService.GetTariff(918);
|
||||
// Assert.IsNotNull(tariff);
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void TestSetTariff()
|
||||
{
|
||||
var duedate = DateTime.UtcNow.AddMonths(1);
|
||||
tariffService.SetTariff(0, new Tariff { QuotaId = -1, DueDate = DateTime.MaxValue });
|
||||
tariffService.SetTariff(0, new Tariff { QuotaId = -21, DueDate = duedate });
|
||||
tariffService.SetTariff(0, new Tariff { QuotaId = -21, DueDate = duedate });
|
||||
tariffService.SetTariff(0, new Tariff { QuotaId = -1, DueDate = DateTime.MaxValue });
|
||||
}
|
||||
// [Test]
|
||||
// public void TestSetTariff()
|
||||
// {
|
||||
// var duedate = DateTime.UtcNow.AddMonths(1);
|
||||
// tariffService.SetTariff(0, new Tariff { QuotaId = -1, DueDate = DateTime.MaxValue });
|
||||
// tariffService.SetTariff(0, new Tariff { QuotaId = -21, DueDate = duedate });
|
||||
// tariffService.SetTariff(0, new Tariff { QuotaId = -21, DueDate = duedate });
|
||||
// tariffService.SetTariff(0, new Tariff { QuotaId = -1, DueDate = DateTime.MaxValue });
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void TestInvoice()
|
||||
{
|
||||
var payments = tariffService.GetPayments(918, DateTime.MinValue, DateTime.MaxValue);
|
||||
foreach (var p in payments)
|
||||
{
|
||||
var invoice = tariffService.GetInvoice(p.CartId);
|
||||
Assert.IsNotNull(invoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// [Test]
|
||||
// public void TestInvoice()
|
||||
// {
|
||||
// var payments = tariffService.GetPayments(918, DateTime.MinValue, DateTime.MaxValue);
|
||||
// foreach (var p in payments)
|
||||
// {
|
||||
// var invoice = tariffService.GetInvoice(p.CartId);
|
||||
// Assert.IsNotNull(invoice);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//#endif
|
||||
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
#if DEBUG
|
||||
namespace ASC.Core.Common.Tests
|
||||
{
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Core.Billing;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
[TestFixture]
|
||||
public class TariffSyncServiceTest
|
||||
{
|
||||
private readonly ITariffSyncService tariffSyncService;
|
||||
|
||||
public IServiceProvider ServiceProvider { get; set; }
|
||||
public IConfiguration Configuration { get; set; }
|
||||
public IOptionsMonitor<ILog> Options { get; set; }
|
||||
|
||||
public TariffSyncServiceTest()
|
||||
{
|
||||
tariffSyncService = new TariffSyncService(null, null, null, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetTeriffsTest()
|
||||
{
|
||||
var tariff = tariffSyncService.GetTariffs(70, null).FirstOrDefault(t => t.Id == -38);
|
||||
Assert.AreEqual(1024 * 1024 * 1024, tariff.MaxFileSize);
|
||||
tariff = tariffSyncService.GetTariffs(74, null).FirstOrDefault(t => t.Id == -38);
|
||||
Assert.AreEqual(100 * 1024 * 1024, tariff.MaxFileSize);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SyncTest()
|
||||
{
|
||||
using var wcfClient = new TariffSyncClient();
|
||||
var tariffs = wcfClient.GetTariffs(74, null);
|
||||
Assert.IsTrue(tariffs.Any());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
@ -60,6 +60,19 @@ namespace ASC.Core.Users
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public int CoefficientOfVisitors
|
||||
{
|
||||
get
|
||||
{
|
||||
int count;
|
||||
if (!int.TryParse(Configuration["core:coefficient-of-visitors"], out count))
|
||||
{
|
||||
count = 2;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
private IConfiguration Configuration { get; }
|
||||
|
@ -43,18 +43,20 @@ namespace ASC.Data.Encryption
|
||||
private EncryptionSettings Settings { get; set; }
|
||||
private string TempDir { get; set; }
|
||||
|
||||
private IConfiguration Configuration { get; set; }
|
||||
|
||||
private IConfiguration Configuration { get; set; }
|
||||
public TempPath TempPath { get; }
|
||||
|
||||
public void Init(string storageName, EncryptionSettings encryptionSettings)
|
||||
{
|
||||
Storage = storageName;
|
||||
Settings = encryptionSettings;
|
||||
TempDir = Configuration["storage:encryption:tempdir"] ?? Path.GetTempPath();
|
||||
TempDir = TempPath.GetTempPath();
|
||||
}
|
||||
|
||||
public Crypt(IConfiguration configuration)
|
||||
public Crypt(IConfiguration configuration, TempPath tempPath)
|
||||
{
|
||||
Configuration = configuration;
|
||||
Configuration = configuration;
|
||||
TempPath = tempPath;
|
||||
}
|
||||
|
||||
public byte Version { get { return 1; } }
|
||||
|
@ -29,8 +29,10 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using ASC.Common;
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Common.Utils;
|
||||
using ASC.Core;
|
||||
@ -45,13 +47,21 @@ namespace ASC.Data.Storage
|
||||
public abstract class BaseStorage : IDataStore
|
||||
{
|
||||
protected ILog Log { get; set; }
|
||||
protected TempStream TempStream { get; }
|
||||
protected TenantManager TenantManager { get; }
|
||||
protected PathUtils PathUtils { get; }
|
||||
protected EmailValidationKeyProvider EmailValidationKeyProvider { get; }
|
||||
protected IHttpContextAccessor HttpContextAccessor { get; }
|
||||
protected IOptionsMonitor<ILog> Options { get; }
|
||||
|
||||
public BaseStorage(
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IOptionsMonitor<ILog> options)
|
||||
{
|
||||
TempStream = tempStream;
|
||||
TenantManager = tenantManager;
|
||||
PathUtils = pathUtils;
|
||||
EmailValidationKeyProvider = emailValidationKeyProvider;
|
||||
@ -60,11 +70,14 @@ namespace ASC.Data.Storage
|
||||
}
|
||||
|
||||
public BaseStorage(
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IOptionsMonitor<ILog> options) : this(tenantManager,
|
||||
IOptionsMonitor<ILog> options) : this(
|
||||
tempStream,
|
||||
tenantManager,
|
||||
pathUtils,
|
||||
emailValidationKeyProvider,
|
||||
options)
|
||||
@ -177,6 +190,7 @@ namespace ASC.Data.Storage
|
||||
|
||||
public abstract Stream GetReadStream(string domain, string path);
|
||||
public abstract Stream GetReadStream(string domain, string path, int offset);
|
||||
public abstract Task<Stream> GetReadStreamAsync(string domain, string path, int offset);
|
||||
|
||||
public abstract Uri Save(string domain, string path, Stream stream);
|
||||
public abstract Uri Save(string domain, string path, Stream stream, ACL acl);
|
||||
@ -228,12 +242,6 @@ namespace ASC.Data.Storage
|
||||
|
||||
public virtual bool IsSupportChunking { get { return false; } }
|
||||
|
||||
protected TenantManager TenantManager { get; }
|
||||
protected PathUtils PathUtils { get; }
|
||||
protected EmailValidationKeyProvider EmailValidationKeyProvider { get; }
|
||||
protected IHttpContextAccessor HttpContextAccessor { get; }
|
||||
protected IOptionsMonitor<ILog> Options { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
public abstract void Delete(string domain, string path);
|
||||
@ -241,11 +249,12 @@ namespace ASC.Data.Storage
|
||||
public abstract void DeleteFiles(string domain, List<string> paths);
|
||||
public abstract void DeleteFiles(string domain, string folderPath, DateTime fromDate, DateTime toDate);
|
||||
public abstract void MoveDirectory(string srcdomain, string srcdir, string newdomain, string newdir);
|
||||
public abstract Uri Move(string srcdomain, string srcpath, string newdomain, string newpath);
|
||||
public abstract Uri Move(string srcdomain, string srcpath, string newdomain, string newpath, bool quotaCheckFileSize = true);
|
||||
public abstract Uri SaveTemp(string domain, out string assignedPath, Stream stream);
|
||||
public abstract string[] ListDirectoriesRelative(string domain, string path, bool recursive);
|
||||
public abstract string[] ListFilesRelative(string domain, string path, string pattern, bool recursive);
|
||||
public abstract bool IsFile(string domain, string path);
|
||||
public abstract Task<bool> IsFileAsync(string domain, string path);
|
||||
public abstract bool IsDirectory(string domain, string path);
|
||||
public abstract void DeleteDirectory(string domain, string path);
|
||||
public abstract long GetFileSize(string domain, string path);
|
||||
@ -370,11 +379,11 @@ namespace ASC.Data.Storage
|
||||
|
||||
#endregion
|
||||
|
||||
internal void QuotaUsedAdd(string domain, long size)
|
||||
internal void QuotaUsedAdd(string domain, long size, bool quotaCheckFileSize = true)
|
||||
{
|
||||
if (QuotaController != null)
|
||||
{
|
||||
QuotaController.QuotaUsedAdd(_modulename, domain, _dataList.GetData(domain), size);
|
||||
QuotaController.QuotaUsedAdd(_modulename, domain, _dataList.GetData(domain), size, quotaCheckFileSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,16 +39,24 @@ namespace ASC.Core.ChunkedUploader
|
||||
{
|
||||
public class CommonChunkedUploadSessionHolder
|
||||
{
|
||||
public static readonly TimeSpan SlidingExpiration = TimeSpan.FromHours(12);
|
||||
|
||||
public static readonly TimeSpan SlidingExpiration = TimeSpan.FromHours(12);
|
||||
|
||||
private TempPath TempPath { get; }
|
||||
private IOptionsMonitor<ILog> Option { get; }
|
||||
private IDataStore DataStore { get; set; }
|
||||
public IDataStore DataStore { get; set; }
|
||||
private string Domain { get; set; }
|
||||
private long MaxChunkUploadSize { get; set; }
|
||||
private long MaxChunkUploadSize { get; set; }
|
||||
|
||||
private const string StoragePath = "sessions";
|
||||
|
||||
public CommonChunkedUploadSessionHolder(IOptionsMonitor<ILog> option, IDataStore dataStore, string domain, long maxChunkUploadSize = 10 * 1024 * 1024)
|
||||
{
|
||||
public CommonChunkedUploadSessionHolder(
|
||||
TempPath tempPath,
|
||||
IOptionsMonitor<ILog> option,
|
||||
IDataStore dataStore,
|
||||
string domain,
|
||||
long maxChunkUploadSize = 10 * 1024 * 1024)
|
||||
{
|
||||
TempPath = tempPath;
|
||||
Option = option;
|
||||
DataStore = dataStore;
|
||||
Domain = domain;
|
||||
@ -110,11 +118,11 @@ namespace ASC.Core.ChunkedUploader
|
||||
|
||||
DataStore.FinalizeChunkedUpload(Domain, tempPath, uploadId, eTags);
|
||||
}
|
||||
|
||||
public void Move(CommonChunkedUploadSession chunkedUploadSession, string newPath)
|
||||
|
||||
public void Move(CommonChunkedUploadSession chunkedUploadSession, string newPath, bool quotaCheckFileSize = true)
|
||||
{
|
||||
DataStore.Move(Domain, chunkedUploadSession.TempPath, string.Empty, newPath);
|
||||
}
|
||||
DataStore.Move(Domain, chunkedUploadSession.TempPath, string.Empty, newPath, quotaCheckFileSize);
|
||||
}
|
||||
|
||||
public void Abort(CommonChunkedUploadSession uploadSession)
|
||||
{
|
||||
@ -157,7 +165,9 @@ namespace ASC.Core.ChunkedUploader
|
||||
//This is hack fixing strange behaviour of plupload in flash mode.
|
||||
|
||||
if (string.IsNullOrEmpty(uploadSession.ChunksBuffer))
|
||||
uploadSession.ChunksBuffer = Path.GetTempFileName();
|
||||
{
|
||||
uploadSession.ChunksBuffer = TempPath.GetTempFileName();
|
||||
}
|
||||
|
||||
using (var bufferStream = new FileStream(uploadSession.ChunksBuffer, FileMode.Append))
|
||||
{
|
||||
|
@ -25,8 +25,9 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using System.IO;
|
||||
|
||||
using ASC.Common;
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Core.ChunkedUploader;
|
||||
|
||||
@ -40,20 +41,28 @@ namespace ASC.Data.Storage
|
||||
private readonly IDataStore source;
|
||||
private readonly IDataStore destination;
|
||||
private readonly long maxChunkUploadSize;
|
||||
private readonly int chunksize;
|
||||
|
||||
public CrossModuleTransferUtility(IOptionsMonitor<ILog> option, IDataStore source, IDataStore destination)
|
||||
private readonly int chunksize;
|
||||
private IOptionsMonitor<ILog> Option { get; }
|
||||
private TempStream TempStream { get; }
|
||||
private TempPath TempPath { get; }
|
||||
|
||||
public CrossModuleTransferUtility(
|
||||
IOptionsMonitor<ILog> option,
|
||||
TempStream tempStream,
|
||||
TempPath tempPath,
|
||||
IDataStore source,
|
||||
IDataStore destination)
|
||||
{
|
||||
Log = option.Get("ASC.CrossModuleTransferUtility");
|
||||
Option = option;
|
||||
Option = option;
|
||||
TempStream = tempStream;
|
||||
TempPath = tempPath;
|
||||
this.source = source ?? throw new ArgumentNullException("source");
|
||||
this.destination = destination ?? throw new ArgumentNullException("destination");
|
||||
maxChunkUploadSize = 10 * 1024 * 1024;
|
||||
chunksize = 5 * 1024 * 1024;
|
||||
}
|
||||
|
||||
private IOptionsMonitor<ILog> Option { get; }
|
||||
|
||||
}
|
||||
|
||||
public void CopyFile(string srcDomain, string srcPath, string destDomain, string destPath)
|
||||
{
|
||||
if (srcDomain == null) throw new ArgumentNullException("srcDomain");
|
||||
@ -69,7 +78,7 @@ namespace ASC.Data.Storage
|
||||
else
|
||||
{
|
||||
var session = new CommonChunkedUploadSession(stream.Length);
|
||||
var holder = new CommonChunkedUploadSessionHolder(Option, destination, destDomain);
|
||||
var holder = new CommonChunkedUploadSessionHolder(TempPath, Option, destination, destDomain);
|
||||
holder.Init(session);
|
||||
try
|
||||
{
|
||||
|
@ -28,6 +28,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ASC.Common;
|
||||
using ASC.Common.Logging;
|
||||
@ -77,19 +78,21 @@ namespace ASC.Data.Storage.DiscStorage
|
||||
}
|
||||
|
||||
public DiscDataStore(
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IOptionsMonitor<ILog> options,
|
||||
EncryptionSettingsHelper encryptionSettingsHelper,
|
||||
EncryptionFactory encryptionFactory)
|
||||
: base(tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IOptionsMonitor<ILog> options,
|
||||
EncryptionSettingsHelper encryptionSettingsHelper,
|
||||
EncryptionFactory encryptionFactory)
|
||||
: base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
{
|
||||
EncryptionSettingsHelper = encryptionSettingsHelper;
|
||||
EncryptionFactory = encryptionFactory;
|
||||
}
|
||||
|
||||
public DiscDataStore(
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
@ -97,7 +100,7 @@ namespace ASC.Data.Storage.DiscStorage
|
||||
IOptionsMonitor<ILog> options,
|
||||
EncryptionSettingsHelper encryptionSettingsHelper,
|
||||
EncryptionFactory encryptionFactory)
|
||||
: base(tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
: base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
{
|
||||
EncryptionSettingsHelper = encryptionSettingsHelper;
|
||||
EncryptionFactory = encryptionFactory;
|
||||
@ -137,6 +140,10 @@ namespace ASC.Data.Storage.DiscStorage
|
||||
throw new FileNotFoundException("File not found", Path.GetFullPath(target));
|
||||
}
|
||||
|
||||
public override Task<Stream> GetReadStreamAsync(string domain, string path, int offset)
|
||||
{
|
||||
return Task.FromResult(GetReadStream(domain, path, offset));
|
||||
}
|
||||
|
||||
public override Stream GetReadStream(string domain, string path, int offset)
|
||||
{
|
||||
@ -171,7 +178,7 @@ namespace ASC.Data.Storage.DiscStorage
|
||||
public override Uri Save(string domain, string path, Stream stream)
|
||||
{
|
||||
Log.Debug("Save " + path);
|
||||
var buffered = stream.GetBuffered();
|
||||
var buffered = TempStream.GetBuffered(stream);
|
||||
if (QuotaController != null)
|
||||
{
|
||||
QuotaController.QuotaUsedCheck(buffered.Length);
|
||||
@ -377,7 +384,7 @@ namespace ASC.Data.Storage.DiscStorage
|
||||
Directory.Move(target, newtarget);
|
||||
}
|
||||
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath)
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath, bool quotaCheckFileSize = true)
|
||||
{
|
||||
if (srcpath == null) throw new ArgumentNullException("srcpath");
|
||||
if (newpath == null) throw new ArgumentNullException("srcpath");
|
||||
@ -401,7 +408,7 @@ namespace ASC.Data.Storage.DiscStorage
|
||||
File.Move(target, newtarget);
|
||||
|
||||
QuotaUsedDelete(srcdomain, flength);
|
||||
QuotaUsedAdd(newdomain, flength);
|
||||
QuotaUsedAdd(newdomain, flength, quotaCheckFileSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -578,6 +585,11 @@ namespace ASC.Data.Storage.DiscStorage
|
||||
return result;
|
||||
}
|
||||
|
||||
public override Task<bool> IsFileAsync(string domain, string path)
|
||||
{
|
||||
return Task.FromResult(IsFile(domain, path));
|
||||
}
|
||||
|
||||
public override long ResetQuota(string domain)
|
||||
{
|
||||
if (QuotaController != null)
|
||||
|
@ -28,15 +28,17 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
using ASC.Common;
|
||||
|
||||
namespace ASC.Data.Storage
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
private const int BufferSize = 2048;//NOTE: set to 2048 to fit in minimum tcp window
|
||||
|
||||
public static Stream IronReadStream(this IDataStore store, string domain, string path, int tryCount)
|
||||
public static Stream IronReadStream(this IDataStore store, TempStream tempStream, string domain, string path, int tryCount)
|
||||
{
|
||||
var ms = TempStream.Create();
|
||||
var ms = tempStream.Create();
|
||||
IronReadToStream(store, domain, path, tryCount, ms);
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
return ms;
|
||||
|
@ -35,6 +35,7 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using ASC.Common;
|
||||
@ -72,19 +73,21 @@ namespace ASC.Data.Storage.GoogleCloud
|
||||
private bool _lowerCasing = true;
|
||||
|
||||
public GoogleCloudStorage(
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IOptionsMonitor<ILog> options) : base(tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
IOptionsMonitor<ILog> options) : base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
{
|
||||
}
|
||||
|
||||
public GoogleCloudStorage(
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IOptionsMonitor<ILog> options) : base(tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
IOptionsMonitor<ILog> options) : base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
{
|
||||
}
|
||||
|
||||
@ -237,6 +240,22 @@ namespace ASC.Data.Storage.GoogleCloud
|
||||
return tempStream;
|
||||
}
|
||||
|
||||
public override async Task<Stream> GetReadStreamAsync(string domain, string path, int offset)
|
||||
{
|
||||
var tempStream = TempStream.Create();
|
||||
|
||||
var storage = GetStorage();
|
||||
|
||||
await storage.DownloadObjectAsync(_bucket, MakePath(domain, path), tempStream);
|
||||
|
||||
if (offset > 0)
|
||||
tempStream.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
tempStream.Position = 0;
|
||||
|
||||
return tempStream;
|
||||
}
|
||||
|
||||
public override Uri Save(string domain, string path, System.IO.Stream stream)
|
||||
{
|
||||
return Save(domain, path, stream, string.Empty, string.Empty);
|
||||
@ -273,7 +292,7 @@ namespace ASC.Data.Storage.GoogleCloud
|
||||
string contentDisposition, ACL acl, string contentEncoding = null, int cacheDays = 5)
|
||||
{
|
||||
|
||||
var buffered = stream.GetBuffered();
|
||||
var buffered = TempStream.GetBuffered(stream);
|
||||
|
||||
if (QuotaController != null)
|
||||
{
|
||||
@ -457,7 +476,7 @@ namespace ASC.Data.Storage.GoogleCloud
|
||||
}
|
||||
}
|
||||
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath)
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath, bool quotaCheckFileSize = true)
|
||||
{
|
||||
using var storage = GetStorage();
|
||||
|
||||
@ -473,7 +492,7 @@ namespace ASC.Data.Storage.GoogleCloud
|
||||
Delete(srcdomain, srcpath);
|
||||
|
||||
QuotaUsedDelete(srcdomain, size);
|
||||
QuotaUsedAdd(newdomain, size);
|
||||
QuotaUsedAdd(newdomain, size, quotaCheckFileSize);
|
||||
|
||||
return GetUri(newdomain, newpath);
|
||||
|
||||
@ -519,6 +538,15 @@ namespace ASC.Data.Storage.GoogleCloud
|
||||
return objects.Count() > 0;
|
||||
}
|
||||
|
||||
public override async Task<bool> IsFileAsync(string domain, string path)
|
||||
{
|
||||
var storage = GetStorage();
|
||||
|
||||
var objects = await storage.ListObjectsAsync(_bucket, MakePath(domain, path)).ReadPageAsync(1);
|
||||
|
||||
return objects.Count() > 0;
|
||||
}
|
||||
|
||||
public override bool IsDirectory(string domain, string path)
|
||||
{
|
||||
return IsFile(domain, path);
|
||||
@ -662,7 +690,7 @@ namespace ASC.Data.Storage.GoogleCloud
|
||||
using var storage = GetStorage();
|
||||
|
||||
var objectKey = MakePath(domain, path);
|
||||
var buffered = stream.GetBuffered();
|
||||
var buffered = TempStream.GetBuffered(stream);
|
||||
|
||||
var uploadObjectOptions = new UploadObjectOptions
|
||||
{
|
||||
|
@ -27,6 +27,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ASC.Data.Storage.Configuration;
|
||||
|
||||
@ -91,6 +92,8 @@ namespace ASC.Data.Storage
|
||||
///<returns></returns>
|
||||
Stream GetReadStream(string domain, string path);
|
||||
|
||||
Task<Stream> GetReadStreamAsync(string domain, string path, int offset);
|
||||
|
||||
///<summary>
|
||||
/// A stream of read-only. In the case of the C3 stream NetworkStream general, and with him we have to work
|
||||
/// Very carefully as a Jedi cutter groin lightsaber.
|
||||
@ -212,7 +215,7 @@ namespace ASC.Data.Storage
|
||||
///<param name="newdomain"></param>
|
||||
///<param name="newpath"></param>
|
||||
///<returns></returns>
|
||||
Uri Move(string srcdomain, string srcpath, string newdomain, string newpath);
|
||||
Uri Move(string srcdomain, string srcpath, string newdomain, string newpath, bool quotaCheckFileSize = true);
|
||||
|
||||
///<summary>
|
||||
/// Saves the file in the temp. In fact, almost no different from the usual Save except that generates the file name itself. An inconvenient thing.
|
||||
@ -260,6 +263,8 @@ namespace ASC.Data.Storage
|
||||
///<returns></returns>
|
||||
bool IsFile(string domain, string path);
|
||||
|
||||
Task<bool> IsFileAsync(string domain, string path);
|
||||
|
||||
///<summary>
|
||||
/// Checks whether a directory exists. On s3 it took long time.
|
||||
///</summary>
|
||||
|
@ -28,7 +28,8 @@ namespace ASC.Data.Storage
|
||||
{
|
||||
public interface IQuotaController
|
||||
{
|
||||
void QuotaUsedAdd(string module, string domain, string dataTag, long size);
|
||||
//quotaCheckFileSize:hack for Backup bug 48873
|
||||
void QuotaUsedAdd(string module, string domain, string dataTag, long size, bool quotaCheckFileSize = true);
|
||||
|
||||
void QuotaUsedDelete(string module, string domain, string dataTag, long size);
|
||||
|
||||
|
@ -86,9 +86,9 @@ namespace ASC.Data.Storage
|
||||
if (virtPath.StartsWith("~") && !Uri.IsWellFormedUriString(virtPath, UriKind.Absolute))
|
||||
{
|
||||
var rootPath = "/";
|
||||
if (!string.IsNullOrEmpty(WebHostEnvironment.WebRootPath) && WebHostEnvironment.WebRootPath.Length > 1)
|
||||
if (!string.IsNullOrEmpty(WebHostEnvironment?.WebRootPath) && WebHostEnvironment?.WebRootPath.Length > 1)
|
||||
{
|
||||
rootPath = WebHostEnvironment.WebRootPath.Trim('/');
|
||||
rootPath = WebHostEnvironment?.WebRootPath.Trim('/');
|
||||
}
|
||||
virtPath = virtPath.Replace("~", rootPath);
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using ASC.Common;
|
||||
@ -65,24 +66,30 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
private readonly ILog _logger;
|
||||
|
||||
public RackspaceCloudStorage(
|
||||
TempPath tempPath,
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IOptionsMonitor<ILog> options)
|
||||
: base(tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
: base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
{
|
||||
_logger = options.Get("ASC.Data.Storage.Rackspace.RackspaceCloudStorage");
|
||||
TempPath = tempPath;
|
||||
}
|
||||
|
||||
public RackspaceCloudStorage(
|
||||
TempPath tempPath,
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IOptionsMonitor<ILog> options)
|
||||
: base(tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
: base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
{
|
||||
_logger = options.Get("ASC.Data.Storage.Rackspace.RackspaceCloudStorage");
|
||||
TempPath = tempPath;
|
||||
}
|
||||
|
||||
private string MakePath(string domain, string path)
|
||||
@ -246,6 +253,11 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
public override Task<Stream> GetReadStreamAsync(string domain, string path, int offset)
|
||||
{
|
||||
return Task.FromResult(GetReadStream(domain, path, offset));
|
||||
}
|
||||
|
||||
public override Uri Save(string domain, string path, Stream stream)
|
||||
{
|
||||
return Save(domain, path, stream, string.Empty, string.Empty);
|
||||
@ -283,7 +295,7 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
string contentDisposition, ACL acl, string contentEncoding = null, int cacheDays = 5,
|
||||
DateTime? deleteAt = null, long? deleteAfter = null)
|
||||
{
|
||||
var buffered = stream.GetBuffered();
|
||||
var buffered = TempStream.GetBuffered(stream);
|
||||
|
||||
if (QuotaController != null)
|
||||
{
|
||||
@ -493,7 +505,7 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
}
|
||||
}
|
||||
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath)
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath, bool quotaCheckFileSize = true)
|
||||
{
|
||||
var srcKey = MakePath(srcdomain, srcpath);
|
||||
var dstKey = MakePath(newdomain, newpath);
|
||||
@ -506,7 +518,7 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
Delete(srcdomain, srcpath);
|
||||
|
||||
QuotaUsedDelete(srcdomain, size);
|
||||
QuotaUsedAdd(newdomain, size);
|
||||
QuotaUsedAdd(newdomain, size, quotaCheckFileSize);
|
||||
|
||||
return GetUri(newdomain, newpath);
|
||||
}
|
||||
@ -547,6 +559,12 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
return objects.Count() > 0;
|
||||
}
|
||||
|
||||
public override Task<bool> IsFileAsync(string domain, string path)
|
||||
{
|
||||
return Task.FromResult(IsFile(domain, path));
|
||||
}
|
||||
|
||||
|
||||
public override bool IsDirectory(string domain, string path)
|
||||
{
|
||||
return IsFile(domain, path);
|
||||
@ -702,7 +720,7 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
|
||||
public override string InitiateChunkedUpload(string domain, string path)
|
||||
{
|
||||
return Path.GetTempFileName();
|
||||
return TempPath.GetTempFileName();
|
||||
}
|
||||
|
||||
public override string UploadChunk(string domain, string path, string filePath, Stream stream, long defaultChunkSize, int chunkNumber, long chunkLength)
|
||||
@ -755,6 +773,8 @@ namespace ASC.Data.Storage.RackspaceCloud
|
||||
|
||||
public override bool IsSupportChunking { get { return true; } }
|
||||
|
||||
public TempPath TempPath { get; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using Amazon;
|
||||
@ -78,21 +79,23 @@ namespace ASC.Data.Storage.S3
|
||||
private string _subDir = string.Empty;
|
||||
|
||||
public S3Storage(
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IOptionsMonitor<ILog> options)
|
||||
: base(tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
: base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, options)
|
||||
{
|
||||
}
|
||||
|
||||
public S3Storage(
|
||||
TempStream tempStream,
|
||||
TenantManager tenantManager,
|
||||
PathUtils pathUtils,
|
||||
EmailValidationKeyProvider emailValidationKeyProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IOptionsMonitor<ILog> options)
|
||||
: base(tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
: base(tempStream, tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options)
|
||||
{
|
||||
}
|
||||
|
||||
@ -193,8 +196,46 @@ namespace ASC.Data.Storage.S3
|
||||
|
||||
if (0 < offset) request.ByteRange = new ByteRange(offset, int.MaxValue);
|
||||
|
||||
using var client = GetClient();
|
||||
return new ResponseStreamWrapper(client.GetObjectAsync(request).Result);
|
||||
try
|
||||
{
|
||||
using var client = GetClient();
|
||||
return new ResponseStreamWrapper(client.GetObjectAsync(request).Result);
|
||||
}
|
||||
catch (AmazonS3Exception ex)
|
||||
{
|
||||
if (ex.ErrorCode == "NoSuchKey")
|
||||
{
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<Stream> GetReadStreamAsync(string domain, string path, int offset)
|
||||
{
|
||||
var request = new GetObjectRequest
|
||||
{
|
||||
BucketName = _bucket,
|
||||
Key = MakePath(domain, path)
|
||||
};
|
||||
|
||||
if (0 < offset) request.ByteRange = new ByteRange(offset, int.MaxValue);
|
||||
|
||||
try
|
||||
{
|
||||
using var client = GetClient();
|
||||
return new ResponseStreamWrapper(await client.GetObjectAsync(request));
|
||||
}
|
||||
catch (AmazonS3Exception ex)
|
||||
{
|
||||
if (ex.ErrorCode == "NoSuchKey")
|
||||
{
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Uri SaveWithAutoAttachment(string domain, string path, Stream stream, string attachmentFileName)
|
||||
@ -218,7 +259,7 @@ namespace ASC.Data.Storage.S3
|
||||
public Uri Save(string domain, string path, Stream stream, string contentType,
|
||||
string contentDisposition, ACL acl, string contentEncoding = null, int cacheDays = 5)
|
||||
{
|
||||
var buffered = stream.GetBuffered();
|
||||
var buffered = TempStream.GetBuffered(stream);
|
||||
if (QuotaController != null)
|
||||
{
|
||||
QuotaController.QuotaUsedCheck(buffered.Length);
|
||||
@ -572,7 +613,7 @@ namespace ASC.Data.Storage.S3
|
||||
}
|
||||
}
|
||||
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath)
|
||||
public override Uri Move(string srcdomain, string srcpath, string newdomain, string newpath, bool quotaCheckFileSize = true)
|
||||
{
|
||||
using var client = GetClient();
|
||||
var srcKey = MakePath(srcdomain, srcpath);
|
||||
@ -594,7 +635,7 @@ namespace ASC.Data.Storage.S3
|
||||
Delete(srcdomain, srcpath);
|
||||
|
||||
QuotaUsedDelete(srcdomain, size);
|
||||
QuotaUsedAdd(newdomain, size);
|
||||
QuotaUsedAdd(newdomain, size, quotaCheckFileSize);
|
||||
|
||||
return GetUri(newdomain, newpath);
|
||||
}
|
||||
@ -618,7 +659,7 @@ namespace ASC.Data.Storage.S3
|
||||
using var client = GetClient();
|
||||
using var uploader = new TransferUtility(client);
|
||||
var objectKey = MakePath(domain, path);
|
||||
var buffered = stream.GetBuffered();
|
||||
var buffered = TempStream.GetBuffered(stream);
|
||||
var request = new TransferUtilityUploadRequest
|
||||
{
|
||||
BucketName = _bucket,
|
||||
@ -854,19 +895,78 @@ namespace ASC.Data.Storage.S3
|
||||
public override bool IsFile(string domain, string path)
|
||||
{
|
||||
using var client = GetClient();
|
||||
var request = new ListObjectsRequest { BucketName = _bucket, Prefix = (MakePath(domain, path)) };
|
||||
var response = client.ListObjectsAsync(request).Result;
|
||||
return response.S3Objects.Count > 0;
|
||||
try
|
||||
{
|
||||
var getObjectMetadataRequest = new GetObjectMetadataRequest
|
||||
{
|
||||
BucketName = _bucket,
|
||||
Key = MakePath(domain, path)
|
||||
};
|
||||
|
||||
client.GetObjectMetadataAsync(getObjectMetadataRequest).Wait();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (AmazonS3Exception ex)
|
||||
{
|
||||
if (string.Equals(ex.ErrorCode, "NoSuchBucket"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(ex.ErrorCode, "NotFound"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<bool> IsFileAsync(string domain, string path)
|
||||
{
|
||||
using var client = GetClient();
|
||||
try
|
||||
{
|
||||
var getObjectMetadataRequest = new GetObjectMetadataRequest
|
||||
{
|
||||
BucketName = _bucket,
|
||||
Key = MakePath(domain, path)
|
||||
};
|
||||
|
||||
await client.GetObjectMetadataAsync(getObjectMetadataRequest);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (AmazonS3Exception ex)
|
||||
{
|
||||
if (string.Equals(ex.ErrorCode, "NoSuchBucket"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(ex.ErrorCode, "NotFound"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsDirectory(string domain, string path)
|
||||
{
|
||||
return IsFile(domain, path);
|
||||
using (var client = GetClient())
|
||||
{
|
||||
var request = new ListObjectsRequest { BucketName = _bucket, Prefix = (MakePath(domain, path)) };
|
||||
var response = client.ListObjectsAsync(request).Result;
|
||||
return response.S3Objects.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override void DeleteDirectory(string domain, string path)
|
||||
{
|
||||
DeleteFiles(domain, path, "*.*", true);
|
||||
DeleteFiles(domain, path, "*", true);
|
||||
}
|
||||
|
||||
public override long GetFileSize(string domain, string path)
|
||||
|
@ -205,7 +205,7 @@ namespace ASC.Data.Storage
|
||||
}
|
||||
|
||||
var settings = SettingsManager.LoadForTenant<StorageSettings>(tenantId);
|
||||
|
||||
//TODO:GetStoreAndCache
|
||||
return GetDataStore(tenant, module, StorageSettingsHelper.DataStoreConsumer(settings), controller);
|
||||
}
|
||||
|
||||
|
@ -26,10 +26,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using ASC.Common;
|
||||
using ASC.Common.Caching;
|
||||
using ASC.Common.Caching;
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Common.Threading;
|
||||
using ASC.Core;
|
||||
@ -42,15 +43,16 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ASC.Data.Storage
|
||||
{
|
||||
{
|
||||
[Singletone]
|
||||
public class StorageUploader
|
||||
{
|
||||
{
|
||||
protected readonly DistributedTaskQueue Queue;
|
||||
|
||||
private static readonly object Locker;
|
||||
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private TempStream TempStream { get; }
|
||||
private ICacheNotify<MigrationProgress> CacheMigrationNotify { get; }
|
||||
|
||||
static StorageUploader()
|
||||
@ -58,38 +60,39 @@ namespace ASC.Data.Storage
|
||||
Locker = new object();
|
||||
}
|
||||
|
||||
public StorageUploader(IServiceProvider serviceProvider, ICacheNotify<MigrationProgress> cacheMigrationNotify, DistributedTaskQueueOptionsManager options)
|
||||
public StorageUploader(IServiceProvider serviceProvider, TempStream tempStream, ICacheNotify<MigrationProgress> cacheMigrationNotify, DistributedTaskQueueOptionsManager options)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
CacheMigrationNotify = cacheMigrationNotify;
|
||||
TempStream = tempStream;
|
||||
CacheMigrationNotify = cacheMigrationNotify;
|
||||
Queue = options.Get(nameof(StorageUploader));
|
||||
}
|
||||
|
||||
public void Start(int tenantId, StorageSettings newStorageSettings, StorageFactoryConfig storageFactoryConfig)
|
||||
{
|
||||
lock (Locker)
|
||||
{
|
||||
{
|
||||
var id = GetCacheKey(tenantId);
|
||||
var migrateOperation = Queue.GetTask<MigrateOperation>(id);
|
||||
if (migrateOperation != null) return;
|
||||
|
||||
migrateOperation = new MigrateOperation(ServiceProvider, CacheMigrationNotify, id, tenantId, newStorageSettings, storageFactoryConfig);
|
||||
migrateOperation = new MigrateOperation(ServiceProvider, CacheMigrationNotify, id, tenantId, newStorageSettings, storageFactoryConfig, TempStream);
|
||||
Queue.QueueTask(migrateOperation);
|
||||
}
|
||||
}
|
||||
|
||||
public MigrateOperation GetProgress(int tenantId)
|
||||
{
|
||||
lock (Locker)
|
||||
{
|
||||
{
|
||||
lock (Locker)
|
||||
{
|
||||
return Queue.GetTask<MigrateOperation>(GetCacheKey(tenantId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
{
|
||||
foreach (var task in Queue.GetTasks<MigrateOperation>().Where(r => r.Status == DistributedTaskStatus.Running))
|
||||
{
|
||||
{
|
||||
Queue.CancelTask(task.Id);
|
||||
}
|
||||
}
|
||||
@ -99,7 +102,7 @@ namespace ASC.Data.Storage
|
||||
return typeof(MigrateOperation).FullName + tenantId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Transient]
|
||||
public class MigrateOperation : DistributedTaskProgress
|
||||
{
|
||||
@ -114,8 +117,15 @@ namespace ASC.Data.Storage
|
||||
ConfigPath = "";
|
||||
}
|
||||
|
||||
public MigrateOperation(IServiceProvider serviceProvider, ICacheNotify<MigrationProgress> cacheMigrationNotify, string id, int tenantId, StorageSettings settings, StorageFactoryConfig storageFactoryConfig)
|
||||
{
|
||||
public MigrateOperation(
|
||||
IServiceProvider serviceProvider,
|
||||
ICacheNotify<MigrationProgress> cacheMigrationNotify,
|
||||
string id,
|
||||
int tenantId,
|
||||
StorageSettings settings,
|
||||
StorageFactoryConfig storageFactoryConfig,
|
||||
TempStream tempStream)
|
||||
{
|
||||
Id = id;
|
||||
Status = DistributedTaskStatus.Created;
|
||||
|
||||
@ -123,24 +133,27 @@ namespace ASC.Data.Storage
|
||||
CacheMigrationNotify = cacheMigrationNotify;
|
||||
this.tenantId = tenantId;
|
||||
this.settings = settings;
|
||||
StorageFactoryConfig = storageFactoryConfig;
|
||||
StorageFactoryConfig = storageFactoryConfig;
|
||||
TempStream = tempStream;
|
||||
Modules = storageFactoryConfig.GetModuleList(ConfigPath, true);
|
||||
StepCount = Modules.Count();
|
||||
Log = serviceProvider.GetService<IOptionsMonitor<ILog>>().CurrentValue;
|
||||
}
|
||||
|
||||
private IServiceProvider ServiceProvider { get; }
|
||||
private StorageFactoryConfig StorageFactoryConfig { get; }
|
||||
private ICacheNotify<MigrationProgress> CacheMigrationNotify { get; }
|
||||
|
||||
private StorageFactoryConfig StorageFactoryConfig { get; }
|
||||
private TempStream TempStream { get; }
|
||||
private ICacheNotify<MigrationProgress> CacheMigrationNotify { get; }
|
||||
|
||||
protected override void DoJob()
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.DebugFormat("Tenant: {0}", tenantId);
|
||||
Log.DebugFormat("Tenant: {0}", tenantId);
|
||||
Status = DistributedTaskStatus.Running;
|
||||
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
var tempPath = scope.ServiceProvider.GetService<TempPath>();
|
||||
var scopeClass = scope.ServiceProvider.GetService<MigrateOperationScope>();
|
||||
var (tenantManager, securityContext, storageFactory, options, storageSettingsHelper, settingsManager) = scopeClass;
|
||||
var tenant = tenantManager.GetTenant(tenantId);
|
||||
@ -154,7 +167,7 @@ namespace ASC.Data.Storage
|
||||
var store = storageFactory.GetStorageFromConsumer(ConfigPath, tenantId.ToString(), module, storageSettingsHelper.DataStoreConsumer(settings));
|
||||
var domains = StorageFactoryConfig.GetDomainList(ConfigPath, module).ToList();
|
||||
|
||||
var crossModuleTransferUtility = new CrossModuleTransferUtility(options, oldStore, store);
|
||||
var crossModuleTransferUtility = new CrossModuleTransferUtility(options, TempStream, tempPath, oldStore, store);
|
||||
|
||||
string[] files;
|
||||
foreach (var domain in domains)
|
||||
@ -189,75 +202,75 @@ namespace ASC.Data.Storage
|
||||
|
||||
settingsManager.Save(settings);
|
||||
tenant.SetStatus(TenantStatus.Active);
|
||||
tenantManager.SaveTenant(tenant);
|
||||
tenantManager.SaveTenant(tenant);
|
||||
|
||||
Status = DistributedTaskStatus.Completed;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
{
|
||||
Status = DistributedTaskStatus.Failted;
|
||||
Exception = e;
|
||||
Log.Error(e);
|
||||
}
|
||||
|
||||
MigrationPublish();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
return MemberwiseClone();
|
||||
}
|
||||
|
||||
private void MigrationPublish()
|
||||
{
|
||||
CacheMigrationNotify.Publish(new MigrationProgress
|
||||
{
|
||||
private void MigrationPublish()
|
||||
{
|
||||
CacheMigrationNotify.Publish(new MigrationProgress
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Progress = Percentage,
|
||||
Error = Exception.ToString(),
|
||||
IsCompleted = IsCompleted
|
||||
IsCompleted = IsCompleted
|
||||
},
|
||||
CacheNotifyAction.Insert);
|
||||
CacheNotifyAction.Insert);
|
||||
}
|
||||
}
|
||||
|
||||
public class MigrateOperationScope
|
||||
{
|
||||
private TenantManager TenantManager { get; }
|
||||
private SecurityContext SecurityContext { get; }
|
||||
private StorageFactory StorageFactory { get; }
|
||||
private IOptionsMonitor<ILog> Options { get; }
|
||||
private StorageSettingsHelper StorageSettingsHelper { get; }
|
||||
private SettingsManager SettingsManager { get; }
|
||||
|
||||
public MigrateOperationScope(TenantManager tenantManager,
|
||||
SecurityContext securityContext,
|
||||
public class MigrateOperationScope
|
||||
{
|
||||
private TenantManager TenantManager { get; }
|
||||
private SecurityContext SecurityContext { get; }
|
||||
private StorageFactory StorageFactory { get; }
|
||||
private IOptionsMonitor<ILog> Options { get; }
|
||||
private StorageSettingsHelper StorageSettingsHelper { get; }
|
||||
private SettingsManager SettingsManager { get; }
|
||||
|
||||
public MigrateOperationScope(TenantManager tenantManager,
|
||||
SecurityContext securityContext,
|
||||
StorageFactory storageFactory,
|
||||
IOptionsMonitor<ILog> options,
|
||||
StorageSettingsHelper storageSettingsHelper,
|
||||
SettingsManager settingsManager)
|
||||
{
|
||||
TenantManager = tenantManager;
|
||||
SecurityContext = securityContext;
|
||||
StorageFactory = storageFactory;
|
||||
Options = options;
|
||||
StorageSettingsHelper = storageSettingsHelper;
|
||||
SettingsManager = settingsManager;
|
||||
}
|
||||
|
||||
public void Deconstruct(out TenantManager tenantManager,
|
||||
out SecurityContext securityContext,
|
||||
StorageSettingsHelper storageSettingsHelper,
|
||||
SettingsManager settingsManager)
|
||||
{
|
||||
TenantManager = tenantManager;
|
||||
SecurityContext = securityContext;
|
||||
StorageFactory = storageFactory;
|
||||
Options = options;
|
||||
StorageSettingsHelper = storageSettingsHelper;
|
||||
SettingsManager = settingsManager;
|
||||
}
|
||||
|
||||
public void Deconstruct(out TenantManager tenantManager,
|
||||
out SecurityContext securityContext,
|
||||
out StorageFactory storageFactory,
|
||||
out IOptionsMonitor<ILog> options,
|
||||
out StorageSettingsHelper storageSettingsHelper,
|
||||
out SettingsManager settingsManager)
|
||||
{
|
||||
tenantManager = TenantManager;
|
||||
securityContext = SecurityContext;
|
||||
storageFactory = StorageFactory;
|
||||
options = Options;
|
||||
storageSettingsHelper = StorageSettingsHelper;
|
||||
settingsManager = SettingsManager;
|
||||
}
|
||||
out IOptionsMonitor<ILog> options,
|
||||
out StorageSettingsHelper storageSettingsHelper,
|
||||
out SettingsManager settingsManager)
|
||||
{
|
||||
tenantManager = TenantManager;
|
||||
securityContext = SecurityContext;
|
||||
storageFactory = StorageFactory;
|
||||
options = Options;
|
||||
storageSettingsHelper = StorageSettingsHelper;
|
||||
settingsManager = SettingsManager;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,12 +67,12 @@ namespace ASC.Data.Storage
|
||||
|
||||
#region IQuotaController Members
|
||||
|
||||
public void QuotaUsedAdd(string module, string domain, string dataTag, long size)
|
||||
public void QuotaUsedAdd(string module, string domain, string dataTag, long size, bool quotaCheckFileSize = true)
|
||||
{
|
||||
size = Math.Abs(size);
|
||||
if (UsedInQuota(dataTag))
|
||||
{
|
||||
QuotaUsedCheck(size);
|
||||
QuotaUsedCheck(size, quotaCheckFileSize);
|
||||
CurrentSize += size;
|
||||
}
|
||||
SetTenantQuotaRow(module, domain, size, dataTag, true);
|
||||
@ -99,11 +99,16 @@ namespace ASC.Data.Storage
|
||||
}
|
||||
|
||||
public void QuotaUsedCheck(long size)
|
||||
{
|
||||
QuotaUsedCheck(size, true);
|
||||
}
|
||||
|
||||
public void QuotaUsedCheck(long size, bool quotaCheckFileSize)
|
||||
{
|
||||
var quota = TenantManager.GetTenantQuota(tenant);
|
||||
if (quota != null)
|
||||
{
|
||||
if (quota.MaxFileSize != 0 && quota.MaxFileSize < size)
|
||||
if (quotaCheckFileSize && quota.MaxFileSize != 0 && quota.MaxFileSize < size)
|
||||
{
|
||||
throw new TenantQuotaException(string.Format("Exceeds the maximum file size ({0}MB)", BytesToMegabytes(quota.MaxFileSize)));
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ namespace ASC.Notify.Textile.Resources {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class NotifyTemplateResource {
|
||||
@ -61,10 +61,10 @@ namespace ASC.Notify.Textile.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
/// <tr border="0" cellspacing="0" cellpadding="0">
|
||||
/// <td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
/// <img src="https://static.onlyoffice.com/media/newslet [rest of string was truncated]";.
|
||||
/// Looks up a localized string similar to <tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
/// <tr border="0" cellspacing="0" cellpadding="0">
|
||||
/// <td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
/// <img src="%IMAGEPATH%/tech-100.png" style="width: [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string FooterCommonV10 {
|
||||
get {
|
||||
@ -76,19 +76,7 @@ namespace ASC.Notify.Textile.Resources {
|
||||
/// Looks up a localized string similar to <tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
/// <tr border="0" cellspacing="0" cellpadding="0">
|
||||
/// <td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
/// <img src="https://static.onlyoffice.com/media/news [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string FooterFreeCloud {
|
||||
get {
|
||||
return ResourceManager.GetString("FooterFreeCloud", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
/// <tr border="0" cellspacing="0" cellpadding="0">
|
||||
/// <td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
/// <img src="https://static.onlyoffice.com/media/news [rest of string was truncated]";.
|
||||
/// <img src="%IMAGEPATH%/tech-100.png" style="width: [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string FooterOpensourceV10 {
|
||||
get {
|
||||
@ -96,34 +84,6 @@ namespace ASC.Notify.Textile.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;">
|
||||
/// <tbody>
|
||||
/// <tr>
|
||||
/// <td>
|
||||
/// <div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;">Please, proceed to our <a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank">FAQ section</a> if you have any questions. ONLYOFFICE is also available in <a href="https://chrome.google.com/webs [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string FooterPersonal {
|
||||
get {
|
||||
return ResourceManager.GetString("FooterPersonal", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <tr border="0" cellspacing="0" cellpadding="0">
|
||||
/// <td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
/// <a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
/// <img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
/// </a>
|
||||
/// </td>
|
||||
/// [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string FooterSocial {
|
||||
get {
|
||||
return ResourceManager.GetString("FooterSocial", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
///<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
@ -137,39 +97,14 @@ namespace ASC.Notify.Textile.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
///<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
/// <table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; background: #fff; text-align: center; width: 600px; margin: 0; padding: 0; border: 0 none; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
/// <tbody>
|
||||
/// <tr border="0 [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string HtmlMasterPersonal {
|
||||
get {
|
||||
return ResourceManager.GetString("HtmlMasterPersonal", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <body style="margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;">
|
||||
/// <div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;">
|
||||
/// <div style="background-color: #fff; width: 600px; padding:0 40px;">
|
||||
/// <a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;">
|
||||
/// <im [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string HtmlMasterPromo {
|
||||
get {
|
||||
return ResourceManager.GetString("HtmlMasterPromo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to <tr border="0" cellspacing="0" cellpadding="0">
|
||||
/// <td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
/// <a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
/// <img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
/// </a>
|
||||
/// </td [rest of string was truncated]";.
|
||||
/// </td>
|
||||
/// <td style="width: 40px; vertical-a [rest of string was truncated]";.
|
||||
/// </summary>
|
||||
public static string SocialNetworksFooterV10 {
|
||||
get {
|
||||
@ -177,18 +112,6 @@ namespace ASC.Notify.Textile.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This email is generated automatically and you do not need to answer it.
|
||||
///<br />You receive this email because you are a registered user of <a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank">onlyoffice.com</a>
|
||||
///<br />If you no longer wish to receive these emails, click on the following link: <a href="{0}" style="color: #7b7b7b;" target="_blank">Unsubscribe</a>
|
||||
///<br />.
|
||||
/// </summary>
|
||||
public static string TextForFooterWithUnsubscribe {
|
||||
get {
|
||||
return ResourceManager.GetString("TextForFooterWithUnsubscribe", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This email is generated automatically and you do not need to answer it.
|
||||
///<br />You receive this email because you are a registered user of <a href="{0}" style="color: #7b7b7b;" target="_blank">{0}</a>
|
||||
|
@ -53,10 +53,10 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FooterCommonV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
@ -88,38 +88,8 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterFreeCloud" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;">
|
||||
<img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; border-top-right-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Benötigen Sie technische Unterstützung?</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;">Verkaufsfragen</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Demoversion bestellen</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;">
|
||||
<a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;"> Senden Sie uns Ihre Frage</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;">
|
||||
<a href="mailto:sales@onlyoffice.com" style="color: #333;">Senden Sie uns eine E-Mail an</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;">
|
||||
<a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;">Senden Sie eine Anfrage an uns</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterOpensourceV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
@ -148,64 +118,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterPersonal" xml:space="preserve">
|
||||
<value><table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;">Besuchen Sie bitte unsere <a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank">FAQ Seite</a>, falls Sie noch Fragen haben. ONLYOFFICE ist verfügbar in <a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank">Chrome Web Store</a>.
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="width: 170px;">
|
||||
<div style="font-size:22px; font-weight: bold; margin-bottom: 5px;">Bleiben Sie dran!</div>
|
||||
<div style="background-color: #b9e3f5; padding: 11px; margin: 0;">
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"><img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"><img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"><img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&gid=3159387&trk=anet_ug_hm"><img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table></value>
|
||||
</data>
|
||||
<data name="FooterSocial" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="HtmlMaster" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
@ -246,85 +158,39 @@
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPersonal" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; background: #fff; text-align: center; width: 600px; margin: 0; padding: 0; border: 0 none; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tbody>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;">
|
||||
<div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;">
|
||||
<a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" />
|
||||
</a>
|
||||
</div>
|
||||
<data name="SocialNetworksFooterV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;">
|
||||
<div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"></div>
|
||||
<div style="font-family: Arial; font-size: 14px; color: #333; margin: 0; padding: 15px 30px 15px; width: 540px; height: auto; overflow: hidden; word-wrap: break-word; vertical-align: top; text-align: left; border: 0 none;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
%FOOTER%
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; text-align: center; vertical-align: top; width: 600px; margin: 0; padding: 0; border-collapse: collapse; border: 0; border-spacing: 0; ">
|
||||
<tbody>
|
||||
%FOOTERSOCIAL%
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;">
|
||||
<p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;">
|
||||
%TEXTFOOTER%
|
||||
</p>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://www.onlyoffice.com/blog" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-blog-40.png" alt="ONLYOFFICE" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPromo" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;">
|
||||
<div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;">
|
||||
<div style="background-color: #fff; width: 600px; padding:0 40px;">
|
||||
<a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;">
|
||||
<img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%>
|
||||
</a>
|
||||
</div>
|
||||
<div id="content">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 600px; background-color: #fff; padding:0 40px;">
|
||||
<div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
%FOOTER%
|
||||
|
||||
<table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;">
|
||||
<p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;">%TEXTFOOTER%</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
|
||||
<value>Diese E-Mail ist automatisch generiert und erfordert keine Antwort.
|
||||
<br />Sie erhalten diese E-Mail, weil Sie ein registrierter Benutzer bzw. eine registrierte Benutzerin von <a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank">onlyoffice.com</a> sind.
|
||||
<br />Wenn Sie keine Nachrichten mehr von uns bekommen möchten, klicken Sie auf den folgenden Link: <a href="{0}" style="color: #7b7b7b;" target="_blank">Unsubscribe</a>
|
||||
<br /></value>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="https://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
|
||||
<value>Diese E-Mail ist automatisch generiert und erfordert keine Antwort.
|
||||
|
@ -53,10 +53,10 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FooterCommonV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
@ -88,36 +88,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterFreeCloud" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;">
|
||||
<img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; border-top-right-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">¿Se necesita ayuda?</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;">Preguntas de ventas</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Solicitar demostración</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;">
|
||||
<a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;">Haga pregunta</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;">
|
||||
<a href="mailto:sales@onlyoffice.com" style="color: #333;">Contáctenos</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;">
|
||||
<a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;">Envie solicitud</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterOpensourceV10" xml:space="preserve">
|
||||
<value><tr borde = "0" espaciadodecelda = "0" rellenodecelda = "0"> <td colspan = "3" estilo = "altura: 10px; línea-altura: 10px; fondo: #fff; relleno: 0; margen: 0 ; "> & nbsp; </ td> </ tr>
|
||||
<tr borde = "0" espaciadodecelda = "0" rellenodecelda = "0">
|
||||
@ -148,64 +118,6 @@
|
||||
</ td>
|
||||
</ tr></value>
|
||||
</data>
|
||||
<data name="FooterPersonal" xml:space="preserve">
|
||||
<value><table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;">Si tiene algunas preguntas, visite <a href="http://helpcenter.onlyoffice.com/ru/ONLYOFFICE-Editors/index.aspx" target="_blank">sección FAQ</a>. También ONLYOFFICE está disponible en <a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank">Chrome Web Store</a>.
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="width: 170px;">
|
||||
<div style="font-size:22px; font-weight: bold; margin-bottom: 5px;">¡Dése cuenta!</div>
|
||||
<div style="background-color: #b9e3f5; padding: 11px; margin: 0;">
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"><img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"><img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"><img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&gid=3159387&trk=anet_ug_hm"><img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table></value>
|
||||
</data>
|
||||
<data name="FooterSocial" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="HtmlMaster" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
@ -246,85 +158,39 @@
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPersonal" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; background: #fff; text-align: center; width: 600px; margin: 0; padding: 0; border: 0 none; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tbody>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;">
|
||||
<div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;">
|
||||
<a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" />
|
||||
</a>
|
||||
</div>
|
||||
<data name="SocialNetworksFooterV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;">
|
||||
<div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"></div>
|
||||
<div style="font-family: Arial; font-size: 14px; color: #333; margin: 0; padding: 15px 30px 15px; width: 540px; height: auto; overflow: hidden; word-wrap: break-word; vertical-align: top; text-align: left; border: 0 none;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
%FOOTER%
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; text-align: center; vertical-align: top; width: 600px; margin: 0; padding: 0; border-collapse: collapse; border: 0; border-spacing: 0; ">
|
||||
<tbody>
|
||||
%FOOTERSOCIAL%
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;">
|
||||
<p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;">
|
||||
%TEXTFOOTER%
|
||||
</p>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://www.onlyoffice.com/blog" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-blog-40.png" alt="ONLYOFFICE" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPromo" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;">
|
||||
<div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;">
|
||||
<div style="background-color: #fff; width: 600px; padding:0 40px;">
|
||||
<a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;">
|
||||
<img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%>
|
||||
</a>
|
||||
</div>
|
||||
<div id="content">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 600px; background-color: #fff; padding:0 40px;">
|
||||
<div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
%FOOTER%
|
||||
|
||||
<table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;">
|
||||
<p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;">%TEXTFOOTER%</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
|
||||
<value>Este correo electrónico se genera automáticamente y no es necesario responder.
|
||||
<br /> Usted recibe este correo electrónico porque usted es un usuario registrado de <a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank">onlyoffice.com</a>
|
||||
<br /> Si ya no desea recibir estos mensajes de correo electrónico, haga click en el siguiente enlace: <a href="{0}" style="color: #7b7b7b;" target="_blank">Unsubscribe</a>
|
||||
<br /></value>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="https://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
|
||||
<value>Este correo electrónico se genera automáticamente y no es necesario responder.
|
||||
|
@ -53,10 +53,10 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FooterCommonV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
@ -88,38 +88,8 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterFreeCloud" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;">
|
||||
<img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; border-top-right-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Нужна помощь?</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;">Вопросы по покупке</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Заказ демонстрации</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;">
|
||||
<a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;">Задайте вопрос</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;">
|
||||
<a href="mailto:sales@onlyoffice.com" style="color: #333;">Напишите нам</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;">
|
||||
<a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;">Отправьте запрос</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterOpensourceV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
@ -148,64 +118,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterPersonal" xml:space="preserve">
|
||||
<value><table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;">Si vous avez des questions, passez à la section <a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank">FAQ</a>. ONLYOFFICE est aussi disponible sur <a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank">Chrome Web Store</a>.
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="width: 170px;">
|
||||
<div style="font-size:22px; font-weight: bold; margin-bottom: 5px;">Restez avec nous !</div>
|
||||
<div style="background-color: #b9e3f5; padding: 11px; margin: 0;">
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"><img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"><img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"><img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&gid=3159387&trk=anet_ug_hm"><img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table></value>
|
||||
</data>
|
||||
<data name="FooterSocial" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="HtmlMaster" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
@ -246,88 +158,39 @@
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPersonal" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; background: #fff; text-align: center; width: 600px; margin: 0; padding: 0; border: 0 none; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tbody>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;">
|
||||
<div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;">
|
||||
<a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" />
|
||||
</a>
|
||||
</div>
|
||||
<data name="SocialNetworksFooterV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;">
|
||||
<div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"></div>
|
||||
<div style="font-family: Arial; font-size: 14px; color: #333; margin: 0; padding: 15px 30px 15px; width: 540px; height: auto; overflow: hidden; word-wrap: break-word; vertical-align: top; text-align: left; border: 0 none;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
%FOOTER%
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; text-align: center; vertical-align: top; width: 600px; margin: 0; padding: 0; border-collapse: collapse; border: 0; border-spacing: 0; ">
|
||||
<tbody>
|
||||
%FOOTERSOCIAL%
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;">
|
||||
<p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;">
|
||||
%TEXTFOOTER%
|
||||
</p>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://www.onlyoffice.com/blog" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-blog-40.png" alt="ONLYOFFICE" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPromo" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;">
|
||||
<div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;">
|
||||
<div style="background-color: #fff; width: 600px; padding:0 40px;">
|
||||
<a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;">
|
||||
<img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%>
|
||||
</a>
|
||||
</div>
|
||||
<div id="content">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 600px; background-color: #fff; padding:0 40px;">
|
||||
<div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
%FOOTER%
|
||||
|
||||
<table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;">
|
||||
<p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;">%TEXTFOOTER%</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="TextForFooter" xml:space="preserve">
|
||||
<value>.</value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
|
||||
<value>Cet e-mail est généré automatiquement et vous n'avez pas besoin de répondre.
|
||||
<br />Vous recevez ce courriel parce que vous êtes un utilisateur enregistré de <a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank">onlyoffice.com</a>
|
||||
<br />Si vous ne souhaitez plus recevoir ces e-mails, cliquez sur le lien suivant: <a href="{0}" style="color: #7b7b7b;" target="_blank">Se Désabonner</a>
|
||||
<br /></value>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="https://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
|
||||
<value>Cet e-mail est généré automatiquement et vous n'avez pas besoin de répondre.
|
||||
|
@ -53,10 +53,10 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FooterCommonV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
@ -88,38 +88,8 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterFreeCloud" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;">
|
||||
<img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; border-top-right-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Serve aiuto?</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;">Domande sulle vendite</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Ordina la dimostrazione</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;">
|
||||
<a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;">Send your question</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;">
|
||||
<a href="mailto:sales@onlyoffice.com" style="color: #333;">Email us</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;">
|
||||
<a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;">invia una richiesta</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterOpensourceV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"> </td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
@ -148,64 +118,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterPersonal" xml:space="preserve">
|
||||
<value><table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;">Prego andare su<a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank">FAQ section</a> se avete domande. ONLYOFFICE è inoltre disponibile in <a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank">Chrome Web Store</a>.
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="width: 170px;">
|
||||
<div style="font-size:22px; font-weight: bold; margin-bottom: 5px;">Stay tuned!</div>
|
||||
<div style="background-color: #b9e3f5; padding: 11px; margin: 0;">
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"><img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"><img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"><img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&gid=3159387&trk=anet_ug_hm"><img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table></value>
|
||||
</data>
|
||||
<data name="FooterSocial" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="HtmlMaster" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
@ -244,80 +156,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPersonal" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; background: #fff; text-align: center; width: 600px; margin: 0; padding: 0; border: 0 none; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tbody>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;">
|
||||
<div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;">
|
||||
<a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;">
|
||||
<div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"></div>
|
||||
<div style="font-family: Arial; font-size: 14px; color: #333; margin: 0; padding: 15px 30px 15px; width: 540px; height: auto; overflow: hidden; word-wrap: break-word; vertical-align: top; text-align: left; border: 0 none;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
%FOOTER%
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; text-align: center; vertical-align: top; width: 600px; margin: 0; padding: 0; border-collapse: collapse; border: 0; border-spacing: 0; ">
|
||||
<tbody>
|
||||
%FOOTERSOCIAL%
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;">
|
||||
<p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;">
|
||||
%TEXTFOOTER%
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPromo" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;">
|
||||
<div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;">
|
||||
<div style="background-color: #fff; width: 600px; padding:0 40px;">
|
||||
<a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;">
|
||||
<img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%>
|
||||
</a>
|
||||
</div>
|
||||
<div id="content">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 600px; background-color: #fff; padding:0 40px;">
|
||||
<div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
%FOOTER%
|
||||
|
||||
<table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;">
|
||||
<p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;">%TEXTFOOTER%</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="SocialNetworksFooterV10" xml:space="preserve">
|
||||
@ -354,12 +192,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
|
||||
<value>Questo messaggio è stato generato automaticamente e non necessità di risposta.
|
||||
<br />Hai ricevuto questo messaggio in quanto sei un untente registrato su <a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank">onlyoffice.com</a>
|
||||
<br />Se non intendi ricevere altri messaggi in futuro, fai click qui: <a href="{0}" style="color: #7b7b7b;" target="_blank">Unsubscribe</a>
|
||||
<br /></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
|
||||
<value>Questo messaggio è stato generato automaticamente e non necessità di risposta.
|
||||
<br />Hai ricevuto questo messaggio in quanto sei un untente registrato su <a href="{0}" style="color: #7b7b7b;" target="_blank">{0}</a>
|
||||
|
@ -1,64 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
@ -112,42 +53,12 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FooterCommonV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;">
|
||||
<img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; border-top-right-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Need tech help?</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;">Sales Questions</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Order Demo</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;">
|
||||
<a href="%SUPPORTURL%" target="_blank" style="color: #333;">Send your question</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;">
|
||||
<a href="mailto:%SALESEMAIL%" style="color: #333;">Email us</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;">
|
||||
<a href="%DEMOURL%" target="_blank" style="color: #333;">Send a request</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterFreeCloud" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
@ -167,13 +78,13 @@
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;">
|
||||
<a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;">Send your question</a>
|
||||
<a href="%SUPPORTURL%" target="_blank" style="color: #333;">Send your question</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;">
|
||||
<a href="mailto:sales@onlyoffice.com" style="color: #333;">Email us</a>
|
||||
<a href="mailto:%SALESEMAIL%" style="color: #333;">Email us</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;">
|
||||
<a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;">Send a request</a>
|
||||
<a href="%DEMOURL%" target="_blank" style="color: #333;">Send a request</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
@ -207,64 +118,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterPersonal" xml:space="preserve">
|
||||
<value><table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;">Please, proceed to our <a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank">FAQ section</a> if you have any questions. ONLYOFFICE is also available in <a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank">Chrome Web Store</a>.
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="width: 170px;">
|
||||
<div style="font-size:22px; font-weight: bold; margin-bottom: 5px;">Stay tuned!</div>
|
||||
<div style="background-color: #b9e3f5; padding: 11px; margin: 0;">
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"><img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"><img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"><img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&gid=3159387&trk=anet_ug_hm"><img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table></value>
|
||||
</data>
|
||||
<data name="FooterSocial" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="HtmlMaster" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
@ -303,80 +156,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPersonal" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; background: #fff; text-align: center; width: 600px; margin: 0; padding: 0; border: 0 none; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tbody>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;">
|
||||
<div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;">
|
||||
<a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;">
|
||||
<div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"></div>
|
||||
<div style="font-family: Arial; font-size: 14px; color: #333; margin: 0; padding: 15px 30px 15px; width: 540px; height: auto; overflow: hidden; word-wrap: break-word; vertical-align: top; text-align: left; border: 0 none;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
%FOOTER%
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; text-align: center; vertical-align: top; width: 600px; margin: 0; padding: 0; border-collapse: collapse; border: 0; border-spacing: 0; ">
|
||||
<tbody>
|
||||
%FOOTERSOCIAL%
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;">
|
||||
<p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;">
|
||||
%TEXTFOOTER%
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPromo" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;">
|
||||
<div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;">
|
||||
<div style="background-color: #fff; width: 600px; padding:0 40px;">
|
||||
<a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;">
|
||||
<img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%>
|
||||
</a>
|
||||
</div>
|
||||
<div id="content">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 600px; background-color: #fff; padding:0 40px;">
|
||||
<div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
%FOOTER%
|
||||
|
||||
<table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;">
|
||||
<p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;">%TEXTFOOTER%</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="SocialNetworksFooterV10" xml:space="preserve">
|
||||
@ -413,12 +192,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
|
||||
<value>This email is generated automatically and you do not need to answer it.
|
||||
<br />You receive this email because you are a registered user of <a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank">onlyoffice.com</a>
|
||||
<br />If you no longer wish to receive these emails, click on the following link: <a href="{0}" style="color: #7b7b7b;" target="_blank">Unsubscribe</a>
|
||||
<br /></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
|
||||
<value>This email is generated automatically and you do not need to answer it.
|
||||
<br />You receive this email because you are a registered user of <a href="{0}" style="color: #7b7b7b;" target="_blank">{0}</a>
|
||||
|
@ -53,10 +53,10 @@
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FooterCommonV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
@ -88,36 +88,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterFreeCloud" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; border-top-left-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;">
|
||||
<img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 210px; height: 108px; background: #f6f6f6; -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; border-top-right-radius: 6px;">
|
||||
<img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Нужна помощь?</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;">Вопросы по покупке</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;">Заказ демонстрации</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;">
|
||||
<a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;">Задайте вопрос</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;">
|
||||
<a href="mailto:sales@onlyoffice.com" style="color: #333;">Напишите нам</a>
|
||||
</td>
|
||||
<td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 210px; background: #f6f6f6;-moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;">
|
||||
<a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;">Отправьте запрос</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterOpensourceV10" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0"><td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;">&nbsp;</td></tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
@ -148,64 +118,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="FooterPersonal" xml:space="preserve">
|
||||
<value><table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;">Если у вас есть какие-то вопросы, перейдите в <a href="http://helpcenter.onlyoffice.com/ru/ONLYOFFICE-Editors/index.aspx" target="_blank">раздел FAQ</a>. ONLYOFFICE также доступен в <a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank">Chrome Web Store</a>.
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="width: 170px;">
|
||||
<div style="font-size:22px; font-weight: bold; margin-bottom: 5px;">Будьте в курсе!</div>
|
||||
<div style="background-color: #b9e3f5; padding: 11px; margin: 0;">
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"><img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"><img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"><img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
<a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&gid=3159387&trk=anet_ug_hm"><img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table></value>
|
||||
</data>
|
||||
<data name="FooterSocial" xml:space="preserve">
|
||||
<value><tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;">
|
||||
<a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;">
|
||||
<a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;">
|
||||
<a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" />
|
||||
</a>
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="HtmlMaster" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
@ -244,80 +156,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPersonal" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;">
|
||||
<div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;">
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; background: #fff; text-align: center; width: 600px; margin: 0; padding: 0; border: 0 none; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tbody>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;">
|
||||
<div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;">
|
||||
<a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank">
|
||||
<img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;">
|
||||
<div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"></div>
|
||||
<div style="font-family: Arial; font-size: 14px; color: #333; margin: 0; padding: 15px 30px 15px; width: 540px; height: auto; overflow: hidden; word-wrap: break-word; vertical-align: top; text-align: left; border: 0 none;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
%FOOTER%
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" cellpadding="0" style="font-family: Arial; font-size: 14px; color: #333; text-align: center; vertical-align: top; width: 600px; margin: 0; padding: 0; border-collapse: collapse; border: 0; border-spacing: 0; ">
|
||||
<tbody>
|
||||
%FOOTERSOCIAL%
|
||||
<tr border="0" cellspacing="0" cellpadding="0">
|
||||
<td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;">
|
||||
<p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;">
|
||||
%TEXTFOOTER%
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="HtmlMasterPromo" xml:space="preserve">
|
||||
<value><body style="margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;">
|
||||
<div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;">
|
||||
<div style="background-color: #fff; width: 600px; padding:0 40px;">
|
||||
<a href="http://www.onlyoffice.com/ru/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;">
|
||||
<img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%>
|
||||
</a>
|
||||
</div>
|
||||
<div id="content">
|
||||
<table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 600px; background-color: #fff; padding:0 40px;">
|
||||
<div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;">
|
||||
%CONTENT%
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
%FOOTER%
|
||||
|
||||
<table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;">
|
||||
<p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;">%TEXTFOOTER%</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body></value>
|
||||
</data>
|
||||
<data name="SocialNetworksFooterV10" xml:space="preserve">
|
||||
@ -354,12 +192,6 @@
|
||||
</td>
|
||||
</tr></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
|
||||
<value>Это сообщение создано автоматически, и отвечать на него не нужно.
|
||||
<br />Вы получили это сообщение, так как являетесь зарегистрированным пользователем <a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank">onlyoffice.com</a>
|
||||
<br />Если вы больше не хотите получать эти сообщения, нажмите на следующую ссылку: <a href="{0}" style="color: #7b7b7b;" target="_blank">Отписаться</a>
|
||||
<br /></value>
|
||||
</data>
|
||||
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
|
||||
<value>Это сообщение создано автоматически, и отвечать на него не нужно.
|
||||
<br />Вы получили это сообщение, так как являетесь зарегистрированным пользователем <a href="{0}" style="color: #7b7b7b;" target="_blank">{0}</a>
|
||||
|
1
common/ASC.SsoAuth/Procfile
Normal file
1
common/ASC.SsoAuth/Procfile
Normal file
@ -0,0 +1 @@
|
||||
web: npm start
|
25
common/ASC.SsoAuth/README.md
Normal file
25
common/ASC.SsoAuth/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# ASC Single Sign-On handler
|
||||
|
||||
ASC Single Sign-On handler to use with [portal "/ssologin.ashx" handler](https://github.com/ONLYOFFICE/portals/blob/develop/web/studio/ASC.Web.Studio/HttpHandlers/SsoHandler.cs) - sso authentication provider for portal access.
|
||||
|
||||
### Installation and usage
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
$ npm start
|
||||
```
|
||||
|
||||
### Integrations
|
||||
|
||||
+ [OneLogin](https://www.onelogin.com/)
|
||||
+ [Shibboleth](http://shibboleth.net/)
|
||||
|
||||
|
||||
### License
|
||||
|
||||
[GNU GPL V3](LICENSE)
|
||||
|
||||
|
||||
### Note
|
||||
|
||||
Based on [samlify](https://samlify.js.org) by [Tony Ngan](https://github.com/tngan)
|
92
common/ASC.SsoAuth/app.js
Normal file
92
common/ASC.SsoAuth/app.js
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || "development";
|
||||
|
||||
const fs = require("fs"),
|
||||
http = require("http"),
|
||||
express = require("express"),
|
||||
morgan = require("morgan"),
|
||||
cookieParser = require("cookie-parser"),
|
||||
bodyParser = require("body-parser"),
|
||||
session = require("express-session"),
|
||||
winston = require("winston"),
|
||||
config = require("./config").get(),
|
||||
path = require("path"),
|
||||
exphbs = require("express-handlebars"),
|
||||
favicon = require("serve-favicon"),
|
||||
cors = require("cors");
|
||||
|
||||
require('winston-daily-rotate-file');
|
||||
|
||||
const app = express();
|
||||
let logDir = config["logPath"] ? config["logPath"] : config.app.logDir;
|
||||
// ensure log directory exists
|
||||
fs.existsSync(logDir) || fs.mkdirSync(logDir);
|
||||
|
||||
let transports = [];
|
||||
|
||||
if (config.logger.file) {
|
||||
logDir = config["logPath"] ? logDir : (config.app.logDir[0] === "." ? path.join(__dirname, config.app.logDir) : config.app.logDir);
|
||||
config.logger.file.filename = path.join(logDir, config.app.logName);
|
||||
transports.push(new (winston.transports.DailyRotateFile)(config.logger.file));
|
||||
}
|
||||
|
||||
if (config.logger.console) {
|
||||
transports.push(new (winston.transports.Console)(config.logger.console));
|
||||
}
|
||||
|
||||
let logger = winston.createLogger({
|
||||
transports: transports,
|
||||
exitOnError: false
|
||||
});
|
||||
|
||||
logger.stream = {
|
||||
write: function(message) {
|
||||
logger.info(message.trim());
|
||||
}
|
||||
};
|
||||
|
||||
// view engine setup
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.engine("handlebars", exphbs({ defaultLayout: "main" }));
|
||||
app.set("view engine", "handlebars");
|
||||
|
||||
app.use(favicon(path.join(__dirname, "public", "favicon.ico")))
|
||||
.use(morgan("combined", { "stream": logger.stream }))
|
||||
.use(cookieParser())
|
||||
.use(bodyParser.json())
|
||||
.use(bodyParser.urlencoded({ extended: false }))
|
||||
.use(session(
|
||||
{
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
secret: config["core.machinekey"] ? config["core.machinekey"] : config.app.machinekey
|
||||
}))
|
||||
.use(cors());
|
||||
|
||||
require("./app/middleware/saml")(app, config, logger);
|
||||
require("./app/routes")(app, config, logger);
|
||||
|
||||
const httpServer = http.createServer(app);
|
||||
|
||||
httpServer.listen(config.app.port,
|
||||
function () {
|
||||
logger.info(`Start SSO Service Provider listening on port ${config.app.port} for http`);
|
||||
});
|
162
common/ASC.SsoAuth/app/fileManager.js
Normal file
162
common/ASC.SsoAuth/app/fileManager.js
Normal file
@ -0,0 +1,162 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require('fs'),
|
||||
path = require('path'),
|
||||
co = require("co"),
|
||||
request = require("request");
|
||||
|
||||
const config = require('../config');
|
||||
const appDataDirPath = path.join(__dirname, config.get("web:data"));
|
||||
|
||||
function getDataDirPath(subdir = "", createIfNotExist = true) {
|
||||
var fullPath = path.join(appDataDirPath, subdir);
|
||||
if (!createIfNotExist) return fullPath;
|
||||
|
||||
return co(function* () {
|
||||
yield createDir(appDataDirPath);
|
||||
return createDir(fullPath);
|
||||
});
|
||||
}
|
||||
|
||||
function createDir(pathToDir) {
|
||||
return co(function* () {
|
||||
const exist = yield checkDirExist(pathToDir);
|
||||
if (exist) {
|
||||
return pathToDir;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.mkdir(pathToDir,
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(pathToDir);
|
||||
});
|
||||
});
|
||||
}).catch((err) => {
|
||||
log.error("Create App_Data error", err);
|
||||
});
|
||||
}
|
||||
|
||||
function checkDirExist(pathToDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(pathToDir,
|
||||
function (err, stats) {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
resolve(false);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stats.isDirectory());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkFileExist(pathToFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(pathToFile,
|
||||
function (err, stats) {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
resolve(false);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(stats.isFile());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyFile(source, target, append = false) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var rd = fs.createReadStream(source);
|
||||
rd.on('error', rejectCleanup);
|
||||
|
||||
const writeOptions = { flags: append ? 'a' : 'w' };
|
||||
|
||||
var wr = fs.createWriteStream(target, writeOptions);
|
||||
wr.on('error', rejectCleanup);
|
||||
|
||||
function rejectCleanup(err) {
|
||||
rd.destroy();
|
||||
wr.end();
|
||||
reject(err);
|
||||
}
|
||||
wr.on('finish', resolve);
|
||||
rd.pipe(wr);
|
||||
});
|
||||
}
|
||||
|
||||
function moveFile(from, to) {
|
||||
return co(function* () {
|
||||
const isExist = yield checkFileExist(from);
|
||||
if (!isExist) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.rename(from, to, () => { resolve(); });
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFile(pathToFile) {
|
||||
return co(function* () {
|
||||
const isExist = yield checkFileExist(pathToFile);
|
||||
if (!isExist) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.unlink(pathToFile, () => { resolve(); });
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadFile(uri, filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = request
|
||||
.get(uri, { rejectUnauthorized: false })
|
||||
.on('error', (err) => {
|
||||
reject(err);
|
||||
})
|
||||
.pipe(fs.createWriteStream(filePath))
|
||||
.on('error', (err) => {
|
||||
reject(err);
|
||||
})
|
||||
.on('finish', () => {
|
||||
resolve(filePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { checkFileExist, createDir, copyFile, moveFile, deleteFile, getDataDirPath, downloadFile };
|
||||
|
91
common/ASC.SsoAuth/app/middleware/saml.js
Normal file
91
common/ASC.SsoAuth/app/middleware/saml.js
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
module.exports = (app, config, logger) => {
|
||||
const urlResolver = require("../utils/resolver")(logger);
|
||||
const coder = require("../utils/coder");
|
||||
const converter = require("../utils/converter")(logger);
|
||||
const _ = require("lodash");
|
||||
const fetch = require("node-fetch");
|
||||
const routes = _.values(config.routes);
|
||||
|
||||
const fetchConfig = async (req, res, next) => {
|
||||
const foundRoutes =
|
||||
req.url && req.url.length > 0
|
||||
? routes.filter(function (route) {
|
||||
return 0 === req.url.indexOf(route);
|
||||
})
|
||||
: [];
|
||||
|
||||
if (!foundRoutes.length) {
|
||||
logger.error(`invalid route ${req.originalUrl}`);
|
||||
return res.redirect(urlResolver.getPortal404Url(req));
|
||||
}
|
||||
|
||||
const baseUrl = urlResolver.getBaseUrl(req);
|
||||
|
||||
const promise = new Promise(async (resolve) => {
|
||||
var url = urlResolver.getPortalSsoConfigUrl(req);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response || response.status === 404) {
|
||||
if (response) {
|
||||
logger.error(response.statusText);
|
||||
}
|
||||
return resolve(res.redirect(urlResolver.getPortal404Url(req)));
|
||||
} else if (response.status !== 200) {
|
||||
throw new Error(`Invalid response status ${response.status}`);
|
||||
} else if (!response.body) {
|
||||
throw new Error("Empty config response");
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
const ssoConfig = coder.decodeData(text);
|
||||
|
||||
const idp = converter.toIdp(ssoConfig);
|
||||
|
||||
const sp = converter.toSp(ssoConfig, baseUrl);
|
||||
|
||||
const providersInfo = {
|
||||
sp: sp,
|
||||
idp: idp,
|
||||
mapping: ssoConfig.FieldMapping,
|
||||
settings: ssoConfig,
|
||||
};
|
||||
|
||||
req.providersInfo = providersInfo;
|
||||
|
||||
return resolve(next());
|
||||
}).catch((error) => {
|
||||
logger.error(error);
|
||||
return res.redirect(
|
||||
urlResolver.getPortalAuthErrorUrl(
|
||||
req,
|
||||
urlResolver.ErrorMessageKey.SsoError
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
app.use(fetchConfig);
|
||||
};
|
27
common/ASC.SsoAuth/app/model/logout.js
Normal file
27
common/ASC.SsoAuth/app/model/logout.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
class LogoutModel {
|
||||
constructor(nameId, sessionIndex) {
|
||||
this.nameID = nameId;
|
||||
this.sessionIndex = sessionIndex;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogoutModel;
|
44
common/ASC.SsoAuth/app/model/user.js
Normal file
44
common/ASC.SsoAuth/app/model/user.js
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
class UserModel {
|
||||
constructor(nameId, sessionIndex, attributes, mapping) {
|
||||
this.nameID = nameId;
|
||||
this.sessionID = sessionIndex;
|
||||
|
||||
const getValue = function (obj) {
|
||||
if (typeof (obj) === "string")
|
||||
return obj;
|
||||
|
||||
if (Array.isArray(obj))
|
||||
return obj.length > 0 ? obj[0] : "";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.email = getValue(attributes[mapping.Email]);
|
||||
this.firstName = getValue(attributes[mapping.FirstName]);
|
||||
this.lastName = getValue(attributes[mapping.LastName]);
|
||||
this.location = getValue(attributes[mapping.Location]);
|
||||
this.phone = getValue(attributes[mapping.Phone]);
|
||||
this.title = getValue(attributes[mapping.Title]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserModel;
|
786
common/ASC.SsoAuth/app/routes.js
Normal file
786
common/ASC.SsoAuth/app/routes.js
Normal file
@ -0,0 +1,786 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
module.exports = function (app, config, logger) {
|
||||
const saml = require("samlify");
|
||||
const { SamlLib: libsaml } = saml;
|
||||
const urlResolver = require("./utils/resolver")(logger);
|
||||
const coder = require("./utils/coder");
|
||||
const urn = require("samlify/build/src/urn");
|
||||
const fetch = require("node-fetch");
|
||||
const fileManager = require('./fileManager');
|
||||
const forge = require('node-forge');
|
||||
const path = require('path');
|
||||
const uuid = require('uuid');
|
||||
const formidable = require('formidable');
|
||||
const UserModel = require("./model/user");
|
||||
const LogoutModel = require("./model/logout");
|
||||
const fs = require('fs');
|
||||
let uploadDir = "";
|
||||
const selfSignedDomain = "myselfsigned.crt";
|
||||
|
||||
function verifySetting(req) {
|
||||
if (!req.providersInfo.settings.EnableSso) {
|
||||
logger.error("Sso settings is disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getError(e) {
|
||||
if (!e) return "EMPTY_ERROR";
|
||||
|
||||
if (typeof e === "object" && e.message) {
|
||||
return e.message;
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
function getSpMetadata(req, res) {
|
||||
const sp = req.providersInfo.sp;
|
||||
|
||||
res.type("application/xml");
|
||||
|
||||
const xml = sp.getMetadata();
|
||||
|
||||
if (config.app.logSamlData) {
|
||||
logger.debug(xml);
|
||||
}
|
||||
|
||||
return res.send(xml);
|
||||
}
|
||||
|
||||
function onGenerateCert(req, res){
|
||||
try {
|
||||
res.status(200).send(generateCertificate());
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).send("Cannot generate certificate");
|
||||
}
|
||||
}
|
||||
function onValidateCerts(req, res){
|
||||
try {
|
||||
res.status(200).send(validateCertificate(req.body.certs));
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).send("Invalid certificate");
|
||||
}
|
||||
}
|
||||
function onUploadMetadata(req, res) {
|
||||
function formParse(err, fields, files) {
|
||||
if (err) {
|
||||
res.status(500).send(err).end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!files || !files.metadata) {
|
||||
res.status(500).send("Metadata file not transferred").end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!files.metadata.name.toLowerCase().endsWith(".xml")) {
|
||||
fileManager.deleteFile(files.metadata.path);
|
||||
res.status(500).send("Incorrect metadata file type. A .xml file is required.").end();
|
||||
return;
|
||||
}
|
||||
|
||||
const idp = saml.IdentityProvider({
|
||||
metadata: fs.readFileSync(files.metadata.path)
|
||||
});
|
||||
const idpMetadata = idp.entityMeta;
|
||||
|
||||
if (!idpMetadata ||
|
||||
!idpMetadata.meta ||
|
||||
!idpMetadata.meta.entityDescriptor ||
|
||||
!idpMetadata.meta.singleSignOnService) {
|
||||
fileManager.deleteFile(files.metadata.path);
|
||||
res.status(500).send("Invalid metadata xml file").end();
|
||||
return;
|
||||
}
|
||||
|
||||
fileManager.deleteFile(files.metadata.path);
|
||||
res.status(200).send(idpMetadata).end();
|
||||
}
|
||||
|
||||
const form = new formidable.IncomingForm();
|
||||
form.uploadDir = uploadDir;
|
||||
form.parse(req, formParse);
|
||||
}
|
||||
|
||||
function onLoadMetadata(req, res) {
|
||||
try {
|
||||
const filePath = path.join(uploadDir, uuid.v1() + ".xml");
|
||||
|
||||
fileManager.downloadFile(req.body.url, filePath)
|
||||
.then((result) => {
|
||||
const idp = saml.IdentityProvider({
|
||||
metadata: fs.readFileSync(result)
|
||||
});
|
||||
const idpMetadata = idp.entityMeta;
|
||||
|
||||
if (!idpMetadata ||
|
||||
!idpMetadata.meta ||
|
||||
!idpMetadata.meta.entityDescriptor ||
|
||||
!idpMetadata.meta.singleSignOnService) {
|
||||
fileManager.deleteFile(result);
|
||||
res.status(500).send("Invalid metadata xml file").end();
|
||||
return;
|
||||
}
|
||||
|
||||
fileManager.deleteFile(result);
|
||||
res.status(200).send(idpMetadata).end();
|
||||
})
|
||||
.catch((error) => {
|
||||
fileManager.deleteFile(filePath);
|
||||
res.status(500).send("Metadata file not transferred");
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
res.status(500).send(error);
|
||||
}
|
||||
}
|
||||
function generateCertificate() {
|
||||
const pki = forge.pki;
|
||||
|
||||
let keys = pki.rsa.generateKeyPair(2048);
|
||||
let cert = pki.createCertificate();
|
||||
|
||||
cert.publicKey = keys.publicKey;
|
||||
cert.serialNumber = "01";
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
let attr = [{
|
||||
name: "commonName",
|
||||
value: selfSignedDomain
|
||||
}];
|
||||
|
||||
cert.setSubject(attr);
|
||||
cert.setIssuer(attr);
|
||||
|
||||
cert.sign(keys.privateKey);
|
||||
|
||||
let crt = pki.certificateToPem(cert);
|
||||
let key = pki.privateKeyToPem(keys.privateKey);
|
||||
|
||||
return {
|
||||
crt: crt,
|
||||
key: key
|
||||
};
|
||||
}
|
||||
function validateCertificate(certs){
|
||||
const result = [];
|
||||
const pki = forge.pki;
|
||||
|
||||
certs.forEach(function (data) {
|
||||
if (!data.crt)
|
||||
throw "Empty public certificate";
|
||||
|
||||
if (data.crt[0] !== "-")
|
||||
data.crt = "-----BEGIN CERTIFICATE-----\n" + data.crt + "\n-----END CERTIFICATE-----";
|
||||
|
||||
const cert = pki.certificateFromPem(data.crt);
|
||||
|
||||
const publicKey = cert.publicKey;
|
||||
if (!publicKey)
|
||||
throw "Invalid public cert";
|
||||
|
||||
if (data.key) {
|
||||
const privateKey = pki.privateKeyFromPem(data.key);
|
||||
if (!privateKey)
|
||||
throw "Invalid private key";
|
||||
|
||||
const md = forge.md.sha1.create();
|
||||
md.update('sign this', 'utf8');
|
||||
const signature = privateKey.sign(md);
|
||||
|
||||
// verify data with a public key
|
||||
// (defaults to RSASSA PKCS#1 v1.5)
|
||||
const verified = publicKey.verify(md.digest().bytes(), signature);
|
||||
|
||||
if (!verified)
|
||||
throw "Invalid key-pair (unverified signed data test)";
|
||||
}
|
||||
|
||||
const domainName = cert.subject.getField("CN").value || cert.issuer.getField("CN").value;
|
||||
const startDate = cert.validity.notBefore.toISOString().split(".")[0] + "Z";
|
||||
const expiredDate = cert.validity.notAfter.toISOString().split(".")[0] + "Z";
|
||||
|
||||
result.push({
|
||||
selfSigned: domainName === selfSignedDomain,
|
||||
crt: data.crt,
|
||||
key: data.key,
|
||||
action: data.action,
|
||||
domainName: domainName,
|
||||
startDate: startDate,
|
||||
expiredDate: expiredDate
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function sendToIDP(res, request, isPost) {
|
||||
logger.info(`SEND ${isPost ? "POST" : "REDIRECT"} to IDP`);
|
||||
if (isPost) {
|
||||
return res.render("actions", request); // see more in "actions.handlebars"
|
||||
} else {
|
||||
return res.redirect(request.context);
|
||||
}
|
||||
}
|
||||
|
||||
const createAuthnTemplateCallback = (_idp, _sp, method) => (template) => {
|
||||
const metadata = { idp: _idp.entityMeta, sp: _sp.entityMeta };
|
||||
const spSetting = _sp.entitySetting;
|
||||
const entityEndpoint = metadata.idp.getSingleSignOnService(method);
|
||||
const nameIDFormat = spSetting.nameIDFormat;
|
||||
const selectedNameIDFormat = Array.isArray(nameIDFormat)
|
||||
? nameIDFormat[0]
|
||||
: nameIDFormat;
|
||||
const id = spSetting.generateID();
|
||||
const assertionConsumerServiceURL = metadata.sp.getAssertionConsumerService(
|
||||
method
|
||||
);
|
||||
|
||||
const data = {
|
||||
ID: id,
|
||||
Destination: entityEndpoint,
|
||||
Issuer: metadata.sp.getEntityID(),
|
||||
IssueInstant: new Date().toISOString(),
|
||||
NameIDFormat: selectedNameIDFormat,
|
||||
AssertionConsumerServiceURL: assertionConsumerServiceURL,
|
||||
EntityID: metadata.sp.getEntityID(),
|
||||
AllowCreate: spSetting.allowCreate,
|
||||
};
|
||||
|
||||
const t = template.context || template;
|
||||
|
||||
const rawSamlRequest = libsaml.replaceTagsByValue(t, data);
|
||||
return {
|
||||
id: id,
|
||||
context: rawSamlRequest,
|
||||
};
|
||||
};
|
||||
|
||||
const sendLoginRequest = (req, res) => {
|
||||
try {
|
||||
if (!verifySetting(req)) {
|
||||
return res.redirect(urlResolver.getPortal500Url(req));
|
||||
}
|
||||
|
||||
const sp = req.providersInfo.sp;
|
||||
const idp = req.providersInfo.idp;
|
||||
|
||||
const isPost =
|
||||
req.providersInfo.settings.IdpSettings.SsoBinding ===
|
||||
urn.namespace.binding.post;
|
||||
|
||||
const method = isPost
|
||||
? urn.wording.binding.post
|
||||
: urn.wording.binding.redirect;
|
||||
|
||||
const data = sp.createLoginRequest(
|
||||
idp,
|
||||
method,
|
||||
createAuthnTemplateCallback(idp, sp, method)
|
||||
);
|
||||
|
||||
return sendToIDP(res, data, isPost);
|
||||
} catch (e) {
|
||||
logger.error(`sendLoginRequest ${getError(e)}`);
|
||||
return res.redirect(
|
||||
urlResolver.getPortalAuthErrorUrl(
|
||||
req,
|
||||
urlResolver.ErrorMessageKey.SsoError
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const parseLoginResponse = (sp, idp, method, req) => {
|
||||
return sp.parseLoginResponse(idp, method, req).catch((e) => {
|
||||
const message = getError(e);
|
||||
if (message == "ERR_FAILED_TO_VERIFY_SIGNATURE") {
|
||||
if (
|
||||
idp.entitySetting.messageSigningOrder == urn.MessageSignatureOrder.ETS
|
||||
) {
|
||||
idp.entitySetting.messageSigningOrder = urn.MessageSignatureOrder.STE;
|
||||
} else {
|
||||
idp.entitySetting.messageSigningOrder = urn.MessageSignatureOrder.ETS;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`parseLoginResponse -> Changing urn.MessageSignatureOrder to ${idp.entitySetting.messageSigningOrder}`
|
||||
);
|
||||
|
||||
return sp.parseLoginResponse(idp, method, req);
|
||||
} else {
|
||||
logger.error(`parseLoginResponse failed ${message}`);
|
||||
}
|
||||
|
||||
return Promise.reject(e);
|
||||
});
|
||||
};
|
||||
|
||||
const onLoginResponse = async (req, res) => {
|
||||
try {
|
||||
if (!verifySetting(req)) {
|
||||
return res.redirect(urlResolver.getPortal500Url(req));
|
||||
}
|
||||
|
||||
const sp = req.providersInfo.sp;
|
||||
const idp = req.providersInfo.idp;
|
||||
|
||||
const method =
|
||||
req.method === "POST"
|
||||
? urn.wording.binding.post
|
||||
: urn.wording.binding.redirect;
|
||||
|
||||
const requestInfo = await parseLoginResponse(sp, idp, method, req);
|
||||
|
||||
if (config.app.logSamlData) {
|
||||
logger.debug(`parseLoginResponse ${JSON.stringify(requestInfo)}`);
|
||||
}
|
||||
|
||||
if (!requestInfo.extract.attributes) {
|
||||
return res.redirect(
|
||||
urlResolver.getPortalAuthErrorUrl(
|
||||
req,
|
||||
urlResolver.ErrorMessageKey.SsoAttributesNotFound
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.app.logSamlData) {
|
||||
logger.debug(`parseLoginResponse nameID=${requestInfo.extract.nameID}`);
|
||||
logger.debug(
|
||||
`parseLoginResponse sessionIndex=${JSON.stringify(
|
||||
requestInfo.extract.sessionIndex
|
||||
)}`
|
||||
);
|
||||
logger.debug(
|
||||
`parseLoginResponse attributes=${JSON.stringify(
|
||||
requestInfo.extract.attributes
|
||||
)}`
|
||||
);
|
||||
logger.debug(
|
||||
`parseLoginResponse mapping=${JSON.stringify(
|
||||
req.providersInfo.mapping
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const user = new UserModel(
|
||||
requestInfo.extract.nameID,
|
||||
requestInfo.extract.sessionIndex.sessionIndex,
|
||||
requestInfo.extract.attributes,
|
||||
req.providersInfo.mapping
|
||||
);
|
||||
|
||||
logger.info(`SSO User ${JSON.stringify(user)}`);
|
||||
|
||||
// Use the parseResult can do customized action
|
||||
const data = coder.encodeData(user);
|
||||
|
||||
if (!data) {
|
||||
logger.error("coder.encodeData", user);
|
||||
return res.redirect(
|
||||
urlResolver.getPortalAuthErrorUrl(
|
||||
req,
|
||||
urlResolver.ErrorMessageKey.SsoError
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return res.redirect(urlResolver.getPortalSsoLoginUrl(req, data));
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`onLoginResponse ${getError(e)}`);
|
||||
return res.redirect(
|
||||
urlResolver.getPortalAuthErrorUrl(
|
||||
req,
|
||||
urlResolver.ErrorMessageKey.SsoAuthFailed
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createLogoutTemplateCallback = (_idp, _sp, user, method) => (
|
||||
template
|
||||
) => {
|
||||
const _target = _idp;
|
||||
const _init = _sp;
|
||||
const metadata = { init: _init.entityMeta, target: _target.entityMeta };
|
||||
const initSetting = _init.entitySetting;
|
||||
const nameIDFormat = initSetting.nameIDFormat;
|
||||
const selectedNameIDFormat = Array.isArray(nameIDFormat)
|
||||
? nameIDFormat[0]
|
||||
: nameIDFormat;
|
||||
const id = initSetting.generateID();
|
||||
const entityEndpoint = metadata.target.getSingleLogoutService(method);
|
||||
|
||||
const data = {
|
||||
ID: id,
|
||||
Destination: entityEndpoint,
|
||||
Issuer: metadata.init.getEntityID(),
|
||||
IssueInstant: new Date().toISOString(),
|
||||
EntityID: metadata.init.getEntityID(),
|
||||
NameQualifier: metadata.target.getEntityID(),
|
||||
NameIDFormat: selectedNameIDFormat,
|
||||
NameID: user.logoutNameID,
|
||||
SessionIndex: user.sessionIndex,
|
||||
};
|
||||
|
||||
const t = template.context || template;
|
||||
|
||||
const rawSamlRequest = libsaml.replaceTagsByValue(t, data);
|
||||
return {
|
||||
id: id,
|
||||
context: rawSamlRequest,
|
||||
};
|
||||
};
|
||||
|
||||
const createLogoutResponseTemplateCallback = (
|
||||
_idp,
|
||||
_sp,
|
||||
method,
|
||||
inResponseTo,
|
||||
relayState,
|
||||
statusCode
|
||||
) => (template) => {
|
||||
const _target = _idp;
|
||||
const _init = _sp;
|
||||
const metadata = { init: _init.entityMeta, target: _target.entityMeta };
|
||||
const initSetting = _init.entitySetting;
|
||||
const id = initSetting.generateID();
|
||||
const entityEndpoint = metadata.target.getSingleLogoutService(method);
|
||||
|
||||
const data = {
|
||||
ID: id,
|
||||
Destination: entityEndpoint,
|
||||
Issuer: metadata.init.getEntityID(),
|
||||
IssueInstant: new Date().toISOString(),
|
||||
InResponseTo: inResponseTo,
|
||||
StatusCode: statusCode,
|
||||
};
|
||||
|
||||
const t = template.context || template;
|
||||
|
||||
const rawSamlResponse = libsaml.replaceTagsByValue(t, data);
|
||||
return {
|
||||
id: id,
|
||||
context: rawSamlResponse,
|
||||
relayState,
|
||||
};
|
||||
};
|
||||
|
||||
const sendLogoutRequest = async (req, res) => {
|
||||
try {
|
||||
if (!verifySetting(req)) {
|
||||
return res.redirect(urlResolver.getPortal500Url(req));
|
||||
}
|
||||
|
||||
const sp = req.providersInfo.sp;
|
||||
const idp = req.providersInfo.idp;
|
||||
|
||||
const isPost =
|
||||
req.providersInfo.settings.IdpSettings.SloBinding ===
|
||||
urn.namespace.binding.post;
|
||||
|
||||
const method = isPost
|
||||
? urn.wording.binding.post
|
||||
: urn.wording.binding.redirect;
|
||||
|
||||
const relayState = urlResolver.getPortalAuthUrl(req);
|
||||
|
||||
const userData = coder.decodeData(req.query["data"]);
|
||||
|
||||
if (!userData) {
|
||||
logger.error(`coder.decodeData ${req.query["data"]}`);
|
||||
return res.redirect(urlResolver.getPortal500Url(req));
|
||||
}
|
||||
|
||||
//const logoutUser = new LogoutModel(userData.NameId, userData.SessionId);
|
||||
const user = {
|
||||
logoutNameID: userData.NameId,
|
||||
sessionIndex: userData.SessionId,
|
||||
};
|
||||
|
||||
const data = sp.createLogoutRequest(
|
||||
idp,
|
||||
method,
|
||||
user,
|
||||
relayState,
|
||||
createLogoutTemplateCallback(idp, sp, user, method)
|
||||
);
|
||||
|
||||
return sendToIDP(res, data, isPost);
|
||||
} catch (e) {
|
||||
logger.error(`sendLogoutRequest ${getError(e)}`);
|
||||
return res.redirect(
|
||||
urlResolver.getPortalAuthErrorUrl(
|
||||
req,
|
||||
urlResolver.ErrorMessageKey.SsoError
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function getRelayState(requestInfo, req) {
|
||||
try {
|
||||
let { relayState } = requestInfo.extract;
|
||||
|
||||
if (!relayState) {
|
||||
return req.body.RelayState || req.query.RelayState || null;
|
||||
}
|
||||
|
||||
return relayState;
|
||||
} catch (e) {
|
||||
logger.error(`getRelayState failed ${getError(e)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionIndex(requestInfo) {
|
||||
try {
|
||||
let { sessionIndex } = requestInfo.extract;
|
||||
|
||||
if (!sessionIndex) return null;
|
||||
|
||||
if (typeof sessionIndex === "object") {
|
||||
sessionIndex = requestInfo.extract.sessionIndex.sessionIndex;
|
||||
}
|
||||
return sessionIndex;
|
||||
} catch (e) {
|
||||
logger.error(`getSessionIndex failed ${getError(e)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getNameId(requestInfo) {
|
||||
try {
|
||||
const { nameID } = requestInfo.extract;
|
||||
|
||||
return nameID;
|
||||
} catch (e) {
|
||||
logger.error(`getNameId failed ${getError(e)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getInResponseTo(requestInfo) {
|
||||
try {
|
||||
const { request } = requestInfo.extract;
|
||||
|
||||
return (request && request.id) || null;
|
||||
} catch (e) {
|
||||
logger.error(`getInResponseTo failed ${getError(e)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const sendPortalLogout = async (user, req) => {
|
||||
try {
|
||||
const data = coder.encodeData(user);
|
||||
|
||||
if (!data) {
|
||||
const errorMessage = `EncodeData is EMPTY`;
|
||||
throw new Error(errorMessage);
|
||||
//return res.redirect(urlResolver.getPortal500Url(req));
|
||||
}
|
||||
|
||||
const url = urlResolver.getPortalSsoLogoutUrl(req, data);
|
||||
|
||||
logger.info(`SEND PORTAL LOGOUT`);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
logger.info(
|
||||
`PORTAL LOGOUT ${
|
||||
response.ok ? "success" : `fail (status=${response.statusText})`
|
||||
}`
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onLogout = async (req, res) => {
|
||||
try {
|
||||
if (!verifySetting(req)) {
|
||||
return res.redirect(urlResolver.getPortal500Url(req));
|
||||
}
|
||||
|
||||
const sp = req.providersInfo.sp;
|
||||
const idp = req.providersInfo.idp;
|
||||
|
||||
let isPost = req.method === "POST";
|
||||
let method = isPost
|
||||
? urn.wording.binding.post
|
||||
: urn.wording.binding.redirect;
|
||||
const isResponse = req.query.SAMLResponse || req.body.SAMLResponse;
|
||||
|
||||
if (isResponse) {
|
||||
const responseInfo = await sp.parseLogoutResponse(idp, method, req);
|
||||
|
||||
if (config.app.logSamlData) {
|
||||
logger.debug(`onLogout->response ${JSON.stringify(responseInfo)}`);
|
||||
}
|
||||
|
||||
return res.redirect(urlResolver.getPortalAuthUrl(req));
|
||||
} else {
|
||||
const requestInfo = await sp.parseLogoutRequest(idp, method, req);
|
||||
|
||||
if (config.app.logSamlData) {
|
||||
logger.debug(`onLogout->request ${JSON.stringify(requestInfo)}`);
|
||||
}
|
||||
|
||||
const nameID = getNameId(requestInfo);
|
||||
const sessionIndex = getSessionIndex(requestInfo);
|
||||
|
||||
const logoutUser = new LogoutModel(nameID, sessionIndex);
|
||||
|
||||
const response = await sendPortalLogout(logoutUser, req);
|
||||
|
||||
const relayState = getRelayState(requestInfo, req);
|
||||
const inResponseTo = getInResponseTo(requestInfo);
|
||||
const statusCode = response.ok
|
||||
? urn.namespace.statusCode.success
|
||||
: urn.namespace.statusCode.partialLogout;
|
||||
|
||||
method = urn.wording.binding.redirect;
|
||||
isPost = false;
|
||||
|
||||
const data = sp.createLogoutResponse(
|
||||
idp,
|
||||
requestInfo,
|
||||
method,
|
||||
relayState,
|
||||
createLogoutResponseTemplateCallback(
|
||||
idp,
|
||||
sp,
|
||||
method,
|
||||
inResponseTo,
|
||||
relayState,
|
||||
statusCode
|
||||
)
|
||||
);
|
||||
|
||||
return sendToIDP(res, data, isPost);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`onLogout ${getError(e)}`);
|
||||
return res.redirect(
|
||||
urlResolver.getPortalAuthErrorUrl(
|
||||
req,
|
||||
urlResolver.ErrorMessageKey.SsoError
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc Route to get Sp metadata
|
||||
* @param {object} req - request
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.get(config.routes.metadata, getSpMetadata);
|
||||
|
||||
/**
|
||||
* @desc Route to send login request from Sp to Idp
|
||||
* @param {object} req - request
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.get(config.routes.login, sendLoginRequest);
|
||||
|
||||
/**
|
||||
* @desc Route to send login request from Sp to Idp
|
||||
* @param {object} req - request
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.post(config.routes.login, sendLoginRequest);
|
||||
|
||||
/**
|
||||
* @desc Route to read login response from Idp to Sp
|
||||
* @param {object} req - request with assertion info
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.get(config.routes.login_callback, onLoginResponse);
|
||||
|
||||
/**
|
||||
* @desc Route to read login response from Idp to Sp
|
||||
* @param {object} req - request with assertion info
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.post(config.routes.login_callback, onLoginResponse);
|
||||
|
||||
/**
|
||||
* @desc Route to send logout request from Sp to Idp
|
||||
* @param {object} req - request with data parameter (NameID required)
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.get(config.routes.logout, sendLogoutRequest);
|
||||
|
||||
/**
|
||||
* @desc Route to read logout response from Idp to Sp
|
||||
* @param {object} req - request with logout info
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.get(config.routes.logout_callback, onLogout);
|
||||
|
||||
/**
|
||||
* @desc Route to read logout response from Idp to Sp
|
||||
* @param {object} req - request with logout info
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.post(config.routes.logout_callback, onLogout);
|
||||
|
||||
/**
|
||||
* @desc Route to generate a certificate
|
||||
*/
|
||||
app.get(config.routes.generatecert, onGenerateCert);
|
||||
|
||||
/**
|
||||
* @desc Route to validate the certificate
|
||||
*/
|
||||
app.post(config.routes.validatecerts, onValidateCerts);
|
||||
|
||||
/**
|
||||
* @desc Route to upload metadata
|
||||
*/
|
||||
app.post(config.routes.uploadmetadata, onUploadMetadata);
|
||||
|
||||
/**
|
||||
* @desc Route to load metadata
|
||||
*/
|
||||
app.post(config.routes.loadmetadata, onLoadMetadata);
|
||||
|
||||
/**
|
||||
* @desc Catch any untracked routes
|
||||
* @param {object} req - request with data parameter (NameID required)
|
||||
* @param {object} res - response
|
||||
*/
|
||||
app.use(function (req, res, next) {
|
||||
next(res.redirect(urlResolver.getPortal404Url(req)));
|
||||
});
|
||||
};
|
18
common/ASC.SsoAuth/app/templates/loginRequestTemplate.xml
Normal file
18
common/ASC.SsoAuth/app/templates/loginRequestTemplate.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<samlp:AuthnRequest
|
||||
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
ID="{ID}"
|
||||
Version="2.0"
|
||||
IssueInstant="{IssueInstant}"
|
||||
Destination="{Destination}"
|
||||
AssertionConsumerServiceURL="{AssertionConsumerServiceURL}">
|
||||
|
||||
<saml:Issuer>{Issuer}</saml:Issuer>
|
||||
<samlp:NameIDPolicy
|
||||
Format="{NameIDFormat}"
|
||||
AllowCreate="{AllowCreate}"/>
|
||||
|
||||
<samlp:RequestedAuthnContext Comparison="exact">
|
||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
|
||||
</samlp:RequestedAuthnContext>
|
||||
</samlp:AuthnRequest>
|
11
common/ASC.SsoAuth/app/templates/logoutRequestTemplate.xml
Normal file
11
common/ASC.SsoAuth/app/templates/logoutRequestTemplate.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<samlp:LogoutRequest
|
||||
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
ID="{ID}"
|
||||
Version="2.0"
|
||||
IssueInstant="{IssueInstant}"
|
||||
Destination="{Destination}">
|
||||
<saml:Issuer>{Issuer}</saml:Issuer>
|
||||
<saml:NameID NameQualifier="{NameQualifier}" SPNameQualifier="{EntityID}" Format="{NameIDFormat}">{NameID}</saml:NameID>
|
||||
<samlp:SessionIndex>{SessionIndex}</samlp:SessionIndex>
|
||||
</samlp:LogoutRequest>
|
11
common/ASC.SsoAuth/app/templates/logoutResponseTemplate.xml
Normal file
11
common/ASC.SsoAuth/app/templates/logoutResponseTemplate.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
Destination="{Destination}"
|
||||
ID="{ID}"
|
||||
InResponseTo="{InResponseTo}"
|
||||
IssueInstant="{IssueInstant}"
|
||||
Version="2.0">
|
||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{Issuer}</saml:Issuer>
|
||||
<samlp:Status>
|
||||
<samlp:StatusCode Value="{StatusCode}" />
|
||||
</samlp:Status>
|
||||
</samlp:LogoutResponse>
|
47
common/ASC.SsoAuth/app/utils/coder.js
Normal file
47
common/ASC.SsoAuth/app/utils/coder.js
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
const config = require("../../config").get(),
|
||||
hash = require("./hash");
|
||||
|
||||
var Coder = function() {
|
||||
return {
|
||||
encodeData: function(data) {
|
||||
if (!data && typeof (data) !== "object")
|
||||
return undefined;
|
||||
|
||||
const jsonStr = JSON.stringify(data);
|
||||
const dataEncoded = hash.encode(jsonStr, config["core.machinekey"] ? config["core.machinekey"] : config.app.machinekey);
|
||||
|
||||
return dataEncoded;
|
||||
},
|
||||
|
||||
decodeData: function(data) {
|
||||
if (!data && typeof (data) !== "string")
|
||||
return undefined;
|
||||
|
||||
const jsonStr = hash.decode(data, config["core.machinekey"] ? config["core.machinekey"] : config.app.machinekey);
|
||||
const dataDecoded = JSON.parse(jsonStr);
|
||||
|
||||
return dataDecoded;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Coder();
|
287
common/ASC.SsoAuth/app/utils/converter.js
Normal file
287
common/ASC.SsoAuth/app/utils/converter.js
Normal file
@ -0,0 +1,287 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
const config = require("../../config").get();
|
||||
const saml = require("samlify");
|
||||
const validator = require("@authenio/samlify-node-xmllint");
|
||||
const urn = require("samlify/build/src/urn");
|
||||
const _ = require("lodash");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const minifyXML = require("minify-xml").minify;
|
||||
|
||||
saml.setSchemaValidator(validator);
|
||||
|
||||
const ServiceProvider = saml.ServiceProvider;
|
||||
const IdentityProvider = saml.IdentityProvider;
|
||||
|
||||
const templDir = path.join(process.cwd(),"../ASC.SsoAuth", "/app/templates/");
|
||||
|
||||
module.exports = function (logger) {
|
||||
function removeCertHead(cert) {
|
||||
var newCert = cert;
|
||||
if (cert && cert[0] === "-") {
|
||||
newCert = cert.replace(/-----.*-----/g, "");
|
||||
}
|
||||
return newCert;
|
||||
}
|
||||
|
||||
function loadTemplate(conf, templName) {
|
||||
try {
|
||||
const tmplPath = path.join(templDir, `${templName}.xml`);
|
||||
|
||||
const template = minifyXML(
|
||||
fs.readFileSync(tmplPath, { encoding: "utf-8" })
|
||||
);
|
||||
|
||||
conf[`${templName}`] = {
|
||||
context: template,
|
||||
};
|
||||
} catch (e) {
|
||||
logger.error(`loadTemplate ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
toIdp: function (ssoConfig) {
|
||||
if (!ssoConfig && typeof ssoConfig !== "string") return undefined;
|
||||
|
||||
const idpSetting = {
|
||||
entityID: ssoConfig.IdpSettings.EntityId,
|
||||
nameIDFormat: [ssoConfig.IdpSettings.NameIdFormat],
|
||||
|
||||
requestSignatureAlgorithm:
|
||||
ssoConfig.IdpCertificateAdvanced.VerifyAlgorithm,
|
||||
dataEncryptionAlgorithm:
|
||||
ssoConfig.IdpCertificateAdvanced.DecryptAlgorithm,
|
||||
|
||||
singleSignOnService: [
|
||||
{
|
||||
Binding: ssoConfig.IdpSettings.SsoBinding,
|
||||
Location: ssoConfig.IdpSettings.SsoUrl,
|
||||
},
|
||||
],
|
||||
singleLogoutService: [
|
||||
{
|
||||
Binding: ssoConfig.IdpSettings.SloBinding,
|
||||
Location: ssoConfig.IdpSettings.SloUrl,
|
||||
},
|
||||
],
|
||||
wantAuthnRequestsSigned:
|
||||
ssoConfig.SpCertificateAdvanced.SignAuthRequests,
|
||||
wantLogoutResponseSigned:
|
||||
ssoConfig.SpCertificateAdvanced.SignLogoutResponses,
|
||||
wantLogoutRequestSigned:
|
||||
ssoConfig.SpCertificateAdvanced.SignLogoutRequests,
|
||||
isAssertionEncrypted: ssoConfig.SpCertificateAdvanced.EncryptAssertions,
|
||||
};
|
||||
|
||||
if (
|
||||
Array.isArray(ssoConfig.IdpCertificates) &&
|
||||
ssoConfig.IdpCertificates.length > 0
|
||||
) {
|
||||
idpSetting.signingCert = removeCertHead(
|
||||
_.result(
|
||||
_.find(ssoConfig.IdpCertificates, function (obj) {
|
||||
return (
|
||||
obj.Action === "verification" ||
|
||||
obj.Action === "verification and decrypt"
|
||||
);
|
||||
}),
|
||||
"Crt"
|
||||
)
|
||||
);
|
||||
|
||||
idpSetting.encryptCert = removeCertHead(
|
||||
_.result(
|
||||
_.find(ssoConfig.IdpCertificates, function (obj) {
|
||||
return (
|
||||
obj.Action === "decrypt" ||
|
||||
obj.Action === "verification and decrypt"
|
||||
);
|
||||
}),
|
||||
"Crt"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const idp = new IdentityProvider(idpSetting);
|
||||
|
||||
return idp;
|
||||
},
|
||||
|
||||
toSp: function (ssoConfig, baseUrl) {
|
||||
if (!ssoConfig && typeof ssoConfig !== "string")
|
||||
throw "Invalid ssoConfig";
|
||||
|
||||
const metaUrl = baseUrl + "/sso" + config.routes.metadata,
|
||||
acsUrl = baseUrl + "/sso" + config.routes.login_callback,
|
||||
sloUrl = baseUrl + "/sso" + config.routes.logout_callback;
|
||||
|
||||
const spSetting = {
|
||||
entityID: metaUrl,
|
||||
|
||||
nameIDFormat: [ssoConfig.IdpSettings.NameIdFormat],
|
||||
|
||||
requestSignatureAlgorithm:
|
||||
ssoConfig.SpCertificateAdvanced.SigningAlgorithm,
|
||||
dataEncryptionAlgorithm:
|
||||
ssoConfig.SpCertificateAdvanced.EncryptAlgorithm,
|
||||
|
||||
assertionConsumerService: [
|
||||
{
|
||||
Binding: urn.namespace.binding.post,
|
||||
Location: acsUrl,
|
||||
},
|
||||
{
|
||||
Binding: urn.namespace.binding.redirect,
|
||||
Location: acsUrl,
|
||||
},
|
||||
],
|
||||
|
||||
singleLogoutService: [
|
||||
{
|
||||
Binding: urn.namespace.binding.post,
|
||||
Location: sloUrl,
|
||||
},
|
||||
{
|
||||
Binding: urn.namespace.binding.redirect,
|
||||
Location: sloUrl,
|
||||
},
|
||||
],
|
||||
|
||||
/*requestedAttributes: [
|
||||
{
|
||||
FriendlyName: "mail",
|
||||
Name: "urn:oid:0.9.2342.19200300.100.1.3",
|
||||
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
FriendlyName: "givenName",
|
||||
Name: "urn:oid:2.5.4.42",
|
||||
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
FriendlyName: "sn",
|
||||
Name: "urn:oid:2.5.4.4",
|
||||
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
FriendlyName: "mobile",
|
||||
Name: "urn:oid:0.9.2342.19200300.100.1.41",
|
||||
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
FriendlyName: "title",
|
||||
Name: "urn:oid:2.5.4.12",
|
||||
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
},
|
||||
{
|
||||
FriendlyName: "l",
|
||||
Name: "urn:oid:2.5.4.7",
|
||||
NameFormat: "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
|
||||
}
|
||||
],*/
|
||||
authnRequestsSigned: ssoConfig.SpCertificateAdvanced.SignAuthRequests,
|
||||
wantAssertionsSigned:
|
||||
ssoConfig.IdpCertificateAdvanced.VerifyAuthResponsesSign,
|
||||
wantLogoutResponseSigned:
|
||||
ssoConfig.IdpCertificateAdvanced.VerifyLogoutResponsesSign,
|
||||
wantLogoutRequestSigned:
|
||||
ssoConfig.IdpCertificateAdvanced.VerifyLogoutRequestsSign,
|
||||
elementsOrder: [
|
||||
"KeyDescriptor",
|
||||
"SingleLogoutService",
|
||||
"NameIDFormat",
|
||||
"AssertionConsumerService",
|
||||
"AttributeConsumingService",
|
||||
],
|
||||
//clockDrifts: [-60000, 60000],
|
||||
};
|
||||
|
||||
if (
|
||||
Array.isArray(ssoConfig.SpCertificates) &&
|
||||
ssoConfig.SpCertificates.length > 0
|
||||
) {
|
||||
spSetting.privateKey = _.result(
|
||||
_.find(ssoConfig.SpCertificates, function (obj) {
|
||||
return (
|
||||
obj.Action === "signing" || obj.Action === "signing and encrypt"
|
||||
);
|
||||
}),
|
||||
"Key"
|
||||
);
|
||||
|
||||
spSetting.privateKeyPass = "";
|
||||
|
||||
spSetting.encPrivateKey = _.result(
|
||||
_.find(ssoConfig.SpCertificates, function (obj) {
|
||||
return (
|
||||
obj.Action === "encrypt" || obj.Action === "signing and encrypt"
|
||||
);
|
||||
}),
|
||||
"Key"
|
||||
);
|
||||
|
||||
spSetting.encPrivateKeyPass = "";
|
||||
|
||||
spSetting.signingCert = _.result(
|
||||
_.find(ssoConfig.SpCertificates, function (obj) {
|
||||
return (
|
||||
obj.Action === "signing" || obj.Action === "signing and encrypt"
|
||||
);
|
||||
}),
|
||||
"Crt"
|
||||
);
|
||||
spSetting.encryptCert = _.result(
|
||||
_.find(ssoConfig.SpCertificates, function (obj) {
|
||||
return (
|
||||
obj.Action === "encrypt" || obj.Action === "signing and encrypt"
|
||||
);
|
||||
}),
|
||||
"Crt"
|
||||
);
|
||||
|
||||
// must have if assertion signature fails validation
|
||||
spSetting.transformationAlgorithms = [
|
||||
"http://www.w3.org/2000/09/xmldsig#enveloped-signature",
|
||||
"http://www.w3.org/2001/10/xml-exc-c14n#",
|
||||
];
|
||||
}
|
||||
|
||||
loadTemplate(spSetting, "loginRequestTemplate");
|
||||
loadTemplate(spSetting, "logoutRequestTemplate");
|
||||
loadTemplate(spSetting, "logoutResponseTemplate");
|
||||
|
||||
//if (config.app.organization) {
|
||||
// spSetting.organization = config.app.organization;
|
||||
//}
|
||||
|
||||
//if (config.app.contact) {
|
||||
// spSetting.contact = config.app.contact;
|
||||
//}
|
||||
|
||||
const sp = new ServiceProvider(spSetting);
|
||||
|
||||
return sp;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
//module.exports = Converter(logger);
|
84
common/ASC.SsoAuth/app/utils/hash.js
Normal file
84
common/ASC.SsoAuth/app/utils/hash.js
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
var Hash = function () {
|
||||
var getHashBase64 = function (str) {
|
||||
const crypto = require("crypto");
|
||||
const sha256 = crypto.createHash("sha256");
|
||||
sha256.update(str, "utf8");
|
||||
const result = sha256.digest("base64");
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
encode: function(str, secret) {
|
||||
try {
|
||||
const strHash = getHashBase64(str + secret) + "?" + str;
|
||||
|
||||
let data = new Buffer(strHash).toString("base64");
|
||||
|
||||
let cnt = 0;
|
||||
while (data.indexOf("=") !== -1) {
|
||||
cnt++;
|
||||
data = data.replace("=", "");
|
||||
}
|
||||
|
||||
data = (data + cnt).replace(/\+/g, "-").replace(/\//g, "_");
|
||||
|
||||
return data;
|
||||
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
decode: function(str, secret) {
|
||||
try {
|
||||
|
||||
let strDecoded = Buffer.from(unescape(str), "base64").toString();
|
||||
|
||||
const lastIndex = strDecoded.lastIndexOf("}");
|
||||
|
||||
if (lastIndex + 1 < strDecoded.length) {
|
||||
strDecoded = strDecoded.substring(0, lastIndex + 1);
|
||||
}
|
||||
|
||||
const index = strDecoded.indexOf("?");
|
||||
|
||||
if (index > 0 && strDecoded[index + 1] == '{') {
|
||||
let hash = strDecoded.substring(0, index);
|
||||
let data = strDecoded.substring(index + 1);
|
||||
if(getHashBase64(data + secret) === hash)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// Sig incorrect
|
||||
return null;
|
||||
|
||||
} catch (ex) {
|
||||
console.error("hash.decode", str, secret, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Hash();
|
||||
|
107
common/ASC.SsoAuth/app/utils/resolver.js
Normal file
107
common/ASC.SsoAuth/app/utils/resolver.js
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
"use strict";
|
||||
|
||||
const config = require("../../config").get(),
|
||||
// ReSharper disable once InconsistentNaming
|
||||
URL = require("url");
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
module.exports = function (logger) {
|
||||
|
||||
function getBaseUrl(req) {
|
||||
const url = req.headers["x-rewriter-url"] || req.protocol + "://" + req.get("host");
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortalSsoHandlerUrl(req) {
|
||||
const url = getBaseUrl(req) + config.app.portal.ssoUrl;
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortalSsoConfigUrl(req) {
|
||||
const url = getPortalSsoHandlerUrl(req) +
|
||||
"?config=saml";
|
||||
logger.debug("getPortalSsoConfigUrl: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortalSsoLoginUrl(req, data) {
|
||||
const url = getPortalSsoHandlerUrl(req) + "?auth=true&data=" + data;
|
||||
logger.debug("getPortalSsoLoginUrl: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortalSsoLogoutUrl(req, data) {
|
||||
const url = getPortalSsoHandlerUrl(req) + "?logout=true&data=" + data;
|
||||
logger.debug("getPortalSsoLogoutUrl: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortalAuthUrl(req) {
|
||||
const url = getBaseUrl(req) + config.app.portal.authUrl;
|
||||
logger.debug("getPortalAuthUrl: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
const ErrorMessageKey = {
|
||||
SsoError: 17,
|
||||
SsoAuthFailed: 18,
|
||||
SsoAttributesNotFound: 19,
|
||||
};
|
||||
|
||||
function getPortalAuthErrorUrl(req, errorKey) {
|
||||
const url = getPortalAuthUrl(req) + "?am=" + errorKey;
|
||||
logger.debug("getPortalAuthErrorUrl: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortalErrorUrl(req) {
|
||||
const url = getBaseUrl(req) + "/500.aspx";
|
||||
logger.debug("getPortal500Url: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function getPortal404Url(req) {
|
||||
const url = getBaseUrl(req) + "/404.aspx";
|
||||
logger.debug("getPortal404Url: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
return {
|
||||
getBaseUrl: getBaseUrl,
|
||||
|
||||
getPortalSsoHandlerUrl: getPortalSsoHandlerUrl,
|
||||
|
||||
getPortalSsoConfigUrl: getPortalSsoConfigUrl,
|
||||
|
||||
getPortalSsoLoginUrl: getPortalSsoLoginUrl,
|
||||
|
||||
getPortalSsoLogoutUrl: getPortalSsoLogoutUrl,
|
||||
|
||||
getPortalAuthUrl: getPortalAuthUrl,
|
||||
|
||||
ErrorMessageKey: ErrorMessageKey,
|
||||
|
||||
getPortalAuthErrorUrl: getPortalAuthErrorUrl,
|
||||
|
||||
getPortal500Url: getPortalErrorUrl,
|
||||
|
||||
getPortal404Url: getPortal404Url
|
||||
};
|
||||
};
|
66
common/ASC.SsoAuth/config/config.json
Normal file
66
common/ASC.SsoAuth/config/config.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Onlyoffice Single Sign-On handler",
|
||||
"port": 9834,
|
||||
"machinekey": "1123askdasjklasbnd",
|
||||
"logDir": "./logs",
|
||||
"logName": "web.sso.%DATE%.log",
|
||||
"contact": {
|
||||
"type": "support",
|
||||
"givenName": "Support",
|
||||
"emailAddress": "support@onlyoffice.com"
|
||||
},
|
||||
"organization": {
|
||||
"name": "Ascensio System SIA",
|
||||
"displayName": "Onlyoffice",
|
||||
"url": "http://www.onlyoffice.com",
|
||||
"lang": "en-US"
|
||||
},
|
||||
"logSamlData": false,
|
||||
"portal": {
|
||||
"baseUrl": "http://localhost",
|
||||
"port": 80,
|
||||
"ssoUrl": "/ssologin.ashx",
|
||||
"authUrl": "/Auth.aspx"
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"login": "/login",
|
||||
"login_callback": "/acs",
|
||||
"logout": "/slo",
|
||||
"logout_callback": "/slo/callback",
|
||||
"metadata": "/metadata",
|
||||
"generatecert": "/generatecert",
|
||||
"validatecerts": "/validatecerts",
|
||||
"uploadmetadata": "/uploadmetadata",
|
||||
"loadmetadata": "/loadmetadata"
|
||||
},
|
||||
"logger": {
|
||||
"file": {
|
||||
"level": "debug",
|
||||
"handleExceptions": true,
|
||||
"humanReadableUnhandledException": true,
|
||||
"json": false,
|
||||
"datePattern": "MM-DD",
|
||||
"zippedArchive": true,
|
||||
"maxSize": "50m",
|
||||
"maxFiles": "30d"
|
||||
},
|
||||
"console": {
|
||||
"level": "debug",
|
||||
"handleExceptions": true,
|
||||
"humanReadableUnhandledException": true,
|
||||
"json": false,
|
||||
"colorize": true
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"portal": "",
|
||||
"apiSystem": "",
|
||||
"appPath": "",
|
||||
"data": "../../Data",
|
||||
"image-path": "images",
|
||||
"rebranding": "rebranding",
|
||||
"https":"certs"
|
||||
}
|
||||
}
|
30
common/ASC.SsoAuth/config/index.js
Normal file
30
common/ASC.SsoAuth/config/index.js
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2021
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
const nconf = require("nconf"),
|
||||
path = require("path"),
|
||||
fs = require("fs");
|
||||
|
||||
nconf.argv()
|
||||
.env()
|
||||
.file({ file: path.join(__dirname, "config.json") });
|
||||
|
||||
if (nconf.get("NODE_ENV") !== "development" && fs.existsSync(path.join(__dirname, nconf.get("NODE_ENV") + ".json"))) {
|
||||
nconf.file({ file: path.join(__dirname, nconf.get("NODE_ENV") + ".json") });
|
||||
}
|
||||
|
||||
module.exports = nconf;
|
41
common/ASC.SsoAuth/config/metadata/idp-onelogin_metadata.xml
Normal file
41
common/ASC.SsoAuth/config/metadata/idp-onelogin_metadata.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0"?>
|
||||
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://app.onelogin.com/saml/metadata/618830">
|
||||
<IDPSSODescriptor xmlns:ds="http://www.w3.org/2000/09/xmldsig#" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIELDCCAxSgAwIBAgIUGigRLXYAdwm+ZudFXLdn+ghLsq4wDQYJKoZIhvcNAQEF
|
||||
BQAwXzELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD21vbm8ubWFpbC40dGVzdDEVMBMG
|
||||
A1UECwwMT25lTG9naW4gSWRQMR8wHQYDVQQDDBZPbmVMb2dpbiBBY2NvdW50IDk3
|
||||
ODA1MB4XDTE3MDExNzEzNDY0OFoXDTIyMDExODEzNDY0OFowXzELMAkGA1UEBhMC
|
||||
VVMxGDAWBgNVBAoMD21vbm8ubWFpbC40dGVzdDEVMBMGA1UECwwMT25lTG9naW4g
|
||||
SWRQMR8wHQYDVQQDDBZPbmVMb2dpbiBBY2NvdW50IDk3ODA1MIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwPaJbyVfqO5v7qzgafBZs5vl8vkuSeZAbG6W
|
||||
jLt9KIQRFbS/EseFo4t8M966SSe4CE2hMlNOLqIhPWcfKsR4E4fTu88xGdtp9ifM
|
||||
9FUZFKqcNs68U7dw0mpJBWDroU+RywYFbFFe7NV42enf1tIpxS+e+QwlkJBq3drr
|
||||
UNfL8+Anz8YxRXsQkkIe38JsosHQtQtalZlanCYIjMTEdBdJifsZzsmxH7gXSPp4
|
||||
PYe77OUKCrXf4JL0HNiqxziOvgMIpLxJtiflL0UD96iNNMWaxc0vKlHLVIfeHr1E
|
||||
ebsZZJF03RMpPqkOw1NZBOde/Y/aoy70ml10UofprMMkfWURwQIDAQABo4HfMIHc
|
||||
MAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFZ7J0T5bU+9t0gmePSU99amDCLGMIGc
|
||||
BgNVHSMEgZQwgZGAFFZ7J0T5bU+9t0gmePSU99amDCLGoWOkYTBfMQswCQYDVQQG
|
||||
EwJVUzEYMBYGA1UECgwPbW9uby5tYWlsLjR0ZXN0MRUwEwYDVQQLDAxPbmVMb2dp
|
||||
biBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgOTc4MDWCFBooES12AHcJ
|
||||
vmbnRVy3Z/oIS7KuMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEA
|
||||
QF6113kC/N8CTob3sk2ThJs4OBbyE4uXEHPB4SwpIXL+bBkmzZFsMj5EyF+SWxKm
|
||||
Vi7RGmwk7mWKAP/0msLseyviFq/gl7tyHjTwS1+xF4ztew5+06hWOtTqsQHA+zM/
|
||||
T61plUwEdbgsfros/GkVdeO2lPCPLl6pR30vsNvLfO6AtmS7QdJtVJfHYninsUcz
|
||||
fXS7taycS36JTkHLTReM2fuIt67Mre45k0ybE1rH+z2wya27oaHod6BXkXOJmzTZ
|
||||
bGqV6v9SEQizQpdKBD1uD9joP1sj9BKFAIi3lv/NIwHZoQX0wZ3nXOFA1a1kMw2g
|
||||
Bu9f0uNPTu9ByFul/SYqTA==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</KeyDescriptor>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://mono-mail-4test.onelogin.com/trust/saml2/http-redirect/slo/618830"/>
|
||||
|
||||
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
|
||||
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://mono-mail-4test.onelogin.com/trust/saml2/http-redirect/sso/618830"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://mono-mail-4test.onelogin.com/trust/saml2/http-post/sso/618830"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://mono-mail-4test.onelogin.com/trust/saml2/soap/sso/618830"/>
|
||||
</IDPSSODescriptor>
|
||||
</EntityDescriptor>
|
214
common/ASC.SsoAuth/config/metadata/idp-shibboleth-metadata.xml
Normal file
214
common/ASC.SsoAuth/config/metadata/idp-shibboleth-metadata.xml
Normal file
@ -0,0 +1,214 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
This is example metadata only. Do *NOT* supply it as is without review,
|
||||
and do *NOT* provide it in real time to your partners.
|
||||
|
||||
This metadata is not dynamic - it will not change as your configuration changes.
|
||||
-->
|
||||
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" entityID="https://idptestbed/idp/shibboleth">
|
||||
|
||||
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
|
||||
|
||||
<Extensions>
|
||||
<shibmd:Scope regexp="false">example.org</shibmd:Scope>
|
||||
<!--
|
||||
Fill in the details for your IdP here
|
||||
|
||||
<mdui:UIInfo>
|
||||
<mdui:DisplayName xml:lang="en">A Name for the IdP at idptestbed</mdui:DisplayName>
|
||||
<mdui:Description xml:lang="en">Enter a description of your IdP at idptestbed</mdui:Description>
|
||||
<mdui:Logo height="80" width="80">https://idptestbed/Path/To/Logo.png</mdui:Logo>
|
||||
</mdui:UIInfo>
|
||||
-->
|
||||
</Extensions>
|
||||
|
||||
<KeyDescriptor use="signing">
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDEzCCAfugAwIBAgIUS9SuTXwsFVVG+LjOEAbLqqT/el0wDQYJKoZIhvcNAQEL
|
||||
BQAwFTETMBEGA1UEAwwKaWRwdGVzdGJlZDAeFw0xNTEyMTEwMjIwMjZaFw0zNTEy
|
||||
MTEwMjIwMjZaMBUxEzARBgNVBAMMCmlkcHRlc3RiZWQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCMAoDHx8xCIfv/6QKqt9mcHYmEJ8y2dKprUbpdcOjH
|
||||
YvNPIl/lHPsUyrb+Nc+q2CDeiWjVk1mWYq0UpIwpBMuw1H6+oOqr4VQRi65pin0M
|
||||
SfE0MWIaFo5FPvpvoptkHD4gvREbm4swyXGMczcMRfqgalFXhUD2wz8W3XAM5Cq2
|
||||
03XeJbj6TwjvKatG5XPdeUe2FBGuOO2q54L1hcIGnLMCQrg7D31lR13PJbjnJ0No
|
||||
5C3k8TPuny6vJsBC03GNLNKfmrKVTdzr3VKp1uay1G3DL9314fgmbl8HA5iRQmy+
|
||||
XInUU6/8NXZSF59p3ITAOvZQeZsbJjg5gGDip5OZo9YlAgMBAAGjWzBZMB0GA1Ud
|
||||
DgQWBBRPlM4VkKZ0U4ec9GrIhFQl0hNbLDA4BgNVHREEMTAvggppZHB0ZXN0YmVk
|
||||
hiFodHRwczovL2lkcHRlc3RiZWQvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQEL
|
||||
BQADggEBAIZ0a1ov3my3ljJG588I/PHx+TxAWONWmpKbO9c/qI3Drxk4oRIffiac
|
||||
ANxdvtabgIzrlk5gMMisD7oyqHJiWgKv5Bgctd8w3IS3lLl7wHX65mTKQRXniG98
|
||||
NIjkvfrhe2eeJxecOqnDI8GOhIGCIqZUn8ShdM/yHjhQ2Mh0Hj3U0LlKvnmfGSQl
|
||||
j0viGwbFCaNaIP3zc5UmCrdE5h8sWL3Fu7ILKM9RyFa2ILHrJScV9t623IcHffHP
|
||||
IeaY/WtuapsrqRFxuQL9QFWN0FsRIdLmjTq+00+B/XnnKRKFBuWfjhHLF/uu8f+E
|
||||
t6Lf23Kb8yD6ZR7dihMZAGHnYQ/hlhM=
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
|
||||
</KeyDescriptor>
|
||||
<KeyDescriptor use="signing">
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDFDCCAfygAwIBAgIVAN3vv+b7KN5Se9m1RZsCllp/B/hdMA0GCSqGSIb3DQEB
|
||||
CwUAMBUxEzARBgNVBAMMCmlkcHRlc3RiZWQwHhcNMTUxMjExMDIyMDE0WhcNMzUx
|
||||
MjExMDIyMDE0WjAVMRMwEQYDVQQDDAppZHB0ZXN0YmVkMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAh91caeY0Q85uhaUyqFwP2bMjwMFxMzRlAoqBHd7g
|
||||
u6eo4duaeLz1BaoR2XTBpNNvFR5oHH+TkKahVDGeH5+kcnIpxI8JPdsZml1srvf2
|
||||
Z6dzJsulJZUdpqnngycTkGtZgEoC1vmYVky2BSAIIifmdh6s0epbHnMGLsHzMKfJ
|
||||
Cb/Q6dYzRWTCPtzE2VMuQqqWgeyMr7u14x/Vqr9RPEFsgY8GIu5jzB6AyUIwrLg+
|
||||
MNkv6aIdcHwxYTGL7ijfy6rSWrgBflQoYRYNEnseK0ZHgJahz4ovCag6wZAoPpBs
|
||||
uYlY7lEr89Ucb6NHx3uqGMsXlDFdE4QwfDLLhCYHPvJ0uwIDAQABo1swWTAdBgNV
|
||||
HQ4EFgQUAkOgED3iYdmvQEOMm6u/JmD/UTQwOAYDVR0RBDEwL4IKaWRwdGVzdGJl
|
||||
ZIYhaHR0cHM6Ly9pZHB0ZXN0YmVkL2lkcC9zaGliYm9sZXRoMA0GCSqGSIb3DQEB
|
||||
CwUAA4IBAQBIdd4YWlnvJjql8+zKKgmWgIY7U8DA8e6QcbAf8f8cdE33RSnjI63X
|
||||
sv/y9GfmbAVAD6RIAXPFFeRYJ08GOxGI9axfNaKdlsklJ9bk4ducHqgCSWYVer3s
|
||||
RQBjxyOfSTvk9YCJvdJVQRJLcCvxwKakFCsOSnV3t9OvN86Ak+fKPVB5j2fM/0fZ
|
||||
Kqjn3iqgdNPTLXPsuJLJO5lITRiBa4onmVelAiCstI9PQiaEck+oAHnMTnC9JE/B
|
||||
DHv3e4rwq3LznlqPw0GSd7xqNTdMDwNOWjkuOr3sGpWS8ms/ZHHXV1Vd22uPe70i
|
||||
s00xrv14zLifcc8oj5DYzOhYRifRXgHX
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
|
||||
</KeyDescriptor>
|
||||
<KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDEzCCAfugAwIBAgIUG6Nn1rlERS1vsi88tcdzSYX0oqAwDQYJKoZIhvcNAQEL
|
||||
BQAwFTETMBEGA1UEAwwKaWRwdGVzdGJlZDAeFw0xNTEyMTEwMjIwMTRaFw0zNTEy
|
||||
MTEwMjIwMTRaMBUxEzARBgNVBAMMCmlkcHRlc3RiZWQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCBXv0o3fmT8iluyLjJ4lBAVCW+ZRVyEXPYQuRi7vfD
|
||||
cO4a6d1kxiJLsaK0W88VNxjFQRr8PgDkWr28vwoH1rgk4pLsszLD48DBzD942peJ
|
||||
l/S6FnsIJjmaHcBh4pbNhU4yowu63iKkvttrcZAEbpEro6Z8CziWEx8sywoaYEQG
|
||||
ifPkr9ORV6Cn3txq+9gMBePG41GrtZrUGIu+xrndL0Shh4Pq0eq/9MAsVlIIXEa8
|
||||
9WfH8J2kFcTOfoWtIc70b7TLZQsx4YnNcnrGLSUEcstFyPLX+Xtv5SNZF89OOIxX
|
||||
VNjNvgE5DbJb9hMM4UAFqI+1bo9QqtxwThjc/sOvIxzNAgMBAAGjWzBZMB0GA1Ud
|
||||
DgQWBBStTyogRPuAVG6q7yPyav1uvE+7pTA4BgNVHREEMTAvggppZHB0ZXN0YmVk
|
||||
hiFodHRwczovL2lkcHRlc3RiZWQvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQEL
|
||||
BQADggEBAFMfoOv+oISGjvamq7+Y4G7ep5vxlAPeK3RATYPYvAmyH946qZXh98ni
|
||||
QXyuqZW5P5eEt86toY45IwDU5r09SKwHughEe99iiEkxh0mb2qo84qX9/qcg+kyN
|
||||
jeLd/OSyolpUCEFNwOFcog7pj7Eer+6AHbwTn1Mjb5TBsKwtDMJsaxPvdj0u7M5r
|
||||
xL/wHkFhn1rCo2QiojzjSlV3yLTh49iTyhE3cG+RxaNKDCxhp0jSSLX1BW/ZoPA8
|
||||
+PMJEA+Q0QbyRD8aJOHN5O8jGxCa/ZzcOnYVL6AsEXoDiY3vAUYh1FUonOWw0m9H
|
||||
p+tGUbGS2l873J5PrsbpeKEVR/IIoKo=
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
|
||||
</KeyDescriptor>
|
||||
|
||||
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://idptestbed:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
|
||||
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idptestbed:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2"/>
|
||||
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idptestbed/idp/profile/SAML2/Redirect/SLO"/>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idptestbed/idp/profile/SAML2/POST/SLO"/>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://idptestbed/idp/profile/SAML2/POST-SimpleSign/SLO"/>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idptestbed:8443/idp/profile/SAML2/SOAP/SLO"/>
|
||||
|
||||
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
|
||||
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
|
||||
|
||||
<SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://idptestbed/idp/profile/Shibboleth/SSO"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idptestbed/idp/profile/SAML2/POST/SSO"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://idptestbed/idp/profile/SAML2/POST-SimpleSign/SSO"/>
|
||||
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idptestbed/idp/profile/SAML2/Redirect/SSO"/>
|
||||
|
||||
</IDPSSODescriptor>
|
||||
|
||||
|
||||
<AttributeAuthorityDescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol">
|
||||
|
||||
<Extensions>
|
||||
<shibmd:Scope regexp="false">example.org</shibmd:Scope>
|
||||
</Extensions>
|
||||
|
||||
<KeyDescriptor use="signing">
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDEzCCAfugAwIBAgIUS9SuTXwsFVVG+LjOEAbLqqT/el0wDQYJKoZIhvcNAQEL
|
||||
BQAwFTETMBEGA1UEAwwKaWRwdGVzdGJlZDAeFw0xNTEyMTEwMjIwMjZaFw0zNTEy
|
||||
MTEwMjIwMjZaMBUxEzARBgNVBAMMCmlkcHRlc3RiZWQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCMAoDHx8xCIfv/6QKqt9mcHYmEJ8y2dKprUbpdcOjH
|
||||
YvNPIl/lHPsUyrb+Nc+q2CDeiWjVk1mWYq0UpIwpBMuw1H6+oOqr4VQRi65pin0M
|
||||
SfE0MWIaFo5FPvpvoptkHD4gvREbm4swyXGMczcMRfqgalFXhUD2wz8W3XAM5Cq2
|
||||
03XeJbj6TwjvKatG5XPdeUe2FBGuOO2q54L1hcIGnLMCQrg7D31lR13PJbjnJ0No
|
||||
5C3k8TPuny6vJsBC03GNLNKfmrKVTdzr3VKp1uay1G3DL9314fgmbl8HA5iRQmy+
|
||||
XInUU6/8NXZSF59p3ITAOvZQeZsbJjg5gGDip5OZo9YlAgMBAAGjWzBZMB0GA1Ud
|
||||
DgQWBBRPlM4VkKZ0U4ec9GrIhFQl0hNbLDA4BgNVHREEMTAvggppZHB0ZXN0YmVk
|
||||
hiFodHRwczovL2lkcHRlc3RiZWQvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQEL
|
||||
BQADggEBAIZ0a1ov3my3ljJG588I/PHx+TxAWONWmpKbO9c/qI3Drxk4oRIffiac
|
||||
ANxdvtabgIzrlk5gMMisD7oyqHJiWgKv5Bgctd8w3IS3lLl7wHX65mTKQRXniG98
|
||||
NIjkvfrhe2eeJxecOqnDI8GOhIGCIqZUn8ShdM/yHjhQ2Mh0Hj3U0LlKvnmfGSQl
|
||||
j0viGwbFCaNaIP3zc5UmCrdE5h8sWL3Fu7ILKM9RyFa2ILHrJScV9t623IcHffHP
|
||||
IeaY/WtuapsrqRFxuQL9QFWN0FsRIdLmjTq+00+B/XnnKRKFBuWfjhHLF/uu8f+E
|
||||
t6Lf23Kb8yD6ZR7dihMZAGHnYQ/hlhM=
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
|
||||
</KeyDescriptor>
|
||||
<KeyDescriptor use="signing">
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDFDCCAfygAwIBAgIVAN3vv+b7KN5Se9m1RZsCllp/B/hdMA0GCSqGSIb3DQEB
|
||||
CwUAMBUxEzARBgNVBAMMCmlkcHRlc3RiZWQwHhcNMTUxMjExMDIyMDE0WhcNMzUx
|
||||
MjExMDIyMDE0WjAVMRMwEQYDVQQDDAppZHB0ZXN0YmVkMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAh91caeY0Q85uhaUyqFwP2bMjwMFxMzRlAoqBHd7g
|
||||
u6eo4duaeLz1BaoR2XTBpNNvFR5oHH+TkKahVDGeH5+kcnIpxI8JPdsZml1srvf2
|
||||
Z6dzJsulJZUdpqnngycTkGtZgEoC1vmYVky2BSAIIifmdh6s0epbHnMGLsHzMKfJ
|
||||
Cb/Q6dYzRWTCPtzE2VMuQqqWgeyMr7u14x/Vqr9RPEFsgY8GIu5jzB6AyUIwrLg+
|
||||
MNkv6aIdcHwxYTGL7ijfy6rSWrgBflQoYRYNEnseK0ZHgJahz4ovCag6wZAoPpBs
|
||||
uYlY7lEr89Ucb6NHx3uqGMsXlDFdE4QwfDLLhCYHPvJ0uwIDAQABo1swWTAdBgNV
|
||||
HQ4EFgQUAkOgED3iYdmvQEOMm6u/JmD/UTQwOAYDVR0RBDEwL4IKaWRwdGVzdGJl
|
||||
ZIYhaHR0cHM6Ly9pZHB0ZXN0YmVkL2lkcC9zaGliYm9sZXRoMA0GCSqGSIb3DQEB
|
||||
CwUAA4IBAQBIdd4YWlnvJjql8+zKKgmWgIY7U8DA8e6QcbAf8f8cdE33RSnjI63X
|
||||
sv/y9GfmbAVAD6RIAXPFFeRYJ08GOxGI9axfNaKdlsklJ9bk4ducHqgCSWYVer3s
|
||||
RQBjxyOfSTvk9YCJvdJVQRJLcCvxwKakFCsOSnV3t9OvN86Ak+fKPVB5j2fM/0fZ
|
||||
Kqjn3iqgdNPTLXPsuJLJO5lITRiBa4onmVelAiCstI9PQiaEck+oAHnMTnC9JE/B
|
||||
DHv3e4rwq3LznlqPw0GSd7xqNTdMDwNOWjkuOr3sGpWS8ms/ZHHXV1Vd22uPe70i
|
||||
s00xrv14zLifcc8oj5DYzOhYRifRXgHX
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
|
||||
</KeyDescriptor>
|
||||
<KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo>
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>
|
||||
MIIDEzCCAfugAwIBAgIUG6Nn1rlERS1vsi88tcdzSYX0oqAwDQYJKoZIhvcNAQEL
|
||||
BQAwFTETMBEGA1UEAwwKaWRwdGVzdGJlZDAeFw0xNTEyMTEwMjIwMTRaFw0zNTEy
|
||||
MTEwMjIwMTRaMBUxEzARBgNVBAMMCmlkcHRlc3RiZWQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCBXv0o3fmT8iluyLjJ4lBAVCW+ZRVyEXPYQuRi7vfD
|
||||
cO4a6d1kxiJLsaK0W88VNxjFQRr8PgDkWr28vwoH1rgk4pLsszLD48DBzD942peJ
|
||||
l/S6FnsIJjmaHcBh4pbNhU4yowu63iKkvttrcZAEbpEro6Z8CziWEx8sywoaYEQG
|
||||
ifPkr9ORV6Cn3txq+9gMBePG41GrtZrUGIu+xrndL0Shh4Pq0eq/9MAsVlIIXEa8
|
||||
9WfH8J2kFcTOfoWtIc70b7TLZQsx4YnNcnrGLSUEcstFyPLX+Xtv5SNZF89OOIxX
|
||||
VNjNvgE5DbJb9hMM4UAFqI+1bo9QqtxwThjc/sOvIxzNAgMBAAGjWzBZMB0GA1Ud
|
||||
DgQWBBStTyogRPuAVG6q7yPyav1uvE+7pTA4BgNVHREEMTAvggppZHB0ZXN0YmVk
|
||||
hiFodHRwczovL2lkcHRlc3RiZWQvaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQEL
|
||||
BQADggEBAFMfoOv+oISGjvamq7+Y4G7ep5vxlAPeK3RATYPYvAmyH946qZXh98ni
|
||||
QXyuqZW5P5eEt86toY45IwDU5r09SKwHughEe99iiEkxh0mb2qo84qX9/qcg+kyN
|
||||
jeLd/OSyolpUCEFNwOFcog7pj7Eer+6AHbwTn1Mjb5TBsKwtDMJsaxPvdj0u7M5r
|
||||
xL/wHkFhn1rCo2QiojzjSlV3yLTh49iTyhE3cG+RxaNKDCxhp0jSSLX1BW/ZoPA8
|
||||
+PMJEA+Q0QbyRD8aJOHN5O8jGxCa/ZzcOnYVL6AsEXoDiY3vAUYh1FUonOWw0m9H
|
||||
p+tGUbGS2l873J5PrsbpeKEVR/IIoKo=
|
||||
</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
|
||||
</KeyDescriptor>
|
||||
|
||||
<AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://idptestbed:8443/idp/profile/SAML1/SOAP/AttributeQuery"/>
|
||||
<!-- <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idptestbed:8443/idp/profile/SAML2/SOAP/AttributeQuery"/> -->
|
||||
<!-- If you uncomment the above you should add urn:oasis:names:tc:SAML:2.0:protocol to the protocolSupportEnumeration above -->
|
||||
|
||||
</AttributeAuthorityDescriptor>
|
||||
|
||||
</EntityDescriptor>
|
40
common/ASC.SsoAuth/config/metadata/sp-onlyoffice.xml
Normal file
40
common/ASC.SsoAuth/config/metadata/sp-onlyoffice.xml
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://localhost:3000/sso/metadata">
|
||||
<SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
<KeyDescriptor use="signing">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIC6zCCAdOgAwIBAgIJAOy0nki3WAOVMA0GCSqGSIb3DQEBBQUAMBcxFTATBgNVBAMTDDc5OTQzZmVlNzg2NTAeFw0xNTEyMTEwMzAwNDJaFw0yNTEyMDgwMzAwNDJaMBcxFTATBgNVBAMTDDc5OTQzZmVlNzg2NTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPBzAz0DTn+j2YsQKfqWI+m08lP5UzwVsE9ZKzLqO3PRHZqiOBmEaFmRrYCZCAOcJ0TXcxPGtNSo8HC4uw5/Y5lJGuI3jN7X7KB1VUQDpUSwfgOqtrouDoVRKrsaYZTnlNV8KbZ0WQz5s4Uw6CxKRB9RZ5iQMP1fuxc8B6GSOb3x69MiY6c1jlgVAc6rV4zGfpafacxOLM8qcYhY8u3TiSd0H+oiGEqi1mFLK8yp6FKzX8OUkQfWe49YHz6wBxFOe+/p+7ziym1rBs/lGfenEo8ziCIMmjnoo257fz00bcz9rFl1rTxKLFfgy72xTlG72l6u+pB9VqK3YNJS52Ns5UCAwEAAaM6MDgwFwYDVR0RBBAwDoIMNzk5NDNmZWU3ODY1MB0GA1UdDgQWBBRiDMNPjiAMC50WWubI3PMjP45S/DANBgkqhkiG9w0BAQUFAAOCAQEAYZM/iWgC93vAq0d98egEzvESKodxHffkDOagd4Kxt/S0AAHsVQCmAK/9kmRhsWzR3f1KIw98q4EX7nH/K68BFrerUvaL5+fEGE9W6Ki6QdW8bM17GQkLyRDKZzGPm/hsaG1Oxru2kDf7qSvv59aRZlZ8skrDEnx8+dZ8JKC02ZDUClC+xWl1UPfO2BL4tJei/siSymGpiRqznQ2JMoTFu5CUUpoxyCVz1bl9lCVceoJ9FaL38knS0p5DnXcm+I8wqNEVGLDPbDalBQryhJT9fIMm1/B85gB3AWAvcu9PPfHKlQQUhxyEXTBJx3luLlpIjoloFKIute9K7pE5qAENjg==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</KeyDescriptor>
|
||||
<KeyDescriptor use="encryption">
|
||||
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:X509Data>
|
||||
<ds:X509Certificate>MIIC6zCCAdOgAwIBAgIJAOy0nki3WAOVMA0GCSqGSIb3DQEBBQUAMBcxFTATBgNVBAMTDDc5OTQzZmVlNzg2NTAeFw0xNTEyMTEwMzAwNDJaFw0yNTEyMDgwMzAwNDJaMBcxFTATBgNVBAMTDDc5OTQzZmVlNzg2NTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPBzAz0DTn+j2YsQKfqWI+m08lP5UzwVsE9ZKzLqO3PRHZqiOBmEaFmRrYCZCAOcJ0TXcxPGtNSo8HC4uw5/Y5lJGuI3jN7X7KB1VUQDpUSwfgOqtrouDoVRKrsaYZTnlNV8KbZ0WQz5s4Uw6CxKRB9RZ5iQMP1fuxc8B6GSOb3x69MiY6c1jlgVAc6rV4zGfpafacxOLM8qcYhY8u3TiSd0H+oiGEqi1mFLK8yp6FKzX8OUkQfWe49YHz6wBxFOe+/p+7ziym1rBs/lGfenEo8ziCIMmjnoo257fz00bcz9rFl1rTxKLFfgy72xTlG72l6u+pB9VqK3YNJS52Ns5UCAwEAAaM6MDgwFwYDVR0RBBAwDoIMNzk5NDNmZWU3ODY1MB0GA1UdDgQWBBRiDMNPjiAMC50WWubI3PMjP45S/DANBgkqhkiG9w0BAQUFAAOCAQEAYZM/iWgC93vAq0d98egEzvESKodxHffkDOagd4Kxt/S0AAHsVQCmAK/9kmRhsWzR3f1KIw98q4EX7nH/K68BFrerUvaL5+fEGE9W6Ki6QdW8bM17GQkLyRDKZzGPm/hsaG1Oxru2kDf7qSvv59aRZlZ8skrDEnx8+dZ8JKC02ZDUClC+xWl1UPfO2BL4tJei/siSymGpiRqznQ2JMoTFu5CUUpoxyCVz1bl9lCVceoJ9FaL38knS0p5DnXcm+I8wqNEVGLDPbDalBQryhJT9fIMm1/B85gB3AWAvcu9PPfHKlQQUhxyEXTBJx3luLlpIjoloFKIute9K7pE5qAENjg==</ds:X509Certificate>
|
||||
</ds:X509Data>
|
||||
</ds:KeyInfo>
|
||||
</KeyDescriptor>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:3000/sso/slo/callback"/>
|
||||
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:3000/sso/slo/callback"/>
|
||||
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
|
||||
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:3000/sso/acs" index="0"/>
|
||||
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:3000/sso/acs" index="1"/>
|
||||
<AttributeConsumingService>
|
||||
<RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
|
||||
<RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
|
||||
<RequestedAttribute FriendlyName="sn" Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
|
||||
<RequestedAttribute FriendlyName="mobile" Name="urn:oid:0.9.2342.19200300.100.1.41" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
|
||||
<RequestedAttribute FriendlyName="title" Name="urn:oid:2.5.4.12" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"/>
|
||||
</AttributeConsumingService>
|
||||
</SPSSODescriptor>
|
||||
<Organization xml:lang="en-US">
|
||||
<OrganizationName>Ascensio System SIA</OrganizationName>
|
||||
<OrganizationDisplayName>Onlyoffice</OrganizationDisplayName>
|
||||
<OrganizationURL>http://www.onlyoffice.com</OrganizationURL>
|
||||
</Organization>
|
||||
<ContactPerson contactType="support">
|
||||
<GivenName>Support Team</GivenName>
|
||||
<EmailAddress>support@onlyoffice.com</EmailAddress>
|
||||
</ContactPerson>
|
||||
</EntityDescriptor>
|
32
common/ASC.SsoAuth/config/production.json
Normal file
32
common/ASC.SsoAuth/config/production.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "Onlyoffice Single Sign-On handler",
|
||||
"port": 9834,
|
||||
"machinekey": "Vskoproizvolny Salt par Chivreski",
|
||||
"logDir": "/var/log/onlyoffice/",
|
||||
"logName": "web.sso.%DATE%.log",
|
||||
"logSamlData": false,
|
||||
"portal": {
|
||||
"ssoUrl": "/ssologin.ashx",
|
||||
"authUrl": "/Auth.aspx"
|
||||
}
|
||||
},
|
||||
"routes": {
|
||||
"login": "/sso/login",
|
||||
"login_callback": "/sso/acs",
|
||||
"logout": "/sso/slo",
|
||||
"logout_callback": "/sso/slo/callback",
|
||||
"metadata": "/sso/metadata"
|
||||
},
|
||||
"logger": {
|
||||
"file": {
|
||||
"level": "debug",
|
||||
"handleExceptions": true,
|
||||
"json": false,
|
||||
"datePattern": "MM-DD",
|
||||
"zippedArchive": true,
|
||||
"maxSize": "50m",
|
||||
"maxFiles": "30d"
|
||||
}
|
||||
}
|
||||
}
|
37
common/ASC.SsoAuth/package.json
Normal file
37
common/ASC.SsoAuth/package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "asc-sso-auth",
|
||||
"version": "2.9.0",
|
||||
"description": "ASC Single Sign-On handler",
|
||||
"homepage": "http://www.onlyoffice.com",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "nodemon app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@authenio/samlify-node-xmllint": "^2.0.0",
|
||||
"body-parser": "1.19.0",
|
||||
"co": "^4.6.0",
|
||||
"cookie-parser": "1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "4.17.1",
|
||||
"express-handlebars": "^4.0.6",
|
||||
"express-session": "1.17.1",
|
||||
"formidable": "^1.2.2",
|
||||
"lodash": "^4.17.20",
|
||||
"minify-xml": "^2.5.0",
|
||||
"morgan": "1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"nodemon": "2.0.4",
|
||||
"request": "^2.88.2",
|
||||
"samlify": "^2.7.6",
|
||||
"serve-favicon": "^2.5.0",
|
||||
"url": "^0.11.0",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
}
|
BIN
common/ASC.SsoAuth/public/favicon.ico
Normal file
BIN
common/ASC.SsoAuth/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 144 KiB |
23
common/ASC.SsoAuth/views/actions.handlebars
Normal file
23
common/ASC.SsoAuth/views/actions.handlebars
Normal file
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
actions.handlebars
|
||||
|
||||
This file provides a template for POST-binding actions.
|
||||
In order to make it more flexible, there are serveral parameters you can set.
|
||||
|
||||
entityEndpoint: either service or identity provider
|
||||
actionType: declares the type of actions (SAMLRequest/SAMLResponse)
|
||||
actionValue: value for action (value should be base64 encoded before subsitution)
|
||||
|
||||
-->
|
||||
<form id="saml-form" method="post" action="{{entityEndpoint}}" autocomplete="off">
|
||||
<input type="hidden" name="{{type}}" value="{{context}}" />
|
||||
{{#if relayState}}
|
||||
<input type="hidden" name="RelayState" value="{{relayState}}" />
|
||||
{{/if}}
|
||||
</form>
|
||||
<script type="text/javascript">
|
||||
// Automatic form submission
|
||||
(function () {
|
||||
document.forms[0].submit();
|
||||
})();
|
||||
</script>
|
1
common/ASC.SsoAuth/views/error.handlebars
Normal file
1
common/ASC.SsoAuth/views/error.handlebars
Normal file
@ -0,0 +1 @@
|
||||
{{message}}
|
10
common/ASC.SsoAuth/views/layouts/main.handlebars
Normal file
10
common/ASC.SsoAuth/views/layouts/main.handlebars
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
<body oncontextmenu="return false;">
|
||||
{{{body}}}
|
||||
</body>
|
||||
</html>
|
62
common/ASC.SsoAuth/web.config
Normal file
62
common/ASC.SsoAuth/web.config
Normal file
@ -0,0 +1,62 @@
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
|
||||
<handlers>
|
||||
<!-- indicates that the app.js file is a node.js application to be handled by the iisnode module -->
|
||||
<add name="iisnode" path="app.js" verb="*" modules="iisnode" />
|
||||
</handlers>
|
||||
|
||||
<rewrite>
|
||||
<rules>
|
||||
<!-- Don't interfere with requests for node-inspector debugging -->
|
||||
<rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
|
||||
<match url="^app.js\/debug[\/]?" />
|
||||
</rule>
|
||||
|
||||
<!-- First we consider whether the incoming URL matches a physical file in the /public folder -->
|
||||
<rule name="StaticContent">
|
||||
<action type="Rewrite" url="public{REQUEST_URI}" />
|
||||
</rule>
|
||||
|
||||
<!-- All other URLs are mapped to the Node.js application entry point -->
|
||||
<rule name="DynamicContent">
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="app.js" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
<httpRedirect enabled="false" destination="" exactDestination="false" />
|
||||
|
||||
<!-- You can control how Node is hosted within IIS using the following options -->
|
||||
<!--<iisnode
|
||||
node_env="%node_env%"
|
||||
nodeProcessCountPerApplication="1"
|
||||
maxConcurrentRequestsPerProcess="1024"
|
||||
maxNamedPipeConnectionRetry="3"
|
||||
namedPipeConnectionRetryDelay="2000"
|
||||
maxNamedPipeConnectionPoolSize="512"
|
||||
maxNamedPipePooledConnectionAge="30000"
|
||||
asyncCompletionThreadCount="0"
|
||||
initialRequestBufferSize="4096"
|
||||
maxRequestBufferSize="65536"
|
||||
watchedFiles="*.js"
|
||||
uncFileChangesPollingInterval="5000"
|
||||
gracefulShutdownTimeout="60000"
|
||||
loggingEnabled="true"
|
||||
logDirectoryNameSuffix="logs"
|
||||
debuggingEnabled="true"
|
||||
debuggerPortRange="5058-6058"
|
||||
debuggerPathSegment="debug"
|
||||
maxLogFileSizeInKB="128"
|
||||
appendToExistingLog="false"
|
||||
logFileFlushInterval="5000"
|
||||
devErrorsEnabled="true"
|
||||
flushResponse="false"
|
||||
enableXFF="false"
|
||||
promoteServerVars=""
|
||||
/>-->
|
||||
|
||||
</system.webServer>
|
||||
</configuration>
|
2485
common/ASC.SsoAuth/yarn.lock
Normal file
2485
common/ASC.SsoAuth/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,191 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
|
||||
using ASC.ApiSystem.Models;
|
||||
using ASC.Common;
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Core;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ASC.ApiSystem.Controllers
|
||||
{
|
||||
[Scope]
|
||||
[Obsolete("CoreSettingsController is deprecated, please use SettingsController instead.")]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class CoreSettingsController : ControllerBase
|
||||
{
|
||||
private CoreSettings CoreSettings { get; }
|
||||
private ILog Log { get; }
|
||||
|
||||
public CoreSettingsController(
|
||||
CoreSettings coreSettings,
|
||||
IOptionsMonitor<ILog> option
|
||||
)
|
||||
{
|
||||
CoreSettings = coreSettings;
|
||||
Log = option.Get("ASC.ApiSystem");
|
||||
}
|
||||
|
||||
#region For TEST api
|
||||
|
||||
[HttpGet("test")]
|
||||
public IActionResult Check()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
value = "CoreSettings api works"
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region API methods
|
||||
|
||||
[HttpGet("get")]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip")]
|
||||
public IActionResult GetSettings(int tenant, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tenant < -1)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "portalNameIncorrect",
|
||||
message = "Tenant is incorrect"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "params",
|
||||
message = "Key is empty"
|
||||
});
|
||||
}
|
||||
|
||||
var settings = CoreSettings.GetSetting(key, tenant);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
settings
|
||||
});
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
Log.Error(ae);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
error = "error",
|
||||
message = ae.Message
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
error = "error",
|
||||
message = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("save")]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip")]
|
||||
public IActionResult SaveSettings([FromBody] CoreSettingsModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (model == null || string.IsNullOrEmpty(model.Key))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "params",
|
||||
message = "Key is empty"
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.Value))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "params",
|
||||
message = "Value is empty"
|
||||
});
|
||||
}
|
||||
|
||||
var tenant = model.Tenant;
|
||||
|
||||
if (tenant < -1)
|
||||
{
|
||||
tenant = -1;
|
||||
}
|
||||
|
||||
CoreSettings.SaveSetting(model.Key, model.Value, tenant);
|
||||
|
||||
var settings = CoreSettings.GetSetting(model.Key, tenant);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
settings
|
||||
});
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
Log.Error(ae);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
error = "params",
|
||||
message = ae.Message
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
error = "error",
|
||||
message = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -1,787 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* (c) Copyright Ascensio System Limited 2010-2018
|
||||
*
|
||||
* This program is freeware. You can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html).
|
||||
* In accordance with Section 7(a) of the GNU GPL 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 more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* You can contact Ascensio System SIA by email at sales@onlyoffice.com
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display
|
||||
* Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3.
|
||||
*
|
||||
* Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains
|
||||
* relevant author attributions when distributing the software. If the display of the logo in its graphic
|
||||
* form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE"
|
||||
* in every copy of the program you distribute.
|
||||
* Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
using ASC.ApiSystem.Classes;
|
||||
using ASC.ApiSystem.Models;
|
||||
using ASC.Common;
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Common.Utils;
|
||||
using ASC.Core;
|
||||
using ASC.Core.Billing;
|
||||
using ASC.Core.Common.Settings;
|
||||
using ASC.Core.Tenants;
|
||||
using ASC.Core.Users;
|
||||
using ASC.Security.Cryptography;
|
||||
using ASC.Web.Core.Helpers;
|
||||
using ASC.Web.Core.Users;
|
||||
using ASC.Web.Core.Utility;
|
||||
using ASC.Web.Core.Utility.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace ASC.ApiSystem.Controllers
|
||||
{
|
||||
[Scope]
|
||||
[Obsolete("Registration is deprecated, please use PortalController or TariffController instead.")]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class RegistrationController : ControllerBase
|
||||
{
|
||||
private CommonMethods CommonMethods { get; }
|
||||
private CommonConstants CommonConstants { get; }
|
||||
private HostedSolution HostedSolution { get; }
|
||||
private TimeZonesProvider TimeZonesProvider { get; }
|
||||
private TimeZoneConverter TimeZoneConverter { get; }
|
||||
private ApiSystemHelper ApiSystemHelper { get; }
|
||||
private SecurityContext SecurityContext { get; }
|
||||
private TenantManager TenantManager { get; }
|
||||
private SettingsManager SettingsManager { get; }
|
||||
private CoreSettings CoreSettings { get; }
|
||||
private TenantDomainValidator TenantDomainValidator { get; }
|
||||
private UserFormatter UserFormatter { get; }
|
||||
private UserManagerWrapper UserManagerWrapper { get; }
|
||||
private IConfiguration Configuration { get; }
|
||||
public PasswordHasher PasswordHasher { get; }
|
||||
private ILog Log { get; }
|
||||
|
||||
public RegistrationController(
|
||||
CommonMethods commonMethods,
|
||||
CommonConstants commonConstants,
|
||||
IOptionsSnapshot<HostedSolution> hostedSolution,
|
||||
TimeZonesProvider timeZonesProvider,
|
||||
TimeZoneConverter timeZoneConverter,
|
||||
ApiSystemHelper apiSystemHelper,
|
||||
SecurityContext securityContext,
|
||||
TenantManager tenantManager,
|
||||
SettingsManager settingsManager,
|
||||
CoreSettings coreSettings,
|
||||
TenantDomainValidator tenantDomainValidator,
|
||||
UserFormatter userFormatter,
|
||||
UserManagerWrapper userManagerWrapper,
|
||||
IConfiguration configuration,
|
||||
IOptionsMonitor<ILog> option,
|
||||
PasswordHasher passwordHasher)
|
||||
{
|
||||
CommonMethods = commonMethods;
|
||||
CommonConstants = commonConstants;
|
||||
HostedSolution = hostedSolution.Value;
|
||||
TimeZonesProvider = timeZonesProvider;
|
||||
TimeZoneConverter = timeZoneConverter;
|
||||
ApiSystemHelper = apiSystemHelper;
|
||||
SecurityContext = securityContext;
|
||||
TenantManager = tenantManager;
|
||||
SettingsManager = settingsManager;
|
||||
CoreSettings = coreSettings;
|
||||
TenantDomainValidator = tenantDomainValidator;
|
||||
UserFormatter = userFormatter;
|
||||
UserManagerWrapper = userManagerWrapper;
|
||||
Configuration = configuration;
|
||||
PasswordHasher = passwordHasher;
|
||||
Log = option.Get("ASC.ApiSystem");
|
||||
}
|
||||
|
||||
#region For TEST api
|
||||
|
||||
[HttpGet("test")]
|
||||
public IActionResult Check()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
value = "Registration api works"
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region API methods
|
||||
|
||||
[HttpPost("registerportal")]
|
||||
[AllowCrossSiteJson]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip.registerportal")]
|
||||
public IActionResult Register(TenantModel model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "Tenant data is required."
|
||||
});
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var errors = new JArray();
|
||||
|
||||
foreach (var k in ModelState.Keys)
|
||||
{
|
||||
errors.Add(ModelState[k].Errors.FirstOrDefault().ErrorMessage);
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
object error;
|
||||
|
||||
if (string.IsNullOrEmpty(model.PasswordHash) && !string.IsNullOrEmpty(model.Password))
|
||||
{
|
||||
if (!CheckPasswordPolicy(model.Password, out error))
|
||||
{
|
||||
sw.Stop();
|
||||
|
||||
return BadRequest(error);
|
||||
}
|
||||
model.PasswordHash = PasswordHasher.GetClientPassword(model.Password);
|
||||
}
|
||||
|
||||
if (!CheckValidName(model.FirstName.Trim() + model.LastName.Trim(), out error))
|
||||
{
|
||||
sw.Stop();
|
||||
|
||||
return BadRequest(error);
|
||||
}
|
||||
|
||||
var checkTenantBusyPesp = CheckExistingNamePortal(model.PortalName.Trim());
|
||||
|
||||
if (checkTenantBusyPesp != null)
|
||||
{
|
||||
sw.Stop();
|
||||
|
||||
return checkTenantBusyPesp;
|
||||
}
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. CheckExistingNamePortal: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
|
||||
var clientIP = CommonMethods.GetClientIp();
|
||||
|
||||
Log.DebugFormat("clientIP = {0}", clientIP);
|
||||
|
||||
if (CommonMethods.CheckMuchRegistration(model, clientIP, sw))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = new[] { "tooMuchAttempts" }
|
||||
});
|
||||
}
|
||||
|
||||
if (CommonConstants.RecaptchaRequired && !CommonMethods.IsTestEmail(model.Email))
|
||||
{
|
||||
/*** validate recaptcha ***/
|
||||
if (!CommonMethods.ValidateRecaptcha(model.RecaptchaResponse, clientIP))
|
||||
{
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. ValidateRecaptcha: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
|
||||
sw.Stop();
|
||||
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = new[] { "recaptchaInvalid" },
|
||||
message = "Recaptcha is invalid"
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. ValidateRecaptcha: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
//check payment portal count
|
||||
if (Configuration["core:base-domain"] == "localhost")
|
||||
{
|
||||
var tenants = HostedSolution.GetTenants(DateTime.MinValue);
|
||||
var firstTenant = tenants.FirstOrDefault();
|
||||
|
||||
if (firstTenant != null)
|
||||
{
|
||||
var activePortals = tenants.Count(r => r.Status != TenantStatus.Suspended && r.Status != TenantStatus.RemovePending);
|
||||
|
||||
var quota = HostedSolution.GetTenantQuota(firstTenant.TenantId);
|
||||
|
||||
if (quota.CountPortals > 0 && quota.CountPortals <= activePortals)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = new[] { "portalsCountTooMuch" },
|
||||
message = "Too much portals registered already",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var language = model.Language ?? string.Empty;
|
||||
|
||||
var tz = TimeZonesProvider.GetCurrentTimeZoneInfo(language);
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. TimeZonesProvider.GetCurrentTimeZoneInfo: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
|
||||
if (!string.IsNullOrEmpty(model.TimeZoneName))
|
||||
{
|
||||
tz = TimeZoneConverter.GetTimeZone(model.TimeZoneName.Trim(), false) ?? tz;
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. TimeZonesProvider.OlsonTimeZoneToTimeZoneInfo: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var lang = TimeZonesProvider.GetCurrentCulture(language);
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; model.Language = {1}, resultLang.DisplayName = {2}", model.PortalName, language, lang.DisplayName);
|
||||
|
||||
var info = new TenantRegistrationInfo
|
||||
{
|
||||
Name = Configuration["web:portal-name"] ?? "Cloud Office Applications",
|
||||
Address = model.PortalName,
|
||||
Culture = lang,
|
||||
FirstName = model.FirstName.Trim(),
|
||||
LastName = model.LastName.Trim(),
|
||||
PasswordHash = string.IsNullOrEmpty(model.PasswordHash) ? null : model.PasswordHash,
|
||||
Email = model.Email.Trim(),
|
||||
TimeZoneInfo = tz,
|
||||
MobilePhone = string.IsNullOrEmpty(model.Phone) ? null : model.Phone.Trim(),
|
||||
Industry = (TenantIndustry)model.Industry,
|
||||
Spam = model.Spam,
|
||||
Calls = model.Calls,
|
||||
LimitedControlPanel = model.LimitedControlPanel
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(model.PartnerId))
|
||||
{
|
||||
if (Guid.TryParse(model.PartnerId, out var guid))
|
||||
{
|
||||
// valid guid
|
||||
info.PartnerId = model.PartnerId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.AffiliateId))
|
||||
{
|
||||
info.AffiliateId = model.AffiliateId;
|
||||
}
|
||||
|
||||
Tenant t;
|
||||
|
||||
try
|
||||
{
|
||||
/****REGISTRATION!!!*****/
|
||||
if (!string.IsNullOrEmpty(ApiSystemHelper.ApiCacheUrl))
|
||||
{
|
||||
ApiSystemHelper.AddTenantToCache(info.Address, SecurityContext.CurrentAccount.ID);
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. CacheController.AddTenantToCache: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
HostedSolution.RegisterTenant(info, out t);
|
||||
|
||||
/*********/
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. HostedSolution.RegisterTenant: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
sw.Stop();
|
||||
|
||||
Log.Error(e);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
errors = new[] { "registerNewTenantError" },
|
||||
message = e.Message,
|
||||
stacktrace = e.StackTrace
|
||||
});
|
||||
}
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
string sendCongratulationsAddress = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(model.PasswordHash))
|
||||
{
|
||||
isFirst = !CommonMethods.SendCongratulations(Request.Scheme, t, model.SkipWelcome, out sendCongratulationsAddress);
|
||||
}
|
||||
else if (Configuration["core:base-domain"] == "localhost")
|
||||
{
|
||||
try
|
||||
{
|
||||
/* set wizard not completed*/
|
||||
TenantManager.SetCurrentTenant(t);
|
||||
|
||||
var settings = SettingsManager.Load<WizardSettings>();
|
||||
|
||||
settings.Completed = false;
|
||||
|
||||
SettingsManager.Save(settings);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
var reference = CommonMethods.CreateReference(Request.Scheme, t.GetTenantDomain(CoreSettings), info.Email, isFirst, model.Module);
|
||||
|
||||
Log.DebugFormat("PortalName = {0}; Elapsed ms. CreateReferenceByCookie...: {1}", model.PortalName, sw.ElapsedMilliseconds);
|
||||
|
||||
sw.Stop();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
errors = "",
|
||||
reference,
|
||||
tenant = ToTenantWrapper(t),
|
||||
referenceWelcome = sendCongratulationsAddress,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("removeportal")]
|
||||
[AllowCrossSiteJson]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip")]
|
||||
public IActionResult Remove(string portalName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(portalName))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "portalName is required."
|
||||
});
|
||||
}
|
||||
|
||||
var tenant = HostedSolution.GetTenant(portalName.Trim());
|
||||
|
||||
if (tenant == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "Tenant not found."
|
||||
});
|
||||
}
|
||||
|
||||
HostedSolution.RemoveTenant(tenant);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
errors = "",
|
||||
tenant = ToTenantWrapper(tenant)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("tariff")]
|
||||
[AllowCrossSiteJson]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip")]
|
||||
public IActionResult SetTariff(TariffModel model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "PortalName is required."
|
||||
});
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var errors = new JArray();
|
||||
|
||||
foreach (var k in ModelState.Keys)
|
||||
{
|
||||
errors.Add(ModelState[k].Errors.FirstOrDefault().ErrorMessage);
|
||||
}
|
||||
|
||||
return BadRequest(new
|
||||
{
|
||||
errors
|
||||
});
|
||||
}
|
||||
|
||||
var tenant = HostedSolution.GetTenant((model.PortalName ?? "").Trim());
|
||||
|
||||
if (tenant == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "Tenant not found."
|
||||
});
|
||||
}
|
||||
|
||||
var quota = new TenantQuota(tenant.TenantId)
|
||||
{
|
||||
ActiveUsers = 10000,
|
||||
Features = model.Features ?? "",
|
||||
MaxFileSize = 1024 * 1024 * 1024,
|
||||
MaxTotalSize = 1024L * 1024 * 1024 * 1024 - 1,
|
||||
Name = "api",
|
||||
};
|
||||
|
||||
if (model.ActiveUsers != default)
|
||||
{
|
||||
quota.ActiveUsers = model.ActiveUsers;
|
||||
}
|
||||
|
||||
if (model.MaxTotalSize != default)
|
||||
{
|
||||
quota.MaxTotalSize = model.MaxTotalSize;
|
||||
}
|
||||
|
||||
if (model.MaxFileSize != default)
|
||||
{
|
||||
quota.MaxFileSize = model.MaxFileSize;
|
||||
}
|
||||
|
||||
HostedSolution.SaveTenantQuota(quota);
|
||||
|
||||
var tariff = new Tariff
|
||||
{
|
||||
QuotaId = quota.Id,
|
||||
DueDate = model.DueDate != default ? model.DueDate : DateTime.MaxValue,
|
||||
};
|
||||
|
||||
HostedSolution.SetTariff(tenant.TenantId, tariff);
|
||||
|
||||
tariff = HostedSolution.GetTariff(tenant.TenantId, false);
|
||||
|
||||
quota = HostedSolution.GetTenantQuota(tenant.TenantId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
errors = "",
|
||||
tenant = ToTenantWrapper(tenant),
|
||||
tariff = ToTariffWrapper(tariff, quota)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("tariff")]
|
||||
[AllowCrossSiteJson]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip")]
|
||||
public IActionResult GetTariff(string portalName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(portalName))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "portalName is required."
|
||||
});
|
||||
}
|
||||
|
||||
var tenant = HostedSolution.GetTenant(portalName.Trim());
|
||||
|
||||
if (tenant == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "Tenant not found."
|
||||
});
|
||||
}
|
||||
|
||||
var tariff = HostedSolution.GetTariff(tenant.TenantId, false);
|
||||
|
||||
var quota = HostedSolution.GetTenantQuota(tenant.TenantId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
errors = "",
|
||||
tenant = ToTenantWrapper(tenant),
|
||||
tariff = ToTariffWrapper(tariff, quota)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("statusportal")]
|
||||
[AllowCrossSiteJson]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip")]
|
||||
public IActionResult ChangeStatus(TenantModel model, bool active)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "PortalName is required."
|
||||
});
|
||||
}
|
||||
|
||||
var tenant = HostedSolution.GetTenant((model.PortalName ?? "").Trim());
|
||||
|
||||
if (tenant == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "Tenant not found."
|
||||
});
|
||||
}
|
||||
|
||||
tenant.SetStatus(active ? TenantStatus.Active : TenantStatus.Suspended);
|
||||
|
||||
HostedSolution.SaveTenant(tenant);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
errors = "",
|
||||
tenant = ToTenantWrapper(tenant)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("validateportalname")]
|
||||
[AllowCrossSiteJson]
|
||||
public IActionResult CheckExistingNamePortal(TenantModel model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = "PortalName is required."
|
||||
});
|
||||
}
|
||||
|
||||
var response = CheckExistingNamePortal((model.PortalName ?? "").Trim());
|
||||
|
||||
return response ?? Ok(new
|
||||
{
|
||||
errors = "",
|
||||
message = "portalNameReadyToRegister"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("getportals")]
|
||||
[AllowCrossSiteJson]
|
||||
[Authorize(AuthenticationSchemes = "auth.allowskip")]
|
||||
public IActionResult GetPortalsByEmail(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tenants = string.IsNullOrEmpty(email)
|
||||
? HostedSolution.GetTenants(DateTime.MinValue).OrderBy(t => t.TenantId).ToList()
|
||||
: HostedSolution.FindTenants(email.Trim()).OrderBy(t => t.TenantId).ToList();
|
||||
|
||||
|
||||
var refers = tenants.Where(t => t.Status == TenantStatus.Active).ToList()
|
||||
.ConvertAll(t => string.Format("{0}{1}{2}",
|
||||
Request.Scheme,
|
||||
Uri.SchemeDelimiter,
|
||||
t.GetTenantDomain(CoreSettings)));
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
errors = "",
|
||||
message = "",
|
||||
portals = refers
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
errors = new[] { "error" },
|
||||
message = ex.Message,
|
||||
stacktrace = ex.StackTrace
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region private methods
|
||||
|
||||
private object ToTenantWrapper(Tenant t)
|
||||
{
|
||||
return new
|
||||
{
|
||||
tenantId = t.TenantId,
|
||||
tenantAlias = t.TenantAlias,
|
||||
tenantDomain = t.GetTenantDomain(CoreSettings),
|
||||
hostedRegion = t.HostedRegion,
|
||||
name = t.Name,
|
||||
ownerId = t.OwnerId,
|
||||
status = t.Status,
|
||||
partnerId = t.PartnerId,
|
||||
paymentId = t.PaymentId,
|
||||
language = t.Language,
|
||||
industry = t.Industry,
|
||||
timeZoneName = TimeZoneConverter.GetTimeZone(t.TimeZone).DisplayName,
|
||||
createdDateTime = t.CreatedDateTime
|
||||
};
|
||||
}
|
||||
|
||||
private object ToTariffWrapper(Tariff t, TenantQuota q)
|
||||
{
|
||||
return new
|
||||
{
|
||||
t.DueDate,
|
||||
t.State,
|
||||
q.MaxTotalSize,
|
||||
q.MaxFileSize,
|
||||
q.ActiveUsers,
|
||||
q.Features
|
||||
};
|
||||
}
|
||||
|
||||
private IActionResult CheckExistingNamePortal(string portalName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(portalName))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "portalNameEmpty"
|
||||
});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ApiSystemHelper.ApiCacheUrl))
|
||||
{
|
||||
ValidateDomain(portalName.Trim());
|
||||
}
|
||||
else
|
||||
{
|
||||
HostedSolution.CheckTenantAddress(portalName.Trim());
|
||||
}
|
||||
}
|
||||
catch (TenantAlreadyExistsException ex)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = new[] { "portalNameExist" },
|
||||
variants = ex.ExistsTenants.ToArray()
|
||||
});
|
||||
}
|
||||
catch (TenantTooShortException)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = new[] { "tooShortError" }
|
||||
});
|
||||
|
||||
}
|
||||
catch (TenantIncorrectCharsException)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
errors = new[] { "portalNameIncorrect" }
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex);
|
||||
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new
|
||||
{
|
||||
errors = new[] { "error" },
|
||||
message = ex.Message,
|
||||
stacktrace = ex.StackTrace
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ValidateDomain(string domain)
|
||||
{
|
||||
// size
|
||||
TenantDomainValidator.ValidateDomainLength(domain);
|
||||
// characters
|
||||
TenantDomainValidator.ValidateDomainCharacters(domain);
|
||||
|
||||
var sameAliasTenants = ApiSystemHelper.FindTenantsInCache(domain, SecurityContext.CurrentAccount.ID);
|
||||
|
||||
if (sameAliasTenants != null)
|
||||
{
|
||||
throw new TenantAlreadyExistsException("Address busy.", sameAliasTenants);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckValidName(string name, out object error)
|
||||
{
|
||||
error = null;
|
||||
|
||||
if (string.IsNullOrEmpty(name = (name ?? "").Trim()))
|
||||
{
|
||||
error = new
|
||||
{
|
||||
error = new[] { "error" },
|
||||
message = "name is required"
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!UserFormatter.IsValidUserName(name, string.Empty))
|
||||
{
|
||||
error = new
|
||||
{
|
||||
error = new[] { "error" },
|
||||
message = "name is incorrect"
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CheckPasswordPolicy(string pwd, out object error)
|
||||
{
|
||||
error = null;
|
||||
//Validate Password match
|
||||
if (string.IsNullOrEmpty(pwd))
|
||||
{
|
||||
error = new
|
||||
{
|
||||
errors = new[] { "passEmpty" },
|
||||
message = "password is empty"
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var passwordSettings = (PasswordSettings)new PasswordSettings().GetDefault(Configuration);
|
||||
|
||||
if (!UserManagerWrapper.CheckPasswordRegex(passwordSettings, pwd))
|
||||
{
|
||||
error = new
|
||||
{
|
||||
errors = new[] { "passPolicyError" },
|
||||
message = "password is incorrect"
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user