Merge branch 'develop' into feature/translations

This commit is contained in:
Alexey Safronov 2021-06-01 20:02:48 +03:00
commit 95dbfb8c5c
419 changed files with 16093 additions and 9999 deletions

4
.gitmodules vendored Normal file
View 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

View File

@ -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
View File

@ -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
View 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

View File

@ -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

View File

@ -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
};

View File

@ -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;
}
}
}

View 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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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"));
}

View File

@ -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('/');
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -48,5 +48,17 @@ namespace ASC.Core.Billing
get;
set;
}
public int PaymentStatus
{
get;
set;
}
public int Quantity
{
get;
set;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)
{

View File

@ -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();

View File

@ -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" },

View File

@ -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;

View File

@ -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");

View File

@ -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");

View File

@ -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",

View File

@ -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")

View File

@ -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"

View File

@ -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" }
});
}

View File

@ -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"

View File

@ -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")

View File

@ -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")
},

View File

@ -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")

View File

@ -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"

View File

@ -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" }
});
}

View File

@ -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"

View File

@ -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;
}

View File

@ -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()
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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; }

View File

@ -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; } }

View File

@ -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);
}
}

View File

@ -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))
{

View File

@ -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
{

View File

@ -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)

View File

@ -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;

View File

@ -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
{

View File

@ -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>

View File

@ -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);

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -205,7 +205,7 @@ namespace ASC.Data.Storage
}
var settings = SettingsManager.LoadForTenant<StorageSettings>(tenantId);
//TODO:GetStoreAndCache
return GetDataStore(tenant, module, StorageSettingsHelper.DataStoreConsumer(settings), controller);
}

View File

@ -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;
}
}
}

View File

@ -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)));
}

View File

