From aa43809b1878ceb684e5c729b0c710daf58583a0 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 21:54:13 +0800 Subject: [PATCH 01/85] fix: harden directory containment checks --- src/SSCMS/Utils/DirectoryUtils.cs | 20 ++++++++++++++++---- tests/SSCMS.Tests/TestDirectoryUtils.cs | 10 ++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/SSCMS/Utils/DirectoryUtils.cs b/src/SSCMS/Utils/DirectoryUtils.cs index f4e39a2f6..563facb1b 100644 --- a/src/SSCMS/Utils/DirectoryUtils.cs +++ b/src/SSCMS/Utils/DirectoryUtils.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; @@ -121,10 +122,21 @@ public static bool IsInDirectory(string parentDirectoryPath, string path) { if (string.IsNullOrEmpty(parentDirectoryPath) || string.IsNullOrEmpty(path)) return false; - parentDirectoryPath = StringUtils.ToLower(parentDirectoryPath.Trim().TrimEnd(Path.DirectorySeparatorChar)); - path = StringUtils.ToLower(path.Trim().TrimEnd(Path.DirectorySeparatorChar)); + try + { + parentDirectoryPath = Path.GetFullPath(parentDirectoryPath.Trim()) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + path = Path.GetFullPath(path.Trim()) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + catch + { + return false; + } - return parentDirectoryPath == path || path.StartsWith(parentDirectoryPath); + return string.Equals(parentDirectoryPath, path, StringComparison.OrdinalIgnoreCase) || + path.StartsWith(parentDirectoryPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || + path.StartsWith(parentDirectoryPath + Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); } public static void MoveDirectory(string srcDirectoryPath, string destDirectoryPath, bool isOverride) diff --git a/tests/SSCMS.Tests/TestDirectoryUtils.cs b/tests/SSCMS.Tests/TestDirectoryUtils.cs index cadc98dfe..ac88c25ee 100644 --- a/tests/SSCMS.Tests/TestDirectoryUtils.cs +++ b/tests/SSCMS.Tests/TestDirectoryUtils.cs @@ -21,5 +21,15 @@ public void TestGetParentPath() var testsDirectoryPath = DirectoryUtils.GetParentPath(binDirectoryPath, 2); Assert.Equal("tests", PathUtils.GetDirectoryName(testsDirectoryPath, false), StringComparer.OrdinalIgnoreCase); } + + [Fact] + public void TestIsInDirectoryDoesNotAllowSiblingPrefix() + { + var parentDirectoryPath = Path.Combine(Path.GetTempPath(), "sscms-upload"); + var siblingDirectoryPath = Path.Combine(Path.GetTempPath(), "sscms-upload-malicious"); + var siblingFilePath = Path.Combine(siblingDirectoryPath, "payload.aspx"); + + Assert.False(DirectoryUtils.IsInDirectory(parentDirectoryPath, siblingFilePath)); + } } } From e2aab77cd9fced2c6fda9678e07f8f3ecfe6d225 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 21:55:44 +0800 Subject: [PATCH 02/85] fix: harden default web security settings --- src/SSCMS.Web/Startup.cs | 49 +++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/SSCMS.Web/Startup.cs b/src/SSCMS.Web/Startup.cs index eba6717b9..5ad5e0ad2 100644 --- a/src/SSCMS.Web/Startup.cs +++ b/src/SSCMS.Web/Startup.cs @@ -59,32 +59,27 @@ public void ConfigureServices(IServiceCollection services) var settingsManager = services.AddSettingsManager(_config, _env.ContentRootPath, _env.WebRootPath, entryAssembly); var pluginManager = services.AddPlugins(_config, settingsManager); - if (settingsManager.CorsIsOrigins) - { - services.AddCors(options => - { - options.AddPolicy(CorsPolicy, - builder => builder - .AllowAnyMethod() - .AllowAnyHeader() - .WithOrigins(settingsManager.CorsOrigins) - .AllowCredentials() - ); - }); - } - else + var corsOrigins = (settingsManager.CorsOrigins ?? Array.Empty()) + .Where(origin => !string.IsNullOrWhiteSpace(origin)) + .Select(origin => origin.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + services.AddCors(options => { - services.AddCors(options => + options.AddPolicy(CorsPolicy, builder => { - options.AddPolicy(CorsPolicy, - builder => builder - .AllowAnyMethod() - .AllowAnyHeader() - .SetIsOriginAllowed(x => true) - .AllowCredentials() - ); + builder.AllowAnyMethod().AllowAnyHeader(); + + if (settingsManager.CorsIsOrigins && corsOrigins.Length > 0) + { + builder.WithOrigins(corsOrigins).AllowCredentials(); + return; + } + + builder.SetIsOriginAllowed(_ => true); }); - } + }); services.AddHttpContextAccessor(); @@ -131,9 +126,10 @@ public void ConfigureServices(IServiceCollection services) // { // options.MultipartBodyLengthLimit = 524288000;//500MB // }); - services.Configure(x => { + services.Configure(x => + { x.ValueLengthLimit = int.MaxValue; - x.MultipartBodyLengthLimit = long.MaxValue; // In case of multipart + x.MultipartBodyLengthLimit = 104857600; // 100MB }); services.AddHealthChecks(); @@ -244,9 +240,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISetting } else { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; result = TranslateUtils.JsonSerialize(new { - exception.Message, + Message = "服务器内部错误,请稍后重试", CreatedDate = DateTime.Now }); } From 8f89194faa57def0c36e216896dd01e5401ca577 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 21:56:51 +0800 Subject: [PATCH 03/85] fix: require authorization for database sync --- .../Controllers/Admin/SyncDatabaseController.Get.cs | 8 ++++---- .../Controllers/Admin/SyncDatabaseController.Submit.cs | 9 ++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Get.cs b/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Get.cs index e1d29f009..d1b1191fc 100644 --- a/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Get.cs +++ b/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Get.cs @@ -16,10 +16,10 @@ public async Task> Get() var config = await _configRepository.GetAsync(); - //if (config.DatabaseVersion == _settingsManager.Version && !await _authManager.IsSuperAdminAsync()) - //{ - // return Unauthorized(); - //} + if (config.DatabaseVersion == _settingsManager.Version && !await _authManager.IsSuperAdminAsync()) + { + return Unauthorized(); + } return new GetResult { diff --git a/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Submit.cs index ffde68ca5..0e927b0c2 100644 --- a/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/SyncDatabaseController.Submit.cs @@ -9,14 +9,9 @@ public partial class SyncDatabaseController [HttpPost, Route(Route)] public async Task> Submit([FromBody] SubmitRequest request) { - var config = await _configRepository.GetAsync(); - - if (config.DatabaseVersion == _settingsManager.Version) + if (request == null || !string.Equals(_settingsManager.SecurityKey, request.SecurityKey)) { - if (_settingsManager.SecurityKey != request.SecurityKey) - { - return this.Error("SecurityKey 输入错误!"); - } + return this.Error("SecurityKey 输入错误!"); } await _databaseManager.SyncDatabaseAsync(); From 000f2f469b76f6e777c3669359e009867739a973 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:01:19 +0800 Subject: [PATCH 04/85] fix: prevent admin password reset enumeration --- .../Admin/LostPasswordController.SendSms.cs | 33 ++++++------ .../Admin/LostPasswordController.cs | 42 ++++++++++++++- .../Admin/LostPasswordControllerTests.cs | 53 +++++++++++++++++++ 3 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/LostPasswordController.SendSms.cs b/src/SSCMS.Web/Controllers/Admin/LostPasswordController.SendSms.cs index 7f463a1e9..cb194195a 100644 --- a/src/SSCMS.Web/Controllers/Admin/LostPasswordController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/Admin/LostPasswordController.SendSms.cs @@ -14,30 +14,33 @@ public partial class LostPasswordController [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> SendSms([FromBody] SendSmsRequest request) { - var administrator = await _administratorRepository.GetByMobileAsync(request.Mobile); - - if (administrator == null) + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) { - return this.Error("此手机号码未关联管理员,请更换手机号码"); + return this.Error("请输入有效的手机号码"); } - var (success, errorMessage) = await _administratorRepository.ValidateLockAsync(administrator); - if (!success) + var mobile = request.Mobile.Trim(); + if (!TryConsumeSendSmsQuota(mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) { - return this.Error(errorMessage); + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); } - var code = StringUtils.GetRandomInt(100000, 999999); - (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.ChangePassword, code); - if (!success) + var administrator = await _administratorRepository.GetByMobileAsync(mobile); + if (administrator != null) { - return this.Error(errorMessage); + var (success, _) = await _administratorRepository.ValidateLockAsync(administrator); + if (success) + { + var code = StringUtils.GetRandomInt(100000, 999999); + (success, _) = await _smsManager.SendSmsAsync(mobile, SmsCodeType.ChangePassword, code); + if (success) + { + var cacheKey = GetSmsCodeCacheKey(mobile); + _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); + } + } } - var cacheKey = GetSmsCodeCacheKey(request.Mobile); - _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); - return new BoolResult { Value = true diff --git a/src/SSCMS.Web/Controllers/Admin/LostPasswordController.cs b/src/SSCMS.Web/Controllers/Admin/LostPasswordController.cs index dca80dbd2..0164a4963 100644 --- a/src/SSCMS.Web/Controllers/Admin/LostPasswordController.cs +++ b/src/SSCMS.Web/Controllers/Admin/LostPasswordController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; using SSCMS.Core.Utils; @@ -44,5 +45,42 @@ private string GetSmsCodeCacheKey(string mobile) { return CacheUtils.GetClassKey(typeof(LostPasswordController), nameof(Administrator), mobile); } + + private const int DefaultSendSmsMaxCount = 5; + private const int DefaultSendSmsWindowMinutes = 10; + + private class SendSmsRateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + private static string GetSendSmsRateLimitCacheKey(string mobile, string ipAddress) + { + return CacheUtils.GetClassKey(typeof(LostPasswordController), "SendSmsRate", + (mobile ?? string.Empty).Trim(), ipAddress ?? "unknown"); + } + + private bool TryConsumeSendSmsQuota(string mobile, string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetSendSmsRateLimitCacheKey(mobile, ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new SendSmsRateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(DefaultSendSmsWindowMinutes) + }; + } + + state.Count++; + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, DefaultSendSmsWindowMinutes); + if (state.Count <= DefaultSendSmsMaxCount) return true; + + retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } } -} \ No newline at end of file +} diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs new file mode 100644 index 000000000..5ff30b12c --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Dto; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin +{ + public class LostPasswordControllerTests + { + [Fact] + public async Task SendSmsReturnsSuccessWithoutSendingSmsForUnknownMobile() + { + var authManager = new Mock(); + var cacheManager = new Mock(); + var smsManager = new Mock(); + var administratorRepository = new Mock(); + administratorRepository + .Setup(x => x.GetByMobileAsync("13800000000")) + .ReturnsAsync(() => null); + + var controller = new LostPasswordController( + authManager.Object, + cacheManager.Object, + smsManager.Object, + administratorRepository.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + var result = await controller.SendSms(new LostPasswordController.SendSmsRequest + { + Mobile = "13800000000" + }); + + var value = Assert.IsType(result.Value); + Assert.True(value.Value); + smsManager.Verify( + x => x.SendSmsAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + cacheManager.Verify( + x => x.AddOrUpdateAbsolute(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + } +} From dbb4b92e385bdc3036a9f84f1103e5ef3719ff3e Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:05:39 +0800 Subject: [PATCH 05/85] fix: rate limit anonymous hits updates --- .../Stl/ActionsHitsController.Submit.cs | 11 ++- .../Controllers/Stl/ActionsHitsController.cs | 45 ++++++++- .../Stl/ActionsHitsControllerTests.cs | 96 +++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Stl/ActionsHitsControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.Submit.cs b/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.Submit.cs index b78b6b987..bdb63b815 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.Submit.cs @@ -12,7 +12,16 @@ public partial class ActionsHitsController [HttpPost, Route(Constants.RouteStlActionsHits)] public async Task> Submit([FromBody] SubmitRequest request) { - + if (request == null) + { + return this.Error(Constants.ErrorNotFound); + } + + if (!TryConsumeRequestQuota(PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + try { var hits = await _contentRepository.GetHitsAsync(request.SiteId, request.ChannelId, request.ContentId); diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.cs index d71e0ae58..ec8026b1e 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsHitsController.cs @@ -1,7 +1,10 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Repositories; +using SSCMS.Services; namespace SSCMS.Web.Controllers.Stl { @@ -9,11 +12,16 @@ namespace SSCMS.Web.Controllers.Stl [Route(Constants.ApiPrefix + Constants.ApiStlPrefix)] public partial class ActionsHitsController : ControllerBase { + private const int RateLimitWindowMinutes = 1; + private const int RateLimitMaxRequests = 60; + private readonly IContentRepository _contentRepository; + private readonly ICacheManager _cacheManager; - public ActionsHitsController(IContentRepository contentRepository) + public ActionsHitsController(IContentRepository contentRepository, ICacheManager cacheManager) { _contentRepository = contentRepository; + _cacheManager = cacheManager; } public class SubmitRequest @@ -23,5 +31,38 @@ public class SubmitRequest public int ContentId { get; set; } public bool AutoIncrease { get; set; } } + + private class RateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + private static string GetRateLimitCacheKey(string ipAddress) + { + return CacheUtils.GetClassKey(typeof(ActionsHitsController), "Rate", ipAddress ?? "unknown"); + } + + private bool TryConsumeRequestQuota(string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetRateLimitCacheKey(ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new RateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(RateLimitWindowMinutes) + }; + } + + state.Count++; + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, RateLimitWindowMinutes); + if (state.Count <= RateLimitMaxRequests) return true; + + retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsHitsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsHitsControllerTests.cs new file mode 100644 index 000000000..ec3feaafa --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsHitsControllerTests.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Stl; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Stl +{ + public class ActionsHitsControllerTests + { + [Fact] + public async Task SubmitRateLimitsRepeatedAnonymousRequests() + { + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.GetHitsAsync(1, 2, 3)) + .ReturnsAsync(10); + + var controller = new ActionsHitsController(contentRepository.Object, new TestCacheManager()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + ActionResult lastResult = null; + for (var i = 0; i < 61; i++) + { + var result = await controller.Submit(new ActionsHitsController.SubmitRequest + { + SiteId = 1, + ChannelId = 2, + ContentId = 3, + AutoIncrease = true + }); + lastResult = result.Result; + } + + Assert.IsType(lastResult); + contentRepository.Verify(x => x.UpdateHitsAsync(1, 2, 3, 11), Times.Exactly(60)); + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } + } +} From 7b78e09203f1b2ad100506757cdaa08c80afc60e Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:08:22 +0800 Subject: [PATCH 06/85] fix: prevent admin login sms enumeration --- .../Admin/LoginController.SendSms.cs | 33 ++++++----- .../Controllers/Admin/LoginController.cs | 42 +++++++++++++- .../Controllers/Admin/LoginControllerTests.cs | 58 +++++++++++++++++++ 3 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/LoginController.SendSms.cs b/src/SSCMS.Web/Controllers/Admin/LoginController.SendSms.cs index b6fb96aa0..6f4adafd8 100644 --- a/src/SSCMS.Web/Controllers/Admin/LoginController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/Admin/LoginController.SendSms.cs @@ -14,30 +14,33 @@ public partial class LoginController [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> SendSms([FromBody] SendSmsRequest request) { - var administrator = await _administratorRepository.GetByMobileAsync(request.Mobile); - - if (administrator == null) + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) { - return this.Error("此手机号码未关联管理员,请更换手机号码"); + return this.Error("请输入有效的手机号码"); } - var (success, errorMessage) = await _administratorRepository.ValidateLockAsync(administrator); - if (!success) + var mobile = request.Mobile.Trim(); + if (!TryConsumeSendSmsQuota(mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) { - return this.Error(errorMessage); + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); } - var code = StringUtils.GetRandomInt(100000, 999999); - (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.LoginConfirmation, code); - if (!success) + var administrator = await _administratorRepository.GetByMobileAsync(mobile); + if (administrator != null) { - return this.Error(errorMessage); + var (success, _) = await _administratorRepository.ValidateLockAsync(administrator); + if (success) + { + var code = StringUtils.GetRandomInt(100000, 999999); + (success, _) = await _smsManager.SendSmsAsync(mobile, SmsCodeType.LoginConfirmation, code); + if (success) + { + var cacheKey = GetSmsCodeCacheKey(mobile); + _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); + } + } } - var cacheKey = GetSmsCodeCacheKey(request.Mobile); - _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); - return new BoolResult { Value = true diff --git a/src/SSCMS.Web/Controllers/Admin/LoginController.cs b/src/SSCMS.Web/Controllers/Admin/LoginController.cs index e7bd2d054..79ff8f7aa 100644 --- a/src/SSCMS.Web/Controllers/Admin/LoginController.cs +++ b/src/SSCMS.Web/Controllers/Admin/LoginController.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; @@ -86,6 +87,43 @@ private string GetSmsCodeCacheKey(string mobile) return CacheUtils.GetClassKey(typeof(LoginController), nameof(Administrator), mobile); } + private const int DefaultSendSmsMaxCount = 5; + private const int DefaultSendSmsWindowMinutes = 10; + + private class SendSmsRateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + private static string GetSendSmsRateLimitCacheKey(string mobile, string ipAddress) + { + return CacheUtils.GetClassKey(typeof(LoginController), "SendSmsRate", + (mobile ?? string.Empty).Trim(), ipAddress ?? "unknown"); + } + + private bool TryConsumeSendSmsQuota(string mobile, string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetSendSmsRateLimitCacheKey(mobile, ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new SendSmsRateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(DefaultSendSmsWindowMinutes) + }; + } + + state.Count++; + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, DefaultSendSmsWindowMinutes); + if (state.Count <= DefaultSendSmsMaxCount) return true; + + retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } + private async Task AdminRedirectCheckAsync() { var redirect = false; @@ -108,4 +146,4 @@ private async Task AdminRedirectCheckAsync() return redirect ? redirectUrl : null; } } -} \ No newline at end of file +} diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs new file mode 100644 index 000000000..3e442a1ff --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Dto; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin +{ + public class LoginControllerTests + { + [Fact] + public async Task SendSmsReturnsSuccessWithoutSendingSmsForUnknownMobile() + { + var smsManager = new Mock(); + var cacheManager = new Mock(); + var administratorRepository = new Mock(); + administratorRepository + .Setup(x => x.GetByMobileAsync("13800000000")) + .ReturnsAsync(() => null); + + var controller = new LoginController( + Mock.Of(), + Mock.Of(), + Mock.Of(), + cacheManager.Object, + smsManager.Object, + Mock.Of(), + administratorRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + var result = await controller.SendSms(new LoginController.SendSmsRequest + { + Mobile = "13800000000" + }); + + var value = Assert.IsType(result.Value); + Assert.True(value.Value); + smsManager.Verify( + x => x.SendSmsAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + cacheManager.Verify( + x => x.AddOrUpdateAbsolute(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + } +} From bbee404203039f25afea5e74c3263998e0a05c59 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:09:55 +0800 Subject: [PATCH 07/85] fix: invalidate password reset sms codes --- .../Admin/LostPasswordController.Submit.cs | 1 + .../Admin/LostPasswordControllerTests.cs | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/SSCMS.Web/Controllers/Admin/LostPasswordController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/LostPasswordController.Submit.cs index 86f878c66..842e8a99d 100644 --- a/src/SSCMS.Web/Controllers/Admin/LostPasswordController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/LostPasswordController.Submit.cs @@ -34,6 +34,7 @@ public async Task> Submit([FromBody] SubmitRequest requ return this.Error($"更改密码失败:{errorMessage}"); } + _cacheManager.Remove(codeCacheKey); await _authManager.AddAdminLogAsync("重设管理员密码", $"管理员:{administrator.UserName}"); return new BoolResult diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs index 5ff30b12c..e7e27b3c8 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/LostPasswordControllerTests.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Moq; using SSCMS.Dto; +using SSCMS.Models; using SSCMS.Repositories; using SSCMS.Services; using SSCMS.Web.Controllers.Admin; @@ -49,5 +50,46 @@ public async Task SendSmsReturnsSuccessWithoutSendingSmsForUnknownMobile() x => x.AddOrUpdateAbsolute(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + + [Fact] + public async Task SubmitRemovesSmsCodeAfterSuccessfulPasswordReset() + { + var authManager = new Mock(); + var cacheManager = new Mock(); + cacheManager + .Setup(x => x.Get(It.IsAny())) + .Returns(123456); + + var smsManager = new Mock(); + var administratorRepository = new Mock(); + var administrator = new Administrator + { + UserName = "admin", + Mobile = "13800000000" + }; + administratorRepository + .Setup(x => x.GetByMobileAsync("13800000000")) + .ReturnsAsync(administrator); + administratorRepository + .Setup(x => x.ChangePasswordAsync(administrator, "new-password")) + .ReturnsAsync((true, string.Empty)); + + var controller = new LostPasswordController( + authManager.Object, + cacheManager.Object, + smsManager.Object, + administratorRepository.Object); + + var result = await controller.Submit(new LostPasswordController.SubmitRequest + { + Mobile = "13800000000", + Code = "123456", + Password = "new-password" + }); + + var value = Assert.IsType(result.Value); + Assert.True(value.Value); + cacheManager.Verify(x => x.Remove(It.IsAny()), Times.Once); + } } } From bc6c02fbb6a65c46afd1f14f5c10f6ee180ad22d Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:12:34 +0800 Subject: [PATCH 08/85] fix: restore TLS certificate validation --- src/SSCMS/Utils/RestUtils.cs | 38 ------------------------------------ 1 file changed, 38 deletions(-) diff --git a/src/SSCMS/Utils/RestUtils.cs b/src/SSCMS/Utils/RestUtils.cs index 85a934721..5b4556ad9 100644 --- a/src/SSCMS/Utils/RestUtils.cs +++ b/src/SSCMS/Utils/RestUtils.cs @@ -15,9 +15,6 @@ private class InternalServerError public static async Task<(bool success, TResult result, string failureMessage)> GetAsync(string url, string accessToken = null) where TResult : class { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -43,9 +40,6 @@ private class InternalServerError public static async Task<(bool success, string result, string errorMessage)> GetStringAsync(string url) { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -67,9 +61,6 @@ private class InternalServerError public static async Task<(bool success, string result, string errorMessage)> PostStringAsync(string url, string body) { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -92,10 +83,6 @@ private class InternalServerError public static async Task<(bool success, TResult result, string failureMessage)> PostAsync(string url, TRequest body, string accessToken = null) where TResult : class { - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -122,9 +109,6 @@ private class InternalServerError public static async Task<(bool success, string failureMessage)> PostAsync(string url, TRequest body, string accessToken = null) where TRequest : class { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -151,9 +135,6 @@ private class InternalServerError public static async Task<(bool success, string failureMessage)> PostAsync(string url, string accessToken = null) { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -180,10 +161,6 @@ private class InternalServerError public static async Task<(bool success, TResult result, string failureMessage)> PostAsync(string url, string accessToken = null) where TResult : class { - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -210,9 +187,6 @@ private class InternalServerError string filePath, string accessToken) where TResult : class { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -242,9 +216,6 @@ private class InternalServerError string filePath, string accessToken) { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -274,9 +245,6 @@ private class InternalServerError string filePath, string accessToken = null) { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient(url); var request = new RestRequest { @@ -304,9 +272,6 @@ private class InternalServerError public static async Task DownloadAsync(string url, string filePath) { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - FileUtils.DeleteFileIfExists(filePath); FileUtils.WriteText(filePath, string.Empty); using (var writer = File.OpenWrite(filePath)) @@ -324,9 +289,6 @@ public static async Task DownloadAsync(string url, string filePath) public static async Task GetIpAddressAsync() { - ServicePointManager.ServerCertificateValidationCallback += - (sender, certificate, chain, errors) => true; - var client = new RestClient("https://api.ipify.org/?format=text"); var request = new RestRequest { From 85033df5abd2a154c392a9cc6e17feac8261667c Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:19:04 +0800 Subject: [PATCH 09/85] fix: update vulnerable package resolutions --- src/Datory/Datory.csproj | 4 +++- src/SSCMS.Core/SSCMS.Core.csproj | 11 +++++++++-- src/SSCMS/SSCMS.csproj | 6 +++++- tests/Datory.Tests/Datory.Tests.csproj | 3 +++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Datory/Datory.csproj b/src/Datory/Datory.csproj index 60fe73530..b43d33ab7 100644 --- a/src/Datory/Datory.csproj +++ b/src/Datory/Datory.csproj @@ -23,6 +23,7 @@ + @@ -31,8 +32,9 @@ + - \ No newline at end of file + diff --git a/src/SSCMS.Core/SSCMS.Core.csproj b/src/SSCMS.Core/SSCMS.Core.csproj index 31a15690f..762fc3d48 100644 --- a/src/SSCMS.Core/SSCMS.Core.csproj +++ b/src/SSCMS.Core/SSCMS.Core.csproj @@ -21,10 +21,11 @@ - + + - + @@ -37,6 +38,12 @@ --> + + + + + + diff --git a/src/SSCMS/SSCMS.csproj b/src/SSCMS/SSCMS.csproj index 50a3020c1..c300a4396 100644 --- a/src/SSCMS/SSCMS.csproj +++ b/src/SSCMS/SSCMS.csproj @@ -31,7 +31,11 @@ - + + + + + diff --git a/tests/Datory.Tests/Datory.Tests.csproj b/tests/Datory.Tests/Datory.Tests.csproj index 2d6b5d6e2..08130c46d 100644 --- a/tests/Datory.Tests/Datory.Tests.csproj +++ b/tests/Datory.Tests/Datory.Tests.csproj @@ -15,6 +15,9 @@ + + + all From b71fee55e8c52bd8674feafc2d4bee71c0da05bc Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:23:35 +0800 Subject: [PATCH 10/85] fix: block private hosts in remote downloads --- src/SSCMS/Utils/HttpClientUtils.cs | 178 +++++++++++++++++++---- tests/SSCMS.Tests/TestHttpClientUtils.cs | 23 +++ 2 files changed, 174 insertions(+), 27 deletions(-) create mode 100644 tests/SSCMS.Tests/TestHttpClientUtils.cs diff --git a/src/SSCMS/Utils/HttpClientUtils.cs b/src/SSCMS/Utils/HttpClientUtils.cs index be034a2fb..63573d074 100644 --- a/src/SSCMS/Utils/HttpClientUtils.cs +++ b/src/SSCMS/Utils/HttpClientUtils.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -8,6 +10,8 @@ namespace SSCMS.Utils { public static class HttpClientUtils { + private const int MaxRedirects = 5; + public static async Task GetStringAsync(string url) { return await GetStringAsync(url, Encoding.UTF8); @@ -17,25 +21,9 @@ public static async Task GetStringAsync(string url, Encoding encoding) { try { - string html; - - if (encoding == Encoding.UTF8) - { - using (var client = new HttpClient()) - { - html = await client.GetStringAsync(url); - } - } - else - { - using (var client = new HttpClient()) - { - var bytes = await client.GetByteArrayAsync(url); - html = ConvertBytesToString(bytes, encoding); - } - } - - return html; + var uri = await ValidatePublicHttpUrlAsync(url); + var bytes = await GetByteArrayAsync(uri); + return encoding == Encoding.UTF8 ? Encoding.UTF8.GetString(bytes) : ConvertBytesToString(bytes, encoding); } catch (Exception ex) { @@ -55,18 +43,13 @@ public static async Task DownloadAsync(string remoteUrl, string filePath) { try { + var uri = await ValidatePublicHttpUrlAsync(remoteUrl); DirectoryUtils.CreateDirectoryIfNotExists(filePath); FileUtils.DeleteFileIfExists(filePath); - using (var client = new HttpClient()) + using (var fs = new FileStream(filePath, FileMode.CreateNew)) { - using (var stream = await client.GetStreamAsync(remoteUrl)) - { - using (var fs = new FileStream(filePath, FileMode.CreateNew)) - { - await stream.CopyToAsync(fs); - } - } + await DownloadToStreamAsync(uri, fs); } // using var client = new WebClient(); @@ -78,5 +61,146 @@ public static async Task DownloadAsync(string remoteUrl, string filePath) } return true; } + + public static async Task ValidatePublicHttpUrlAsync(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || + (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) + { + throw new ArgumentException("Only absolute HTTP(S) URLs are allowed.", nameof(url)); + } + + if (IPAddress.TryParse(uri.Host, out var address)) + { + if (!IsPublicIPAddress(address)) + { + throw new ArgumentException("Private, loopback, link-local, and reserved IP addresses are not allowed.", nameof(url)); + } + + return uri; + } + + var addresses = await Dns.GetHostAddressesAsync(uri.IdnHost); + if (addresses.Length == 0 || addresses.Any(address => !IsPublicIPAddress(address))) + { + throw new ArgumentException("The URL host must resolve only to public IP addresses.", nameof(url)); + } + + return uri; + } + + private static async Task GetByteArrayAsync(Uri uri) + { + for (var i = 0; i <= MaxRedirects; i++) + { + uri = await ValidatePublicHttpUrlAsync(uri.ToString()); + + using (var client = CreateHttpClient()) + using (var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)) + { + if (TryGetRedirectUri(uri, response, out var redirectUri)) + { + uri = redirectUri; + continue; + } + + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsByteArrayAsync(); + } + } + + throw new HttpRequestException("Too many redirects."); + } + + private static async Task DownloadToStreamAsync(Uri uri, Stream output) + { + for (var i = 0; i <= MaxRedirects; i++) + { + uri = await ValidatePublicHttpUrlAsync(uri.ToString()); + + using (var client = CreateHttpClient()) + using (var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead)) + { + if (TryGetRedirectUri(uri, response, out var redirectUri)) + { + uri = redirectUri; + continue; + } + + response.EnsureSuccessStatusCode(); + using (var stream = await response.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(output); + } + return; + } + } + + throw new HttpRequestException("Too many redirects."); + } + + private static HttpClient CreateHttpClient() + { + return new HttpClient(new HttpClientHandler + { + AllowAutoRedirect = false + }); + } + + private static bool TryGetRedirectUri(Uri requestUri, HttpResponseMessage response, out Uri redirectUri) + { + redirectUri = null; + var statusCode = (int)response.StatusCode; + if (statusCode < 300 || statusCode > 399 || response.Headers.Location == null) + { + return false; + } + + redirectUri = response.Headers.Location.IsAbsoluteUri + ? response.Headers.Location + : new Uri(requestUri, response.Headers.Location); + return true; + } + + private static bool IsPublicIPAddress(IPAddress address) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (IPAddress.IsLoopback(address)) + { + return false; + } + + if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + var bytes = address.GetAddressBytes(); + return bytes[0] != 0 && + bytes[0] != 10 && + bytes[0] != 127 && + !(bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127) && + !(bytes[0] == 169 && bytes[1] == 254) && + !(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) && + !(bytes[0] == 192 && bytes[1] == 168) && + !(bytes[0] == 192 && bytes[1] == 0 && bytes[2] == 0) && + !(bytes[0] == 198 && (bytes[1] == 18 || bytes[1] == 19)) && + bytes[0] < 224; + } + + if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + var bytes = address.GetAddressBytes(); + return !address.IsIPv6LinkLocal && + !address.IsIPv6Multicast && + !address.IsIPv6SiteLocal && + !address.Equals(IPAddress.IPv6None) && + !address.Equals(IPAddress.IPv6Any) && + (bytes[0] & 0xfe) != 0xfc; + } + + return false; + } } } diff --git a/tests/SSCMS.Tests/TestHttpClientUtils.cs b/tests/SSCMS.Tests/TestHttpClientUtils.cs new file mode 100644 index 000000000..3ecace01a --- /dev/null +++ b/tests/SSCMS.Tests/TestHttpClientUtils.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using SSCMS.Utils; +using Xunit; + +namespace SSCMS.Tests +{ + public class TestHttpClientUtils + { + [Theory] + [InlineData("http://127.0.0.1/admin")] + [InlineData("http://10.0.0.1/admin")] + [InlineData("http://172.16.0.1/admin")] + [InlineData("http://192.168.1.1/admin")] + [InlineData("http://169.254.169.254/latest/meta-data/")] + [InlineData("http://[::1]/admin")] + [InlineData("file:///etc/passwd")] + public async Task TestValidatePublicHttpUrlRejectsUnsafeUrls(string url) + { + await Assert.ThrowsAsync(() => HttpClientUtils.ValidatePublicHttpUrlAsync(url)); + } + } +} From d03f26cc5c6e7e015dfb3572a4ff139d05106d7a Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:27:31 +0800 Subject: [PATCH 11/85] fix: harden v1 administrator login --- .../V1/AdministratorsController.Login.cs | 14 +- .../V1/AdministratorsController.cs | 74 +++++++- .../V1/AdministratorsControllerTests.cs | 167 ++++++++++++++++++ 3 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Login.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Login.cs index c53a2cb0d..0bf90753f 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Login.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Login.cs @@ -14,6 +14,17 @@ public partial class AdministratorsController [HttpPost, Route(RouteActionsLogin)] public async Task> Login([FromBody] LoginRequest request) { + if (request == null) + { + return this.Error(Constants.ErrorNotFound); + } + + var ipAddress = PageUtils.GetIpAddress(Request); + if (!TryConsumeLoginAttempt(request.Account, ipAddress, out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + var (administrator, userName, errorMessage) = await _administratorRepository.ValidateAsync(request.Account, request.Password, true); if (administrator == null) @@ -29,6 +40,7 @@ public async Task> Login([FromBody] LoginRequest reque } administrator = await _administratorRepository.GetByUserNameAsync(userName); + ClearLoginRateLimit(request.Account, ipAddress); await _administratorRepository.UpdateLastActivityDateAndCountOfLoginAsync(administrator); // 记录最后登录时间、失败次数清零 var token = _authManager.AuthenticateAdministrator(administrator, request.IsAutoLogin); @@ -60,7 +72,7 @@ public async Task> Login([FromBody] LoginRequest reque return new LoginResult { - Administrator = administrator, + Administrator = LoginAdministrator.From(administrator), AccessToken = token, SessionId = sessionId, IsEnforcePasswordChange = isEnforcePasswordChange diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs index 74dc4f28e..a4050d55b 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Models; using SSCMS.Repositories; using SSCMS.Services; +using SSCMS.Utils; namespace SSCMS.Web.Controllers.V1 { @@ -20,6 +22,8 @@ public partial class AdministratorsController : ControllerBase private const string RouteAdministrator = "administrators/{id:int}"; private const string RouteAdministratorUpdate = "administrators/{id:int}/actions/update"; private const string RouteAdministratorDelete = "administrators/{id:int}/actions/delete"; + private const int LoginRateLimitWindowMinutes = 10; + private const int LoginRateLimitMaxAttempts = 10; private readonly ISettingsManager _settingsManager; private readonly IAuthManager _authManager; @@ -29,8 +33,9 @@ public partial class AdministratorsController : ControllerBase private readonly IDbCacheRepository _dbCacheRepository; private readonly ILogRepository _logRepository; private readonly IStatRepository _statRepository; + private readonly ICacheManager _cacheManager; - public AdministratorsController(ISettingsManager settingsManager, IAuthManager authManager, IConfigRepository configRepository, IAccessTokenRepository accessTokenRepository, IAdministratorRepository administratorRepository, IDbCacheRepository dbCacheRepository, ILogRepository logRepository, IStatRepository statRepository) + public AdministratorsController(ISettingsManager settingsManager, IAuthManager authManager, IConfigRepository configRepository, IAccessTokenRepository accessTokenRepository, IAdministratorRepository administratorRepository, IDbCacheRepository dbCacheRepository, ILogRepository logRepository, IStatRepository statRepository, ICacheManager cacheManager) { _settingsManager = settingsManager; _authManager = authManager; @@ -40,6 +45,7 @@ public AdministratorsController(ISettingsManager settingsManager, IAuthManager a _dbCacheRepository = dbCacheRepository; _logRepository = logRepository; _statRepository = statRepository; + _cacheManager = cacheManager; } public class ListRequest @@ -74,18 +80,82 @@ public class LoginRequest public class LoginResult { - public Administrator Administrator { get; set; } + public LoginAdministrator Administrator { get; set; } public string AccessToken { get; set; } public DateTime? ExpiresAt { get; set; } public string SessionId { get; set; } public bool IsEnforcePasswordChange { get; set; } } + public class LoginAdministrator + { + public int Id { get; set; } + public string Guid { get; set; } + public string UserName { get; set; } + public string DisplayName { get; set; } + public string AvatarUrl { get; set; } + public DateTime? LastActivityDate { get; set; } + + public static LoginAdministrator From(Administrator administrator) + { + if (administrator == null) return null; + + return new LoginAdministrator + { + Id = administrator.Id, + Guid = administrator.Guid, + UserName = administrator.UserName, + DisplayName = administrator.DisplayName, + AvatarUrl = administrator.AvatarUrl, + LastActivityDate = administrator.LastActivityDate + }; + } + } + + private class LoginRateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + public class ResetPasswordRequest { public string Account { get; set; } public string Password { get; set; } public string NewPassword { get; set; } } + + private static string GetLoginRateLimitCacheKey(string account, string ipAddress) + { + return CacheUtils.GetClassKey(typeof(AdministratorsController), nameof(Login), StringUtils.ToLower(account), ipAddress); + } + + private bool TryConsumeLoginAttempt(string account, string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetLoginRateLimitCacheKey(account, ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new LoginRateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(LoginRateLimitWindowMinutes) + }; + } + + state.Count++; + var minutes = Math.Max(1, (int)Math.Ceiling((state.ExpireAt - DateTime.Now).TotalMinutes)); + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, minutes); + if (state.Count <= LoginRateLimitMaxAttempts) return true; + + retryAfterSeconds = Math.Max(1, (int)Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } + + private void ClearLoginRateLimit(string account, string ipAddress) + { + _cacheManager.Remove(GetLoginRateLimitCacheKey(account, ipAddress)); + } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs new file mode 100644 index 000000000..aa0e3f35a --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.V1; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.V1 +{ + public class AdministratorsControllerTests + { + [Fact] + public async Task LoginReturnsSanitizedAdministrator() + { + var administrator = new Administrator + { + Id = 1, + Guid = "admin-guid", + UserName = "admin", + DisplayName = "Admin", + Email = "admin@example.com", + Mobile = "13800000000", + LastActivityDate = DateTime.UtcNow + }; + + var administratorRepository = new Mock(); + administratorRepository + .Setup(x => x.ValidateAsync("admin", "password-md5", true)) + .ReturnsAsync((administrator, "admin", string.Empty)); + administratorRepository + .Setup(x => x.GetByUserNameAsync("admin")) + .ReturnsAsync(administrator); + + var authManager = new Mock(); + authManager + .Setup(x => x.AuthenticateAdministrator(administrator, false)) + .Returns("token"); + + var configRepository = new Mock(); + configRepository.Setup(x => x.GetAsync()).ReturnsAsync(new Config()); + + var controller = CreateController( + administratorRepository.Object, + authManager.Object, + configRepository.Object, + new TestCacheManager()); + + var result = await controller.Login(new AdministratorsController.LoginRequest + { + Account = "admin", + Password = "password-md5" + }); + + var value = Assert.IsType(result.Value); + Assert.IsType(value.Administrator); + Assert.Equal("admin", value.Administrator.UserName); + Assert.Null(value.Administrator.GetType().GetProperty(nameof(Administrator.Email))); + Assert.Null(value.Administrator.GetType().GetProperty(nameof(Administrator.Mobile))); + } + + [Fact] + public async Task LoginRateLimitsRepeatedFailures() + { + var administratorRepository = new Mock(); + administratorRepository + .Setup(x => x.ValidateAsync("admin", "bad-password", true)) + .ReturnsAsync((null, "admin", "invalid")); + + var controller = CreateController( + administratorRepository.Object, + Mock.Of(), + Mock.Of(), + new TestCacheManager()); + + ActionResult lastResult = null; + for (var i = 0; i < 11; i++) + { + var result = await controller.Login(new AdministratorsController.LoginRequest + { + Account = "admin", + Password = "bad-password" + }); + lastResult = result.Result; + } + + Assert.IsType(lastResult); + administratorRepository.Verify(x => x.ValidateAsync("admin", "bad-password", true), Times.Exactly(10)); + } + + private static AdministratorsController CreateController( + IAdministratorRepository administratorRepository, + IAuthManager authManager, + IConfigRepository configRepository, + ICacheManager cacheManager) + { + return new AdministratorsController( + Mock.Of(), + authManager, + configRepository, + Mock.Of(), + administratorRepository, + Mock.Of(), + Mock.Of(), + Mock.Of(), + cacheManager) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } + } +} From c119fe0998a2afc0b13ffd328684a68cd0bc92df Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:30:01 +0800 Subject: [PATCH 12/85] fix: validate scrawl image payloads --- src/SSCMS.Core/Utils/ImageUtils.cs | 14 ++++++ .../Editor/ActionsController.UploadScrawl.cs | 22 ++++++++- .../Editor/ActionsController.UploadScrawl.cs | 22 ++++++++- .../Common/Editor/ActionsControllerTests.cs | 48 +++++++++++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs diff --git a/src/SSCMS.Core/Utils/ImageUtils.cs b/src/SSCMS.Core/Utils/ImageUtils.cs index ed58aa253..1e5d6245e 100644 --- a/src/SSCMS.Core/Utils/ImageUtils.cs +++ b/src/SSCMS.Core/Utils/ImageUtils.cs @@ -91,6 +91,20 @@ public static void Save(byte[] imgByte, string imagePath) } } + public static bool IsValidImage(byte[] imgByte) + { + if (imgByte == null || imgByte.Length == 0) return false; + + try + { + return Image.Identify(imgByte) != null; + } + catch + { + return false; + } + } + public static void ResizeImageIfExceeding(string imagePath, int resizeWidth) { if (string.IsNullOrEmpty(imagePath) || resizeWidth <= 0) return; diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs index d9688b95e..3751ecec6 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs @@ -15,7 +15,18 @@ public async Task> UploadScrawl([FromQuery] int { var site = await _siteRepository.GetAsync(siteId); - var bytes = Convert.FromBase64String(request.File); + byte[] bytes; + try + { + bytes = Convert.FromBase64String(request.File); + } + catch + { + return new UploadScrawlResult + { + Error = Constants.ErrorUpload + }; + } var original = "scrawl.png"; var fileName = _pathManager.GetUploadFileName(site, original); @@ -27,13 +38,20 @@ public async Task> UploadScrawl([FromQuery] int Error = Constants.ErrorImageExtensionAllowed }; } - if (!_pathManager.IsImageSizeAllowed(site, request.File.Length)) + if (!_pathManager.IsImageSizeAllowed(site, bytes.LongLength)) { return new UploadScrawlResult { Error = Constants.ErrorImageSizeAllowed }; } + if (!ImageUtils.IsValidImage(bytes)) + { + return new UploadScrawlResult + { + Error = Constants.ErrorUpload + }; + } var localDirectoryPath = await _pathManager.GetUploadDirectoryPathAsync(site, UploadType.Image); var filePath = PathUtils.Combine(localDirectoryPath, fileName); diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadScrawl.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadScrawl.cs index c477214c6..301f11cd3 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadScrawl.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadScrawl.cs @@ -18,7 +18,18 @@ public async Task> UploadScrawl([FromQuery] int var site = await _siteRepository.GetAsync(siteId); - var bytes = Convert.FromBase64String(request.File); + byte[] bytes; + try + { + bytes = Convert.FromBase64String(request.File); + } + catch + { + return new UploadScrawlResult + { + Error = Constants.ErrorUpload + }; + } var original = "scrawl.png"; var fileName = _pathManager.GetUploadFileName(site, original); @@ -30,13 +41,20 @@ public async Task> UploadScrawl([FromQuery] int Error = Constants.ErrorImageExtensionAllowed }; } - if (!_pathManager.IsImageSizeAllowed(site, request.File.Length)) + if (!_pathManager.IsImageSizeAllowed(site, bytes.LongLength)) { return new UploadScrawlResult { Error = Constants.ErrorImageSizeAllowed }; } + if (!ImageUtils.IsValidImage(bytes)) + { + return new UploadScrawlResult + { + Error = Constants.ErrorUpload + }; + } var localDirectoryPath = await _pathManager.GetUploadDirectoryPathAsync(site, UploadType.Image); var filePath = PathUtils.Combine(localDirectoryPath, fileName); diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs new file mode 100644 index 000000000..7e9ceea1e --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Moq; +using SSCMS.Configuration; +using SSCMS.Enums; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin.Common.Editor; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin.Common.Editor +{ + public class ActionsControllerTests + { + [Fact] + public async Task UploadScrawlRejectsNonImageBytes() + { + var site = new Site + { + Id = 1 + }; + + var siteRepository = new Mock(); + siteRepository.Setup(x => x.GetAsync(1)).ReturnsAsync(site); + + var pathManager = new Mock(); + pathManager.Setup(x => x.GetUploadFileName(site, "scrawl.png")).Returns("scrawl.png"); + pathManager.Setup(x => x.IsImageExtensionAllowed(site, ".png")).Returns(true); + pathManager.Setup(x => x.IsImageSizeAllowed(site, It.IsAny())).Returns(true); + pathManager.Setup(x => x.GetUploadDirectoryPathAsync(site, UploadType.Image)).ReturnsAsync("/tmp"); + + var controller = new ActionsController( + pathManager.Object, + Mock.Of(), + Mock.Of(), + siteRepository.Object); + + var result = await controller.UploadScrawl(1, new ActionsController.UploadScrawlRequest + { + File = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("")) + }); + + Assert.Equal(Constants.ErrorUpload, result.Value.Error); + pathManager.Verify(x => x.UploadAsync(It.IsAny(), It.IsAny()), Times.Never); + } + } +} From 8e62de98356e7fbcb8c876077d41ee0c17ddc1d0 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:32:24 +0800 Subject: [PATCH 13/85] fix: constrain form file deletion paths --- .../Forms/FormDataAddController.DeleteFile.cs | 6 ++ .../Cms/Forms/FormDataAddControllerTests.cs | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Forms/FormDataAddControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataAddController.DeleteFile.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataAddController.DeleteFile.cs index 885a89574..b81ced12a 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataAddController.DeleteFile.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataAddController.DeleteFile.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using SSCMS.Configuration; using SSCMS.Core.Utils; using SSCMS.Dto; using SSCMS.Utils; @@ -18,6 +19,11 @@ public async Task> DeleteFile([FromBody] DeleteRequest } var filePath = PathUtils.Combine(_pathManager.ContentRootPath, request.FileUrl); + if (!DirectoryUtils.IsInDirectory(_pathManager.ContentRootPath, filePath)) + { + return this.Error(Constants.ErrorNotFound); + } + FileUtils.DeleteFileIfExists(filePath); return new BoolResult diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Forms/FormDataAddControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Forms/FormDataAddControllerTests.cs new file mode 100644 index 000000000..21e7f70d3 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Forms/FormDataAddControllerTests.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Threading.Tasks; +using Moq; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin.Cms.Forms; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin.Cms.Forms +{ + public class FormDataAddControllerTests + { + [Fact] + public async Task DeleteFileRejectsPathTraversalOutsideContentRoot() + { + var tempRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var contentRoot = Path.Combine(tempRoot, "content"); + Directory.CreateDirectory(contentRoot); + var outsideFilePath = Path.Combine(tempRoot, "outside.txt"); + await File.WriteAllTextAsync(outsideFilePath, "do not delete"); + + try + { + var authManager = new Mock(); + authManager + .Setup(x => x.HasSitePermissionsAsync(1, It.IsAny())) + .ReturnsAsync(true); + + var pathManager = new Mock(); + pathManager.Setup(x => x.ContentRootPath).Returns(contentRoot); + + var controller = new FormDataAddController( + authManager.Object, + pathManager.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + + await controller.DeleteFile(new FormDataAddController.DeleteRequest + { + SiteId = 1, + FormId = 1, + FileUrl = "../outside.txt" + }); + + Assert.True(File.Exists(outsideFilePath)); + } + finally + { + if (Directory.Exists(tempRoot)) + { + Directory.Delete(tempRoot, true); + } + } + } + } +} From 40bcd6753af023bd5e74a2f24f78f3d03cc1881c Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:34:04 +0800 Subject: [PATCH 14/85] fix: enforce captcha during forced admin login --- .../Admin/LoginController.Submit.cs | 2 +- .../Controllers/Admin/LoginControllerTests.cs | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs index a1eb97e3d..73345601a 100644 --- a/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs @@ -43,7 +43,7 @@ public async Task> Submit([FromBody] SubmitRequest re } else { - if (!request.IsForceLogoutAndLogin && !config.IsAdminCaptchaDisabled) + if (!config.IsAdminCaptchaDisabled) { var captcha = TranslateUtils.JsonDeserialize(_settingsManager.Decrypt(request.Token)); if (captcha == null || string.IsNullOrEmpty(captcha.Value) || captcha.ExpireAt < DateTime.Now) diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs index 3e442a1ff..866946461 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Moq; using SSCMS.Dto; +using SSCMS.Models; using SSCMS.Repositories; using SSCMS.Services; using SSCMS.Web.Controllers.Admin; @@ -54,5 +55,45 @@ public async Task SendSmsReturnsSuccessWithoutSendingSmsForUnknownMobile() x => x.AddOrUpdateAbsolute(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + + [Fact] + public async Task SubmitRequiresCaptchaWhenForceLogoutRequested() + { + var configRepository = new Mock(); + configRepository.Setup(x => x.GetAsync()).ReturnsAsync(new Config + { + IsAdminCaptchaDisabled = false + }); + + var administratorRepository = new Mock(); + + var controller = new LoginController( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + configRepository.Object, + administratorRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + var result = await controller.Submit(new LoginController.SubmitRequest + { + Account = "admin", + Password = "bad-password", + IsForceLogoutAndLogin = true + }); + + Assert.IsType(result.Result); + administratorRepository.Verify(x => x.ValidateAsync(It.IsAny(), It.IsAny(), true), Times.Never); + } } } From 7a15281df942f9046172578578df4169e773f4f6 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:37:42 +0800 Subject: [PATCH 15/85] fix: rate limit v1 user login --- .../Controllers/V1/UsersController.Login.cs | 13 +++ .../Controllers/V1/UsersController.cs | 51 ++++++++- .../Controllers/V1/UsersControllerTests.cs | 105 ++++++++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/V1/UsersController.Login.cs b/src/SSCMS.Web/Controllers/V1/UsersController.Login.cs index 8f8935ab9..cad07510d 100644 --- a/src/SSCMS.Web/Controllers/V1/UsersController.Login.cs +++ b/src/SSCMS.Web/Controllers/V1/UsersController.Login.cs @@ -14,6 +14,18 @@ public partial class UsersController [HttpPost, Route(RouteActionsLogin)] public async Task> Login([FromBody] LoginRequest request) { + if (request == null) + { + return this.Error(Constants.ErrorNotFound); + } + + var loginAccount = !string.IsNullOrEmpty(request.OpenId) ? request.OpenId : request.Account; + var ipAddress = PageUtils.GetIpAddress(Request); + if (!TryConsumeLoginAttempt(loginAccount, ipAddress, out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + User user = null; var errorMessage = Constants.ErrorNotFound; @@ -31,6 +43,7 @@ public async Task> Login([FromBody] LoginRequest reque return this.Error(errorMessage); } + ClearLoginRateLimit(loginAccount, ipAddress); var accessToken = _authManager.AuthenticateUser(user, request.IsPersistent); await _userRepository.UpdateLastActivityDateAndCountOfLoginAsync(user); diff --git a/src/SSCMS.Web/Controllers/V1/UsersController.cs b/src/SSCMS.Web/Controllers/V1/UsersController.cs index 3c0e8cace..e9ce0944c 100644 --- a/src/SSCMS.Web/Controllers/V1/UsersController.cs +++ b/src/SSCMS.Web/Controllers/V1/UsersController.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Models; using SSCMS.Repositories; using SSCMS.Services; +using SSCMS.Utils; namespace SSCMS.Web.Controllers.V1 { @@ -20,6 +23,8 @@ public partial class UsersController : ControllerBase private const string RouteUserUpdate = "users/{id:int}/actions/update"; private const string RouteUserDelete = "users/{id:int}/actions/delete"; private const string RouteUserResetPassword = "users/{id:int}/actions/resetPassword"; + private const int LoginRateLimitWindowMinutes = 10; + private const int LoginRateLimitMaxAttempts = 10; private readonly IAuthManager _authManager; private readonly IPathManager _pathManager; @@ -31,6 +36,7 @@ public partial class UsersController : ControllerBase private readonly IDbCacheRepository _dbCacheRepository; private readonly IUserGroupRepository _userGroupRepository; private readonly IUsersInGroupsRepository _usersInGroupsRepository; + private readonly ICacheManager _cacheManager; public UsersController( IAuthManager authManager, @@ -42,7 +48,8 @@ public UsersController( IStatRepository statRepository, IDbCacheRepository dbCacheRepository, IUserGroupRepository userGroupRepository, - IUsersInGroupsRepository usersInGroupsRepository + IUsersInGroupsRepository usersInGroupsRepository, + ICacheManager cacheManager ) { _authManager = authManager; @@ -55,6 +62,7 @@ IUsersInGroupsRepository usersInGroupsRepository _dbCacheRepository = dbCacheRepository; _userGroupRepository = userGroupRepository; _usersInGroupsRepository = usersInGroupsRepository; + _cacheManager = cacheManager; } public class ListRequest @@ -104,10 +112,49 @@ public class ResetPasswordRequest public string NewPassword { get; set; } } + private class LoginRateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + public class CreateRequest { public User User { get; set; } public List GroupNames { get; set; } } + + private static string GetLoginRateLimitCacheKey(string account, string ipAddress) + { + return CacheUtils.GetClassKey(typeof(UsersController), nameof(Login), StringUtils.ToLower(account), ipAddress); + } + + private bool TryConsumeLoginAttempt(string account, string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetLoginRateLimitCacheKey(account, ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new LoginRateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(LoginRateLimitWindowMinutes) + }; + } + + state.Count++; + var minutes = Math.Max(1, (int)Math.Ceiling((state.ExpireAt - DateTime.Now).TotalMinutes)); + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, minutes); + if (state.Count <= LoginRateLimitMaxAttempts) return true; + + retryAfterSeconds = Math.Max(1, (int)Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } + + private void ClearLoginRateLimit(string account, string ipAddress) + { + _cacheManager.Remove(GetLoginRateLimitCacheKey(account, ipAddress)); + } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs new file mode 100644 index 000000000..d319412d2 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.V1; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.V1 +{ + public class UsersControllerTests + { + [Fact] + public async Task LoginRateLimitsRepeatedFailures() + { + var userRepository = new Mock(); + userRepository + .Setup(x => x.ValidateAsync("user", "bad-password", true)) + .ReturnsAsync((null, "user", "invalid")); + + var controller = new UsersController( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + userRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + new TestCacheManager()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + ActionResult lastResult = null; + for (var i = 0; i < 11; i++) + { + var result = await controller.Login(new UsersController.LoginRequest + { + Account = "user", + Password = "bad-password" + }); + lastResult = result.Result; + } + + Assert.IsType(lastResult); + userRepository.Verify(x => x.ValidateAsync("user", "bad-password", true), Times.Exactly(10)); + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } + } +} From f91a375c81a3c23c1b2c186c340cb0d1166d955f Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:41:03 +0800 Subject: [PATCH 16/85] fix: sign stl trigger requests --- src/SSCMS.Core/Services/PathManager.Root.cs | 4 +- .../Stl/ActionsTriggerController.Get.cs | 7 +- .../Stl/ActionsTriggerController.cs | 31 ++++++- .../Stl/ActionsTriggerControllerTests.cs | 86 +++++++++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs diff --git a/src/SSCMS.Core/Services/PathManager.Root.cs b/src/SSCMS.Core/Services/PathManager.Root.cs index d05f976b3..8c4b5542a 100644 --- a/src/SSCMS.Core/Services/PathManager.Root.cs +++ b/src/SSCMS.Core/Services/PathManager.Root.cs @@ -271,6 +271,7 @@ public string GetTriggerApiUrl(int siteId, int channelId, int contentId, int fileTemplateId, int specialId, bool isRedirect) { var apiUrl = PageUtils.GetLocalApiUrl(Constants.ApiStlPrefix); + var tokenPayload = $"{siteId}:{channelId}:{contentId}:{fileTemplateId}:{specialId}:{isRedirect}"; return PageUtils.AddQueryString(PageUtils.Combine(apiUrl, Constants.RouteStlActionsTrigger), new NameValueCollection { {"siteId", siteId.ToString()}, @@ -278,7 +279,8 @@ public string GetTriggerApiUrl(int siteId, int channelId, int contentId, {"contentId", contentId.ToString()}, {"fileTemplateId", fileTemplateId.ToString()}, {"specialId", specialId.ToString()}, - {"isRedirect", isRedirect.ToString()} + {"isRedirect", isRedirect.ToString()}, + {"token", _settingsManager.Encrypt(tokenPayload)} }); } } diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs index f3ab8eb16..aa5141263 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs @@ -10,8 +10,13 @@ namespace SSCMS.Web.Controllers.Stl public partial class ActionsTriggerController { [HttpGet, Route(Constants.RouteStlActionsTrigger)] - public async Task Get([FromQuery] GetRequest request) + public async Task Get([FromQuery] GetRequest request) { + if (!IsValidTriggerToken(request)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); var redirectUrl = await _pathManager.GetIndexPageUrlAsync(site, false); diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs index 692fce913..06bebf1f4 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs @@ -16,14 +16,16 @@ public partial class ActionsTriggerController : ControllerBase private readonly ISiteRepository _siteRepository; private readonly IChannelRepository _channelRepository; private readonly IContentRepository _contentRepository; + private readonly ISettingsManager _settingsManager; - public ActionsTriggerController(ICreateManager createManager, IPathManager pathManager, ISiteRepository siteRepository, IChannelRepository channelRepository, IContentRepository contentRepository) + public ActionsTriggerController(ICreateManager createManager, IPathManager pathManager, ISiteRepository siteRepository, IChannelRepository channelRepository, IContentRepository contentRepository, ISettingsManager settingsManager) { _createManager = createManager; _pathManager = pathManager; _siteRepository = siteRepository; _channelRepository = channelRepository; _contentRepository = contentRepository; + _settingsManager = settingsManager; } public class GetRequest : ChannelRequest @@ -33,6 +35,33 @@ public class GetRequest : ChannelRequest public int SpecialId { get; set; } public bool IsRedirect { get; set; } public string ReturnUrl { get; set; } + public string Token { get; set; } + } + + private static string GetTriggerTokenPayload(int siteId, int channelId, int contentId, int fileTemplateId, int specialId, bool isRedirect) + { + return $"{siteId}:{channelId}:{contentId}:{fileTemplateId}:{specialId}:{isRedirect}"; + } + + public static string GetTriggerTokenPayload(GetRequest request) + { + return request == null + ? string.Empty + : GetTriggerTokenPayload(request.SiteId, request.ChannelId, request.ContentId, request.FileTemplateId, request.SpecialId, request.IsRedirect); + } + + private bool IsValidTriggerToken(GetRequest request) + { + if (request == null || string.IsNullOrEmpty(request.Token)) return false; + + try + { + return _settingsManager.Decrypt(request.Token) == GetTriggerTokenPayload(request); + } + catch + { + return false; + } } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs new file mode 100644 index 000000000..866bef6e6 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Enums; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Stl; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Stl +{ + public class ActionsTriggerControllerTests + { + [Fact] + public async Task GetRejectsUnsignedTriggerRequests() + { + var createManager = new Mock(); + var siteRepository = new Mock(); + siteRepository.Setup(x => x.GetAsync(1)).ReturnsAsync(new Site + { + Id = 1 + }); + + var controller = new ActionsTriggerController( + createManager.Object, + Mock.Of(), + siteRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of()); + + var result = await controller.Get(new ActionsTriggerController.GetRequest + { + SiteId = 1 + }); + + Assert.IsType(result); + createManager.Verify(x => x.ExecuteAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public async Task GetAllowsSignedTriggerRequests() + { + var createManager = new Mock(); + var pathManager = new Mock(); + pathManager.Setup(x => x.GetIndexPageUrlAsync(It.IsAny(), false)).ReturnsAsync("/"); + + var siteRepository = new Mock(); + siteRepository.Setup(x => x.GetAsync(1)).ReturnsAsync(new Site + { + Id = 1 + }); + + var settingsManager = new Mock(); + settingsManager + .Setup(x => x.Decrypt("signed", null)) + .Returns("1:1:0:0:0:False"); + + var controller = new ActionsTriggerController( + createManager.Object, + pathManager.Object, + siteRepository.Object, + Mock.Of(), + Mock.Of(), + settingsManager.Object); + + var result = await controller.Get(new ActionsTriggerController.GetRequest + { + SiteId = 1, + ChannelId = 1, + Token = "signed" + }); + + Assert.IsType(result); + createManager.Verify(x => x.ExecuteAsync(1, CreateType.Channel, 1, 0, 0, 0), Times.Once); + } + } +} From c54c03ffb81407cc0a17023e682f8037802b8532 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:46:30 +0800 Subject: [PATCH 17/85] fix: rate limit stl rendering endpoints --- .../Stl/ActionsDynamicController.Submit.cs | 13 +- .../Stl/ActionsDynamicController.cs | 44 +++++- .../Stl/ActionsSearchController.Submit.cs | 10 ++ .../Stl/ActionsSearchController.cs | 44 +++++- .../Stl/ActionsPublicRateLimitTests.cs | 126 ++++++++++++++++++ 5 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.Submit.cs b/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.Submit.cs index dbd038ef3..c1cc2af37 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.Submit.cs @@ -2,14 +2,25 @@ using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; using SSCMS.Core.StlParser.StlElement; +using SSCMS.Utils; namespace SSCMS.Web.Controllers.Stl { public partial class ActionsDynamicController { [HttpPost, Route(Constants.RouteStlActionsDynamic)] - public async Task Submit([FromBody] SubmitRequest request) + public async Task> Submit([FromBody] SubmitRequest request) { + if (request == null) + { + return this.Error(Constants.ErrorNotFound); + } + + if (!TryConsumeRequestQuota(PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + var user = await _authManager.GetUserAsync(); var dynamicInfo = StlDynamic.GetDynamicInfo(_settingsManager, request.Value, request.Page, user, Request.Path + Request.QueryString); diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.cs index 57bfc868d..b4679cd24 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsDynamicController.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Services; namespace SSCMS.Web.Controllers.Stl @@ -9,15 +11,20 @@ namespace SSCMS.Web.Controllers.Stl [Route(Constants.ApiPrefix + Constants.ApiStlPrefix)] public partial class ActionsDynamicController : ControllerBase { + private const int RateLimitWindowMinutes = 1; + private const int RateLimitMaxRequests = 60; + private readonly ISettingsManager _settingsManager; private readonly IAuthManager _authManager; private readonly IParseManager _parseManager; + private readonly ICacheManager _cacheManager; - public ActionsDynamicController(ISettingsManager settingsManager, IAuthManager authManager, IParseManager parseManager) + public ActionsDynamicController(ISettingsManager settingsManager, IAuthManager authManager, IParseManager parseManager, ICacheManager cacheManager) { _settingsManager = settingsManager; _authManager = authManager; _parseManager = parseManager; + _cacheManager = cacheManager; } public class SubmitRequest @@ -31,5 +38,38 @@ public class SubmitResult public bool Value { get; set; } public string Html { get; set; } } + + private class RateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + private static string GetRateLimitCacheKey(string ipAddress) + { + return CacheUtils.GetClassKey(typeof(ActionsDynamicController), "Rate", ipAddress ?? "unknown"); + } + + private bool TryConsumeRequestQuota(string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetRateLimitCacheKey(ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new RateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(RateLimitWindowMinutes) + }; + } + + state.Count++; + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, RateLimitWindowMinutes); + if (state.Count <= RateLimitMaxRequests) return true; + + retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } } } diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs index de87edcb1..ee17354b7 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs @@ -17,6 +17,16 @@ public partial class ActionsSearchController [HttpPost, Route(Constants.RouteStlActionsSearch)] public async Task> Submit([FromBody] StlSearchRequest request) { + if (request == null) + { + return this.Error(Constants.ErrorNotFound); + } + + if (!TryConsumeRequestQuota(PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + var template = string.Empty; try { diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs index 56cb2dcdb..b07e9d5e8 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs @@ -1,8 +1,10 @@ -using System.Collections.Specialized; +using System; +using System.Collections.Specialized; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; using SSCMS.Core.StlParser.Models; +using SSCMS.Core.Utils; using SSCMS.Repositories; using SSCMS.Services; @@ -12,14 +14,18 @@ namespace SSCMS.Web.Controllers.Stl [Route(Constants.ApiPrefix + Constants.ApiStlPrefix)] public partial class ActionsSearchController : ControllerBase { + private const int RateLimitWindowMinutes = 1; + private const int RateLimitMaxRequests = 60; + private readonly ISettingsManager _settingsManager; private readonly IAuthManager _authManager; private readonly IParseManager _parseManager; private readonly IDatabaseManager _databaseManager; private readonly ISiteRepository _siteRepository; private readonly IContentRepository _contentRepository; + private readonly ICacheManager _cacheManager; - public ActionsSearchController(ISettingsManager settingsManager, IAuthManager authManager, IParseManager parseManager, IDatabaseManager databaseManager, ISiteRepository siteRepository, IContentRepository contentRepository) + public ActionsSearchController(ISettingsManager settingsManager, IAuthManager authManager, IParseManager parseManager, IDatabaseManager databaseManager, ISiteRepository siteRepository, IContentRepository contentRepository, ICacheManager cacheManager) { _settingsManager = settingsManager; _authManager = authManager; @@ -27,6 +33,40 @@ public ActionsSearchController(ISettingsManager settingsManager, IAuthManager au _databaseManager = databaseManager; _siteRepository = siteRepository; _contentRepository = contentRepository; + _cacheManager = cacheManager; + } + + private class RateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + private static string GetRateLimitCacheKey(string ipAddress) + { + return CacheUtils.GetClassKey(typeof(ActionsSearchController), "Rate", ipAddress ?? "unknown"); + } + + private bool TryConsumeRequestQuota(string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetRateLimitCacheKey(ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new RateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(RateLimitWindowMinutes) + }; + } + + state.Count++; + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, RateLimitWindowMinutes); + if (state.Count <= RateLimitMaxRequests) return true; + + retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; } private static NameValueCollection GetPostCollection(StlSearchRequest request) diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs new file mode 100644 index 000000000..1eaed77f3 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Core.StlParser.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Stl; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Stl +{ + public class ActionsPublicRateLimitTests + { + [Fact] + public async Task SearchRateLimitsRepeatedAnonymousRequests() + { + var controller = new ActionsSearchController( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + new TestCacheManager()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + ActionResult lastResult = null; + for (var i = 0; i < 61; i++) + { + var result = await controller.Submit(new StlSearchRequest + { + SiteId = 1 + }); + lastResult = result.Result; + } + + Assert.IsType(lastResult); + } + + [Fact] + public async Task DynamicRateLimitsRepeatedAnonymousRequests() + { + var settingsManager = new Mock(); + settingsManager.Setup(x => x.Decrypt("{}", null)).Returns("{}"); + + var controller = new ActionsDynamicController( + settingsManager.Object, + Mock.Of(), + Mock.Of(), + new TestCacheManager()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + ActionResult lastResult = null; + for (var i = 0; i < 61; i++) + { + var result = await controller.Submit(new ActionsDynamicController.SubmitRequest + { + Value = "{}" + }); + lastResult = result.Result; + } + + Assert.IsType(lastResult); + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } + } +} From aa274d7c829de1216e2312696fffb904ec9a8567 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:49:24 +0800 Subject: [PATCH 18/85] fix: sign stl page contents requests --- src/SSCMS.Core/Services/PathManager.Root.cs | 5 +- .../ActionsPageContentsController.Submit.cs | 5 ++ .../Stl/ActionsPageContentsController.cs | 13 ++++ .../Stl/ActionsPageContentsControllerTests.cs | 74 +++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs diff --git a/src/SSCMS.Core/Services/PathManager.Root.cs b/src/SSCMS.Core/Services/PathManager.Root.cs index 8c4b5542a..70c62ffb1 100644 --- a/src/SSCMS.Core/Services/PathManager.Root.cs +++ b/src/SSCMS.Core/Services/PathManager.Root.cs @@ -255,6 +255,8 @@ public string GetPageContentsApiUrl(Site site) public string GetPageContentsApiParameters(int siteId, int pageChannelId, int templateId, int totalNum, int pageCount, int currentPageIndex, string stlPageContentsElement) { + var encryptedElement = _settingsManager.Encrypt(stlPageContentsElement); + var tokenPayload = $"{siteId}:{pageChannelId}:{templateId}:{totalNum}:{pageCount}:{currentPageIndex}:{encryptedElement}"; return $@" {{ siteId: {siteId}, @@ -263,7 +265,8 @@ public string GetPageContentsApiParameters(int siteId, int pageChannelId, int te totalNum: {totalNum}, pageCount: {pageCount}, currentPageIndex: {currentPageIndex}, - stlPageContentsElement: '{_settingsManager.Encrypt(stlPageContentsElement)}' + stlPageContentsElement: '{encryptedElement}', + token: '{_settingsManager.Encrypt(tokenPayload)}' }}"; } diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.Submit.cs b/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.Submit.cs index 0c5ba0c0a..2a2fd3cce 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.Submit.cs @@ -11,6 +11,11 @@ public partial class ActionsPageContentsController [HttpPost, Route(Constants.RouteStlActionsPageContents)] public async Task> Submit([FromBody] SubmitRequest request) { + if (!IsValidToken(request)) + { + return Unauthorized(); + } + var user = await _authManager.GetUserAsync(); var site = await _siteRepository.GetAsync(request.SiteId); diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs index 87d8c1da0..5a9fc8d29 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs @@ -38,11 +38,24 @@ public class SubmitRequest : SiteRequest public int PageCount { get; set; } public int CurrentPageIndex { get; set; } public string StlPageContentsElement { get; set; } + public string Token { get; set; } } public class SubmitResult { public string Html { get; set; } } + + private static string GetPageContentsTokenPayload(SubmitRequest request) + { + return $"{request.SiteId}:{request.PageChannelId}:{request.TemplateId}:{request.TotalNum}:{request.PageCount}:{request.CurrentPageIndex}:{request.StlPageContentsElement}"; + } + + private bool IsValidToken(SubmitRequest request) + { + if (request == null || string.IsNullOrEmpty(request.Token)) return false; + + return _settingsManager.Decrypt(request.Token) == GetPageContentsTokenPayload(request); + } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs new file mode 100644 index 000000000..cbe7be738 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs @@ -0,0 +1,74 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Stl; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Stl +{ + public class ActionsPageContentsControllerTests + { + [Fact] + public async Task SubmitRejectsUnsignedPageContentsRequests() + { + var siteRepository = new Mock(); + + var controller = new ActionsPageContentsController( + Mock.Of(), + Mock.Of(), + Mock.Of(), + siteRepository.Object, + Mock.Of(), + Mock.Of()); + + var result = await controller.Submit(new ActionsPageContentsController.SubmitRequest + { + SiteId = 1, + PageChannelId = 1, + TemplateId = 1, + TotalNum = 10, + PageCount = 2, + CurrentPageIndex = 1, + StlPageContentsElement = "encrypted" + }); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SubmitRejectsTamperedPageContentsRequests() + { + var siteRepository = new Mock(); + var settingsManager = new Mock(); + settingsManager + .Setup(x => x.Decrypt("signed", null)) + .Returns("1:1:1:10:2:0:encrypted"); + + var controller = new ActionsPageContentsController( + settingsManager.Object, + Mock.Of(), + Mock.Of(), + siteRepository.Object, + Mock.Of(), + Mock.Of()); + + var result = await controller.Submit(new ActionsPageContentsController.SubmitRequest + { + SiteId = 1, + PageChannelId = 1, + TemplateId = 1, + TotalNum = 10, + PageCount = 2, + CurrentPageIndex = 1, + StlPageContentsElement = "encrypted", + Token = "signed" + }); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); + } + } +} From 1280e4cf92a0ee2fe46ca8772eb7e133aa45fda1 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:51:55 +0800 Subject: [PATCH 19/85] fix: sign content download requests --- src/SSCMS.Core/Services/PathManager.Root.cs | 5 +- .../Stl/ActionsDownloadController.Get.cs | 5 ++ .../Stl/ActionsDownloadController.cs | 20 +++++++ .../Stl/ActionsDownloadControllerTests.cs | 59 +++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Stl/ActionsDownloadControllerTests.cs diff --git a/src/SSCMS.Core/Services/PathManager.Root.cs b/src/SSCMS.Core/Services/PathManager.Root.cs index 70c62ffb1..aed88404f 100644 --- a/src/SSCMS.Core/Services/PathManager.Root.cs +++ b/src/SSCMS.Core/Services/PathManager.Root.cs @@ -197,12 +197,15 @@ public async Task GetDownloadApiUrlAsync(Site site, int channelId, int c } var apiUrl = GetApiHostUrl(site, Constants.ApiPrefix); + var encryptedFileUrl = _settingsManager.Encrypt(fileUrl); + var tokenPayload = $"{site.Id}:{channelId}:{contentId}:{encryptedFileUrl}"; return PageUtils.AddQueryString(PageUtils.Combine(apiUrl, Constants.ApiStlPrefix, Constants.RouteStlActionsDownload), new NameValueCollection { {"siteId", site.Id.ToString()}, {"channelId", channelId.ToString()}, {"contentId", contentId.ToString()}, - {"fileUrl", _settingsManager.Encrypt(fileUrl)} + {"fileUrl", encryptedFileUrl}, + {"token", _settingsManager.Encrypt(tokenPayload)} }); } diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.Get.cs b/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.Get.cs index abf6179c0..2cf40bffc 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.Get.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.Get.cs @@ -86,6 +86,11 @@ public async Task Get([FromQuery] GetRequest request) } else if (request.SiteId.HasValue && request.ChannelId.HasValue && request.ContentId.HasValue && !string.IsNullOrEmpty(request.FileUrl)) { + if (!IsValidContentDownloadToken(request)) + { + return Unauthorized(); + } + var fileUrl = _settingsManager.Decrypt(request.FileUrl); var site = await _siteRepository.GetAsync(request.SiteId.Value); var channel = await _channelRepository.GetAsync(request.ChannelId.Value); diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.cs index f6a69f019..3a21f8685 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsDownloadController.cs @@ -32,6 +32,26 @@ public class GetRequest public int? ContentId { get; set; } public string FileUrl { get; set; } public string FilePath { get; set; } + public string Token { get; set; } + } + + private static string GetContentDownloadTokenPayload(GetRequest request) + { + return $"{request.SiteId}:{request.ChannelId}:{request.ContentId}:{request.FileUrl}"; + } + + private bool IsValidContentDownloadToken(GetRequest request) + { + if (request == null || string.IsNullOrEmpty(request.Token)) return false; + + try + { + return _settingsManager.Decrypt(request.Token) == GetContentDownloadTokenPayload(request); + } + catch + { + return false; + } } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsDownloadControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsDownloadControllerTests.cs new file mode 100644 index 000000000..ed53a5c85 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsDownloadControllerTests.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Stl; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Stl +{ + public class ActionsDownloadControllerTests + { + [Fact] + public async Task GetRejectsUnsignedContentDownloadBeforeIncrementing() + { + var settingsManager = new Mock(); + settingsManager.Setup(x => x.Decrypt("encrypted", null)).Returns("/files/missing.pdf"); + + var pathManager = new Mock(); + pathManager.Setup(x => x.GetSitePathAsync(It.IsAny())).ReturnsAsync("/tmp/site"); + pathManager.Setup(x => x.ParseSitePathAsync(It.IsAny(), "/files/missing.pdf")).ReturnsAsync("/tmp/site/files/missing.pdf"); + pathManager.Setup(x => x.IsFileDownload(It.IsAny(), ".pdf")).Returns(true); + + var siteRepository = new Mock(); + siteRepository.Setup(x => x.GetAsync(1)).ReturnsAsync(new Site + { + Id = 1 + }); + + var channelRepository = new Mock(); + channelRepository.Setup(x => x.GetAsync(2)).ReturnsAsync(new Channel + { + Id = 2 + }); + channelRepository.Setup(x => x.GetTableName(It.IsAny(), It.IsAny())).Returns("sscms_Content"); + + var contentRepository = new Mock(); + + var controller = new ActionsDownloadController( + settingsManager.Object, + pathManager.Object, + siteRepository.Object, + channelRepository.Object, + contentRepository.Object); + + var result = await controller.Get(new ActionsDownloadController.GetRequest + { + SiteId = 1, + ChannelId = 2, + ContentId = 3, + FileUrl = "encrypted" + }); + + Assert.IsType(result); + contentRepository.Verify(x => x.AddDownloadsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + } +} From 203383928501aaefb31ea0dee98770e6d207f372 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:55:15 +0800 Subject: [PATCH 20/85] fix: invalidate tokens on password change --- .../Repositories/AdministratorRepository.cs | 11 ++- src/SSCMS.Core/Repositories/UserRepository.cs | 8 +- .../AuthTokenInvalidationTests.cs | 80 +++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 tests/SSCMS.Core.Tests/Repositories/AuthTokenInvalidationTests.cs diff --git a/src/SSCMS.Core/Repositories/AdministratorRepository.cs b/src/SSCMS.Core/Repositories/AdministratorRepository.cs index 833e65ded..0b9e96af3 100644 --- a/src/SSCMS.Core/Repositories/AdministratorRepository.cs +++ b/src/SSCMS.Core/Repositories/AdministratorRepository.cs @@ -17,14 +17,16 @@ namespace SSCMS.Core.Repositories public partial class AdministratorRepository : IAdministratorRepository { private readonly Repository _repository; + private readonly ICacheManager _cacheManager; private readonly IConfigRepository _configRepository; private readonly IAdministratorsInRolesRepository _administratorsInRolesRepository; private readonly IRoleRepository _roleRepository; - public AdministratorRepository(ISettingsManager settingsManager, IConfigRepository configRepository, + public AdministratorRepository(ISettingsManager settingsManager, ICacheManager cacheManager, IConfigRepository configRepository, IAdministratorsInRolesRepository administratorsInRolesRepository, IRoleRepository roleRepository) { _repository = new Repository(settingsManager.Database, settingsManager.Redis); + _cacheManager = cacheManager; _configRepository = configRepository; _administratorsInRolesRepository = administratorsInRolesRepository; _roleRepository = roleRepository; @@ -142,6 +144,13 @@ await _repository.UpdateAsync(Q .Where(nameof(Administrator.Id), administrator.Id) .CachingRemove(cacheKeys.ToArray()) ); + + _cacheManager.Remove(GetTokenCacheKey(administrator)); + } + + private static string GetTokenCacheKey(Administrator administrator) + { + return $"admin:{administrator.Id}:token"; } public async Task LockAsync(IList userNames) diff --git a/src/SSCMS.Core/Repositories/UserRepository.cs b/src/SSCMS.Core/Repositories/UserRepository.cs index 2e4b85250..2aeb906cb 100644 --- a/src/SSCMS.Core/Repositories/UserRepository.cs +++ b/src/SSCMS.Core/Repositories/UserRepository.cs @@ -362,6 +362,13 @@ await _repository.UpdateAsync(Q .Where(nameof(User.Id), user.Id) .CachingRemove(GetCacheKeysToRemove(user)) ); + + _cacheManager.Remove(GetTokenCacheKey(user)); + } + + private static string GetTokenCacheKey(User user) + { + return $"user:{user.Id}:token"; } public async Task CheckAsync(IList userIds) @@ -715,4 +722,3 @@ public async Task DeleteAsync(int userId) } } } - diff --git a/tests/SSCMS.Core.Tests/Repositories/AuthTokenInvalidationTests.cs b/tests/SSCMS.Core.Tests/Repositories/AuthTokenInvalidationTests.cs new file mode 100644 index 000000000..010b10335 --- /dev/null +++ b/tests/SSCMS.Core.Tests/Repositories/AuthTokenInvalidationTests.cs @@ -0,0 +1,80 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using Xunit; + +namespace SSCMS.Core.Tests.Repositories +{ + [Collection("Database collection")] + public class AuthTokenInvalidationTests + { + private readonly IAdministratorRepository _administratorRepository; + private readonly IUserRepository _userRepository; + private readonly ICacheManager _cacheManager; + + public AuthTokenInvalidationTests(IntegrationTestsFixture fixture) + { + _administratorRepository = fixture.Provider.GetService(); + _userRepository = fixture.Provider.GetService(); + _cacheManager = fixture.Provider.GetService(); + } + + [Fact] + public async Task UserChangePasswordInvalidatesCachedToken() + { + const string userName = "Tests_Token_User"; + var existing = await _userRepository.GetByUserNameAsync(userName); + if (existing != null) + { + await _userRepository.DeleteAsync(existing.Id); + } + + var (user, errorMessage) = await _userRepository.InsertAsync(new User + { + UserName = userName + }, "Password1", true, "127.0.0.1"); + Assert.True(user != null, errorMessage); + + var cacheKey = $"user:{user.Id}:token"; + _cacheManager.AddOrUpdate(cacheKey, "old-token"); + + var (success, changeErrorMessage) = await _userRepository.ChangePasswordAsync(user.Id, "Password2"); + + Assert.True(success, changeErrorMessage); + Assert.False(_cacheManager.Exists(cacheKey)); + + await _userRepository.DeleteAsync(user.Id); + } + + [Fact] + public async Task AdministratorChangePasswordInvalidatesCachedToken() + { + const string userName = "Tests_Token_Admin"; + var existing = await _administratorRepository.GetByUserNameAsync(userName); + if (existing != null) + { + await _administratorRepository.DeleteAsync(existing.Id); + } + + var administrator = new Administrator + { + UserName = userName + }; + var (isValid, errorMessage) = await _administratorRepository.InsertAsync(administrator, "Password1"); + Assert.True(isValid, errorMessage); + + administrator = await _administratorRepository.GetByUserNameAsync(userName); + var cacheKey = $"admin:{administrator.Id}:token"; + _cacheManager.AddOrUpdate(cacheKey, "old-token"); + + var (changeIsValid, changeErrorMessage) = await _administratorRepository.ChangePasswordAsync(administrator, "Password2"); + + Assert.True(changeIsValid, changeErrorMessage); + Assert.False(_cacheManager.Exists(cacheKey)); + + await _administratorRepository.DeleteAsync(administrator.Id); + } + } +} From 0e77e17c3c70feb84c4699353743854f1e5e09be Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:57:30 +0800 Subject: [PATCH 21/85] fix: restrict stl sql to read only queries --- .../Services/DatabaseManager.Parser.cs | 123 +++++++++++++++++- src/SSCMS.Core/Services/DatabaseManager.cs | 11 +- .../Services/DatabaseManagerSqlSafetyTests.cs | 29 +++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Services/DatabaseManagerSqlSafetyTests.cs diff --git a/src/SSCMS.Core/Services/DatabaseManager.Parser.cs b/src/SSCMS.Core/Services/DatabaseManager.Parser.cs index 38657e89a..ba98bc6b6 100644 --- a/src/SSCMS.Core/Services/DatabaseManager.Parser.cs +++ b/src/SSCMS.Core/Services/DatabaseManager.Parser.cs @@ -1,5 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; +using System.Text; +using System.Text.RegularExpressions; using Dapper; using Datory; using Datory.Utils; @@ -11,8 +14,126 @@ namespace SSCMS.Core.Services { public partial class DatabaseManager { + public static bool IsReadOnlySelectSql(string sqlString) + { + var sql = GetSqlOutsideLiteralsAndComments(sqlString); + if (string.IsNullOrWhiteSpace(sql)) return false; + if (sql.Contains(';')) return false; + + var normalized = sql.Trim(); + if (!Regex.IsMatch(normalized, @"^(select|with)\b", RegexOptions.IgnoreCase)) + { + return false; + } + + if (Regex.IsMatch(normalized, @"\b(insert|update|delete|drop|alter|create|truncate|merge|exec|execute|grant|revoke|backup|restore|call)\b", RegexOptions.IgnoreCase)) + { + return false; + } + + if (Regex.IsMatch(normalized, @"\binto\s+(out|dump)?file\b", RegexOptions.IgnoreCase)) + { + return false; + } + + return !Regex.IsMatch(normalized, @"^select\b[\s\S]*\binto\b", RegexOptions.IgnoreCase); + } + + private static string GetSqlOutsideLiteralsAndComments(string sqlString) + { + if (string.IsNullOrEmpty(sqlString)) return string.Empty; + + var builder = new StringBuilder(sqlString.Length); + var quote = '\0'; + var lineComment = false; + var blockComment = false; + + for (var i = 0; i < sqlString.Length; i++) + { + var c = sqlString[i]; + var next = i + 1 < sqlString.Length ? sqlString[i + 1] : '\0'; + + if (lineComment) + { + if (c == '\r' || c == '\n') + { + lineComment = false; + builder.Append(c); + } + else + { + builder.Append(' '); + } + continue; + } + + if (blockComment) + { + if (c == '*' && next == '/') + { + blockComment = false; + builder.Append(" "); + i++; + } + else + { + builder.Append(' '); + } + continue; + } + + if (quote != '\0') + { + builder.Append(' '); + if (c == quote) + { + if (quote == '\'' && next == '\'') + { + builder.Append(' '); + i++; + continue; + } + quote = '\0'; + } + continue; + } + + if (c == '-' && next == '-') + { + lineComment = true; + builder.Append(" "); + i++; + continue; + } + + if (c == '/' && next == '*') + { + blockComment = true; + builder.Append(" "); + i++; + continue; + } + + if (c == '\'' || c == '"' || c == '`') + { + quote = c; + builder.Append(' '); + continue; + } + + builder.Append(c); + } + + return builder.ToString(); + } + public async Task>>> ParserGetSqlDataSourceAsync(DatabaseType databaseType, string connectionString, string queryString) { + if (!IsReadOnlySelectSql(queryString)) + { + throw new InvalidOperationException("Only read-only SELECT SQL is allowed."); + } + var rows = new List>>(); var itemIndex = 0; using (var connection = GetConnection(databaseType, connectionString)) diff --git a/src/SSCMS.Core/Services/DatabaseManager.cs b/src/SSCMS.Core/Services/DatabaseManager.cs index e9d5c65bf..49a33af1c 100644 --- a/src/SSCMS.Core/Services/DatabaseManager.cs +++ b/src/SSCMS.Core/Services/DatabaseManager.cs @@ -370,6 +370,11 @@ public IEnumerable> GetRows(DatabaseType databaseTyp public int GetPageTotalCount(string sqlString) { + if (!IsReadOnlySelectSql(sqlString)) + { + throw new InvalidOperationException("Only read-only SELECT SQL is allowed."); + } + var temp = StringUtils.ToLower(sqlString); var pos = temp.LastIndexOf("order by", StringComparison.OrdinalIgnoreCase); if (pos > -1) @@ -384,6 +389,11 @@ public int GetPageTotalCount(string sqlString) public string GetStlPageSqlString(string sqlString, string orderString, int totalCount, int itemsPerPage, int currentPageIndex) { + if (!IsReadOnlySelectSql(sqlString)) + { + throw new InvalidOperationException("Only read-only SELECT SQL is allowed."); + } + string retVal; var temp = StringUtils.ToLower(sqlString); @@ -623,4 +633,3 @@ private string GetValueFromConnectionString(string connectionString, string attr } } } - diff --git a/tests/SSCMS.Web.Tests/Services/DatabaseManagerSqlSafetyTests.cs b/tests/SSCMS.Web.Tests/Services/DatabaseManagerSqlSafetyTests.cs new file mode 100644 index 000000000..49eb4eccb --- /dev/null +++ b/tests/SSCMS.Web.Tests/Services/DatabaseManagerSqlSafetyTests.cs @@ -0,0 +1,29 @@ +using SSCMS.Core.Services; +using Xunit; + +namespace SSCMS.Web.Tests.Services +{ + public class DatabaseManagerSqlSafetyTests + { + [Theory] + [InlineData("select * from siteserver_Content")] + [InlineData("WITH recent AS (SELECT * FROM siteserver_Content) SELECT * FROM recent")] + [InlineData("select 'delete from table' as Text from siteserver_Content")] + public void IsReadOnlySelectSqlAllowsSingleSelectQueries(string sql) + { + Assert.True(DatabaseManager.IsReadOnlySelectSql(sql)); + } + + [Theory] + [InlineData("delete from siteserver_Administrator")] + [InlineData("select * from siteserver_Content; delete from siteserver_Administrator")] + [InlineData("update siteserver_Administrator set Password = 'x'")] + [InlineData("drop table siteserver_Administrator")] + [InlineData("select * into backup_table from siteserver_Administrator")] + [InlineData("select * from siteserver_Content into outfile '/tmp/data.txt'")] + public void IsReadOnlySelectSqlRejectsMutatingOrMultiStatementSql(string sql) + { + Assert.False(DatabaseManager.IsReadOnlySelectSql(sql)); + } + } +} From 190aa57dc281612477005bdef49dcda58863fba0 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 22:59:26 +0800 Subject: [PATCH 22/85] fix: reject malformed stl page tokens --- .../Stl/ActionsPageContentsController.cs | 9 ++++- .../Stl/ActionsPageContentsControllerTests.cs | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs index 5a9fc8d29..1404184d8 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsPageContentsController.cs @@ -55,7 +55,14 @@ private bool IsValidToken(SubmitRequest request) { if (request == null || string.IsNullOrEmpty(request.Token)) return false; - return _settingsManager.Decrypt(request.Token) == GetPageContentsTokenPayload(request); + try + { + return _settingsManager.Decrypt(request.Token) == GetPageContentsTokenPayload(request); + } + catch + { + return false; + } } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs index cbe7be738..33afbaf50 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPageContentsControllerTests.cs @@ -70,5 +70,38 @@ public async Task SubmitRejectsTamperedPageContentsRequests() Assert.IsType(result.Result); siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); } + + [Fact] + public async Task SubmitRejectsInvalidEncryptedToken() + { + var siteRepository = new Mock(); + var settingsManager = new Mock(); + settingsManager + .Setup(x => x.Decrypt("invalid", null)) + .Throws(new System.Exception("invalid token")); + + var controller = new ActionsPageContentsController( + settingsManager.Object, + Mock.Of(), + Mock.Of(), + siteRepository.Object, + Mock.Of(), + Mock.Of()); + + var result = await controller.Submit(new ActionsPageContentsController.SubmitRequest + { + SiteId = 1, + PageChannelId = 1, + TemplateId = 1, + TotalNum = 10, + PageCount = 2, + CurrentPageIndex = 1, + StlPageContentsElement = "encrypted", + Token = "invalid" + }); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); + } } } From abd0e7b242ef66660d645b120a84fca545dbc94e Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:02:20 +0800 Subject: [PATCH 23/85] fix: validate uploaded image content --- src/SSCMS.Core/Services/PathManager.cs | 16 ++++- .../Services/PathManagerUploadImageTests.cs | 63 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Services/PathManagerUploadImageTests.cs diff --git a/src/SSCMS.Core/Services/PathManager.cs b/src/SSCMS.Core/Services/PathManager.cs index 2b2be9cdb..d2a7beafe 100644 --- a/src/SSCMS.Core/Services/PathManager.cs +++ b/src/SSCMS.Core/Services/PathManager.cs @@ -116,10 +116,22 @@ public async Task UploadAsync(byte[] bytes, string filePath) return (false, string.Empty, Constants.ErrorImageSizeAllowed); } + byte[] bytes; + await using (var stream = file.OpenReadStream()) + { + await using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + bytes = memoryStream.ToArray(); + } + if (!ImageUtils.IsValidImage(bytes)) + { + return (false, string.Empty, Constants.ErrorImageExtensionAllowed); + } + var localDirectoryPath = await GetUploadDirectoryPathAsync(site, UploadType.Image); var filePath = PathUtils.Combine(localDirectoryPath, GetUploadFileName(site, fileName)); - await UploadAsync(file, filePath); + await UploadAsync(bytes, filePath); if (site.IsImageAutoResize) { @@ -153,4 +165,4 @@ public async Task UploadAsync(byte[] bytes, string filePath) return (true, filePath, string.Empty); } } -} \ No newline at end of file +} diff --git a/tests/SSCMS.Web.Tests/Services/PathManagerUploadImageTests.cs b/tests/SSCMS.Web.Tests/Services/PathManagerUploadImageTests.cs new file mode 100644 index 000000000..d7fcb2c17 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Services/PathManagerUploadImageTests.cs @@ -0,0 +1,63 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using SSCMS.Core.Services; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using Xunit; + +namespace SSCMS.Web.Tests.Services +{ + public class PathManagerUploadImageTests + { + [Fact] + public async Task UploadImageRejectsNonImageContent() + { + var webRootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(webRootPath); + + try + { + var settingsManager = new Mock(); + settingsManager.Setup(x => x.WebRootPath).Returns(webRootPath); + settingsManager.Setup(x => x.ContentRootPath).Returns(webRootPath); + + var pathManager = new PathManager( + Mock.Of(), + settingsManager.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + + var bytes = Encoding.UTF8.GetBytes("not an image"); + await using var stream = new MemoryStream(bytes); + var file = new FormFile(stream, 0, bytes.Length, "file", "payload.png"); + + var (success, filePath, errorMessage) = await pathManager.UploadImageAsync(new Site + { + Root = true, + ImageUploadExtensions = ".png", + ImageUploadTypeMaxSize = 1024 + }, file); + + Assert.False(success); + Assert.Equal(string.Empty, filePath); + Assert.NotEmpty(errorMessage); + Assert.Empty(Directory.GetFiles(webRootPath, "*", SearchOption.AllDirectories)); + } + finally + { + Directory.Delete(webRootPath, true); + } + } + } +} From e6d5719bef4eca20b87338ae40cefbe4e0e70a76 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:06:55 +0800 Subject: [PATCH 24/85] fix: validate direct image uploads --- src/SSCMS.Core/Utils/ImageUtils.cs | 12 +++++ .../Admin/Clouds/AdminController.Upload.cs | 5 ++ .../ContentsLayerImportController.Upload.cs | 14 +++-- .../Cms/Material/ImageController.Create.cs | 4 ++ .../LayerImageUploadController.Upload.cs | 7 ++- .../TemplatesAssetsController.Upload.cs | 4 ++ .../LayerVideoController.UploadImage.cs | 5 ++ .../Form/LayerImageUploadController.Upload.cs | 7 ++- .../Material/LayerImageController.Upload.cs | 4 ++ .../LayerVideoController.UploadImage.cs | 4 ++ ...nistratorsLayerProfileController.Upload.cs | 4 ++ .../Home/HomeConfigController.Upload.cs | 4 ++ .../UsersLayerProfileController.Upload.cs | 4 ++ .../Admin/Wx/ChatSendController.Upload.cs | 4 ++ .../Admin/Wx/ReplyMessageController.Upload.cs | 4 ++ .../Admin/Wx/SendController.Upload.cs | 4 ++ .../Form/LayerImageUploadController.Upload.cs | 7 ++- .../Home/ProfileController.Upload.cs | 5 ++ .../V1/UsersController.UploadAvatar.cs | 4 ++ .../Controllers/V1/UsersControllerTests.cs | 51 +++++++++++++++++++ 20 files changed, 151 insertions(+), 6 deletions(-) diff --git a/src/SSCMS.Core/Utils/ImageUtils.cs b/src/SSCMS.Core/Utils/ImageUtils.cs index 1e5d6245e..c2be5eb24 100644 --- a/src/SSCMS.Core/Utils/ImageUtils.cs +++ b/src/SSCMS.Core/Utils/ImageUtils.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Numerics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using SixLabors.Fonts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; @@ -105,6 +107,16 @@ public static bool IsValidImage(byte[] imgByte) } } + public static async Task IsValidImageAsync(IFormFile file) + { + if (file == null || file.Length == 0) return false; + + await using var stream = file.OpenReadStream(); + await using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + return IsValidImage(memoryStream.ToArray()); + } + public static void ResizeImageIfExceeding(string imagePath, int resizeWidth) { if (string.IsNullOrEmpty(imagePath) || resizeWidth <= 0) return; diff --git a/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs index 6f6244602..09aab75ef 100644 --- a/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Utils; namespace SSCMS.Web.Controllers.Admin.Clouds @@ -23,6 +24,10 @@ public async Task> Upload([FromQuery] string type, [F { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var fileName = $"{type}{extension}"; var filePath = _pathManager.GetSiteFilesPath(fileName); await _pathManager.UploadAsync(file, filePath); diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs index 0cd04e7db..95c0d7fb5 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs @@ -59,7 +59,12 @@ public async Task> Upload([FromQuery] UploadRequest r return this.Error(Constants.ErrorUpload); } - (_, filePath, _) = await _pathManager.UploadImageAsync(site, file); + var (success, imageFilePath, errorMessage) = await _pathManager.UploadImageAsync(site, file); + if (!success) + { + return this.Error(errorMessage); + } + filePath = imageFilePath; url = await _pathManager.GetVirtualUrlByPhysicalPathAsync(site, filePath); } else if (request.ImportType == "txt") @@ -70,7 +75,10 @@ public async Task> Upload([FromQuery] UploadRequest r } } - await _pathManager.UploadAsync(file, filePath); + if (request.ImportType != "image") + { + await _pathManager.UploadAsync(file, filePath); + } if (request.ImportType == "excel") { @@ -124,4 +132,4 @@ public async Task> Upload([FromQuery] UploadRequest r }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs index 8236e3a8b..48696e834 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs @@ -38,6 +38,10 @@ public async Task> Create([FromQuery] CreateRequest { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var materialFileName = PathUtils.GetMaterialFileName(fileName); var virtualDirectoryPath = PathUtils.GetMaterialVirtualDirectoryPath(UploadType.Image); diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs index 0ff72303b..1c8c75826 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Enums; using SSCMS.Utils; @@ -30,6 +31,10 @@ public async Task> Upload([FromQuery] int siteId, [Fr { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var materialFileName = PathUtils.GetMaterialFileName(fileName); var virtualDirectoryPath = PathUtils.GetMaterialVirtualDirectoryPath(UploadType.Image); @@ -47,4 +52,4 @@ public async Task> Upload([FromQuery] int siteId, [Fr }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs index af2651b33..1cd9fa927 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs @@ -70,6 +70,10 @@ public async Task> Upload([FromQuery] UploadRequest req { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } } else { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs index ab0a1d5f8..786ac3da7 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Dto; using SSCMS.Enums; using SSCMS.Utils; @@ -33,6 +34,10 @@ public async Task> UploadImage([FromQuery] SiteR { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var localDirectoryPath = await _pathManager.GetUploadDirectoryPathAsync(site, UploadType.Image); var filePath = PathUtils.Combine(localDirectoryPath, _pathManager.GetUploadFileName(site, fileName)); diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs index 32b463788..f98791a8e 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Enums; using SSCMS.Utils; @@ -38,6 +39,10 @@ public async Task> Upload([FromQuery] int siteId, [Fr { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } await _pathManager.UploadAsync(file, filePath); } @@ -51,4 +56,4 @@ public async Task> Upload([FromQuery] int siteId, [Fr }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs index 0167c82ef..266b46f40 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs @@ -40,6 +40,10 @@ public async Task> Upload([FromQuery] SiteRequest req { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var virtualUrl = PathUtils.GetMaterialVirtualFilePath(UploadType.Image, _pathManager.GetUploadFileName(site, fileName)); var filePath = PathUtils.Combine(_settingsManager.WebRootPath, virtualUrl); diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs index 2ee50fa91..a4b9560a7 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs @@ -40,6 +40,10 @@ public async Task> UploadImage([FromQuery] SiteReques { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var localDirectoryPath = await _pathManager.GetUploadDirectoryPathAsync(site, UploadType.Image); var filePath = PathUtils.Combine(localDirectoryPath, _pathManager.GetUploadFileName(site, fileName)); diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs index f3d3d80b1..54eac89ec 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs @@ -28,6 +28,10 @@ public async Task> Upload([FromQuery] int userId, [Fr { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } await _pathManager.UploadAsync(file, filePath); diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs index e9da54e30..9173549ce 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs @@ -25,6 +25,10 @@ public async Task> Upload([FromForm] IFormFile file) { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var filePath = _pathManager.GetHomeUploadPath(fileName); await _pathManager.UploadAsync(file, filePath); diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs index e5844f00d..bf0531d75 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs @@ -26,6 +26,10 @@ public async Task> Upload([FromQuery] int userId, [Fr { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } await _pathManager.UploadAsync(file, filePath); diff --git a/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs index c3ba9c911..da1a72891 100644 --- a/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs @@ -44,6 +44,10 @@ public async Task> Upload([FromQuery] UploadRequest r { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var materialFileName = PathUtils.GetMaterialFileName(fileName); var virtualDirectoryPath = PathUtils.GetMaterialVirtualDirectoryPath(UploadType.Image); diff --git a/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs index dbfcc645f..a219d1c51 100644 --- a/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs @@ -45,6 +45,10 @@ public async Task> Upload([FromQuery] UploadRequest r { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var materialFileName = PathUtils.GetMaterialFileName(fileName); var virtualDirectoryPath = PathUtils.GetMaterialVirtualDirectoryPath(UploadType.Image); diff --git a/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs index d9f982fa8..2896a5b66 100644 --- a/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs @@ -44,6 +44,10 @@ public async Task> Upload([FromQuery] UploadRequest r { return this.Error(Constants.ErrorImageSizeAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } var materialFileName = PathUtils.GetMaterialFileName(fileName); var virtualDirectoryPath = PathUtils.GetMaterialVirtualDirectoryPath(UploadType.Image); diff --git a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs index 28f00a492..5cc9dd555 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Enums; using SSCMS.Utils; @@ -43,6 +44,10 @@ public async Task> Upload([FromQuery] int siteId, [Fr { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } await _pathManager.UploadAsync(file, filePath); } @@ -54,4 +59,4 @@ public async Task> Upload([FromQuery] int siteId, [Fr }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs b/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs index d0f67d8a2..fa7477fea 100644 --- a/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Dto; using SSCMS.Utils; @@ -20,6 +21,10 @@ public async Task> Upload([FromForm] IFormFile file) { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } await _pathManager.UploadAsync(file, filePath); diff --git a/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs b/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs index 801316693..e57132555 100644 --- a/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs +++ b/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs @@ -42,6 +42,10 @@ public async Task> UploadAvatar([FromRoute] int id, [FromForm { return this.Error(Constants.ErrorImageExtensionAllowed); } + if (!await ImageUtils.IsValidImageAsync(file)) + { + return this.Error(Constants.ErrorImageExtensionAllowed); + } await _pathManager.UploadAsync(file, filePath); diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs index d319412d2..f2fb78717 100644 --- a/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs @@ -1,9 +1,12 @@ using System.Collections.Generic; +using System.IO; +using System.Text; using System.Threading.Tasks; using CacheManager.Core; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Moq; +using SSCMS.Models; using SSCMS.Repositories; using SSCMS.Services; using SSCMS.Web.Controllers.V1; @@ -55,6 +58,54 @@ public async Task LoginRateLimitsRepeatedFailures() userRepository.Verify(x => x.ValidateAsync("user", "bad-password", true), Times.Exactly(10)); } + [Fact] + public async Task UploadAvatarRejectsNonImageContent() + { + var authManager = new Mock(); + authManager.Setup(x => x.ApiToken).Returns("token"); + authManager + .Setup(x => x.HasAppPermissionsAsync(It.IsAny())) + .ReturnsAsync(true); + + var accessTokenRepository = new Mock(); + accessTokenRepository + .Setup(x => x.IsScopeAsync("token", It.IsAny())) + .ReturnsAsync(true); + + var userRepository = new Mock(); + userRepository + .Setup(x => x.GetByUserIdAsync(1)) + .ReturnsAsync(new User + { + Id = 1 + }); + + var pathManager = new Mock(); + pathManager.Setup(x => x.GetUserUploadFileName("payload.png")).Returns("payload.png"); + pathManager.Setup(x => x.GetUserUploadPath(1, "payload.png")).Returns("/tmp/payload.png"); + + var controller = new UsersController( + authManager.Object, + pathManager.Object, + Mock.Of(), + accessTokenRepository.Object, + userRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + new TestCacheManager()); + + var bytes = Encoding.UTF8.GetBytes("not an image"); + await using var stream = new MemoryStream(bytes); + var result = await controller.UploadAvatar(1, new FormFile(stream, 0, bytes.Length, "file", "payload.png")); + + Assert.IsType(result.Result); + pathManager.Verify(x => x.UploadAsync(It.IsAny(), It.IsAny()), Times.Never); + userRepository.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); + } + private class TestCacheManager : ICacheManager { private readonly Dictionary _cache = new Dictionary(); From 11e63f1eab266f48d090a7870438168ad271692c Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:10:39 +0800 Subject: [PATCH 25/85] fix: cap upload request sizes --- .../Admin/Clouds/AdminController.Upload.cs | 2 +- .../Cms/Channels/ChannelsController.Upload.cs | 2 +- .../ContentsLayerImportController.Upload.cs | 2 +- .../ContentsLayerWordController.Upload.cs | 2 +- .../Cms/Editor/EditorController.Upload.cs | 2 +- .../Cms/Forms/FormDataController.Import.cs | 2 +- .../Cms/Forms/FormListController.Import.cs | 3 +- .../Cms/Forms/FormStylesController.Import.cs | 3 +- .../Forms/FormTemplatesController.Import.cs | 2 +- .../Cms/Material/AudioController.Create.cs | 2 +- .../Cms/Material/FileController.Create.cs | 2 +- .../Cms/Material/ImageController.Create.cs | 2 +- .../LayerImageUploadController.Upload.cs | 2 +- .../LayerVideoUploadController.Submit.cs | 2 +- .../Cms/Material/MessageController.Create.cs | 2 +- .../SettingsStyleChannelController.Import.cs | 2 +- .../SettingsStyleContentController.Import.cs | 2 +- ...tingsStyleRelatedFieldController.Import.cs | 2 +- .../SettingsStyleSiteController.Import.cs | 2 +- .../SettingsWaterMarkController.Upload.cs | 2 +- .../TemplatesAssetsController.Upload.cs | 2 +- .../Templates/TemplatesController.Import.cs | 2 +- .../TemplatesSpecialController.Upload.cs | 2 +- .../Editor/ActionsController.UploadFile.cs | 2 +- .../Editor/ActionsController.UploadImage.cs | 2 +- .../Editor/ActionsController.UploadVideo.cs | 2 +- .../Editor/LayerAudioController.Upload.cs | 2 +- .../Editor/LayerFileController.Upload.cs | 2 +- .../Editor/LayerImageController.Upload.cs | 2 +- .../LayerVideoController.UploadImage.cs | 2 +- .../LayerVideoController.UploadVideo.cs | 2 +- .../Editor/LayerWordController.Upload.cs | 2 +- .../Form/LayerFileUploadController.Upload.cs | 2 +- .../Form/LayerImageUploadController.Upload.cs | 2 +- .../Form/LayerVideoUploadController.Submit.cs | 2 +- .../Material/LayerImageController.Upload.cs | 2 +- .../LayerVideoController.UploadImage.cs | 2 +- .../LayerVideoController.UploadVideo.cs | 2 +- .../Material/LayerWordController.Upload.cs | 2 +- .../AddLayerUploadController.Upload.cs | 2 +- .../AdministratorsController.Import.cs | 2 +- ...nistratorsLayerProfileController.Upload.cs | 2 +- .../Home/HomeConfigController.Upload.cs | 2 +- .../Sites/SitesAddController.Upload.cs | 2 +- .../Sites/SitesTemplatesController.Upload.cs | 2 +- .../Settings/Users/UsersController.Import.cs | 2 +- .../UsersLayerProfileController.Upload.cs | 2 +- .../Users/UsersStyleController.Import.cs | 2 +- .../Admin/Wx/ChatSendController.Upload.cs | 2 +- .../Admin/Wx/ReplyMessageController.Upload.cs | 2 +- .../Admin/Wx/SendController.Upload.cs | 2 +- .../Editor/ActionsController.UploadFile.cs | 2 +- .../Editor/ActionsController.UploadImage.cs | 2 +- .../Editor/ActionsController.UploadVideo.cs | 2 +- .../Editor/LayerAudioController.Upload.cs | 2 +- .../Editor/LayerFileController.Upload.cs | 2 +- .../Editor/LayerImageController.Upload.cs | 2 +- .../LayerVideoController.UploadImage.cs | 2 +- .../LayerVideoController.UploadVideo.cs | 2 +- .../Editor/LayerWordController.Upload.cs | 2 +- .../Form/LayerFileUploadController.Upload.cs | 2 +- .../Form/LayerImageUploadController.Upload.cs | 2 +- .../Form/LayerVideoUploadController.Upload.cs | 2 +- .../Home/ProfileController.Upload.cs | 2 +- .../ContentsLayerWordController.Upload.cs | 2 +- .../Controllers/V1/FormsController.Upload.cs | 2 +- .../V1/UsersController.UploadAvatar.cs | 2 +- src/SSCMS.Web/Program.cs | 4 +-- src/SSCMS/Configuration/Constants.cs | 1 + .../Security/RequestSizeLimitTests.cs | 33 +++++++++++++++++++ 70 files changed, 105 insertions(+), 69 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs index 09aab75ef..33beb169f 100644 --- a/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Clouds/AdminController.Upload.cs @@ -9,7 +9,7 @@ namespace SSCMS.Web.Controllers.Admin.Clouds { public partial class AdminController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] string type, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Channels/ChannelsController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Channels/ChannelsController.Upload.cs index 4cb3747ab..3ea36b6e6 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Channels/ChannelsController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Channels/ChannelsController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Channels { public partial class ChannelsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] int siteId, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs index 95c0d7fb5..fb96e8124 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Upload.cs @@ -15,7 +15,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Contents { public partial class ContentsLayerImportController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Upload.cs index ee5e3d9f7..adf8c59dd 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Contents { public partial class ContentsLayerWordController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] ChannelRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs index 6471aa7c2..3cc5f981e 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Editor { public partial class EditorController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataController.Import.cs index a4d184683..d5da365cb 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormDataController.Import.cs @@ -13,7 +13,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Forms { public partial class FormDataController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] ImportRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormListController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormListController.Import.cs index 2d401a8fb..6b97f57fc 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormListController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormListController.Import.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using SSCMS.Configuration; using SSCMS.Core.Utils; using SSCMS.Dto; using SSCMS.Utils; @@ -10,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Forms { public partial class FormListController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormStylesController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormStylesController.Import.cs index 44b5d5c77..03fcb0d73 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormStylesController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormStylesController.Import.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using SSCMS.Configuration; using SSCMS.Core.Repositories; using SSCMS.Core.Utils; using SSCMS.Dto; @@ -10,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Forms { public partial class FormStylesController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] FormRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormTemplatesController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormTemplatesController.Import.cs index 0c4e0e3fe..697b549d1 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormTemplatesController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Forms/FormTemplatesController.Import.cs @@ -14,7 +14,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Forms { public partial class FormTemplatesController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/AudioController.Create.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/AudioController.Create.cs index c64e665b6..5d707e4c3 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/AudioController.Create.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/AudioController.Create.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Material { public partial class AudioController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(Route)] public async Task> Create([FromQuery] CreateRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/FileController.Create.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/FileController.Create.cs index 820cd1ccd..1069b9a2b 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/FileController.Create.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/FileController.Create.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Material { public partial class FileController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(Route)] public async Task> Create([FromQuery] CreateRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs index 48696e834..eab77a914 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/ImageController.Create.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Material { public partial class ImageController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(Route)] public async Task> Create([FromQuery] CreateRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs index 1c8c75826..08edcf1b1 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerImageUploadController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Material { public partial class LayerImageUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] int siteId, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerVideoUploadController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerVideoUploadController.Submit.cs index 333a80185..ce35cc9b2 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerVideoUploadController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/LayerVideoUploadController.Submit.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Material { public partial class LayerVideoUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(Route)] public async Task> Submit([FromQuery] SubmitRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Material/MessageController.Create.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Material/MessageController.Create.cs index f0170df9d..12031ed01 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Material/MessageController.Create.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Material/MessageController.Create.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Material { public partial class MessageController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(Route)] public async Task> Create([FromQuery] CreateRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleChannelController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleChannelController.Import.cs index 7b3ed87db..9e6152063 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleChannelController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleChannelController.Import.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Settings { public partial class SettingsStyleChannelController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] ImportRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleContentController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleContentController.Import.cs index fa78a8928..a7ee188e3 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleContentController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleContentController.Import.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Settings { public partial class SettingsStyleContentController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] ImportRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleRelatedFieldController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleRelatedFieldController.Import.cs index 4b739c1cd..fb6622c2e 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleRelatedFieldController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleRelatedFieldController.Import.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Settings { public partial class SettingsStyleRelatedFieldController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleSiteController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleSiteController.Import.cs index b4f74e49a..60032babc 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleSiteController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsStyleSiteController.Import.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Settings { public partial class SettingsStyleSiteController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsWaterMarkController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsWaterMarkController.Upload.cs index f8e86038b..2d20c1172 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsWaterMarkController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Settings/SettingsWaterMarkController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Settings { public partial class SettingsWaterMarkController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs index 1cd9fa927..855967086 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesAssetsController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Templates { public partial class TemplatesAssetsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesController.Import.cs index bf3f8acc2..00eaf60e5 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesController.Import.cs @@ -13,7 +13,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Templates { public partial class TemplatesController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] ImportRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesSpecialController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesSpecialController.Upload.cs index 497f22183..61c2ba4ed 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesSpecialController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Templates/TemplatesSpecialController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Cms.Templates { public partial class TemplatesSpecialController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs index 2cbd6c5f8..a12ccfbfb 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class ActionsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUploadFile)] public async Task> UploadFile([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs index 8f6dbe0e2..d405ea919 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class ActionsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUploadImage)] public async Task> UploadImage([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs index 9c5fc02b2..8933723a7 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class ActionsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUploadVideo)] public async Task> UploadVideo([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerAudioController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerAudioController.Upload.cs index 8493c04e5..9d42ca9bd 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerAudioController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerAudioController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class LayerAudioController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerFileController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerFileController.Upload.cs index eb35c1061..9851ba2d7 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerFileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerFileController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class LayerFileController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerImageController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerImageController.Upload.cs index 12c96fc8c..7f74174fd 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerImageController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerImageController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class LayerImageController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs index 786ac3da7..058f32798 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadImage.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class LayerVideoController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUploadImage)] public async Task> UploadImage([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadVideo.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadVideo.cs index ac71134c1..5e8e5777e 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadVideo.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerVideoController.UploadVideo.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class LayerVideoController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUploadVideo)] public async Task> UploadVideo([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerWordController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerWordController.Upload.cs index 498f642fb..ad6a365f9 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerWordController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/LayerWordController.Upload.cs @@ -9,7 +9,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Editor { public partial class LayerWordController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerFileUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerFileUploadController.Upload.cs index 0021ff9c6..b7f0d73bf 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerFileUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerFileUploadController.Upload.cs @@ -9,7 +9,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Form { public partial class LayerFileUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs index f98791a8e..68e9e17af 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerImageUploadController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Form { public partial class LayerImageUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] int siteId, [FromQuery] int userId, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerVideoUploadController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerVideoUploadController.Submit.cs index 24bdc3f5b..3862fd0bc 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerVideoUploadController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Form/LayerVideoUploadController.Submit.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Form { public partial class LayerVideoUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(Route)] public async Task> Submit([FromQuery] SubmitRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs index 266b46f40..f70912d54 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerImageController.Upload.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Material { public partial class LayerImageController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs index a4b9560a7..90535dfc4 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadImage.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Material { public partial class LayerVideoController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUploadImage)] public async Task> UploadImage([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadVideo.cs b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadVideo.cs index 8e21a5bb4..b5ed90a31 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadVideo.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerVideoController.UploadVideo.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Material { public partial class LayerVideoController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUploadVideo)] public async Task> UploadVideo([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerWordController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerWordController.Upload.cs index fb9ba2e04..358127178 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerWordController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Material/LayerWordController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Common.Material { public partial class LayerWordController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs index 92b74f149..281b00b30 100644 --- a/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Plugins { public partial class AddLayerUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUpload)] public async Task> Upload([FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.Import.cs index b914d4994..80936cbc0 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.Import.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Administrators { public partial class AdministratorsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs index 54eac89ec..3bd3c623f 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsLayerProfileController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Administrators { public partial class AdministratorsLayerProfileController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] int userId, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs index 9173549ce..73f969ed5 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Home/HomeConfigController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Home { public partial class HomeConfigController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesAddController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesAddController.Upload.cs index 6cb33ee1f..5803a05c7 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesAddController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesAddController.Upload.cs @@ -9,7 +9,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Sites { public partial class SitesAddController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesTemplatesController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesTemplatesController.Upload.cs index 1124cdc6b..819849464 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesTemplatesController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesTemplatesController.Upload.cs @@ -9,7 +9,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Sites { public partial class SitesTemplatesController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersController.Import.cs index ea2b9c862..38f1a988c 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersController.Import.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Users { public partial class UsersController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromQuery] ImportRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs index bf0531d75..3f8a60e0d 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersLayerProfileController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Users { public partial class UsersLayerProfileController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] int userId, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersStyleController.Import.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersStyleController.Import.cs index 0b45ada11..4682ba99c 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersStyleController.Import.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Users/UsersStyleController.Import.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Settings.Users { public partial class UsersStyleController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteImport)] public async Task> Import([FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs index da1a72891..e66f62776 100644 --- a/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Wx/ChatSendController.Upload.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Wx { public partial class ChatSendController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs index a219d1c51..a6c0e7923 100644 --- a/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Wx/ReplyMessageController.Upload.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Wx { public partial class ReplyMessageController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs index 2896a5b66..80241d0f3 100644 --- a/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Wx/SendController.Upload.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.Admin.Wx { public partial class SendController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadFile.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadFile.cs index 2c5f9e780..dacf8dff1 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadFile.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadFile.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class ActionsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUploadFile)] public async Task> UploadFile([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadImage.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadImage.cs index 440ff8133..83e0a9edb 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadImage.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class ActionsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUploadImage)] public async Task> UploadImage([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadVideo.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadVideo.cs index 6871e5ff5..5f329f8ee 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadVideo.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/ActionsController.UploadVideo.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class ActionsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteActionsUploadVideo)] public async Task> UploadVideo([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerAudioController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerAudioController.Upload.cs index 32a2839bf..c408e42b7 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerAudioController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerAudioController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class LayerAudioController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerFileController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerFileController.Upload.cs index dd0db7e7e..a1c22fa55 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerFileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerFileController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class LayerFileController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerImageController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerImageController.Upload.cs index 290b69fdb..4fff22b10 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerImageController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerImageController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class LayerImageController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadImage.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadImage.cs index 43c932a02..c11933dc0 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadImage.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class LayerVideoController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUploadImage)] public async Task> UploadImage([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadVideo.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadVideo.cs index 6019ed9b1..10082ccfd 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadVideo.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerVideoController.UploadVideo.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class LayerVideoController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUploadVideo)] public async Task> UploadVideo([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerWordController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerWordController.Upload.cs index e78e986fb..7b3ff0366 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerWordController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Editor/LayerWordController.Upload.cs @@ -9,7 +9,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Editor { public partial class LayerWordController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] SiteRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerFileUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerFileUploadController.Upload.cs index ffbfebad8..321edce2c 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerFileUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerFileUploadController.Upload.cs @@ -9,7 +9,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Form { public partial class LayerFileUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs index 5cc9dd555..350c13807 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerImageUploadController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Form { public partial class LayerImageUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] int siteId, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerVideoUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerVideoUploadController.Upload.cs index 905425dd7..492217446 100644 --- a/src/SSCMS.Web/Controllers/Home/Common/Form/LayerVideoUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Common/Form/LayerVideoUploadController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Home.Common.Form { public partial class LayerVideoUploadController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs b/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs index fa7477fea..52c447cc8 100644 --- a/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/ProfileController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Home { public partial class ProfileController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Upload.cs b/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Upload.cs index 93136556b..b78c49a13 100644 --- a/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Upload.cs @@ -10,7 +10,7 @@ namespace SSCMS.Web.Controllers.Home.Write { public partial class ContentsLayerWordController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] ChannelRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/V1/FormsController.Upload.cs b/src/SSCMS.Web/Controllers/V1/FormsController.Upload.cs index 77b166c0a..f7f407d2d 100644 --- a/src/SSCMS.Web/Controllers/V1/FormsController.Upload.cs +++ b/src/SSCMS.Web/Controllers/V1/FormsController.Upload.cs @@ -11,7 +11,7 @@ namespace SSCMS.Web.Controllers.V1 { public partial class FormsController { - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs b/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs index e57132555..74f3d847f 100644 --- a/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs +++ b/src/SSCMS.Web/Controllers/V1/UsersController.UploadAvatar.cs @@ -12,7 +12,7 @@ namespace SSCMS.Web.Controllers.V1 public partial class UsersController { [OpenApiOperation("上传用户头像 API", "上传用户头像,使用POST发起请求,请求地址为/api/v1/users/{id}/avatar")] - [RequestSizeLimit(long.MaxValue)] + [RequestSizeLimit(Constants.MaxUploadRequestSize)] [HttpPost, Route(RouteUserAvatar)] public async Task> UploadAvatar([FromRoute] int id, [FromForm] IFormFile file) { diff --git a/src/SSCMS.Web/Program.cs b/src/SSCMS.Web/Program.cs index 756be7c80..62db2b72f 100644 --- a/src/SSCMS.Web/Program.cs +++ b/src/SSCMS.Web/Program.cs @@ -31,7 +31,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder - .UseKestrel(options => { options.Limits.MaxRequestBodySize = long.MaxValue; }) + .UseKestrel(options => { options.Limits.MaxRequestBodySize = Constants.MaxUploadRequestSize; }) .UseIIS() .UseStartup(); }) @@ -41,4 +41,4 @@ public static IHostBuilder CreateHostBuilder(string[] args) => loggerConfiguration.Enrich.FromLogContext(); }); } -} \ No newline at end of file +} diff --git a/src/SSCMS/Configuration/Constants.cs b/src/SSCMS/Configuration/Constants.cs index 1a89a54be..369b16371 100644 --- a/src/SSCMS/Configuration/Constants.cs +++ b/src/SSCMS/Configuration/Constants.cs @@ -19,6 +19,7 @@ public static class Constants public const string EncryptStingIndicator = "0secret0"; public const int AccessTokenExpireDays = 7; + public const long MaxUploadRequestSize = 100L * 1024 * 1024; public const string PagePlaceHolder = "[SITESERVER_PAGE]";//内容翻页占位符 diff --git a/tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs b/tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs new file mode 100644 index 000000000..f4ced6ca7 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Mvc; +using SSCMS.Web.Controllers.Admin; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class RequestSizeLimitTests + { + [Fact] + public void ControllersDoNotDisableRequestSizeLimits() + { + var offenders = typeof(InstallController).Assembly + .GetTypes() + .Where(type => type.Namespace != null && type.Namespace.StartsWith("SSCMS.Web.Controllers", StringComparison.Ordinal)) + .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)) + .Select(method => new + { + Method = method, + Attribute = method.GetCustomAttribute() + }) + .Where(x => ((IRequestSizeLimitMetadata)x.Attribute)?.MaxRequestBodySize == long.MaxValue) + .Select(x => $"{x.Method.DeclaringType?.FullName}.{x.Method.Name}") + .OrderBy(x => x) + .ToList(); + + Assert.Empty(offenders); + } + } +} From 95f0ce334e3b0cb2e8e63db538e703f346a12b7b Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:14:16 +0800 Subject: [PATCH 26/85] fix: validate v1 content query fields --- .../Controllers/V1/ContentsController.List.cs | 4 + .../Controllers/V1/ContentsController.cs | 63 ++++++++++++ .../Controllers/V1/ContentsControllerTests.cs | 99 +++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/V1/ContentsController.List.cs b/src/SSCMS.Web/Controllers/V1/ContentsController.List.cs index 856179a29..029f763d2 100644 --- a/src/SSCMS.Web/Controllers/V1/ContentsController.List.cs +++ b/src/SSCMS.Web/Controllers/V1/ContentsController.List.cs @@ -19,6 +19,10 @@ public async Task> List([FromBody] QueryRequest reques { return Unauthorized(); } + if (!TryValidateQueryRequest(request, out var queryErrorMessage)) + { + return this.Error(queryErrorMessage); + } var site = await _siteRepository.GetAsync(request.SiteId); if (site == null) return this.Error(Constants.ErrorNotFound); diff --git a/src/SSCMS.Web/Controllers/V1/ContentsController.cs b/src/SSCMS.Web/Controllers/V1/ContentsController.cs index bb6d6f1f2..0da04b4ca 100644 --- a/src/SSCMS.Web/Controllers/V1/ContentsController.cs +++ b/src/SSCMS.Web/Controllers/V1/ContentsController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Datory; using Microsoft.AspNetCore.Mvc; @@ -32,6 +33,21 @@ public partial class ContentsController : ControllerBase public const string OpNotIn = "NotIn"; public const string OpLike = "Like"; public const string OpNotLike = "NotLike"; + private static readonly Regex SafeQueryIdentifierRegex = new Regex(@"^[A-Za-z_][A-Za-z0-9_]*$", RegexOptions.Compiled); + private static readonly HashSet SafeQueryOperators = new HashSet + { + OpEquals, + "!=", + "<>", + ">", + ">=", + "<", + "<=", + OpIn, + OpNotIn, + OpLike, + OpNotLike + }; private readonly IAuthManager _authManager; private readonly ICreateManager _createManager; @@ -107,6 +123,53 @@ public class CheckResult public List Contents { get; set; } } + private static bool IsSafeQueryIdentifier(string identifier) + { + return !string.IsNullOrEmpty(identifier) && SafeQueryIdentifierRegex.IsMatch(identifier); + } + + private static bool IsSafeQueryOperator(string op) + { + return !string.IsNullOrEmpty(op) && SafeQueryOperators.Contains(op); + } + + private static bool TryValidateQueryRequest(QueryRequest request, out string errorMessage) + { + errorMessage = string.Empty; + if (request?.Wheres != null) + { + foreach (var where in request.Wheres) + { + if (!IsSafeQueryIdentifier(where.Column)) + { + errorMessage = "Invalid query column"; + return false; + } + + var op = string.IsNullOrEmpty(where.Operator) ? OpEquals : where.Operator; + if (!IsSafeQueryOperator(op)) + { + errorMessage = "Invalid query operator"; + return false; + } + } + } + + if (request?.Orders != null) + { + foreach (var order in request.Orders) + { + if (!IsSafeQueryIdentifier(order.Column)) + { + errorMessage = "Invalid query column"; + return false; + } + } + } + + return true; + } + private async Task GetQueryAsync(int siteId, int? channelId, QueryRequest request) { var query = Q.Where(nameof(Models.Content.SiteId), siteId).Where(nameof(Models.Content.ChannelId), ">", 0); diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs new file mode 100644 index 000000000..fd9558e2a --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Configuration; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.V1; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.V1 +{ + public class ContentsControllerTests + { + [Fact] + public async Task ListRejectsUnsafeWhereColumnsBeforeQueryingSite() + { + var authManager = new Mock(); + authManager.Setup(x => x.ApiToken).Returns("token"); + + var accessTokenRepository = new Mock(); + accessTokenRepository + .Setup(x => x.IsScopeAsync("token", Constants.ScopeContents)) + .ReturnsAsync(true); + + var siteRepository = new Mock(); + + var controller = new ContentsController( + authManager.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + accessTokenRepository.Object, + siteRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of()); + + var result = await controller.List(new ContentsController.QueryRequest + { + SiteId = 1, + Wheres = new List + { + new ContentsController.ClauseWhere + { + Column = "Title; drop table siteserver_Administrator", + Value = "test" + } + } + }); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task ListRejectsUnsafeOrderColumnsBeforeQueryingSite() + { + var authManager = new Mock(); + authManager.Setup(x => x.ApiToken).Returns("token"); + + var accessTokenRepository = new Mock(); + accessTokenRepository + .Setup(x => x.IsScopeAsync("token", Constants.ScopeContents)) + .ReturnsAsync(true); + + var siteRepository = new Mock(); + + var controller = new ContentsController( + authManager.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + accessTokenRepository.Object, + siteRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of()); + + var result = await controller.List(new ContentsController.QueryRequest + { + SiteId = 1, + Orders = new List + { + new ContentsController.ClauseOrder + { + Column = "Title desc; drop table siteserver_Administrator" + } + } + }); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); + } + } +} From ec68c6a66d16147d500ece2c86002243b78e6818 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:16:14 +0800 Subject: [PATCH 27/85] fix: require permission for v1 user lookup --- .../Controllers/V1/UsersController.Get.cs | 5 +++ .../Controllers/V1/UsersControllerTests.cs | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/SSCMS.Web/Controllers/V1/UsersController.Get.cs b/src/SSCMS.Web/Controllers/V1/UsersController.Get.cs index 28b26e97d..9ed74c067 100644 --- a/src/SSCMS.Web/Controllers/V1/UsersController.Get.cs +++ b/src/SSCMS.Web/Controllers/V1/UsersController.Get.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Models; using SSCMS.Utils; @@ -17,6 +18,10 @@ public async Task> Get([FromRoute] string account) { return Unauthorized(); } + if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsUsers)) + { + return Unauthorized(); + } if (string.IsNullOrEmpty(account)) { diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs index f2fb78717..dccae1583 100644 --- a/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/V1/UsersControllerTests.cs @@ -106,6 +106,42 @@ public async Task UploadAvatarRejectsNonImageContent() userRepository.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); } + [Fact] + public async Task GetRequiresSettingsUsersPermission() + { + var authManager = new Mock(); + authManager.Setup(x => x.ApiToken).Returns("token"); + authManager + .Setup(x => x.HasAppPermissionsAsync(It.IsAny())) + .ReturnsAsync(false); + + var accessTokenRepository = new Mock(); + accessTokenRepository + .Setup(x => x.IsScopeAsync("token", It.IsAny())) + .ReturnsAsync(true); + + var userRepository = new Mock(); + + var controller = new UsersController( + authManager.Object, + Mock.Of(), + Mock.Of(), + accessTokenRepository.Object, + userRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + new TestCacheManager()); + + var result = await controller.Get("alice"); + + Assert.IsType(result.Result); + userRepository.Verify(x => x.IsUserNameExistsAsync(It.IsAny()), Times.Never); + userRepository.Verify(x => x.GetByUserNameAsync(It.IsAny()), Times.Never); + } + private class TestCacheManager : ICacheManager { private readonly Dictionary _cache = new Dictionary(); From 7a12ae7deb0c610f186932c66c39c292cf6413b5 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:19:18 +0800 Subject: [PATCH 28/85] fix: rate limit agent security key attempts --- .../Admin/AgentController.AddSite.cs | 6 +- .../Admin/AgentController.Install.cs | 8 +- .../Admin/AgentController.Plugins.cs | 8 +- .../Admin/AgentController.Process.cs | 8 +- .../Admin/AgentController.SetDomain.cs | 6 +- .../Admin/AgentController.Sites.cs | 8 +- .../Controllers/Admin/AgentController.cs | 79 +++++++++++- .../Controllers/Admin/AgentControllerTests.cs | 114 ++++++++++++++++++ 8 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/AgentControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/AgentController.AddSite.cs b/src/SSCMS.Web/Controllers/Admin/AgentController.AddSite.cs index 8ac6ae8fb..09acbed6e 100644 --- a/src/SSCMS.Web/Controllers/Admin/AgentController.AddSite.cs +++ b/src/SSCMS.Web/Controllers/Admin/AgentController.AddSite.cs @@ -13,13 +13,13 @@ public partial class AgentController [HttpPost, Route(RouteAddSite)] public async Task> AddSite([FromBody] AddSiteRequest request) { - if (string.IsNullOrEmpty(request.SecurityKey)) + if (request == null) { return this.Error("系统参数不足"); } - if (_settingsManager.SecurityKey != request.SecurityKey) + if (!TryValidateAgentSecurityKey(request.SecurityKey, out var securityKeyErrorMessage)) { - return this.Error("SecurityKey不正确"); + return this.Error(securityKeyErrorMessage); } if (!request.Root) diff --git a/src/SSCMS.Web/Controllers/Admin/AgentController.Install.cs b/src/SSCMS.Web/Controllers/Admin/AgentController.Install.cs index d79fa5477..3915b5248 100644 --- a/src/SSCMS.Web/Controllers/Admin/AgentController.Install.cs +++ b/src/SSCMS.Web/Controllers/Admin/AgentController.Install.cs @@ -15,15 +15,15 @@ public partial class AgentController [HttpPost, Route(RouteInstall)] public async Task> Install([FromBody] InstallRequest request) { - if (string.IsNullOrEmpty(request.SecurityKey) || + if (request == null || string.IsNullOrEmpty(request.UserName) || string.IsNullOrEmpty(request.Password)) { return this.Error("系统参数不足"); } - if (_settingsManager.SecurityKey != request.SecurityKey) + if (!TryValidateAgentSecurityKey(request.SecurityKey, out var securityKeyErrorMessage)) { - return this.Error("SecurityKey不正确"); + return this.Error(securityKeyErrorMessage); } var (success, errorMessage) = await _administratorRepository.InsertValidateAsync(request.UserName, request.Password, string.Empty, string.Empty); if (!success) @@ -143,4 +143,4 @@ public async Task> Install([FromBody] InstallRequest re }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/AgentController.Plugins.cs b/src/SSCMS.Web/Controllers/Admin/AgentController.Plugins.cs index f3ddf2862..1ee8f8214 100644 --- a/src/SSCMS.Web/Controllers/Admin/AgentController.Plugins.cs +++ b/src/SSCMS.Web/Controllers/Admin/AgentController.Plugins.cs @@ -9,13 +9,13 @@ public partial class AgentController [HttpGet, Route(RoutePlugins)] public ActionResult Plugins([FromQuery] AgentRequest request) { - if (string.IsNullOrEmpty(request.SecurityKey)) + if (request == null) { return this.Error("系统参数不足"); } - if (_settingsManager.SecurityKey != request.SecurityKey) + if (!TryValidateAgentSecurityKey(request.SecurityKey, out var securityKeyErrorMessage)) { - return this.Error("SecurityKey不正确"); + return this.Error(securityKeyErrorMessage); } var allPlugins = _pluginManager.Plugins; @@ -37,4 +37,4 @@ public ActionResult Plugins([FromQuery] AgentRequest request) }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/AgentController.Process.cs b/src/SSCMS.Web/Controllers/Admin/AgentController.Process.cs index ff14303a9..9c3ba3025 100644 --- a/src/SSCMS.Web/Controllers/Admin/AgentController.Process.cs +++ b/src/SSCMS.Web/Controllers/Admin/AgentController.Process.cs @@ -9,17 +9,17 @@ public partial class AgentController [HttpPost, Route(RouteProcess)] public ActionResult Process([FromBody] ProcessRequest request) { - if (string.IsNullOrEmpty(request.SecurityKey)) + if (request == null) { return this.Error("系统参数不足"); } - if (_settingsManager.SecurityKey != request.SecurityKey) + if (!TryValidateAgentSecurityKey(request.SecurityKey, out var securityKeyErrorMessage)) { - return this.Error("SecurityKey不正确"); + return this.Error(securityKeyErrorMessage); } var caching = new CacheUtils(_cacheManager); return caching.GetProcess(request.Guid); } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/AgentController.SetDomain.cs b/src/SSCMS.Web/Controllers/Admin/AgentController.SetDomain.cs index ed5a393b4..1d54a96e2 100644 --- a/src/SSCMS.Web/Controllers/Admin/AgentController.SetDomain.cs +++ b/src/SSCMS.Web/Controllers/Admin/AgentController.SetDomain.cs @@ -10,13 +10,13 @@ public partial class AgentController [HttpPost, Route(RouteSetDomain)] public async Task> SetDomain([FromBody] SetDomainRequest request) { - if (string.IsNullOrEmpty(request.SecurityKey)) + if (request == null) { return this.Error("系统参数不足"); } - if (_settingsManager.SecurityKey != request.SecurityKey) + if (!TryValidateAgentSecurityKey(request.SecurityKey, out var securityKeyErrorMessage)) { - return this.Error("SecurityKey不正确"); + return this.Error(securityKeyErrorMessage); } var site = await _siteRepository.GetAsync(request.SiteId); diff --git a/src/SSCMS.Web/Controllers/Admin/AgentController.Sites.cs b/src/SSCMS.Web/Controllers/Admin/AgentController.Sites.cs index 6106aef2d..7f7bde766 100644 --- a/src/SSCMS.Web/Controllers/Admin/AgentController.Sites.cs +++ b/src/SSCMS.Web/Controllers/Admin/AgentController.Sites.cs @@ -9,13 +9,13 @@ public partial class AgentController [HttpGet, Route(RouteSites)] public async Task> Sites([FromQuery] AgentRequest request) { - if (string.IsNullOrEmpty(request.SecurityKey)) + if (request == null) { return this.Error("系统参数不足"); } - if (_settingsManager.SecurityKey != request.SecurityKey) + if (!TryValidateAgentSecurityKey(request.SecurityKey, out var securityKeyErrorMessage)) { - return this.Error("SecurityKey不正确"); + return this.Error(securityKeyErrorMessage); } var sites = await _siteRepository.GetSitesWithChildrenAsync(0, async x => new @@ -33,4 +33,4 @@ public async Task> Sites([FromQuery] AgentRequest requ }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/AgentController.cs b/src/SSCMS.Web/Controllers/Admin/AgentController.cs index 78ea462d5..7387b8069 100644 --- a/src/SSCMS.Web/Controllers/Admin/AgentController.cs +++ b/src/SSCMS.Web/Controllers/Admin/AgentController.cs @@ -1,11 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Dto; using SSCMS.Models; using SSCMS.Repositories; using SSCMS.Services; +using SSCMS.Utils; namespace SSCMS.Web.Controllers.Admin { @@ -13,6 +18,9 @@ namespace SSCMS.Web.Controllers.Admin [Route(Constants.ApiAdminPrefix)] public partial class AgentController : ControllerBase { + private const int SecurityKeyRateLimitWindowMinutes = 10; + private const int SecurityKeyRateLimitMaxFailures = 10; + public const string Route = "agent"; private const string RouteSites = "agent/sites"; private const string RoutePlugins = "agent/plugins"; @@ -103,5 +111,72 @@ public class PluginsResult { public List Plugins { get; set; } } + + private class SecurityKeyRateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + private static string GetSecurityKeyRateLimitCacheKey(string ipAddress) + { + return CacheUtils.GetClassKey(typeof(AgentController), "SecurityKey", ipAddress ?? "unknown"); + } + + private static bool FixedTimeEquals(string left, string right) + { + if (left == null || right == null) return false; + + var leftHash = SHA256.HashData(Encoding.UTF8.GetBytes(left)); + var rightHash = SHA256.HashData(Encoding.UTF8.GetBytes(right)); + return CryptographicOperations.FixedTimeEquals(leftHash, rightHash); + } + + private bool TryValidateAgentSecurityKey(string securityKey, out string errorMessage) + { + errorMessage = string.Empty; + if (string.IsNullOrEmpty(securityKey)) + { + errorMessage = "系统参数不足"; + return false; + } + + var cacheKey = GetSecurityKeyRateLimitCacheKey(PageUtils.GetIpAddress(Request)); + var state = _cacheManager.Get(cacheKey); + if (state != null && state.ExpireAt <= DateTime.Now) + { + state = null; + } + + if (state != null && state.Count >= SecurityKeyRateLimitMaxFailures) + { + var retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + errorMessage = $"请求过于频繁,请在{retryAfterSeconds}秒后重试"; + return false; + } + + if (FixedTimeEquals(_settingsManager.SecurityKey, securityKey)) + { + if (state != null) + { + _cacheManager.Remove(cacheKey); + } + return true; + } + + if (state == null) + { + state = new SecurityKeyRateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(SecurityKeyRateLimitWindowMinutes) + }; + } + + state.Count++; + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, SecurityKeyRateLimitWindowMinutes); + errorMessage = "SecurityKey不正确"; + return false; + } } -} \ No newline at end of file +} diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/AgentControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/AgentControllerTests.cs new file mode 100644 index 000000000..57e0279eb --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/AgentControllerTests.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin +{ + public class AgentControllerTests + { + [Fact] + public async Task InstallRateLimitsRepeatedBadSecurityKeys() + { + var settingsManager = new Mock(); + settingsManager.Setup(x => x.SecurityKey).Returns("correct-key"); + + var administratorRepository = new Mock(); + administratorRepository + .Setup(x => x.InsertValidateAsync("admin", "Password1", string.Empty, string.Empty)) + .ReturnsAsync((true, string.Empty)); + + var controller = new AgentController( + settingsManager.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + new TestCacheManager(), + Mock.Of(), + Mock.Of(), + administratorRepository.Object, + Mock.Of(), + Mock.Of(), + Mock.Of()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + for (var i = 0; i < 10; i++) + { + await controller.Install(new AgentController.InstallRequest + { + SecurityKey = "bad-key", + UserName = "admin", + Password = "Password1" + }); + } + + var result = await controller.Install(new AgentController.InstallRequest + { + SecurityKey = "correct-key", + UserName = "admin", + Password = "Password1" + }); + + Assert.IsType(result.Result); + administratorRepository.Verify(x => x.InsertValidateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } + } +} From f4569e3cbba876197524f2bdcb52797904e83fef Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:24:37 +0800 Subject: [PATCH 29/85] fix: require content check permission on v1 update --- .../V1/ContentsController.Update.cs | 9 ++ .../Controllers/V1/ContentsControllerTests.cs | 93 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs b/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs index d279b6c1b..6178e8a18 100644 --- a/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs +++ b/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs @@ -35,6 +35,9 @@ public async Task> Update([FromRoute] int siteId, [FromRou return Unauthorized(); } + var originalChecked = content.Checked; + var originalCheckedLevel = content.CheckedLevel; + content.LoadDict(request); content.SiteId = siteId; @@ -45,6 +48,12 @@ public async Task> Update([FromRoute] int siteId, [FromRou var isChecked = postCheckedLevel >= site.CheckContentLevel; var checkedLevel = postCheckedLevel; + if (isChecked && !await _authManager.HasContentPermissionsAsync(siteId, channelId, MenuUtils.ContentPermissions.CheckLevel1)) + { + isChecked = originalChecked; + checkedLevel = originalCheckedLevel; + } + content.Checked = isChecked; content.CheckedLevel = checkedLevel; diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs index fd9558e2a..cefb40d18 100644 --- a/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Moq; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Models; using SSCMS.Repositories; using SSCMS.Services; @@ -95,5 +96,97 @@ public async Task ListRejectsUnsafeOrderColumnsBeforeQueryingSite() Assert.IsType(result.Result); siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); } + + [Fact] + public async Task UpdateKeepsContentUnapprovedWithoutCheckPermission() + { + var authManager = new Mock(); + authManager.Setup(x => x.ApiToken).Returns("token"); + authManager + .Setup(x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.Edit)) + .ReturnsAsync(true); + authManager + .Setup(x => x.AddSiteLogAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + + var createManager = new Mock(); + createManager + .Setup(x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + createManager + .Setup(x => x.TriggerContentChangedEventAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var accessTokenRepository = new Mock(); + accessTokenRepository + .Setup(x => x.IsScopeAsync("token", Constants.ScopeContents)) + .ReturnsAsync(true); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Channel { Id = 2, SiteId = 1 }); + channelRepository + .Setup(x => x.GetChannelNameNavigationAsync(1, 2)) + .ReturnsAsync("channel"); + + var content = new Content + { + Id = 3, + SiteId = 1, + ChannelId = 2, + Title = "draft", + Checked = false, + CheckedLevel = 0 + }; + + Content updatedContent = null; + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.GetAsync(It.IsAny(), It.IsAny(), 3)) + .ReturnsAsync(content); + contentRepository + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, updated) => updatedContent = updated) + .Returns(Task.CompletedTask); + + var controller = new ContentsController( + authManager.Object, + createManager.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + accessTokenRepository.Object, + siteRepository.Object, + channelRepository.Object, + contentRepository.Object, + Mock.Of()); + + await controller.Update(1, 2, 3, new Dictionary + { + [nameof(Content.Checked)] = true, + [nameof(Content.CheckedLevel)] = 1 + }); + + Assert.NotNull(updatedContent); + Assert.False(updatedContent.Checked); + Assert.Equal(0, updatedContent.CheckedLevel); + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + createManager.Verify( + x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } } } From a0d296d05992a8c4def8ea5eafdd31b169d0b519 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:35:17 +0800 Subject: [PATCH 30/85] fix: enforce content check permissions on editor saves --- src/SSCMS.Core/Utils/CheckManager.cs | 30 ++ .../Cms/Editor/EditorController.Insert.cs | 6 +- .../Cms/Editor/EditorController.Update.cs | 9 +- .../Home/Write/EditorController.Insert.cs | 6 +- .../Home/Write/EditorController.Update.cs | 9 +- .../Home/Write/EditorController.cs | 1 + .../V1/ContentsController.Update.cs | 7 +- .../Admin/Cms/Editor/EditorControllerTests.cs | 296 ++++++++++++++++++ .../Home/Write/EditorControllerTests.cs | 232 ++++++++++++++ .../Controllers/V1/ContentsControllerTests.cs | 86 +++++ 10 files changed, 657 insertions(+), 25 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs create mode 100644 tests/SSCMS.Web.Tests/Controllers/Home/Write/EditorControllerTests.cs diff --git a/src/SSCMS.Core/Utils/CheckManager.cs b/src/SSCMS.Core/Utils/CheckManager.cs index 237f53fd2..27338dba8 100644 --- a/src/SSCMS.Core/Utils/CheckManager.cs +++ b/src/SSCMS.Core/Utils/CheckManager.cs @@ -722,5 +722,35 @@ public static async Task> GetUserCheckLevelPairAsync(IAu } return (isChecked, checkedLevel); } + + public static async Task<(bool IsChecked, int CheckedLevel)> GetAllowedCheckStateAsync(IAuthManager authManager, Site site, int channelId, int requestedCheckedLevel, Content source = null) + { + if (site.CheckContentLevel <= 0) + { + return (true, 0); + } + + var (userIsChecked, userCheckedLevel) = await GetUserCheckLevelAsync(authManager, site, channelId); + if (requestedCheckedLevel >= site.CheckContentLevel) + { + if (userIsChecked) + { + return (true, 0); + } + + return source != null + ? (source.Checked, source.CheckedLevel) + : (false, userCheckedLevel); + } + + if (requestedCheckedLevel > userCheckedLevel) + { + return source != null + ? (source.Checked, source.CheckedLevel) + : (false, userCheckedLevel); + } + + return (false, requestedCheckedLevel); + } } } diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Insert.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Insert.cs index 9dcef1826..8e424b6b8 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Insert.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Insert.cs @@ -47,11 +47,7 @@ public async Task> Insert([FromBody] SubmitRequest requ } else { - content.Checked = request.Content.CheckedLevel >= site.CheckContentLevel; - if (content.Checked) - { - content.CheckedLevel = 0; - } + (content.Checked, content.CheckedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channel.Id, request.Content.CheckedLevel); } if (content.LinkType == Enums.LinkType.None) diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Update.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Update.cs index 9d14e7fb7..727321073 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Update.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Update.cs @@ -42,17 +42,12 @@ public async Task> Update([FromBody] SubmitRequest requ content.ChannelId = channel.Id; content.LastEditAdminId = adminId; - var isChecked = request.Content.CheckedLevel >= site.CheckContentLevel; - if (isChecked != source.Checked) + (content.Checked, content.CheckedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channel.Id, request.Content.CheckedLevel, source); + if (content.Checked != source.Checked) { content.Set(ColumnsManager.CheckAdminId, adminId); content.Set(ColumnsManager.CheckDate, DateTime.Now); content.Set(ColumnsManager.CheckReasons, string.Empty); - content.Checked = isChecked; - if (isChecked) - { - content.CheckedLevel = 0; - } await _contentCheckRepository.InsertAsync(new ContentCheck { diff --git a/src/SSCMS.Web/Controllers/Home/Write/EditorController.Insert.cs b/src/SSCMS.Web/Controllers/Home/Write/EditorController.Insert.cs index c4d3db7ff..1b1adbd2c 100644 --- a/src/SSCMS.Web/Controllers/Home/Write/EditorController.Insert.cs +++ b/src/SSCMS.Web/Controllers/Home/Write/EditorController.Insert.cs @@ -39,11 +39,7 @@ public async Task> Insert([FromBody] SaveRequest reques content.LastEditAdminId = _authManager.AdminId; content.UserId = _authManager.UserId; - content.Checked = request.Content.CheckedLevel >= site.CheckContentLevel; - if (content.Checked) - { - content.CheckedLevel = 0; - } + (content.Checked, content.CheckedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channel.Id, request.Content.CheckedLevel); await _contentRepository.InsertAsync(site, channel, content); diff --git a/src/SSCMS.Web/Controllers/Home/Write/EditorController.Update.cs b/src/SSCMS.Web/Controllers/Home/Write/EditorController.Update.cs index d4f8263ef..e63613dff 100644 --- a/src/SSCMS.Web/Controllers/Home/Write/EditorController.Update.cs +++ b/src/SSCMS.Web/Controllers/Home/Write/EditorController.Update.cs @@ -41,17 +41,12 @@ public async Task> Update([FromBody] SaveRequest reques content.LastEditAdminId = adminId; content.Hits = source.Hits; - var isChecked = request.Content.CheckedLevel >= site.CheckContentLevel; - if (isChecked != source.Checked) + (content.Checked, content.CheckedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channel.Id, request.Content.CheckedLevel, source); + if (content.Checked != source.Checked) { content.Set(ColumnsManager.CheckAdminId, adminId); content.Set(ColumnsManager.CheckDate, DateTime.Now); content.Set(ColumnsManager.CheckReasons, string.Empty); - content.Checked = isChecked; - if (isChecked) - { - content.CheckedLevel = 0; - } await _contentCheckRepository.InsertAsync(new ContentCheck { diff --git a/src/SSCMS.Web/Controllers/Home/Write/EditorController.cs b/src/SSCMS.Web/Controllers/Home/Write/EditorController.cs index e21972514..b0a3f66bb 100644 --- a/src/SSCMS.Web/Controllers/Home/Write/EditorController.cs +++ b/src/SSCMS.Web/Controllers/Home/Write/EditorController.cs @@ -100,5 +100,6 @@ public class SaveRequest public int ContentId { get; set; } public Content Content { get; set; } } + } } diff --git a/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs b/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs index 6178e8a18..8cab82794 100644 --- a/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs +++ b/src/SSCMS.Web/Controllers/V1/ContentsController.Update.cs @@ -48,7 +48,12 @@ public async Task> Update([FromRoute] int siteId, [FromRou var isChecked = postCheckedLevel >= site.CheckContentLevel; var checkedLevel = postCheckedLevel; - if (isChecked && !await _authManager.HasContentPermissionsAsync(siteId, channelId, MenuUtils.ContentPermissions.CheckLevel1)) + if (site.CheckContentLevel <= 0) + { + isChecked = true; + checkedLevel = 0; + } + else if (isChecked && !await _authManager.HasContentPermissionsAsync(siteId, channelId, MenuUtils.ContentPermissions.CheckLevel1)) { isChecked = originalChecked; checkedLevel = originalCheckedLevel; diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs new file mode 100644 index 000000000..cda778982 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs @@ -0,0 +1,296 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using SSCMS.Core.Utils; +using SSCMS.Dto; +using SSCMS.Enums; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using Xunit; +using EditorController = SSCMS.Web.Controllers.Admin.Cms.Editor.EditorController; + +namespace SSCMS.Web.Tests.Controllers.Admin.Cms.Editor +{ + public class EditorControllerTests + { + [Fact] + public async Task InsertKeepsContentUnapprovedWithoutCheckPermission() + { + var authManager = CreateAuthManager(MenuUtils.ContentPermissions.Add); + var createManager = CreateCreateManager(); + var storageManager = CreateStorageManager(); + var mailManager = CreateMailManager(); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + + var channelRepository = CreateChannelRepository(); + + var requestContent = new Content + { + Id = 3, + Title = "draft", + Checked = true, + CheckedLevel = 1 + }; + + var pathManager = new Mock(); + pathManager + .Setup(x => x.EncodeContentAsync( + It.IsAny(), + It.IsAny(), + requestContent, + It.IsAny())) + .ReturnsAsync(requestContent); + + Content insertedContent = null; + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.InsertAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, content) => insertedContent = content) + .ReturnsAsync(3); + + var controller = CreateController( + authManager.Object, + createManager, + pathManager.Object, + storageManager, + siteRepository.Object, + channelRepository, + contentRepository.Object, + mailManager, + CreateContentTagRepository().Object, + CreateTranslateRepository().Object); + + await controller.Insert(new EditorController.SubmitRequest + { + SiteId = 1, + ChannelId = 2, + Content = requestContent + }); + + Assert.NotNull(insertedContent); + Assert.False(insertedContent.Checked); + Assert.Equal(0, insertedContent.CheckedLevel); + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + + [Fact] + public async Task UpdateKeepsContentUnapprovedWithoutCheckPermission() + { + var authManager = CreateAuthManager(MenuUtils.ContentPermissions.Edit); + var createManager = CreateCreateManager(); + var storageManager = CreateStorageManager(); + var mailManager = CreateMailManager(); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + + var channelRepository = CreateChannelRepository(); + + var sourceContent = new Content + { + Id = 3, + SiteId = 1, + ChannelId = 2, + Title = "source", + Checked = false, + CheckedLevel = 0 + }; + var requestContent = new Content + { + Id = 3, + SiteId = 1, + ChannelId = 2, + Title = "updated", + Checked = true, + CheckedLevel = 1 + }; + + var pathManager = new Mock(); + pathManager + .Setup(x => x.EncodeContentAsync( + It.IsAny(), + It.IsAny(), + requestContent, + It.IsAny())) + .ReturnsAsync(requestContent); + + Content updatedContent = null; + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.GetAsync(It.IsAny(), It.IsAny(), 3)) + .ReturnsAsync(sourceContent); + contentRepository + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, content) => updatedContent = content) + .Returns(Task.CompletedTask); + + var contentCheckRepository = new Mock(); + contentCheckRepository + .Setup(x => x.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var controller = CreateController( + authManager.Object, + createManager, + pathManager.Object, + storageManager, + siteRepository.Object, + channelRepository, + contentRepository.Object, + mailManager, + CreateContentTagRepository().Object, + CreateTranslateRepository().Object, + contentCheckRepository.Object); + + await controller.Update(new EditorController.SubmitRequest + { + SiteId = 1, + ChannelId = 2, + ContentId = 3, + Content = requestContent + }); + + Assert.NotNull(updatedContent); + Assert.False(updatedContent.Checked); + Assert.Equal(0, updatedContent.CheckedLevel); + contentCheckRepository.Verify(x => x.InsertAsync(It.IsAny()), Times.Never); + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + + private static Mock CreateAuthManager(string contentPermission) + { + var authManager = new Mock(); + authManager.Setup(x => x.AdminId).Returns(7); + authManager.Setup(x => x.AdminName).Returns("admin"); + authManager.Setup(x => x.IsSiteAdminAsync()).ReturnsAsync(false); + authManager + .Setup(x => x.HasSitePermissionsAsync(1, MenuUtils.SitePermissions.Contents)) + .ReturnsAsync(true); + authManager + .Setup(x => x.HasContentPermissionsAsync(1, 2, contentPermission)) + .ReturnsAsync(true); + authManager + .Setup(x => x.AddSiteLogAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + return authManager; + } + + private static ICreateManager CreateCreateManager() + { + var createManager = new Mock(); + createManager + .Setup(x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + createManager + .Setup(x => x.TriggerContentChangedEventAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + return createManager.Object; + } + + private static IStorageManager CreateStorageManager() + { + var storageManager = new Mock(); + storageManager + .Setup(x => x.IsStorageAsync(It.IsAny(), SyncType.Images)) + .ReturnsAsync(false); + return storageManager.Object; + } + + private static IMailManager CreateMailManager() + { + var mailManager = new Mock(); + mailManager + .Setup(x => x.GetMailSettingsAsync()) + .ReturnsAsync((MailSettings)null); + return mailManager.Object; + } + + private static IChannelRepository CreateChannelRepository() + { + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Channel { Id = 2, SiteId = 1 }); + channelRepository + .Setup(x => x.GetChannelNameNavigationAsync(1, 2)) + .ReturnsAsync("channel"); + return channelRepository.Object; + } + + private static Mock CreateContentTagRepository() + { + var contentTagRepository = new Mock(); + contentTagRepository + .Setup(x => x.UpdateTagsAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + return contentTagRepository; + } + + private static Mock CreateTranslateRepository() + { + var translateRepository = new Mock(); + translateRepository + .Setup(x => x.GetTranslatesAsync(1, 2, false)) + .ReturnsAsync(new List()); + return translateRepository; + } + + private static EditorController CreateController( + IAuthManager authManager, + ICreateManager createManager, + IPathManager pathManager, + IStorageManager storageManager, + ISiteRepository siteRepository, + IChannelRepository channelRepository, + IContentRepository contentRepository, + IMailManager mailManager, + IContentTagRepository contentTagRepository, + ITranslateRepository translateRepository, + IContentCheckRepository contentCheckRepository = null) + { + return new EditorController( + authManager, + Mock.Of(), + createManager, + pathManager, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + mailManager, + storageManager, + siteRepository, + channelRepository, + contentRepository, + Mock.Of(), + contentTagRepository, + Mock.Of(), + Mock.Of(), + Mock.Of(), + contentCheckRepository ?? Mock.Of(), + translateRepository, + Mock.Of(), + Mock.Of()); + } + } +} diff --git a/tests/SSCMS.Web.Tests/Controllers/Home/Write/EditorControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Home/Write/EditorControllerTests.cs new file mode 100644 index 000000000..1eaaa163f --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Home/Write/EditorControllerTests.cs @@ -0,0 +1,232 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using SSCMS.Core.Utils; +using SSCMS.Enums; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using Xunit; +using EditorController = SSCMS.Web.Controllers.Home.Write.EditorController; + +namespace SSCMS.Web.Tests.Controllers.Home.Write +{ + public class EditorControllerTests + { + [Fact] + public async Task InsertKeepsContentUnapprovedWithoutCheckPermission() + { + var authManager = CreateAuthManager(MenuUtils.ContentPermissions.Add); + var createManager = CreateCreateManager(); + var storageManager = CreateStorageManager(); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Channel { Id = 2, SiteId = 1 }); + + var requestContent = new Content + { + Id = 3, + Title = "draft", + Checked = true, + CheckedLevel = 1 + }; + + var pathManager = new Mock(); + pathManager + .Setup(x => x.EncodeContentAsync( + It.IsAny(), + It.IsAny(), + requestContent, + It.IsAny())) + .ReturnsAsync(requestContent); + + Content insertedContent = null; + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.InsertAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, content) => insertedContent = content) + .ReturnsAsync(3); + + var controller = CreateController( + authManager.Object, + createManager, + pathManager.Object, + storageManager, + siteRepository.Object, + channelRepository.Object, + contentRepository.Object); + + await controller.Insert(new EditorController.SaveRequest + { + SiteId = 1, + ChannelId = 2, + Content = requestContent + }); + + Assert.NotNull(insertedContent); + Assert.False(insertedContent.Checked); + Assert.Equal(0, insertedContent.CheckedLevel); + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + + [Fact] + public async Task UpdateKeepsContentUnapprovedWithoutCheckPermission() + { + var authManager = CreateAuthManager(MenuUtils.ContentPermissions.Edit); + var createManager = CreateCreateManager(); + var storageManager = CreateStorageManager(); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Channel { Id = 2, SiteId = 1 }); + + var sourceContent = new Content + { + Id = 3, + SiteId = 1, + ChannelId = 2, + Title = "source", + Checked = false, + CheckedLevel = 0 + }; + var requestContent = new Content + { + Id = 3, + SiteId = 1, + ChannelId = 2, + Title = "updated", + Checked = true, + CheckedLevel = 1 + }; + + var pathManager = new Mock(); + pathManager + .Setup(x => x.EncodeContentAsync( + It.IsAny(), + It.IsAny(), + requestContent, + It.IsAny())) + .ReturnsAsync(requestContent); + + Content updatedContent = null; + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.GetAsync(It.IsAny(), It.IsAny(), 3)) + .ReturnsAsync(sourceContent); + contentRepository + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, content) => updatedContent = content) + .Returns(Task.CompletedTask); + + var contentCheckRepository = new Mock(); + contentCheckRepository + .Setup(x => x.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var controller = CreateController( + authManager.Object, + createManager, + pathManager.Object, + storageManager, + siteRepository.Object, + channelRepository.Object, + contentRepository.Object, + contentCheckRepository.Object); + + await controller.Update(new EditorController.SaveRequest + { + SiteId = 1, + ChannelId = 2, + ContentId = 3, + Content = requestContent + }); + + Assert.NotNull(updatedContent); + Assert.False(updatedContent.Checked); + Assert.Equal(0, updatedContent.CheckedLevel); + contentCheckRepository.Verify(x => x.InsertAsync(It.IsAny()), Times.Never); + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + + private static Mock CreateAuthManager(string contentPermission) + { + var authManager = new Mock(); + authManager.Setup(x => x.AdminId).Returns(7); + authManager.Setup(x => x.UserId).Returns(8); + authManager.Setup(x => x.IsSiteAdminAsync()).ReturnsAsync(false); + authManager + .Setup(x => x.HasSitePermissionsAsync(1, MenuUtils.SitePermissions.Contents)) + .ReturnsAsync(true); + authManager + .Setup(x => x.HasContentPermissionsAsync(1, 2, contentPermission)) + .ReturnsAsync(true); + return authManager; + } + + private static ICreateManager CreateCreateManager() + { + var createManager = new Mock(); + createManager + .Setup(x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + createManager + .Setup(x => x.TriggerContentChangedEventAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + return createManager.Object; + } + + private static IStorageManager CreateStorageManager() + { + var storageManager = new Mock(); + storageManager + .Setup(x => x.IsStorageAsync(It.IsAny(), SyncType.Images)) + .ReturnsAsync(false); + return storageManager.Object; + } + + private static EditorController CreateController( + IAuthManager authManager, + ICreateManager createManager, + IPathManager pathManager, + IStorageManager storageManager, + ISiteRepository siteRepository, + IChannelRepository channelRepository, + IContentRepository contentRepository, + IContentCheckRepository contentCheckRepository = null) + { + return new EditorController( + authManager, + Mock.Of(), + createManager, + pathManager, + Mock.Of(), + Mock.Of(), + storageManager, + siteRepository, + channelRepository, + contentRepository, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + contentCheckRepository ?? Mock.Of()); + } + } +} diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs index cefb40d18..1159b1445 100644 --- a/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/V1/ContentsControllerTests.cs @@ -188,5 +188,91 @@ public async Task UpdateKeepsContentUnapprovedWithoutCheckPermission() x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } + + [Fact] + public async Task UpdateAllowsApprovalWhenSiteHasNoCheckWorkflow() + { + var authManager = new Mock(); + authManager.Setup(x => x.ApiToken).Returns("token"); + authManager + .Setup(x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.Edit)) + .ReturnsAsync(true); + authManager + .Setup(x => x.AddSiteLogAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + + var createManager = new Mock(); + createManager + .Setup(x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + createManager + .Setup(x => x.TriggerContentChangedEventAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var accessTokenRepository = new Mock(); + accessTokenRepository + .Setup(x => x.IsScopeAsync("token", Constants.ScopeContents)) + .ReturnsAsync(true); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 0 }); + + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Channel { Id = 2, SiteId = 1 }); + channelRepository + .Setup(x => x.GetChannelNameNavigationAsync(1, 2)) + .ReturnsAsync("channel"); + + Content updatedContent = null; + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.GetAsync(It.IsAny(), It.IsAny(), 3)) + .ReturnsAsync(new Content + { + Id = 3, + SiteId = 1, + ChannelId = 2, + Title = "draft", + Checked = false, + CheckedLevel = 0 + }); + contentRepository + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, updated) => updatedContent = updated) + .Returns(Task.CompletedTask); + + var controller = new ContentsController( + authManager.Object, + createManager.Object, + Mock.Of(), + Mock.Of(), + Mock.Of(), + accessTokenRepository.Object, + siteRepository.Object, + channelRepository.Object, + contentRepository.Object, + Mock.Of()); + + await controller.Update(1, 2, 3, new Dictionary + { + [nameof(Content.CheckedLevel)] = 0 + }); + + Assert.NotNull(updatedContent); + Assert.True(updatedContent.Checked); + Assert.Equal(0, updatedContent.CheckedLevel); + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Never); + } } } From 69b0b8384401778138df3f4d029f0fb3ab487789 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:39:11 +0800 Subject: [PATCH 31/85] fix: enforce check permissions on content imports --- .../ContentsLayerAddController.Submit.cs | 6 +- .../ContentsLayerImportController.Submit.cs | 12 +- .../ContentsLayerWordController.Submit.cs | 6 +- .../ContentsLayerWordController.Submit.cs | 4 +- .../ContentImportCheckPermissionTests.cs | 171 ++++++++++++++++++ .../ContentsLayerAddControllerTests.cs | 78 ++++++++ 6 files changed, 263 insertions(+), 14 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentImportCheckPermissionTests.cs create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentsLayerAddControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerAddController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerAddController.Submit.cs index d5068cb62..955f1a27d 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerAddController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerAddController.Submit.cs @@ -29,7 +29,7 @@ public async Task>>> Submit([FromBody] Submi var channel = await _channelRepository.GetAsync(request.ChannelId); if (channel == null) return this.Error("无法确定内容对应的栏目"); - var isChecked = request.CheckedLevel >= site.CheckContentLevel; + var (isChecked, checkedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channel.Id, request.CheckedLevel); var adminId = _authManager.AdminId; var contentIdList = new List(); @@ -92,7 +92,7 @@ public async Task>>> Submit([FromBody] Submi LastEditAdminId = adminId, AddDate = DateTime.Now, Checked = isChecked, - CheckedLevel = request.CheckedLevel, + CheckedLevel = checkedLevel, Title = StringUtils.Trim(title), ImageUrl = string.Empty, Body = body @@ -121,4 +121,4 @@ public async Task>>> Submit([FromBody] Submi }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Submit.cs index 3644d6f16..15cfd4a25 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerImportController.Submit.cs @@ -29,7 +29,7 @@ public async Task> Submit([FromBody] SubmitRequest requ if (channelInfo == null) return this.Error("无法确定内容对应的栏目"); var caching = new CacheUtils(_cacheManager); - var isChecked = request.CheckedLevel >= site.CheckContentLevel; + var (isChecked, checkedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channelInfo.Id, request.CheckedLevel); var contentIdList = new List(); @@ -44,7 +44,7 @@ public async Task> Submit([FromBody] SubmitRequest requ continue; var importObject = new ImportObject(_pathManager, _databaseManager, caching, site, adminId); - contentIdList.AddRange(await importObject.ImportContentsByZipFileAsync(channelInfo, localFilePath, request.IsOverride, isChecked, request.CheckedLevel, adminId, 0, SourceManager.Default)); + contentIdList.AddRange(await importObject.ImportContentsByZipFileAsync(channelInfo, localFilePath, request.IsOverride, isChecked, checkedLevel, adminId, 0, SourceManager.Default)); } } else if (request.ImportType == "excel") @@ -62,7 +62,7 @@ public async Task> Submit([FromBody] SubmitRequest requ continue; var importObject = new ImportObject(_pathManager, _databaseManager, caching, site, adminId); - contentIdList.AddRange(await importObject.ImportContentsByXlsxFileAsync(channelInfo, localFilePath, request.Attributes, request.IsOverride, isChecked, request.CheckedLevel, adminId, 0, SourceManager.Default)); + contentIdList.AddRange(await importObject.ImportContentsByXlsxFileAsync(channelInfo, localFilePath, request.Attributes, request.IsOverride, isChecked, checkedLevel, adminId, 0, SourceManager.Default)); } } else if (request.ImportType == "image") @@ -84,7 +84,7 @@ public async Task> Submit([FromBody] SubmitRequest requ } var importObject = new ImportObject(_pathManager, _databaseManager, caching, site, adminId); - contentIdList.AddRange(await importObject.ImportContentsByImageFileAsync(channelInfo, fileName, fileUrl, request.IsOverride, isChecked, request.CheckedLevel, adminId, 0, SourceManager.Default)); + contentIdList.AddRange(await importObject.ImportContentsByImageFileAsync(channelInfo, fileName, fileUrl, request.IsOverride, isChecked, checkedLevel, adminId, 0, SourceManager.Default)); } } else if (request.ImportType == "txt") @@ -96,7 +96,7 @@ public async Task> Submit([FromBody] SubmitRequest requ continue; var importObject = new ImportObject(_pathManager, _databaseManager, caching, site, adminId); - contentIdList.AddRange(await importObject.ImportContentsByTxtFileAsync(channelInfo, localFilePath, request.IsOverride, isChecked, request.CheckedLevel, adminId, 0, SourceManager.Default)); + contentIdList.AddRange(await importObject.ImportContentsByTxtFileAsync(channelInfo, localFilePath, request.IsOverride, isChecked, checkedLevel, adminId, 0, SourceManager.Default)); } } @@ -122,4 +122,4 @@ public async Task> Submit([FromBody] SubmitRequest requ }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Submit.cs index a3a4df053..75a44cb2b 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Contents/ContentsLayerWordController.Submit.cs @@ -29,7 +29,7 @@ public async Task>>> Submit([FromBody] Submi var channel = await _channelRepository.GetAsync(request.ChannelId); if (channel == null) return this.Error("无法确定内容对应的栏目"); - var isChecked = request.CheckedLevel >= site.CheckContentLevel; + var (isChecked, checkedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channel.Id, request.CheckedLevel); var adminId = _authManager.AdminId; var contentIdList = new List(); @@ -58,7 +58,7 @@ public async Task>>> Submit([FromBody] Submi LastEditAdminId = adminId, AddDate = DateTime.Now, Checked = isChecked, - CheckedLevel = request.CheckedLevel, + CheckedLevel = checkedLevel, Title = wordManager.Title, ImageUrl = wordManager.ImageUrl, Body = wordManager.Body @@ -88,4 +88,4 @@ public async Task>>> Submit([FromBody] Submi }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Submit.cs b/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Submit.cs index 1cd2e7460..40cdf248e 100644 --- a/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Home/Write/ContentsLayerWordController.Submit.cs @@ -29,7 +29,7 @@ public async Task> Submit([FromBody] SubmitRequest requ if (channel == null) return this.Error(Constants.ErrorNotFound); var styles = await _tableStyleRepository.GetContentStylesAsync(site, channel); - var isChecked = request.CheckedLevel >= site.CheckContentLevel; + var (isChecked, checkedLevel) = await CheckManager.GetAllowedCheckStateAsync(_authManager, site, channel.Id, request.CheckedLevel); var adminId = _authManager.AdminId; var userId = _authManager.UserId; @@ -56,7 +56,7 @@ public async Task> Submit([FromBody] SubmitRequest requ UserId = userId, LastEditAdminId = adminId, Checked = isChecked, - CheckedLevel = request.CheckedLevel + CheckedLevel = checkedLevel }; contentInfo.LoadDict(dict); diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentImportCheckPermissionTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentImportCheckPermissionTests.cs new file mode 100644 index 000000000..6218eebac --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentImportCheckPermissionTests.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using SSCMS.Core.Utils; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using Xunit; +using AdminWordController = SSCMS.Web.Controllers.Admin.Cms.Contents.ContentsLayerWordController; +using HomeWordController = SSCMS.Web.Controllers.Home.Write.ContentsLayerWordController; +using ImportController = SSCMS.Web.Controllers.Admin.Cms.Contents.ContentsLayerImportController; + +namespace SSCMS.Web.Tests.Controllers.Admin.Cms.Contents +{ + public class ContentImportCheckPermissionTests + { + [Fact] + public async Task AdminWordSubmitChecksApprovalPermissionBeforeImport() + { + var authManager = CreateAuthManager(); + var controller = new AdminWordController( + authManager.Object, + Mock.Of(), + CreateCreateManager(), + CreateSiteRepository(), + CreateChannelRepository(), + Mock.Of(), + Mock.Of()); + + await controller.Submit(new AdminWordController.SubmitRequest + { + SiteId = 1, + ChannelId = 2, + CheckedLevel = 1, + FileNames = new List(), + FileUrls = new List() + }); + + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + + [Fact] + public async Task HomeWordSubmitChecksApprovalPermissionBeforeImport() + { + var authManager = CreateAuthManager(); + var tableStyleRepository = new Mock(); + tableStyleRepository + .Setup(x => x.GetContentStylesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + var controller = new HomeWordController( + authManager.Object, + Mock.Of(), + CreateCreateManager(), + CreateSiteRepository(), + CreateChannelRepository(), + Mock.Of(), + tableStyleRepository.Object); + + await controller.Submit(new HomeWordController.SubmitRequest + { + SiteId = 1, + ChannelId = 2, + CheckedLevel = 1, + Files = new List() + }); + + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + + [Fact] + public async Task ImportSubmitChecksApprovalPermissionBeforeImport() + { + var authManager = CreateAuthManager(); + authManager + .Setup(x => x.AddSiteLogAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.CompletedTask); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + siteRepository + .Setup(x => x.UpdateAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var controller = new ImportController( + Mock.Of(), + authManager.Object, + Mock.Of(), + Mock.Of(), + CreateCreateManager(), + Mock.Of(), + siteRepository.Object, + CreateChannelRepository(), + Mock.Of()); + + await controller.Submit(new ImportController.SubmitRequest + { + SiteId = 1, + ChannelId = 2, + ImportType = "zip", + CheckedLevel = 1, + FileNames = new List(), + FileUrls = new List(), + Attributes = new List() + }); + + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + + private static Mock CreateAuthManager() + { + var authManager = new Mock(); + authManager.Setup(x => x.AdminId).Returns(7); + authManager.Setup(x => x.UserId).Returns(8); + authManager.Setup(x => x.IsSiteAdminAsync()).ReturnsAsync(false); + authManager + .Setup(x => x.HasSitePermissionsAsync(1, MenuUtils.SitePermissions.Contents)) + .ReturnsAsync(true); + authManager + .Setup(x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.Add)) + .ReturnsAsync(true); + return authManager; + } + + private static ICreateManager CreateCreateManager() + { + var createManager = new Mock(); + createManager + .Setup(x => x.CreateChannelAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + createManager + .Setup(x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + createManager + .Setup(x => x.TriggerContentChangedEventAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + return createManager.Object; + } + + private static ISiteRepository CreateSiteRepository() + { + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + return siteRepository.Object; + } + + private static IChannelRepository CreateChannelRepository() + { + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Channel { Id = 2, SiteId = 1 }); + return channelRepository.Object; + } + } +} diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentsLayerAddControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentsLayerAddControllerTests.cs new file mode 100644 index 000000000..eddc0ec40 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Contents/ContentsLayerAddControllerTests.cs @@ -0,0 +1,78 @@ +using System.Threading.Tasks; +using Moq; +using SSCMS.Core.Utils; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin.Cms.Contents; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin.Cms.Contents +{ + public class ContentsLayerAddControllerTests + { + [Fact] + public async Task SubmitKeepsContentUnapprovedWithoutCheckPermission() + { + var authManager = new Mock(); + authManager.Setup(x => x.AdminId).Returns(7); + authManager.Setup(x => x.IsSiteAdminAsync()).ReturnsAsync(false); + authManager + .Setup(x => x.HasSitePermissionsAsync(1, MenuUtils.SitePermissions.Contents)) + .ReturnsAsync(true); + authManager + .Setup(x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.Add)) + .ReturnsAsync(true); + + var createManager = new Mock(); + createManager + .Setup(x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + createManager + .Setup(x => x.TriggerContentChangedEventAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, CheckContentLevel = 1 }); + + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Channel { Id = 2, SiteId = 1 }); + + Content insertedContent = null; + var contentRepository = new Mock(); + contentRepository + .Setup(x => x.InsertAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, content) => insertedContent = content) + .ReturnsAsync(3); + + var controller = new ContentsLayerAddController( + authManager.Object, + createManager.Object, + siteRepository.Object, + channelRepository.Object, + contentRepository.Object); + + await controller.Submit(new ContentsLayerAddController.SubmitRequest + { + SiteId = 1, + ChannelId = 2, + CheckedLevel = 1, + Titles = "draft" + }); + + Assert.NotNull(insertedContent); + Assert.False(insertedContent.Checked); + Assert.Equal(0, insertedContent.CheckedLevel); + createManager.Verify( + x => x.CreateContentAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + authManager.Verify( + x => x.HasContentPermissionsAsync(1, 2, MenuUtils.ContentPermissions.CheckLevel1), + Times.Once); + } + } +} From c4a7370be15115fe92d6a6a52a786c7c81013f3c Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:42:03 +0800 Subject: [PATCH 32/85] fix: require auth for error log details --- .../Controllers/Admin/ErrorController.cs | 2 ++ .../Controllers/Home/ErrorController.cs | 2 ++ .../ErrorControllerAuthorizationTests.cs | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 tests/SSCMS.Web.Tests/Controllers/ErrorControllerAuthorizationTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/ErrorController.cs b/src/SSCMS.Web/Controllers/Admin/ErrorController.cs index af268a692..5bf361525 100644 --- a/src/SSCMS.Web/Controllers/Admin/ErrorController.cs +++ b/src/SSCMS.Web/Controllers/Admin/ErrorController.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; @@ -8,6 +9,7 @@ namespace SSCMS.Web.Controllers.Admin { [OpenApiIgnore] + [Authorize(Roles = Types.Roles.Administrator)] [Route(Constants.ApiAdminPrefix)] public partial class ErrorController : ControllerBase { diff --git a/src/SSCMS.Web/Controllers/Home/ErrorController.cs b/src/SSCMS.Web/Controllers/Home/ErrorController.cs index 3fb6ec515..14bd219bb 100644 --- a/src/SSCMS.Web/Controllers/Home/ErrorController.cs +++ b/src/SSCMS.Web/Controllers/Home/ErrorController.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; @@ -8,6 +9,7 @@ namespace SSCMS.Web.Controllers.Home { [OpenApiIgnore] + [Authorize(Roles = Types.Roles.User)] [Route(Constants.ApiHomePrefix)] public partial class ErrorController : ControllerBase { diff --git a/tests/SSCMS.Web.Tests/Controllers/ErrorControllerAuthorizationTests.cs b/tests/SSCMS.Web.Tests/Controllers/ErrorControllerAuthorizationTests.cs new file mode 100644 index 000000000..50bbefca8 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/ErrorControllerAuthorizationTests.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using SSCMS.Configuration; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers +{ + public class ErrorControllerAuthorizationTests + { + [Fact] + public void AdminErrorControllerRequiresAdministratorRole() + { + var attributes = typeof(Web.Controllers.Admin.ErrorController) + .GetCustomAttributes(typeof(AuthorizeAttribute), true) + .Cast(); + + Assert.Contains(attributes, attribute => attribute.Roles == Types.Roles.Administrator); + } + + [Fact] + public void HomeErrorControllerRequiresUserRole() + { + var attributes = typeof(Web.Controllers.Home.ErrorController) + .GetCustomAttributes(typeof(AuthorizeAttribute), true) + .Cast(); + + Assert.Contains(attributes, attribute => attribute.Roles == Types.Roles.User); + } + } +} From 39e0e9a8a8f7a9a4c034e0ae8e09964309aa3df7 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:44:32 +0800 Subject: [PATCH 33/85] fix: sanitize admin login response --- .../Admin/LoginController.Submit.cs | 2 +- .../Controllers/Admin/LoginController.cs | 27 ++++++++++++++++++- .../Controllers/Admin/LoginControllerTests.cs | 13 +++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs index 73345601a..9086145b3 100644 --- a/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs @@ -118,7 +118,7 @@ public async Task> Submit([FromBody] SubmitRequest re return new SubmitResult { IsLoginExists = false, - Administrator = administrator, + Administrator = LoginAdministrator.From(administrator), SessionId = sessionId, IsEnforcePasswordChange = isEnforcePasswordChange, Token = token diff --git a/src/SSCMS.Web/Controllers/Admin/LoginController.cs b/src/SSCMS.Web/Controllers/Admin/LoginController.cs index 79ff8f7aa..f9ec11b59 100644 --- a/src/SSCMS.Web/Controllers/Admin/LoginController.cs +++ b/src/SSCMS.Web/Controllers/Admin/LoginController.cs @@ -71,12 +71,37 @@ public class SubmitRequest public class SubmitResult { public bool IsLoginExists { get; set; } - public Administrator Administrator { get; set; } + public LoginAdministrator Administrator { get; set; } public string SessionId { get; set; } public bool IsEnforcePasswordChange { get; set; } public string Token { get; set; } } + public class LoginAdministrator + { + public int Id { get; set; } + public string Guid { get; set; } + public string UserName { get; set; } + public string DisplayName { get; set; } + public string AvatarUrl { get; set; } + public DateTime? LastActivityDate { get; set; } + + public static LoginAdministrator From(Administrator administrator) + { + if (administrator == null) return null; + + return new LoginAdministrator + { + Id = administrator.Id, + Guid = administrator.Guid, + UserName = administrator.UserName, + DisplayName = administrator.DisplayName, + AvatarUrl = administrator.AvatarUrl, + LastActivityDate = administrator.LastActivityDate + }; + } + } + public class SendSmsRequest { public string Mobile { get; set; } diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs index 866946461..be0651c31 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/LoginControllerTests.cs @@ -95,5 +95,18 @@ public async Task SubmitRequiresCaptchaWhenForceLogoutRequested() Assert.IsType(result.Result); administratorRepository.Verify(x => x.ValidateAsync(It.IsAny(), It.IsAny(), true), Times.Never); } + + [Fact] + public void SubmitResultUsesSanitizedAdministratorDto() + { + var property = typeof(LoginController.SubmitResult).GetProperty(nameof(LoginController.SubmitResult.Administrator)); + + Assert.NotNull(property); + Assert.NotEqual(typeof(Administrator), property.PropertyType); + Assert.Null(property.PropertyType.GetProperty(nameof(Administrator.Password))); + Assert.Null(property.PropertyType.GetProperty(nameof(Administrator.PasswordSalt))); + Assert.Null(property.PropertyType.GetProperty(nameof(Administrator.Email))); + Assert.Null(property.PropertyType.GetProperty(nameof(Administrator.Mobile))); + } } } From 6f60f481d85a8efb2b416ee1de5d01233ddd8db9 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:46:12 +0800 Subject: [PATCH 34/85] fix: reject jwt tokens in query strings --- src/SSCMS.Web/Startup.cs | 21 ----------- .../Security/StartupAuthenticationTests.cs | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/StartupAuthenticationTests.cs diff --git a/src/SSCMS.Web/Startup.cs b/src/SSCMS.Web/Startup.cs index 5ad5e0ad2..aa56f89de 100644 --- a/src/SSCMS.Web/Startup.cs +++ b/src/SSCMS.Web/Startup.cs @@ -99,27 +99,6 @@ public void ConfigureServices(IServiceCollection services) ValidateIssuer = false, ValidateAudience = false }; - x.Events = new JwtBearerEvents - { - OnMessageReceived = (context) => - { - if (!context.Request.Query.TryGetValue("access_token", out var values)) - { - return Task.CompletedTask; - } - if (values.Count > 1) - { - return Task.CompletedTask; - } - var token = values.Single(); - if (string.IsNullOrWhiteSpace(token)) - { - return Task.CompletedTask; - } - context.Token = token; - return Task.CompletedTask; - } - }; }); // services.Configure(options => diff --git a/tests/SSCMS.Web.Tests/Security/StartupAuthenticationTests.cs b/tests/SSCMS.Web.Tests/Security/StartupAuthenticationTests.cs new file mode 100644 index 000000000..4cacf8650 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/StartupAuthenticationTests.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class StartupAuthenticationTests + { + [Fact] + public void JwtBearerDoesNotAcceptAccessTokenFromQueryString() + { + var startupPath = FindRepositoryFile("src/SSCMS.Web/Startup.cs"); + var source = File.ReadAllText(startupPath); + + Assert.DoesNotContain("access_token", source, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("OnMessageReceived", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From ab0386407a42d07f38ed4237b3da4f68a9c28cc7 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:48:35 +0800 Subject: [PATCH 35/85] fix: restrict administrator permission changes to super admins --- ...dministratorsController.SavePermissions.cs | 3 +- .../AdministratorsControllerTests.cs | 71 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Administrators/AdministratorsControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.SavePermissions.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.SavePermissions.cs index e39ad984a..641ae1d46 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.SavePermissions.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Administrators/AdministratorsController.SavePermissions.cs @@ -11,7 +11,8 @@ public partial class AdministratorsController [HttpPost, Route(RoutePermissions)] public async Task> SavePermissions([FromRoute] int adminId, [FromBody] SavePermissionsRequest request) { - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators) || + !await _authManager.IsSuperAdminAsync()) { return Unauthorized(); } diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Administrators/AdministratorsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Administrators/AdministratorsControllerTests.cs new file mode 100644 index 000000000..5059538a6 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Administrators/AdministratorsControllerTests.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Core.Utils; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin.Settings.Administrators; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin.Settings.Administrators +{ + public class AdministratorsControllerTests + { + [Fact] + public async Task SavePermissionsRequiresSuperAdmin() + { + var authManager = new Mock(); + authManager + .Setup(x => x.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + .ReturnsAsync(true); + authManager + .Setup(x => x.IsSuperAdminAsync()) + .ReturnsAsync(false); + + var administratorRepository = new Mock(); + administratorRepository + .Setup(x => x.GetByUserIdAsync(9)) + .ReturnsAsync(new Administrator { Id = 9, UserName = "target" }); + administratorRepository + .Setup(x => x.AddUserToRoleAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + administratorRepository + .Setup(x => x.AddUserToRolesAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + administratorRepository + .Setup(x => x.UpdateSiteIdsAsync(It.IsAny(), It.IsAny>())) + .Returns(Task.CompletedTask); + administratorRepository + .Setup(x => x.GetRolesAsync(It.IsAny())) + .ReturnsAsync("roles"); + + var administratorsInRolesRepository = new Mock(); + administratorsInRolesRepository + .Setup(x => x.RemoveUserAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var controller = new AdministratorsController( + Mock.Of(), + authManager.Object, + Mock.Of(), + Mock.Of(), + administratorRepository.Object, + Mock.Of(), + Mock.Of(), + administratorsInRolesRepository.Object); + + var result = await controller.SavePermissions(9, new AdministratorsController.SavePermissionsRequest + { + AdminLevel = "SuperAdmin", + CheckedRoles = new List(), + CheckedSites = new List() + }); + + Assert.IsType(result.Result); + administratorRepository.Verify(x => x.GetByUserIdAsync(It.IsAny()), Times.Never); + administratorsInRolesRepository.Verify(x => x.RemoveUserAsync(It.IsAny()), Times.Never); + } + } +} From f4184340e34178ebb0a93017a4b9e82a92082f88 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:53:38 +0800 Subject: [PATCH 36/85] fix: require super admin for v1 administrator management --- .../V1/AdministratorsController.Create.cs | 6 +-- .../V1/AdministratorsController.Delete.cs | 6 +-- .../V1/AdministratorsController.Get.cs | 6 +-- .../V1/AdministratorsController.List.cs | 6 +-- .../AdministratorsController.ResetPassword.cs | 6 +-- .../V1/AdministratorsController.Update.cs | 6 +-- .../V1/AdministratorsController.cs | 8 ++++ .../V1/AdministratorsControllerTests.cs | 46 ++++++++++++++++++- 8 files changed, 58 insertions(+), 32 deletions(-) diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Create.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Create.cs index a51afc464..4a1c423ec 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Create.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Create.cs @@ -14,11 +14,7 @@ public partial class AdministratorsController [HttpPost, Route(Route)] public async Task> Create([FromBody] Administrator request) { - if (!await _accessTokenRepository.IsScopeAsync(_authManager.ApiToken, Constants.ScopeAdministrators)) - { - return Unauthorized(); - } - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + if (!await IsAdministratorManagementAllowedAsync()) { return Unauthorized(); } diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Delete.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Delete.cs index 959140793..d5e7cafa3 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Delete.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Delete.cs @@ -14,11 +14,7 @@ public partial class AdministratorsController [HttpPost, Route(RouteAdministratorDelete)] public async Task> Delete([FromRoute]int id) { - if (!await _accessTokenRepository.IsScopeAsync(_authManager.ApiToken, Constants.ScopeAdministrators)) - { - return Unauthorized(); - } - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + if (!await IsAdministratorManagementAllowedAsync()) { return Unauthorized(); } diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Get.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Get.cs index ec161ebf6..ee1f9d85d 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Get.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Get.cs @@ -14,11 +14,7 @@ public partial class AdministratorsController [HttpGet, Route(RouteAdministrator)] public async Task> Get([FromRoute] int id) { - if (!await _accessTokenRepository.IsScopeAsync(_authManager.ApiToken, Constants.ScopeAdministrators)) - { - return Unauthorized(); - } - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + if (!await IsAdministratorManagementAllowedAsync()) { return Unauthorized(); } diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.List.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.List.cs index e6662cc15..6f7e28931 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.List.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.List.cs @@ -12,11 +12,7 @@ public partial class AdministratorsController [HttpGet, Route(Route)] public async Task> List([FromQuery] ListRequest request) { - if (!await _accessTokenRepository.IsScopeAsync(_authManager.ApiToken, Constants.ScopeAdministrators)) - { - return Unauthorized(); - } - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + if (!await IsAdministratorManagementAllowedAsync()) { return Unauthorized(); } diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.ResetPassword.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.ResetPassword.cs index 9f82b4a29..344c0b45d 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.ResetPassword.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.ResetPassword.cs @@ -14,11 +14,7 @@ public partial class AdministratorsController [HttpPost, Route(RouteActionsResetPassword)] public async Task> ResetPassword([FromBody] ResetPasswordRequest request) { - if (!await _accessTokenRepository.IsScopeAsync(_authManager.ApiToken, Constants.ScopeAdministrators)) - { - return Unauthorized(); - } - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + if (!await IsAdministratorManagementAllowedAsync()) { return Unauthorized(); } diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Update.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Update.cs index 63ef5cf6d..e6bf1fbb2 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.Update.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.Update.cs @@ -14,11 +14,7 @@ public partial class AdministratorsController [HttpPost, Route(RouteAdministratorUpdate)] public async Task> Update([FromRoute] int id, [FromBody] Administrator administrator) { - if (!await _accessTokenRepository.IsScopeAsync(_authManager.ApiToken, Constants.ScopeAdministrators)) - { - return Unauthorized(); - } - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators)) + if (!await IsAdministratorManagementAllowedAsync()) { return Unauthorized(); } diff --git a/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs b/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs index a4050d55b..65e5d3084 100644 --- a/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs +++ b/src/SSCMS.Web/Controllers/V1/AdministratorsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; using SSCMS.Core.Utils; @@ -125,6 +126,13 @@ public class ResetPasswordRequest public string NewPassword { get; set; } } + private async Task IsAdministratorManagementAllowedAsync() + { + return await _accessTokenRepository.IsScopeAsync(_authManager.ApiToken, Constants.ScopeAdministrators) && + await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsAdministrators) && + await _authManager.IsSuperAdminAsync(); + } + private static string GetLoginRateLimitCacheKey(string account, string ipAddress) { return CacheUtils.GetClassKey(typeof(AdministratorsController), nameof(Login), StringUtils.ToLower(account), ipAddress); diff --git a/tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs index aa0e3f35a..3c7de2fda 100644 --- a/tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/V1/AdministratorsControllerTests.cs @@ -93,17 +93,59 @@ public async Task LoginRateLimitsRepeatedFailures() administratorRepository.Verify(x => x.ValidateAsync("admin", "bad-password", true), Times.Exactly(10)); } + [Fact] + public async Task CreateRequiresSuperAdmin() + { + var administratorRepository = new Mock(); + administratorRepository + .Setup(x => x.InsertAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((true, string.Empty)); + + var authManager = new Mock(); + authManager.Setup(x => x.ApiToken).Returns("token"); + authManager + .Setup(x => x.HasAppPermissionsAsync(Core.Utils.MenuUtils.AppPermissions.SettingsAdministrators)) + .ReturnsAsync(true); + authManager + .Setup(x => x.IsSuperAdminAsync()) + .ReturnsAsync(false); + + var accessTokenRepository = new Mock(); + accessTokenRepository + .Setup(x => x.IsScopeAsync("token", Configuration.Constants.ScopeAdministrators)) + .ReturnsAsync(true); + + var controller = CreateController( + administratorRepository.Object, + authManager.Object, + Mock.Of(), + new TestCacheManager(), + accessTokenRepository.Object); + + var result = await controller.Create(new Administrator + { + UserName = "new-admin", + Password = "password-md5" + }); + + Assert.IsType(result.Result); + administratorRepository.Verify( + x => x.InsertAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + private static AdministratorsController CreateController( IAdministratorRepository administratorRepository, IAuthManager authManager, IConfigRepository configRepository, - ICacheManager cacheManager) + ICacheManager cacheManager, + IAccessTokenRepository accessTokenRepository = null) { return new AdministratorsController( Mock.Of(), authManager, configRepository, - Mock.Of(), + accessTokenRepository ?? Mock.Of(), administratorRepository, Mock.Of(), Mock.Of(), From d39995faee81dca09473cfb3861516b04335ba94 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sat, 30 May 2026 23:57:37 +0800 Subject: [PATCH 37/85] fix: restrict site admin permissions to assigned sites --- .../Services/AuthManager.Permissions.cs | 4 +- .../Security/AuthManagerPermissionsTests.cs | 174 ++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/AuthManagerPermissionsTests.cs diff --git a/src/SSCMS.Core/Services/AuthManager.Permissions.cs b/src/SSCMS.Core/Services/AuthManager.Permissions.cs index bd3164a69..153aff430 100644 --- a/src/SSCMS.Core/Services/AuthManager.Permissions.cs +++ b/src/SSCMS.Core/Services/AuthManager.Permissions.cs @@ -192,7 +192,7 @@ public async Task> GetAppPermissionsAsync() public async Task HasSitePermissionsAsync(int siteId) { var dict = await GetSitePermissionDictAsync(); - return await IsSiteAdminAsync() || dict.ContainsKey(siteId); + return await IsSiteAdminAsync(siteId) || dict.ContainsKey(siteId); } public async Task HasSitePermissionsAsync(int siteId, params string[] permissions) @@ -221,7 +221,7 @@ public async Task HasContentPermissionsAsync(int siteId, int channelId, pa while (true) { if (channelId == 0) return false; - if (await IsSiteAdminAsync()) return true; + if (await IsSiteAdminAsync(siteId)) return true; var dictKey = GetPermissionDictKey(siteId, channelId); var dict = await GetContentPermissionDictAsync(); if (dict.ContainsKey(dictKey) && await HasPermissionsAsync(dict[dictKey], permissions)) return true; diff --git a/tests/SSCMS.Web.Tests/Security/AuthManagerPermissionsTests.cs b/tests/SSCMS.Web.Tests/Security/AuthManagerPermissionsTests.cs new file mode 100644 index 000000000..a8e695eff --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/AuthManagerPermissionsTests.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Security.Claims; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using Moq; +using SSCMS.Configuration; +using SSCMS.Core.Services; +using SSCMS.Core.Utils; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class AuthManagerPermissionsTests + { + [Fact] + public async Task SiteAdminContentPermissionsDoNotCrossAllowedSites() + { + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetParentIdAsync(2, 2)) + .ReturnsAsync(0); + + var authManager = CreateSiteAdminAuthManager(new List { 1 }, channelRepository.Object); + + var allowed = await authManager.HasContentPermissionsAsync(2, 2, MenuUtils.ContentPermissions.Add); + + Assert.False(allowed); + } + + [Fact] + public async Task SiteAdminSitePermissionsDoNotCrossAllowedSites() + { + var authManager = CreateSiteAdminAuthManager(new List { 1 }, Mock.Of()); + + var allowed = await authManager.HasSitePermissionsAsync(2); + + Assert.False(allowed); + } + + private static AuthManager CreateSiteAdminAuthManager(List siteIds, IChannelRepository channelRepository) + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, "site-admin"), + new Claim(ClaimTypes.Role, Types.Roles.Administrator) + }, "Test")) + }; + var httpContextAccessor = new Mock(); + httpContextAccessor.Setup(x => x.HttpContext).Returns(context); + + var roles = new List { "site-admin-role" }; + var administratorsInRolesRepository = new Mock(); + administratorsInRolesRepository + .Setup(x => x.GetRolesForUserAsync("site-admin")) + .ReturnsAsync(roles); + + var roleRepository = new Mock(); + roleRepository + .Setup(x => x.IsConsoleAdministrator(It.IsAny>())) + .Returns(false); + roleRepository + .Setup(x => x.IsSystemAdministrator(It.IsAny>())) + .Returns(true); + + var settingsManager = new Mock(); + settingsManager + .Setup(x => x.GetPermissions()) + .Returns(new List + { + new Permission + { + Id = MenuUtils.ContentPermissions.Add, + Type = new List { Types.PermissionTypes.Channel } + } + }); + settingsManager + .Setup(x => x.GetSiteType(Types.SiteTypes.Web)) + .Returns(new SiteType { Id = Types.SiteTypes.Web }); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(1)) + .ReturnsAsync(new Site { Id = 1, SiteType = Types.SiteTypes.Web }); + + var formRepository = new Mock(); + formRepository + .Setup(x => x.GetFormsAsync(1)) + .ReturnsAsync(new List
()); + + var databaseManager = new Mock(); + databaseManager.SetupGet(x => x.AdministratorsInRolesRepository).Returns(administratorsInRolesRepository.Object); + databaseManager.SetupGet(x => x.ChannelRepository).Returns(channelRepository); + databaseManager.SetupGet(x => x.FormRepository).Returns(formRepository.Object); + databaseManager.SetupGet(x => x.RoleRepository).Returns(roleRepository.Object); + databaseManager.SetupGet(x => x.SiteRepository).Returns(siteRepository.Object); + + var authManager = new AuthManager( + httpContextAccessor.Object, + Mock.Of(), + new TestCacheManager(), + settingsManager.Object, + databaseManager.Object); + + SetAdmin(authManager, new Administrator + { + UserName = "site-admin", + SiteIds = siteIds + }); + + return authManager; + } + + private static void SetAdmin(AuthManager authManager, Administrator administrator) + { + var field = typeof(AuthManager).GetField("_admin", BindingFlags.Instance | BindingFlags.NonPublic); + field.SetValue(authManager, administrator); + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } + } +} From 42ab4ba6b8a57699efeccf3355ac91394f3b731b Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:00:59 +0800 Subject: [PATCH 38/85] fix: enforce site access for site management --- .../Settings/Sites/SitesController.Delete.cs | 4 +- .../Settings/Sites/SitesController.Update.cs | 4 +- .../Admin/Settings/Sites/SitesController.cs | 7 + .../Settings/Sites/SitesControllerTests.cs | 160 ++++++++++++++++++ 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Sites/SitesControllerTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Delete.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Delete.cs index 3be479188..8d8c2b656 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Delete.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Delete.cs @@ -13,7 +13,7 @@ public partial class SitesController [HttpPost, Route(RouteDelete)] public async Task> Delete([FromBody] DeleteRequest request) { - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsSites)) + if (!await HasSiteManagementPermissionAsync(request.SiteId)) { return Unauthorized(); } @@ -82,4 +82,4 @@ public async Task> Delete([FromBody] DeleteRequest req }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Update.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Update.cs index 074f27473..5ffcf9c48 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Update.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.Update.cs @@ -14,7 +14,7 @@ public partial class SitesController [HttpPost, Route(RouteUpdate)] public async Task> Edit([FromBody] EditRequest request) { - if (!await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsSites)) + if (!await HasSiteManagementPermissionAsync(request.SiteId)) { return Unauthorized(); } @@ -128,4 +128,4 @@ public async Task> Edit([FromBody] EditRequest request }; } } -} \ No newline at end of file +} diff --git a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.cs b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.cs index 6ecad19e3..fd1af168a 100644 --- a/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.cs +++ b/src/SSCMS.Web/Controllers/Admin/Settings/Sites/SitesController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; @@ -111,5 +112,11 @@ public class SitesResult { public List Sites { get; set; } } + + private async Task HasSiteManagementPermissionAsync(int siteId) + { + return await _authManager.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsSites) && + (await _authManager.IsSuperAdminAsync() || await _authManager.HasSitePermissionsAsync(siteId)); + } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Sites/SitesControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Sites/SitesControllerTests.cs new file mode 100644 index 000000000..4f11018d8 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Settings/Sites/SitesControllerTests.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Configuration; +using SSCMS.Core.Utils; +using SSCMS.Enums; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using SSCMS.Web.Controllers.Admin.Settings.Sites; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Admin.Settings.Sites +{ + public class SitesControllerTests + { + [Fact] + public async Task EditRequiresAccessToRequestedSite() + { + var authManager = new Mock(); + authManager + .Setup(x => x.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsSites)) + .ReturnsAsync(true); + authManager + .Setup(x => x.IsSuperAdminAsync()) + .ReturnsAsync(false); + authManager + .Setup(x => x.HasSitePermissionsAsync(2)) + .ReturnsAsync(false); + + var settingsManager = new Mock(); + settingsManager.Setup(x => x.IsSafeMode).Returns(false); + settingsManager + .Setup(x => x.GetSiteType(Types.SiteTypes.Web)) + .Returns(new SiteType { Id = Types.SiteTypes.Web }); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Site + { + Id = 2, + Root = true, + SiteName = "Target", + SiteType = Types.SiteTypes.Web, + TableName = "cms_Content" + }); + siteRepository + .Setup(x => x.GetSiteIdsAsync(0)) + .ReturnsAsync(new List()); + + var controller = CreateController(settingsManager.Object, authManager.Object, siteRepository.Object); + + var result = await controller.Edit(new SitesController.EditRequest + { + SiteId = 2, + SiteDir = "target", + SiteName = "Target", + SiteType = Types.SiteTypes.Web, + ParentId = 0, + TableRule = TableRule.Choose, + TableChoose = "cms_Content" + }); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.UpdateAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task DeleteRequiresAccessToRequestedSite() + { + var authManager = new Mock(); + authManager + .Setup(x => x.HasAppPermissionsAsync(MenuUtils.AppPermissions.SettingsSites)) + .ReturnsAsync(true); + authManager + .Setup(x => x.IsSuperAdminAsync()) + .ReturnsAsync(false); + authManager + .Setup(x => x.HasSitePermissionsAsync(2)) + .ReturnsAsync(false); + + var settingsManager = new Mock(); + settingsManager.Setup(x => x.IsSafeMode).Returns(false); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Site + { + Id = 2, + SiteDir = "target", + SiteName = "Target", + TableName = "cms_Content", + Children = new List() + }); + siteRepository + .Setup(x => x.GetSiteIdsAsync(0)) + .ReturnsAsync(new List()); + + var channelRepository = new Mock(); + channelRepository + .Setup(x => x.GetChannelIdsAsync(2)) + .ReturnsAsync(new List()); + + var controller = CreateController( + settingsManager.Object, + authManager.Object, + siteRepository.Object, + channelRepository.Object); + + var result = await controller.Delete(new SitesController.DeleteRequest + { + SiteId = 2, + SiteDir = "target", + DeleteFiles = false + }); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.DeleteAsync(2), Times.Never); + } + + private static SitesController CreateController( + ISettingsManager settingsManager, + IAuthManager authManager, + ISiteRepository siteRepository, + IChannelRepository channelRepository = null) + { + return new SitesController( + settingsManager, + authManager, + Mock.Of(), + siteRepository, + channelRepository ?? Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of()); + } + } +} From d384f7e0b56e81b7966d71881b2ab43b7c1e38eb Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:04:10 +0800 Subject: [PATCH 39/85] fix: require site access for editor uploads --- .../Cms/Editor/EditorController.Upload.cs | 5 +++ .../Admin/Cms/Editor/EditorControllerTests.cs | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs index 3cc5f981e..26a961707 100644 --- a/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Cms/Editor/EditorController.Upload.cs @@ -15,6 +15,11 @@ public partial class EditorController [HttpPost, Route(RouteUpload)] public async Task> Upload([FromQuery] UploadRequest request, [FromForm] IFormFile file) { + if (!await _authManager.IsSuperAdminAsync() && !await _authManager.HasSitePermissionsAsync(request.SiteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); if (file == null) diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs index cda778982..f5bc02d79 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Cms/Editor/EditorControllerTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; using Moq; using SSCMS.Core.Utils; using SSCMS.Dto; @@ -168,6 +169,41 @@ await controller.Update(new EditorController.SubmitRequest Times.Once); } + [Fact] + public async Task UploadRequiresAccessToRequestedSite() + { + var authManager = new Mock(); + authManager + .Setup(x => x.HasSitePermissionsAsync(2)) + .ReturnsAsync(false); + + var siteRepository = new Mock(); + siteRepository + .Setup(x => x.GetAsync(2)) + .ReturnsAsync(new Site { Id = 2 }); + + var controller = CreateController( + authManager.Object, + CreateCreateManager(), + Mock.Of(), + CreateStorageManager(), + siteRepository.Object, + CreateChannelRepository(), + Mock.Of(), + CreateMailManager(), + CreateContentTagRepository().Object, + CreateTranslateRepository().Object); + + var result = await controller.Upload(new EditorController.UploadRequest + { + SiteId = 2, + Type = nameof(Content.ImageUrl) + }, null); + + Assert.IsType(result.Result); + siteRepository.Verify(x => x.GetAsync(It.IsAny()), Times.Never); + } + private static Mock CreateAuthManager(string contentPermission) { var authManager = new Mock(); From 32c55b2fb9b50f0c946e3382c723daf62120aafa Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:07:15 +0800 Subject: [PATCH 40/85] fix: enforce site access for common editor uploads --- .../Common/Editor/ActionsController.Config.cs | 5 +++ .../Editor/ActionsController.ListFile.cs | 5 +++ .../Editor/ActionsController.ListImage.cs | 5 +++ .../Editor/ActionsController.UploadFile.cs | 5 +++ .../Editor/ActionsController.UploadImage.cs | 5 +++ .../Editor/ActionsController.UploadScrawl.cs | 5 +++ .../Editor/ActionsController.UploadVideo.cs | 5 +++ .../Admin/Common/Editor/ActionsController.cs | 13 ++++++-- .../Common/Editor/ActionsControllerTests.cs | 32 ++++++++++++++++++- 9 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.Config.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.Config.cs index 695101523..35213616a 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.Config.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.Config.cs @@ -11,6 +11,11 @@ public partial class ActionsController [HttpGet, Route(RouteActionsConfig)] public async Task> GetConfig([FromQuery]SiteRequest request) { + if (!await HasSiteAccessAsync(request.SiteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); if (site == null) { diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListFile.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListFile.cs index 6d8676366..4a078bc4a 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListFile.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListFile.cs @@ -14,6 +14,11 @@ public partial class ActionsController [HttpGet, Route(RouteActionsListFile)] public async Task> ListFile([FromQuery] ListFileRequest request) { + if (!await HasSiteAccessAsync(request.SiteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); var directoryPath = await _pathManager.GetUploadDirectoryPathAsync(site, UploadType.File); diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListImage.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListImage.cs index bbf718689..bc5e51eda 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListImage.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.ListImage.cs @@ -13,6 +13,11 @@ public partial class ActionsController [HttpGet, Route(RouteActionsListImage)] public async Task> ListImage([FromQuery] ListImageRequest request) { + if (!await HasSiteAccessAsync(request.SiteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); var directoryPath = await _pathManager.GetUploadDirectoryPathAsync(site, UploadType.Image); diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs index a12ccfbfb..e996c1414 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadFile.cs @@ -15,6 +15,11 @@ public partial class ActionsController [HttpPost, Route(RouteActionsUploadFile)] public async Task> UploadFile([FromQuery] SiteRequest request, [FromForm] IFormFile file) { + if (!await HasSiteAccessAsync(request.SiteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); if (file == null) diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs index d405ea919..40d234af9 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadImage.cs @@ -15,6 +15,11 @@ public partial class ActionsController [HttpPost, Route(RouteActionsUploadImage)] public async Task> UploadImage([FromQuery] SiteRequest request, [FromForm] IFormFile file) { + if (!await HasSiteAccessAsync(request.SiteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); if (file == null) diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs index 3751ecec6..d2cf8fc3c 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadScrawl.cs @@ -13,6 +13,11 @@ public partial class ActionsController [HttpPost, Route(RouteActionsUploadScrawl)] public async Task> UploadScrawl([FromQuery] int siteId, [FromForm] UploadScrawlRequest request) { + if (!await HasSiteAccessAsync(siteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(siteId); byte[] bytes; diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs index 8933723a7..bb69af284 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.UploadVideo.cs @@ -15,6 +15,11 @@ public partial class ActionsController [HttpPost, Route(RouteActionsUploadVideo)] public async Task> UploadVideo([FromQuery] SiteRequest request, [FromForm] IFormFile file) { + if (!await HasSiteAccessAsync(request.SiteId)) + { + return Unauthorized(); + } + var site = await _siteRepository.GetAsync(request.SiteId); if (file == null) diff --git a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.cs b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.cs index 1bca4b339..83f0218b1 100644 --- a/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.cs +++ b/src/SSCMS.Web/Controllers/Admin/Common/Editor/ActionsController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; @@ -26,18 +27,21 @@ public partial class ActionsController : ControllerBase private readonly IStorageManager _storageManager; private readonly IVodManager _vodManager; private readonly ISiteRepository _siteRepository; + private readonly IAuthManager _authManager; public ActionsController( IPathManager pathManager, IStorageManager storageManager, IVodManager vodManager, - ISiteRepository siteRepository + ISiteRepository siteRepository, + IAuthManager authManager ) { _pathManager = pathManager; _storageManager = storageManager; _vodManager = vodManager; _siteRepository = siteRepository; + _authManager = authManager; } public class ConfigResult @@ -113,6 +117,11 @@ public class ListFileResult public IEnumerable List { get; set; } } + private async Task HasSiteAccessAsync(int siteId) + { + return await _authManager.IsSuperAdminAsync() || await _authManager.HasSitePermissionsAsync(siteId); + } + public class ListImageRequest : SiteRequest { public int Start { get; set; } @@ -156,4 +165,4 @@ public class UploadVideoResult public string Error { get; set; } } } -} \ No newline at end of file +} diff --git a/tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs index 7e9ceea1e..8e920aaa4 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Admin/Common/Editor/ActionsControllerTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; using Moq; using SSCMS.Configuration; using SSCMS.Enums; @@ -13,6 +14,29 @@ namespace SSCMS.Web.Tests.Controllers.Admin.Common.Editor { public class ActionsControllerTests { + [Fact] + public async Task UploadScrawlRequiresAccessToRequestedSite() + { + var authManager = new Mock(); + authManager + .Setup(x => x.HasSitePermissionsAsync(2)) + .ReturnsAsync(false); + + var controller = new ActionsController( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + authManager.Object); + + var result = await controller.UploadScrawl(2, new ActionsController.UploadScrawlRequest + { + File = "not-base64" + }); + + Assert.IsType(result.Result); + } + [Fact] public async Task UploadScrawlRejectsNonImageBytes() { @@ -30,11 +54,17 @@ public async Task UploadScrawlRejectsNonImageBytes() pathManager.Setup(x => x.IsImageSizeAllowed(site, It.IsAny())).Returns(true); pathManager.Setup(x => x.GetUploadDirectoryPathAsync(site, UploadType.Image)).ReturnsAsync("/tmp"); + var authManager = new Mock(); + authManager + .Setup(x => x.HasSitePermissionsAsync(1)) + .ReturnsAsync(true); + var controller = new ActionsController( pathManager.Object, Mock.Of(), Mock.Of(), - siteRepository.Object); + siteRepository.Object, + authManager.Object); var result = await controller.UploadScrawl(1, new ActionsController.UploadScrawlRequest { From a99a2f569bca4beb50015a6dcce4e45301d43164 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:14:24 +0800 Subject: [PATCH 41/85] fix: validate rest downloads against ssrf --- src/SSCMS/Utils/RestUtils.cs | 17 ++--------- .../Security/RestUtilsTests.cs | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/RestUtilsTests.cs diff --git a/src/SSCMS/Utils/RestUtils.cs b/src/SSCMS/Utils/RestUtils.cs index 5b4556ad9..b60c8fd54 100644 --- a/src/SSCMS/Utils/RestUtils.cs +++ b/src/SSCMS/Utils/RestUtils.cs @@ -1,5 +1,4 @@ -using System.IO; -using System.Net; +using System.Net; using System.Threading.Tasks; using RestSharp; @@ -272,19 +271,7 @@ private class InternalServerError public static async Task DownloadAsync(string url, string filePath) { - FileUtils.DeleteFileIfExists(filePath); - FileUtils.WriteText(filePath, string.Empty); - using (var writer = File.OpenWrite(filePath)) - { - var client = new RestClient(url); - var request = new RestRequest(); - - var stream = await client.DownloadStreamAsync(request); - using (stream) - { - stream.CopyTo(writer); - } - } + await HttpClientUtils.DownloadAsync(url, filePath); } public static async Task GetIpAddressAsync() diff --git a/tests/SSCMS.Web.Tests/Security/RestUtilsTests.cs b/tests/SSCMS.Web.Tests/Security/RestUtilsTests.cs new file mode 100644 index 000000000..03b887c4c --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/RestUtilsTests.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SSCMS.Utils; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class RestUtilsTests + { + [Theory] + [InlineData("file:///etc/passwd")] + [InlineData("http://127.0.0.1/admin")] + [InlineData("http://169.254.169.254/latest/meta-data/")] + public async Task DownloadRejectsUnsafeUrls(string url) + { + var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.tmp"); + + var ex = await Assert.ThrowsAsync(() => RestUtils.DownloadAsync(url, filePath)); + + Assert.True( + ex.Message.Contains("HTTP", StringComparison.OrdinalIgnoreCase) || + ex.Message.Contains("Private", StringComparison.OrdinalIgnoreCase), + ex.Message); + Assert.False(File.Exists(filePath)); + } + } +} From 4c15caf58a874dd4343d4a29e4bc97fc7567db8f Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:18:00 +0800 Subject: [PATCH 42/85] fix: sanitize channel group filters --- .../Repositories/ChannelRepository.cs | 2 +- .../ChannelRepositorySqlSafetyTests.cs | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/SSCMS.Web.Tests/Security/ChannelRepositorySqlSafetyTests.cs diff --git a/src/SSCMS.Core/Repositories/ChannelRepository.cs b/src/SSCMS.Core/Repositories/ChannelRepository.cs index 90376cc88..68be003cb 100644 --- a/src/SSCMS.Core/Repositories/ChannelRepository.cs +++ b/src/SSCMS.Core/Repositories/ChannelRepository.cs @@ -230,7 +230,7 @@ private string GetGroupWhereString(string group, string groupNot) whereStringBuilder.Append(" AND ("); foreach (var theGroup in groupArr) { - var trimGroup = theGroup.Trim(); + var trimGroup = Utilities.FilterSql(theGroup.Trim()); whereStringBuilder.Append( $" (siteserver_Channel.GroupNames = '{trimGroup}' OR {DbUtils.GetInStr(_settingsManager.DatabaseType, "siteserver_Channel.GroupNames", trimGroup + ",")} OR {DbUtils.GetInStr(_settingsManager.DatabaseType, "siteserver_Channel.GroupNames", "," + trimGroup + ",")} OR {DbUtils.GetInStr(_settingsManager.DatabaseType, "siteserver_Channel.GroupNames", "," + trimGroup)}) OR "); diff --git a/tests/SSCMS.Web.Tests/Security/ChannelRepositorySqlSafetyTests.cs b/tests/SSCMS.Web.Tests/Security/ChannelRepositorySqlSafetyTests.cs new file mode 100644 index 000000000..0f9677396 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/ChannelRepositorySqlSafetyTests.cs @@ -0,0 +1,36 @@ +using Datory; +using Moq; +using SSCMS.Core.Repositories; +using SSCMS.Repositories; +using SSCMS.Services; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class ChannelRepositorySqlSafetyTests + { + [Fact] + public void GetWhereStringSanitizesGroupChannelNames() + { + var repository = CreateRepository(); + + var where = repository.GetWhereString("news' OR 1=1--", null, false, false); + + Assert.DoesNotContain("news' OR", where); + Assert.Contains("_sqlquote_", where); + } + + private static ChannelRepository CreateRepository() + { + var settingsManager = new Mock(); + settingsManager + .Setup(x => x.DatabaseType) + .Returns(DatabaseType.SQLite); + settingsManager + .Setup(x => x.Database) + .Returns(new Database(DatabaseType.SQLite, "Data Source=:memory:")); + + return new ChannelRepository(settingsManager.Object, Mock.Of()); + } + } +} From 59316f9440074e869f9ecb348c96ba12dd5dc636 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:22:25 +0800 Subject: [PATCH 43/85] fix: escape search highlight regex --- .../Controllers/Stl/ActionsSearchController.Submit.cs | 4 ++-- .../Controllers/Stl/ActionsSearchController.cs | 7 +++++++ .../Controllers/Stl/ActionsPublicRateLimitTests.cs | 10 ++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs index ee17354b7..4d3f82514 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.Submit.cs @@ -94,7 +94,7 @@ public async Task> Submit([FromBody] StlSearchRequest var pagedContents = pagedBuilder.ToString(); pagedBuilder = new StringBuilder(); pagedBuilder.Append(RegexUtils.Replace( - $"({request.Word.Replace(" ", "\\s")})(?!)(?![^><]*>)", pagedContents, + GetHighlightRegexPattern(request.Word), pagedContents, $"{request.Word}")); } @@ -131,7 +131,7 @@ public async Task> Submit([FromBody] StlSearchRequest var pagedContents = pagedBuilder.ToString(); pagedBuilder = new StringBuilder(); pagedBuilder.Append(RegexUtils.Replace( - $"({request.Word.Replace(" ", "\\s")})(?!)(?![^><]*>)", pagedContents, + GetHighlightRegexPattern(request.Word), pagedContents, $"{request.Word}")); } diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs index b07e9d5e8..624e9a7e8 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsSearchController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Specialized; +using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; @@ -47,6 +48,12 @@ private static string GetRateLimitCacheKey(string ipAddress) return CacheUtils.GetClassKey(typeof(ActionsSearchController), "Rate", ipAddress ?? "unknown"); } + public static string GetHighlightRegexPattern(string word) + { + var escapedWord = Regex.Escape(word).Replace("\\ ", "\\s"); + return $"({escapedWord})(?!)(?![^><]*>)"; + } + private bool TryConsumeRequestQuota(string ipAddress, out int retryAfterSeconds) { retryAfterSeconds = 0; diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs index 1eaed77f3..f773e8284 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsPublicRateLimitTests.cs @@ -14,6 +14,16 @@ namespace SSCMS.Web.Tests.Controllers.Stl { public class ActionsPublicRateLimitTests { + [Fact] + public void SearchHighlightEscapesRegexMetaCharacters() + { + var pattern = ActionsSearchController.GetHighlightRegexPattern("(a+)+ test"); + + Assert.DoesNotContain("(a+)+", pattern); + Assert.Matches(pattern, "(a+)+ test"); + Assert.DoesNotMatch(pattern, "aaaa test"); + } + [Fact] public async Task SearchRateLimitsRepeatedAnonymousRequests() { From 0bcc27811c0f2249ad91ad690c43ce09678f2f5a Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:27:28 +0800 Subject: [PATCH 44/85] fix: rate limit public sms endpoints --- .../Home/LoginController.SendSms.cs | 17 +- .../Home/LostPasswordController.SendSms.cs | 17 +- .../Home/ProfileController.SendSms.cs | 19 +- .../Home/RegisterController.SendSms.cs | 17 +- .../Home/VerifyMobileController.SendSms.cs | 19 +- .../Controllers/SmsRateLimitUtils.cs | 46 +++ .../Controllers/V1/FormsController.SendSms.cs | 15 +- .../Controllers/Home/HomeSmsRateLimitTests.cs | 269 ++++++++++++++++++ 8 files changed, 400 insertions(+), 19 deletions(-) create mode 100644 src/SSCMS.Web/Controllers/SmsRateLimitUtils.cs create mode 100644 tests/SSCMS.Web.Tests/Controllers/Home/HomeSmsRateLimitTests.cs diff --git a/src/SSCMS.Web/Controllers/Home/LoginController.SendSms.cs b/src/SSCMS.Web/Controllers/Home/LoginController.SendSms.cs index 7f45e61fd..f88cc3864 100644 --- a/src/SSCMS.Web/Controllers/Home/LoginController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/Home/LoginController.SendSms.cs @@ -11,7 +11,18 @@ public partial class LoginController [HttpPost, Route(RouteSendSms)] public async Task> SendSms([FromBody] SendSmsRequest request) { - var user = await _userRepository.GetByMobileAsync(request.Mobile); + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) + { + return this.Error("请输入有效的手机号码"); + } + + var mobile = request.Mobile.Trim(); + if (!SmsRateLimitUtils.TryConsume(_cacheManager, typeof(LoginController), mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + + var user = await _userRepository.GetByMobileAsync(mobile); if (user == null) { @@ -26,13 +37,13 @@ public async Task> SendSms([FromBody] SendSmsRequest re var code = StringUtils.GetRandomInt(100000, 999999); (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.LoginConfirmation, code); + await _smsManager.SendSmsAsync(mobile, SmsCodeType.LoginConfirmation, code); if (!success) { return this.Error(errorMessage); } - var cacheKey = GetSmsCodeCacheKey(request.Mobile); + var cacheKey = GetSmsCodeCacheKey(mobile); _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); return new BoolResult diff --git a/src/SSCMS.Web/Controllers/Home/LostPasswordController.SendSms.cs b/src/SSCMS.Web/Controllers/Home/LostPasswordController.SendSms.cs index bb62bfb8c..f3c9f2cde 100644 --- a/src/SSCMS.Web/Controllers/Home/LostPasswordController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/Home/LostPasswordController.SendSms.cs @@ -11,7 +11,18 @@ public partial class LostPasswordController [HttpPost, Route(RouteSendSms)] public async Task> SendSms([FromBody] SendSmsRequest request) { - var user = await _userRepository.GetByMobileAsync(request.Mobile); + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) + { + return this.Error("请输入有效的手机号码"); + } + + var mobile = request.Mobile.Trim(); + if (!SmsRateLimitUtils.TryConsume(_cacheManager, typeof(LostPasswordController), mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + + var user = await _userRepository.GetByMobileAsync(mobile); if (user == null) { @@ -26,13 +37,13 @@ public async Task> SendSms([FromBody] SendSmsRequest re var code = StringUtils.GetRandomInt(100000, 999999); (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.ChangePassword, code); + await _smsManager.SendSmsAsync(mobile, SmsCodeType.ChangePassword, code); if (!success) { return this.Error(errorMessage); } - var cacheKey = GetSmsCodeCacheKey(request.Mobile); + var cacheKey = GetSmsCodeCacheKey(mobile); _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); return new BoolResult diff --git a/src/SSCMS.Web/Controllers/Home/ProfileController.SendSms.cs b/src/SSCMS.Web/Controllers/Home/ProfileController.SendSms.cs index b2e2ed85e..4d767bd76 100644 --- a/src/SSCMS.Web/Controllers/Home/ProfileController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/Home/ProfileController.SendSms.cs @@ -11,10 +11,21 @@ public partial class ProfileController [HttpPost, Route(RouteSendSms)] public async Task> SendSms([FromBody] SendSmsRequest request) { + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) + { + return this.Error("请输入有效的手机号码"); + } + + var mobile = request.Mobile.Trim(); + if (!SmsRateLimitUtils.TryConsume(_cacheManager, typeof(ProfileController), mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + var user = await _authManager.GetUserAsync(); - if (!StringUtils.EqualsIgnoreCase(user.Mobile, request.Mobile)) + if (!StringUtils.EqualsIgnoreCase(user.Mobile, mobile)) { - var exists = await _userRepository.IsMobileExistsAsync(request.Mobile); + var exists = await _userRepository.IsMobileExistsAsync(mobile); if (exists) { return this.Error("此手机号码已注册,请更换手机号码"); @@ -23,13 +34,13 @@ public async Task> SendSms([FromBody] SendSmsRequest re var code = StringUtils.GetRandomInt(100000, 999999); var (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.InformationChanges, code); + await _smsManager.SendSmsAsync(mobile, SmsCodeType.InformationChanges, code); if (!success) { return this.Error(errorMessage); } - var cacheKey = GetSmsCodeCacheKey(request.Mobile); + var cacheKey = GetSmsCodeCacheKey(mobile); _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); return new BoolResult diff --git a/src/SSCMS.Web/Controllers/Home/RegisterController.SendSms.cs b/src/SSCMS.Web/Controllers/Home/RegisterController.SendSms.cs index 16e0169c5..5a9bb1010 100644 --- a/src/SSCMS.Web/Controllers/Home/RegisterController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/Home/RegisterController.SendSms.cs @@ -11,7 +11,18 @@ public partial class RegisterController [HttpPost, Route(RouteSendSms)] public async Task> SendSms([FromBody] SendSmsRequest request) { - var exists = await _userRepository.IsMobileExistsAsync(request.Mobile); + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) + { + return this.Error("请输入有效的手机号码"); + } + + var mobile = request.Mobile.Trim(); + if (!SmsRateLimitUtils.TryConsume(_cacheManager, typeof(RegisterController), mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + + var exists = await _userRepository.IsMobileExistsAsync(mobile); if (exists) { @@ -20,13 +31,13 @@ public async Task> SendSms([FromBody] SendSmsRequest re var code = StringUtils.GetRandomInt(100000, 999999); var (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.Registration, code); + await _smsManager.SendSmsAsync(mobile, SmsCodeType.Registration, code); if (!success) { return this.Error(errorMessage); } - var cacheKey = GetSmsCodeCacheKey(request.Mobile); + var cacheKey = GetSmsCodeCacheKey(mobile); _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); return new BoolResult diff --git a/src/SSCMS.Web/Controllers/Home/VerifyMobileController.SendSms.cs b/src/SSCMS.Web/Controllers/Home/VerifyMobileController.SendSms.cs index 8386d130b..015c8f614 100644 --- a/src/SSCMS.Web/Controllers/Home/VerifyMobileController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/Home/VerifyMobileController.SendSms.cs @@ -11,10 +11,21 @@ public partial class VerifyMobileController [HttpPost, Route(RouteSendSms)] public async Task> SendSms([FromBody] SendSmsRequest request) { + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) + { + return this.Error("请输入有效的手机号码"); + } + + var mobile = request.Mobile.Trim(); + if (!SmsRateLimitUtils.TryConsume(_cacheManager, typeof(VerifyMobileController), mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + var user = await _authManager.GetUserAsync(); - if (!StringUtils.EqualsIgnoreCase(user.Mobile, request.Mobile)) + if (!StringUtils.EqualsIgnoreCase(user.Mobile, mobile)) { - var exists = await _userRepository.IsMobileExistsAsync(request.Mobile); + var exists = await _userRepository.IsMobileExistsAsync(mobile); if (exists) { return this.Error("此手机号码已注册,请更换手机号码"); @@ -23,13 +34,13 @@ public async Task> SendSms([FromBody] SendSmsRequest re var code = StringUtils.GetRandomInt(100000, 999999); var (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.Authentication, code); + await _smsManager.SendSmsAsync(mobile, SmsCodeType.Authentication, code); if (!success) { return this.Error(errorMessage); } - var cacheKey = GetSmsCodeCacheKey(request.Mobile); + var cacheKey = GetSmsCodeCacheKey(mobile); _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); return new BoolResult diff --git a/src/SSCMS.Web/Controllers/SmsRateLimitUtils.cs b/src/SSCMS.Web/Controllers/SmsRateLimitUtils.cs new file mode 100644 index 000000000..e0f4848dc --- /dev/null +++ b/src/SSCMS.Web/Controllers/SmsRateLimitUtils.cs @@ -0,0 +1,46 @@ +using System; +using SSCMS.Core.Utils; +using SSCMS.Services; + +namespace SSCMS.Web.Controllers +{ + internal static class SmsRateLimitUtils + { + private const int DefaultSendSmsMaxCount = 5; + private const int DefaultSendSmsWindowMinutes = 10; + + private class SendSmsRateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + public static bool TryConsume( + ICacheManager cacheManager, + Type ownerType, + string mobile, + string ipAddress, + out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = CacheUtils.GetClassKey(ownerType, "SendSmsRate", + (mobile ?? string.Empty).Trim(), ipAddress ?? "unknown"); + var state = cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new SendSmsRateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(DefaultSendSmsWindowMinutes) + }; + } + + state.Count++; + cacheManager.AddOrUpdateAbsolute(cacheKey, state, DefaultSendSmsWindowMinutes); + if (state.Count <= DefaultSendSmsMaxCount) return true; + + retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } + } +} diff --git a/src/SSCMS.Web/Controllers/V1/FormsController.SendSms.cs b/src/SSCMS.Web/Controllers/V1/FormsController.SendSms.cs index e93c303c3..a39903e9a 100644 --- a/src/SSCMS.Web/Controllers/V1/FormsController.SendSms.cs +++ b/src/SSCMS.Web/Controllers/V1/FormsController.SendSms.cs @@ -14,6 +14,17 @@ public partial class FormsController [HttpPost, Route(RouteSendSms)] public async Task> SendSms([FromQuery] FormRequest formRequest, [FromBody] SendSmsRequest request) { + if (request == null || string.IsNullOrWhiteSpace(request.Mobile)) + { + return this.Error("请输入有效的手机号码"); + } + + var mobile = request.Mobile.Trim(); + if (!SmsRateLimitUtils.TryConsume(_cacheManager, typeof(FormsController), mobile, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + Form form = null; if (formRequest.FormId > 0) { @@ -45,13 +56,13 @@ public async Task> SendSms([FromQuery] FormRequest form var code = StringUtils.GetRandomInt(100000, 999999); var (success, errorMessage) = - await _smsManager.SendSmsAsync(request.Mobile, SmsCodeType.Authentication, code); + await _smsManager.SendSmsAsync(mobile, SmsCodeType.Authentication, code); if (!success) { return this.Error(errorMessage); } - var cacheKey = GetSmsCodeCacheKey(form.Id, request.Mobile); + var cacheKey = GetSmsCodeCacheKey(form.Id, mobile); _cacheManager.AddOrUpdateAbsolute(cacheKey, code, 10); return new BoolResult diff --git a/tests/SSCMS.Web.Tests/Controllers/Home/HomeSmsRateLimitTests.cs b/tests/SSCMS.Web.Tests/Controllers/Home/HomeSmsRateLimitTests.cs new file mode 100644 index 000000000..6c771b6bc --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/Home/HomeSmsRateLimitTests.cs @@ -0,0 +1,269 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SSCMS.Dto; +using SSCMS.Enums; +using SSCMS.Models; +using SSCMS.Repositories; +using SSCMS.Services; +using HomeLoginController = SSCMS.Web.Controllers.Home.LoginController; +using HomeLostPasswordController = SSCMS.Web.Controllers.Home.LostPasswordController; +using HomeProfileController = SSCMS.Web.Controllers.Home.ProfileController; +using HomeRegisterController = SSCMS.Web.Controllers.Home.RegisterController; +using HomeVerifyMobileController = SSCMS.Web.Controllers.Home.VerifyMobileController; +using V1FormsController = SSCMS.Web.Controllers.V1.FormsController; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers.Home +{ + public class HomeSmsRateLimitTests + { + [Fact] + public async Task LoginSendSmsRateLimitsRepeatedRequests() + { + var smsManager = CreateSmsManager(); + var userRepository = new Mock(); + userRepository.Setup(x => x.GetByMobileAsync("13800000000")).ReturnsAsync(new User()); + userRepository.Setup(x => x.ValidateStateAsync(It.IsAny())).ReturnsAsync((true, string.Empty)); + + var controller = WithHttpContext(new HomeLoginController( + Mock.Of(), + Mock.Of(), + new TestCacheManager(), + smsManager.Object, + Mock.Of(), + userRepository.Object, + Mock.Of(), + Mock.Of())); + + var lastResult = await SendRepeatedly(() => controller.SendSms(new HomeLoginController.SendSmsRequest + { + Mobile = "13800000000" + })); + + Assert.IsType(lastResult.Result); + VerifySmsCount(smsManager, 5); + } + + [Fact] + public async Task LostPasswordSendSmsRateLimitsRepeatedRequests() + { + var smsManager = CreateSmsManager(); + var userRepository = new Mock(); + userRepository.Setup(x => x.GetByMobileAsync("13800000000")).ReturnsAsync(new User()); + userRepository.Setup(x => x.ValidateStateAsync(It.IsAny())).ReturnsAsync((true, string.Empty)); + + var controller = WithHttpContext(new HomeLostPasswordController( + Mock.Of(), + new TestCacheManager(), + smsManager.Object, + userRepository.Object)); + + var lastResult = await SendRepeatedly(() => controller.SendSms(new HomeLostPasswordController.SendSmsRequest + { + Mobile = "13800000000" + })); + + Assert.IsType(lastResult.Result); + VerifySmsCount(smsManager, 5); + } + + [Fact] + public async Task RegisterSendSmsRateLimitsRepeatedRequests() + { + var smsManager = CreateSmsManager(); + var userRepository = new Mock(); + userRepository.Setup(x => x.IsMobileExistsAsync("13800000000")).ReturnsAsync(false); + + var controller = WithHttpContext(new HomeRegisterController( + Mock.Of(), + Mock.Of(), + smsManager.Object, + new TestCacheManager(), + Mock.Of(), + Mock.Of(), + userRepository.Object, + Mock.Of(), + Mock.Of())); + + var lastResult = await SendRepeatedly(() => controller.SendSms(new HomeRegisterController.SendSmsRequest + { + Mobile = "13800000000" + })); + + Assert.IsType(lastResult.Result); + VerifySmsCount(smsManager, 5); + } + + [Fact] + public async Task ProfileSendSmsRateLimitsRepeatedRequests() + { + var smsManager = CreateSmsManager(); + var authManager = new Mock(); + authManager.Setup(x => x.GetUserAsync()).ReturnsAsync(new User { Mobile = "13800000000" }); + + var controller = WithHttpContext(new HomeProfileController( + authManager.Object, + Mock.Of(), + Mock.Of(), + smsManager.Object, + new TestCacheManager(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of())); + + var lastResult = await SendRepeatedly(() => controller.SendSms(new HomeProfileController.SendSmsRequest + { + Mobile = "13800000000" + })); + + Assert.IsType(lastResult.Result); + VerifySmsCount(smsManager, 5); + } + + [Fact] + public async Task VerifyMobileSendSmsRateLimitsRepeatedRequests() + { + var smsManager = CreateSmsManager(); + var authManager = new Mock(); + authManager.Setup(x => x.GetUserAsync()).ReturnsAsync(new User { Mobile = "13800000000" }); + + var controller = WithHttpContext(new HomeVerifyMobileController( + authManager.Object, + new TestCacheManager(), + smsManager.Object, + Mock.Of())); + + var lastResult = await SendRepeatedly(() => controller.SendSms(new HomeVerifyMobileController.SendSmsRequest + { + Mobile = "13800000000" + })); + + Assert.IsType(lastResult.Result); + VerifySmsCount(smsManager, 5); + } + + [Fact] + public async Task V1FormsSendSmsRateLimitsRepeatedRequests() + { + var smsManager = CreateSmsManager(); + smsManager.Setup(x => x.IsSmsEnabledAsync()).ReturnsAsync(true); + var formRepository = new Mock(); + formRepository.Setup(x => x.GetAsync(1, 7)).ReturnsAsync(new Form + { + Id = 7, + IsSms = true + }); + + var controller = WithHttpContext(new V1FormsController( + new TestCacheManager(), + Mock.Of(), + smsManager.Object, + Mock.Of(), + Mock.Of(), + formRepository.Object, + Mock.Of())); + + var formRequest = new V1FormsController.FormRequest + { + SiteId = 1, + FormId = 7 + }; + var lastResult = await SendRepeatedly(() => controller.SendSms(formRequest, new V1FormsController.SendSmsRequest + { + Mobile = "13800000000" + })); + + Assert.IsType(lastResult.Result); + VerifySmsCount(smsManager, 5); + } + + private static Mock CreateSmsManager() + { + var smsManager = new Mock(); + smsManager + .Setup(x => x.SendSmsAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((true, string.Empty)); + return smsManager; + } + + private static void VerifySmsCount(Mock smsManager, int count) + { + smsManager.Verify( + x => x.SendSmsAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(count)); + } + + private static T WithHttpContext(T controller) where T : ControllerBase + { + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + return controller; + } + + private static async Task> SendRepeatedly( + System.Func>> sendSms) + { + ActionResult lastResult = null; + for (var i = 0; i < 6; i++) + { + lastResult = await sendSms(); + } + + return lastResult; + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } + } +} From c4e35ca4b2fd21f12e9ca886fcb6f47353d5cb55 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:30:42 +0800 Subject: [PATCH 45/85] fix: require auth for preview routes --- .../Controllers/Preview/PreviewController.cs | 2 ++ .../PreviewControllerAuthorizationTests.cs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/SSCMS.Web.Tests/Controllers/PreviewControllerAuthorizationTests.cs diff --git a/src/SSCMS.Web/Controllers/Preview/PreviewController.cs b/src/SSCMS.Web/Controllers/Preview/PreviewController.cs index 4c86d3470..0e3dfa59b 100644 --- a/src/SSCMS.Web/Controllers/Preview/PreviewController.cs +++ b/src/SSCMS.Web/Controllers/Preview/PreviewController.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; @@ -16,6 +17,7 @@ namespace SSCMS.Web.Controllers.Preview { [OpenApiIgnore] + [Authorize(Roles = Types.Roles.Administrator + "," + Types.Roles.User)] public partial class PreviewController : ControllerBase { private readonly IPathManager _pathManager; diff --git a/tests/SSCMS.Web.Tests/Controllers/PreviewControllerAuthorizationTests.cs b/tests/SSCMS.Web.Tests/Controllers/PreviewControllerAuthorizationTests.cs new file mode 100644 index 000000000..772ff1268 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Controllers/PreviewControllerAuthorizationTests.cs @@ -0,0 +1,22 @@ +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using SSCMS.Configuration; +using SSCMS.Web.Controllers.Preview; +using Xunit; + +namespace SSCMS.Web.Tests.Controllers +{ + public class PreviewControllerAuthorizationTests + { + [Fact] + public void PreviewControllerRequiresAuthenticatedAdminOrUser() + { + var attributes = typeof(PreviewController) + .GetCustomAttributes(typeof(AuthorizeAttribute), true) + .Cast(); + + Assert.Contains(attributes, attribute => + attribute.Roles == $"{Types.Roles.Administrator},{Types.Roles.User}"); + } + } +} From fe0e823d0a715f74944ce98e4c4bcf0215273449 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:33:10 +0800 Subject: [PATCH 46/85] fix: avoid persistent cloud token storage --- .../wwwroot/sitefiles/assets/js/cloud.js | 13 ++++++- .../Security/CloudTokenStorageTests.cs | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/CloudTokenStorageTests.cs diff --git a/src/SSCMS.Web/wwwroot/sitefiles/assets/js/cloud.js b/src/SSCMS.Web/wwwroot/sitefiles/assets/js/cloud.js index 6c59366e1..e8f183e7d 100644 --- a/src/SSCMS.Web/wwwroot/sitefiles/assets/js/cloud.js +++ b/src/SSCMS.Web/wwwroot/sitefiles/assets/js/cloud.js @@ -1,7 +1,8 @@ var CLOUD_ACCESS_TOKEN_NAME = 'ss_cloud_access_token'; var CLOUD_USER_NAME = 'ss_cloud_user_name'; -var $cloudToken = localStorage.getItem(CLOUD_ACCESS_TOKEN_NAME); +var $cloudToken = sessionStorage.getItem(CLOUD_ACCESS_TOKEN_NAME); var $cloudUserName = localStorage.getItem(CLOUD_USER_NAME); +localStorage.removeItem(CLOUD_ACCESS_TOKEN_NAME); var cloud = _.extend(axios.create({ baseURL: 'http://localhost:6060/v7', @@ -134,13 +135,21 @@ var cloud = _.extend(axios.create({ logout: function() { localStorage.removeItem(CLOUD_USER_NAME); + sessionStorage.removeItem(CLOUD_ACCESS_TOKEN_NAME); localStorage.removeItem(CLOUD_ACCESS_TOKEN_NAME); + $cloudToken = null; + $cloudUserName = null; + this.defaults.headers.Authorization = ''; }, login: function(userName, token) { if (userName && token) { localStorage.setItem(CLOUD_USER_NAME, userName); - localStorage.setItem(CLOUD_ACCESS_TOKEN_NAME, token); + sessionStorage.setItem(CLOUD_ACCESS_TOKEN_NAME, token); + localStorage.removeItem(CLOUD_ACCESS_TOKEN_NAME); + $cloudToken = token; + $cloudUserName = userName; + this.defaults.headers.Authorization = "Bearer " + token; } else { this.logout(); } diff --git a/tests/SSCMS.Web.Tests/Security/CloudTokenStorageTests.cs b/tests/SSCMS.Web.Tests/Security/CloudTokenStorageTests.cs new file mode 100644 index 000000000..c96a537b2 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/CloudTokenStorageTests.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class CloudTokenStorageTests + { + [Fact] + public void CloudAccessTokenIsNotPersistedInLocalStorage() + { + var cloudJsPath = FindRepositoryFile("src/SSCMS.Web/wwwroot/sitefiles/assets/js/cloud.js"); + var source = File.ReadAllText(cloudJsPath); + + Assert.DoesNotContain("localStorage.getItem(CLOUD_ACCESS_TOKEN_NAME)", source, StringComparison.Ordinal); + Assert.DoesNotContain("localStorage.setItem(CLOUD_ACCESS_TOKEN_NAME", source, StringComparison.Ordinal); + Assert.Contains("sessionStorage.getItem(CLOUD_ACCESS_TOKEN_NAME)", source, StringComparison.Ordinal); + Assert.Contains("sessionStorage.setItem(CLOUD_ACCESS_TOKEN_NAME", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From 83eb28950ec63bb01befd66e70a5db33bd11719c Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:36:12 +0800 Subject: [PATCH 47/85] fix: add baseline security headers --- src/SSCMS.Web/Startup.cs | 32 ++++++++++++++ .../Security/SecurityHeadersTests.cs | 42 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/SSCMS.Web.Tests/Security/SecurityHeadersTests.cs diff --git a/src/SSCMS.Web/Startup.cs b/src/SSCMS.Web/Startup.cs index aa56f89de..16e9ea5d8 100644 --- a/src/SSCMS.Web/Startup.cs +++ b/src/SSCMS.Web/Startup.cs @@ -230,6 +230,38 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISetting await context.Response.WriteAsync(result); })); + app.Use(async (context, next) => + { + context.Response.OnStarting(() => + { + var headers = context.Response.Headers; + if (!headers.ContainsKey("X-Content-Type-Options")) + { + headers["X-Content-Type-Options"] = "nosniff"; + } + if (!headers.ContainsKey("X-Frame-Options")) + { + headers["X-Frame-Options"] = "SAMEORIGIN"; + } + if (!headers.ContainsKey("Content-Security-Policy")) + { + headers["Content-Security-Policy"] = "frame-ancestors 'self'"; + } + if (!headers.ContainsKey("Referrer-Policy")) + { + headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; + } + if (!headers.ContainsKey("Permissions-Policy")) + { + headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"; + } + + return Task.CompletedTask; + }); + + await next(); + }); + app.UseCors(CorsPolicy); app.UseForwardedHeaders(new ForwardedHeadersOptions diff --git a/tests/SSCMS.Web.Tests/Security/SecurityHeadersTests.cs b/tests/SSCMS.Web.Tests/Security/SecurityHeadersTests.cs new file mode 100644 index 000000000..c91eb89c2 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/SecurityHeadersTests.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class SecurityHeadersTests + { + [Fact] + public void StartupAddsBaselineSecurityHeaders() + { + var startupPath = FindRepositoryFile("src/SSCMS.Web/Startup.cs"); + var source = File.ReadAllText(startupPath); + + Assert.Contains("X-Content-Type-Options", source, StringComparison.Ordinal); + Assert.Contains("nosniff", source, StringComparison.Ordinal); + Assert.Contains("X-Frame-Options", source, StringComparison.Ordinal); + Assert.Contains("SAMEORIGIN", source, StringComparison.Ordinal); + Assert.Contains("Content-Security-Policy", source, StringComparison.Ordinal); + Assert.Contains("frame-ancestors 'self'", source, StringComparison.Ordinal); + Assert.Contains("Referrer-Policy", source, StringComparison.Ordinal); + Assert.Contains("Permissions-Policy", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From 6fa87b8ef98b0e815b26e4e00f3b18bb8041a9e9 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:38:53 +0800 Subject: [PATCH 48/85] fix: bound form value length --- src/SSCMS.Web/Startup.cs | 2 +- .../Security/RequestSizeLimitTests.cs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/SSCMS.Web/Startup.cs b/src/SSCMS.Web/Startup.cs index 16e9ea5d8..06b66571e 100644 --- a/src/SSCMS.Web/Startup.cs +++ b/src/SSCMS.Web/Startup.cs @@ -107,7 +107,7 @@ public void ConfigureServices(IServiceCollection services) // }); services.Configure(x => { - x.ValueLengthLimit = int.MaxValue; + x.ValueLengthLimit = 10485760; // 10MB x.MultipartBodyLengthLimit = 104857600; // 100MB }); diff --git a/tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs b/tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs index f4ced6ca7..15a2ad37c 100644 --- a/tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs +++ b/tests/SSCMS.Web.Tests/Security/RequestSizeLimitTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Http.Metadata; @@ -29,5 +30,31 @@ public void ControllersDoNotDisableRequestSizeLimits() Assert.Empty(offenders); } + + [Fact] + public void FormOptionsDoNotUseUnboundedValueLengthLimit() + { + var startupPath = FindRepositoryFile("src/SSCMS.Web/Startup.cs"); + var source = File.ReadAllText(startupPath); + + Assert.DoesNotContain("ValueLengthLimit = int.MaxValue", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } } } From 19497b7d481197b442a14ffc8d95dc10d5d895cf Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:43:34 +0800 Subject: [PATCH 49/85] fix: store passwords with one-way hashes --- .../Repositories/AdministratorRepository.cs | 24 ++++------ src/SSCMS.Core/Repositories/UserRepository.cs | 30 +++++-------- src/SSCMS/Utils/PasswordHashUtils.cs | 44 +++++++++++++++++++ .../Repositories/AdministratorDaoTest.cs | 2 +- .../Security/PasswordHashUtilsTests.cs | 21 +++++++++ .../Security/PasswordStorageSourceTests.cs | 39 ++++++++++++++++ 6 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 src/SSCMS/Utils/PasswordHashUtils.cs create mode 100644 tests/SSCMS.Web.Tests/Security/PasswordHashUtilsTests.cs create mode 100644 tests/SSCMS.Web.Tests/Security/PasswordStorageSourceTests.cs diff --git a/src/SSCMS.Core/Repositories/AdministratorRepository.cs b/src/SSCMS.Core/Repositories/AdministratorRepository.cs index 0b9e96af3..18884f23b 100644 --- a/src/SSCMS.Core/Repositories/AdministratorRepository.cs +++ b/src/SSCMS.Core/Repositories/AdministratorRepository.cs @@ -346,18 +346,7 @@ private string EncodePassword(string password, PasswordFormat passwordFormat, ou } else if (passwordFormat == PasswordFormat.Hashed) { - passwordSalt = GenerateSalt(); - - var src = Encoding.Unicode.GetBytes(password); - var buffer2 = Convert.FromBase64String(passwordSalt); - var dst = new byte[buffer2.Length + src.Length]; - Buffer.BlockCopy(buffer2, 0, dst, 0, buffer2.Length); - Buffer.BlockCopy(src, 0, dst, buffer2.Length, src.Length); - var algorithm = SHA1.Create(); // HashAlgorithm.Create("SHA1"); - if (algorithm == null) return retVal; - var inArray = algorithm.ComputeHash(dst); - - retVal = Convert.ToBase64String(inArray); + retVal = PasswordHashUtils.HashPassword(password, out passwordSalt); } else if (passwordFormat == PasswordFormat.Encrypted) { @@ -472,7 +461,7 @@ private static string GenerateSalt() { administrator.LastActivityDate = DateTime.Now; administrator.LastChangePasswordDate = DateTime.Now; - administrator.PasswordFormat = PasswordFormat.SM4; + administrator.PasswordFormat = PasswordFormat.Hashed; administrator.Password = EncodePassword(password, administrator.PasswordFormat, out var passwordSalt); administrator.PasswordSalt = passwordSalt; administrator.Set("ConfirmPassword", string.Empty); @@ -537,8 +526,8 @@ await _repository.UpdateAsync(Q return (false, $"密码不符合规则,请包含{config.AdminPasswordRestriction.GetDisplayName()}"); } - password = EncodePassword(password, PasswordFormat.SM4, out var passwordSalt); - await ChangePasswordAsync(adminEntity, PasswordFormat.SM4, passwordSalt, password); + password = EncodePassword(password, PasswordFormat.Hashed, out var passwordSalt); + await ChangePasswordAsync(adminEntity, PasswordFormat.Hashed, passwordSalt, password); return (true, string.Empty); } @@ -645,6 +634,11 @@ private async Task CheckPasswordByAdminIdAsync(int adminId, string passwor return false; } + if (dbAdmin.PasswordFormat == PasswordFormat.Hashed) + { + return PasswordHashUtils.VerifyPassword(password, isPasswordMd5, dbAdmin.Password, dbAdmin.PasswordSalt); + } + var decodePassword = DecodePassword(dbAdmin.Password, dbAdmin.PasswordFormat, dbAdmin.PasswordSalt); if (isPasswordMd5) { diff --git a/src/SSCMS.Core/Repositories/UserRepository.cs b/src/SSCMS.Core/Repositories/UserRepository.cs index 2aeb906cb..da6b1da63 100644 --- a/src/SSCMS.Core/Repositories/UserRepository.cs +++ b/src/SSCMS.Core/Repositories/UserRepository.cs @@ -136,11 +136,11 @@ IDepartmentRepository departmentRepository return (null, errorMessage); } - password = EncodePassword(password, PasswordFormat.SM4, out var passwordSalt); + password = EncodePassword(password, PasswordFormat.Hashed, out var passwordSalt); user.LastActivityDate = DateTime.Now; user.LastResetPasswordDate = DateTime.Now; - user.Id = await InsertWithoutValidationAsync(user, password, PasswordFormat.SM4, passwordSalt); + user.Id = await InsertWithoutValidationAsync(user, password, PasswordFormat.Hashed, passwordSalt); await CacheIpAddressAsync(ipAddress); @@ -154,11 +154,11 @@ public async Task InsertWithoutValidationAsync(User user, string password) user.Mobile = user.UserName; } - password = EncodePassword(password, PasswordFormat.SM4, out var passwordSalt); + password = EncodePassword(password, PasswordFormat.Hashed, out var passwordSalt); user.LastActivityDate = DateTime.Now; user.LastResetPasswordDate = DateTime.Now; - return await InsertWithoutValidationAsync(user, password, PasswordFormat.SM4, passwordSalt); + return await InsertWithoutValidationAsync(user, password, PasswordFormat.Hashed, passwordSalt); } private async Task InsertWithoutValidationAsync(User user, string password, PasswordFormat passwordFormat, string passwordSalt) @@ -272,18 +272,7 @@ private static string EncodePassword(string password, PasswordFormat passwordFor } else if (passwordFormat == PasswordFormat.Hashed) { - passwordSalt = GenerateSalt(); - - var src = Encoding.Unicode.GetBytes(password); - var buffer2 = Convert.FromBase64String(passwordSalt); - var dst = new byte[buffer2.Length + src.Length]; - byte[] inArray = null; - Buffer.BlockCopy(buffer2, 0, dst, 0, buffer2.Length); - Buffer.BlockCopy(src, 0, dst, buffer2.Length, src.Length); - var algorithm = SHA1.Create(); // HashAlgorithm.Create("SHA1"); - if (algorithm != null) inArray = algorithm.ComputeHash(dst); - - if (inArray != null) retVal = Convert.ToBase64String(inArray); + retVal = PasswordHashUtils.HashPassword(password, out passwordSalt); } else if (passwordFormat == PasswordFormat.Encrypted) { @@ -342,8 +331,8 @@ private static string GenerateSalt() return (false, $"密码不符合规则,请包含{config.UserPasswordRestriction.GetDisplayName()}"); } - password = EncodePassword(password, PasswordFormat.SM4, out var passwordSalt); - await ChangePasswordAsync(userId, PasswordFormat.SM4, passwordSalt, password); + password = EncodePassword(password, PasswordFormat.Hashed, out var passwordSalt); + await ChangePasswordAsync(userId, PasswordFormat.Hashed, passwordSalt, password); return (true, string.Empty); } @@ -477,6 +466,11 @@ public async Task> GetUserIdsAsync(bool isChecked) public bool CheckPassword(string password, bool isPasswordMd5, string dbPassword, PasswordFormat passwordFormat, string passwordSalt) { + if (passwordFormat == PasswordFormat.Hashed) + { + return PasswordHashUtils.VerifyPassword(password, isPasswordMd5, dbPassword, passwordSalt); + } + var decodePassword = DecodePassword(dbPassword, passwordFormat, passwordSalt); if (isPasswordMd5) { diff --git a/src/SSCMS/Utils/PasswordHashUtils.cs b/src/SSCMS/Utils/PasswordHashUtils.cs new file mode 100644 index 000000000..404002e57 --- /dev/null +++ b/src/SSCMS/Utils/PasswordHashUtils.cs @@ -0,0 +1,44 @@ +using System; +using System.Security.Cryptography; + +namespace SSCMS.Utils +{ + public static class PasswordHashUtils + { + private const int SaltSize = 16; + private const int KeySize = 32; + private const int Iterations = 100000; + + public static string HashPassword(string password, out string salt) + { + var saltBytes = RandomNumberGenerator.GetBytes(SaltSize); + salt = Convert.ToBase64String(saltBytes); + return HashNormalizedPassword(AuthUtils.Md5ByString(password), saltBytes); + } + + public static bool VerifyPassword(string password, bool isPasswordMd5, string hash, string salt) + { + if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(hash) || string.IsNullOrEmpty(salt)) + { + return false; + } + + var normalizedPassword = isPasswordMd5 ? password : AuthUtils.Md5ByString(password); + var saltBytes = Convert.FromBase64String(salt); + var computedHash = HashNormalizedPassword(normalizedPassword, saltBytes); + return CryptographicOperations.FixedTimeEquals( + Convert.FromBase64String(hash), + Convert.FromBase64String(computedHash)); + } + + private static string HashNormalizedPassword(string normalizedPassword, byte[] saltBytes) + { + using var pbkdf2 = new Rfc2898DeriveBytes( + normalizedPassword, + saltBytes, + Iterations, + HashAlgorithmName.SHA256); + return Convert.ToBase64String(pbkdf2.GetBytes(KeySize)); + } + } +} diff --git a/tests/SSCMS.Core.Tests/Repositories/AdministratorDaoTest.cs b/tests/SSCMS.Core.Tests/Repositories/AdministratorDaoTest.cs index 8007f1973..a0f54be8c 100644 --- a/tests/SSCMS.Core.Tests/Repositories/AdministratorDaoTest.cs +++ b/tests/SSCMS.Core.Tests/Repositories/AdministratorDaoTest.cs @@ -60,7 +60,7 @@ public async Task TestInsert() Assert.NotNull(entity); Assert.True(!string.IsNullOrWhiteSpace(userInfo.Password)); - Assert.True(userInfo.PasswordFormat == PasswordFormat.SM4); + Assert.True(userInfo.PasswordFormat == PasswordFormat.Hashed); Assert.True(!string.IsNullOrWhiteSpace(userInfo.PasswordSalt)); userInfo = await _userRepository.GetByUserNameAsync(TestUserName); diff --git a/tests/SSCMS.Web.Tests/Security/PasswordHashUtilsTests.cs b/tests/SSCMS.Web.Tests/Security/PasswordHashUtilsTests.cs new file mode 100644 index 000000000..5a6afaf5e --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/PasswordHashUtilsTests.cs @@ -0,0 +1,21 @@ +using SSCMS.Utils; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class PasswordHashUtilsTests + { + [Fact] + public void HashPasswordStoresOneWayVerifierCompatibleWithMd5Submissions() + { + const string password = "Password1"; + var hash = PasswordHashUtils.HashPassword(password, out var salt); + + Assert.NotEqual(password, hash); + Assert.DoesNotContain(password, hash); + Assert.True(PasswordHashUtils.VerifyPassword(password, false, hash, salt)); + Assert.True(PasswordHashUtils.VerifyPassword(AuthUtils.Md5ByString(password), true, hash, salt)); + Assert.False(PasswordHashUtils.VerifyPassword("wrong-password", false, hash, salt)); + } + } +} diff --git a/tests/SSCMS.Web.Tests/Security/PasswordStorageSourceTests.cs b/tests/SSCMS.Web.Tests/Security/PasswordStorageSourceTests.cs new file mode 100644 index 000000000..12d0f56de --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/PasswordStorageSourceTests.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class PasswordStorageSourceTests + { + [Fact] + public void NewAndChangedPasswordsAreNotStoredAsSm4() + { + var administratorRepository = File.ReadAllText(FindRepositoryFile("src/SSCMS.Core/Repositories/AdministratorRepository.cs")); + var userRepository = File.ReadAllText(FindRepositoryFile("src/SSCMS.Core/Repositories/UserRepository.cs")); + var source = administratorRepository + userRepository; + + Assert.DoesNotContain("PasswordFormat = PasswordFormat.SM4", source, StringComparison.Ordinal); + Assert.DoesNotContain("EncodePassword(password, PasswordFormat.SM4", source, StringComparison.Ordinal); + Assert.Contains("PasswordFormat = PasswordFormat.Hashed", source, StringComparison.Ordinal); + Assert.Contains("EncodePassword(password, PasswordFormat.Hashed", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From 4ef219d348d5ab226090cd04b27df13e48d37b67 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:51:24 +0800 Subject: [PATCH 50/85] fix: reject unsafe zip entries --- .../Services/PathManager.ZipImage.cs | 4 +- .../Services/PluginManager.Actions.cs | 5 +- src/SSCMS.Core/Utils/SafeZipUtils.cs | 92 +++++++++++++++++++ .../Security/ZipExtractionSafetyTests.cs | 76 +++++++++++++++ 4 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 src/SSCMS.Core/Utils/SafeZipUtils.cs create mode 100644 tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs diff --git a/src/SSCMS.Core/Services/PathManager.ZipImage.cs b/src/SSCMS.Core/Services/PathManager.ZipImage.cs index 6fd7eee70..1a3c9d1fb 100644 --- a/src/SSCMS.Core/Services/PathManager.ZipImage.cs +++ b/src/SSCMS.Core/Services/PathManager.ZipImage.cs @@ -1,6 +1,7 @@ using ICSharpCode.SharpZipLib.Zip; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; +using SSCMS.Core.Utils; using SSCMS.Utils; namespace SSCMS.Core.Services @@ -15,8 +16,7 @@ public void CreateZip(string zipFilePath, string directoryPath, string fileFilte public void ExtractZip(string zipFilePath, string directoryPath, string fileFilter = null) { - var fz = new FastZip(); - fz.ExtractZip(zipFilePath, directoryPath, fileFilter); + SafeZipUtils.ExtractZip(zipFilePath, directoryPath, fileFilter); } public (int width, int height) GetImageSize(string filePath) diff --git a/src/SSCMS.Core/Services/PluginManager.Actions.cs b/src/SSCMS.Core/Services/PluginManager.Actions.cs index 963fa4b33..ede2e011d 100644 --- a/src/SSCMS.Core/Services/PluginManager.Actions.cs +++ b/src/SSCMS.Core/Services/PluginManager.Actions.cs @@ -1,4 +1,4 @@ -using ICSharpCode.SharpZipLib.Zip; +using SSCMS.Core.Utils; using SSCMS.Utils; using System.Linq; using System.Threading.Tasks; @@ -14,8 +14,7 @@ public async Task InstallAsync(string userName, string name, string version, str var zipFilePath = await DownloadExtensionAsync(packagesPath, userName, name, version, downloadUrl); - var fz = new FastZip(); - fz.ExtractZip(zipFilePath, pluginPath, null); + SafeZipUtils.ExtractZip(zipFilePath, pluginPath); } private async Task DownloadExtensionAsync(string packagesPath, string userName, string name, string version, string downloadUrl) diff --git a/src/SSCMS.Core/Utils/SafeZipUtils.cs b/src/SSCMS.Core/Utils/SafeZipUtils.cs new file mode 100644 index 000000000..89cc6dbfa --- /dev/null +++ b/src/SSCMS.Core/Utils/SafeZipUtils.cs @@ -0,0 +1,92 @@ +using ICSharpCode.SharpZipLib.Zip; +using System; +using System.IO; +using System.Text.RegularExpressions; + +namespace SSCMS.Core.Utils +{ + public static class SafeZipUtils + { + public static void ExtractZip(string zipFilePath, string directoryPath, string fileFilter = null) + { + var destinationRoot = Path.GetFullPath(directoryPath); + Directory.CreateDirectory(destinationRoot); + + using var fileStream = File.OpenRead(zipFilePath); + using var zipFile = new ZipFile(fileStream); + foreach (ZipEntry entry in zipFile) + { + if (!ShouldExtract(entry.Name, fileFilter)) + { + continue; + } + + var destinationPath = GetDestinationPath(destinationRoot, entry.Name); + if (entry.IsDirectory) + { + Directory.CreateDirectory(destinationPath); + continue; + } + + if (!entry.IsFile) + { + continue; + } + + var destinationDirectory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + using var inputStream = zipFile.GetInputStream(entry); + using var outputStream = File.Create(destinationPath); + inputStream.CopyTo(outputStream); + } + } + + private static bool ShouldExtract(string entryName, string fileFilter) + { + return string.IsNullOrEmpty(fileFilter) || Regex.IsMatch(entryName, fileFilter); + } + + private static string GetDestinationPath(string destinationRoot, string entryName) + { + if (string.IsNullOrWhiteSpace(entryName)) + { + throw new InvalidOperationException("Zip entry name cannot be empty."); + } + + var normalizedEntryName = entryName.Replace('\\', '/'); + if (normalizedEntryName.StartsWith("/", StringComparison.Ordinal) || + Regex.IsMatch(normalizedEntryName, "^[A-Za-z]:/")) + { + throw new InvalidOperationException($"Zip entry '{entryName}' is outside the destination directory."); + } + + var destinationPath = Path.GetFullPath(Path.Combine( + destinationRoot, + normalizedEntryName.Replace('/', Path.DirectorySeparatorChar))); + + if (!IsInsideDirectory(destinationRoot, destinationPath)) + { + throw new InvalidOperationException($"Zip entry '{entryName}' is outside the destination directory."); + } + + return destinationPath; + } + + private static bool IsInsideDirectory(string directoryPath, string filePath) + { + return string.Equals(directoryPath, filePath, StringComparison.OrdinalIgnoreCase) || + filePath.StartsWith(EnsureTrailingSeparator(directoryPath), StringComparison.OrdinalIgnoreCase); + } + + private static string EnsureTrailingSeparator(string directoryPath) + { + return directoryPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) + ? directoryPath + : directoryPath + Path.DirectorySeparatorChar; + } + } +} diff --git a/tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs b/tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs new file mode 100644 index 000000000..441bf9c89 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.IO.Compression; +using SSCMS.Core.Services; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class ZipExtractionSafetyTests + { + [Fact] + public void ExtractZipAllowsEntriesInsideDestination() + { + var basePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var zipPath = Path.Combine(basePath, "safe.zip"); + var extractPath = Path.Combine(basePath, "extract"); + var extractedPath = Path.Combine(extractPath, "assets", "style.css"); + Directory.CreateDirectory(basePath); + Directory.CreateDirectory(extractPath); + try + { + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + { + var entry = archive.CreateEntry("assets/style.css"); + using var writer = new StreamWriter(entry.Open()); + writer.Write("body{}"); + } + + var pathManager = new PathManager(null, null, null, null, null, null, null, null, null, null, null); + + pathManager.ExtractZip(zipPath, extractPath); + + Assert.Equal("body{}", File.ReadAllText(extractedPath)); + } + finally + { + if (Directory.Exists(basePath)) + { + Directory.Delete(basePath, true); + } + } + } + + [Fact] + public void ExtractZipRejectsEntriesOutsideDestination() + { + var basePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var zipPath = Path.Combine(basePath, "malicious.zip"); + var extractPath = Path.Combine(basePath, "extract"); + var outsidePath = Path.Combine(basePath, "outside.txt"); + Directory.CreateDirectory(basePath); + Directory.CreateDirectory(extractPath); + try + { + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + { + var entry = archive.CreateEntry("../outside.txt"); + using var writer = new StreamWriter(entry.Open()); + writer.Write("escaped"); + } + + var pathManager = new PathManager(null, null, null, null, null, null, null, null, null, null, null); + + Assert.Throws(() => pathManager.ExtractZip(zipPath, extractPath)); + Assert.False(File.Exists(outsidePath)); + } + finally + { + if (Directory.Exists(basePath)) + { + Directory.Delete(basePath, true); + } + } + } + } +} From ada1c63c6a1f85c45131edbeebbf3aaa49d6af08 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 00:57:25 +0800 Subject: [PATCH 51/85] fix: install plugins from validated staging --- .../AddLayerUploadController.Override.cs | 18 +++++-- .../AddLayerUploadController.Upload.cs | 21 ++++---- .../PluginUploadInstallSourceTests.cs | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/PluginUploadInstallSourceTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Override.cs b/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Override.cs index 06c9eeb90..79b218ffd 100644 --- a/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Override.cs +++ b/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Override.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; +using SSCMS.Core.Plugins; using SSCMS.Core.Utils; using SSCMS.Dto; using SSCMS.Utils; @@ -18,7 +19,18 @@ public async Task> Override([FromBody] OverrideRequest } var fileName = PathUtils.RemoveParentPath(request.FileName); - var filePath = _pathManager.GetTemporaryFilesPath(fileName); + var tempPluginPath = _pathManager.GetTemporaryFilesPath(PathUtils.GetFileNameWithoutExtension(fileName)); + var (plugin, errorMessage) = await PluginUtils.ValidateManifestAsync(tempPluginPath); + if (plugin == null) + { + return this.Error(errorMessage); + } + + if (!StringUtils.EqualsIgnoreCase(plugin.PluginId, request.PluginId)) + { + return this.Error("插件包与插件Id不匹配"); + } + var pluginPath = _pathManager.GetPluginPath(request.PluginId); var configPath = PathUtils.Combine(pluginPath, Constants.PluginConfigFileName); var configValue = string.Empty; @@ -28,8 +40,8 @@ public async Task> Override([FromBody] OverrideRequest } DirectoryUtils.DeleteDirectoryIfExists(pluginPath); - DirectoryUtils.CreateDirectoryIfNotExists(pluginPath); - _pathManager.ExtractZip(filePath, pluginPath); + DirectoryUtils.MoveDirectory(tempPluginPath, pluginPath, true); + DirectoryUtils.DeleteDirectoryIfExists(tempPluginPath); if (!string.IsNullOrEmpty(configValue)) { await FileUtils.WriteTextAsync(configPath, configValue); diff --git a/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs b/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs index 281b00b30..8b58aec6d 100644 --- a/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs +++ b/src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SSCMS.Configuration; @@ -32,11 +33,13 @@ public async Task> Upload([FromForm] IFormFile file) return this.Error("插件包为Zip格式,请选择有效的文件上传"); } - var filePath = _pathManager.GetTemporaryFilesPath(fileName); + var uploadId = Guid.NewGuid().ToString("N"); + var temporaryFileName = $"{uploadId}{sExt}"; + var filePath = _pathManager.GetTemporaryFilesPath(temporaryFileName); FileUtils.DeleteFileIfExists(filePath); await _pathManager.UploadAsync(file, filePath); - var tempPluginPath = _pathManager.GetTemporaryFilesPath(PathUtils.GetFileNameWithoutExtension(fileName)); + var tempPluginPath = _pathManager.GetTemporaryFilesPath(uploadId); DirectoryUtils.DeleteDirectoryIfExists(tempPluginPath); DirectoryUtils.CreateDirectoryIfNotExists(tempPluginPath); _pathManager.ExtractZip(filePath, tempPluginPath); @@ -44,26 +47,28 @@ public async Task> Upload([FromForm] IFormFile file) var (plugin, errorMessage) = await PluginUtils.ValidateManifestAsync(tempPluginPath); if (plugin == null) { + FileUtils.DeleteFileIfExists(filePath); + DirectoryUtils.DeleteDirectoryIfExists(tempPluginPath); return this.Error(errorMessage); } - DirectoryUtils.DeleteDirectoryIfExists(tempPluginPath); - var oldPlugin = _pluginManager.GetPlugin(plugin.PluginId); if (oldPlugin == null) { var pluginPath = _pathManager.GetPluginPath(plugin.PluginId); DirectoryUtils.DeleteDirectoryIfExists(pluginPath); - DirectoryUtils.CreateDirectoryIfNotExists(pluginPath); - _pathManager.ExtractZip(filePath, pluginPath); + DirectoryUtils.MoveDirectory(tempPluginPath, pluginPath, true); + DirectoryUtils.DeleteDirectoryIfExists(tempPluginPath); } + FileUtils.DeleteFileIfExists(filePath); + return new UploadResult { OldPlugin = oldPlugin, NewPlugin = plugin, - FileName = fileName + FileName = temporaryFileName }; } } diff --git a/tests/SSCMS.Web.Tests/Security/PluginUploadInstallSourceTests.cs b/tests/SSCMS.Web.Tests/Security/PluginUploadInstallSourceTests.cs new file mode 100644 index 000000000..c47187758 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/PluginUploadInstallSourceTests.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class PluginUploadInstallSourceTests + { + [Fact] + public void PluginUploadUsesServerGeneratedStagingPath() + { + var uploadSource = File.ReadAllText(FindRepositoryFile("src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs")); + + Assert.Contains("Guid.NewGuid().ToString(\"N\")", uploadSource, StringComparison.Ordinal); + Assert.DoesNotContain("GetTemporaryFilesPath(fileName)", uploadSource, StringComparison.Ordinal); + } + + [Fact] + public void PluginInstallUsesValidatedStagingDirectory() + { + var uploadSource = File.ReadAllText(FindRepositoryFile("src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Upload.cs")); + var overrideSource = File.ReadAllText(FindRepositoryFile("src/SSCMS.Web/Controllers/Admin/Plugins/AddLayerUploadController.Override.cs")); + + Assert.DoesNotContain("_pathManager.ExtractZip(filePath, pluginPath)", uploadSource, StringComparison.Ordinal); + Assert.DoesNotContain("_pathManager.ExtractZip(filePath, pluginPath)", overrideSource, StringComparison.Ordinal); + Assert.Contains("DirectoryUtils.MoveDirectory(tempPluginPath, pluginPath, true)", uploadSource, StringComparison.Ordinal); + Assert.Contains("DirectoryUtils.MoveDirectory(tempPluginPath, pluginPath, true)", overrideSource, StringComparison.Ordinal); + Assert.Contains("PluginUtils.ValidateManifestAsync(tempPluginPath)", overrideSource, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From b74f811a5f2758b949f4fdded5e7baa7cd5defa4 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:02:50 +0800 Subject: [PATCH 52/85] fix: consume sms codes after verification --- .../Admin/LoginController.Submit.cs | 2 + .../Home/LoginController.Submit.cs | 2 + .../Home/LostPasswordController.Submit.cs | 2 + .../Home/ProfileController.VerifyMobile.cs | 1 + .../Home/RegisterController.VerifyMobile.cs | 2 + .../Home/VerifyMobileController.Submit.cs | 1 + .../Controllers/V1/FormsController.Submit.cs | 8 +++- .../Security/SmsCodeConsumptionSourceTests.cs | 42 +++++++++++++++++++ 8 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/SSCMS.Web.Tests/Security/SmsCodeConsumptionSourceTests.cs diff --git a/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs b/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs index 9086145b3..aa6d69757 100644 --- a/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs @@ -40,6 +40,8 @@ public async Task> Submit([FromBody] SubmitRequest re administrator.MobileVerified = true; await _administratorRepository.UpdateAsync(administrator); } + + _cacheManager.Remove(codeCacheKey); } else { diff --git a/src/SSCMS.Web/Controllers/Home/LoginController.Submit.cs b/src/SSCMS.Web/Controllers/Home/LoginController.Submit.cs index c13e28987..b61b01dbc 100644 --- a/src/SSCMS.Web/Controllers/Home/LoginController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Home/LoginController.Submit.cs @@ -38,6 +38,8 @@ public async Task> Submit([FromBody] SubmitRequest re user.MobileVerified = true; await _userRepository.UpdateAsync(user); } + + _cacheManager.Remove(codeCacheKey); } else { diff --git a/src/SSCMS.Web/Controllers/Home/LostPasswordController.Submit.cs b/src/SSCMS.Web/Controllers/Home/LostPasswordController.Submit.cs index 8e3aae162..f68ec168c 100644 --- a/src/SSCMS.Web/Controllers/Home/LostPasswordController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Home/LostPasswordController.Submit.cs @@ -37,6 +37,8 @@ public async Task> Submit([FromBody] SubmitRequest requ return this.Error($"更改密码失败:{errorMessage}"); } + _cacheManager.Remove(codeCacheKey); + return new BoolResult { Value = true diff --git a/src/SSCMS.Web/Controllers/Home/ProfileController.VerifyMobile.cs b/src/SSCMS.Web/Controllers/Home/ProfileController.VerifyMobile.cs index 34f0656e5..aa49c0e15 100644 --- a/src/SSCMS.Web/Controllers/Home/ProfileController.VerifyMobile.cs +++ b/src/SSCMS.Web/Controllers/Home/ProfileController.VerifyMobile.cs @@ -22,6 +22,7 @@ public async Task> VerifyMobile([FromBody] VerifyMobile user.MobileVerified = true; var (success, errorMessage) = await _userRepository.UpdateAsync(user); + _cacheManager.Remove(codeCacheKey); return new BoolResult { diff --git a/src/SSCMS.Web/Controllers/Home/RegisterController.VerifyMobile.cs b/src/SSCMS.Web/Controllers/Home/RegisterController.VerifyMobile.cs index 9aebbd00d..e9d25c3be 100644 --- a/src/SSCMS.Web/Controllers/Home/RegisterController.VerifyMobile.cs +++ b/src/SSCMS.Web/Controllers/Home/RegisterController.VerifyMobile.cs @@ -16,6 +16,8 @@ public ActionResult VerifyMobile([FromBody] VerifyMobileRequest requ return this.Error("输入的验证码有误或验证码已超时"); } + _cacheManager.Remove(codeCacheKey); + return new BoolResult { Value = true diff --git a/src/SSCMS.Web/Controllers/Home/VerifyMobileController.Submit.cs b/src/SSCMS.Web/Controllers/Home/VerifyMobileController.Submit.cs index 7fb8e58eb..34545c374 100644 --- a/src/SSCMS.Web/Controllers/Home/VerifyMobileController.Submit.cs +++ b/src/SSCMS.Web/Controllers/Home/VerifyMobileController.Submit.cs @@ -22,6 +22,7 @@ public async Task> Submit([FromBody] SubmitRequest requ user.Mobile = request.Mobile; user.MobileVerified = true; await _userRepository.UpdateAsync(user); + _cacheManager.Remove(codeCacheKey); return new BoolResult { diff --git a/src/SSCMS.Web/Controllers/V1/FormsController.Submit.cs b/src/SSCMS.Web/Controllers/V1/FormsController.Submit.cs index 3078ee917..279c4d78c 100644 --- a/src/SSCMS.Web/Controllers/V1/FormsController.Submit.cs +++ b/src/SSCMS.Web/Controllers/V1/FormsController.Submit.cs @@ -36,9 +36,10 @@ public async Task> Submit([FromQuery] SubmitRequest reque } var isSmsEnabled = await _smsManager.IsSmsEnabledAsync(); + var codeCacheKey = string.Empty; if (isSmsEnabled && form.IsSms) { - var codeCacheKey = GetSmsCodeCacheKey(form.Id, formData.Get("SmsMobile")); + codeCacheKey = GetSmsCodeCacheKey(form.Id, formData.Get("SmsMobile")); var code = _cacheManager.Get(codeCacheKey); if (code == 0 || TranslateUtils.ToInt(formData.Get("SmsCode")) != code) { @@ -54,6 +55,11 @@ public async Task> Submit([FromQuery] SubmitRequest reque formData.FormId = form.Id; formData.Id = await _formDataRepository.InsertAsync(form, formData); + if (isSmsEnabled && form.IsSms) + { + _cacheManager.Remove(codeCacheKey); + } + await _formManager.SendNotifyAsync(form, styles, formData); return formData; diff --git a/tests/SSCMS.Web.Tests/Security/SmsCodeConsumptionSourceTests.cs b/tests/SSCMS.Web.Tests/Security/SmsCodeConsumptionSourceTests.cs new file mode 100644 index 000000000..8f10f2efe --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/SmsCodeConsumptionSourceTests.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class SmsCodeConsumptionSourceTests + { + [Theory] + [InlineData("src/SSCMS.Web/Controllers/Admin/LoginController.Submit.cs")] + [InlineData("src/SSCMS.Web/Controllers/Admin/LostPasswordController.Submit.cs")] + [InlineData("src/SSCMS.Web/Controllers/Home/LoginController.Submit.cs")] + [InlineData("src/SSCMS.Web/Controllers/Home/LostPasswordController.Submit.cs")] + [InlineData("src/SSCMS.Web/Controllers/Home/RegisterController.VerifyMobile.cs")] + [InlineData("src/SSCMS.Web/Controllers/Home/ProfileController.VerifyMobile.cs")] + [InlineData("src/SSCMS.Web/Controllers/Home/VerifyMobileController.Submit.cs")] + [InlineData("src/SSCMS.Web/Controllers/V1/FormsController.Submit.cs")] + public void SuccessfulSmsCodeVerificationConsumesCode(string relativePath) + { + var source = File.ReadAllText(FindRepositoryFile(relativePath)); + + Assert.Contains("_cacheManager.Remove(codeCacheKey)", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From c6a9139887ccc7c82c2dae08e3232b7043c4eb23 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:06:31 +0800 Subject: [PATCH 53/85] fix: cap zip extracted size --- src/SSCMS.Core/Utils/SafeZipUtils.cs | 15 ++++++++- .../Security/ZipExtractionSafetyTests.cs | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/SSCMS.Core/Utils/SafeZipUtils.cs b/src/SSCMS.Core/Utils/SafeZipUtils.cs index 89cc6dbfa..b6fb5baf6 100644 --- a/src/SSCMS.Core/Utils/SafeZipUtils.cs +++ b/src/SSCMS.Core/Utils/SafeZipUtils.cs @@ -2,15 +2,17 @@ using System; using System.IO; using System.Text.RegularExpressions; +using SSCMS.Configuration; namespace SSCMS.Core.Utils { public static class SafeZipUtils { - public static void ExtractZip(string zipFilePath, string directoryPath, string fileFilter = null) + public static void ExtractZip(string zipFilePath, string directoryPath, string fileFilter = null, long maxExtractedBytes = Constants.MaxUploadRequestSize) { var destinationRoot = Path.GetFullPath(directoryPath); Directory.CreateDirectory(destinationRoot); + long extractedBytes = 0; using var fileStream = File.OpenRead(zipFilePath); using var zipFile = new ZipFile(fileStream); @@ -33,6 +35,17 @@ public static void ExtractZip(string zipFilePath, string directoryPath, string f continue; } + if (entry.Size < 0) + { + throw new InvalidOperationException($"Zip entry '{entry.Name}' has an unknown extracted size."); + } + + extractedBytes += entry.Size; + if (extractedBytes > maxExtractedBytes) + { + throw new InvalidOperationException("Zip archive exceeds the maximum extracted size."); + } + var destinationDirectory = Path.GetDirectoryName(destinationPath); if (!string.IsNullOrEmpty(destinationDirectory)) { diff --git a/tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs b/tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs index 441bf9c89..8e3938f4e 100644 --- a/tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs +++ b/tests/SSCMS.Web.Tests/Security/ZipExtractionSafetyTests.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.IO.Compression; +using SSCMS.Core.Utils; using SSCMS.Core.Services; using Xunit; @@ -41,6 +42,37 @@ public void ExtractZipAllowsEntriesInsideDestination() } } + [Fact] + public void ExtractZipRejectsArchivesOverExpandedSizeLimit() + { + var basePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + var zipPath = Path.Combine(basePath, "large.zip"); + var extractPath = Path.Combine(basePath, "extract"); + var extractedPath = Path.Combine(extractPath, "large.txt"); + Directory.CreateDirectory(basePath); + Directory.CreateDirectory(extractPath); + try + { + using (var archive = ZipFile.Open(zipPath, ZipArchiveMode.Create)) + { + var entry = archive.CreateEntry("large.txt"); + using var writer = new StreamWriter(entry.Open()); + writer.Write(new string('A', 32)); + } + + Assert.Throws(() => + SafeZipUtils.ExtractZip(zipPath, extractPath, maxExtractedBytes: 16)); + Assert.False(File.Exists(extractedPath)); + } + finally + { + if (Directory.Exists(basePath)) + { + Directory.Delete(basePath, true); + } + } + } + [Fact] public void ExtractZipRejectsEntriesOutsideDestination() { From 3b17c9e91e56add1c568c8f21ce358e7d1024336 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:12:09 +0800 Subject: [PATCH 54/85] fix: rate limit signed trigger requests --- .../Stl/ActionsTriggerController.Get.cs | 5 + .../Stl/ActionsTriggerController.cs | 44 +++++++- .../Stl/ActionsTriggerControllerTests.cs | 104 +++++++++++++++++- 3 files changed, 149 insertions(+), 4 deletions(-) diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs index aa5141263..43dcde73e 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.Get.cs @@ -17,6 +17,11 @@ public async Task Get([FromQuery] GetRequest request) return Unauthorized(); } + if (!TryConsumeRequestQuota(request.Token, PageUtils.GetIpAddress(Request), out var retryAfterSeconds)) + { + return this.Error($"请求过于频繁,请在{retryAfterSeconds}秒后重试"); + } + var site = await _siteRepository.GetAsync(request.SiteId); var redirectUrl = await _pathManager.GetIndexPageUrlAsync(site, false); diff --git a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs index 06bebf1f4..c85f16890 100644 --- a/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs +++ b/src/SSCMS.Web/Controllers/Stl/ActionsTriggerController.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; using SSCMS.Configuration; +using SSCMS.Core.Utils; using SSCMS.Dto; using SSCMS.Repositories; using SSCMS.Services; @@ -11,14 +13,18 @@ namespace SSCMS.Web.Controllers.Stl [Route(Constants.ApiPrefix + Constants.ApiStlPrefix)] public partial class ActionsTriggerController : ControllerBase { + private const int RateLimitWindowMinutes = 1; + private const int RateLimitMaxRequests = 60; + private readonly ICreateManager _createManager; private readonly IPathManager _pathManager; private readonly ISiteRepository _siteRepository; private readonly IChannelRepository _channelRepository; private readonly IContentRepository _contentRepository; private readonly ISettingsManager _settingsManager; + private readonly ICacheManager _cacheManager; - public ActionsTriggerController(ICreateManager createManager, IPathManager pathManager, ISiteRepository siteRepository, IChannelRepository channelRepository, IContentRepository contentRepository, ISettingsManager settingsManager) + public ActionsTriggerController(ICreateManager createManager, IPathManager pathManager, ISiteRepository siteRepository, IChannelRepository channelRepository, IContentRepository contentRepository, ISettingsManager settingsManager, ICacheManager cacheManager) { _createManager = createManager; _pathManager = pathManager; @@ -26,6 +32,7 @@ public ActionsTriggerController(ICreateManager createManager, IPathManager pathM _channelRepository = channelRepository; _contentRepository = contentRepository; _settingsManager = settingsManager; + _cacheManager = cacheManager; } public class GetRequest : ChannelRequest @@ -63,5 +70,38 @@ private bool IsValidTriggerToken(GetRequest request) return false; } } + + private class RateLimitState + { + public int Count { get; set; } + public DateTime ExpireAt { get; set; } + } + + private static string GetRateLimitCacheKey(string token, string ipAddress) + { + return CacheUtils.GetClassKey(typeof(ActionsTriggerController), "Rate", token ?? string.Empty, ipAddress ?? "unknown"); + } + + private bool TryConsumeRequestQuota(string token, string ipAddress, out int retryAfterSeconds) + { + retryAfterSeconds = 0; + var cacheKey = GetRateLimitCacheKey(token, ipAddress); + var state = _cacheManager.Get(cacheKey); + if (state == null || state.ExpireAt <= DateTime.Now) + { + state = new RateLimitState + { + Count = 0, + ExpireAt = DateTime.Now.AddMinutes(RateLimitWindowMinutes) + }; + } + + state.Count++; + _cacheManager.AddOrUpdateAbsolute(cacheKey, state, RateLimitWindowMinutes); + if (state.Count <= RateLimitMaxRequests) return true; + + retryAfterSeconds = (int)Math.Max(1, Math.Ceiling((state.ExpireAt - DateTime.Now).TotalSeconds)); + return false; + } } } diff --git a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs index 866bef6e6..2b1f6de48 100644 --- a/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs +++ b/tests/SSCMS.Web.Tests/Controllers/Stl/ActionsTriggerControllerTests.cs @@ -1,4 +1,7 @@ +using System.Collections.Generic; using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Moq; using SSCMS.Enums; @@ -28,7 +31,8 @@ public async Task GetRejectsUnsignedTriggerRequests() siteRepository.Object, Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of(), + new TestCacheManager()); var result = await controller.Get(new ActionsTriggerController.GetRequest { @@ -70,7 +74,8 @@ public async Task GetAllowsSignedTriggerRequests() siteRepository.Object, Mock.Of(), Mock.Of(), - settingsManager.Object); + settingsManager.Object, + new TestCacheManager()); var result = await controller.Get(new ActionsTriggerController.GetRequest { @@ -82,5 +87,100 @@ public async Task GetAllowsSignedTriggerRequests() Assert.IsType(result); createManager.Verify(x => x.ExecuteAsync(1, CreateType.Channel, 1, 0, 0, 0), Times.Once); } + + [Fact] + public async Task GetRateLimitsRepeatedSignedTriggerRequests() + { + var createManager = new Mock(); + var pathManager = new Mock(); + pathManager.Setup(x => x.GetIndexPageUrlAsync(It.IsAny(), false)).ReturnsAsync("/"); + + var siteRepository = new Mock(); + siteRepository.Setup(x => x.GetAsync(1)).ReturnsAsync(new Site + { + Id = 1 + }); + + var settingsManager = new Mock(); + settingsManager + .Setup(x => x.Decrypt("signed", null)) + .Returns("1:1:0:0:0:False"); + + var controller = new ActionsTriggerController( + createManager.Object, + pathManager.Object, + siteRepository.Object, + Mock.Of(), + Mock.Of(), + settingsManager.Object, + new TestCacheManager()) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + IActionResult lastResult = null; + for (var i = 0; i < 61; i++) + { + lastResult = await controller.Get(new ActionsTriggerController.GetRequest + { + SiteId = 1, + ChannelId = 1, + Token = "signed" + }); + } + + Assert.IsType(lastResult); + createManager.Verify(x => x.ExecuteAsync(1, CreateType.Channel, 1, 0, 0, 0), Times.Exactly(60)); + } + + private class TestCacheManager : ICacheManager + { + private readonly Dictionary _cache = new Dictionary(); + + public IReadOnlyCacheManagerConfiguration Configuration => null; + + public T Get(string key) + { + return _cache.TryGetValue(key, out var value) ? (T)value : default; + } + + public string GetByFilePath(string filePath) + { + return string.Empty; + } + + public bool Exists(string key) + { + return _cache.ContainsKey(key); + } + + public void AddOrUpdateSliding(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdateAbsolute(string key, T value, int minutes) + { + _cache[key] = value; + } + + public void AddOrUpdate(string key, T value) + { + _cache[key] = value; + } + + public void Remove(string key) + { + _cache.Remove(key); + } + + public void Clear() + { + _cache.Clear(); + } + } } } From 23b04355645af4325c050ed78a8ea319c6450fbb Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:21:04 +0800 Subject: [PATCH 55/85] fix: validate cloud restore keys --- .../Services/CloudManager.Restore.cs | 30 ++++++++++ .../Security/CloudRestoreSafetyTests.cs | 57 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/SSCMS.Web.Tests/Security/CloudRestoreSafetyTests.cs diff --git a/src/SSCMS.Core/Services/CloudManager.Restore.cs b/src/SSCMS.Core/Services/CloudManager.Restore.cs index 820ceeea9..76c968442 100644 --- a/src/SSCMS.Core/Services/CloudManager.Restore.cs +++ b/src/SSCMS.Core/Services/CloudManager.Restore.cs @@ -48,6 +48,11 @@ public async Task RestoreAsync(string restoreId, string backupId) var eTag = listObject.Value; var key = StringUtils.ReplaceStartsWith(storageKey, storagePrefix, string.Empty); + if (!IsSafeBackupFileKey(key)) + { + throw new InvalidOperationException("Unsafe cloud backup file key."); + } + var storageFile = storageFiles.FirstOrDefault(x => x.Key == key); var filePath = PathUtils.Combine(rootPath, key); @@ -110,9 +115,34 @@ await _storageFileRepository.InsertAsync(new StorageFile public static string GetBackupPrefixKey(int userId, string backupId) { + if (!IsSafeBackupId(backupId)) + { + throw new ArgumentException("Invalid backup id.", nameof(backupId)); + } + return $"backups/{userId}/{backupId}/"; } + private static bool IsSafeBackupId(string backupId) + { + return !string.IsNullOrWhiteSpace(backupId) && + !backupId.Contains('/') && + !backupId.Contains('\\') && + backupId != "." && + backupId != ".."; + } + + private static bool IsSafeBackupFileKey(string key) + { + if (string.IsNullOrWhiteSpace(key) || key.Contains('\\') || key.Contains(':') || key.StartsWith("/")) + { + return false; + } + + var segments = key.Split('/'); + return segments.All(segment => !string.IsNullOrEmpty(segment) && segment != "." && segment != ".."); + } + public int GetRestoreProgress(string restoreId) { var progress = _cacheManager.Get(restoreId); diff --git a/tests/SSCMS.Web.Tests/Security/CloudRestoreSafetyTests.cs b/tests/SSCMS.Web.Tests/Security/CloudRestoreSafetyTests.cs new file mode 100644 index 000000000..f6ced52b3 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/CloudRestoreSafetyTests.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using SSCMS.Core.Services; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class CloudRestoreSafetyTests + { + [Theory] + [InlineData("")] + [InlineData("../backup")] + [InlineData("backup/../other")] + [InlineData("/backup")] + [InlineData("backup\\other")] + public void BackupPrefixRejectsUnsafeBackupIds(string backupId) + { + Assert.Throws(() => CloudManager.GetBackupPrefixKey(1, backupId)); + } + + [Theory] + [InlineData("2026-04-12T10:20:30.000Z")] + [InlineData("70b38f98-2c13-4560-9a2f-6207be9bf0be")] + public void BackupPrefixAllowsSingleSegmentBackupIds(string backupId) + { + var prefix = CloudManager.GetBackupPrefixKey(1, backupId); + + Assert.Equal($"backups/1/{backupId}/", prefix); + } + + [Fact] + public void CloudRestoreValidatesObjectKeyBeforeCombiningWithRootPath() + { + var source = File.ReadAllText(FindRepositoryFile("src/SSCMS.Core/Services/CloudManager.Restore.cs")); + + Assert.Contains("if (!IsSafeBackupFileKey(key))", source, StringComparison.Ordinal); + Assert.Contains("var filePath = PathUtils.Combine(rootPath, key);", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From f117dea5898d71890b615454058af548002647c0 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:26:21 +0800 Subject: [PATCH 56/85] fix: cap stl include depth --- .../StlParser/StlElement/StlInclude.cs | 34 ++++++--- src/SSCMS/Parse/ParsePage.cs | 3 + .../Security/StlIncludeSafetyTests.cs | 75 +++++++++++++++++++ 3 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/StlIncludeSafetyTests.cs diff --git a/src/SSCMS.Core/StlParser/StlElement/StlInclude.cs b/src/SSCMS.Core/StlParser/StlElement/StlInclude.cs index ec9df07cf..a4a36bff7 100644 --- a/src/SSCMS.Core/StlParser/StlElement/StlInclude.cs +++ b/src/SSCMS.Core/StlParser/StlElement/StlInclude.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using SSCMS.Core.StlParser.Attributes; @@ -11,6 +12,7 @@ namespace SSCMS.Core.StlParser.StlElement public static class StlInclude { public const string ElementName = "stl:include"; + private const int MaxIncludeDepth = 32; [StlAttribute(Title = "文件路径")] private const string File = nameof(File); @@ -43,24 +45,34 @@ private static async Task ParseAsync(IParseManager parseManager, string { if (string.IsNullOrEmpty(file)) return string.Empty; var pageInfo = parseManager.PageInfo; + if (pageInfo.IncludeDepth >= MaxIncludeDepth) + { + throw new InvalidOperationException($"{ElementName} nesting is too deep."); + } var pageParameters = pageInfo.Parameters; pageInfo.Parameters = parameters; var pageIncludeFile = pageInfo.IncludeFile; pageInfo.IncludeFile = file; + var pageIncludeDepth = pageInfo.IncludeDepth; + pageInfo.IncludeDepth = pageIncludeDepth + 1; var pageEditableIndex = pageInfo.EditableIndex; pageInfo.EditableIndex = 0; - var content = await parseManager.PathManager.GetIncludeContentAsync(pageInfo.Site, file); - var contentBuilder = new StringBuilder(content); - await parseManager.ParseTemplateContentAsync(contentBuilder); - var parsedContent = contentBuilder.ToString(); - - pageInfo.Parameters = pageParameters; - pageInfo.IncludeFile = pageIncludeFile; - pageInfo.EditableIndex = pageEditableIndex; - - return parsedContent; + try + { + var content = await parseManager.PathManager.GetIncludeContentAsync(pageInfo.Site, file); + var contentBuilder = new StringBuilder(content); + await parseManager.ParseTemplateContentAsync(contentBuilder); + return contentBuilder.ToString(); + } + finally + { + pageInfo.Parameters = pageParameters; + pageInfo.IncludeFile = pageIncludeFile; + pageInfo.IncludeDepth = pageIncludeDepth; + pageInfo.EditableIndex = pageEditableIndex; + } } } } diff --git a/src/SSCMS/Parse/ParsePage.cs b/src/SSCMS/Parse/ParsePage.cs index bbb1fa530..4caa7031e 100644 --- a/src/SSCMS/Parse/ParsePage.cs +++ b/src/SSCMS/Parse/ParsePage.cs @@ -38,6 +38,7 @@ public class ParsePage public bool IsLocal { get; set; } public EditMode EditMode { get; } public string IncludeFile { get; set; } + public int IncludeDepth { get; set; } public int EditableIndex { get; set; } public List Editables { get; } @@ -60,6 +61,7 @@ public ParsePage Clone() HeadCodes = new SortedDictionary(HeadCodes), BodyCodes = new SortedDictionary(BodyCodes), FootCodes = new SortedDictionary(FootCodes), + IncludeDepth = IncludeDepth, IsLocal = IsLocal }; } @@ -74,6 +76,7 @@ public ParsePage(IPathManager pathManager, EditMode editMode, Config config, int SpecialId = specialId; IsLocal = false; EditMode = editMode; + IncludeDepth = 0; EditableIndex = 0; Editables = new List(); HeadCodes = new SortedDictionary(); diff --git a/tests/SSCMS.Web.Tests/Security/StlIncludeSafetyTests.cs b/tests/SSCMS.Web.Tests/Security/StlIncludeSafetyTests.cs new file mode 100644 index 000000000..b61454eb7 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/StlIncludeSafetyTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using System.Threading.Tasks; +using Moq; +using SSCMS.Core.StlParser.StlElement; +using SSCMS.Enums; +using SSCMS.Models; +using SSCMS.Parse; +using SSCMS.Services; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class StlIncludeSafetyTests + { + [Fact] + public async Task IncludeRejectsRequestsAfterMaxDepth() + { + const string file = "/loop.html"; + var (parseManager, pathManager, page) = CreateParseManager(file, "recursive"); + page.IncludeDepth = 32; + + var ex = await Assert.ThrowsAsync(() => StlInclude.ParseAsync(parseManager.Object)); + + Assert.Contains("stl:include", ex.Message, StringComparison.OrdinalIgnoreCase); + pathManager.Verify(x => x.GetIncludeContentAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task IncludeRestoresDepthAfterSuccessfulParse() + { + const string file = "/header.html"; + var (parseManager, _, page) = CreateParseManager(file, "header"); + page.IncludeDepth = 2; + + await StlInclude.ParseAsync(parseManager.Object); + + Assert.Equal(2, page.IncludeDepth); + parseManager.Verify(x => x.ParseTemplateContentAsync(It.IsAny()), Times.Once); + } + + private static (Mock ParseManager, Mock PathManager, ParsePage Page) CreateParseManager(string file, string includeContent) + { + var pathManager = new Mock(); + var site = new Site { Id = 1 }; + var template = new Template { TemplateType = TemplateType.IndexPageTemplate }; + var page = new ParsePage(pathManager.Object, EditMode.Default, new Config(), 0, 0, 0, site, template, new Dictionary()); + var context = new ParseContext(page) + { + Attributes = new NameValueCollection + { + { "file", file } + } + }; + + pathManager.Setup(x => x.AddVirtualToUrl(file)).Returns(file); + pathManager.Setup(x => x.GetIncludeContentAsync(site, file)).ReturnsAsync(includeContent); + + var parseManager = new Mock(); + parseManager.SetupGet(x => x.PathManager).Returns(pathManager.Object); + parseManager.SetupGet(x => x.PageInfo).Returns(page); + parseManager.SetupGet(x => x.ContextInfo).Returns(context); + parseManager + .Setup(x => x.ReplaceStlEntitiesForAttributeValueAsync(It.IsAny())) + .ReturnsAsync((string value) => value); + parseManager + .Setup(x => x.ParseTemplateContentAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + return (parseManager, pathManager, page); + } + } +} From 5e0220074e20161bd624c7402ca9fa6ceafb3a6f Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:29:54 +0800 Subject: [PATCH 57/85] fix: enable ueditor xss filters --- .../assets/lib/ueditor/editor_config.js | 6 +-- .../Security/UeditorXssFilterConfigTests.cs | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/UeditorXssFilterConfigTests.cs diff --git a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/editor_config.js b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/editor_config.js index c65639812..9e2b40379 100644 --- a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/editor_config.js +++ b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/editor_config.js @@ -123,9 +123,9 @@ saveInterval: 9999999999999, allHtmlEnabled: !1, pageBreakTag: "[SITESERVER_PAGE]", - xssFilterRules: false, - inputXssFilter: false, - outputXssFilter: false, + xssFilterRules: true, + inputXssFilter: true, + outputXssFilter: true, whitList: { a: ["target", "href", "title", "class", "style", "name"], abbr: ["title", "class", "style"], diff --git a/tests/SSCMS.Web.Tests/Security/UeditorXssFilterConfigTests.cs b/tests/SSCMS.Web.Tests/Security/UeditorXssFilterConfigTests.cs new file mode 100644 index 000000000..627e946b3 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/UeditorXssFilterConfigTests.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class UeditorXssFilterConfigTests + { + [Fact] + public void UeditorEnablesXssFilteringByDefault() + { + var source = File.ReadAllText(FindRepositoryFile("src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/editor_config.js")); + + Assert.Contains("xssFilterRules: true", source, StringComparison.Ordinal); + Assert.Contains("inputXssFilter: true", source, StringComparison.Ordinal); + Assert.Contains("outputXssFilter: true", source, StringComparison.Ordinal); + Assert.DoesNotContain("xssFilterRules: false", source, StringComparison.Ordinal); + Assert.DoesNotContain("inputXssFilter: false", source, StringComparison.Ordinal); + Assert.DoesNotContain("outputXssFilter: false", source, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From 695668950dfd04b4e8dfbeefb3d62b5d462fa8f8 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:35:49 +0800 Subject: [PATCH 58/85] fix: remove docker example secrets --- docker/README.md | 8 ++- docker/cluster/docker-compose.yml | 9 +-- docker/mysql/docker-compose.yml | 18 +++--- docker/postgres/docker-compose.yml | 17 +++--- .../Security/DockerExampleSecurityTests.cs | 56 +++++++++++++++++++ 5 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 tests/SSCMS.Web.Tests/Security/DockerExampleSecurityTests.cs diff --git a/docker/README.md b/docker/README.md index ec7973c0c..0467cca98 100644 --- a/docker/README.md +++ b/docker/README.md @@ -27,12 +27,14 @@ mkdir wwwroot 接下来,我们使用 SQLite 本地数据库运行 SSCMS: ```bash +export SSCMS_SECURITY_KEY="$(uuidgen)" + docker run -d \ --name my-sscms \ -p 80:80 \ --restart=always \ -v "$(pwd)"/wwwroot:/app/wwwroot \ - -e SSCMS_SECURITY_KEY=e2a3d303-ac9b-41ff-9154-930710af0845 \ + -e SSCMS_SECURITY_KEY="$SSCMS_SECURITY_KEY" \ -e SSCMS_DATABASE_TYPE=SQLite \ sscms/core:latest ``` @@ -50,12 +52,14 @@ docker run -d \ 除了将当前文件夹下的 `wwwroot` 目录作为站点根目录存储数据,我们也可以将镜像数据持久化存储在 Volume 中: ```bash +export SSCMS_SECURITY_KEY="$(uuidgen)" + docker run -d \ --name my-sscms \ -p 80:80 \ --restart=always \ -v volume-sscms:/app/wwwroot \ - -e SSCMS_SECURITY_KEY=e2a3d303-ac9b-41ff-9154-930710af0845 \ + -e SSCMS_SECURITY_KEY="$SSCMS_SECURITY_KEY" \ -e SSCMS_DATABASE_TYPE=SQLite \ sscms/core:latest ``` diff --git a/docker/cluster/docker-compose.yml b/docker/cluster/docker-compose.yml index 6152af192..757a9a33b 100644 --- a/docker/cluster/docker-compose.yml +++ b/docker/cluster/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.7" - volumes: volume-sscms: @@ -16,8 +14,7 @@ services: cache: image: "redis" - ports: - - "6379:6379" + command: ["redis-server", "--requirepass", "${SSCMS_REDIS_PASSWORD:?set SSCMS_REDIS_PASSWORD}"] restart: always sscms: @@ -28,9 +25,9 @@ services: expose: - "80" environment: - SSCMS_SECURITY_KEY: e2a3d303-ac9b-41ff-9154-930710af0845 + SSCMS_SECURITY_KEY: ${SSCMS_SECURITY_KEY:?set SSCMS_SECURITY_KEY} SSCMS_DATABASE_TYPE: SQLite - SSCMS_REDIS_HOST: redis://cache + SSCMS_REDIS_HOST: redis://:${SSCMS_REDIS_PASSWORD:?set SSCMS_REDIS_PASSWORD}@cache:6379 volumes: - volume-sscms:/app/wwwroot restart: always diff --git a/docker/mysql/docker-compose.yml b/docker/mysql/docker-compose.yml index 09c9a0895..ec0809421 100644 --- a/docker/mysql/docker-compose.yml +++ b/docker/mysql/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.7" - volumes: volume-mysql: @@ -14,11 +12,11 @@ services: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always - ports: - - "3306:3306" environment: - MYSQL_ROOT_PASSWORD: mysql-password - MYSQL_DATABASE: mysql-db + MYSQL_ROOT_PASSWORD: ${SSCMS_MYSQL_ROOT_PASSWORD:?set SSCMS_MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: sscms + MYSQL_USER: sscms + MYSQL_PASSWORD: ${SSCMS_DATABASE_PASSWORD:?set SSCMS_DATABASE_PASSWORD} volumes: - volume-mysql:/var/lib/mysql @@ -30,12 +28,12 @@ services: ports: - "80:80" environment: - SSCMS_SECURITY_KEY: e2a3d303-ac9b-41ff-9154-930710af0845 + SSCMS_SECURITY_KEY: ${SSCMS_SECURITY_KEY:?set SSCMS_SECURITY_KEY} SSCMS_DATABASE_TYPE: MySQL SSCMS_DATABASE_HOST: sscms-mysql - SSCMS_DATABASE_USER: root - SSCMS_DATABASE_PASSWORD: mysql-password - SSCMS_DATABASE_NAME: mysql-db + SSCMS_DATABASE_USER: sscms + SSCMS_DATABASE_PASSWORD: ${SSCMS_DATABASE_PASSWORD:?set SSCMS_DATABASE_PASSWORD} + SSCMS_DATABASE_NAME: sscms volumes: - volume-sscms:/app/wwwroot diff --git a/docker/postgres/docker-compose.yml b/docker/postgres/docker-compose.yml index b3e5b2e1c..1e96ee1e0 100644 --- a/docker/postgres/docker-compose.yml +++ b/docker/postgres/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.7" - volumes: volume-postgres: @@ -13,11 +11,10 @@ services: sscms-postgres: image: postgres restart: always - ports: - - "5432:5432" environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres-password + POSTGRES_USER: sscms + POSTGRES_PASSWORD: ${SSCMS_DATABASE_PASSWORD:?set SSCMS_DATABASE_PASSWORD} + POSTGRES_DB: sscms volumes: - volume-postgres:/var/lib/postgresql/data @@ -29,12 +26,12 @@ services: ports: - "80:80" environment: - SSCMS_SECURITY_KEY: e2a3d303-ac9b-41ff-9154-930710af0845 + SSCMS_SECURITY_KEY: ${SSCMS_SECURITY_KEY:?set SSCMS_SECURITY_KEY} SSCMS_DATABASE_TYPE: PostgreSQL SSCMS_DATABASE_HOST: sscms-postgres - SSCMS_DATABASE_USER: postgres - SSCMS_DATABASE_PASSWORD: postgres-password - SSCMS_DATABASE_NAME: postgres + SSCMS_DATABASE_USER: sscms + SSCMS_DATABASE_PASSWORD: ${SSCMS_DATABASE_PASSWORD:?set SSCMS_DATABASE_PASSWORD} + SSCMS_DATABASE_NAME: sscms volumes: - volume-sscms:/app/wwwroot diff --git a/tests/SSCMS.Web.Tests/Security/DockerExampleSecurityTests.cs b/tests/SSCMS.Web.Tests/Security/DockerExampleSecurityTests.cs new file mode 100644 index 000000000..302da6e07 --- /dev/null +++ b/tests/SSCMS.Web.Tests/Security/DockerExampleSecurityTests.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using Xunit; + +namespace SSCMS.Web.Tests.Security +{ + public class DockerExampleSecurityTests + { + [Theory] + [InlineData("docker/mysql/docker-compose.yml")] + [InlineData("docker/postgres/docker-compose.yml")] + [InlineData("docker/cluster/docker-compose.yml")] + [InlineData("docker/README.md")] + public void DockerExamplesDoNotShipReusableSecrets(string relativePath) + { + var source = File.ReadAllText(FindRepositoryFile(relativePath)); + + Assert.DoesNotContain("e2a3d303-ac9b-41ff-9154-930710af0845", source, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("mysql-password", source, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("postgres-password", source, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void DockerDatabaseExamplesDoNotUseRootApplicationConnectionOrExposeInternalPorts() + { + var mysql = File.ReadAllText(FindRepositoryFile("docker/mysql/docker-compose.yml")); + var postgres = File.ReadAllText(FindRepositoryFile("docker/postgres/docker-compose.yml")); + var cluster = File.ReadAllText(FindRepositoryFile("docker/cluster/docker-compose.yml")); + + Assert.DoesNotContain("SSCMS_DATABASE_USER: root", mysql, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("\"3306:3306\"", mysql, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("\"5432:5432\"", postgres, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("\"6379:6379\"", cluster, StringComparison.OrdinalIgnoreCase); + Assert.Contains("SSCMS_SECURITY_KEY: ${SSCMS_SECURITY_KEY:?", mysql, StringComparison.Ordinal); + Assert.Contains("SSCMS_SECURITY_KEY: ${SSCMS_SECURITY_KEY:?", postgres, StringComparison.Ordinal); + Assert.Contains("SSCMS_SECURITY_KEY: ${SSCMS_SECURITY_KEY:?", cluster, StringComparison.Ordinal); + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + { + return candidate; + } + + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } + } +} From 5075afd5ab5afcad78e68ec4e3ec5a89e5c793d4 Mon Sep 17 00:00:00 2001 From: bbingz Date: Sun, 31 May 2026 01:40:10 +0800 Subject: [PATCH 59/85] fix: stop publishing source maps --- .../ueditor/third-party/jquery-1.10.2.min.js | 1 - .../ueditor/third-party/jquery-1.10.2.min.map | 1 - .../assets/lib/wangEditor/wangEditor.min.js | 1 - .../lib/wangEditor/wangEditor.min.js.map | 1 - .../Security/PublicSourceMapTests.cs | 64 +++++++++++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) delete mode 100644 src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.map delete mode 100644 src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js.map create mode 100644 tests/SSCMS.Web.Tests/Security/PublicSourceMapTests.cs diff --git a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.js b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.js index da4170647..ce1b6b6e0 100644 --- a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.js +++ b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.js @@ -1,5 +1,4 @@ /*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license -//@ sourceMappingURL=jquery-1.10.2.min.map */ (function(e,t){var n,r,i=typeof t,o=e.location,a=e.document,s=a.documentElement,l=e.jQuery,u=e.$,c={},p=[],f="1.10.2",d=p.concat,h=p.push,g=p.slice,m=p.indexOf,y=c.toString,v=c.hasOwnProperty,b=f.trim,x=function(e,t){return new x.fn.init(e,t,r)},w=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=/\S+/g,C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="
",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
a",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="
t
",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t }({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:x.support.htmlSerialize?[0,"",""]:[1,"X
","
"]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle); diff --git a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.map b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.map deleted file mode 100644 index 4dc4920bb..000000000 --- a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/ueditor/third-party/jquery-1.10.2.min.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"jquery-1.10.2.min.js","sources":["jquery-1.10.2.js"],"names":["window","undefined","readyList","rootjQuery","core_strundefined","location","document","docElem","documentElement","_jQuery","jQuery","_$","$","class2type","core_deletedIds","core_version","core_concat","concat","core_push","push","core_slice","slice","core_indexOf","indexOf","core_toString","toString","core_hasOwn","hasOwnProperty","core_trim","trim","selector","context","fn","init","core_pnum","source","core_rnotwhite","rtrim","rquickExpr","rsingleTag","rvalidchars","rvalidbraces","rvalidescape","rvalidtokens","rmsPrefix","rdashAlpha","fcamelCase","all","letter","toUpperCase","completed","event","addEventListener","type","readyState","detach","ready","removeEventListener","detachEvent","prototype","jquery","constructor","match","elem","this","charAt","length","exec","find","merge","parseHTML","nodeType","ownerDocument","test","isPlainObject","isFunction","attr","getElementById","parentNode","id","makeArray","toArray","call","get","num","pushStack","elems","ret","prevObject","each","callback","args","promise","done","apply","arguments","first","eq","last","i","len","j","map","end","sort","splice","extend","src","copyIsArray","copy","name","options","clone","target","deep","isArray","expando","Math","random","replace","noConflict","isReady","readyWait","holdReady","hold","wait","body","setTimeout","resolveWith","trigger","off","obj","Array","isWindow","isNumeric","isNaN","parseFloat","isFinite","String","key","e","support","ownLast","isEmptyObject","error","msg","Error","data","keepScripts","parsed","scripts","createElement","buildFragment","remove","childNodes","parseJSON","JSON","parse","Function","parseXML","xml","tmp","DOMParser","parseFromString","ActiveXObject","async","loadXML","getElementsByTagName","noop","globalEval","execScript","camelCase","string","nodeName","toLowerCase","value","isArraylike","text","arr","results","Object","inArray","max","second","l","grep","inv","retVal","arg","guid","proxy","access","chainable","emptyGet","raw","bulk","now","Date","getTime","swap","old","style","Deferred","attachEvent","top","frameElement","doScroll","doScrollCheck","split","cachedruns","Expr","getText","isXML","compile","outermostContext","sortInput","setDocument","documentIsHTML","rbuggyQSA","rbuggyMatches","matches","contains","preferredDoc","dirruns","classCache","createCache","tokenCache","compilerCache","hasDuplicate","sortOrder","a","b","strundefined","MAX_NEGATIVE","hasOwn","pop","push_native","booleans","whitespace","characterEncoding","identifier","attributes","pseudos","RegExp","rcomma","rcombinators","rsibling","rattributeQuotes","rpseudo","ridentifier","matchExpr","ID","CLASS","TAG","ATTR","PSEUDO","CHILD","bool","needsContext","rnative","rinputs","rheader","rescape","runescape","funescape","_","escaped","escapedWhitespace","high","fromCharCode","els","Sizzle","seed","m","groups","nid","newContext","newSelector","getElementsByClassName","qsa","tokenize","getAttribute","setAttribute","toSelector","join","querySelectorAll","qsaError","removeAttribute","select","keys","cache","cacheLength","shift","markFunction","assert","div","removeChild","addHandle","attrs","handler","attrHandle","siblingCheck","cur","diff","sourceIndex","nextSibling","createInputPseudo","createButtonPseudo","createPositionalPseudo","argument","matchIndexes","node","doc","parent","defaultView","className","appendChild","createComment","innerHTML","firstChild","getById","getElementsByName","filter","attrId","getAttributeNode","tag","input","matchesSelector","webkitMatchesSelector","mozMatchesSelector","oMatchesSelector","msMatchesSelector","disconnectedMatch","compareDocumentPosition","adown","bup","compare","sortDetached","aup","ap","bp","unshift","expr","elements","val","specified","uniqueSort","duplicates","detectDuplicates","sortStable","textContent","nodeValue","selectors","createPseudo","relative",">","dir"," ","+","~","preFilter","excess","unquoted","nodeNameSelector","pattern","operator","check","result","what","simple","forward","ofType","outerCache","nodeIndex","start","useCache","lastChild","pseudo","setFilters","idx","matched","not","matcher","unmatched","has","innerText","lang","elemLang","hash","root","focus","activeElement","hasFocus","href","tabIndex","enabled","disabled","checked","selected","selectedIndex","empty","header","button","even","odd","lt","gt","radio","checkbox","file","password","image","submit","reset","filters","parseOnly","tokens","soFar","preFilters","cached","addCombinator","combinator","base","checkNonElements","doneName","dirkey","elementMatcher","matchers","condense","newUnmatched","mapped","setMatcher","postFilter","postFinder","postSelector","temp","preMap","postMap","preexisting","multipleContexts","matcherIn","matcherOut","matcherFromTokens","checkContext","leadingRelative","implicitRelative","matchContext","matchAnyContext","matcherFromGroupMatchers","elementMatchers","setMatchers","matcherCachedRuns","bySet","byElement","superMatcher","expandContext","setMatched","matchedCount","outermost","contextBackup","dirrunsUnique","group","contexts","token","div1","defaultValue","unique","isXMLDoc","optionsCache","createOptions","object","flag","Callbacks","firing","memory","fired","firingLength","firingIndex","firingStart","list","stack","once","fire","stopOnFalse","self","disable","add","index","lock","locked","fireWith","func","tuples","state","always","deferred","fail","then","fns","newDefer","tuple","action","returned","resolve","reject","progress","notify","pipe","stateString","when","subordinate","resolveValues","remaining","updateFunc","values","progressValues","notifyWith","progressContexts","resolveContexts","fragment","opt","eventName","isSupported","cssText","getSetAttribute","leadingWhitespace","tbody","htmlSerialize","hrefNormalized","opacity","cssFloat","checkOn","optSelected","enctype","html5Clone","cloneNode","outerHTML","inlineBlockNeedsLayout","shrinkWrapBlocks","pixelPosition","deleteExpando","noCloneEvent","reliableMarginRight","boxSizingReliable","noCloneChecked","optDisabled","radioValue","createDocumentFragment","appendChecked","checkClone","click","change","focusin","backgroundClip","clearCloneStyle","container","marginDiv","tds","divReset","offsetHeight","display","reliableHiddenOffsets","zoom","boxSizing","offsetWidth","getComputedStyle","width","marginRight","rbrace","rmultiDash","internalData","pvt","acceptData","thisCache","internalKey","isNode","toJSON","internalRemoveData","isEmptyDataObject","cleanData","noData","applet","embed","hasData","removeData","_data","_removeData","dataAttr","queue","dequeue","startLength","hooks","_queueHooks","next","stop","setter","delay","time","fx","speeds","timeout","clearTimeout","clearQueue","count","defer","nodeHook","boolHook","rclass","rreturn","rfocusable","rclickable","ruseDefault","getSetInput","removeAttr","prop","removeProp","propFix","addClass","classes","clazz","proceed","removeClass","toggleClass","stateVal","classNames","hasClass","valHooks","set","option","one","optionSet","nType","attrHooks","propName","attrNames","for","class","notxml","propHooks","tabindex","parseInt","getter","setAttributeNode","createAttribute","coords","contenteditable","rformElems","rkeyEvent","rmouseEvent","rfocusMorph","rtypenamespace","returnTrue","returnFalse","safeActiveElement","err","global","types","events","t","handleObjIn","special","eventHandle","handleObj","handlers","namespaces","origType","elemData","handle","triggered","dispatch","delegateType","bindType","namespace","delegateCount","setup","mappedTypes","origCount","teardown","removeEvent","onlyHandlers","ontype","bubbleType","eventPath","Event","isTrigger","namespace_re","noBubble","parentWindow","isPropagationStopped","preventDefault","isDefaultPrevented","_default","fix","handlerQueue","delegateTarget","preDispatch","currentTarget","isImmediatePropagationStopped","stopPropagation","postDispatch","sel","originalEvent","fixHook","fixHooks","mouseHooks","keyHooks","props","srcElement","metaKey","original","which","charCode","keyCode","eventDoc","fromElement","pageX","clientX","scrollLeft","clientLeft","pageY","clientY","scrollTop","clientTop","relatedTarget","toElement","load","blur","beforeunload","returnValue","simulate","bubble","isSimulated","defaultPrevented","getPreventDefault","timeStamp","cancelBubble","stopImmediatePropagation","mouseenter","mouseleave","orig","related","submitBubbles","form","_submit_bubble","changeBubbles","propertyName","_just_changed","focusinBubbles","attaches","on","origFn","triggerHandler","isSimple","rparentsprev","rneedsContext","guaranteedUnique","children","contents","prev","targets","winnow","is","closest","pos","prevAll","addBack","sibling","parents","parentsUntil","until","nextAll","nextUntil","prevUntil","siblings","contentDocument","contentWindow","reverse","n","r","qualifier","createSafeFragment","nodeNames","safeFrag","rinlinejQuery","rnoshimcache","rleadingWhitespace","rxhtmlTag","rtagName","rtbody","rhtml","rnoInnerhtml","manipulation_rcheckableType","rchecked","rscriptType","rscriptTypeMasked","rcleanScript","wrapMap","legend","area","param","thead","tr","col","td","safeFragment","fragmentDiv","optgroup","tfoot","colgroup","caption","th","append","createTextNode","domManip","manipulationTarget","prepend","insertBefore","before","after","keepData","getAll","setGlobalEval","dataAndEvents","deepDataAndEvents","html","replaceWith","allowIntersection","hasScripts","iNoClone","disableScript","restoreScript","_evalUrl","content","refElements","cloneCopyEvent","dest","oldData","curData","fixCloneNodeIssues","defaultChecked","defaultSelected","appendTo","prependTo","insertAfter","replaceAll","insert","found","fixDefaultChecked","destElements","srcElements","inPage","selection","wrap","safe","nodes","url","ajax","dataType","throws","wrapAll","wrapInner","unwrap","iframe","getStyles","curCSS","ralpha","ropacity","rposition","rdisplayswap","rmargin","rnumsplit","rnumnonpx","rrelNum","elemdisplay","BODY","cssShow","position","visibility","cssNormalTransform","letterSpacing","fontWeight","cssExpand","cssPrefixes","vendorPropName","capName","origName","isHidden","el","css","showHide","show","hidden","css_defaultDisplay","styles","hide","toggle","cssHooks","computed","cssNumber","columnCount","fillOpacity","lineHeight","order","orphans","widows","zIndex","cssProps","float","extra","_computed","minWidth","maxWidth","getPropertyValue","currentStyle","left","rs","rsLeft","runtimeStyle","pixelLeft","setPositiveNumber","subtract","augmentWidthOrHeight","isBorderBox","getWidthOrHeight","valueIsBorderBox","actualDisplay","write","close","$1","visible","margin","padding","border","prefix","suffix","expand","expanded","parts","r20","rbracket","rCRLF","rsubmitterTypes","rsubmittable","serialize","serializeArray","traditional","s","encodeURIComponent","ajaxSettings","buildParams","v","hover","fnOver","fnOut","bind","unbind","delegate","undelegate","ajaxLocParts","ajaxLocation","ajax_nonce","ajax_rquery","rhash","rts","rheaders","rlocalProtocol","rnoContent","rprotocol","rurl","_load","prefilters","transports","allTypes","addToPrefiltersOrTransports","structure","dataTypeExpression","dataTypes","inspectPrefiltersOrTransports","originalOptions","jqXHR","inspected","seekingTransport","inspect","prefilterOrFactory","dataTypeOrTransport","ajaxExtend","flatOptions","params","response","responseText","complete","status","active","lastModified","etag","isLocal","processData","contentType","accepts","*","json","responseFields","converters","* text","text html","text json","text xml","ajaxSetup","settings","ajaxPrefilter","ajaxTransport","cacheURL","responseHeadersString","timeoutTimer","fireGlobals","transport","responseHeaders","callbackContext","globalEventContext","completeDeferred","statusCode","requestHeaders","requestHeadersNames","strAbort","getResponseHeader","getAllResponseHeaders","setRequestHeader","lname","overrideMimeType","mimeType","code","abort","statusText","finalText","success","method","crossDomain","hasContent","ifModified","headers","beforeSend","send","nativeStatusText","responses","isSuccess","modified","ajaxHandleResponses","ajaxConvert","rejectWith","getJSON","getScript","firstDataType","ct","finalDataType","conv2","current","conv","dataFilter","script","text script","head","scriptCharset","charset","onload","onreadystatechange","isAbort","oldCallbacks","rjsonp","jsonp","jsonpCallback","originalSettings","callbackName","overwritten","responseContainer","jsonProp","xhrCallbacks","xhrSupported","xhrId","xhrOnUnloadAbort","createStandardXHR","XMLHttpRequest","createActiveXHR","xhr","cors","username","open","xhrFields","firefoxAccessException","unload","fxNow","timerId","rfxtypes","rfxnum","rrun","animationPrefilters","defaultPrefilter","tweeners","tween","createTween","unit","scale","maxIterations","createFxNow","animation","collection","Animation","properties","stopped","tick","currentTime","startTime","duration","percent","tweens","run","opts","specialEasing","originalProperties","Tween","easing","gotoEnd","propFilter","timer","anim","tweener","prefilter","oldfire","dataShow","unqueued","overflow","overflowX","overflowY","eased","step","cssFn","speed","animate","genFx","fadeTo","to","optall","doAnimation","finish","stopQueue","timers","includeWidth","height","slideDown","slideUp","slideToggle","fadeIn","fadeOut","fadeToggle","linear","p","swing","cos","PI","interval","setInterval","clearInterval","slow","fast","animated","offset","setOffset","win","box","getBoundingClientRect","getWindow","pageYOffset","pageXOffset","curElem","curOffset","curCSSTop","curCSSLeft","calculatePosition","curPosition","curTop","curLeft","using","offsetParent","parentOffset","scrollTo","Height","Width","defaultExtra","funcName","size","andSelf","module","exports","define","amd"],"mappings":"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"} \ No newline at end of file diff --git a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js index dd2ede2ef..141621702 100644 --- a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js +++ b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js @@ -1,4 +1,3 @@ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.wangEditor=t()}(this,function(){"use strict";function e(e){var t=void 0;return t=document.createElement("div"),t.innerHTML=e,t.children}function t(e){return!!e&&(e instanceof HTMLCollection||e instanceof NodeList)}function n(e){var n=document.querySelectorAll(e);return t(n)?n:[n]}function i(o){if(o){if(o instanceof i)return o;this.selector=o;var A=o.nodeType,r=[];9===A?r=[o]:1===A?r=[o]:t(o)||o instanceof Array?r=o:"string"==typeof o&&(o=o.replace("/\n/mg","").trim(),r=0===o.indexOf("<")?e(o):n(o));var c=r.length;if(!c)return this;var a=void 0;for(a=0;a/gm,">").replace(/"/gm,""").replace(/(\r\n|\r|\n)/g,"
")}function s(e){return"function"==typeof e}function l(e){this.editor=e,this.$elem=o('
\n \n
'),this.type="click",this._active=!1}function d(e,t){var n=this,i=e.editor;this.menu=e,this.opt=t;var A=o('
'),r=t.$title,c=void 0;r&&(c=r.html(),c=O(i,c),r.html(c),r.addClass("w-e-dp-title"),A.append(r));var a=t.list||[],s=t.type||"list",l=t.onClick||$,d=o('
    ');A.append(d),a.forEach(function(e){var t=e.$elem,A=t.html();A=O(i,A),t.html(A);var r=e.value,c=o('
  • ');t&&(c.append(t),d.append(c),c.on("click",function(e){l(r),n.hideTimeoutId=setTimeout(function(){n.hide()},0)}))}),A.on("mouseleave",function(e){n.hideTimeoutId=setTimeout(function(){n.hide()},0)}),this.$container=A,this._rendered=!1,this._show=!1}function u(e){var t=this;this.editor=e,this.$elem=o('
    '),this.type="droplist",this._active=!1,this.droplist=new d(this,{width:100,$title:o("

    设置标题

    "),type:"list",list:[{$elem:o("

    H1

    "),value:"

    "},{$elem:o("

    H2

    "),value:"

    "},{$elem:o("

    H3

    "),value:"

    "},{$elem:o("

    H4

    "),value:"

    "},{$elem:o("

    H5
    "),value:"
    "},{$elem:o("

    正文

    "),value:"

    "}],onClick:function(e){t._command(e)}})}function h(e){var t=this;this.editor=e,this.$elem=o('

    '),this.type="droplist",this._active=!1,this.droplist=new d(this,{width:160,$title:o("

    字号

    "),type:"list",list:[{$elem:o('x-small'),value:"1"},{$elem:o('small'),value:"2"},{$elem:o("normal"),value:"3"},{$elem:o('large'),value:"4"},{$elem:o('x-large'),value:"5"},{$elem:o('xx-large'),value:"6"}],onClick:function(e){t._command(e)}})}function p(e){var t=this;this.editor=e,this.$elem=o('
    '),this.type="droplist",this._active=!1;var n=e.config,i=n.fontNames||[];this.droplist=new d(this,{width:100,$title:o("

    字体

    "),type:"list",list:i.map(function(e){return{$elem:o(''+e+""),value:e}}),onClick:function(e){t._command(e)}})}function f(e,t){this.menu=e,this.opt=t}function m(e){this.editor=e,this.$elem=o('
    '),this.type="panel",this._active=!1}function g(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="click",this._active=!1}function w(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="click",this._active=!1}function v(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="click",this._active=!1}function E(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="click",this._active=!1}function b(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="click",this._active=!1}function B(e){var t=this;this.editor=e,this.$elem=o('
    '),this.type="droplist",this._active=!1,this.droplist=new d(this,{width:120,$title:o("

    设置列表

    "),type:"list",list:[{$elem:o(' 有序列表'),value:"insertOrderedList"},{$elem:o(' 无序列表'),value:"insertUnorderedList"}],onClick:function(e){t._command(e)}})}function y(e){var t=this;this.editor=e,this.$elem=o('
    '),this.type="droplist",this._active=!1,this.droplist=new d(this,{width:100,$title:o("

    对齐方式

    "),type:"list",list:[{$elem:o(' 靠左'),value:"justifyLeft"},{$elem:o(' 居中'),value:"justifyCenter"},{$elem:o(' 靠右'),value:"justifyRight"}],onClick:function(e){t._command(e)}})}function C(e){var t=this;this.editor=e,this.$elem=o('
    '),this.type="droplist";var n=e.config,i=n.colors||[];this._active=!1,this.droplist=new d(this,{width:120,$title:o("

    文字颜色

    "),type:"inline-block",list:i.map(function(e){return{$elem:o(''),value:e}}),onClick:function(e){t._command(e)}})}function x(e){var t=this;this.editor=e,this.$elem=o('
    '),this.type="droplist";var n=e.config,i=n.colors||[];this._active=!1,this.droplist=new d(this,{width:120,$title:o("

    背景色

    "),type:"inline-block",list:i.map(function(e){return{$elem:o(''),value:e}}),onClick:function(e){t._command(e)}})}function I(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="click",this._active=!1}function Q(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="panel",this._active=!1}function M(e){this.editor=e,this.$elem=o('
    \n \n
    '),this.type="panel",this._active=!1}function S(e){this.editor=e,this.$elem=o('
    '),this.type="panel",this._active=!1}function k(e){this.editor=e,this.$elem=o('
    '),this.type="panel",this._active=!1}function D(e){this.editor=e;var t=c("w-e-img");this.$elem=o('
    '),e.imgMenuId=t,this.type="panel",this._active=!1}function _(e){this.editor=e,this.menus={}}function N(e){var t=e.clipboardData||e.originalEvent&&e.originalEvent.clipboardData,n=void 0;return n=null==t?window.clipboardData&&window.clipboardData.getData("text"):t.getData("text/plain"),a(n)}function F(e,t,n){var i=e.clipboardData||e.originalEvent&&e.originalEvent.clipboardData,o=void 0,A=void 0;if(null==i?o=window.clipboardData&&window.clipboardData.getData("text"):(o=i.getData("text/plain"),A=i.getData("text/html")),!A&&o&&(A="

    "+a(o)+"

    "),A){var r=A.split("");return 2===r.length&&(A=r[0]),A=A.replace(/<(meta|script|link).+?>/gim,""),A=A.replace(//gm,""),A=A.replace(/\s?data-.+?=('|").+?('|")/gim,""),n&&(A=A.replace(//gim,"")),A=t?A.replace(/\s?(class|style)=('|").*?('|")/gim,""):A.replace(/\s?class=('|").*?('|")/gim,"")}}function T(e){var t=[];if(N(e))return t;var n=e.clipboardData||e.originalEvent&&e.originalEvent.clipboardData||{},i=n.items;return i?(A(i,function(e,n){var i=n.type;/image/i.test(i)&&t.push(n.getAsFile())}),t):t}function R(e){var t=[];return(e.childNodes()||[]).forEach(function(e){var n=void 0,i=e.nodeType;if(3===i&&(n=e.textContent,n=a(n)),1===i){n={},n.tag=e.nodeName.toLowerCase();for(var A=[],r=e.attributes||{},c=r.length||0,s=0;s')}function L(e){this.editor=e}function j(e,t){if(null==e)throw new Error("错误:初始化编辑器时候未传入任何参数,请查阅文档");this.id="wangEditor-"+W++,this.toolbarSelector=e,this.textSelector=t,this.customConfig={}}var G=[];i.prototype={constructor:i,forEach:function(e){var t=void 0;for(t=0;t=t&&(e%=t),o(this[e])},first:function(){return this.get(0)},last:function(){var e=this.length;return this.get(e-1)},on:function(e,t,n){n||(n=t,t=null);var i=[];return i=e.split(/\s+/),this.forEach(function(e){i.forEach(function(i){if(i){if(G.push({elem:e,type:i,fn:n}),!t)return void e.addEventListener(i,n);e.addEventListener(i,function(e){var i=e.target;i.matches(t)&&n.call(i,e)})}})})},off:function(e,t){return this.forEach(function(n){n.removeEventListener(e,t)})},attr:function(e,t){return null==t?this[0].getAttribute(e):this.forEach(function(n){n.setAttribute(e,t)})},addClass:function(e){return e?this.forEach(function(t){var n=void 0;t.className?(n=t.className.split(/\s/),n=n.filter(function(e){return!!e.trim()}),n.indexOf(e)<0&&n.push(e),t.className=n.join(" ")):t.className=e}):this},removeClass:function(e){return e?this.forEach(function(t){var n=void 0;t.className&&(n=t.className.split(/\s/),n=n.filter(function(t){return!(!(t=t.trim())||t===e)}),t.className=n.join(" "))}):this},css:function(e,t){var n=e+":"+t+";";return this.forEach(function(t){var i=(t.getAttribute("style")||"").trim(),o=void 0,A=[];i?(o=i.split(";"),o.forEach(function(e){var t=e.split(":").map(function(e){return e.trim()});2===t.length&&A.push(t[0]+":"+t[1])}),A=A.map(function(t){return 0===t.indexOf(e)?n:t}),A.indexOf(n)<0&&A.push(n),t.setAttribute("style",A.join("; "))):t.setAttribute("style",n)})},show:function(){return this.css("display","block")},hide:function(){return this.css("display","none")},children:function(){var e=this[0];return e?o(e.children):null},childNodes:function(){var e=this[0];return e?o(e.childNodes):null},append:function(e){return this.forEach(function(t){e.forEach(function(e){t.appendChild(e)})})},remove:function(){return this.forEach(function(e){if(e.remove)e.remove();else{var t=e.parentElement;t&&t.removeChild(e)}})},isContain:function(e){var t=this[0],n=e[0];return t.contains(n)},getSizeData:function(){return this[0].getBoundingClientRect()},getNodeName:function(){return this[0].nodeName},find:function(e){return o(this[0].querySelectorAll(e))},text:function(e){return e?this.forEach(function(t){t.innerHTML=e}):this[0].innerHTML.replace(/<.*?>/g,function(){return""})},html:function(e){var t=this[0];return null==e?t.innerHTML:(t.innerHTML=e,this)},val:function(){return this[0].value.trim()},focus:function(){return this.forEach(function(e){e.focus()})},parent:function(){return o(this[0].parentElement)},parentUntil:function(e,t){var n=document.querySelectorAll(e),i=n.length;if(!i)return null;var A=t||this[0];if("BODY"===A.nodeName)return null;var r=A.parentElement,c=void 0;for(c=0;c=0)){var n=t.editor,i=o("body"),A=n.$textContainerElem,r=this.opt,c=o('
    '),a=r.width||300;c.css("width",a+"px").css("margin-left",(0-a)/2+"px");var s=o('');c.append(s),s.on("click",function(){e.hide()});var l=o('
      '),d=o('
      ');c.append(l).append(d);var u=r.height;u&&d.css("height",u+"px").css("overflow-y","auto");var h=r.tabs||[],p=[],f=[];h.forEach(function(e,t){if(e){var i=e.title||"",A=e.tpl||"";i=O(n,i),A=O(n,A);var r=o('
    • '+i+"
    • ");l.append(r);var c=o(A);d.append(c),r._index=t,p.push(r),f.push(c),0===t?(r._active=!0,r.addClass("w-e-active")):c.hide(),r.on("click",function(e){r._active||(p.forEach(function(e){e._active=!1,e.removeClass("w-e-active")}),f.forEach(function(e){e.hide()}),r._active=!0,r.addClass("w-e-active"),c.show())})}}),c.on("click",function(e){e.stopPropagation()}),i.on("click",function(t){e.hide()}),A.append(c),h.forEach(function(t,n){if(t){(t.events||[]).forEach(function(t){var i=t.selector,o=t.type,A=t.fn||V;f[n].find(i).on(o,function(t){t.stopPropagation(),A(t)&&e.hide()})})}});var m=c.find("input[type=text],textarea");m.length&&m.get(0).focus(),this.$container=c,this._hideOtherPanels(),K.push(t)}},hide:function(){var e=this.menu,t=this.$container;t&&t.remove(),K=K.filter(function(t){return t!==e})},_hideOtherPanels:function(){K.length&&K.forEach(function(e){var t=e.panel||{};t.hide&&t.hide()})}},m.prototype={constructor:m,onClick:function(e){var t=this.editor,n=void 0;if(this._active){if(!(n=t.selection.getSelectionContainerElem()))return;t.selection.createRangeByElem(n),t.selection.restoreSelection(),this._createPanel(n.text(),n.attr("href"))}else t.selection.isSelectionEmpty()?this._createPanel("",""):this._createPanel(t.selection.getSelectionText(),"")},_createPanel:function(e,t){var n=this,i=c("input-link"),A=c("input-text"),r=c("btn-ok"),a=c("btn-del"),s=this._active?"inline-block":"none",l=new f(this,{width:300,tabs:[{title:"链接",tpl:'
      \n \n \n
      \n \n \n
      \n
      ',events:[{selector:"#"+r,type:"click",fn:function(){var e=o("#"+i),t=o("#"+A),r=e.val(),c=t.val();return n._insertLink(c,r),!0}},{selector:"#"+a,type:"click",fn:function(){return n._delLink(),!0}}]}]});l.show(),this.panel=l},_delLink:function(){if(this._active){var e=this.editor;if(e.selection.getSelectionContainerElem()){var t=e.selection.getSelectionText();e.cmd.do("insertHTML",""+t+"")}}},_insertLink:function(e,t){var n=this.editor,i=n.config,o=i.linkCheck,A=!0;o&&"function"==typeof o&&(A=o(e,t)),!0===A?n.cmd.do("insertHTML",''+e+""):alert(A)},tryChangeActive:function(e){var t=this.editor,n=this.$elem,i=t.selection.getSelectionContainerElem();i&&("A"===i.getNodeName()?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active")))}},g.prototype={constructor:g,onClick:function(e){var t=this.editor,n=t.selection.isSelectionEmpty();n&&t.selection.createEmptyRange(),t.cmd.do("italic"),n&&(t.selection.collapseRange(),t.selection.restoreSelection())},tryChangeActive:function(e){var t=this.editor,n=this.$elem;t.cmd.queryCommandState("italic")?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}},w.prototype={constructor:w,onClick:function(e){this.editor.cmd.do("redo")}},v.prototype={constructor:v,onClick:function(e){var t=this.editor,n=t.selection.isSelectionEmpty();n&&t.selection.createEmptyRange(),t.cmd.do("strikeThrough"),n&&(t.selection.collapseRange(),t.selection.restoreSelection())},tryChangeActive:function(e){var t=this.editor,n=this.$elem;t.cmd.queryCommandState("strikeThrough")?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}},E.prototype={constructor:E,onClick:function(e){var t=this.editor,n=t.selection.isSelectionEmpty();n&&t.selection.createEmptyRange(),t.cmd.do("underline"),n&&(t.selection.collapseRange(),t.selection.restoreSelection())},tryChangeActive:function(e){var t=this.editor,n=this.$elem;t.cmd.queryCommandState("underline")?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}},b.prototype={constructor:b,onClick:function(e){this.editor.cmd.do("undo")}},B.prototype={constructor:B,_command:function(e){var t=this.editor,n=t.$textElem;if(t.selection.restoreSelection(),!t.cmd.queryCommandState(e)){t.cmd.do(e);var i=t.selection.getSelectionContainerElem();if("LI"===i.getNodeName()&&(i=i.parent()),!1!==/^ol|ul$/i.test(i.getNodeName())&&!i.equal(n)){var o=i.parent();o.equal(n)||(i.insertAfter(o),o.remove())}}},tryChangeActive:function(e){var t=this.editor,n=this.$elem;t.cmd.queryCommandState("insertUnOrderedList")||t.cmd.queryCommandState("insertOrderedList")?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}},y.prototype={constructor:y,_command:function(e){this.editor.cmd.do(e)}},C.prototype={constructor:C,_command:function(e){this.editor.cmd.do("foreColor",e)}},x.prototype={constructor:x,_command:function(e){this.editor.cmd.do("backColor",e)}},I.prototype={constructor:I,onClick:function(e){var t=this.editor,n=t.selection.getSelectionContainerElem(),i=n.getNodeName();if(!J.isIE())return void("BLOCKQUOTE"===i?t.cmd.do("formatBlock","

      "):t.cmd.do("formatBlock","

      "));var A=void 0,r=void 0;if("P"===i)return A=n.text(),r=o("
      "+A+"
      "),r.insertAfter(n),void n.remove();"BLOCKQUOTE"===i&&(A=n.text(),r=o("

      "+A+"

      "),r.insertAfter(n),n.remove())},tryChangeActive:function(e){var t=this.editor,n=this.$elem,i=/^BLOCKQUOTE$/i,o=t.cmd.queryCommandValue("formatBlock");i.test(o)?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}},Q.prototype={constructor:Q,onClick:function(e){var t=this.editor,n=t.selection.getSelectionStartElem(),i=t.selection.getSelectionEndElem(),A=t.selection.isSelectionEmpty(),r=t.selection.getSelectionText(),c=void 0;return n.equal(i)?A?void(this._active?this._createPanel(n.html()):this._createPanel()):(c=o(""+r+""),t.cmd.do("insertElem",c),t.selection.createRangeByElem(c,!1),void t.selection.restoreSelection()):void t.selection.restoreSelection()},_createPanel:function(e){var t=this;e=e||"";var n=e?"edit":"new",i=c("texxt"),A=c("btn"),r=new f(this,{width:500,tabs:[{title:"插入代码",tpl:'
      \n \n
      \n \n
      \n
      ',events:[{selector:"#"+A,type:"click",fn:function(){var e=o("#"+i),A=e.val()||e.html();return A=a(A),"new"===n?t._insertCode(A):t._updateCode(A),!0}}]}]});r.show(),this.panel=r},_insertCode:function(e){this.editor.cmd.do("insertHTML","
      "+e+"


      ")},_updateCode:function(e){var t=this.editor,n=t.selection.getSelectionContainerElem();n&&(n.html(e),t.selection.restoreSelection())},tryChangeActive:function(e){var t=this.editor,n=this.$elem,i=t.selection.getSelectionContainerElem();if(i){var o=i.parent();"CODE"===i.getNodeName()&&"PRE"===o.getNodeName()?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}}},M.prototype={constructor:M,onClick:function(){this._createPanel()},_createPanel:function(){var e=this,t=this.editor,n=t.config,i=n.emotions||[],A=[];i.forEach(function(t){var n=t.type,i=t.content||[],r="";"emoji"===n&&i.forEach(function(e){e&&(r+=''+e+"")}),"image"===n&&i.forEach(function(e){var t=e.src,n=e.alt;t&&(r+=''+n+'')}),A.push({title:t.title,tpl:'
      '+r+"
      ",events:[{selector:"span.w-e-item",type:"click",fn:function(t){var n=t.target,i=o(n),A=i.getNodeName(),r=void 0;return r="IMG"===A?i.parent().html():""+i.html()+"",e._insert(r),!0}}]})});var r=new f(this,{width:300,height:200,tabs:A});r.show(),this.panel=r},_insert:function(e){this.editor.cmd.do("insertHTML",e)}},S.prototype={constructor:S,onClick:function(){this._active?this._createEditPanel():this._createInsertPanel()},_createInsertPanel:function(){var e=this,t=c("btn"),n=c("row"),i=c("col"),A=new f(this,{width:250,tabs:[{title:"插入表格",tpl:'
      \n

      \n 创建\n \n 行\n \n 列的表格\n

      \n
      \n \n
      \n
      ',events:[{selector:"#"+t,type:"click",fn:function(){var t=parseInt(o("#"+n).val()),A=parseInt(o("#"+i).val());return t&&A&&t>0&&A>0&&e._insert(t,A),!0}}]}]});A.show(),this.panel=A},_insert:function(e,t){var n=void 0,i=void 0,o='
      ';for(n=0;n",0===n)for(i=0;i ";else for(i=0;i ";o+=""}o+="


      ";var A=this.editor;A.cmd.do("insertHTML",o),A.cmd.do("enableObjectResizing",!1),A.cmd.do("enableInlineTableEditing",!1)},_createEditPanel:function(){var e=this,t=c("add-row"),n=c("add-col"),i=c("del-row"),o=c("del-col"),A=c("del-table");new f(this,{width:320,tabs:[{title:"编辑表格",tpl:'
      \n
      \n \n \n \n \n
      \n
      \n \n \n
      ',events:[{selector:"#"+t,type:"click",fn:function(){return e._addRow(),!0}},{selector:"#"+n,type:"click",fn:function(){return e._addCol(),!0}},{selector:"#"+i,type:"click",fn:function(){return e._delRow(),!0}},{selector:"#"+o,type:"click",fn:function(){return e._delCol(),!0}},{selector:"#"+A,type:"click",fn:function(){return e._delTable(),!0}}]}]}).show()},_getLocationData:function(){var e={},t=this.editor,n=t.selection.getSelectionContainerElem();if(n){var i=n.getNodeName();if("TD"===i||"TH"===i){var o=n.parent(),A=o.children(),r=A.length;A.forEach(function(t,i){if(t===n[0])return e.td={index:i,elem:t,length:r},!1});var c=o.parent(),a=c.children(),s=a.length;return a.forEach(function(t,n){if(t===o[0])return e.tr={index:n,elem:t,length:s},!1}),e}}},_addRow:function(){var e=this._getLocationData();if(e){var t=e.tr,n=o(t.elem),i=e.td,A=i.length,r=document.createElement("tr"),c="",a=void 0;for(a=0;a ";r.innerHTML=c,o(r).insertAfter(n)}},_addCol:function(){var e=this._getLocationData();if(e){var t=e.tr,n=e.td,i=n.index;o(t.elem).parent().children().forEach(function(e){var t=o(e),n=t.children(),A=n.get(i),r=A.getNodeName().toLowerCase();o(document.createElement(r)).insertAfter(A)})}},_delRow:function(){var e=this._getLocationData();if(e){o(e.tr.elem).remove()}},_delCol:function(){var e=this._getLocationData();if(e){var t=e.tr,n=e.td,i=n.index;o(t.elem).parent().children().forEach(function(e){o(e).children().get(i).remove()})}},_delTable:function(){var e=this.editor,t=e.selection.getSelectionContainerElem();if(t){var n=t.parentUntil("table");n&&n.remove()}},tryChangeActive:function(e){var t=this.editor,n=this.$elem,i=t.selection.getSelectionContainerElem();if(i){var o=i.getNodeName();"TD"===o||"TH"===o?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}}},k.prototype={constructor:k,onClick:function(){this._createPanel()},_createPanel:function(){var e=this,t=c("text-val"),n=c("btn"),i=new f(this,{width:350,tabs:[{title:"插入视频",tpl:'
      \n \n
      \n \n
      \n
      ',events:[{selector:"#"+n,type:"click",fn:function(){var n=o("#"+t),i=n.val().trim();return i&&e._insert(i),!0}}]}]});i.show(),this.panel=i},_insert:function(e){this.editor.cmd.do("insertHTML",e+"


      ")}},D.prototype={constructor:D,onClick:function(){this.editor.config.qiniu||(this._active?this._createEditPanel():this._createInsertPanel())},_createEditPanel:function(){var e=this.editor,t=c("width-30"),n=c("width-50"),i=c("width-100"),o=c("del-btn"),A=[{title:"编辑图片",tpl:'
      \n
      \n 最大宽度:\n \n \n \n
      \n
      \n \n \n
      ',events:[{selector:"#"+t,type:"click",fn:function(){var t=e._selectedImg;return t&&t.css("max-width","30%"),!0}},{selector:"#"+n,type:"click",fn:function(){var t=e._selectedImg;return t&&t.css("max-width","50%"),!0}},{selector:"#"+i,type:"click",fn:function(){var t=e._selectedImg;return t&&t.css("max-width","100%"),!0}},{selector:"#"+o,type:"click",fn:function(){var t=e._selectedImg;return t&&t.remove(),!0}}]}],r=new f(this,{width:300,tabs:A});r.show(),this.panel=r},_createInsertPanel:function(){var e=this.editor,t=e.uploadImg,n=e.config,i=c("up-trigger"),A=c("up-file"),r=c("link-url"),a=c("link-btn"),s=[{title:"上传图片",tpl:'
      \n
      \n \n
      \n
      \n \n
      \n
      ',events:[{selector:"#"+i,type:"click",fn:function(){var e=o("#"+A),t=e[0];if(!t)return!0;t.click()}},{selector:"#"+A,type:"change",fn:function(){var e=o("#"+A),n=e[0];if(!n)return!0;var i=n.files;return i.length&&t.uploadImg(i),!0}}]},{title:"网络图片", tpl:'
      \n \n
      \n \n
      \n
      ',events:[{selector:"#"+a,type:"click",fn:function(){var e=o("#"+r),n=e.val().trim();return n&&t.insertLinkImg(n),!0}}]}],l=[];(n.uploadImgShowBase64||n.uploadImgServer||n.customUploadImg)&&window.FileReader&&l.push(s[0]),n.showLinkImg&&l.push(s[1]);var d=new f(this,{width:300,tabs:l});d.show(),this.panel=d},tryChangeActive:function(e){var t=this.editor,n=this.$elem;t._selectedImg?(this._active=!0,n.addClass("w-e-active")):(this._active=!1,n.removeClass("w-e-active"))}};var q={};q.bold=l,q.head=u,q.fontSize=h,q.fontName=p,q.link=m,q.italic=g,q.redo=w,q.strikeThrough=v,q.underline=E,q.undo=b,q.list=B,q.justify=y,q.foreColor=C,q.backColor=x,q.quote=I,q.code=Q,q.emoticon=M,q.table=S,q.video=k,q.image=D,_.prototype={constructor:_,init:function(){var e=this,t=this.editor;((t.config||{}).menus||[]).forEach(function(n){var i=q[n];i&&"function"==typeof i&&(e.menus[n]=new i(t))}),this._addToToolbar(),this._bindEvent()},_addToToolbar:function(){var e=this.editor,t=e.$toolbarElem,n=this.menus,i=e.config,o=i.zIndex+1;A(n,function(e,n){var i=n.$elem;i&&(i.css("z-index",o),t.append(i))})},_bindEvent:function(){var e=this.menus,t=this.editor;A(e,function(e,n){var i=n.type;if(i){var o=n.$elem,A=n.droplist;n.panel;"click"===i&&n.onClick&&o.on("click",function(e){null!=t.selection.getRange()&&n.onClick(e)}),"droplist"===i&&A&&o.on("mouseenter",function(e){null!=t.selection.getRange()&&(A.showTimeoutId=setTimeout(function(){A.show()},200))}).on("mouseleave",function(e){A.hideTimeoutId=setTimeout(function(){A.hide()},0)}),"panel"===i&&n.onClick&&o.on("click",function(e){e.stopPropagation(),null!=t.selection.getRange()&&n.onClick(e)})}})},changeActive:function(){A(this.menus,function(e,t){t.tryChangeActive&&setTimeout(function(){t.tryChangeActive()},100)})}},U.prototype={constructor:U,init:function(){this._bindEvent()},clear:function(){this.html("


      ")},html:function(e){var t=this.editor,n=t.$textElem,i=void 0;if(null==e)return i=n.html(),i=i.replace(/\u200b/gm,""),i;n.html(e),t.initSelection()},getJSON:function(){return R(this.editor.$textElem)},text:function(e){var t=this.editor,n=t.$textElem,i=void 0;if(null==e)return i=n.text(),i=i.replace(/\u200b/gm,""),i;n.text("

      "+e+"

      "),t.initSelection()},append:function(e){var t=this.editor;t.$textElem.append(o(e)),t.initSelection()},_bindEvent:function(){this._saveRangeRealTime(),this._enterKeyHandle(),this._clearHandle(),this._pasteHandle(),this._tabHandle(),this._imgHandle(),this._dragHandle()},_saveRangeRealTime:function(){function e(e){t.selection.saveRange(),t.menus.changeActive()}var t=this.editor,n=t.$textElem;n.on("keyup",e),n.on("mousedown",function(t){n.on("mouseleave",e)}),n.on("mouseup",function(t){e(),n.off("mouseleave",e)})},_enterKeyHandle:function(){function e(e){var t=o("


      ");t.insertBefore(e),i.selection.createRangeByElem(t,!0),i.selection.restoreSelection(),e.remove()}function t(t){var n=i.selection.getSelectionContainerElem(),o=n.parent();if("
      "===o.html())return void e(n);if(o.equal(A)){"P"!==n.getNodeName()&&(n.text()||e(n))}}function n(e){var t=i.selection.getSelectionContainerElem();if(t){var n=t.parent(),A=t.getNodeName(),r=n.getNodeName();if("CODE"===A&&"PRE"===r&&i.cmd.queryCommandSupported("insertHTML")){if(!0===i._willBreakCode){var c=o("


      ");return c.insertAfter(n),i.selection.createRangeByElem(c,!0),i.selection.restoreSelection(),i._willBreakCode=!1,void e.preventDefault()}var a=i.selection.getRange().startOffset;i.cmd.do("insertHTML","\n"),i.selection.saveRange(),i.selection.getRange().startOffset===a&&i.cmd.do("insertHTML","\n");var s=t.html().length;i.selection.getRange().startOffset+1===s&&(i._willBreakCode=!0),e.preventDefault()}}}var i=this.editor,A=i.$textElem;A.on("keyup",function(e){13===e.keyCode&&t(e)}),A.on("keydown",function(e){if(13!==e.keyCode)return void(i._willBreakCode=!1);n(e)})},_clearHandle:function(){var e=this.editor,t=e.$textElem;t.on("keydown",function(e){if(8===e.keyCode){return"


      "===t.html().toLowerCase().trim()?void e.preventDefault():void 0}}),t.on("keyup",function(n){if(8===n.keyCode){var i=void 0,A=t.html().toLowerCase().trim();A&&"
      "!==A||(i=o("


      "),t.html(""),t.append(i),e.selection.createRangeByElem(i,!1,!0),e.selection.restoreSelection())}})},_pasteHandle:function(){function e(){var e=Date.now(),t=!1;return e-a>=100&&(t=!0),a=e,t}function t(){a=0}var n=this.editor,i=n.config,o=i.pasteFilterStyle,A=i.pasteTextHandle,r=i.pasteIgnoreImg,c=n.$textElem,a=0;c.on("paste",function(i){if(!J.isIE()&&(i.preventDefault(),e())){var c=F(i,o,r),a=N(i);a=a.replace(/\n/gm,"
      ");var l=n.selection.getSelectionContainerElem();if(l){var d=l.getNodeName();if("CODE"===d||"PRE"===d)return A&&s(A)&&(a=""+(A(a)||"")),void n.cmd.do("insertHTML","

      "+a+"

      ");if(!c)return void t();try{A&&s(A)&&(c=""+(A(c)||"")),n.cmd.do("insertHTML",c)}catch(e){A&&s(A)&&(a=""+(A(a)||"")),n.cmd.do("insertHTML","

      "+a+"

      ")}}}}),c.on("paste",function(t){if(!J.isIE()&&(t.preventDefault(),e())){var i=T(t);if(i&&i.length){var o=n.selection.getSelectionContainerElem();if(o){var A=o.getNodeName();if("CODE"!==A&&"PRE"!==A){n.uploadImg.uploadImg(i)}}}}})},_tabHandle:function(){var e=this.editor;e.$textElem.on("keydown",function(t){if(9===t.keyCode&&e.cmd.queryCommandSupported("insertHTML")){var n=e.selection.getSelectionContainerElem();if(n){var i=n.parent(),o=n.getNodeName(),A=i.getNodeName();"CODE"===o&&"PRE"===A?e.cmd.do("insertHTML"," "):e.cmd.do("insertHTML","    "),t.preventDefault()}}})},_imgHandle:function(){var e=this.editor,t=e.$textElem;t.on("click","img",function(t){var n=this,i=o(n);"1"!==i.attr("data-w-e")&&(e._selectedImg=i,e.selection.createRangeByElem(i),e.selection.restoreSelection())}),t.on("click keyup",function(t){t.target.matches("img")||(e._selectedImg=null)})},_dragHandle:function(){var e=this.editor;o(document).on("dragleave drop dragenter dragover",function(e){e.preventDefault()}),e.$textElem.on("drop",function(t){t.preventDefault();var n=t.dataTransfer&&t.dataTransfer.files;n&&n.length&&e.uploadImg.uploadImg(n)})}},Y.prototype={constructor:Y,do:function(e,t){var n=this.editor;if(n._useStyleWithCSS||(document.execCommand("styleWithCSS",null,!0),n._useStyleWithCSS=!0),n.selection.getRange()){n.selection.restoreSelection();var i="_"+e;this[i]?this[i](t):this._execCommand(e,t),n.menus.changeActive(),n.selection.saveRange(),n.selection.restoreSelection(),n.change&&n.change()}},_insertHTML:function(e){var t=this.editor,n=t.selection.getRange();this.queryCommandSupported("insertHTML")?this._execCommand("insertHTML",e):n.insertNode?(n.deleteContents(),n.insertNode(o(e)[0])):n.pasteHTML&&n.pasteHTML(e)},_insertElem:function(e){var t=this.editor,n=t.selection.getRange();n.insertNode&&(n.deleteContents(),n.insertNode(e[0]))},_execCommand:function(e,t){document.execCommand(e,!1,t)},queryCommandValue:function(e){return document.queryCommandValue(e)},queryCommandState:function(e){return document.queryCommandState(e)},queryCommandSupported:function(e){return document.queryCommandSupported(e)}},P.prototype={constructor:P,getRange:function(){return this._currentRange},saveRange:function(e){if(e)return void(this._currentRange=e);var t=window.getSelection();if(0!==t.rangeCount){var n=t.getRangeAt(0),i=this.getSelectionContainerElem(n);if(i&&"false"!==i.attr("contenteditable")&&!i.parentUntil("[contenteditable=false]")){this.editor.$textElem.isContain(i)&&(this._currentRange=n)}}},collapseRange:function(e){null==e&&(e=!1);var t=this._currentRange;t&&t.collapse(e)},getSelectionText:function(){return this._currentRange?this._currentRange.toString():""},getSelectionContainerElem:function(e){e=e||this._currentRange;var t=void 0;if(e)return t=e.commonAncestorContainer,o(1===t.nodeType?t:t.parentNode)},getSelectionStartElem:function(e){e=e||this._currentRange;var t=void 0;if(e)return t=e.startContainer,o(1===t.nodeType?t:t.parentNode)},getSelectionEndElem:function(e){e=e||this._currentRange;var t=void 0;if(e)return t=e.endContainer,o(1===t.nodeType?t:t.parentNode)},isSelectionEmpty:function(){var e=this._currentRange;return!(!e||!e.startContainer||e.startContainer!==e.endContainer||e.startOffset!==e.endOffset)},restoreSelection:function(){var e=window.getSelection();e.removeAllRanges(),e.addRange(this._currentRange)},createEmptyRange:function(){var e=this.editor,t=this.getRange(),n=void 0;if(t&&this.isSelectionEmpty())try{J.isWebkit()?(e.cmd.do("insertHTML","​"),t.setEnd(t.endContainer,t.endOffset+1),this.saveRange(t)):(n=o(""),e.cmd.do("insertElem",n),this.createRangeByElem(n,!0))}catch(e){}},createRangeByElem:function(e,t,n){if(e.length){var i=e[0],o=document.createRange();n?o.selectNodeContents(i):o.selectNode(i),"boolean"==typeof t&&o.collapse(t),this.saveRange(o)}}},H.prototype={constructor:H,show:function(e){var t=this;if(!this._isShow){this._isShow=!0;var n=this.$bar;if(this._isRender)this._isRender=!0;else{this.$textContainer.append(n)}Date.now()-this._time>100&&e<=1&&(n.css("width",100*e+"%"),this._time=Date.now());var i=this._timeoutId;i&&clearTimeout(i),i=setTimeout(function(){t._hide()},500)}},_hide:function(){this.$bar.remove(),this._time=0,this._isShow=!1,this._isRender=!1}};var X="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};L.prototype={constructor:L,_alert:function(e,t){var n=this.editor,i=n.config.debug,o=n.config.customAlert;if(i)throw new Error("wangEditor: "+(t||e));o&&"function"==typeof o?o(e):alert(e)},insertLinkImg:function(e){var t=this;if(e){var n=this.editor,i=n.config,o=i.linkImgCheck,A=void 0;if(o&&"function"==typeof o&&"string"==typeof(A=o(e)))return void alert(A);n.cmd.do("insertHTML",'');var r=document.createElement("img");r.onload=function(){var t=i.linkImgCallback;t&&"function"==typeof t&&t(e),r=null},r.onerror=function(){r=null,t._alert("插入图片错误",'wangEditor: 插入图片出错,图片链接是 "'+e+'",下载该链接失败')},r.onabort=function(){r=null},r.src=e}},uploadImg:function(e){var t=this;if(e&&e.length){var n=this.editor,i=n.config,o=i.uploadImgServer,c=i.uploadImgShowBase64,a=i.uploadImgMaxSize,s=a/1024/1024,l=i.uploadImgMaxLength||1e4,d=i.uploadFileName||"",u=i.uploadImgParams||{},h=i.uploadImgParamsWithUrl,p=i.uploadImgHeaders||{},f=i.uploadImgHooks||{},m=i.uploadImgTimeout||3e3,g=i.withCredentials;null==g&&(g=!1);var w=i.customUploadImg;if(w||o||c){var v=[],E=[];if(r(e,function(e){var t=e.name,n=e.size;if(t&&n)return!1===/\.(jpg|jpeg|png|bmp|gif|webp)$/i.test(t)?void E.push("【"+t+"】不是图片"):al)return void this._alert("一次最多上传"+l+"张图片");if(w&&"function"==typeof w)return void w(v,this.insertLinkImg.bind(this));var b=new FormData;if(r(v,function(e){var t=d||e.name;b.append(t,e)}),o&&"string"==typeof o){var B=o.split("#");o=B[0];var y=B[1]||"";A(u,function(e,t){h&&(o.indexOf("?")>0?o+="&":o+="?",o=o+e+"="+t),b.append(e,t)}),y&&(o+="#"+y);var C=new XMLHttpRequest;if(C.open("POST",o),C.timeout=m,C.ontimeout=function(){f.timeout&&"function"==typeof f.timeout&&f.timeout(C,n),t._alert("上传图片超时")},C.upload&&(C.upload.onprogress=function(e){var t=void 0,i=new H(n);e.lengthComputable&&(t=e.loaded/e.total,i.show(t))}),C.onreadystatechange=function(){var e=void 0;if(4===C.readyState){if(C.status<200||C.status>=300)return f.error&&"function"==typeof f.error&&f.error(C,n),void t._alert("上传图片发生错误","上传图片发生错误,服务器返回状态是 "+C.status);if(e=C.responseText,"object"!==(void 0===e?"undefined":X(e)))try{e=JSON.parse(e)}catch(i){return f.fail&&"function"==typeof f.fail&&f.fail(C,n,e),void t._alert("上传图片失败","上传图片返回结果错误,返回结果是: "+e)}if(f.customInsert||"0"==e.errno){if(f.customInsert&&"function"==typeof f.customInsert)f.customInsert(t.insertLinkImg.bind(t),e,n);else{(e.data||[]).forEach(function(e){t.insertLinkImg(e)})}f.success&&"function"==typeof f.success&&f.success(C,n,e)}else f.fail&&"function"==typeof f.fail&&f.fail(C,n,e),t._alert("上传图片失败","上传图片返回结果错误,返回结果 errno="+e.errno)}},f.before&&"function"==typeof f.before){var x=f.before(C,n,v);if(x&&"object"===(void 0===x?"undefined":X(x))&&x.prevent)return void this._alert(x.msg)}return A(p,function(e,t){C.setRequestHeader(e,t)}),C.withCredentials=g,void C.send(b)}c&&r(e,function(e){var n=t,i=new FileReader;i.readAsDataURL(e),i.onload=function(){n.insertLinkImg(this.result)}})}}}};var W=1;j.prototype={constructor:j,_initConfig:function(){var e={};this.config=Object.assign(e,z,this.customConfig);var t=this.config.lang||{},n=[];A(t,function(e,t){n.push({reg:new RegExp(e,"img"),val:t})}),this.config.langArgs=n},_initDom:function(){var e=this,t=this.toolbarSelector,n=o(t),i=this.textSelector,A=this.config,r=A.zIndex,a=void 0,s=void 0,l=void 0,d=void 0;null==i?(a=o("
      "),s=o("
      "),d=n.children(),n.append(a).append(s),a.css("background-color","#f1f1f1").css("border","1px solid #ccc"),s.css("border","1px solid #ccc").css("border-top","none").css("height","300px")):(a=n,s=o(i),d=s.children()),l=o("
      "),l.attr("contenteditable","true").css("width","100%").css("height","100%"),d&&d.length?l.append(d):l.append(o("


      ")),s.append(l),a.addClass("w-e-toolbar"),s.addClass("w-e-text-container"),s.css("z-index",r),l.addClass("w-e-text");var u=c("toolbar-elem");a.attr("id",u);var h=c("text-elem");l.attr("id",h),this.$toolbarElem=a,this.$textContainerElem=s,this.$textElem=l,this.toolbarElemId=u,this.textElemId=h;var p=!0;s.on("compositionstart",function(){p=!1}),s.on("compositionend",function(){p=!0}),s.on("click keyup",function(){p&&e.change&&e.change()}),a.on("click",function(){this.change&&this.change()}),(A.onfocus||A.onblur)&&(this.isFocus=!1,o(document).on("click",function(t){var n=l.isContain(o(t.target)),i=a.isContain(o(t.target)),A=a[0]==t.target;if(n)e.isFocus||e.onfocus&&e.onfocus(),e.isFocus=!0;else{if(i&&!A)return;e.isFocus&&e.onblur&&e.onblur(),e.isFocus=!1}}))},_initCommand:function(){this.cmd=new Y(this)},_initSelectionAPI:function(){this.selection=new P(this)},_initUploadImg:function(){this.uploadImg=new L(this)},_initMenus:function(){this.menus=new _(this),this.menus.init()},_initText:function(){this.txt=new U(this),this.txt.init()},initSelection:function(e){var t=this.$textElem,n=t.children();if(!n.length)return t.append(o("


      ")),void this.initSelection();var i=n.last();if(e){var A=i.html().toLowerCase(),r=i.getNodeName();if("
      "!==A&&"
      "!==A||"P"!==r)return t.append(o("


      ")),void this.initSelection()}this.selection.createRangeByElem(i,!1,!0),this.selection.restoreSelection()},_bindEvent:function(){var e=0,t=this.txt.html(),n=this.config,i=n.onchangeTimeout;(!(i=parseInt(i,10))||i<=0)&&(i=200);var o=n.onchange;o&&"function"==typeof o&&(this.change=function(){var n=this.txt.html();n.length===t.length&&n===t||(e&&clearTimeout(e),e=setTimeout(function(){o(n),t=n},i))});var A=n.onblur;A&&"function"==typeof A&&(this.onblur=function(){var e=this.txt.html();A(e)});var r=n.onfocus;r&&"function"==typeof r&&(this.onfocus=function(){r()})},create:function(){this._initConfig(),this._initDom(),this._initCommand(),this._initSelectionAPI(),this._initText(),this._initMenus(),this._initUploadImg(),this.initSelection(!0),this._bindEvent()},_offAllEvent:function(){o.offAll()}};try{document}catch(e){throw new Error("请在浏览器环境下运行")}!function(){"function"!=typeof Object.assign&&(Object.assign=function(e,t){if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(e),i=1;i=0&&t.item(n)!==this;);return n>-1})}();var Z=document.createElement("style");return Z.type="text/css", Z.innerHTML='.w-e-toolbar,.w-e-text-container,.w-e-menu-panel { padding: 0; margin: 0; box-sizing: border-box;}.w-e-toolbar *,.w-e-text-container *,.w-e-menu-panel * { padding: 0; margin: 0; box-sizing: border-box;}.w-e-clear-fix:after { content: ""; display: table; clear: both;}.w-e-toolbar .w-e-droplist { position: absolute; left: 0; top: 0; background-color: #fff; border: 1px solid #f1f1f1; border-right-color: #ccc; border-bottom-color: #ccc;}.w-e-toolbar .w-e-droplist .w-e-dp-title { text-align: center; color: #999; line-height: 2; border-bottom: 1px solid #f1f1f1; font-size: 13px;}.w-e-toolbar .w-e-droplist ul.w-e-list { list-style: none; line-height: 1;}.w-e-toolbar .w-e-droplist ul.w-e-list li.w-e-item { color: #333; padding: 5px 0;}.w-e-toolbar .w-e-droplist ul.w-e-list li.w-e-item:hover { background-color: #f1f1f1;}.w-e-toolbar .w-e-droplist ul.w-e-block { list-style: none; text-align: left; padding: 5px;}.w-e-toolbar .w-e-droplist ul.w-e-block li.w-e-item { display: inline-block; *display: inline; *zoom: 1; padding: 3px 5px;}.w-e-toolbar .w-e-droplist ul.w-e-block li.w-e-item:hover { background-color: #f1f1f1;}@font-face { font-family: \'w-e-icon\'; src: url(data:application/x-font-woff;charset=utf-8;base64,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) format(\'truetype\'); font-weight: normal; font-style: normal;}[class^="w-e-icon-"],[class*=" w-e-icon-"] { /* use !important to prevent issues with browser extensions that change fonts */ font-family: \'w-e-icon\' !important; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}.w-e-icon-close:before { content: "\\f00d";}.w-e-icon-upload2:before { content: "\\e9c6";}.w-e-icon-trash-o:before { content: "\\f014";}.w-e-icon-header:before { content: "\\f1dc";}.w-e-icon-pencil2:before { content: "\\e906";}.w-e-icon-paint-brush:before { content: "\\f1fc";}.w-e-icon-image:before { content: "\\e90d";}.w-e-icon-play:before { content: "\\e912";}.w-e-icon-location:before { content: "\\e947";}.w-e-icon-undo:before { content: "\\e965";}.w-e-icon-redo:before { content: "\\e966";}.w-e-icon-quotes-left:before { content: "\\e977";}.w-e-icon-list-numbered:before { content: "\\e9b9";}.w-e-icon-list2:before { content: "\\e9bb";}.w-e-icon-link:before { content: "\\e9cb";}.w-e-icon-happy:before { content: "\\e9df";}.w-e-icon-bold:before { content: "\\ea62";}.w-e-icon-underline:before { content: "\\ea63";}.w-e-icon-italic:before { content: "\\ea64";}.w-e-icon-strikethrough:before { content: "\\ea65";}.w-e-icon-table2:before { content: "\\ea71";}.w-e-icon-paragraph-left:before { content: "\\ea77";}.w-e-icon-paragraph-center:before { content: "\\ea78";}.w-e-icon-paragraph-right:before { content: "\\ea79";}.w-e-icon-terminal:before { content: "\\f120";}.w-e-icon-page-break:before { content: "\\ea68";}.w-e-icon-cancel-circle:before { content: "\\ea0d";}.w-e-icon-font:before { content: "\\ea5c";}.w-e-icon-text-heigh:before { content: "\\ea5f";}.w-e-toolbar { display: -webkit-box; display: -ms-flexbox; display: flex; padding: 0 5px; /* flex-wrap: wrap; */ /* 单个菜单 */}.w-e-toolbar .w-e-menu { position: relative; text-align: center; padding: 5px 10px; cursor: pointer;}.w-e-toolbar .w-e-menu i { color: #999;}.w-e-toolbar .w-e-menu:hover i { color: #333;}.w-e-toolbar .w-e-active i { color: #1e88e5;}.w-e-toolbar .w-e-active:hover i { color: #1e88e5;}.w-e-text-container .w-e-panel-container { position: absolute; top: 0; left: 50%; border: 1px solid #ccc; border-top: 0; box-shadow: 1px 1px 2px #ccc; color: #333; background-color: #fff; /* 为 emotion panel 定制的样式 */ /* 上传图片的 panel 定制样式 */}.w-e-text-container .w-e-panel-container .w-e-panel-close { position: absolute; right: 0; top: 0; padding: 5px; margin: 2px 5px 0 0; cursor: pointer; color: #999;}.w-e-text-container .w-e-panel-container .w-e-panel-close:hover { color: #333;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-title { list-style: none; display: -webkit-box; display: -ms-flexbox; display: flex; font-size: 14px; margin: 2px 10px 0 10px; border-bottom: 1px solid #f1f1f1;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-title .w-e-item { padding: 3px 5px; color: #999; cursor: pointer; margin: 0 3px; position: relative; top: 1px;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-title .w-e-active { color: #333; border-bottom: 1px solid #333; cursor: default; font-weight: 700;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content { padding: 10px 15px 10px 15px; font-size: 16px; /* 输入框的样式 */ /* 按钮的样式 */}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content input:focus,.w-e-text-container .w-e-panel-container .w-e-panel-tab-content textarea:focus,.w-e-text-container .w-e-panel-container .w-e-panel-tab-content button:focus { outline: none;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content textarea { width: 100%; border: 1px solid #ccc; padding: 5px;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content textarea:focus { border-color: #1e88e5;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content input[type=text] { border: none; border-bottom: 1px solid #ccc; font-size: 14px; height: 20px; color: #333; text-align: left;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content input[type=text].small { width: 30px; text-align: center;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content input[type=text].block { display: block; width: 100%; margin: 10px 0;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content input[type=text]:focus { border-bottom: 2px solid #1e88e5;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content .w-e-button-container button { font-size: 14px; color: #1e88e5; border: none; padding: 5px 10px; background-color: #fff; cursor: pointer; border-radius: 3px;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content .w-e-button-container button.left { float: left; margin-right: 10px;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content .w-e-button-container button.right { float: right; margin-left: 10px;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content .w-e-button-container button.gray { color: #999;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content .w-e-button-container button.red { color: #c24f4a;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content .w-e-button-container button:hover { background-color: #f1f1f1;}.w-e-text-container .w-e-panel-container .w-e-panel-tab-content .w-e-button-container:after { content: ""; display: table; clear: both;}.w-e-text-container .w-e-panel-container .w-e-emoticon-container .w-e-item { cursor: pointer; font-size: 18px; padding: 0 3px; display: inline-block; *display: inline; *zoom: 1;}.w-e-text-container .w-e-panel-container .w-e-up-img-container { text-align: center;}.w-e-text-container .w-e-panel-container .w-e-up-img-container .w-e-up-btn { display: inline-block; *display: inline; *zoom: 1; color: #999; cursor: pointer; font-size: 60px; line-height: 1;}.w-e-text-container .w-e-panel-container .w-e-up-img-container .w-e-up-btn:hover { color: #333;}.w-e-text-container { position: relative;}.w-e-text-container .w-e-progress { position: absolute; background-color: #1e88e5; bottom: 0; left: 0; height: 1px;}.w-e-text { padding: 0 10px; overflow-y: scroll;}.w-e-text p,.w-e-text h1,.w-e-text h2,.w-e-text h3,.w-e-text h4,.w-e-text h5,.w-e-text table,.w-e-text pre { margin: 10px 0; line-height: 1.5;}.w-e-text ul,.w-e-text ol { margin: 10px 0 10px 20px;}.w-e-text blockquote { display: block; border-left: 8px solid #d0e5f2; padding: 5px 10px; margin: 10px 0; line-height: 1.4; font-size: 100%; background-color: #f1f1f1;}.w-e-text code { display: inline-block; *display: inline; *zoom: 1; background-color: #f1f1f1; border-radius: 3px; padding: 3px 5px; margin: 0 3px;}.w-e-text pre code { display: block;}.w-e-text table { border-top: 1px solid #ccc; border-left: 1px solid #ccc;}.w-e-text table td,.w-e-text table th { border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; padding: 3px 5px;}.w-e-text table th { border-bottom: 2px solid #ccc; text-align: center;}.w-e-text:focus { outline: none;}.w-e-text img { cursor: pointer;}.w-e-text img:hover { box-shadow: 0 0 5px #333;}',document.getElementsByTagName("HEAD").item(0).appendChild(Z),window.wangEditor||j}); -//# sourceMappingURL=wangEditor.min.js.map diff --git a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js.map b/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js.map deleted file mode 100644 index edff0d0a8..000000000 --- a/src/SSCMS.Web/wwwroot/sitefiles/assets/lib/wangEditor/wangEditor.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["wangEditor.js"],"names":["global","factory","exports","module","define","amd","wangEditor","this","createElemByHTML","html","div","document","createElement","innerHTML","children","isDOMList","selector","HTMLCollection","NodeList","querySelectorAll","result","DomElement","nodeType","selectorResult","Array","replace","trim","indexOf","length","i","$","objForEach","obj","fn","key","hasOwnProperty","call","arrForEach","fakeArr","item","getRandom","prefix","Math","random","toString","slice","replaceHtmlSymbol","isFunction","Bold","editor","$elem","type","_active","DropList","menu","opt","_this","$container","$title","titleHtml","replaceLang","addClass","append","list","onClick","_emptyFn","$list","forEach","elemHtml","value","$li","on","e","hideTimeoutId","setTimeout","hide","_rendered","_show","Head","droplist","width","_command","FontSize","FontName","config","fontNames","map","fontName","Panel","Link","Italic","Redo","StrikeThrough","Underline","Undo","List","Justify","ForeColor","colors","color","BackColor","Quote","Code","Emoticon","Table","Video","Image","imgMenuId","Menus","menus","getPasteText","clipboardData","originalEvent","pasteText","window","getData","getPasteHtml","filterStyle","ignoreImg","pasteHtml","docSplitHtml","split","getPasteImgs","items","test","push","getAsFile","getChildrenJSON","childNodes","curElem","elemResult","textContent","tag","nodeName","toLowerCase","attrData","attrList","attributes","attrListLength","attr","name","attrs","Text","Command","API","_currentRange","Progress","_time","_isShow","_isRender","_timeoutId","$textContainer","$textContainerElem","$bar","UploadImg","Editor","toolbarSelector","textSelector","Error","id","editorId","customConfig","eventList","prototype","constructor","elem","clone","deep","cloneList","cloneNode","get","index","first","last","types","addEventListener","target","matches","off","removeEventListener","val","getAttribute","setAttribute","className","arr","filter","join","removeClass","css","currentStyle","style","styleArr","resultArr","show","$children","child","appendChild","remove","parent","parentElement","removeChild","isContain","$child","contains","getSizeData","getBoundingClientRect","getNodeName","find","text","focus","parentUntil","_currentElem","results","equal","insertBefore","$referenceNode","referenceNode","parentNode","insertAfter","lastChild","nextSibling","offAll","emotions","title","content","alt","src","zIndex","debug","linkCheck","link","linkImgCheck","pasteFilterStyle","pasteIgnoreImg","pasteTextHandle","showLinkImg","linkImgCallback","url","uploadImgMaxSize","uploadImgShowBase64","uploadFileName","uploadImgParams","uploadImgHeaders","withCredentials","uploadImgTimeout","uploadImgHooks","before","xhr","files","success","fail","error","timeout","qiniu","UA","_ua","navigator","userAgent","isWebkit","isIE","isSeleEmpty","selection","isSelectionEmpty","createEmptyRange","cmd","do","collapseRange","restoreSelection","tryChangeActive","queryCommandState","str","langArgs","reg","clearTimeout","$menuELem","menuHeight","height","showTimeoutId","$selectionElem","getSelectionContainerElem","$textElem","cmdValue","queryCommandValue","emptyFn","_isCreatedPanelMenus","$body","$closeBtn","$tabTitleContainer","$tabContentContainer","tabs","tabTitleArr","tabContentArr","tab","tabIndex","tpl","$content","_index","stopPropagation","events","event","$inputs","_hideOtherPanels","panel","$linkelem","createRangeByElem","_createPanel","getSelectionText","inputLinkId","inputTextId","btnOkId","btnDelId","delBtnDisplay","$link","$text","_insertLink","_delLink","selectionText","checkResult","alert","$selectionELem","$parent","$targetELem","$startElem","getSelectionStartElem","$endElem","getSelectionEndElem","$code","textId","btnId","_insertCode","_updateCode","$parentElem","tabConfig","emotData","emotType","faceHtml","$target","insertHtml","_insert","emotHtml","_createEditPanel","_createInsertPanel","btnInsertId","textRowNum","textColNum","rowNum","parseInt","colNum","r","c","_this2","addRowBtnId","addColBtnId","delRowBtnId","delColBtnId","delTableBtnId","_addRow","_addCol","_delRow","_delCol","_delTable","_getLocationData","$tr","$tds","tdLength","td","$tbody","$trs","trLength","tr","locationData","trData","$currentTr","tdData","newTr","tdIndex","$currentTd","$table","textValId","width30","width50","width100","delBtn","tabsConfig","$img","_selectedImg","uploadImg","upTriggerId","upFileId","linkUrlId","linkBtnId","$file","fileElem","click","fileList","$linkUrl","insertLinkImg","tabsConfigResult","uploadImgServer","customUploadImg","FileReader","MenuConstructors","bold","head","fontSize","italic","redo","strikeThrough","underline","undo","justify","foreColor","backColor","quote","code","emoticon","table","video","image","init","menuKey","MenuConstructor","_addToToolbar","_bindEvent","$toolbarElem","getRange","changeActive","clear","initSelection","getJSON","_saveRangeRealTime","_enterKeyHandle","_clearHandle","_pasteHandle","_tabHandle","_imgHandle","_dragHandle","saveRange","insertEmptyP","$p","pHandle","codeHandle","selectionNodeName","parentNodeName","queryCommandSupported","_willBreakCode","preventDefault","_startOffset","startOffset","codeLength","keyCode","txtHtml","canDo","now","Date","flag","pasteTime","resetTime","ex","pasteFiles","img","dataTransfer","_useStyleWithCSS","execCommand","_name","_execCommand","change","_insertHTML","range","insertNode","deleteContents","pasteHTML","_insertElem","_range","getSelection","rangeCount","getRangeAt","$containerElem","toStart","collapse","commonAncestorContainer","startContainer","endContainer","endOffset","removeAllRanges","addRange","setEnd","isContent","createRange","selectNodeContents","selectNode","progress","timeoutId","_hide","_typeof","Symbol","iterator","_alert","alertInfo","debugInfo","customAlert","onload","callback","onerror","onabort","_this3","maxSize","maxSizeM","maxLength","uploadImgMaxLength","uploadImgParamsWithUrl","hooks","resultFiles","errInfo","file","size","bind","formdata","FormData","uploadImgServerArr","uploadImgServerHash","XMLHttpRequest","open","ontimeout","upload","onprogress","percent","progressBar","lengthComputable","loaded","total","onreadystatechange","readyState","status","responseText","JSON","parse","customInsert","errno","data","beforeResult","prevent","msg","setRequestHeader","send","reader","readAsDataURL","_initConfig","Object","assign","langConfig","lang","RegExp","_initDom","$toolbarSelector","config$$1","toolbarElemId","textElemId","compositionEnd","onfocus","onblur","isFocus","isChild","isToolbar","isMenu","_initCommand","_initSelectionAPI","_initUploadImg","_initMenus","_initText","txt","newLine","$last","onChangeTimeoutId","beforeChangeHtml","onchangeTimeout","onchange","currentHtml","create","_offAllEvent","varArgs","TypeError","to","arguments","nextSource","nextKey","Element","matchesSelector","mozMatchesSelector","msMatchesSelector","oMatchesSelector","webkitMatchesSelector","s","ownerDocument","getElementsByTagName"],"mappings":"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","file":"wangEditor.min.js","sourcesContent":["(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.wangEditor = factory());\n}(this, (function () { 'use strict';\n\n/*\n poly-fill\n*/\n\nvar polyfill = function () {\n\n // Object.assign\n if (typeof Object.assign != 'function') {\n Object.assign = function (target, varArgs) {\n // .length of function is 2\n if (target == null) {\n // TypeError if undefined or null\n throw new TypeError('Cannot convert undefined or null to object');\n }\n\n var to = Object(target);\n\n for (var index = 1; index < arguments.length; index++) {\n var nextSource = arguments[index];\n\n if (nextSource != null) {\n // Skip over if undefined or null\n for (var nextKey in nextSource) {\n // Avoid bugs when hasOwnProperty is shadowed\n if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {\n to[nextKey] = nextSource[nextKey];\n }\n }\n }\n }\n return to;\n };\n }\n\n // IE 中兼容 Element.prototype.matches\n if (!Element.prototype.matches) {\n Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || function (s) {\n var matches = (this.document || this.ownerDocument).querySelectorAll(s),\n i = matches.length;\n while (--i >= 0 && matches.item(i) !== this) {}\n return i > -1;\n };\n }\n};\n\n/*\n DOM 操作 API\n*/\n\n// 根据 html 代码片段创建 dom 对象\nfunction createElemByHTML(html) {\n var div = void 0;\n div = document.createElement('div');\n div.innerHTML = html;\n return div.children;\n}\n\n// 是否是 DOM List\nfunction isDOMList(selector) {\n if (!selector) {\n return false;\n }\n if (selector instanceof HTMLCollection || selector instanceof NodeList) {\n return true;\n }\n return false;\n}\n\n// 封装 document.querySelectorAll\nfunction querySelectorAll(selector) {\n var result = document.querySelectorAll(selector);\n if (isDOMList(result)) {\n return result;\n } else {\n return [result];\n }\n}\n\n// 记录所有的事件绑定\nvar eventList = [];\n\n// 创建构造函数\nfunction DomElement(selector) {\n if (!selector) {\n return;\n }\n\n // selector 本来就是 DomElement 对象,直接返回\n if (selector instanceof DomElement) {\n return selector;\n }\n\n this.selector = selector;\n var nodeType = selector.nodeType;\n\n // 根据 selector 得出的结果(如 DOM,DOM List)\n var selectorResult = [];\n if (nodeType === 9) {\n // document 节点\n selectorResult = [selector];\n } else if (nodeType === 1) {\n // 单个 DOM 节点\n selectorResult = [selector];\n } else if (isDOMList(selector) || selector instanceof Array) {\n // DOM List 或者数组\n selectorResult = selector;\n } else if (typeof selector === 'string') {\n // 字符串\n selector = selector.replace('/\\n/mg', '').trim();\n if (selector.indexOf('<') === 0) {\n // 如
      \n selectorResult = createElemByHTML(selector);\n } else {\n // 如 #id .class\n selectorResult = querySelectorAll(selector);\n }\n }\n\n var length = selectorResult.length;\n if (!length) {\n // 空数组\n return this;\n }\n\n // 加入 DOM 节点\n var i = void 0;\n for (i = 0; i < length; i++) {\n this[i] = selectorResult[i];\n }\n this.length = length;\n}\n\n// 修改原型\nDomElement.prototype = {\n constructor: DomElement,\n\n // 类数组,forEach\n forEach: function forEach(fn) {\n var i = void 0;\n for (i = 0; i < this.length; i++) {\n var elem = this[i];\n var result = fn.call(elem, elem, i);\n if (result === false) {\n break;\n }\n }\n return this;\n },\n\n // clone\n clone: function clone(deep) {\n var cloneList = [];\n this.forEach(function (elem) {\n cloneList.push(elem.cloneNode(!!deep));\n });\n return $(cloneList);\n },\n\n // 获取第几个元素\n get: function get(index) {\n var length = this.length;\n if (index >= length) {\n index = index % length;\n }\n return $(this[index]);\n },\n\n // 第一个\n first: function first() {\n return this.get(0);\n },\n\n // 最后一个\n last: function last() {\n var length = this.length;\n return this.get(length - 1);\n },\n\n // 绑定事件\n on: function on(type, selector, fn) {\n // selector 不为空,证明绑定事件要加代理\n if (!fn) {\n fn = selector;\n selector = null;\n }\n\n // type 是否有多个\n var types = [];\n types = type.split(/\\s+/);\n\n return this.forEach(function (elem) {\n types.forEach(function (type) {\n if (!type) {\n return;\n }\n\n // 记录下,方便后面解绑\n eventList.push({\n elem: elem,\n type: type,\n fn: fn\n });\n\n if (!selector) {\n // 无代理\n elem.addEventListener(type, fn);\n return;\n }\n\n // 有代理\n elem.addEventListener(type, function (e) {\n var target = e.target;\n if (target.matches(selector)) {\n fn.call(target, e);\n }\n });\n });\n });\n },\n\n // 取消事件绑定\n off: function off(type, fn) {\n return this.forEach(function (elem) {\n elem.removeEventListener(type, fn);\n });\n },\n\n // 获取/设置 属性\n attr: function attr(key, val) {\n if (val == null) {\n // 获取值\n return this[0].getAttribute(key);\n } else {\n // 设置值\n return this.forEach(function (elem) {\n elem.setAttribute(key, val);\n });\n }\n },\n\n // 添加 class\n addClass: function addClass(className) {\n if (!className) {\n return this;\n }\n return this.forEach(function (elem) {\n var arr = void 0;\n if (elem.className) {\n // 解析当前 className 转换为数组\n arr = elem.className.split(/\\s/);\n arr = arr.filter(function (item) {\n return !!item.trim();\n });\n // 添加 class\n if (arr.indexOf(className) < 0) {\n arr.push(className);\n }\n // 修改 elem.class\n elem.className = arr.join(' ');\n } else {\n elem.className = className;\n }\n });\n },\n\n // 删除 class\n removeClass: function removeClass(className) {\n if (!className) {\n return this;\n }\n return this.forEach(function (elem) {\n var arr = void 0;\n if (elem.className) {\n // 解析当前 className 转换为数组\n arr = elem.className.split(/\\s/);\n arr = arr.filter(function (item) {\n item = item.trim();\n // 删除 class\n if (!item || item === className) {\n return false;\n }\n return true;\n });\n // 修改 elem.class\n elem.className = arr.join(' ');\n }\n });\n },\n\n // 修改 css\n css: function css(key, val) {\n var currentStyle = key + ':' + val + ';';\n return this.forEach(function (elem) {\n var style = (elem.getAttribute('style') || '').trim();\n var styleArr = void 0,\n resultArr = [];\n if (style) {\n // 将 style 按照 ; 拆分为数组\n styleArr = style.split(';');\n styleArr.forEach(function (item) {\n // 对每项样式,按照 : 拆分为 key 和 value\n var arr = item.split(':').map(function (i) {\n return i.trim();\n });\n if (arr.length === 2) {\n resultArr.push(arr[0] + ':' + arr[1]);\n }\n });\n // 替换或者新增\n resultArr = resultArr.map(function (item) {\n if (item.indexOf(key) === 0) {\n return currentStyle;\n } else {\n return item;\n }\n });\n if (resultArr.indexOf(currentStyle) < 0) {\n resultArr.push(currentStyle);\n }\n // 结果\n elem.setAttribute('style', resultArr.join('; '));\n } else {\n // style 无值\n elem.setAttribute('style', currentStyle);\n }\n });\n },\n\n // 显示\n show: function show() {\n return this.css('display', 'block');\n },\n\n // 隐藏\n hide: function hide() {\n return this.css('display', 'none');\n },\n\n // 获取子节点\n children: function children() {\n var elem = this[0];\n if (!elem) {\n return null;\n }\n\n return $(elem.children);\n },\n\n // 获取子节点(包括文本节点)\n childNodes: function childNodes() {\n var elem = this[0];\n if (!elem) {\n return null;\n }\n\n return $(elem.childNodes);\n },\n\n // 增加子节点\n append: function append($children) {\n return this.forEach(function (elem) {\n $children.forEach(function (child) {\n elem.appendChild(child);\n });\n });\n },\n\n // 移除当前节点\n remove: function remove() {\n return this.forEach(function (elem) {\n if (elem.remove) {\n elem.remove();\n } else {\n var parent = elem.parentElement;\n parent && parent.removeChild(elem);\n }\n });\n },\n\n // 是否包含某个子节点\n isContain: function isContain($child) {\n var elem = this[0];\n var child = $child[0];\n return elem.contains(child);\n },\n\n // 尺寸数据\n getSizeData: function getSizeData() {\n var elem = this[0];\n return elem.getBoundingClientRect(); // 可得到 bottom height left right top width 的数据\n },\n\n // 封装 nodeName\n getNodeName: function getNodeName() {\n var elem = this[0];\n return elem.nodeName;\n },\n\n // 从当前元素查找\n find: function find(selector) {\n var elem = this[0];\n return $(elem.querySelectorAll(selector));\n },\n\n // 获取当前元素的 text\n text: function text(val) {\n if (!val) {\n // 获取 text\n var elem = this[0];\n return elem.innerHTML.replace(/<.*?>/g, function () {\n return '';\n });\n } else {\n // 设置 text\n return this.forEach(function (elem) {\n elem.innerHTML = val;\n });\n }\n },\n\n // 获取 html\n html: function html(value) {\n var elem = this[0];\n if (value == null) {\n return elem.innerHTML;\n } else {\n elem.innerHTML = value;\n return this;\n }\n },\n\n // 获取 value\n val: function val() {\n var elem = this[0];\n return elem.value.trim();\n },\n\n // focus\n focus: function focus() {\n return this.forEach(function (elem) {\n elem.focus();\n });\n },\n\n // parent\n parent: function parent() {\n var elem = this[0];\n return $(elem.parentElement);\n },\n\n // parentUntil 找到符合 selector 的父节点\n parentUntil: function parentUntil(selector, _currentElem) {\n var results = document.querySelectorAll(selector);\n var length = results.length;\n if (!length) {\n // 传入的 selector 无效\n return null;\n }\n\n var elem = _currentElem || this[0];\n if (elem.nodeName === 'BODY') {\n return null;\n }\n\n var parent = elem.parentElement;\n var i = void 0;\n for (i = 0; i < length; i++) {\n if (parent === results[i]) {\n // 找到,并返回\n return $(parent);\n }\n }\n\n // 继续查找\n return this.parentUntil(selector, parent);\n },\n\n // 判断两个 elem 是否相等\n equal: function equal($elem) {\n if ($elem.nodeType === 1) {\n return this[0] === $elem;\n } else {\n return this[0] === $elem[0];\n }\n },\n\n // 将该元素插入到某个元素前面\n insertBefore: function insertBefore(selector) {\n var $referenceNode = $(selector);\n var referenceNode = $referenceNode[0];\n if (!referenceNode) {\n return this;\n }\n return this.forEach(function (elem) {\n var parent = referenceNode.parentNode;\n parent.insertBefore(elem, referenceNode);\n });\n },\n\n // 将该元素插入到某个元素后面\n insertAfter: function insertAfter(selector) {\n var $referenceNode = $(selector);\n var referenceNode = $referenceNode[0];\n if (!referenceNode) {\n return this;\n }\n return this.forEach(function (elem) {\n var parent = referenceNode.parentNode;\n if (parent.lastChild === referenceNode) {\n // 最后一个元素\n parent.appendChild(elem);\n } else {\n // 不是最后一个元素\n parent.insertBefore(elem, referenceNode.nextSibling);\n }\n });\n }\n};\n\n// new 一个对象\nfunction $(selector) {\n return new DomElement(selector);\n}\n\n// 解绑所有事件,用于销毁编辑器\n$.offAll = function () {\n eventList.forEach(function (item) {\n var elem = item.elem;\n var type = item.type;\n var fn = item.fn;\n // 解绑\n elem.removeEventListener(type, fn);\n });\n};\n\n/*\n 配置信息\n*/\n\nvar config = {\n\n // 默认菜单配置\n menus: ['head', 'bold', 'fontSize', 'fontName', 'italic', 'underline', 'strikeThrough', 'foreColor', 'backColor', 'link', 'list', 'justify', 'quote', 'emoticon', 'image', 'table', 'video', 'code', 'undo', 'redo'],\n\n fontNames: ['宋体', '微软雅黑', 'Arial', 'Tahoma', 'Verdana'],\n\n colors: ['#000000', '#eeece0', '#1c487f', '#4d80bf', '#c24f4a', '#8baa4a', '#7b5ba1', '#46acc8', '#f9963b', '#ffffff'],\n\n // // 语言配置\n // lang: {\n // '设置标题': 'title',\n // '正文': 'p',\n // '链接文字': 'link text',\n // '链接': 'link',\n // '插入': 'insert',\n // '创建': 'init'\n // },\n\n // 表情\n emotions: [{\n // tab 的标题\n title: '默认',\n // type -> 'emoji' / 'image'\n type: 'image',\n // content -> 数组\n content: [{\n alt: '[坏笑]',\n src: 'http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/50/pcmoren_huaixiao_org.png'\n }, {\n alt: '[舔屏]',\n src: 'http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/pcmoren_tian_org.png'\n }, {\n alt: '[污]',\n src: 'http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/3c/pcmoren_wu_org.png'\n }]\n }, {\n // tab 的标题\n title: '新浪',\n // type -> 'emoji' / 'image'\n type: 'image',\n // content -> 数组\n content: [{\n src: 'http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/7a/shenshou_thumb.gif',\n alt: '[草泥马]'\n }, {\n src: 'http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/60/horse2_thumb.gif',\n alt: '[神马]'\n }, {\n src: 'http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/bc/fuyun_thumb.gif',\n alt: '[浮云]'\n }]\n }, {\n // tab 的标题\n title: 'emoji',\n // type -> 'emoji' / 'image'\n type: 'emoji',\n // content -> 数组\n content: '😀 😃 😄 😁 😆 😅 😂 😊 😇 🙂 🙃 😉 😓 😪 😴 🙄 🤔 😬 🤐'.split(/\\s/)\n }],\n\n // 编辑区域的 z-index\n zIndex: 10000,\n\n // 是否开启 debug 模式(debug 模式下错误会 throw error 形式抛出)\n debug: false,\n\n // 插入链接时候的格式校验\n linkCheck: function linkCheck(text, link) {\n // text 是插入的文字\n // link 是插入的链接\n return true; // 返回 true 即表示成功\n // return '校验失败' // 返回字符串即表示失败的提示信息\n },\n\n // 插入网络图片的校验\n linkImgCheck: function linkImgCheck(src) {\n // src 即图片的地址\n return true; // 返回 true 即表示成功\n // return '校验失败' // 返回字符串即表示失败的提示信息\n },\n\n // 粘贴过滤样式,默认开启\n pasteFilterStyle: true,\n\n // 粘贴内容时,忽略图片。默认关闭\n pasteIgnoreImg: false,\n\n // 对粘贴的文字进行自定义处理,返回处理后的结果。编辑器会将处理后的结果粘贴到编辑区域中。\n // IE 暂时不支持\n pasteTextHandle: function pasteTextHandle(content) {\n // content 即粘贴过来的内容(html 或 纯文本),可进行自定义处理然后返回\n return content;\n },\n\n // onchange 事件\n // onchange: function (html) {\n // // html 即变化之后的内容\n // console.log(html)\n // },\n\n // 是否显示添加网络图片的 tab\n showLinkImg: true,\n\n // 插入网络图片的回调\n linkImgCallback: function linkImgCallback(url) {\n // console.log(url) // url 即插入图片的地址\n },\n\n // 默认上传图片 max size: 5M\n uploadImgMaxSize: 5 * 1024 * 1024,\n\n // 配置一次最多上传几个图片\n // uploadImgMaxLength: 5,\n\n // 上传图片,是否显示 base64 格式\n uploadImgShowBase64: false,\n\n // 上传图片,server 地址(如果有值,则 base64 格式的配置则失效)\n // uploadImgServer: '/upload',\n\n // 自定义配置 filename\n uploadFileName: '',\n\n // 上传图片的自定义参数\n uploadImgParams: {\n // token: 'abcdef12345'\n },\n\n // 上传图片的自定义header\n uploadImgHeaders: {\n // 'Accept': 'text/x-json'\n },\n\n // 配置 XHR withCredentials\n withCredentials: false,\n\n // 自定义上传图片超时时间 ms\n uploadImgTimeout: 10000,\n\n // 上传图片 hook \n uploadImgHooks: {\n // customInsert: function (insertLinkImg, result, editor) {\n // console.log('customInsert')\n // // 图片上传并返回结果,自定义插入图片的事件,而不是编辑器自动插入图片\n // const data = result.data1 || []\n // data.forEach(link => {\n // insertLinkImg(link)\n // })\n // },\n before: function before(xhr, editor, files) {\n // 图片上传之前触发\n\n // 如果返回的结果是 {prevent: true, msg: 'xxxx'} 则表示用户放弃上传\n // return {\n // prevent: true,\n // msg: '放弃上传'\n // }\n },\n success: function success(xhr, editor, result) {\n // 图片上传并返回结果,图片插入成功之后触发\n },\n fail: function fail(xhr, editor, result) {\n // 图片上传并返回结果,但图片插入错误时触发\n },\n error: function error(xhr, editor) {\n // 图片上传出错时触发\n },\n timeout: function timeout(xhr, editor) {\n // 图片上传超时时触发\n }\n },\n\n // 是否上传七牛云,默认为 false\n qiniu: false\n\n};\n\n/*\n 工具\n*/\n\n// 和 UA 相关的属性\nvar UA = {\n _ua: navigator.userAgent,\n\n // 是否 webkit\n isWebkit: function isWebkit() {\n var reg = /webkit/i;\n return reg.test(this._ua);\n },\n\n // 是否 IE\n isIE: function isIE() {\n return 'ActiveXObject' in window;\n }\n};\n\n// 遍历对象\nfunction objForEach(obj, fn) {\n var key = void 0,\n result = void 0;\n for (key in obj) {\n if (obj.hasOwnProperty(key)) {\n result = fn.call(obj, key, obj[key]);\n if (result === false) {\n break;\n }\n }\n }\n}\n\n// 遍历类数组\nfunction arrForEach(fakeArr, fn) {\n var i = void 0,\n item = void 0,\n result = void 0;\n var length = fakeArr.length || 0;\n for (i = 0; i < length; i++) {\n item = fakeArr[i];\n result = fn.call(fakeArr, item, i);\n if (result === false) {\n break;\n }\n }\n}\n\n// 获取随机数\nfunction getRandom(prefix) {\n return prefix + Math.random().toString().slice(2);\n}\n\n// 替换 html 特殊字符\nfunction replaceHtmlSymbol(html) {\n if (html == null) {\n return '';\n }\n return html.replace(//gm, '>').replace(/\"/gm, '"').replace(/(\\r\\n|\\r|\\n)/g, '
      ');\n}\n\n// 返回百分比的格式\n\n\n// 判断是不是 function\nfunction isFunction(fn) {\n return typeof fn === 'function';\n}\n\n/*\n bold-menu\n*/\n// 构造函数\nfunction Bold(editor) {\n this.editor = editor;\n this.$elem = $('
      \\n \\n
      ');\n this.type = 'click';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nBold.prototype = {\n constructor: Bold,\n\n // 点击事件\n onClick: function onClick(e) {\n // 点击菜单将触发这里\n\n var editor = this.editor;\n var isSeleEmpty = editor.selection.isSelectionEmpty();\n\n if (isSeleEmpty) {\n // 选区是空的,插入并选中一个“空白”\n editor.selection.createEmptyRange();\n }\n\n // 执行 bold 命令\n editor.cmd.do('bold');\n\n if (isSeleEmpty) {\n // 需要将选取折叠起来\n editor.selection.collapseRange();\n editor.selection.restoreSelection();\n }\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n if (editor.cmd.queryCommandState('bold')) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n 替换多语言\n */\n\nvar replaceLang = function (editor, str) {\n var langArgs = editor.config.langArgs || [];\n var result = str;\n\n langArgs.forEach(function (item) {\n var reg = item.reg;\n var val = item.val;\n\n if (reg.test(result)) {\n result = result.replace(reg, function () {\n return val;\n });\n }\n });\n\n return result;\n};\n\n/*\n droplist\n*/\nvar _emptyFn = function _emptyFn() {};\n\n// 构造函数\nfunction DropList(menu, opt) {\n var _this = this;\n\n // droplist 所依附的菜单\n var editor = menu.editor;\n this.menu = menu;\n this.opt = opt;\n // 容器\n var $container = $('
      ');\n\n // 标题\n var $title = opt.$title;\n var titleHtml = void 0;\n if ($title) {\n // 替换多语言\n titleHtml = $title.html();\n titleHtml = replaceLang(editor, titleHtml);\n $title.html(titleHtml);\n\n $title.addClass('w-e-dp-title');\n $container.append($title);\n }\n\n var list = opt.list || [];\n var type = opt.type || 'list'; // 'list' 列表形式(如“标题”菜单) / 'inline-block' 块状形式(如“颜色”菜单)\n var onClick = opt.onClick || _emptyFn;\n\n // 加入 DOM 并绑定事件\n var $list = $('
        ');\n $container.append($list);\n list.forEach(function (item) {\n var $elem = item.$elem;\n\n // 替换多语言\n var elemHtml = $elem.html();\n elemHtml = replaceLang(editor, elemHtml);\n $elem.html(elemHtml);\n\n var value = item.value;\n var $li = $('
      • ');\n if ($elem) {\n $li.append($elem);\n $list.append($li);\n $li.on('click', function (e) {\n onClick(value);\n\n // 隐藏\n _this.hideTimeoutId = setTimeout(function () {\n _this.hide();\n }, 0);\n });\n }\n });\n\n // 绑定隐藏事件\n $container.on('mouseleave', function (e) {\n _this.hideTimeoutId = setTimeout(function () {\n _this.hide();\n }, 0);\n });\n\n // 记录属性\n this.$container = $container;\n\n // 基本属性\n this._rendered = false;\n this._show = false;\n}\n\n// 原型\nDropList.prototype = {\n constructor: DropList,\n\n // 显示(插入DOM)\n show: function show() {\n if (this.hideTimeoutId) {\n // 清除之前的定时隐藏\n clearTimeout(this.hideTimeoutId);\n }\n\n var menu = this.menu;\n var $menuELem = menu.$elem;\n var $container = this.$container;\n if (this._show) {\n return;\n }\n if (this._rendered) {\n // 显示\n $container.show();\n } else {\n // 加入 DOM 之前先定位位置\n var menuHeight = $menuELem.getSizeData().height || 0;\n var width = this.opt.width || 100; // 默认为 100\n $container.css('margin-top', menuHeight + 'px').css('width', width + 'px');\n\n // 加入到 DOM\n $menuELem.append($container);\n this._rendered = true;\n }\n\n // 修改属性\n this._show = true;\n },\n\n // 隐藏(移除DOM)\n hide: function hide() {\n if (this.showTimeoutId) {\n // 清除之前的定时显示\n clearTimeout(this.showTimeoutId);\n }\n\n var $container = this.$container;\n if (!this._show) {\n return;\n }\n // 隐藏并需改属性\n $container.hide();\n this._show = false;\n }\n};\n\n/*\n menu - header\n*/\n// 构造函数\nfunction Head(editor) {\n var _this = this;\n\n this.editor = editor;\n this.$elem = $('
        ');\n this.type = 'droplist';\n\n // 当前是否 active 状态\n this._active = false;\n\n // 初始化 droplist\n this.droplist = new DropList(this, {\n width: 100,\n $title: $('

        设置标题

        '),\n type: 'list', // droplist 以列表形式展示\n list: [{ $elem: $('

        H1

        '), value: '

        ' }, { $elem: $('

        H2

        '), value: '

        ' }, { $elem: $('

        H3

        '), value: '

        ' }, { $elem: $('

        H4

        '), value: '

        ' }, { $elem: $('

        H5
        '), value: '
        ' }, { $elem: $('

        正文

        '), value: '

        ' }],\n onClick: function onClick(value) {\n // 注意 this 是指向当前的 Head 对象\n _this._command(value);\n }\n });\n}\n\n// 原型\nHead.prototype = {\n constructor: Head,\n\n // 执行命令\n _command: function _command(value) {\n var editor = this.editor;\n\n var $selectionElem = editor.selection.getSelectionContainerElem();\n if (editor.$textElem.equal($selectionElem)) {\n // 不能选中多行来设置标题,否则会出现问题\n // 例如选中的是

        xxx

        yyy

        来设置标题,设置之后会成为

        xxx
        yyy

        不符合预期\n return;\n }\n\n editor.cmd.do('formatBlock', value);\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n var reg = /^h/i;\n var cmdValue = editor.cmd.queryCommandValue('formatBlock');\n if (reg.test(cmdValue)) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n menu - fontSize\n*/\n\n// 构造函数\nfunction FontSize(editor) {\n var _this = this;\n\n this.editor = editor;\n this.$elem = $('
        ');\n this.type = 'droplist';\n\n // 当前是否 active 状态\n this._active = false;\n\n // 初始化 droplist\n this.droplist = new DropList(this, {\n width: 160,\n $title: $('

        字号

        '),\n type: 'list', // droplist 以列表形式展示\n list: [{ $elem: $('x-small'), value: '1' }, { $elem: $('small'), value: '2' }, { $elem: $('normal'), value: '3' }, { $elem: $('large'), value: '4' }, { $elem: $('x-large'), value: '5' }, { $elem: $('xx-large'), value: '6' }],\n onClick: function onClick(value) {\n // 注意 this 是指向当前的 FontSize 对象\n _this._command(value);\n }\n });\n}\n\n// 原型\nFontSize.prototype = {\n constructor: FontSize,\n\n // 执行命令\n _command: function _command(value) {\n var editor = this.editor;\n editor.cmd.do('fontSize', value);\n }\n};\n\n/*\n menu - fontName\n*/\n\n// 构造函数\nfunction FontName(editor) {\n var _this = this;\n\n this.editor = editor;\n this.$elem = $('
        ');\n this.type = 'droplist';\n\n // 当前是否 active 状态\n this._active = false;\n\n // 获取配置的字体\n var config = editor.config;\n var fontNames = config.fontNames || [];\n\n // 初始化 droplist\n this.droplist = new DropList(this, {\n width: 100,\n $title: $('

        字体

        '),\n type: 'list', // droplist 以列表形式展示\n list: fontNames.map(function (fontName) {\n return { $elem: $('' + fontName + ''), value: fontName };\n }),\n onClick: function onClick(value) {\n // 注意 this 是指向当前的 FontName 对象\n _this._command(value);\n }\n });\n}\n\n// 原型\nFontName.prototype = {\n constructor: FontName,\n\n _command: function _command(value) {\n var editor = this.editor;\n editor.cmd.do('fontName', value);\n }\n};\n\n/*\n panel\n*/\n\nvar emptyFn = function emptyFn() {};\n\n// 记录已经显示 panel 的菜单\nvar _isCreatedPanelMenus = [];\n\n// 构造函数\nfunction Panel(menu, opt) {\n this.menu = menu;\n this.opt = opt;\n}\n\n// 原型\nPanel.prototype = {\n constructor: Panel,\n\n // 显示(插入DOM)\n show: function show() {\n var _this = this;\n\n var menu = this.menu;\n if (_isCreatedPanelMenus.indexOf(menu) >= 0) {\n // 该菜单已经创建了 panel 不能再创建\n return;\n }\n\n var editor = menu.editor;\n var $body = $('body');\n var $textContainerElem = editor.$textContainerElem;\n var opt = this.opt;\n\n // panel 的容器\n var $container = $('
        ');\n var width = opt.width || 300; // 默认 300px\n $container.css('width', width + 'px').css('margin-left', (0 - width) / 2 + 'px');\n\n // 添加关闭按钮\n var $closeBtn = $('');\n $container.append($closeBtn);\n $closeBtn.on('click', function () {\n _this.hide();\n });\n\n // 准备 tabs 容器\n var $tabTitleContainer = $('
          ');\n var $tabContentContainer = $('
          ');\n $container.append($tabTitleContainer).append($tabContentContainer);\n\n // 设置高度\n var height = opt.height;\n if (height) {\n $tabContentContainer.css('height', height + 'px').css('overflow-y', 'auto');\n }\n\n // tabs\n var tabs = opt.tabs || [];\n var tabTitleArr = [];\n var tabContentArr = [];\n tabs.forEach(function (tab, tabIndex) {\n if (!tab) {\n return;\n }\n var title = tab.title || '';\n var tpl = tab.tpl || '';\n\n // 替换多语言\n title = replaceLang(editor, title);\n tpl = replaceLang(editor, tpl);\n\n // 添加到 DOM\n var $title = $('
        • ' + title + '
        • ');\n $tabTitleContainer.append($title);\n var $content = $(tpl);\n $tabContentContainer.append($content);\n\n // 记录到内存\n $title._index = tabIndex;\n tabTitleArr.push($title);\n tabContentArr.push($content);\n\n // 设置 active 项\n if (tabIndex === 0) {\n $title._active = true;\n $title.addClass('w-e-active');\n } else {\n $content.hide();\n }\n\n // 绑定 tab 的事件\n $title.on('click', function (e) {\n if ($title._active) {\n return;\n }\n // 隐藏所有的 tab\n tabTitleArr.forEach(function ($title) {\n $title._active = false;\n $title.removeClass('w-e-active');\n });\n tabContentArr.forEach(function ($content) {\n $content.hide();\n });\n\n // 显示当前的 tab\n $title._active = true;\n $title.addClass('w-e-active');\n $content.show();\n });\n });\n\n // 绑定关闭事件\n $container.on('click', function (e) {\n // 点击时阻止冒泡\n e.stopPropagation();\n });\n $body.on('click', function (e) {\n _this.hide();\n });\n\n // 添加到 DOM\n $textContainerElem.append($container);\n\n // 绑定 opt 的事件,只有添加到 DOM 之后才能绑定成功\n tabs.forEach(function (tab, index) {\n if (!tab) {\n return;\n }\n var events = tab.events || [];\n events.forEach(function (event) {\n var selector = event.selector;\n var type = event.type;\n var fn = event.fn || emptyFn;\n var $content = tabContentArr[index];\n $content.find(selector).on(type, function (e) {\n e.stopPropagation();\n var needToHide = fn(e);\n // 执行完事件之后,是否要关闭 panel\n if (needToHide) {\n _this.hide();\n }\n });\n });\n });\n\n // focus 第一个 elem\n var $inputs = $container.find('input[type=text],textarea');\n if ($inputs.length) {\n $inputs.get(0).focus();\n }\n\n // 添加到属性\n this.$container = $container;\n\n // 隐藏其他 panel\n this._hideOtherPanels();\n // 记录该 menu 已经创建了 panel\n _isCreatedPanelMenus.push(menu);\n },\n\n // 隐藏(移除DOM)\n hide: function hide() {\n var menu = this.menu;\n var $container = this.$container;\n if ($container) {\n $container.remove();\n }\n\n // 将该 menu 记录中移除\n _isCreatedPanelMenus = _isCreatedPanelMenus.filter(function (item) {\n if (item === menu) {\n return false;\n } else {\n return true;\n }\n });\n },\n\n // 一个 panel 展示时,隐藏其他 panel\n _hideOtherPanels: function _hideOtherPanels() {\n if (!_isCreatedPanelMenus.length) {\n return;\n }\n _isCreatedPanelMenus.forEach(function (menu) {\n var panel = menu.panel || {};\n if (panel.hide) {\n panel.hide();\n }\n });\n }\n};\n\n/*\n menu - link\n*/\n// 构造函数\nfunction Link(editor) {\n this.editor = editor;\n this.$elem = $('
          ');\n this.type = 'panel';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nLink.prototype = {\n constructor: Link,\n\n // 点击事件\n onClick: function onClick(e) {\n var editor = this.editor;\n var $linkelem = void 0;\n\n if (this._active) {\n // 当前选区在链接里面\n $linkelem = editor.selection.getSelectionContainerElem();\n if (!$linkelem) {\n return;\n }\n // 将该元素都包含在选取之内,以便后面整体替换\n editor.selection.createRangeByElem($linkelem);\n editor.selection.restoreSelection();\n // 显示 panel\n this._createPanel($linkelem.text(), $linkelem.attr('href'));\n } else {\n // 当前选区不在链接里面\n if (editor.selection.isSelectionEmpty()) {\n // 选区是空的,未选中内容\n this._createPanel('', '');\n } else {\n // 选中内容了\n this._createPanel(editor.selection.getSelectionText(), '');\n }\n }\n },\n\n // 创建 panel\n _createPanel: function _createPanel(text, link) {\n var _this = this;\n\n // panel 中需要用到的id\n var inputLinkId = getRandom('input-link');\n var inputTextId = getRandom('input-text');\n var btnOkId = getRandom('btn-ok');\n var btnDelId = getRandom('btn-del');\n\n // 是否显示“删除链接”\n var delBtnDisplay = this._active ? 'inline-block' : 'none';\n\n // 初始化并显示 panel\n var panel = new Panel(this, {\n width: 300,\n // panel 中可包含多个 tab\n tabs: [{\n // tab 的标题\n title: '链接',\n // 模板\n tpl: '
          \\n \\n \\n
          \\n \\n \\n
          \\n
          ',\n // 事件绑定\n events: [\n // 插入链接\n {\n selector: '#' + btnOkId,\n type: 'click',\n fn: function fn() {\n // 执行插入链接\n var $link = $('#' + inputLinkId);\n var $text = $('#' + inputTextId);\n var link = $link.val();\n var text = $text.val();\n _this._insertLink(text, link);\n\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n },\n // 删除链接\n {\n selector: '#' + btnDelId,\n type: 'click',\n fn: function fn() {\n // 执行删除链接\n _this._delLink();\n\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }]\n } // tab end\n ] // tabs end\n });\n\n // 显示 panel\n panel.show();\n\n // 记录属性\n this.panel = panel;\n },\n\n // 删除当前链接\n _delLink: function _delLink() {\n if (!this._active) {\n return;\n }\n var editor = this.editor;\n var $selectionELem = editor.selection.getSelectionContainerElem();\n if (!$selectionELem) {\n return;\n }\n var selectionText = editor.selection.getSelectionText();\n editor.cmd.do('insertHTML', '' + selectionText + '');\n },\n\n // 插入链接\n _insertLink: function _insertLink(text, link) {\n var editor = this.editor;\n var config = editor.config;\n var linkCheck = config.linkCheck;\n var checkResult = true; // 默认为 true\n if (linkCheck && typeof linkCheck === 'function') {\n checkResult = linkCheck(text, link);\n }\n if (checkResult === true) {\n editor.cmd.do('insertHTML', '' + text + '');\n } else {\n alert(checkResult);\n }\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n var $selectionELem = editor.selection.getSelectionContainerElem();\n if (!$selectionELem) {\n return;\n }\n if ($selectionELem.getNodeName() === 'A') {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n italic-menu\n*/\n// 构造函数\nfunction Italic(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'click';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nItalic.prototype = {\n constructor: Italic,\n\n // 点击事件\n onClick: function onClick(e) {\n // 点击菜单将触发这里\n\n var editor = this.editor;\n var isSeleEmpty = editor.selection.isSelectionEmpty();\n\n if (isSeleEmpty) {\n // 选区是空的,插入并选中一个“空白”\n editor.selection.createEmptyRange();\n }\n\n // 执行 italic 命令\n editor.cmd.do('italic');\n\n if (isSeleEmpty) {\n // 需要将选取折叠起来\n editor.selection.collapseRange();\n editor.selection.restoreSelection();\n }\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n if (editor.cmd.queryCommandState('italic')) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n redo-menu\n*/\n// 构造函数\nfunction Redo(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'click';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nRedo.prototype = {\n constructor: Redo,\n\n // 点击事件\n onClick: function onClick(e) {\n // 点击菜单将触发这里\n\n var editor = this.editor;\n\n // 执行 redo 命令\n editor.cmd.do('redo');\n }\n};\n\n/*\n strikeThrough-menu\n*/\n// 构造函数\nfunction StrikeThrough(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'click';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nStrikeThrough.prototype = {\n constructor: StrikeThrough,\n\n // 点击事件\n onClick: function onClick(e) {\n // 点击菜单将触发这里\n\n var editor = this.editor;\n var isSeleEmpty = editor.selection.isSelectionEmpty();\n\n if (isSeleEmpty) {\n // 选区是空的,插入并选中一个“空白”\n editor.selection.createEmptyRange();\n }\n\n // 执行 strikeThrough 命令\n editor.cmd.do('strikeThrough');\n\n if (isSeleEmpty) {\n // 需要将选取折叠起来\n editor.selection.collapseRange();\n editor.selection.restoreSelection();\n }\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n if (editor.cmd.queryCommandState('strikeThrough')) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n underline-menu\n*/\n// 构造函数\nfunction Underline(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'click';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nUnderline.prototype = {\n constructor: Underline,\n\n // 点击事件\n onClick: function onClick(e) {\n // 点击菜单将触发这里\n\n var editor = this.editor;\n var isSeleEmpty = editor.selection.isSelectionEmpty();\n\n if (isSeleEmpty) {\n // 选区是空的,插入并选中一个“空白”\n editor.selection.createEmptyRange();\n }\n\n // 执行 underline 命令\n editor.cmd.do('underline');\n\n if (isSeleEmpty) {\n // 需要将选取折叠起来\n editor.selection.collapseRange();\n editor.selection.restoreSelection();\n }\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n if (editor.cmd.queryCommandState('underline')) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n undo-menu\n*/\n// 构造函数\nfunction Undo(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'click';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nUndo.prototype = {\n constructor: Undo,\n\n // 点击事件\n onClick: function onClick(e) {\n // 点击菜单将触发这里\n\n var editor = this.editor;\n\n // 执行 undo 命令\n editor.cmd.do('undo');\n }\n};\n\n/*\n menu - list\n*/\n// 构造函数\nfunction List(editor) {\n var _this = this;\n\n this.editor = editor;\n this.$elem = $('
          ');\n this.type = 'droplist';\n\n // 当前是否 active 状态\n this._active = false;\n\n // 初始化 droplist\n this.droplist = new DropList(this, {\n width: 120,\n $title: $('

          设置列表

          '),\n type: 'list', // droplist 以列表形式展示\n list: [{ $elem: $(' 有序列表'), value: 'insertOrderedList' }, { $elem: $(' 无序列表'), value: 'insertUnorderedList' }],\n onClick: function onClick(value) {\n // 注意 this 是指向当前的 List 对象\n _this._command(value);\n }\n });\n}\n\n// 原型\nList.prototype = {\n constructor: List,\n\n // 执行命令\n _command: function _command(value) {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n editor.selection.restoreSelection();\n if (editor.cmd.queryCommandState(value)) {\n return;\n }\n editor.cmd.do(value);\n\n // 验证列表是否被包裹在

          之内\n var $selectionElem = editor.selection.getSelectionContainerElem();\n if ($selectionElem.getNodeName() === 'LI') {\n $selectionElem = $selectionElem.parent();\n }\n if (/^ol|ul$/i.test($selectionElem.getNodeName()) === false) {\n return;\n }\n if ($selectionElem.equal($textElem)) {\n // 证明是顶级标签,没有被

          包裹\n return;\n }\n var $parent = $selectionElem.parent();\n if ($parent.equal($textElem)) {\n // $parent 是顶级标签,不能删除\n return;\n }\n\n $selectionElem.insertAfter($parent);\n $parent.remove();\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n if (editor.cmd.queryCommandState('insertUnOrderedList') || editor.cmd.queryCommandState('insertOrderedList')) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n menu - justify\n*/\n// 构造函数\nfunction Justify(editor) {\n var _this = this;\n\n this.editor = editor;\n this.$elem = $('

          ');\n this.type = 'droplist';\n\n // 当前是否 active 状态\n this._active = false;\n\n // 初始化 droplist\n this.droplist = new DropList(this, {\n width: 100,\n $title: $('

          对齐方式

          '),\n type: 'list', // droplist 以列表形式展示\n list: [{ $elem: $(' 靠左'), value: 'justifyLeft' }, { $elem: $(' 居中'), value: 'justifyCenter' }, { $elem: $(' 靠右'), value: 'justifyRight' }],\n onClick: function onClick(value) {\n // 注意 this 是指向当前的 List 对象\n _this._command(value);\n }\n });\n}\n\n// 原型\nJustify.prototype = {\n constructor: Justify,\n\n // 执行命令\n _command: function _command(value) {\n var editor = this.editor;\n editor.cmd.do(value);\n }\n};\n\n/*\n menu - Forecolor\n*/\n// 构造函数\nfunction ForeColor(editor) {\n var _this = this;\n\n this.editor = editor;\n this.$elem = $('
          ');\n this.type = 'droplist';\n\n // 获取配置的颜色\n var config = editor.config;\n var colors = config.colors || [];\n\n // 当前是否 active 状态\n this._active = false;\n\n // 初始化 droplist\n this.droplist = new DropList(this, {\n width: 120,\n $title: $('

          文字颜色

          '),\n type: 'inline-block', // droplist 内容以 block 形式展示\n list: colors.map(function (color) {\n return { $elem: $(''), value: color };\n }),\n onClick: function onClick(value) {\n // 注意 this 是指向当前的 ForeColor 对象\n _this._command(value);\n }\n });\n}\n\n// 原型\nForeColor.prototype = {\n constructor: ForeColor,\n\n // 执行命令\n _command: function _command(value) {\n var editor = this.editor;\n editor.cmd.do('foreColor', value);\n }\n};\n\n/*\n menu - BackColor\n*/\n// 构造函数\nfunction BackColor(editor) {\n var _this = this;\n\n this.editor = editor;\n this.$elem = $('
          ');\n this.type = 'droplist';\n\n // 获取配置的颜色\n var config = editor.config;\n var colors = config.colors || [];\n\n // 当前是否 active 状态\n this._active = false;\n\n // 初始化 droplist\n this.droplist = new DropList(this, {\n width: 120,\n $title: $('

          背景色

          '),\n type: 'inline-block', // droplist 内容以 block 形式展示\n list: colors.map(function (color) {\n return { $elem: $(''), value: color };\n }),\n onClick: function onClick(value) {\n // 注意 this 是指向当前的 BackColor 对象\n _this._command(value);\n }\n });\n}\n\n// 原型\nBackColor.prototype = {\n constructor: BackColor,\n\n // 执行命令\n _command: function _command(value) {\n var editor = this.editor;\n editor.cmd.do('backColor', value);\n }\n};\n\n/*\n menu - quote\n*/\n// 构造函数\nfunction Quote(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'click';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nQuote.prototype = {\n constructor: Quote,\n\n onClick: function onClick(e) {\n var editor = this.editor;\n var $selectionElem = editor.selection.getSelectionContainerElem();\n var nodeName = $selectionElem.getNodeName();\n\n if (!UA.isIE()) {\n if (nodeName === 'BLOCKQUOTE') {\n // 撤销 quote\n editor.cmd.do('formatBlock', '

          ');\n } else {\n // 转换为 quote\n editor.cmd.do('formatBlock', '

          ');\n }\n return;\n }\n\n // IE 中不支持 formatBlock
          ,要用其他方式兼容\n var content = void 0,\n $targetELem = void 0;\n if (nodeName === 'P') {\n // 将 P 转换为 quote\n content = $selectionElem.text();\n $targetELem = $('
          ' + content + '
          ');\n $targetELem.insertAfter($selectionElem);\n $selectionElem.remove();\n return;\n }\n if (nodeName === 'BLOCKQUOTE') {\n // 撤销 quote\n content = $selectionElem.text();\n $targetELem = $('

          ' + content + '

          ');\n $targetELem.insertAfter($selectionElem);\n $selectionElem.remove();\n }\n },\n\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n var reg = /^BLOCKQUOTE$/i;\n var cmdValue = editor.cmd.queryCommandValue('formatBlock');\n if (reg.test(cmdValue)) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n menu - code\n*/\n// 构造函数\nfunction Code(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'panel';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nCode.prototype = {\n constructor: Code,\n\n onClick: function onClick(e) {\n var editor = this.editor;\n var $startElem = editor.selection.getSelectionStartElem();\n var $endElem = editor.selection.getSelectionEndElem();\n var isSeleEmpty = editor.selection.isSelectionEmpty();\n var selectionText = editor.selection.getSelectionText();\n var $code = void 0;\n\n if (!$startElem.equal($endElem)) {\n // 跨元素选择,不做处理\n editor.selection.restoreSelection();\n return;\n }\n if (!isSeleEmpty) {\n // 选取不是空,用 包裹即可\n $code = $('' + selectionText + '');\n editor.cmd.do('insertElem', $code);\n editor.selection.createRangeByElem($code, false);\n editor.selection.restoreSelection();\n return;\n }\n\n // 选取是空,且没有夸元素选择,则插入
          \n        if (this._active) {\n            // 选中状态,将编辑内容\n            this._createPanel($startElem.html());\n        } else {\n            // 未选中状态,将创建内容\n            this._createPanel();\n        }\n    },\n\n    _createPanel: function _createPanel(value) {\n        var _this = this;\n\n        // value - 要编辑的内容\n        value = value || '';\n        var type = !value ? 'new' : 'edit';\n        var textId = getRandom('texxt');\n        var btnId = getRandom('btn');\n\n        var panel = new Panel(this, {\n            width: 500,\n            // 一个 Panel 包含多个 tab\n            tabs: [{\n                // 标题\n                title: '插入代码',\n                // 模板\n                tpl: '
          \\n \\n
          \\n \\n
          \\n
          ',\n // 事件绑定\n events: [\n // 插入代码\n {\n selector: '#' + btnId,\n type: 'click',\n fn: function fn() {\n var $text = $('#' + textId);\n var text = $text.val() || $text.html();\n text = replaceHtmlSymbol(text);\n if (type === 'new') {\n // 新插入\n _this._insertCode(text);\n } else {\n // 编辑更新\n _this._updateCode(text);\n }\n\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }]\n } // first tab end\n ] // tabs end\n }); // new Panel end\n\n // 显示 panel\n panel.show();\n\n // 记录属性\n this.panel = panel;\n },\n\n // 插入代码\n _insertCode: function _insertCode(value) {\n var editor = this.editor;\n editor.cmd.do('insertHTML', '
          ' + value + '


          ');\n },\n\n // 更新代码\n _updateCode: function _updateCode(value) {\n var editor = this.editor;\n var $selectionELem = editor.selection.getSelectionContainerElem();\n if (!$selectionELem) {\n return;\n }\n $selectionELem.html(value);\n editor.selection.restoreSelection();\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n var $selectionELem = editor.selection.getSelectionContainerElem();\n if (!$selectionELem) {\n return;\n }\n var $parentElem = $selectionELem.parent();\n if ($selectionELem.getNodeName() === 'CODE' && $parentElem.getNodeName() === 'PRE') {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n menu - emoticon\n*/\n// 构造函数\nfunction Emoticon(editor) {\n this.editor = editor;\n this.$elem = $('
          \\n \\n
          ');\n this.type = 'panel';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nEmoticon.prototype = {\n constructor: Emoticon,\n\n onClick: function onClick() {\n this._createPanel();\n },\n\n _createPanel: function _createPanel() {\n var _this = this;\n\n var editor = this.editor;\n var config = editor.config;\n // 获取表情配置\n var emotions = config.emotions || [];\n\n // 创建表情 dropPanel 的配置\n var tabConfig = [];\n emotions.forEach(function (emotData) {\n var emotType = emotData.type;\n var content = emotData.content || [];\n\n // 这一组表情最终拼接出来的 html\n var faceHtml = '';\n\n // emoji 表情\n if (emotType === 'emoji') {\n content.forEach(function (item) {\n if (item) {\n faceHtml += '' + item + '';\n }\n });\n }\n // 图片表情\n if (emotType === 'image') {\n content.forEach(function (item) {\n var src = item.src;\n var alt = item.alt;\n if (src) {\n // 加一个 data-w-e 属性,点击图片的时候不再提示编辑图片\n faceHtml += '\"'';\n }\n });\n }\n\n tabConfig.push({\n title: emotData.title,\n tpl: '
          ' + faceHtml + '
          ',\n events: [{\n selector: 'span.w-e-item',\n type: 'click',\n fn: function fn(e) {\n var target = e.target;\n var $target = $(target);\n var nodeName = $target.getNodeName();\n\n var insertHtml = void 0;\n if (nodeName === 'IMG') {\n // 插入图片\n insertHtml = $target.parent().html();\n } else {\n // 插入 emoji\n insertHtml = '' + $target.html() + '';\n }\n\n _this._insert(insertHtml);\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }]\n });\n });\n\n var panel = new Panel(this, {\n width: 300,\n height: 200,\n // 一个 Panel 包含多个 tab\n tabs: tabConfig\n });\n\n // 显示 panel\n panel.show();\n\n // 记录属性\n this.panel = panel;\n },\n\n // 插入表情\n _insert: function _insert(emotHtml) {\n var editor = this.editor;\n editor.cmd.do('insertHTML', emotHtml);\n }\n};\n\n/*\n menu - table\n*/\n// 构造函数\nfunction Table(editor) {\n this.editor = editor;\n this.$elem = $('
          ');\n this.type = 'panel';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nTable.prototype = {\n constructor: Table,\n\n onClick: function onClick() {\n if (this._active) {\n // 编辑现有表格\n this._createEditPanel();\n } else {\n // 插入新表格\n this._createInsertPanel();\n }\n },\n\n // 创建插入新表格的 panel\n _createInsertPanel: function _createInsertPanel() {\n var _this = this;\n\n // 用到的 id\n var btnInsertId = getRandom('btn');\n var textRowNum = getRandom('row');\n var textColNum = getRandom('col');\n\n var panel = new Panel(this, {\n width: 250,\n // panel 包含多个 tab\n tabs: [{\n // 标题\n title: '插入表格',\n // 模板\n tpl: '
          \\n

          \\n \\u521B\\u5EFA\\n \\n \\u884C\\n \\n \\u5217\\u7684\\u8868\\u683C\\n

          \\n
          \\n \\n
          \\n
          ',\n // 事件绑定\n events: [{\n // 点击按钮,插入表格\n selector: '#' + btnInsertId,\n type: 'click',\n fn: function fn() {\n var rowNum = parseInt($('#' + textRowNum).val());\n var colNum = parseInt($('#' + textColNum).val());\n\n if (rowNum && colNum && rowNum > 0 && colNum > 0) {\n // form 数据有效\n _this._insert(rowNum, colNum);\n }\n\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }]\n } // first tab end\n ] // tabs end\n }); // panel end\n\n // 展示 panel\n panel.show();\n\n // 记录属性\n this.panel = panel;\n },\n\n // 插入表格\n _insert: function _insert(rowNum, colNum) {\n // 拼接 table 模板\n var r = void 0,\n c = void 0;\n var html = '';\n for (r = 0; r < rowNum; r++) {\n html += '';\n if (r === 0) {\n for (c = 0; c < colNum; c++) {\n html += '';\n }\n } else {\n for (c = 0; c < colNum; c++) {\n html += '';\n }\n }\n html += '';\n }\n html += '
            


          ';\n\n // 执行命令\n var editor = this.editor;\n editor.cmd.do('insertHTML', html);\n\n // 防止 firefox 下出现 resize 的控制点\n editor.cmd.do('enableObjectResizing', false);\n editor.cmd.do('enableInlineTableEditing', false);\n },\n\n // 创建编辑表格的 panel\n _createEditPanel: function _createEditPanel() {\n var _this2 = this;\n\n // 可用的 id\n var addRowBtnId = getRandom('add-row');\n var addColBtnId = getRandom('add-col');\n var delRowBtnId = getRandom('del-row');\n var delColBtnId = getRandom('del-col');\n var delTableBtnId = getRandom('del-table');\n\n // 创建 panel 对象\n var panel = new Panel(this, {\n width: 320,\n // panel 包含多个 tab\n tabs: [{\n // 标题\n title: '编辑表格',\n // 模板\n tpl: '
          \\n
          \\n \\n \\n \\n \\n
          \\n
          \\n \\n \\n
          ',\n // 事件绑定\n events: [{\n // 增加行\n selector: '#' + addRowBtnId,\n type: 'click',\n fn: function fn() {\n _this2._addRow();\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }, {\n // 增加列\n selector: '#' + addColBtnId,\n type: 'click',\n fn: function fn() {\n _this2._addCol();\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }, {\n // 删除行\n selector: '#' + delRowBtnId,\n type: 'click',\n fn: function fn() {\n _this2._delRow();\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }, {\n // 删除列\n selector: '#' + delColBtnId,\n type: 'click',\n fn: function fn() {\n _this2._delCol();\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }, {\n // 删除表格\n selector: '#' + delTableBtnId,\n type: 'click',\n fn: function fn() {\n _this2._delTable();\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }]\n }]\n });\n // 显示 panel\n panel.show();\n },\n\n // 获取选中的单元格的位置信息\n _getLocationData: function _getLocationData() {\n var result = {};\n var editor = this.editor;\n var $selectionELem = editor.selection.getSelectionContainerElem();\n if (!$selectionELem) {\n return;\n }\n var nodeName = $selectionELem.getNodeName();\n if (nodeName !== 'TD' && nodeName !== 'TH') {\n return;\n }\n\n // 获取 td index\n var $tr = $selectionELem.parent();\n var $tds = $tr.children();\n var tdLength = $tds.length;\n $tds.forEach(function (td, index) {\n if (td === $selectionELem[0]) {\n // 记录并跳出循环\n result.td = {\n index: index,\n elem: td,\n length: tdLength\n };\n return false;\n }\n });\n\n // 获取 tr index\n var $tbody = $tr.parent();\n var $trs = $tbody.children();\n var trLength = $trs.length;\n $trs.forEach(function (tr, index) {\n if (tr === $tr[0]) {\n // 记录并跳出循环\n result.tr = {\n index: index,\n elem: tr,\n length: trLength\n };\n return false;\n }\n });\n\n // 返回结果\n return result;\n },\n\n // 增加行\n _addRow: function _addRow() {\n // 获取当前单元格的位置信息\n var locationData = this._getLocationData();\n if (!locationData) {\n return;\n }\n var trData = locationData.tr;\n var $currentTr = $(trData.elem);\n var tdData = locationData.td;\n var tdLength = tdData.length;\n\n // 拼接即将插入的字符串\n var newTr = document.createElement('tr');\n var tpl = '',\n i = void 0;\n for (i = 0; i < tdLength; i++) {\n tpl += ' ';\n }\n newTr.innerHTML = tpl;\n // 插入\n $(newTr).insertAfter($currentTr);\n },\n\n // 增加列\n _addCol: function _addCol() {\n // 获取当前单元格的位置信息\n var locationData = this._getLocationData();\n if (!locationData) {\n return;\n }\n var trData = locationData.tr;\n var tdData = locationData.td;\n var tdIndex = tdData.index;\n var $currentTr = $(trData.elem);\n var $trParent = $currentTr.parent();\n var $trs = $trParent.children();\n\n // 遍历所有行\n $trs.forEach(function (tr) {\n var $tr = $(tr);\n var $tds = $tr.children();\n var $currentTd = $tds.get(tdIndex);\n var name = $currentTd.getNodeName().toLowerCase();\n\n // new 一个 td,并插入\n var newTd = document.createElement(name);\n $(newTd).insertAfter($currentTd);\n });\n },\n\n // 删除行\n _delRow: function _delRow() {\n // 获取当前单元格的位置信息\n var locationData = this._getLocationData();\n if (!locationData) {\n return;\n }\n var trData = locationData.tr;\n var $currentTr = $(trData.elem);\n $currentTr.remove();\n },\n\n // 删除列\n _delCol: function _delCol() {\n // 获取当前单元格的位置信息\n var locationData = this._getLocationData();\n if (!locationData) {\n return;\n }\n var trData = locationData.tr;\n var tdData = locationData.td;\n var tdIndex = tdData.index;\n var $currentTr = $(trData.elem);\n var $trParent = $currentTr.parent();\n var $trs = $trParent.children();\n\n // 遍历所有行\n $trs.forEach(function (tr) {\n var $tr = $(tr);\n var $tds = $tr.children();\n var $currentTd = $tds.get(tdIndex);\n // 删除\n $currentTd.remove();\n });\n },\n\n // 删除表格\n _delTable: function _delTable() {\n var editor = this.editor;\n var $selectionELem = editor.selection.getSelectionContainerElem();\n if (!$selectionELem) {\n return;\n }\n var $table = $selectionELem.parentUntil('table');\n if (!$table) {\n return;\n }\n $table.remove();\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n var $selectionELem = editor.selection.getSelectionContainerElem();\n if (!$selectionELem) {\n return;\n }\n var nodeName = $selectionELem.getNodeName();\n if (nodeName === 'TD' || nodeName === 'TH') {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n menu - video\n*/\n// 构造函数\nfunction Video(editor) {\n this.editor = editor;\n this.$elem = $('
          ');\n this.type = 'panel';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nVideo.prototype = {\n constructor: Video,\n\n onClick: function onClick() {\n this._createPanel();\n },\n\n _createPanel: function _createPanel() {\n var _this = this;\n\n // 创建 id\n var textValId = getRandom('text-val');\n var btnId = getRandom('btn');\n\n // 创建 panel\n var panel = new Panel(this, {\n width: 350,\n // 一个 panel 多个 tab\n tabs: [{\n // 标题\n title: '插入视频',\n // 模板\n tpl: '
          \\n \"/>\\n
          \\n \\n
          \\n
          ',\n // 事件绑定\n events: [{\n selector: '#' + btnId,\n type: 'click',\n fn: function fn() {\n var $text = $('#' + textValId);\n var val = $text.val().trim();\n\n // 测试用视频地址\n // \n\n if (val) {\n // 插入视频\n _this._insert(val);\n }\n\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }]\n } // first tab end\n ] // tabs end\n }); // panel end\n\n // 显示 panel\n panel.show();\n\n // 记录属性\n this.panel = panel;\n },\n\n // 插入视频\n _insert: function _insert(val) {\n var editor = this.editor;\n editor.cmd.do('insertHTML', val + '


          ');\n }\n};\n\n/*\n menu - img\n*/\n// 构造函数\nfunction Image(editor) {\n this.editor = editor;\n var imgMenuId = getRandom('w-e-img');\n this.$elem = $('
          ');\n editor.imgMenuId = imgMenuId;\n this.type = 'panel';\n\n // 当前是否 active 状态\n this._active = false;\n}\n\n// 原型\nImage.prototype = {\n constructor: Image,\n\n onClick: function onClick() {\n var editor = this.editor;\n var config = editor.config;\n if (config.qiniu) {\n return;\n }\n if (this._active) {\n this._createEditPanel();\n } else {\n this._createInsertPanel();\n }\n },\n\n _createEditPanel: function _createEditPanel() {\n var editor = this.editor;\n\n // id\n var width30 = getRandom('width-30');\n var width50 = getRandom('width-50');\n var width100 = getRandom('width-100');\n var delBtn = getRandom('del-btn');\n\n // tab 配置\n var tabsConfig = [{\n title: '编辑图片',\n tpl: '
          \\n
          \\n \\u6700\\u5927\\u5BBD\\u5EA6\\uFF1A\\n \\n \\n \\n
          \\n
          \\n \\n \\n
          ',\n events: [{\n selector: '#' + width30,\n type: 'click',\n fn: function fn() {\n var $img = editor._selectedImg;\n if ($img) {\n $img.css('max-width', '30%');\n }\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }, {\n selector: '#' + width50,\n type: 'click',\n fn: function fn() {\n var $img = editor._selectedImg;\n if ($img) {\n $img.css('max-width', '50%');\n }\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }, {\n selector: '#' + width100,\n type: 'click',\n fn: function fn() {\n var $img = editor._selectedImg;\n if ($img) {\n $img.css('max-width', '100%');\n }\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }, {\n selector: '#' + delBtn,\n type: 'click',\n fn: function fn() {\n var $img = editor._selectedImg;\n if ($img) {\n $img.remove();\n }\n // 返回 true,表示该事件执行完之后,panel 要关闭。否则 panel 不会关闭\n return true;\n }\n }]\n }];\n\n // 创建 panel 并显示\n var panel = new Panel(this, {\n width: 300,\n tabs: tabsConfig\n });\n panel.show();\n\n // 记录属性\n this.panel = panel;\n },\n\n _createInsertPanel: function _createInsertPanel() {\n var editor = this.editor;\n var uploadImg = editor.uploadImg;\n var config = editor.config;\n\n // id\n var upTriggerId = getRandom('up-trigger');\n var upFileId = getRandom('up-file');\n var linkUrlId = getRandom('link-url');\n var linkBtnId = getRandom('link-btn');\n\n // tabs 的配置\n var tabsConfig = [{\n title: '上传图片',\n tpl: '
          \\n
          \\n \\n
          \\n
          \\n \\n
          \\n
          ',\n events: [{\n // 触发选择图片\n selector: '#' + upTriggerId,\n type: 'click',\n fn: function fn() {\n var $file = $('#' + upFileId);\n var fileElem = $file[0];\n if (fileElem) {\n fileElem.click();\n } else {\n // 返回 true 可关闭 panel\n return true;\n }\n }\n }, {\n // 选择图片完毕\n selector: '#' + upFileId,\n type: 'change',\n fn: function fn() {\n var $file = $('#' + upFileId);\n var fileElem = $file[0];\n if (!fileElem) {\n // 返回 true 可关闭 panel\n return true;\n }\n\n // 获取选中的 file 对象列表\n var fileList = fileElem.files;\n if (fileList.length) {\n uploadImg.uploadImg(fileList);\n }\n\n // 返回 true 可关闭 panel\n return true;\n }\n }]\n }, // first tab end\n {\n title: '网络图片',\n tpl: '
          \\n \\n
          \\n \\n
          \\n
          ',\n events: [{\n selector: '#' + linkBtnId,\n type: 'click',\n fn: function fn() {\n var $linkUrl = $('#' + linkUrlId);\n var url = $linkUrl.val().trim();\n\n if (url) {\n uploadImg.insertLinkImg(url);\n }\n\n // 返回 true 表示函数执行结束之后关闭 panel\n return true;\n }\n }]\n } // second tab end\n ]; // tabs end\n\n // 判断 tabs 的显示\n var tabsConfigResult = [];\n if ((config.uploadImgShowBase64 || config.uploadImgServer || config.customUploadImg) && window.FileReader) {\n // 显示“上传图片”\n tabsConfigResult.push(tabsConfig[0]);\n }\n if (config.showLinkImg) {\n // 显示“网络图片”\n tabsConfigResult.push(tabsConfig[1]);\n }\n\n // 创建 panel 并显示\n var panel = new Panel(this, {\n width: 300,\n tabs: tabsConfigResult\n });\n panel.show();\n\n // 记录属性\n this.panel = panel;\n },\n\n // 试图改变 active 状态\n tryChangeActive: function tryChangeActive(e) {\n var editor = this.editor;\n var $elem = this.$elem;\n if (editor._selectedImg) {\n this._active = true;\n $elem.addClass('w-e-active');\n } else {\n this._active = false;\n $elem.removeClass('w-e-active');\n }\n }\n};\n\n/*\n 所有菜单的汇总\n*/\n\n// 存储菜单的构造函数\nvar MenuConstructors = {};\n\nMenuConstructors.bold = Bold;\n\nMenuConstructors.head = Head;\n\nMenuConstructors.fontSize = FontSize;\n\nMenuConstructors.fontName = FontName;\n\nMenuConstructors.link = Link;\n\nMenuConstructors.italic = Italic;\n\nMenuConstructors.redo = Redo;\n\nMenuConstructors.strikeThrough = StrikeThrough;\n\nMenuConstructors.underline = Underline;\n\nMenuConstructors.undo = Undo;\n\nMenuConstructors.list = List;\n\nMenuConstructors.justify = Justify;\n\nMenuConstructors.foreColor = ForeColor;\n\nMenuConstructors.backColor = BackColor;\n\nMenuConstructors.quote = Quote;\n\nMenuConstructors.code = Code;\n\nMenuConstructors.emoticon = Emoticon;\n\nMenuConstructors.table = Table;\n\nMenuConstructors.video = Video;\n\nMenuConstructors.image = Image;\n\n/*\n 菜单集合\n*/\n// 构造函数\nfunction Menus(editor) {\n this.editor = editor;\n this.menus = {};\n}\n\n// 修改原型\nMenus.prototype = {\n constructor: Menus,\n\n // 初始化菜单\n init: function init() {\n var _this = this;\n\n var editor = this.editor;\n var config = editor.config || {};\n var configMenus = config.menus || []; // 获取配置中的菜单\n\n // 根据配置信息,创建菜单\n configMenus.forEach(function (menuKey) {\n var MenuConstructor = MenuConstructors[menuKey];\n if (MenuConstructor && typeof MenuConstructor === 'function') {\n // 创建单个菜单\n _this.menus[menuKey] = new MenuConstructor(editor);\n }\n });\n\n // 添加到菜单栏\n this._addToToolbar();\n\n // 绑定事件\n this._bindEvent();\n },\n\n // 添加到菜单栏\n _addToToolbar: function _addToToolbar() {\n var editor = this.editor;\n var $toolbarElem = editor.$toolbarElem;\n var menus = this.menus;\n var config = editor.config;\n // config.zIndex 是配置的编辑区域的 z-index,菜单的 z-index 得在其基础上 +1\n var zIndex = config.zIndex + 1;\n objForEach(menus, function (key, menu) {\n var $elem = menu.$elem;\n if ($elem) {\n // 设置 z-index\n $elem.css('z-index', zIndex);\n $toolbarElem.append($elem);\n }\n });\n },\n\n // 绑定菜单 click mouseenter 事件\n _bindEvent: function _bindEvent() {\n var menus = this.menus;\n var editor = this.editor;\n objForEach(menus, function (key, menu) {\n var type = menu.type;\n if (!type) {\n return;\n }\n var $elem = menu.$elem;\n var droplist = menu.droplist;\n var panel = menu.panel;\n\n // 点击类型,例如 bold\n if (type === 'click' && menu.onClick) {\n $elem.on('click', function (e) {\n if (editor.selection.getRange() == null) {\n return;\n }\n menu.onClick(e);\n });\n }\n\n // 下拉框,例如 head\n if (type === 'droplist' && droplist) {\n $elem.on('mouseenter', function (e) {\n if (editor.selection.getRange() == null) {\n return;\n }\n // 显示\n droplist.showTimeoutId = setTimeout(function () {\n droplist.show();\n }, 200);\n }).on('mouseleave', function (e) {\n // 隐藏\n droplist.hideTimeoutId = setTimeout(function () {\n droplist.hide();\n }, 0);\n });\n }\n\n // 弹框类型,例如 link\n if (type === 'panel' && menu.onClick) {\n $elem.on('click', function (e) {\n e.stopPropagation();\n if (editor.selection.getRange() == null) {\n return;\n }\n // 在自定义事件中显示 panel\n menu.onClick(e);\n });\n }\n });\n },\n\n // 尝试修改菜单状态\n changeActive: function changeActive() {\n var menus = this.menus;\n objForEach(menus, function (key, menu) {\n if (menu.tryChangeActive) {\n setTimeout(function () {\n menu.tryChangeActive();\n }, 100);\n }\n });\n }\n};\n\n/*\n 粘贴信息的处理\n*/\n\n// 获取粘贴的纯文本\nfunction getPasteText(e) {\n var clipboardData = e.clipboardData || e.originalEvent && e.originalEvent.clipboardData;\n var pasteText = void 0;\n if (clipboardData == null) {\n pasteText = window.clipboardData && window.clipboardData.getData('text');\n } else {\n pasteText = clipboardData.getData('text/plain');\n }\n\n return replaceHtmlSymbol(pasteText);\n}\n\n// 获取粘贴的html\nfunction getPasteHtml(e, filterStyle, ignoreImg) {\n var clipboardData = e.clipboardData || e.originalEvent && e.originalEvent.clipboardData;\n var pasteText = void 0,\n pasteHtml = void 0;\n if (clipboardData == null) {\n pasteText = window.clipboardData && window.clipboardData.getData('text');\n } else {\n pasteText = clipboardData.getData('text/plain');\n pasteHtml = clipboardData.getData('text/html');\n }\n if (!pasteHtml && pasteText) {\n pasteHtml = '

          ' + replaceHtmlSymbol(pasteText) + '

          ';\n }\n if (!pasteHtml) {\n return;\n }\n\n // 过滤word中状态过来的无用字符\n var docSplitHtml = pasteHtml.split('');\n if (docSplitHtml.length === 2) {\n pasteHtml = docSplitHtml[0];\n }\n\n // 过滤无用标签\n pasteHtml = pasteHtml.replace(/<(meta|script|link).+?>/igm, '');\n // 去掉注释\n pasteHtml = pasteHtml.replace(//mg, '');\n // 过滤 data-xxx 属性\n pasteHtml = pasteHtml.replace(/\\s?data-.+?=('|\").+?('|\")/igm, '');\n\n if (ignoreImg) {\n // 忽略图片\n pasteHtml = pasteHtml.replace(//igm, '');\n }\n\n if (filterStyle) {\n // 过滤样式\n pasteHtml = pasteHtml.replace(/\\s?(class|style)=('|\").*?('|\")/igm, '');\n } else {\n // 保留样式\n pasteHtml = pasteHtml.replace(/\\s?class=('|\").*?('|\")/igm, '');\n }\n\n return pasteHtml;\n}\n\n// 获取粘贴的图片文件\nfunction getPasteImgs(e) {\n var result = [];\n var txt = getPasteText(e);\n if (txt) {\n // 有文字,就忽略图片\n return result;\n }\n\n var clipboardData = e.clipboardData || e.originalEvent && e.originalEvent.clipboardData || {};\n var items = clipboardData.items;\n if (!items) {\n return result;\n }\n\n objForEach(items, function (key, value) {\n var type = value.type;\n if (/image/i.test(type)) {\n result.push(value.getAsFile());\n }\n });\n\n return result;\n}\n\n/*\n 编辑区域\n*/\n\n// 获取一个 elem.childNodes 的 JSON 数据\nfunction getChildrenJSON($elem) {\n var result = [];\n var $children = $elem.childNodes() || []; // 注意 childNodes() 可以获取文本节点\n $children.forEach(function (curElem) {\n var elemResult = void 0;\n var nodeType = curElem.nodeType;\n\n // 文本节点\n if (nodeType === 3) {\n elemResult = curElem.textContent;\n elemResult = replaceHtmlSymbol(elemResult);\n }\n\n // 普通 DOM 节点\n if (nodeType === 1) {\n elemResult = {};\n\n // tag\n elemResult.tag = curElem.nodeName.toLowerCase();\n // attr\n var attrData = [];\n var attrList = curElem.attributes || {};\n var attrListLength = attrList.length || 0;\n for (var i = 0; i < attrListLength; i++) {\n var attr = attrList[i];\n attrData.push({\n name: attr.name,\n value: attr.value\n });\n }\n elemResult.attrs = attrData;\n // children(递归)\n elemResult.children = getChildrenJSON($(curElem));\n }\n\n result.push(elemResult);\n });\n return result;\n}\n\n// 构造函数\nfunction Text(editor) {\n this.editor = editor;\n}\n\n// 修改原型\nText.prototype = {\n constructor: Text,\n\n // 初始化\n init: function init() {\n // 绑定事件\n this._bindEvent();\n },\n\n // 清空内容\n clear: function clear() {\n this.html('


          ');\n },\n\n // 获取 设置 html\n html: function html(val) {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n var html = void 0;\n if (val == null) {\n html = $textElem.html();\n // 未选中任何内容的时候点击“加粗”或者“斜体”等按钮,就得需要一个空的占位符 ​ ,这里替换掉\n html = html.replace(/\\u200b/gm, '');\n return html;\n } else {\n $textElem.html(val);\n\n // 初始化选取,将光标定位到内容尾部\n editor.initSelection();\n }\n },\n\n // 获取 JSON\n getJSON: function getJSON() {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n return getChildrenJSON($textElem);\n },\n\n // 获取 设置 text\n text: function text(val) {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n var text = void 0;\n if (val == null) {\n text = $textElem.text();\n // 未选中任何内容的时候点击“加粗”或者“斜体”等按钮,就得需要一个空的占位符 ​ ,这里替换掉\n text = text.replace(/\\u200b/gm, '');\n return text;\n } else {\n $textElem.text('

          ' + val + '

          ');\n\n // 初始化选取,将光标定位到内容尾部\n editor.initSelection();\n }\n },\n\n // 追加内容\n append: function append(html) {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n $textElem.append($(html));\n\n // 初始化选取,将光标定位到内容尾部\n editor.initSelection();\n },\n\n // 绑定事件\n _bindEvent: function _bindEvent() {\n // 实时保存选取\n this._saveRangeRealTime();\n\n // 按回车建时的特殊处理\n this._enterKeyHandle();\n\n // 清空时保留


          \n this._clearHandle();\n\n // 粘贴事件(粘贴文字,粘贴图片)\n this._pasteHandle();\n\n // tab 特殊处理\n this._tabHandle();\n\n // img 点击\n this._imgHandle();\n\n // 拖拽事件\n this._dragHandle();\n },\n\n // 实时保存选取\n _saveRangeRealTime: function _saveRangeRealTime() {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n\n // 保存当前的选区\n function saveRange(e) {\n // 随时保存选区\n editor.selection.saveRange();\n // 更新按钮 ative 状态\n editor.menus.changeActive();\n }\n // 按键后保存\n $textElem.on('keyup', saveRange);\n $textElem.on('mousedown', function (e) {\n // mousedown 状态下,鼠标滑动到编辑区域外面,也需要保存选区\n $textElem.on('mouseleave', saveRange);\n });\n $textElem.on('mouseup', function (e) {\n saveRange();\n // 在编辑器区域之内完成点击,取消鼠标滑动到编辑区外面的事件\n $textElem.off('mouseleave', saveRange);\n });\n },\n\n // 按回车键时的特殊处理\n _enterKeyHandle: function _enterKeyHandle() {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n\n function insertEmptyP($selectionElem) {\n var $p = $('


          ');\n $p.insertBefore($selectionElem);\n editor.selection.createRangeByElem($p, true);\n editor.selection.restoreSelection();\n $selectionElem.remove();\n }\n\n // 将回车之后生成的非

          的顶级标签,改为

          \n function pHandle(e) {\n var $selectionElem = editor.selection.getSelectionContainerElem();\n var $parentElem = $selectionElem.parent();\n\n if ($parentElem.html() === '
          ') {\n // 回车之前光标所在一个

          .....

          ,忽然回车生成一个空的


          \n // 而且继续回车跳不出去,因此只能特殊处理\n insertEmptyP($selectionElem);\n return;\n }\n\n if (!$parentElem.equal($textElem)) {\n // 不是顶级标签\n return;\n }\n\n var nodeName = $selectionElem.getNodeName();\n if (nodeName === 'P') {\n // 当前的标签是 P ,不用做处理\n return;\n }\n\n if ($selectionElem.text()) {\n // 有内容,不做处理\n return;\n }\n\n // 插入

          ,并将选取定位到

          ,删除当前标签\n insertEmptyP($selectionElem);\n }\n\n $textElem.on('keyup', function (e) {\n if (e.keyCode !== 13) {\n // 不是回车键\n return;\n }\n // 将回车之后生成的非

          的顶级标签,改为

          \n pHandle(e);\n });\n\n //

          回车时 特殊处理\n function codeHandle(e) {\n var $selectionElem = editor.selection.getSelectionContainerElem();\n if (!$selectionElem) {\n return;\n }\n var $parentElem = $selectionElem.parent();\n var selectionNodeName = $selectionElem.getNodeName();\n var parentNodeName = $parentElem.getNodeName();\n\n if (selectionNodeName !== 'CODE' || parentNodeName !== 'PRE') {\n // 不符合要求 忽略\n return;\n }\n\n if (!editor.cmd.queryCommandSupported('insertHTML')) {\n // 必须原生支持 insertHTML 命令\n return;\n }\n\n // 处理:光标定位到代码末尾,联系点击两次回车,即跳出代码块\n if (editor._willBreakCode === true) {\n // 此时可以跳出代码块\n // 插入

          ,并将选取定位到

          \n var $p = $('


          ');\n $p.insertAfter($parentElem);\n editor.selection.createRangeByElem($p, true);\n editor.selection.restoreSelection();\n\n // 修改状态\n editor._willBreakCode = false;\n\n e.preventDefault();\n return;\n }\n\n var _startOffset = editor.selection.getRange().startOffset;\n\n // 处理:回车时,不能插入
          而是插入 \\n ,因为是在 pre 标签里面\n editor.cmd.do('insertHTML', '\\n');\n editor.selection.saveRange();\n if (editor.selection.getRange().startOffset === _startOffset) {\n // 没起作用,再来一遍\n editor.cmd.do('insertHTML', '\\n');\n }\n\n var codeLength = $selectionElem.html().length;\n if (editor.selection.getRange().startOffset + 1 === codeLength) {\n // 说明光标在代码最后的位置,执行了回车操作\n // 记录下来,以便下次回车时候跳出 code\n editor._willBreakCode = true;\n }\n\n // 阻止默认行为\n e.preventDefault();\n }\n\n $textElem.on('keydown', function (e) {\n if (e.keyCode !== 13) {\n // 不是回车键\n // 取消即将跳转代码块的记录\n editor._willBreakCode = false;\n return;\n }\n //
          回车时 特殊处理\n codeHandle(e);\n });\n },\n\n // 清空时保留


          \n _clearHandle: function _clearHandle() {\n var editor = this.editor;\n var $textElem = editor.$textElem;\n\n $textElem.on('keydown', function (e) {\n if (e.keyCode !== 8) {\n return;\n }\n var txtHtml = $textElem.html().toLowerCase().trim();\n if (txtHtml === '


          ') {\n // 最后剩下一个空行,就不再删除了\n e.preventDefault();\n return;\n }\n });\n\n $textElem.on('keyup', function (e) {\n if (e.keyCode !== 8) {\n return;\n }\n var $p = void 0;\n var txtHtml = $textElem.html().toLowerCase().trim();\n\n // firefox 时用 txtHtml === '
          ' 判断,其他用 !txtHtml 判断\n if (!txtHtml || txtHtml === '
          ') {\n // 内容空了\n $p = $('


          ');\n $textElem.html(''); // 一定要先清空,否则在 firefox 下有问题\n $textElem.append($p);\n editor.selection.createRangeByElem($p, false, true);\n editor.selection.restoreSelection();\n }\n });\n },\n\n // 粘贴事件(粘贴文字 粘贴图片)\n _pasteHandle: function _pasteHandle() {\n var editor = this.editor;\n var config = editor.config;\n var pasteFilterStyle = config.pasteFilterStyle;\n var pasteTextHandle = config.pasteTextHandle;\n var ignoreImg = config.pasteIgnoreImg;\n var $textElem = editor.$textElem;\n\n // 粘贴图片、文本的事件,每次只能执行一个\n // 判断该次粘贴事件是否可以执行\n var pasteTime = 0;\n function canDo() {\n var now = Date.now();\n var flag = false;\n if (now - pasteTime >= 100) {\n // 间隔大于 100 ms ,可以执行\n flag = true;\n }\n pasteTime = now;\n return flag;\n }\n function resetTime() {\n pasteTime = 0;\n }\n\n // 粘贴文字\n $textElem.on('paste', function (e) {\n if (UA.isIE()) {\n return;\n } else {\n // 阻止默认行为,使用 execCommand 的粘贴命令\n e.preventDefault();\n }\n\n // 粘贴图片和文本,只能同时使用一个\n if (!canDo()) {\n return;\n }\n\n // 获取粘贴的文字\n var pasteHtml = getPasteHtml(e, pasteFilterStyle, ignoreImg);\n var pasteText = getPasteText(e);\n pasteText = pasteText.replace(/\\n/gm, '
          ');\n\n var $selectionElem = editor.selection.getSelectionContainerElem();\n if (!$selectionElem) {\n return;\n }\n var nodeName = $selectionElem.getNodeName();\n\n // code 中只能粘贴纯文本\n if (nodeName === 'CODE' || nodeName === 'PRE') {\n if (pasteTextHandle && isFunction(pasteTextHandle)) {\n // 用户自定义过滤处理粘贴内容\n pasteText = '' + (pasteTextHandle(pasteText) || '');\n }\n editor.cmd.do('insertHTML', '

          ' + pasteText + '

          ');\n return;\n }\n\n // 先放开注释,有问题再追查 ————\n // // 表格中忽略,可能会出现异常问题\n // if (nodeName === 'TD' || nodeName === 'TH') {\n // return\n // }\n\n if (!pasteHtml) {\n // 没有内容,可继续执行下面的图片粘贴\n resetTime();\n return;\n }\n try {\n // firefox 中,获取的 pasteHtml 可能是没有