@ -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 &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;&lt;td colspan=&quot;3&quot; style=&quot;height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;&quot;&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
/// &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;
/// &lt;td style=&quot;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;&quot;&gt;
/// &lt;img src=&quot;https://static.onlyoffice.com/media/newslet [rest of string was truncated]&quot;;.
/// Looks up a localized string similar to &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;&lt;td colspan=&quot;3&quot; style=&quot;height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;&quot;&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
/// &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;
/// &lt;td style=&quot;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;&quot;&gt;
/// &lt;img src=&quot;%IMAGEPATH%/tech-100.png&quot; style=&quot;width: [rest of string was truncated]&quot;;.
/// </summary>
public static string FooterCommonV10 {
get {
@ -76,19 +76,7 @@ namespace ASC.Notify.Textile.Resources {
/// Looks up a localized string similar to &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;&lt;td colspan=&quot;3&quot; style=&quot;height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;&quot;&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
/// &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;
/// &lt;td style=&quot;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;&quot;&gt;
/// &lt;img src=&quot;https://static.onlyoffice.com/media/news [rest of string was truncated]&quot;;.
/// </summary>
public static string FooterFreeCloud {
get {
return ResourceManager.GetString("FooterFreeCloud", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;&lt;td colspan=&quot;3&quot; style=&quot;height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;&quot;&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
/// &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;
/// &lt;td style=&quot;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;&quot;&gt;
/// &lt;img src=&quot;https://static.onlyoffice.com/media/news [rest of string was truncated]&quot;;.
/// &lt;img src=&quot;%IMAGEPATH%/tech-100.png&quot; style=&quot;width: [rest of string was truncated]&quot;;.
/// </summary>
public static string FooterOpensourceV10 {
get {
@ -96,34 +84,6 @@ namespace ASC.Notify.Textile.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to &lt;table cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; style=&quot;border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;&quot;&gt;
/// &lt;tbody&gt;
/// &lt;tr&gt;
/// &lt;td&gt;
/// &lt;div style=&quot;width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;&quot;&gt;Please, proceed to our &lt;a href=&quot;http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx&quot; target=&quot;_blank&quot;&gt;FAQ section&lt;/a&gt; if you have any questions. ONLYOFFICE is also available in &lt;a href=&quot;https://chrome.google.com/webs [rest of string was truncated]&quot;;.
/// </summary>
public static string FooterPersonal {
get {
return ResourceManager.GetString("FooterPersonal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;
/// &lt;td style=&quot;width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;&quot;&gt;
/// &lt;a href=&quot;https://www.facebook.com/pages/OnlyOffice/833032526736775&quot; style=&quot;width: 40px; height: 40px; display: block; margin: 0; padding: 0;&quot; target=&quot;_blank&quot;&gt;
/// &lt;img src=&quot;%IMAGEPATH%/social-fb-40.png&quot; alt=&quot;Facebook&quot; style=&quot;width: 40px; height: 40px;&quot; /&gt;
/// &lt;/a&gt;
/// &lt;/td&gt;
/// [rest of string was truncated]&quot;;.
/// </summary>
public static string FooterSocial {
get {
return ResourceManager.GetString("FooterSocial", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;body style=&quot;margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;&quot;&gt;
///&lt;div style=&quot;background-color: #fff; width: 600px; margin: 0 auto; text-align: left;&quot;&gt;
@ -137,39 +97,14 @@ namespace ASC.Notify.Textile.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to &lt;body style=&quot;margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;&quot;&gt;
///&lt;div style=&quot;background-color: #fff; width: 600px; margin: 0 auto; text-align: left;&quot;&gt;
/// &lt;table cellspacing=&quot;0&quot; cellpadding=&quot;0&quot; style=&quot;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;&quot;&gt;
/// &lt;tbody&gt;
/// &lt;tr border=&quot;0 [rest of string was truncated]&quot;;.
/// </summary>
public static string HtmlMasterPersonal {
get {
return ResourceManager.GetString("HtmlMasterPersonal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;body style=&quot;margin: 0; padding: 10px 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #5e5e5e; background-color: #dadada;&quot;&gt;
/// &lt;div id=&quot;common_style&quot; style=&quot;background-color: #fff; width: 680px; margin: 20px auto; text-align: left;&quot;&gt;
/// &lt;div style=&quot;background-color: #fff; width: 600px; padding:0 40px;&quot;&gt;
/// &lt;a href=&quot;http://www.onlyoffice.com/&quot; style=&quot;text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;&quot;&gt;
/// &lt;im [rest of string was truncated]&quot;;.
/// </summary>
public static string HtmlMasterPromo {
get {
return ResourceManager.GetString("HtmlMasterPromo", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;tr border=&quot;0&quot; cellspacing=&quot;0&quot; cellpadding=&quot;0&quot;&gt;
/// &lt;td style=&quot;width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;&quot;&gt;
/// &lt;a href=&quot;https://www.facebook.com/pages/OnlyOffice/833032526736775&quot; style=&quot;width: 40px; height: 40px; display: block; margin: 0; padding: 0;&quot; target=&quot;_blank&quot;&gt;
/// &lt;img src=&quot;%IMAGEPATH%/social-fb-40.png&quot; alt=&quot;Facebook&quot; style=&quot;width: 40px; height: 40px;&quot; /&gt;
/// &lt;/a&gt;
/// &lt;/td [rest of string was truncated]&quot;;.
/// &lt;/td&gt;
/// &lt;td style=&quot;width: 40px; vertical-a [rest of string was truncated]&quot;;.
/// </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.
///&lt;br /&gt;You receive this email because you are a registered user of &lt;a href=&quot;http://www.onlyoffice.com/&quot; style=&quot;color: #7b7b7b;&quot; target=&quot;_blank&quot;&gt;onlyoffice.com&lt;/a&gt;
///&lt;br /&gt;If you no longer wish to receive these emails, click on the following link: &lt;a href=&quot;{0}&quot; style=&quot;color: #7b7b7b;&quot; target=&quot;_blank&quot;&gt;Unsubscribe&lt;/a&gt;
///&lt;br /&gt;.
/// </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.
///&lt;br /&gt;You receive this email because you are a registered user of &lt;a href=&quot;{0}&quot; style=&quot;color: #7b7b7b;&quot; target=&quot;_blank&quot;&gt;{0}&lt;/a&gt;

View File

@ -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>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
@ -88,38 +88,8 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterFreeCloud" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;"&gt;
&lt;img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Benötigen Sie technische Unterstützung?&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;"&gt;Verkaufsfragen&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Demoversion bestellen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;"&gt; Senden Sie uns Ihre Frage&lt;/a&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;"&gt;
&lt;a href="mailto:sales@onlyoffice.com" style="color: #333;"&gt;Senden Sie uns eine E-Mail an&lt;/a&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;"&gt;Senden Sie eine Anfrage an uns&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterOpensourceV10" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
@ -148,64 +118,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterPersonal" xml:space="preserve">
<value>&lt;table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;"&gt;Besuchen Sie bitte unsere &lt;a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank"&gt;FAQ Seite&lt;/a&gt;, falls Sie noch Fragen haben. ONLYOFFICE ist verfügbar in &lt;a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank"&gt;Chrome Web Store&lt;/a&gt;.
&lt;/div&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div style="width: 170px;"&gt;
&lt;div style="font-size:22px; font-weight: bold; margin-bottom: 5px;"&gt;Bleiben Sie dran!&lt;/div&gt;
&lt;div style="background-color: #b9e3f5; padding: 11px; margin: 0;"&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"&gt;&lt;img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"&gt;&lt;img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"&gt;&lt;img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&amp;gid=3159387&amp;trk=anet_ug_hm"&gt;&lt;img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</value>
</data>
<data name="FooterSocial" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="HtmlMaster" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
@ -246,85 +158,39 @@
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPersonal" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
&lt;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;"&gt;
&lt;tbody&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;"&gt;
&lt;div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;"&gt;
&lt;a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" /&gt;
&lt;/a&gt;
&lt;/div&gt;
<data name="SocialNetworksFooterV10" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;"&gt;
&lt;div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"&gt;&lt;/div&gt;
&lt;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;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
%FOOTER%
&lt;/tbody&gt;
&lt;/table&gt;
&lt;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; "&gt;
&lt;tbody&gt;
%FOOTERSOCIAL%
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;"&gt;
&lt;p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;"&gt;
%TEXTFOOTER%
&lt;/p&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://www.onlyoffice.com/blog" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-blog-40.png" alt="ONLYOFFICE" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPromo" xml:space="preserve">
<value>&lt;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;"&gt;
&lt;div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;"&gt;
&lt;div style="background-color: #fff; width: 600px; padding:0 40px;"&gt;
&lt;a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;"&gt;
&lt;img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div id="content"&gt;
&lt;table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="width: 600px; background-color: #fff; padding:0 40px;"&gt;
&lt;div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
%FOOTER%
&lt;table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;"&gt;
&lt;tr&gt;
&lt;td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;"&gt;
&lt;p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;"&gt;%TEXTFOOTER%&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
<value>Diese E-Mail ist automatisch generiert und erfordert keine Antwort.
&lt;br /&gt;Sie erhalten diese E-Mail, weil Sie ein registrierter Benutzer bzw. eine registrierte Benutzerin von &lt;a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank"&gt;onlyoffice.com&lt;/a&gt; sind.
&lt;br /&gt;Wenn Sie keine Nachrichten mehr von uns bekommen möchten, klicken Sie auf den folgenden Link: &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;Unsubscribe&lt;/a&gt;
&lt;br /&gt;</value>
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="https://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
<value>Diese E-Mail ist automatisch generiert und erfordert keine Antwort.

View File

@ -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>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
@ -88,36 +88,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterFreeCloud" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;"&gt;
&lt;img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;¿Se necesita ayuda?&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;"&gt;Preguntas de ventas&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Solicitar demostración&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;"&gt;Haga pregunta&lt;/a&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;"&gt;
&lt;a href="mailto:sales@onlyoffice.com" style="color: #333;"&gt;Contáctenos&lt;/a&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;"&gt;Envie solicitud&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterOpensourceV10" xml:space="preserve">
<value>&lt;tr borde = "0" espaciadodecelda = "0" rellenodecelda = "0"&gt; &lt;td colspan = "3" estilo = "altura: 10px; línea-altura: 10px; fondo: #fff; relleno: 0; margen: 0 ; "&gt; &amp; nbsp; &lt;/ td&gt; &lt;/ tr&gt;
        &lt;tr borde = "0" espaciadodecelda = "0" rellenodecelda = "0"&gt;
@ -148,64 +118,6 @@
            &lt;/ td&gt;
        &lt;/ tr&gt;</value>
</data>
<data name="FooterPersonal" xml:space="preserve">
<value>&lt;table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;"&gt;Si tiene algunas preguntas, visite &lt;a href="http://helpcenter.onlyoffice.com/ru/ONLYOFFICE-Editors/index.aspx" target="_blank"&gt;sección FAQ&lt;/a&gt;. También ONLYOFFICE está disponible en &lt;a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank"&gt;Chrome Web Store&lt;/a&gt;.
&lt;/div&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div style="width: 170px;"&gt;
&lt;div style="font-size:22px; font-weight: bold; margin-bottom: 5px;"&gt;¡Dése cuenta!&lt;/div&gt;
&lt;div style="background-color: #b9e3f5; padding: 11px; margin: 0;"&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"&gt;&lt;img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"&gt;&lt;img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"&gt;&lt;img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&amp;gid=3159387&amp;trk=anet_ug_hm"&gt;&lt;img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</value>
</data>
<data name="FooterSocial" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="HtmlMaster" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
@ -246,85 +158,39 @@
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPersonal" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
&lt;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;"&gt;
&lt;tbody&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;"&gt;
&lt;div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;"&gt;
&lt;a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" /&gt;
&lt;/a&gt;
&lt;/div&gt;
<data name="SocialNetworksFooterV10" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;"&gt;
&lt;div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"&gt;&lt;/div&gt;
&lt;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;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
%FOOTER%
&lt;/tbody&gt;
&lt;/table&gt;
&lt;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; "&gt;
&lt;tbody&gt;
%FOOTERSOCIAL%
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;"&gt;
&lt;p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;"&gt;
%TEXTFOOTER%
&lt;/p&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://www.onlyoffice.com/blog" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-blog-40.png" alt="ONLYOFFICE" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPromo" xml:space="preserve">
<value>&lt;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;"&gt;
&lt;div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;"&gt;
&lt;div style="background-color: #fff; width: 600px; padding:0 40px;"&gt;
&lt;a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;"&gt;
&lt;img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div id="content"&gt;
&lt;table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="width: 600px; background-color: #fff; padding:0 40px;"&gt;
&lt;div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
%FOOTER%
&lt;table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;"&gt;
&lt;tr&gt;
&lt;td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;"&gt;
&lt;p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;"&gt;%TEXTFOOTER%&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
<value>Este correo electrónico se genera automáticamente y no es necesario responder.
&lt;br /&gt; Usted recibe este correo electrónico porque usted es un usuario registrado de &lt;a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank"&gt;onlyoffice.com&lt;/a&gt;
&lt;br /&gt; Si ya no desea recibir estos mensajes de correo electrónico, haga click en el siguiente enlace: &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;Unsubscribe&lt;/a&gt;
&lt;br /&gt;</value>
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="https://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
<value>Este correo electrónico se genera automáticamente y no es necesario responder.

View File

@ -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>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
@ -88,38 +88,8 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterFreeCloud" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;"&gt;
&lt;img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Нужна помощь?&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;"&gt;Вопросы по покупке&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Заказ демонстрации&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;"&gt;Задайте вопрос&lt;/a&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;"&gt;
&lt;a href="mailto:sales@onlyoffice.com" style="color: #333;"&gt;Напишите нам&lt;/a&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;"&gt;Отправьте запрос&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterOpensourceV10" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
@ -148,64 +118,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterPersonal" xml:space="preserve">
<value>&lt;table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;"&gt;Si vous avez des questions, passez à la section &lt;a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank"&gt;FAQ&lt;/a&gt;. ONLYOFFICE est aussi disponible sur &lt;a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank"&gt;Chrome Web Store&lt;/a&gt;.
&lt;/div&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div style="width: 170px;"&gt;
&lt;div style="font-size:22px; font-weight: bold; margin-bottom: 5px;"&gt;Restez avec nous !&lt;/div&gt;
&lt;div style="background-color: #b9e3f5; padding: 11px; margin: 0;"&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"&gt;&lt;img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"&gt;&lt;img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"&gt;&lt;img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&amp;gid=3159387&amp;trk=anet_ug_hm"&gt;&lt;img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</value>
</data>
<data name="FooterSocial" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="HtmlMaster" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
@ -246,88 +158,39 @@
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPersonal" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
&lt;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;"&gt;
&lt;tbody&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;"&gt;
&lt;div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;"&gt;
&lt;a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" /&gt;
&lt;/a&gt;
&lt;/div&gt;
<data name="SocialNetworksFooterV10" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;"&gt;
&lt;div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"&gt;&lt;/div&gt;
&lt;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;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
%FOOTER%
&lt;/tbody&gt;
&lt;/table&gt;
&lt;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; "&gt;
&lt;tbody&gt;
%FOOTERSOCIAL%
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;"&gt;
&lt;p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;"&gt;
%TEXTFOOTER%
&lt;/p&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://www.onlyoffice.com/blog" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-blog-40.png" alt="ONLYOFFICE" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPromo" xml:space="preserve">
<value>&lt;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;"&gt;
&lt;div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;"&gt;
&lt;div style="background-color: #fff; width: 600px; padding:0 40px;"&gt;
&lt;a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;"&gt;
&lt;img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div id="content"&gt;
&lt;table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="width: 600px; background-color: #fff; padding:0 40px;"&gt;
&lt;div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
%FOOTER%
&lt;table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;"&gt;
&lt;tr&gt;
&lt;td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;"&gt;
&lt;p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;"&gt;%TEXTFOOTER%&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</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.
&lt;br /&gt;Vous recevez ce courriel parce que vous êtes un utilisateur enregistré de &lt;a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank"&gt;onlyoffice.com&lt;/a&gt;
&lt;br /&gt;Si vous ne souhaitez plus recevoir ces e-mails, cliquez sur le lien suivant: &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;Se Désabonner&lt;/a&gt;
&lt;br /&gt;</value>
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="https://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</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.

View File

@ -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>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
@ -88,38 +88,8 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterFreeCloud" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;"&gt;
&lt;img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Serve aiuto?&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;"&gt;Domande sulle vendite&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Ordina la dimostrazione&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;"&gt;Send your question&lt;/a&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;"&gt;
&lt;a href="mailto:sales@onlyoffice.com" style="color: #333;"&gt;Email us&lt;/a&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;"&gt;invia una richiesta&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterOpensourceV10" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt; &lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
@ -148,64 +118,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterPersonal" xml:space="preserve">
<value>&lt;table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;"&gt;Prego andare su&lt;a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank"&gt;FAQ section&lt;/a&gt; se avete domande. ONLYOFFICE è inoltre disponibile in &lt;a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank"&gt;Chrome Web Store&lt;/a&gt;.
&lt;/div&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div style="width: 170px;"&gt;
&lt;div style="font-size:22px; font-weight: bold; margin-bottom: 5px;"&gt;Stay tuned!&lt;/div&gt;
&lt;div style="background-color: #b9e3f5; padding: 11px; margin: 0;"&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"&gt;&lt;img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"&gt;&lt;img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"&gt;&lt;img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&amp;gid=3159387&amp;trk=anet_ug_hm"&gt;&lt;img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</value>
</data>
<data name="FooterSocial" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="HtmlMaster" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
@ -244,80 +156,6 @@
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPersonal" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
&lt;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;"&gt;
&lt;tbody&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;"&gt;
&lt;div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;"&gt;
&lt;a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" /&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;"&gt;
&lt;div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"&gt;&lt;/div&gt;
&lt;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;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
%FOOTER%
&lt;/tbody&gt;
&lt;/table&gt;
&lt;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; "&gt;
&lt;tbody&gt;
%FOOTERSOCIAL%
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;"&gt;
&lt;p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;"&gt;
%TEXTFOOTER%
&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPromo" xml:space="preserve">
<value>&lt;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;"&gt;
&lt;div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;"&gt;
&lt;div style="background-color: #fff; width: 600px; padding:0 40px;"&gt;
&lt;a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;"&gt;
&lt;img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div id="content"&gt;
&lt;table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="width: 600px; background-color: #fff; padding:0 40px;"&gt;
&lt;div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
%FOOTER%
&lt;table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;"&gt;
&lt;tr&gt;
&lt;td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;"&gt;
&lt;p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;"&gt;%TEXTFOOTER%&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="SocialNetworksFooterV10" xml:space="preserve">
@ -354,12 +192,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
<value>Questo messaggio è stato generato automaticamente e non necessità di risposta.
&lt;br /&gt;Hai ricevuto questo messaggio in quanto sei un untente registrato su &lt;a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank"&gt;onlyoffice.com&lt;/a&gt;
&lt;br /&gt;Se non intendi ricevere altri messaggi in futuro, fai click qui: &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;Unsubscribe&lt;/a&gt;
&lt;br /&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
<value>Questo messaggio è stato generato automaticamente e non necessità di risposta.
&lt;br /&gt;Hai ricevuto questo messaggio in quanto sei un untente registrato su &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;{0}&lt;/a&gt;

View File

@ -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>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;"&gt;
&lt;img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Need tech help?&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;"&gt;Sales Questions&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Order Demo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;a href="%SUPPORTURL%" target="_blank" style="color: #333;"&gt;Send your question&lt;/a&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;"&gt;
&lt;a href="mailto:%SALESEMAIL%" style="color: #333;"&gt;Email us&lt;/a&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;a href="%DEMOURL%" target="_blank" style="color: #333;"&gt;Send a request&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterFreeCloud" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
@ -167,13 +78,13 @@
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;"&gt;Send your question&lt;/a&gt;
&lt;a href="%SUPPORTURL%" target="_blank" style="color: #333;"&gt;Send your question&lt;/a&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;"&gt;
&lt;a href="mailto:sales@onlyoffice.com" style="color: #333;"&gt;Email us&lt;/a&gt;
&lt;a href="mailto:%SALESEMAIL%" style="color: #333;"&gt;Email us&lt;/a&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;"&gt;Send a request&lt;/a&gt;
&lt;a href="%DEMOURL%" target="_blank" style="color: #333;"&gt;Send a request&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
@ -207,64 +118,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterPersonal" xml:space="preserve">
<value>&lt;table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;"&gt;Please, proceed to our &lt;a href="http://helpcenter.onlyoffice.com/ONLYOFFICE-Editors/index.aspx" target="_blank"&gt;FAQ section&lt;/a&gt; if you have any questions. ONLYOFFICE is also available in &lt;a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank"&gt;Chrome Web Store&lt;/a&gt;.
&lt;/div&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div style="width: 170px;"&gt;
&lt;div style="font-size:22px; font-weight: bold; margin-bottom: 5px;"&gt;Stay tuned!&lt;/div&gt;
&lt;div style="background-color: #b9e3f5; padding: 11px; margin: 0;"&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"&gt;&lt;img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"&gt;&lt;img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"&gt;&lt;img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&amp;gid=3159387&amp;trk=anet_ug_hm"&gt;&lt;img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</value>
</data>
<data name="FooterSocial" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="HtmlMaster" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
@ -303,80 +156,6 @@
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPersonal" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
&lt;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;"&gt;
&lt;tbody&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;"&gt;
&lt;div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;"&gt;
&lt;a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" /&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;"&gt;
&lt;div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"&gt;&lt;/div&gt;
&lt;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;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
%FOOTER%
&lt;/tbody&gt;
&lt;/table&gt;
&lt;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; "&gt;
&lt;tbody&gt;
%FOOTERSOCIAL%
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;"&gt;
&lt;p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;"&gt;
%TEXTFOOTER%
&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPromo" xml:space="preserve">
<value>&lt;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;"&gt;
&lt;div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;"&gt;
&lt;div style="background-color: #fff; width: 600px; padding:0 40px;"&gt;
&lt;a href="http://www.onlyoffice.com/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;"&gt;
&lt;img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div id="content"&gt;
&lt;table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="width: 600px; background-color: #fff; padding:0 40px;"&gt;
&lt;div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
%FOOTER%
&lt;table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;"&gt;
&lt;tr&gt;
&lt;td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;"&gt;
&lt;p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;"&gt;%TEXTFOOTER%&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="SocialNetworksFooterV10" xml:space="preserve">
@ -413,12 +192,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
<value>This email is generated automatically and you do not need to answer it.
&lt;br /&gt;You receive this email because you are a registered user of &lt;a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank"&gt;onlyoffice.com&lt;/a&gt;
&lt;br /&gt;If you no longer wish to receive these emails, click on the following link: &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;Unsubscribe&lt;/a&gt;
&lt;br /&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
<value>This email is generated automatically and you do not need to answer it.
&lt;br /&gt;You receive this email because you are a registered user of &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;{0}&lt;/a&gt;

View File

@ -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>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
@ -88,36 +88,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterFreeCloud" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/tech-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 34px 0 0 0; width: 180px; height: 108px; background: #f6f6f6;"&gt;
&lt;img src="%IMAGEPATH%/mailus-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;img src="%IMAGEPATH%/demo-100.png" style="width: 100px; height: 100px;margin: 0 auto;" /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Нужна помощь?&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 180px; height: 16px; background: #f6f6f6;"&gt;Вопросы по покупке&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0; width: 210px; height: 16px; background: #f6f6f6;"&gt;Заказ демонстрации&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;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;"&gt;
&lt;a href="http://cloud.onlyoffice.org/" target="_blank" style="color: #333;"&gt;Задайте вопрос&lt;/a&gt;
&lt;/td&gt;
&lt;td style="vertical-align: top; margin: 0; padding: 0 0 30px; width: 180px; background: #f6f6f6;"&gt;
&lt;a href="mailto:sales@onlyoffice.com" style="color: #333;"&gt;Напишите нам&lt;/a&gt;
&lt;/td&gt;
&lt;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;"&gt;
&lt;a href="http://www.onlyoffice.com/demo-order.aspx" target="_blank" style="color: #333;"&gt;Отправьте запрос&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterOpensourceV10" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;&lt;td colspan="3" style="height: 10px; line-height: 10px; background: #fff; padding: 0; margin: 0;"&gt;&amp;nbsp;&lt;/td&gt;&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
@ -148,64 +118,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="FooterPersonal" xml:space="preserve">
<value>&lt;table cellpadding="0" cellspacing="0" style="border-spacing: 0; empty-cells: show; color: #5e5e5e; width: 600px; margin:0 40px 54px; font-size: 14px;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;div style="width: 400px; margin-top: 30px; font-size: 14px; line-height:18px;"&gt;Если у вас есть какие-то вопросы, перейдите в &lt;a href="http://helpcenter.onlyoffice.com/ru/ONLYOFFICE-Editors/index.aspx" target="_blank"&gt;раздел FAQ&lt;/a&gt;. ONLYOFFICE также доступен в &lt;a href="https://chrome.google.com/webstore/detail/onlyoffice-personal/iohfebkcjhlelaoibebeohcgkohkcgpn?utm_source=chrome-ntp-icon" target="_blank"&gt;Chrome Web Store&lt;/a&gt;.
&lt;/div&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;div style="width: 170px;"&gt;
&lt;div style="font-size:22px; font-weight: bold; margin-bottom: 5px;"&gt;Будьте в курсе!&lt;/div&gt;
&lt;div style="background-color: #b9e3f5; padding: 11px; margin: 0;"&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://www.facebook.com/pages/OnlyOffice/833032526736775"&gt;&lt;img style="vertical-align: middle; border: 0; width: 26px; height: 26px;" src="%IMAGEPATH%/social_01.jpg" alt="Facebook" title="Facebook" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="http://twitter.com/ONLY_OFFICE"&gt;&lt;img src="%IMAGEPATH%/social_02.jpg" alt="Twitter" title="Twitter" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B; margin-right: 10px;" href="https://plus.google.com/u/0/+teamlab/"&gt;&lt;img src="%IMAGEPATH%/social_03.jpg" alt="Google+" title="Google+" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;a target="_blank" style="color: #3D4A6B;" href="http://www.linkedin.com/groups?home=&amp;gid=3159387&amp;trk=anet_ug_hm"&gt;&lt;img src="%IMAGEPATH%/social_04.jpg" alt="Linked In" title="Linked In" style="vertical-align: middle; border: 0; width: 26px; height: 26px;" /&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</value>
</data>
<data name="FooterSocial" xml:space="preserve">
<value>&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 155px;"&gt;
&lt;a href="https://www.facebook.com/pages/OnlyOffice/833032526736775" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-fb-40.png" alt="Facebook" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://twitter.com/ONLY_OFFICE" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-tw-40.png" alt="Twitter" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="https://plus.google.com/+Onlyoffice_Community" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-gplus-40.png" alt="Google+" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://www.youtube.com/user/onlyofficeTV" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-youtube-40.png" alt="YouTube" style="width: 40px; height: 40x;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 5px 0 5px;"&gt;
&lt;a href="http://vk.com/onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-vk-40.png" alt="VK" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;td style="width: 40px; vertical-align: top; margin: 0; padding: 22px 155px 0 5px;"&gt;
&lt;a href="http://www.instagram.com/the_onlyoffice" style="width: 40px; height: 40px; display: block; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%IMAGEPATH%/social-inst-40.png" alt="Instagram" style="width: 40px; height: 40px;" /&gt;
&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="HtmlMaster" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
@ -244,80 +156,6 @@
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPersonal" xml:space="preserve">
<value>&lt;body style="margin: 0; padding: 0; text-align: center; width: 100%; font-family: Arial, sans-serif; font-size: 14px; color: #333;"&gt;
&lt;div style="background-color: #fff; width: 600px; margin: 0 auto; text-align: left;"&gt;
&lt;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;"&gt;
&lt;tbody&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #fff; height: 65px; padding: 20px 0 0 30px;"&gt;
&lt;div style="text-align: left; height: 65px; width: 570px; margin: 0; padding: 0;"&gt;
&lt;a href="%SITEURL%" style="text-decoration: none; display: inline-block; width: 216px; height: 35px; margin: 0; padding: 0;" target="_blank"&gt;
&lt;img src="%LOGO%" style="border: 0px none; width: 216px; height: 35px; margin: 0; padding: 0;" alt="%LOGOTEXT%" /&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="3" style="margin: 0; padding: 0; background-color: #f6f6f6; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px;"&gt;
&lt;div style="width: 600px; height: 340px; background: url('%IMAGEPATH%/personal-wellcome-bg.png') 0 0 no-repeat; margin: 0; padding: 0; border: 0 none;"&gt;&lt;/div&gt;
&lt;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;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
%FOOTER%
&lt;/tbody&gt;
&lt;/table&gt;
&lt;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; "&gt;
&lt;tbody&gt;
%FOOTERSOCIAL%
&lt;tr border="0" cellspacing="0" cellpadding="0"&gt;
&lt;td colspan="6" style="width: 600px; vertical-align: top; margin: 0; padding: 20px 30px 15px;"&gt;
&lt;p style="color: #7b7b7b; font-family: Arial, Tahoma; font-size: 12px; margin: 0; padding: 0; text-align: center; width: 540px;"&gt;
%TEXTFOOTER%
&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="HtmlMasterPromo" xml:space="preserve">
<value>&lt;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;"&gt;
&lt;div id="common_style" style="background-color: #fff; width: 680px; margin: 20px auto; text-align: left;"&gt;
&lt;div style="background-color: #fff; width: 600px; padding:0 40px;"&gt;
&lt;a href="http://www.onlyoffice.com/ru/" style="text-decoration: none; margin: 0; padding: 0; display: block; width: 199px;"&gt;
&lt;img alt="ONLYOFFICE" style="border: 0; margin: 0; padding: 0; color: #5e5e5e; font-size: 28px; width: 199px; height: 72px; display: block;" src=%LOGO%&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;div id="content"&gt;
&lt;table cellpadding="0" cellspacing="0" style="margin: 0; border-spacing: 0; empty-cells: show; word-wrap: break-word;"&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="width: 600px; background-color: #fff; padding:0 40px;"&gt;
&lt;div style="font-family: Arial; font-size: 14px; color: #5e5e5e; background-color: #fff; padding: 40px 0; width: 600px; overflow: hidden; word-wrap: break-word;"&gt;
%CONTENT%
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
%FOOTER%
&lt;table cellpadding="0" cellspacing="0" style="background-color: #5e5e5e; margin: 0; padding: 0; border: 0; border-collapse: collapse; empty-cells: show; border-spacing: 0;"&gt;
&lt;tr&gt;
&lt;td style="font-family: Arial; width: 680px; height: 108px; font-size: 11px; vertical-align: middle; margin: 0; padding: 0;"&gt;
&lt;p style="font-family: Arial; font-size: 11px; color: #fff; margin: 11px 40px 10px; text-align: center; line-height: 20px;"&gt;%TEXTFOOTER%&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;</value>
</data>
<data name="SocialNetworksFooterV10" xml:space="preserve">
@ -354,12 +192,6 @@
&lt;/td&gt;
&lt;/tr&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribe" xml:space="preserve">
<value>Это сообщение создано автоматически, и отвечать на него не нужно.
&lt;br /&gt;Вы получили это сообщение, так как являетесь зарегистрированным пользователем &lt;a href="http://www.onlyoffice.com/" style="color: #7b7b7b;" target="_blank"&gt;onlyoffice.com&lt;/a&gt;
&lt;br /&gt;Если вы больше не хотите получать эти сообщения, нажмите на следующую ссылку: &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;Отписаться&lt;/a&gt;
&lt;br /&gt;</value>
</data>
<data name="TextForFooterWithUnsubscribeLink" xml:space="preserve">
<value>Это сообщение создано автоматически, и отвечать на него не нужно.
&lt;br /&gt;Вы получили это сообщение, так как являетесь зарегистрированным пользователем &lt;a href="{0}" style="color: #7b7b7b;" target="_blank"&gt;{0}&lt;/a&gt;

View File

@ -0,0 +1 @@
web: npm start

View 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
View 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`);
});

View 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 };

View 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);
};

View 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;

View 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;

View 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)));
});
};

View 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>

View 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>

View 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>

View 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();

View 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);

View 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();

View 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
};
};

View 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"
}
}

View 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;

View 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>

View 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>

View 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>

View 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"
}
}
}

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View 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>

View File

@ -0,0 +1 @@
{{message}}

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{title}}</title>
</head>
<body oncontextmenu="return false;">
{{{body}}}
</body>
</html>

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}

View File

@ -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