diff --git a/src/Business/Grand.Business.Authentication/Services/TwoFactorAuthenticationService.cs b/src/Business/Grand.Business.Authentication/Services/TwoFactorAuthenticationService.cs index 85b04ab6ae..19826e36f0 100644 --- a/src/Business/Grand.Business.Authentication/Services/TwoFactorAuthenticationService.cs +++ b/src/Business/Grand.Business.Authentication/Services/TwoFactorAuthenticationService.cs @@ -5,7 +5,7 @@ using Grand.Domain.Common; using Grand.Domain.Customers; using Grand.Domain.Localization; -using Grand.Infrastructure; +using Grand.Domain.Stores; namespace Grand.Business.Authentication.Services; @@ -14,14 +14,11 @@ public class TwoFactorAuthenticationService : ITwoFactorAuthenticationService private readonly IEnumerable _smsVerificationService; private readonly TwoFactorAuthenticator _twoFactorAuthentication; private readonly ICustomerService _customerService; - private readonly IContextAccessor _contextAccessor; public TwoFactorAuthenticationService( - IContextAccessor contextAccessor, ICustomerService customerService, IEnumerable smsVerificationService) { - _contextAccessor = contextAccessor; _customerService = customerService; _smsVerificationService = smsVerificationService; _twoFactorAuthentication = new TwoFactorAuthenticator(); @@ -55,14 +52,16 @@ public virtual async Task AuthenticateTwoFactor(string secretKey, string t } public virtual async Task GenerateCodeSetup(string secretKey, Customer customer, - Language language, TwoFactorAuthenticationType twoFactorAuthenticationType) + Store store, Language language, TwoFactorAuthenticationType twoFactorAuthenticationType) { + ArgumentNullException.ThrowIfNull(store); + var model = new TwoFactorCodeSetup(); switch (twoFactorAuthenticationType) { case TwoFactorAuthenticationType.AppVerification: - var setupInfo = _twoFactorAuthentication.GenerateSetupCode(_contextAccessor.StoreContext.CurrentStore.CompanyName, + var setupInfo = _twoFactorAuthentication.GenerateSetupCode(store.CompanyName, customer.Email, secretKey, false); model.CustomValues.Add("QrCodeImageUrl", setupInfo.QrCodeSetupImageUrl); model.CustomValues.Add("ManualEntryQrCode", setupInfo.ManualEntryKey); diff --git a/src/Business/Grand.Business.Cms/Services/KnowledgebaseService.cs b/src/Business/Grand.Business.Cms/Services/KnowledgebaseService.cs index 4a1a3cad8d..72e3d9dc05 100644 --- a/src/Business/Grand.Business.Cms/Services/KnowledgebaseService.cs +++ b/src/Business/Grand.Business.Cms/Services/KnowledgebaseService.cs @@ -1,11 +1,11 @@ using Grand.Business.Core.Interfaces.Cms; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain; using Grand.Domain.Customers; using Grand.Domain.Knowledgebase; using Grand.Domain.Permissions; using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -22,7 +22,6 @@ public class KnowledgebaseService : IKnowledgebaseService private readonly IRepository _knowledgebaseArticleRepository; private readonly IRepository _knowledgebaseCategoryRepository; private readonly IMediator _mediator; - private readonly IContextAccessor _contextAccessor; /// /// Ctor @@ -32,35 +31,36 @@ public KnowledgebaseService IRepository knowledgebaseArticleRepository, IRepository articleCommentRepository, IMediator mediator, - IContextAccessor contextAccessor, ICacheBase cacheBase, AccessControlConfig accessControlConfig) { _knowledgebaseCategoryRepository = knowledgebaseCategoryRepository; _knowledgebaseArticleRepository = knowledgebaseArticleRepository; _articleCommentRepository = articleCommentRepository; _mediator = mediator; - _contextAccessor = contextAccessor; _cacheBase = cacheBase; _accessControlConfig = accessControlConfig; } - private IQueryable ApplyStandardFilter(IQueryable query) where T : BaseEntity, IGroupLinkEntity, IStoreLinkEntity + private IQueryable ApplyStandardFilter(IQueryable query, CatalogAccess access) + where T : BaseEntity, IGroupLinkEntity, IStoreLinkEntity { - return ApplyStandardFilter(query, _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(), - _contextAccessor.StoreContext.CurrentStore.Id); + ArgumentNullException.ThrowIfNull(access); + return ApplyStandardFilter(query, access.ShowHidden ? null : access.Customer.GetCustomerGroupIds(), + access.StoreId); } + //null group ids skip the group filter, an empty store id the store filter private IQueryable ApplyStandardFilter(IQueryable query, string[] allowedCustomerGroupsIds, string storeId) where T : BaseEntity, IGroupLinkEntity, IStoreLinkEntity { - if (!_accessControlConfig.IgnoreAcl) + if (allowedCustomerGroupsIds != null && !_accessControlConfig.IgnoreAcl) { query = from p in query where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) select p; } - if (!_accessControlConfig.IgnoreStoreLimitations) + if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) { query = from p in query where !p.LimitedToStores || p.Stores.Contains(storeId) @@ -70,13 +70,14 @@ private IQueryable ApplyStandardFilter(IQueryable query, string[] allow return query; } - private string GetCacheKey(string format, params object[] additionalParams) + private string GetCacheKey(string format, CatalogAccess access, params object[] additionalParams) { - var standardParams = new[] - { - string.Join(",", _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds()), - _contextAccessor.StoreContext.CurrentStore.Id - }; + ArgumentNullException.ThrowIfNull(access); + //groups only matter with the ACL on; "*" keeps reads including hidden records apart from a customer with no groups + var groupIds = _accessControlConfig.IgnoreAcl ? "" + : access.ShowHidden ? "*" + : string.Join(",", access.Customer.GetCustomerGroupIds()); + var standardParams = new[] { groupIds, access.StoreId }; var allParams = additionalParams != null ? standardParams.Concat(additionalParams).ToArray() @@ -132,9 +133,9 @@ public virtual async Task GetKnowledgebaseCategory(string /// /// /// knowledge base category - public virtual async Task GetPublicKnowledgebaseCategory(string id) + public virtual async Task GetPublicKnowledgebaseCategory(string id, CatalogAccess access) { - var key = GetCacheKey(CacheKey.KNOWLEDGEBASE_CATEGORY_BY_ID, id); + var key = GetCacheKey(CacheKey.KNOWLEDGEBASE_CATEGORY_BY_ID, access, id); return await _cacheBase.GetAsync(key, async () => { @@ -143,7 +144,7 @@ public virtual async Task GetPublicKnowledgebaseCategory( query = query.Where(x => x.Published); query = query.Where(x => x.Id == id); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); var toReturn = await _knowledgebaseCategoryRepository.FirstOrDefaultAsync(query); return toReturn; }); @@ -185,15 +186,13 @@ public virtual Task GetKnowledgebaseArticle(string id) } /// - /// Gets knowledge base articles + /// Gets every knowledge base article, unpublished and group- or store-limited ones included /// /// List of knowledge base articles - /// Store ident - public virtual async Task> GetKnowledgebaseArticles(string storeId = "") + public virtual async Task> GetKnowledgebaseArticles() { var query = from p in _knowledgebaseArticleRepository.Table select p; - query = ApplyStandardFilter(query); query = query.OrderBy(x => x.DisplayOrder); return (await _knowledgebaseArticleRepository.ToListAsync(query)).ToList(); } @@ -253,9 +252,9 @@ public virtual async Task> GetKnowledgebaseArti /// Gets public(published etc) knowledge base categories /// /// List of public knowledgebase categories - public virtual async Task> GetPublicKnowledgebaseCategories() + public virtual async Task> GetPublicKnowledgebaseCategories(CatalogAccess access) { - var key = GetCacheKey(CacheKey.KNOWLEDGEBASE_CATEGORIES); + var key = GetCacheKey(CacheKey.KNOWLEDGEBASE_CATEGORIES, access); return await _cacheBase.GetAsync(key, async () => { @@ -264,7 +263,7 @@ public virtual async Task> GetPublicKnowledgebaseCat query = query.Where(x => x.Published); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); query = query.OrderBy(x => x.DisplayOrder); return (await _knowledgebaseCategoryRepository.ToListAsync(query)).ToList(); }); @@ -274,9 +273,9 @@ public virtual async Task> GetPublicKnowledgebaseCat /// Gets public(published etc) knowledge base articles /// /// List of public knowledge base articles - public virtual async Task> GetPublicKnowledgebaseArticles() + public virtual async Task> GetPublicKnowledgebaseArticles(CatalogAccess access) { - var key = GetCacheKey(CacheKey.ARTICLES); + var key = GetCacheKey(CacheKey.ARTICLES, access); return await _cacheBase.GetAsync(key, async () => { @@ -284,7 +283,7 @@ public virtual async Task> GetPublicKnowledgebaseArti select p; query = query.Where(x => x.Published); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); query = query.OrderBy(x => x.DisplayOrder); return (await _knowledgebaseArticleRepository.ToListAsync(query)).ToList(); @@ -314,9 +313,9 @@ public virtual async Task> GetPublicKnowledgebaseArti /// Gets knowledge base article if it is published etc /// /// knowledge base article - public virtual async Task GetPublicKnowledgebaseArticle(string id) + public virtual async Task GetPublicKnowledgebaseArticle(string id, CatalogAccess access) { - var key = GetCacheKey(CacheKey.ARTICLE_BY_ID, id); + var key = GetCacheKey(CacheKey.ARTICLE_BY_ID, access, id); return await _cacheBase.GetAsync(key, async () => { @@ -325,7 +324,7 @@ public virtual async Task GetPublicKnowledgebaseArticle(st query = query.Where(x => x.Published); query = query.Where(x => x.Id == id); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); return await _knowledgebaseArticleRepository.FirstOrDefaultAsync(query); }); } @@ -334,9 +333,10 @@ public virtual async Task GetPublicKnowledgebaseArticle(st /// Gets public(published etc) knowledge base articles for category id /// /// List of public knowledge base articles - public virtual async Task> GetPublicKnowledgebaseArticlesByCategory(string categoryId) + public virtual async Task> GetPublicKnowledgebaseArticlesByCategory(string categoryId, + CatalogAccess access) { - var key = GetCacheKey(CacheKey.ARTICLES_BY_CATEGORY_ID, categoryId); + var key = GetCacheKey(CacheKey.ARTICLES_BY_CATEGORY_ID, access, categoryId); return await _cacheBase.GetAsync(key, async () => { @@ -346,7 +346,7 @@ public virtual async Task> GetPublicKnowledgebaseArti query = query.Where(x => x.Published); query = query.Where(x => x.ParentCategoryId == categoryId); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); query = query.OrderBy(x => x.DisplayOrder); return (await _knowledgebaseArticleRepository.ToListAsync(query)).ToList(); }); @@ -356,9 +356,10 @@ public virtual async Task> GetPublicKnowledgebaseArti /// Gets public(published etc) knowledge base articles for keyword /// /// List of public knowledge base articles - public virtual async Task> GetPublicKnowledgebaseArticlesByKeyword(string keyword) + public virtual async Task> GetPublicKnowledgebaseArticlesByKeyword(string keyword, + CatalogAccess access) { - var key = GetCacheKey(CacheKey.ARTICLES_BY_KEYWORD, keyword); + var key = GetCacheKey(CacheKey.ARTICLES_BY_KEYWORD, access, keyword); return await _cacheBase.GetAsync(key, async () => { @@ -371,7 +372,7 @@ public virtual async Task> GetPublicKnowledgebaseArti p.Locales.Any(x => x.LocaleValue != null && x.LocaleValue.ToLower().Contains(keyword.ToLower())) || p.Name.ToLower().Contains(keyword.ToLower()) || p.Content.ToLower().Contains(keyword.ToLower())); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); query = query.OrderBy(x => x.DisplayOrder); return (await _knowledgebaseArticleRepository.ToListAsync(query)).ToList(); }); @@ -381,9 +382,10 @@ public virtual async Task> GetPublicKnowledgebaseArti /// Gets public(published etc) knowledge base categories for keyword /// /// List of public knowledge base categories - public virtual async Task> GetPublicKnowledgebaseCategoriesByKeyword(string keyword) + public virtual async Task> GetPublicKnowledgebaseCategoriesByKeyword(string keyword, + CatalogAccess access) { - var key = GetCacheKey(CacheKey.KNOWLEDGEBASE_CATEGORIES_BY_KEYWORD, keyword); + var key = GetCacheKey(CacheKey.KNOWLEDGEBASE_CATEGORIES_BY_KEYWORD, access, keyword); return await _cacheBase.GetAsync(key, async () => { @@ -396,7 +398,7 @@ public virtual async Task> GetPublicKnowledgebaseCat p.Locales.Any(x => x.LocaleValue != null && x.LocaleValue.ToLower().Contains(keyword.ToLower())) || p.Name.ToLower().Contains(keyword.ToLower()) || p.Description.ToLower().Contains(keyword.ToLower())); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); query = query.OrderBy(x => x.DisplayOrder); return (await _knowledgebaseCategoryRepository.ToListAsync(query)).ToList(); }); @@ -406,9 +408,9 @@ public virtual async Task> GetPublicKnowledgebaseCat /// Gets homepage knowledge base articles /// /// List of homepage knowledge base articles - public virtual async Task> GetHomepageKnowledgebaseArticles() + public virtual async Task> GetHomepageKnowledgebaseArticles(CatalogAccess access) { - var key = GetCacheKey(CacheKey.HOMEPAGE_ARTICLES); + var key = GetCacheKey(CacheKey.HOMEPAGE_ARTICLES, access); return await _cacheBase.GetAsync(key, async () => { @@ -417,7 +419,7 @@ public virtual async Task> GetHomepageKnowledgebaseAr query = query.Where(x => x.Published); query = query.Where(x => x.ShowOnHomepage); - query = ApplyStandardFilter(query); + query = ApplyStandardFilter(query, access); query = query.OrderBy(x => x.DisplayOrder); return (await _knowledgebaseArticleRepository.ToListAsync(query)).ToList(); }); diff --git a/src/Business/Grand.Business.Cms/Services/NewsService.cs b/src/Business/Grand.Business.Cms/Services/NewsService.cs index 09ab397bd5..028b5cb466 100644 --- a/src/Business/Grand.Business.Cms/Services/NewsService.cs +++ b/src/Business/Grand.Business.Cms/Services/NewsService.cs @@ -1,9 +1,9 @@ using Grand.Business.Core.Interfaces.Cms; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain; using Grand.Domain.Customers; using Grand.Domain.News; -using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Extensions; using Grand.Mediator; @@ -19,11 +19,10 @@ public class NewsService : INewsService public NewsService(IRepository newsItemRepository, IMediator mediator, - IContextAccessor contextAccessor, AccessControlConfig accessControlConfig) + AccessControlConfig accessControlConfig) { _newsItemRepository = newsItemRepository; _mediator = mediator; - _contextAccessor = contextAccessor; _accessControlConfig = accessControlConfig; } @@ -33,7 +32,6 @@ public NewsService(IRepository newsItemRepository, private readonly IRepository _newsItemRepository; private readonly IMediator _mediator; - private readonly IContextAccessor _contextAccessor; private readonly AccessControlConfig _accessControlConfig; #endregion @@ -53,24 +51,28 @@ public virtual Task GetNewsById(string newsId) /// /// Gets all news /// - /// Store identifier; 0 if you want to get all records + /// + /// Whose news: the customer's groups limit them unless hidden ones are included; an empty store id loads + /// the news of every store + /// /// Page index /// Page size - /// - /// A value indicating whether to show hidden records + /// A value indicating whether to include unpublished and out-of-date news /// News title /// News items - public virtual async Task> GetAllNews(string storeId, - int pageIndex = 0, int pageSize = int.MaxValue, bool ignoreAcl = false, bool showHidden = false, - string newsTitle = "") + public virtual async Task> GetAllNews(CatalogAccess access, + int pageIndex = 0, int pageSize = int.MaxValue, bool includeUnpublished = false, string newsTitle = "") { + ArgumentNullException.ThrowIfNull(access); + + var storeId = access.StoreId; var query = from p in _newsItemRepository.Table select p; if (!string.IsNullOrWhiteSpace(newsTitle)) query = query.Where(n => n.Title != null && n.Title.ToLower().Contains(newsTitle.ToLower())); - if (!showHidden) + if (!includeUnpublished) { var utcNow = DateTime.UtcNow; query = query.Where(n => n.Published); @@ -78,24 +80,20 @@ public virtual async Task> GetAllNews(string storeId, query = query.Where(n => !n.EndDateUtc.HasValue || n.EndDateUtc >= utcNow); } - if ((!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) || - (!ignoreAcl && !_accessControlConfig.IgnoreAcl)) + if (!access.ShowHidden && !_accessControlConfig.IgnoreAcl) { - if (!ignoreAcl && !_accessControlConfig.IgnoreAcl) - { - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - } - - //Store acl - if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) - query = from p in query - where !p.LimitedToStores || p.Stores.Contains(storeId) - select p; + var allowedCustomerGroupsIds = access.Customer.GetCustomerGroupIds(); + query = from p in query + where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) + select p; } + //Store acl + if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(storeId) + select p; + query = query.OrderByDescending(n => n.CreatedOnUtc); return await _newsItemRepository.PagedAsync(query, pageIndex, pageSize); } diff --git a/src/Business/Grand.Business.Cms/Services/PageService.cs b/src/Business/Grand.Business.Cms/Services/PageService.cs index 8173d2e64b..29a7357e16 100644 --- a/src/Business/Grand.Business.Cms/Services/PageService.cs +++ b/src/Business/Grand.Business.Cms/Services/PageService.cs @@ -1,9 +1,9 @@ using Grand.Business.Core.Interfaces.Cms; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Domain.Customers; using Grand.Domain.Pages; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Configuration; @@ -20,13 +20,11 @@ public class PageService : IPageService #region Ctor public PageService(IRepository pageRepository, - IContextAccessor contextAccessor, IAclService aclService, IMediator mediator, ICacheBase cacheBase, AccessControlConfig accessControlConfig) { _pageRepository = pageRepository; - _contextAccessor = contextAccessor; _aclService = aclService; _mediator = mediator; _cacheBase = cacheBase; @@ -38,7 +36,6 @@ public PageService(IRepository pageRepository, #region Fields private readonly IRepository _pageRepository; - private readonly IContextAccessor _contextAccessor; private readonly IAclService _aclService; private readonly IMediator _mediator; private readonly ICacheBase _cacheBase; @@ -92,39 +89,37 @@ public virtual async Task GetPageBySystemName(string systemName, string st /// /// Gets all pages /// - /// Store identifier; pass "" to load all records - /// + /// + /// Whose pages: the customer's groups limit them unless hidden ones are included; an empty store id loads + /// the pages of every store + /// /// Pages - public virtual async Task> GetAllPages(string storeId, bool ignoreAcl = false) + public virtual async Task> GetAllPages(CatalogAccess access) { - var key = string.Format(CacheKey.PAGES_ALL_KEY, storeId, ignoreAcl); + ArgumentNullException.ThrowIfNull(access); + + var storeId = access.StoreId; + var ignoreAcl = access.ShowHidden || _accessControlConfig.IgnoreAcl; + var allowedCustomerGroupsIds = ignoreAcl ? Array.Empty() : access.Customer.GetCustomerGroupIds(); + var key = string.Format(CacheKey.PAGES_ALL_KEY, storeId, access.ShowHidden, + string.Join(",", allowedCustomerGroupsIds)); return await _cacheBase.GetAsync(key, async () => { var query = from p in _pageRepository.Table select p; - query = query.OrderBy(t => t.DisplayOrder).ThenBy(t => t.SystemName); + if (!ignoreAcl) + query = from p in query + where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) + select p; - if ((string.IsNullOrEmpty(storeId) || _accessControlConfig.IgnoreStoreLimitations) && - (ignoreAcl || _accessControlConfig.IgnoreAcl)) return await _pageRepository.ToListAsync(query); - { - if (!ignoreAcl && !_accessControlConfig.IgnoreAcl) - { - var allowedCustomerGroupsIds = _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds(); - query = from p in query - where !p.LimitedToGroups || allowedCustomerGroupsIds.Any(x => p.CustomerGroups.Contains(x)) - select p; - } - - //Store acl - if (string.IsNullOrEmpty(storeId) || _accessControlConfig.IgnoreStoreLimitations) - return await _pageRepository.ToListAsync(query); + //Store acl + if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) query = from p in query where !p.LimitedToStores || p.Stores.Contains(storeId) select p; - query = query.OrderBy(t => t.SystemName); - } + query = query.OrderBy(t => t.DisplayOrder).ThenBy(t => t.SystemName); return await _pageRepository.ToListAsync(query); }); } diff --git a/src/Business/Grand.Business.Common/Services/Localization/TranslationService.cs b/src/Business/Grand.Business.Common/Services/Localization/TranslationService.cs index d72d03c8e0..b4b318204e 100644 --- a/src/Business/Grand.Business.Common/Services/Localization/TranslationService.cs +++ b/src/Business/Grand.Business.Common/Services/Localization/TranslationService.cs @@ -141,6 +141,10 @@ public virtual async Task DeleteTranslateResource(TranslationResource translateR /// /// A string representing a name. /// A string representing the requested resource string. + /// + /// Reads the working language of the current request. Code that may run outside a request uses the + /// overload that takes the language. + /// public virtual string GetResource(string name) { return _contextAccessor.WorkContext.WorkingLanguage != null ? GetResource(name, _contextAccessor.WorkContext.WorkingLanguage.Id) : ""; @@ -248,8 +252,7 @@ await _translationRepository.InsertAsync(new TranslationResource { LanguageId = language.Id, Name = item.Name.ToLowerInvariant(), Value = item.Value, - Area = areaEnum, - CreatedBy = _contextAccessor.WorkContext.CurrentCustomer.Email + Area = areaEnum }); } } diff --git a/src/Business/Grand.Business.Common/Services/Security/PermissionService.cs b/src/Business/Grand.Business.Common/Services/Security/PermissionService.cs index e19d1aa8fa..361fa9b507 100644 --- a/src/Business/Grand.Business.Common/Services/Security/PermissionService.cs +++ b/src/Business/Grand.Business.Common/Services/Security/PermissionService.cs @@ -160,6 +160,10 @@ public virtual async Task UpdatePermission(Permission permission) /// /// Permission record /// true - authorized; otherwise, false + /// + /// Reads the customer of the current request. Code that may run outside a request uses the overload that + /// takes the customer. + /// public virtual async Task Authorize(Permission permission) { return await Authorize(permission, _contextAccessor.WorkContext.CurrentCustomer); @@ -187,6 +191,10 @@ public virtual async Task Authorize(Permission permission, Customer custom /// /// Permission system name /// true - authorized; otherwise, false + /// + /// Reads the customer of the current request. Code that may run outside a request uses the overload that + /// takes the customer. + /// public virtual async Task Authorize(string permissionSystemName) { return await Authorize(permissionSystemName, _contextAccessor.WorkContext.CurrentCustomer); @@ -258,21 +266,25 @@ public virtual async Task DeletePermissionAction(PermissionAction permissionActi /// /// Permission system name /// Permission action name + /// Customer /// true - authorized; otherwise, false - public virtual async Task AuthorizeAction(string permissionSystemName, string permissionActionName) + public virtual async Task AuthorizeAction(string permissionSystemName, string permissionActionName, + Customer customer) { - if (string.IsNullOrEmpty(permissionSystemName) || string.IsNullOrEmpty(permissionActionName)) - return false; + ArgumentNullException.ThrowIfNull(customer); - if (!await Authorize(permissionSystemName)) + if (string.IsNullOrEmpty(permissionSystemName) || string.IsNullOrEmpty(permissionActionName)) return false; - var customerGroups = await _groupService.GetAllByIds(_contextAccessor.WorkContext.CurrentCustomer.Groups.ToArray()); + var authorized = false; + var customerGroups = await _groupService.GetAllByIds(customer.Groups.ToArray()); foreach (var group in customerGroups) { if (!await Authorize(permissionSystemName, group)) continue; + authorized = true; + var key = string.Format(CacheKey.PERMISSIONS_ALLOWED_ACTION_KEY, group.Id, permissionSystemName, permissionActionName); var permissionAction = await _cacheBase.GetAsync(key, async () => @@ -286,7 +298,7 @@ public virtual async Task AuthorizeAction(string permissionSystemName, str return false; } - return true; + return authorized; } #endregion diff --git a/src/Business/Grand.Business.Core/Extensions/TranslateExtensions.cs b/src/Business/Grand.Business.Core/Extensions/TranslateExtensions.cs index 32d9972da7..31b2edead9 100644 --- a/src/Business/Grand.Business.Core/Extensions/TranslateExtensions.cs +++ b/src/Business/Grand.Business.Core/Extensions/TranslateExtensions.cs @@ -2,7 +2,6 @@ using Grand.Domain; using Grand.Domain.Localization; using Grand.Domain.Permissions; -using Grand.Infrastructure; using Grand.SharedKernel.Extensions; using System.Linq.Expressions; using System.Reflection; @@ -56,24 +55,6 @@ public static string GetTranslation(this T entity, return result; } - /// - /// Get translation value of enum - /// - /// Enum - /// Enum value - /// Translation service - /// Work context - /// Translation value - public static string GetTranslationEnum(this T enumValue, - ITranslationService translationService, - IContextAccessor contextAccessor) - where T : struct - { - ArgumentNullException.ThrowIfNull(contextAccessor); - - return GetTranslationEnum(enumValue, translationService, contextAccessor.WorkContext.WorkingLanguage.Id); - } - /// /// Get translation value of enum /// diff --git a/src/Business/Grand.Business.Core/Interfaces/Authentication/ITwoFactorAuthenticationService.cs b/src/Business/Grand.Business.Core/Interfaces/Authentication/ITwoFactorAuthenticationService.cs index e408caf7f1..9f65d828fe 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Authentication/ITwoFactorAuthenticationService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Authentication/ITwoFactorAuthenticationService.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Utilities.Authentication; using Grand.Domain.Customers; using Grand.Domain.Localization; +using Grand.Domain.Stores; namespace Grand.Business.Core.Interfaces.Authentication; @@ -9,6 +10,7 @@ public interface ITwoFactorAuthenticationService Task AuthenticateTwoFactor(string secretKey, string token, Customer customer, TwoFactorAuthenticationType twoFactorAuthenticationType); - Task GenerateCodeSetup(string secretKey, Customer customer, Language language, + /// The store whose company name the authenticator app shows as the issuer + Task GenerateCodeSetup(string secretKey, Customer customer, Store store, Language language, TwoFactorAuthenticationType twoFactorAuthenticationType); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Core/Interfaces/Cms/IKnowledgebaseService.cs b/src/Business/Grand.Business.Core/Interfaces/Cms/IKnowledgebaseService.cs index 2493d401af..40585cb5b5 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Cms/IKnowledgebaseService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Cms/IKnowledgebaseService.cs @@ -1,4 +1,5 @@ -using Grand.Domain; +using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain; using Grand.Domain.Knowledgebase; namespace Grand.Business.Core.Interfaces.Cms; @@ -16,8 +17,9 @@ public interface IKnowledgebaseService /// Gets public knowledge base category /// /// + /// Whose view: the customer's groups and the store limit the result /// knowledge base category - Task GetPublicKnowledgebaseCategory(string id); + Task GetPublicKnowledgebaseCategory(string id, CatalogAccess access); /// /// Gets knowledge base categories @@ -28,8 +30,9 @@ public interface IKnowledgebaseService /// /// Gets public(published etc) knowledge base categories /// + /// Whose view: the customer's groups and the store limit the result /// List of public knowledge base categories - Task> GetPublicKnowledgebaseCategories(); + Task> GetPublicKnowledgebaseCategories(CatalogAccess access); /// /// Inserts knowledge base category @@ -60,21 +63,22 @@ public interface IKnowledgebaseService /// Gets public knowledge base article /// /// + /// Whose view: the customer's groups and the store limit the result /// knowledge base article - Task GetPublicKnowledgebaseArticle(string id); + Task GetPublicKnowledgebaseArticle(string id, CatalogAccess access); /// - /// Gets knowledge base articles + /// Gets every knowledge base article, unpublished and group- or store-limited ones included /// /// List of knowledge base articles - /// Store ident - Task> GetKnowledgebaseArticles(string storeId = ""); + Task> GetKnowledgebaseArticles(); /// /// Gets public(published etc) knowledge base articles /// + /// Whose view: the customer's groups and the store limit the result /// List of public knowledge base articles - Task> GetPublicKnowledgebaseArticles(); + Task> GetPublicKnowledgebaseArticles(CatalogAccess access); /// /// Gets public(published etc) knowledge base articles for an explicit store and customer groups, @@ -88,26 +92,31 @@ public interface IKnowledgebaseService /// /// Gets homepage knowledge base articles /// + /// Whose view: the customer's groups and the store limit the result /// List of homepage knowledge base articles - Task> GetHomepageKnowledgebaseArticles(); + Task> GetHomepageKnowledgebaseArticles(CatalogAccess access); /// /// Gets public(published etc) knowledge base articles for category id /// + /// Whose view: the customer's groups and the store limit the result /// List of public knowledge base articles - Task> GetPublicKnowledgebaseArticlesByCategory(string categoryId); + Task> GetPublicKnowledgebaseArticlesByCategory(string categoryId, CatalogAccess access); /// /// Gets public(published etc) knowledge base articles for keyword /// + /// Whose view: the customer's groups and the store limit the result /// List of public knowledge base articles - Task> GetPublicKnowledgebaseArticlesByKeyword(string keyword); + Task> GetPublicKnowledgebaseArticlesByKeyword(string keyword, CatalogAccess access); /// /// Gets public(published etc) knowledge base categories for keyword /// + /// Whose view: the customer's groups and the store limit the result /// List of public knowledge base categories - Task> GetPublicKnowledgebaseCategoriesByKeyword(string keyword); + Task> GetPublicKnowledgebaseCategoriesByKeyword(string keyword, + CatalogAccess access); /// /// Inserts knowledge base article diff --git a/src/Business/Grand.Business.Core/Interfaces/Cms/INewsService.cs b/src/Business/Grand.Business.Core/Interfaces/Cms/INewsService.cs index 33841ceb3d..cd70a511c0 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Cms/INewsService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Cms/INewsService.cs @@ -1,3 +1,4 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.News; @@ -18,16 +19,17 @@ public interface INewsService /// /// Gets all news /// - /// Store identifier; 0 if you want to get all records + /// + /// Whose news: the customer's groups limit them unless hidden ones are included; an empty store id loads + /// the news of every store + /// /// Page index /// Page size - /// - /// A value indicating whether to show hidden records + /// A value indicating whether to include unpublished and out-of-date news /// News title /// News items - Task> GetAllNews(string storeId, - int pageIndex = 0, int pageSize = int.MaxValue, bool ignoreAcl = false, bool showHidden = false, - string newsTitle = ""); + Task> GetAllNews(CatalogAccess access, + int pageIndex = 0, int pageSize = int.MaxValue, bool includeUnpublished = false, string newsTitle = ""); /// /// Inserts a news item diff --git a/src/Business/Grand.Business.Core/Interfaces/Cms/IPageService.cs b/src/Business/Grand.Business.Core/Interfaces/Cms/IPageService.cs index 30bcde062f..36e04e0fae 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Cms/IPageService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Cms/IPageService.cs @@ -1,3 +1,4 @@ +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Pages; namespace Grand.Business.Core.Interfaces.Cms; @@ -25,10 +26,12 @@ public interface IPageService /// /// Gets all pages /// - /// Store identifier; pass "" to load all records - /// + /// + /// Whose pages: the customer's groups limit them unless hidden ones are included; an empty store id loads + /// the pages of every store + /// /// Pages - Task> GetAllPages(string storeId, bool ignoreAcl = false); + Task> GetAllPages(CatalogAccess access); /// /// Inserts a page diff --git a/src/Business/Grand.Business.Core/Interfaces/Common/Localization/ITranslationService.cs b/src/Business/Grand.Business.Core/Interfaces/Common/Localization/ITranslationService.cs index 935879638a..be8816ed97 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Common/Localization/ITranslationService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Common/Localization/ITranslationService.cs @@ -52,6 +52,10 @@ public interface ITranslationService /// /// A string representing a name. /// A string representing the requested resource string. + /// + /// Reads the working language of the current request. Code that may run outside a request uses the + /// overload that takes the language. + /// string GetResource(string name); /// diff --git a/src/Business/Grand.Business.Core/Interfaces/Common/Security/IPermissionService.cs b/src/Business/Grand.Business.Core/Interfaces/Common/Security/IPermissionService.cs index be079ddcad..2c9341e648 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Common/Security/IPermissionService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Common/Security/IPermissionService.cs @@ -51,6 +51,10 @@ public interface IPermissionService /// /// Permission /// true - authorized; otherwise, false + /// + /// Reads the customer of the current request. Code that may run outside a request uses the overload that + /// takes the customer. + /// Task Authorize(Permission permission); /// @@ -66,6 +70,10 @@ public interface IPermissionService /// /// Permission system name /// true - authorized; otherwise, false + /// + /// Reads the customer of the current request. Code that may run outside a request uses the overload that + /// takes the customer. + /// Task Authorize(string permissionSystemName); /// @@ -101,6 +109,7 @@ public interface IPermissionService /// /// Permission record system name /// Permission action name + /// Customer /// true - authorized; otherwise, false - Task AuthorizeAction(string permissionSystemName, string permissionActionName); + Task AuthorizeAction(string permissionSystemName, string permissionActionName, Customer customer); } \ No newline at end of file diff --git a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs index f83dd5bb8b..9e6b8151da 100644 --- a/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs +++ b/src/Business/Grand.Business.Messages/Commands/Handlers/Common/GetSitemapXMLCommandHandler.cs @@ -328,7 +328,7 @@ private async Task> GetProductUrls(Language language, St private async Task> GetPagesUrls(Language language, Store store) { var now = DateTime.UtcNow; - return (await _pageService.GetAllPages(store.Id, ignoreAcl: true)) + return (await _pageService.GetAllPages(CatalogAccess.IncludingHidden(store.Id))) .Where(IsVisibleToGuests) .PreferStoreOverrides(store.Id) .Where(t => t.Published && t.IncludeInSitemap && (!t.StartDateUtc.HasValue || t.StartDateUtc < now) && diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs b/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs index 660c6befca..7b6a30d16c 100644 --- a/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs +++ b/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs @@ -190,8 +190,9 @@ public static partial class CacheKey /// /// {0} : store ID /// {1} : ignore ACL? + /// {2} : customer group ids; empty when ACL is ignored /// - public static string PAGES_ALL_KEY => "Grand.pages.all-{0}-{1}"; + public static string PAGES_ALL_KEY => "Grand.pages.all-{0}-{1}-{2}"; /// /// Key for caching diff --git a/src/Tests/Grand.Business.Authentication.Tests/Services/TwoFactorAuthenticationServiceTests.cs b/src/Tests/Grand.Business.Authentication.Tests/Services/TwoFactorAuthenticationServiceTests.cs index 84022c8119..4e2b978b64 100644 --- a/src/Tests/Grand.Business.Authentication.Tests/Services/TwoFactorAuthenticationServiceTests.cs +++ b/src/Tests/Grand.Business.Authentication.Tests/Services/TwoFactorAuthenticationServiceTests.cs @@ -4,7 +4,6 @@ using Grand.Domain.Customers; using Grand.Domain.Localization; using Grand.Domain.Stores; -using Grand.Infrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -16,27 +15,22 @@ public class TwoFactorAuthenticationServiceTests private Mock> _sMsVerificationService; private TwoFactorAuthenticationService _twoFactorAuthenticationService; private Mock _customerServiceMock; - private Mock _workContextMock; [TestInitialize] public void Init() { - _workContextMock = new Mock(); _customerServiceMock = new Mock(); _sMsVerificationService = new Mock>(); - _twoFactorAuthenticationService = new TwoFactorAuthenticationService(_workContextMock.Object, + _twoFactorAuthenticationService = new TwoFactorAuthenticationService( _customerServiceMock.Object, _sMsVerificationService.Object); } [TestMethod] public async Task GenerateCodeSetupTest_AppVerification() { - //Arrange - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); //Act var result = await _twoFactorAuthenticationService.GenerateCodeSetup(Guid.NewGuid().ToString(), - new Customer { Email = "test@test.com" }, + new Customer { Email = "test@test.com" }, new Store { CompanyName = "test store" }, new Language { LanguageCulture = "en" }, TwoFactorAuthenticationType.AppVerification); //Assert Assert.IsTrue(result.CustomValues.ContainsKey("QrCodeImageUrl")); @@ -46,12 +40,9 @@ public async Task GenerateCodeSetupTest_AppVerification() [TestMethod] public async Task GenerateCodeSetupTest_Email() { - //Arrange - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); //Act var result = await _twoFactorAuthenticationService.GenerateCodeSetup(Guid.NewGuid().ToString(), - new Customer { Email = "test@test.com" }, + new Customer { Email = "test@test.com" }, new Store { CompanyName = "test store" }, new Language { LanguageCulture = "en" }, TwoFactorAuthenticationType.EmailVerification); //Assert Assert.IsTrue(result.CustomValues.ContainsKey("Token")); diff --git a/src/Tests/Grand.Business.Cms.Tests/Services/KnowledgebaseServiceTests.cs b/src/Tests/Grand.Business.Cms.Tests/Services/KnowledgebaseServiceTests.cs index d2930791d2..5fa261891c 100644 --- a/src/Tests/Grand.Business.Cms.Tests/Services/KnowledgebaseServiceTests.cs +++ b/src/Tests/Grand.Business.Cms.Tests/Services/KnowledgebaseServiceTests.cs @@ -1,10 +1,9 @@ using Grand.Business.Cms.Services; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; using Grand.Domain.Knowledgebase; -using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Tests.Caching; @@ -25,7 +24,6 @@ public class KnowledgebaseServiceTests private IRepository _repositoryKnowledgebaseArticleComment; private IRepository _repositoryKnowledgebaseCategory; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -35,16 +33,11 @@ public void Init() _repositoryKnowledgebaseArticleComment = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); - _workContextMock = new Mock(); - - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); - _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); _knowledgebaseService = new KnowledgebaseService(_repositoryKnowledgebaseCategory, _repositoryKnowledgebaseArticle, _repositoryKnowledgebaseArticleComment, _mediatorMock.Object, - _workContextMock.Object, _cacheBase, new AccessControlConfig()); + _cacheBase, new AccessControlConfig()); } [TestMethod] @@ -93,7 +86,8 @@ public async Task GetPublicKnowledgebaseCategoryTest() var knowledgebaseCategory = new KnowledgebaseCategory { Published = true }; await _repositoryKnowledgebaseCategory.InsertAsync(knowledgebaseCategory); //Act - var result = await _knowledgebaseService.GetPublicKnowledgebaseCategory(knowledgebaseCategory.Id); + var result = await _knowledgebaseService.GetPublicKnowledgebaseCategory(knowledgebaseCategory.Id, + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsNotNull(result); } @@ -146,6 +140,20 @@ public async Task GetKnowledgebaseArticlesTest() Assert.IsTrue(result.Any()); } + [TestMethod] + public async Task GetKnowledgebaseArticles_ReturnsGroupAndStoreLimitedArticles() + { + //Arrange + var limited = new KnowledgebaseArticle { + Published = true, LimitedToGroups = true, CustomerGroups = ["g1"], LimitedToStores = true, Stores = ["s1"] + }; + await _repositoryKnowledgebaseArticle.InsertAsync(limited); + //Act + var result = await _knowledgebaseService.GetKnowledgebaseArticles(); + //Assert + Assert.IsTrue(result.Any(x => x.Id == limited.Id)); + } + [TestMethod] public async Task InsertKnowledgebaseArticleTest() { @@ -201,7 +209,8 @@ public async Task GetPublicKnowledgebaseCategoriesTest() var knowledgebaseCategory = new KnowledgebaseCategory { Published = true }; await _repositoryKnowledgebaseCategory.InsertAsync(knowledgebaseCategory); //Act - var result = await _knowledgebaseService.GetPublicKnowledgebaseCategories(); + var result = await _knowledgebaseService.GetPublicKnowledgebaseCategories( + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsTrue(result.Any()); } @@ -213,7 +222,7 @@ public async Task GetPublicKnowledgebaseArticlesTest() var knowledgebaseArticle = new KnowledgebaseArticle { Published = true }; await _knowledgebaseService.InsertKnowledgebaseArticle(knowledgebaseArticle); //Act - var result = await _knowledgebaseService.GetPublicKnowledgebaseArticles(); + var result = await _knowledgebaseService.GetPublicKnowledgebaseArticles(CatalogAccess.For(new Customer(), "")); //Assert Assert.IsTrue(result.Any()); } @@ -225,7 +234,8 @@ public async Task GetPublicKnowledgebaseArticleTest() var knowledgebaseArticle = new KnowledgebaseArticle { Published = true }; await _knowledgebaseService.InsertKnowledgebaseArticle(knowledgebaseArticle); //Act - var result = await _knowledgebaseService.GetPublicKnowledgebaseArticle(knowledgebaseArticle.Id); + var result = await _knowledgebaseService.GetPublicKnowledgebaseArticle(knowledgebaseArticle.Id, + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsNotNull(result); } @@ -238,7 +248,8 @@ public async Task GetPublicKnowledgebaseArticlesByCategoryTest() await _knowledgebaseService.InsertKnowledgebaseArticle(knowledgebaseArticle); //Act var result = - await _knowledgebaseService.GetPublicKnowledgebaseArticlesByCategory(knowledgebaseArticle.ParentCategoryId); + await _knowledgebaseService.GetPublicKnowledgebaseArticlesByCategory(knowledgebaseArticle.ParentCategoryId, + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsTrue(result.Any()); } @@ -250,7 +261,8 @@ public async Task GetPublicKnowledgebaseArticlesByKeywordTest() var knowledgebaseArticle = new KnowledgebaseArticle { Published = true, Name = "test" }; await _knowledgebaseService.InsertKnowledgebaseArticle(knowledgebaseArticle); //Act - var result = await _knowledgebaseService.GetPublicKnowledgebaseArticlesByKeyword("test"); + var result = await _knowledgebaseService.GetPublicKnowledgebaseArticlesByKeyword("test", + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsTrue(result.Any()); } @@ -262,7 +274,8 @@ public async Task GetPublicKnowledgebaseCategoriesByKeywordTest() var knowledgebaseCategory = new KnowledgebaseCategory { Published = true, Name = "test" }; await _repositoryKnowledgebaseCategory.InsertAsync(knowledgebaseCategory); //Act - var result = await _knowledgebaseService.GetPublicKnowledgebaseCategoriesByKeyword("test"); + var result = await _knowledgebaseService.GetPublicKnowledgebaseCategoriesByKeyword("test", + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsTrue(result.Any()); } @@ -274,7 +287,8 @@ public async Task GetHomepageKnowledgebaseArticlesTest() var knowledgebaseArticle = new KnowledgebaseArticle { Published = true, Name = "test", ShowOnHomepage = true }; await _knowledgebaseService.InsertKnowledgebaseArticle(knowledgebaseArticle); //Act - var result = await _knowledgebaseService.GetHomepageKnowledgebaseArticles(); + var result = await _knowledgebaseService.GetHomepageKnowledgebaseArticles( + CatalogAccess.For(new Customer(), "")); //Assert Assert.IsTrue(result.Any()); } @@ -378,13 +392,76 @@ public async Task DeleteArticleCommentTest() _repositoryKnowledgebaseArticleComment.Table.FirstOrDefault(x => x.Id == knowledgebaseArticleComment.Id)); } + private static Customer CustomerIn(params string[] groupIds) + { + var customer = new Customer(); + foreach (var groupId in groupIds) + customer.Groups.Add(groupId); + return customer; + } + + [TestMethod] + public async Task GetPublicKnowledgebaseCategories_DifferentGroups_DoNotShareTheCachedList() + { + //Arrange + var category = new KnowledgebaseCategory { Published = true, LimitedToGroups = true, CustomerGroups = ["g1"] }; + await _repositoryKnowledgebaseCategory.InsertAsync(category); + //Act + var forMember = await _knowledgebaseService.GetPublicKnowledgebaseCategories( + CatalogAccess.For(CustomerIn("g1"), "s1")); + var forOther = await _knowledgebaseService.GetPublicKnowledgebaseCategories( + CatalogAccess.For(CustomerIn("g2"), "s1")); + //Assert + Assert.IsTrue(forMember.Any(x => x.Id == category.Id)); + Assert.IsFalse(forOther.Any(x => x.Id == category.Id)); + } + + [TestMethod] + public async Task GetPublicKnowledgebaseArticles_IncludingHidden_DoesNotNeedACustomer() + { + //Arrange + var article = new KnowledgebaseArticle { Published = true, LimitedToGroups = true, CustomerGroups = ["g1"] }; + await _repositoryKnowledgebaseArticle.InsertAsync(article); + //Act + var result = await _knowledgebaseService.GetPublicKnowledgebaseArticles(CatalogAccess.IncludingHidden()); + //Assert + Assert.IsTrue(result.Any(x => x.Id == article.Id)); + } + + [TestMethod] + public async Task GetPublicKnowledgebaseArticles_IncludingHiddenWithoutStore_ReturnsEveryStoresArticles() + { + //Arrange + var article = new KnowledgebaseArticle { Published = true, LimitedToStores = true, Stores = ["s1"] }; + await _repositoryKnowledgebaseArticle.InsertAsync(article); + //Act + var result = await _knowledgebaseService.GetPublicKnowledgebaseArticles(CatalogAccess.IncludingHidden()); + //Assert + Assert.IsTrue(result.Any(x => x.Id == article.Id)); + } + + [TestMethod] + public async Task GetPublicKnowledgebaseArticles_IncludingHiddenThenNoGroups_DoNotShareTheCachedList() + { + //Arrange + var article = new KnowledgebaseArticle { Published = true, LimitedToGroups = true, CustomerGroups = ["g1"] }; + await _repositoryKnowledgebaseArticle.InsertAsync(article); + //Act + var hidden = await _knowledgebaseService.GetPublicKnowledgebaseArticles(CatalogAccess.IncludingHidden("s1")); + var noGroups = await _knowledgebaseService.GetPublicKnowledgebaseArticles( + CatalogAccess.For(CustomerIn(), "s1")); + //Assert + Assert.IsTrue(hidden.Any(x => x.Id == article.Id)); + Assert.IsFalse(noGroups.Any(x => x.Id == article.Id)); + } + [TestMethod] public async Task GetPublicKnowledgebaseArticles_WhenScopeGivenWithoutContext_FiltersByStoreAndGroups() { //Arrange - no ambient work or store context, as in a scheduled task var service = new KnowledgebaseService(_repositoryKnowledgebaseCategory, _repositoryKnowledgebaseArticle, _repositoryKnowledgebaseArticleComment, _mediatorMock.Object, - new Mock().Object, _cacheBase, new AccessControlConfig()); + _cacheBase, new AccessControlConfig()); await _repositoryKnowledgebaseArticle.InsertAsync(new KnowledgebaseArticle { Id = "open", Published = true }); await _repositoryKnowledgebaseArticle.InsertAsync(new KnowledgebaseArticle { Id = "guests", Published = true, LimitedToGroups = true, CustomerGroups = ["guests-group"] diff --git a/src/Tests/Grand.Business.Cms.Tests/Services/NewsServiceTests.cs b/src/Tests/Grand.Business.Cms.Tests/Services/NewsServiceTests.cs index b86bed89ab..933a45d015 100644 --- a/src/Tests/Grand.Business.Cms.Tests/Services/NewsServiceTests.cs +++ b/src/Tests/Grand.Business.Cms.Tests/Services/NewsServiceTests.cs @@ -1,10 +1,9 @@ using Grand.Business.Cms.Services; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; using Grand.Domain.News; -using Grand.Domain.Stores; -using Grand.Infrastructure; using Grand.Infrastructure.Configuration; using Grand.Mediator; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -19,7 +18,6 @@ public class NewsServiceTests private NewsService _newsService; private IRepository _repository; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -27,13 +25,7 @@ public void Init() _repository = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); - _workContextMock = new Mock(); - - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); - - _newsService = new NewsService(_repository, _mediatorMock.Object, _workContextMock.Object, - new AccessControlConfig()); + _newsService = new NewsService(_repository, _mediatorMock.Object, new AccessControlConfig()); } [TestMethod] @@ -55,11 +47,61 @@ public async Task GetAllNewsTest() var newsItem = new NewsItem { Published = true }; await _repository.InsertAsync(newsItem); //Act - var result = await _newsService.GetAllNews(storeId: ""); + var result = await _newsService.GetAllNews(CatalogAccess.For(new Customer(), "")); //Assert Assert.IsTrue(result.Any()); } + private static Customer CustomerIn(params string[] groupIds) + { + var customer = new Customer(); + foreach (var groupId in groupIds) + customer.Groups.Add(groupId); + return customer; + } + + [TestMethod] + public async Task GetAllNews_For_FiltersByTheGivenCustomersGroups() + { + //Arrange + var newsItem = new NewsItem { Published = true, LimitedToGroups = true, CustomerGroups = ["g1"] }; + await _repository.InsertAsync(newsItem); + //Act + var forMember = await _newsService.GetAllNews(CatalogAccess.For(CustomerIn("g1"), "")); + var forOther = await _newsService.GetAllNews(CatalogAccess.For(CustomerIn("g2"), "")); + var includingHidden = await _newsService.GetAllNews(CatalogAccess.IncludingHidden()); + //Assert + Assert.IsTrue(forMember.Any(x => x.Id == newsItem.Id)); + Assert.IsFalse(forOther.Any(x => x.Id == newsItem.Id)); + Assert.IsTrue(includingHidden.Any(x => x.Id == newsItem.Id)); + } + + [TestMethod] + public async Task GetAllNews_EmptyStore_DoesNotFilterByStore() + { + //Arrange + var newsItem = new NewsItem { Published = true, LimitedToStores = true, Stores = ["s1"] }; + await _repository.InsertAsync(newsItem); + //Act + var result = await _newsService.GetAllNews(CatalogAccess.For(new Customer(), "")); + //Assert + Assert.IsTrue(result.Any(x => x.Id == newsItem.Id)); + } + + [TestMethod] + public async Task GetAllNews_IncludeUnpublished_ReturnsUnpublishedItems() + { + //Arrange + var newsItem = new NewsItem { Published = false }; + await _repository.InsertAsync(newsItem); + //Act + var published = await _newsService.GetAllNews(CatalogAccess.IncludingHidden()); + var all = await _newsService.GetAllNews(CatalogAccess.IncludingHidden(), includeUnpublished: true); + //Assert + Assert.IsFalse(published.Any(x => x.Id == newsItem.Id)); + Assert.IsTrue(all.Any(x => x.Id == newsItem.Id)); + } + [TestMethod] public async Task InsertNewsTest() { diff --git a/src/Tests/Grand.Business.Cms.Tests/Services/PageServiceTests.cs b/src/Tests/Grand.Business.Cms.Tests/Services/PageServiceTests.cs index e52a5b57c9..802eefaa06 100644 --- a/src/Tests/Grand.Business.Cms.Tests/Services/PageServiceTests.cs +++ b/src/Tests/Grand.Business.Cms.Tests/Services/PageServiceTests.cs @@ -1,12 +1,12 @@ using Grand.Business.Cms.Services; using Grand.Business.Common.Services.Security; using Grand.Business.Core.Interfaces.Common.Security; +using Grand.Business.Core.Utilities.Catalog; using Grand.Data; using Grand.Data.Tests.MongoDb; using Grand.Domain.Customers; using Grand.Domain.Pages; -using Grand.Domain.Stores; -using Grand.Infrastructure; +using Grand.Infrastructure.Caching.Constants; using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Infrastructure.Tests.Caching; @@ -25,7 +25,6 @@ public class PageServiceTests private PageService _pageService; private IRepository _repository; - private Mock _workContextMock; [TestInitialize] public void Init() @@ -33,17 +32,13 @@ public void Init() _repository = new MongoDBRepositoryTest(); _mediatorMock = new Mock(); - _workContextMock = new Mock(); _cacheBase = new MemoryCacheBase(MemoryCacheTest.Get(), _mediatorMock.Object, new CacheConfig { DefaultCacheTimeMinutes = 1 }); _aclService = new AclService(new AccessControlConfig()); - _workContextMock.Setup(c => c.StoreContext.CurrentStore).Returns(() => new Store { Id = "", Name = "test store" }); - _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => new Customer()); - - _pageService = new PageService(_repository, _workContextMock.Object, _aclService, _mediatorMock.Object, + _pageService = new PageService(_repository, _aclService, _mediatorMock.Object, _cacheBase, new AccessControlConfig()); } @@ -147,11 +142,105 @@ public async Task GetAllPagesTest() var page = new Page { SystemName = "test", Published = true }; await _repository.InsertAsync(page); //Act - var result = await _pageService.GetAllPages(""); + var result = await _pageService.GetAllPages(CatalogAccess.IncludingHidden()); //Assert Assert.IsTrue(result.Any()); } + [TestMethod] + public async Task GetAllPages_InAStore_KeepsTheDisplayOrder() + { + //Arrange + await _repository.InsertAsync(new Page { Id = "second", SystemName = "a", DisplayOrder = 2 }); + await _repository.InsertAsync(new Page { Id = "first", SystemName = "b", DisplayOrder = 1 }); + //Act + var result = await _pageService.GetAllPages(CatalogAccess.For(CustomerIn("g1"), "store-1")); + //Assert + CollectionAssert.AreEqual(new[] { "first", "second" }, result.Select(x => x.Id).ToArray()); + } + + private static Customer CustomerIn(params string[] groupIds) + { + var customer = new Customer(); + foreach (var groupId in groupIds) + customer.Groups.Add(groupId); + return customer; + } + + [TestMethod] + public async Task GetAllPages_For_FiltersByTheGivenCustomersGroups() + { + //Arrange + var page = new Page { SystemName = "vip", LimitedToGroups = true, CustomerGroups = ["g1"] }; + await _repository.InsertAsync(page); + var member = CustomerIn("g1"); + var other = CustomerIn("g2"); + //Act + var forMember = await _pageService.GetAllPages(CatalogAccess.For(member, "")); + await _cacheBase.Clear(); + var forOther = await _pageService.GetAllPages(CatalogAccess.For(other, "")); + //Assert + Assert.IsTrue(forMember.Any(x => x.Id == page.Id)); + Assert.IsFalse(forOther.Any(x => x.Id == page.Id)); + } + + [TestMethod] + public async Task GetAllPages_DifferentGroups_DoNotShareTheCachedList() + { + //Arrange + var page = new Page { SystemName = "vip", LimitedToGroups = true, CustomerGroups = ["g1"] }; + await _repository.InsertAsync(page); + //Act + var forMember = await _pageService.GetAllPages(CatalogAccess.For(CustomerIn("g1"), "")); + var forOther = await _pageService.GetAllPages(CatalogAccess.For(CustomerIn("g2"), "")); + var includingHidden = await _pageService.GetAllPages(CatalogAccess.IncludingHidden()); + //Assert + Assert.IsTrue(forMember.Any(x => x.Id == page.Id)); + Assert.IsFalse(forOther.Any(x => x.Id == page.Id)); + Assert.IsTrue(includingHidden.Any(x => x.Id == page.Id)); + } + + [TestMethod] + public async Task GetAllPages_SameGroupsInAnotherOrder_SeeTheSamePages() + { + //Arrange + var page = new Page { SystemName = "a-only", LimitedToGroups = true, CustomerGroups = ["a"] }; + await _repository.InsertAsync(page); + //Act + var first = await _pageService.GetAllPages(CatalogAccess.For(CustomerIn("a", "b"), "")); + var second = await _pageService.GetAllPages(CatalogAccess.For(CustomerIn("b", "a"), "")); + //Assert + Assert.IsTrue(first.Any(x => x.Id == page.Id)); + Assert.IsTrue(second.Any(x => x.Id == page.Id)); + } + + [TestMethod] + public async Task GetAllPages_AfterPagePrefixIsCleared_ReturnsNewPages() + { + //Arrange + var access = CatalogAccess.For(CustomerIn("g1"), ""); + await _pageService.GetAllPages(access); + var page = new Page { SystemName = "late" }; + await _repository.InsertAsync(page); + //Act + await _cacheBase.RemoveByPrefix(CacheKey.PAGES_PATTERN_KEY); + var result = await _pageService.GetAllPages(access); + //Assert + Assert.IsTrue(result.Any(x => x.Id == page.Id)); + } + + [TestMethod] + public async Task GetAllPages_EmptyStore_DoesNotFilterByStore() + { + //Arrange + var page = new Page { SystemName = "s1page", LimitedToStores = true, Stores = ["s1"] }; + await _repository.InsertAsync(page); + //Act + var result = await _pageService.GetAllPages(CatalogAccess.For(new Customer(), "")); + //Assert + Assert.IsTrue(result.Any(x => x.Id == page.Id)); + } + [TestMethod] public async Task InsertPageTest() { diff --git a/src/Tests/Grand.Business.Common.Tests/Services/Localization/TranslationServiceTests.cs b/src/Tests/Grand.Business.Common.Tests/Services/Localization/TranslationServiceTests.cs index ab8d265312..194d638f16 100644 --- a/src/Tests/Grand.Business.Common.Tests/Services/Localization/TranslationServiceTests.cs +++ b/src/Tests/Grand.Business.Common.Tests/Services/Localization/TranslationServiceTests.cs @@ -171,6 +171,19 @@ await _translationService.ImportResourcesFromXml(new Language { Id = "1", Name = Assert.IsTrue(_repository.Table.Any()); } + [TestMethod] + public async Task ImportResourcesFromXml_WithoutCurrentCustomer_InsertsResources() + { + //Arrange - no customer, as outside a request + _workContextMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => null); + //Act + await _translationService.ImportResourcesFromXml(new Language { Id = "1", Name = "en" }, + "value2"); + + //Assert + Assert.IsTrue(_repository.Table.Any(x => x.Name == "name2")); + } + [TestMethod] public async Task ImportResourcesFromXmlInstallTest() { diff --git a/src/Tests/Grand.Business.Common.Tests/Services/Security/PermissionServiceTests.cs b/src/Tests/Grand.Business.Common.Tests/Services/Security/PermissionServiceTests.cs index 9fcec5f23b..0317a12f4e 100644 --- a/src/Tests/Grand.Business.Common.Tests/Services/Security/PermissionServiceTests.cs +++ b/src/Tests/Grand.Business.Common.Tests/Services/Security/PermissionServiceTests.cs @@ -61,6 +61,35 @@ public async Task Authorize_NullPermission_ReturnFalse() Assert.IsFalse(await _service.Authorize(permission)); } + private Customer CustomerAuthorizedFor(string groupId, PermissionAction deniedAction) + { + var customer = new Customer(); + customer.Groups.Add(groupId); + _cacheMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>())) + .Returns(Task.FromResult(true)); + _cacheMock.Setup(c => c.GetAsync(It.IsAny(), It.IsAny>>())) + .Returns(Task.FromResult(deniedAction)); + _groupServiceMock.Setup(c => c.GetAllByIds(It.Is(ids => ids.SequenceEqual(new[] { groupId })))) + .Returns(Task.FromResult>(new List { new() { Id = groupId } })); + return customer; + } + + [TestMethod] + public async Task AuthorizeAction_GivenCustomersGroupDenied_ReturnsFalse() + { + var customer = CustomerAuthorizedFor("g1", new PermissionAction { Action = "Edit" }); + + Assert.IsFalse(await _service.AuthorizeAction("ManageProducts", "Edit", customer)); + } + + [TestMethod] + public async Task AuthorizeAction_NoActionDeniedForGivenCustomer_ReturnsTrue() + { + var customer = CustomerAuthorizedFor("g1", null); + + Assert.IsTrue(await _service.AuthorizeAction("ManageProducts", "Edit", customer)); + } + [TestMethod] public async Task InsertPermission_InovokeMethods() { diff --git a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs index 11e093ea89..f5620396dd 100644 --- a/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs +++ b/src/Tests/Grand.Business.Messages.Tests/Commands/GetSitemapXmlCommandHandlerTests.cs @@ -66,7 +66,7 @@ public void Init() .ReturnsAsync(new PagedList()); _brandServiceMock.Setup(x => x.GetAllBrands("", CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue)) .ReturnsAsync(new PagedList()); - _pageServiceMock.Setup(x => x.GetAllPages(StoreId, true)).ReturnsAsync(new List()); + _pageServiceMock.Setup(x => x.GetAllPages(CatalogAccess.IncludingHidden(StoreId))).ReturnsAsync(new List()); _knowledgebaseServiceMock.Setup(x => x.GetPublicKnowledgebaseArticles(StoreId, It.Is>(g => g.SequenceEqual(new[] { GuestsGroupId })))) .ReturnsAsync(new List()); @@ -103,7 +103,7 @@ private static List Locations(string xml) public async Task Handle_WhenPagesIncludedInSitemap_WritesNonEmptyPageLocations() { //Arrange - _pageServiceMock.Setup(x => x.GetAllPages(StoreId, true)).ReturnsAsync(new List { + _pageServiceMock.Setup(x => x.GetAllPages(CatalogAccess.IncludingHidden(StoreId))).ReturnsAsync(new List { new() { Id = "1", SystemName = "AboutUs", SeName = "about-us", Published = true, IncludeInSitemap = true }, new() { Id = "2", SystemName = "Shipping", SeName = "shipping", Published = true, IncludeInSitemap = true } }); @@ -130,7 +130,7 @@ public async Task Handle_WithoutWorkContext_UsesOnlyExplicitlyScopedQueries() CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue), Times.Once); _brandServiceMock.Verify(x => x.GetAllBrands("", CatalogAccess.IncludingHidden(StoreId), 0, int.MaxValue), Times.Once); - _pageServiceMock.Verify(x => x.GetAllPages(StoreId, true), Times.Once); + _pageServiceMock.Verify(x => x.GetAllPages(CatalogAccess.IncludingHidden(StoreId)), Times.Once); _mediatorMock.Verify(x => x.Send(It.Is(q => q.Customer == null && q.StoreId == StoreId && q.VisibleIndividuallyOnly && q.CustomerGroupIds.SequenceEqual(new[] { GuestsGroupId })), It.IsAny()), Times.Once); @@ -140,7 +140,7 @@ public async Task Handle_WithoutWorkContext_UsesOnlyExplicitlyScopedQueries() public async Task Handle_ListsOnlyWhatAGuestCanSee() { //Arrange - _pageServiceMock.Setup(x => x.GetAllPages(StoreId, true)).ReturnsAsync(new List { + _pageServiceMock.Setup(x => x.GetAllPages(CatalogAccess.IncludingHidden(StoreId))).ReturnsAsync(new List { new() { Id = "1", SystemName = "Open", SeName = "open-page", Published = true, IncludeInSitemap = true }, new() { Id = "2", SystemName = "ForGuests", SeName = "guest-page", Published = true, IncludeInSitemap = true, diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs index 29a62ecfaa..b8877e66be 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseNewsControllerTests.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.News; using Grand.Infrastructure.Mapper; using Grand.Mapping; @@ -93,8 +94,8 @@ public async Task ListPost_UsesSharedPrepareNewsItemModel_NotGetAllNewsDirectly( await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, new NewsItemListModel()); _newsViewModelServiceMock.Verify(v => v.PrepareNewsItemModel(It.IsAny(), 1, 10), Times.Once); - _newsServiceMock.Verify(s => s.GetAllNews(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _newsServiceMock.Verify(s => s.GetAllNews(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Never); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BasePageControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BasePageControllerTests.cs index c338fcc18e..cf32a8b839 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BasePageControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BasePageControllerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Cms; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Pages; using Grand.Infrastructure.Mapper; using Grand.Mapping; @@ -82,26 +83,26 @@ public void Setup() public async Task ListPost_ForcesScopeDefaultStoreId() { _scopeMock.Setup(s => s.DefaultStoreId).Returns("store-1"); - _pageServiceMock.Setup(p => p.GetAllPages("store-1", true)).ReturnsAsync(new List()); + _pageServiceMock.Setup(p => p.GetAllPages(CatalogAccess.IncludingHidden("store-1"))).ReturnsAsync(new List()); var model = new PageListModel { SearchStoreId = "attacker-supplied" }; await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, model); Assert.AreEqual("store-1", model.SearchStoreId); - _pageServiceMock.Verify(p => p.GetAllPages("store-1", true), Times.Once); + _pageServiceMock.Verify(p => p.GetAllPages(CatalogAccess.IncludingHidden("store-1")), Times.Once); } [TestMethod] public async Task ListPost_GlobalScope_LeavesSubmittedSearchStoreIdUntouched() { _scopeMock.Setup(s => s.DefaultStoreId).Returns((string)null); - _pageServiceMock.Setup(p => p.GetAllPages("admin-submitted-store", true)).ReturnsAsync(new List()); + _pageServiceMock.Setup(p => p.GetAllPages(CatalogAccess.IncludingHidden("admin-submitted-store"))).ReturnsAsync(new List()); var model = new PageListModel { SearchStoreId = "admin-submitted-store" }; await _controller.List(new DataSourceRequest { Page = 1, PageSize = 10 }, model); Assert.AreEqual("admin-submitted-store", model.SearchStoreId); - _pageServiceMock.Verify(p => p.GetAllPages("admin-submitted-store", true), Times.Once); + _pageServiceMock.Verify(p => p.GetAllPages(CatalogAccess.IncludingHidden("admin-submitted-store")), Times.Once); } [TestMethod] diff --git a/src/Tests/Grand.Web.Admin.Tests/Services/KnowledgebaseViewModelServiceTests.cs b/src/Tests/Grand.Web.Admin.Tests/Services/KnowledgebaseViewModelServiceTests.cs index a697e0f554..4d58976ee1 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Services/KnowledgebaseViewModelServiceTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Services/KnowledgebaseViewModelServiceTests.cs @@ -25,7 +25,7 @@ public void Setup() private void Arrange(List categories, List articles) { _knowledgebaseServiceMock.Setup(x => x.GetKnowledgebaseCategories()).ReturnsAsync(categories); - _knowledgebaseServiceMock.Setup(x => x.GetKnowledgebaseArticles(It.IsAny())).ReturnsAsync(articles); + _knowledgebaseServiceMock.Setup(x => x.GetKnowledgebaseArticles()).ReturnsAsync(articles); } private async Task> Level(string parentId) diff --git a/src/Tests/Grand.Web.Store.Tests/Controllers/PageControllerTests.cs b/src/Tests/Grand.Web.Store.Tests/Controllers/PageControllerTests.cs index cfcc99d598..d08315db13 100644 --- a/src/Tests/Grand.Web.Store.Tests/Controllers/PageControllerTests.cs +++ b/src/Tests/Grand.Web.Store.Tests/Controllers/PageControllerTests.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Cms; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Pages; using Grand.Infrastructure.Mapper; using Grand.Mapping; @@ -138,7 +139,7 @@ public async Task StorePagesList_ReturnsOnlyExclusivelyOwnedPages() var owned = new Page { Id = "owned", LimitedToStores = true, Stores = ["store-1"] }; var shared = new Page { Id = "shared", LimitedToStores = true, Stores = ["store-1", "store-2"] }; var global = new Page { Id = "global", LimitedToStores = false }; - _pageServiceMock.Setup(p => p.GetAllPages("store-1", true)).ReturnsAsync(new List { owned, shared, global }); + _pageServiceMock.Setup(p => p.GetAllPages(CatalogAccess.IncludingHidden("store-1"))).ReturnsAsync(new List { owned, shared, global }); var result = await _controller.StorePagesList(new DataSourceRequest { Page = 1, PageSize = 10 }, new PageListModel()); @@ -153,7 +154,7 @@ public async Task GlobalPagesList_ReturnsSharedAndUnrestrictedPages() var owned = new Page { Id = "owned", LimitedToStores = true, Stores = ["store-1"] }; var shared = new Page { Id = "shared", LimitedToStores = true, Stores = ["store-1", "store-2"] }; var global = new Page { Id = "global", LimitedToStores = false }; - _pageServiceMock.Setup(p => p.GetAllPages("store-1", true)).ReturnsAsync(new List { owned, shared, global }); + _pageServiceMock.Setup(p => p.GetAllPages(CatalogAccess.IncludingHidden("store-1"))).ReturnsAsync(new List { owned, shared, global }); var result = await _controller.GlobalPagesList(new DataSourceRequest { Page = 1, PageSize = 10 }, new PageListModel()); @@ -197,7 +198,7 @@ public async Task Copy_DuplicateSystemNameInTargetStore_RedirectsToEditWithoutIn var page = new Page { Id = "p1", SystemName = "about-us", LimitedToStores = false }; var existing = new Page { Id = "p2", SystemName = "about-us" }; _pageServiceMock.Setup(p => p.GetPageById("p1")).ReturnsAsync(page); - _pageServiceMock.Setup(p => p.GetAllPages("store-1", true)).ReturnsAsync(new List { page, existing }); + _pageServiceMock.Setup(p => p.GetAllPages(CatalogAccess.IncludingHidden("store-1"))).ReturnsAsync(new List { page, existing }); var pageViewModelServiceMock = new Mock(); var controller = new PageController( @@ -220,7 +221,7 @@ public async Task Copy_SharedPage_InsertsCopyScopedToCurrentStore() { var page = new Page { Id = "p1", SystemName = "about-us", LimitedToStores = true, Stores = ["store-1", "store-2"] }; _pageServiceMock.Setup(p => p.GetPageById("p1")).ReturnsAsync(page); - _pageServiceMock.Setup(p => p.GetAllPages("store-1", true)).ReturnsAsync(new List { page }); + _pageServiceMock.Setup(p => p.GetAllPages(CatalogAccess.IncludingHidden("store-1"))).ReturnsAsync(new List { page }); var pageViewModelServiceMock = new Mock(); var newPage = new Page { Id = "new-1" }; diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs index a141a9b587..71d95888e2 100644 --- a/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Catalog/GetMenuHandlerTests.cs @@ -59,7 +59,7 @@ public void Init() .ReturnsAsync(new List()); var pageServiceMock = new Mock(); - pageServiceMock.Setup(s => s.GetAllPages(It.IsAny(), It.IsAny())) + pageServiceMock.Setup(s => s.GetAllPages(It.IsAny())) .ReturnsAsync(new List()); var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/Common/GetSitemapHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/Common/GetSitemapHandlerTests.cs index 8b73577588..2761c21f95 100644 --- a/src/Tests/Grand.Web.Tests/Features/Handlers/Common/GetSitemapHandlerTests.cs +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/Common/GetSitemapHandlerTests.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Catalog.Categories; using Grand.Business.Core.Interfaces.Catalog.Products; using Grand.Business.Core.Interfaces.Cms; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain; using Grand.Domain.Blogs; using Grand.Domain.Common; @@ -41,7 +42,8 @@ public void Init() .ReturnsAsync(new PagedList()); var knowledgebaseMock = new Mock(); - knowledgebaseMock.Setup(s => s.GetPublicKnowledgebaseArticles()).ReturnsAsync(new List()); + knowledgebaseMock.Setup(s => s.GetPublicKnowledgebaseArticles(It.IsAny())) + .ReturnsAsync(new List()); _handler = new GetSitemapHandler(cacheMock.Object, new Mock().Object, new Mock().Object, new Mock().Object, _pageServiceMock.Object, @@ -63,7 +65,8 @@ private static Page CreatePage(string id, bool published, bool includeInSitemap) [TestMethod] public async Task Handle_ListsOnlyPublishedPagesIncludedInSitemap() { - _pageServiceMock.Setup(s => s.GetAllPages("store-1", It.IsAny())).ReturnsAsync(new List { + _pageServiceMock.Setup(s => s.GetAllPages(It.Is(a => a.StoreId == "store-1"))) + .ReturnsAsync(new List { CreatePage("published", true, true), CreatePage("unpublished", false, true), CreatePage("hidden", true, false) diff --git a/src/Tests/Grand.Web.Tests/Features/Handlers/News/GetHomePageNewsItemsHandlerTests.cs b/src/Tests/Grand.Web.Tests/Features/Handlers/News/GetHomePageNewsItemsHandlerTests.cs new file mode 100644 index 0000000000..ac1997a196 --- /dev/null +++ b/src/Tests/Grand.Web.Tests/Features/Handlers/News/GetHomePageNewsItemsHandlerTests.cs @@ -0,0 +1,95 @@ +using Grand.Business.Core.Interfaces.Cms; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; +using Grand.Domain; +using Grand.Domain.Customers; +using Grand.Domain.Localization; +using Grand.Domain.Media; +using Grand.Domain.News; +using Grand.Infrastructure; +using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Configuration; +using Grand.Mediator; +using Grand.Web.Features.Handlers.News; +using Grand.Web.Features.Models.News; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Web.Tests.Features.Handlers.News; + +[TestClass] +public class GetHomePageNewsItemsHandlerTests +{ + private readonly AccessControlConfig _accessControlConfig = new(); + private Customer _currentCustomer; + private GetHomePageNewsItemsHandler _handler; + private Mock _newsServiceMock; + + [TestInitialize] + public void Init() + { + _newsServiceMock = new Mock(); + _newsServiceMock.Setup(s => s.GetAllNews(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((CatalogAccess access, int _, int _, bool _, string _) => { + var news = new List { new() { Id = "public" } }; + if (access.Customer.Groups.Contains("vip")) + news.Add(new NewsItem { Id = "vip-only" }); + return new PagedList(news, 0, int.MaxValue); + }); + + var contextAccessorMock = new Mock(); + contextAccessorMock.Setup(c => c.WorkContext.CurrentCustomer).Returns(() => _currentCustomer); + contextAccessorMock.Setup(c => c.WorkContext.WorkingLanguage).Returns(new Language { Id = "lang-1" }); + contextAccessorMock.Setup(c => c.StoreContext.CurrentStore).Returns(new Domain.Stores.Store { Id = "store-1" }); + + var cacheBase = new MemoryCacheBase(new MemoryCache(new MemoryCacheOptions()), new Mock().Object, + new CacheConfig { DefaultCacheTimeMinutes = 1 }); + + _handler = new GetHomePageNewsItemsHandler(cacheBase, contextAccessorMock.Object, _newsServiceMock.Object, + new Mock().Object, new Mock().Object, + new Mock().Object, new NewsSettings(), new MediaSettings(), + _accessControlConfig); + } + + private Task HandleAs(params string[] groups) + { + _currentCustomer = new Customer(); + foreach (var group in groups) _currentCustomer.Groups.Add(group); + return _handler.Handle(new GetHomePageNewsItems(), CancellationToken.None); + } + + [TestMethod] + public async Task Handle_GuestAfterGroupMember_DoesNotSeeGroupLimitedNews() + { + var vip = await HandleAs("registered", "vip"); + var guest = await HandleAs("guests"); + + Assert.IsTrue(vip.NewsItems.Any(x => x.Id == "vip-only")); + Assert.IsFalse(guest.NewsItems.Any(x => x.Id == "vip-only")); + } + + [TestMethod] + public async Task Handle_GroupMemberAfterGuest_SeesGroupLimitedNews() + { + await HandleAs("guests"); + var vip = await HandleAs("registered", "vip"); + + Assert.IsTrue(vip.NewsItems.Any(x => x.Id == "vip-only")); + } + + [TestMethod] + public async Task Handle_IgnoreAcl_SharesOneEntryAcrossGroups() + { + _accessControlConfig.IgnoreAcl = true; + + await HandleAs("guests"); + await HandleAs("registered", "vip"); + + _newsServiceMock.Verify(s => s.GetAllNews(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs index 9b212a3b04..777679219e 100644 --- a/src/Web/Grand.Web.Admin/Controllers/SearchController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/SearchController.cs @@ -128,7 +128,7 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInPages) { - var pages = (await _pageService.GetAllPages("")).Where(x => + var pages = (await _pageService.GetAllPages(CatalogAccess.IncludingHidden())).Where(x => (x.SystemName != null && x.SystemName.ToLower().Contains(searchTerm.ToLower())) || (x.Title != null && x.Title.ToLower().Contains(searchTerm.ToLower()))); foreach (var page in pages) @@ -142,8 +142,8 @@ public async Task Index(string searchTerm, FoundMenuItem[] foundM if (result.Count < _adminSearchSettings.MaxSearchResultsCount && _adminSearchSettings.SearchInNews) { - var news = await _newsService.GetAllNews(storeId: "", newsTitle: searchTerm, - pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, showHidden: true); + var news = await _newsService.GetAllNews(CatalogAccess.IncludingHidden(), newsTitle: searchTerm, + pageSize: _adminSearchSettings.MaxSearchResultsCount - result.Count, includeUnpublished: true); foreach (var signleNews in news) result.Add(new Tuple(new { diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs index 0f25a00708..fad33c836c 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseLoginController.cs @@ -248,7 +248,8 @@ public async Task TwoFactorAuthorization( if (_customerSettings.TwoFactorAuthenticationType != TwoFactorAuthenticationType.AppVerification) { - await twoFactorAuthenticationService.GenerateCodeSetup("", customer, _contextAccessor.WorkContext.WorkingLanguage, + await twoFactorAuthenticationService.GenerateCodeSetup("", customer, + _contextAccessor.StoreContext.CurrentStore, _contextAccessor.WorkContext.WorkingLanguage, _customerSettings.TwoFactorAuthenticationType); if (_customerSettings.TwoFactorAuthenticationType == TwoFactorAuthenticationType.EmailVerification) await _messageProviderService.SendCustomerEmailTokenValidationMessage(customer, diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs index c091153957..e0f8621302 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Cms; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Pages; using Grand.Domain.Permissions; using Grand.Web.AdminShared.Extensions.Mapping; @@ -53,7 +54,7 @@ public async Task List(DataSourceRequest command, PageListModel m { if (scope.DefaultStoreId is not null) model.SearchStoreId = scope.DefaultStoreId; - var pageModels = (await pageService.GetAllPages(model.SearchStoreId, true)) + var pageModels = (await pageService.GetAllPages(CatalogAccess.IncludingHidden(model.SearchStoreId))) .Select(x => x.ToModel(dateTimeService)) .ToList(); diff --git a/src/Web/Grand.Web.AdminShared/Services/NewsViewModelService.cs b/src/Web/Grand.Web.AdminShared/Services/NewsViewModelService.cs index e4c6eede6c..353e4f3740 100644 --- a/src/Web/Grand.Web.AdminShared/Services/NewsViewModelService.cs +++ b/src/Web/Grand.Web.AdminShared/Services/NewsViewModelService.cs @@ -4,6 +4,7 @@ using Grand.Business.Core.Interfaces.Common.Seo; using Grand.Business.Core.Interfaces.Customers; using Grand.Business.Core.Interfaces.Storage; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.News; using Grand.SharedKernel.Extensions; using Grand.Web.AdminShared.Extensions; @@ -49,7 +50,8 @@ public NewsViewModelService(INewsService newsService, public virtual async Task<(IEnumerable newsItemModels, int totalCount)> PrepareNewsItemModel( NewsItemListModel model, int pageIndex, int pageSize) { - var news = await _newsService.GetAllNews(model.SearchStoreId, pageIndex - 1, pageSize, true, true, model.SearchNewsTitle); + var news = await _newsService.GetAllNews(CatalogAccess.IncludingHidden(model.SearchStoreId), pageIndex - 1, pageSize, + includeUnpublished: true, newsTitle: model.SearchNewsTitle); return (news.Select(x => { var m = x.ToModel(_dateTimeService); diff --git a/src/Web/Grand.Web.Common/Filters/ClosedStoreAttribute.cs b/src/Web/Grand.Web.Common/Filters/ClosedStoreAttribute.cs index e9e687e2b3..7782b96838 100644 --- a/src/Web/Grand.Web.Common/Filters/ClosedStoreAttribute.cs +++ b/src/Web/Grand.Web.Common/Filters/ClosedStoreAttribute.cs @@ -4,6 +4,7 @@ using Grand.Data; using Grand.Domain.Stores; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; @@ -91,7 +92,7 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE { //get identifiers of pages are accessible when a store is closed var now = DateTime.UtcNow; - var allowedPageIds = (await pageService.GetAllPages(contextAccessor.StoreContext.CurrentStore.Id)) + var allowedPageIds = (await pageService.GetAllPages(contextAccessor.CatalogAccess())) .Where(t => t.AccessibleWhenStoreClosed && (!t.StartDateUtc.HasValue || t.StartDateUtc < now) && (!t.EndDateUtc.HasValue || t.EndDateUtc > now)) diff --git a/src/Web/Grand.Web.Common/Security/Authorization/PermissionAuthorizeActionAttribute.cs b/src/Web/Grand.Web.Common/Security/Authorization/PermissionAuthorizeActionAttribute.cs index a5b24ec64e..a62d3707bf 100644 --- a/src/Web/Grand.Web.Common/Security/Authorization/PermissionAuthorizeActionAttribute.cs +++ b/src/Web/Grand.Web.Common/Security/Authorization/PermissionAuthorizeActionAttribute.cs @@ -1,5 +1,6 @@ using Grand.Business.Core.Interfaces.Common.Security; using Grand.Domain.Permissions; +using Grand.Infrastructure; using Grand.Web.Common.DataSource; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -35,9 +36,11 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) return; var permissionService = context.HttpContext.RequestServices.GetRequiredService(); + var contextAccessor = context.HttpContext.RequestServices.GetRequiredService(); //check whether current customer has access to a public store - if (await permissionService.AuthorizeAction(permissionAuthorize.Permission, PermissionAction)) + if (await permissionService.AuthorizeAction(permissionAuthorize.Permission, PermissionAction, + contextAccessor.WorkContext.CurrentCustomer)) return; //authorize permission of access to the admin area diff --git a/src/Web/Grand.Web.Store/Controllers/PageController.cs b/src/Web/Grand.Web.Store/Controllers/PageController.cs index 436cc12e32..9591853ce2 100644 --- a/src/Web/Grand.Web.Store/Controllers/PageController.cs +++ b/src/Web/Grand.Web.Store/Controllers/PageController.cs @@ -2,6 +2,7 @@ using Grand.Business.Core.Interfaces.Cms; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Pages; using Grand.Domain.Permissions; using Grand.Web.AdminShared.Controllers; @@ -42,7 +43,7 @@ protected override void EditWarningCheck(Page page) [HttpPost] public async Task StorePagesList(DataSourceRequest command, PageListModel model) { - var pages = await PageService.GetAllPages(Scope.DefaultStoreId, true); + var pages = await PageService.GetAllPages(CatalogAccess.IncludingHidden(Scope.DefaultStoreId)); var pageModels = pages .Where(x => x.LimitedToStores && x.Stores.Count == 1) @@ -65,7 +66,7 @@ public async Task StorePagesList(DataSourceRequest command, PageL [HttpPost] public async Task GlobalPagesList(DataSourceRequest command, PageListModel model) { - var pages = await PageService.GetAllPages(Scope.DefaultStoreId, true); + var pages = await PageService.GetAllPages(CatalogAccess.IncludingHidden(Scope.DefaultStoreId)); var pageModels = pages .Where(x => !x.LimitedToStores || x.Stores.Count > 1) @@ -101,7 +102,7 @@ public async Task Copy(string id) if (page.LimitedToStores && page.Stores.Count <= 1) return RedirectToAction("Edit", new { id }); - var storePages = await PageService.GetAllPages(storeId, true); + var storePages = await PageService.GetAllPages(CatalogAccess.IncludingHidden(storeId)); if (storePages.Any(p => p.Id != page.Id && p.SystemName.Equals(page.SystemName, StringComparison.OrdinalIgnoreCase))) { diff --git a/src/Web/Grand.Web/Components/Footer.cs b/src/Web/Grand.Web/Components/Footer.cs index 956baf2a0a..368b1cdc03 100644 --- a/src/Web/Grand.Web/Components/Footer.cs +++ b/src/Web/Grand.Web/Components/Footer.cs @@ -12,6 +12,7 @@ using Grand.Domain.Vendors; using Grand.Infrastructure; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Models.Common; using Microsoft.AspNetCore.Mvc; @@ -65,7 +66,7 @@ public async Task InvokeAsync() private async Task PrepareFooter() { var now = DateTime.UtcNow; - var pageModel = (await _pageService.GetAllPages(_contextAccessor.StoreContext.CurrentStore.Id)) + var pageModel = (await _pageService.GetAllPages(_contextAccessor.CatalogAccess())) .PreferStoreOverrides(_contextAccessor.StoreContext.CurrentStore.Id) .Where(t => (t.IncludeInFooterRow1 || t.IncludeInFooterRow2 || t.IncludeInFooterRow3) && t.Published && (!t.StartDateUtc.HasValue || t.StartDateUtc < now) && diff --git a/src/Web/Grand.Web/Components/KnowledgebaseCategories.cs b/src/Web/Grand.Web/Components/KnowledgebaseCategories.cs index 9025ea9b2a..150a2c2811 100644 --- a/src/Web/Grand.Web/Components/KnowledgebaseCategories.cs +++ b/src/Web/Grand.Web/Components/KnowledgebaseCategories.cs @@ -3,6 +3,7 @@ using Grand.Domain.Knowledgebase; using Grand.Infrastructure; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Models.Knowledgebase; using Microsoft.AspNetCore.Mvc; @@ -27,7 +28,7 @@ public async Task InvokeAsync(KnowledgebaseHomePageModel m if (!_knowledgebaseSettings.Enabled) return Content(""); - var categories = await _knowledgebaseService.GetPublicKnowledgebaseCategories(); + var categories = await _knowledgebaseService.GetPublicKnowledgebaseCategories(_contextAccessor.CatalogAccess()); foreach (var category in categories) { diff --git a/src/Web/Grand.Web/Components/KnowledgebaseHomepageArticles.cs b/src/Web/Grand.Web/Components/KnowledgebaseHomepageArticles.cs index e28b1a9756..b34e255712 100644 --- a/src/Web/Grand.Web/Components/KnowledgebaseHomepageArticles.cs +++ b/src/Web/Grand.Web/Components/KnowledgebaseHomepageArticles.cs @@ -3,6 +3,7 @@ using Grand.Domain.Knowledgebase; using Grand.Infrastructure; using Grand.Web.Common.Components; +using Grand.Web.Common.Extensions; using Grand.Web.Models.Knowledgebase; using Microsoft.AspNetCore.Mvc; @@ -27,7 +28,7 @@ public async Task InvokeAsync(KnowledgebaseHomePageModel m if (!_knowledgebaseSettings.Enabled) return Content(""); - var articles = await _knowledgebaseService.GetHomepageKnowledgebaseArticles(); + var articles = await _knowledgebaseService.GetHomepageKnowledgebaseArticles(_contextAccessor.CatalogAccess()); foreach (var article in articles) { diff --git a/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs b/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs index cd8e4f75fd..d098c7381b 100644 --- a/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs +++ b/src/Web/Grand.Web/Controllers/KnowledgebaseController.cs @@ -14,6 +14,7 @@ using Grand.Infrastructure.Caching; using Grand.SharedKernel.Attributes; using Grand.Web.Common.Controllers; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Filters; using Grand.Web.Events.Cache; using Grand.Web.Extensions; @@ -83,7 +84,8 @@ public virtual async Task ArticlesByCategory(string categoryId) if (!_knowledgebaseSettings.Enabled) return RedirectToRoute("HomePage"); - var category = await _knowledgebaseService.GetPublicKnowledgebaseCategory(categoryId); + var category = await _knowledgebaseService.GetPublicKnowledgebaseCategory(categoryId, + _contextAccessor.CatalogAccess()); if (category == null) { //Check whether the current user has a "Manage knowledgebase" permission @@ -100,7 +102,8 @@ await _permissionService.Authorize(StandardPermission.ManageKnowledgebase)) } var model = new KnowledgebaseHomePageModel(); - var articles = await _knowledgebaseService.GetPublicKnowledgebaseArticlesByCategory(categoryId); + var articles = await _knowledgebaseService.GetPublicKnowledgebaseArticlesByCategory(categoryId, + _contextAccessor.CatalogAccess()); articles.ForEach(x => model.Items.Add(new KnowledgebaseItemModel { Name = x.GetTranslation(y => y.Name, _contextAccessor.WorkContext.WorkingLanguage.Id), Id = x.Id, @@ -151,8 +154,9 @@ public virtual async Task ItemsByKeyword(string keyword) if (!string.IsNullOrEmpty(keyword)) { - var categories = await _knowledgebaseService.GetPublicKnowledgebaseCategoriesByKeyword(keyword); - var allCategories = await _knowledgebaseService.GetPublicKnowledgebaseCategories(); + var access = _contextAccessor.CatalogAccess(); + var categories = await _knowledgebaseService.GetPublicKnowledgebaseCategoriesByKeyword(keyword, access); + var allCategories = await _knowledgebaseService.GetPublicKnowledgebaseCategories(access); categories.ForEach(x => model.Items.Add(new KnowledgebaseItemModel { Name = x.GetTranslation(y => y.Name, _contextAccessor.WorkContext.WorkingLanguage.Id), Id = x.Id, @@ -161,7 +165,7 @@ public virtual async Task ItemsByKeyword(string keyword) FormattedBreadcrumbs = x.GetFormattedBreadCrumb(allCategories, ">") })); - var articles = await _knowledgebaseService.GetPublicKnowledgebaseArticlesByKeyword(keyword); + var articles = await _knowledgebaseService.GetPublicKnowledgebaseArticlesByKeyword(keyword, access); foreach (var item in articles) { var kbm = new KnowledgebaseItemModel { @@ -170,8 +174,8 @@ public virtual async Task ItemsByKeyword(string keyword) SeName = item.GetTranslation(y => y.SeName, _contextAccessor.WorkContext.WorkingLanguage.Id), IsArticle = true, FormattedBreadcrumbs = - (await _knowledgebaseService.GetPublicKnowledgebaseCategory(item.ParentCategoryId)) - ?.GetFormattedBreadCrumb(allCategories, ">") + + allCategories.FirstOrDefault(x => x.Id == item.ParentCategoryId) + ?.GetFormattedBreadCrumb(allCategories, ">") + " > " + item.GetTranslation(y => y.Name, _contextAccessor.WorkContext.WorkingLanguage.Id) }; model.Items.Add(kbm); @@ -253,7 +257,7 @@ private async Task PrepareKnowledgebaseArticleModel(KnowledgebaseArticleModel mo foreach (var id in article.RelatedArticles) { - var a = await _knowledgebaseService.GetPublicKnowledgebaseArticle(id); + var a = await _knowledgebaseService.GetPublicKnowledgebaseArticle(id, _contextAccessor.CatalogAccess()); if (a != null) model.RelatedArticles.Add(new KnowledgebaseArticleModel { SeName = a.SeName, @@ -290,7 +294,8 @@ public virtual async Task ArticleCommentAdd(KnowledgebaseArticleM if (!_knowledgebaseSettings.Enabled) return RedirectToRoute("HomePage"); - var article = await _knowledgebaseService.GetPublicKnowledgebaseArticle(model.ArticleId); + var article = await _knowledgebaseService.GetPublicKnowledgebaseArticle(model.ArticleId, + _contextAccessor.CatalogAccess()); if (article is not { AllowComments: true }) return RedirectToRoute("HomePage"); diff --git a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs index 222cb9ea21..7752ef63a2 100644 --- a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs +++ b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs @@ -272,8 +272,9 @@ public static class CacheKeyConst /// /// {0} : language ID /// {1} : current store ID + /// {2} : customer group ids /// - public const string HOMEPAGE_NEWSMODEL_KEY = "Grand.pres.news.homepage-{0}-{1}"; + public const string HOMEPAGE_NEWSMODEL_KEY = "Grand.pres.news.homepage-{0}-{1}-{2}"; public const string NEWS_PATTERN_KEY = "Grand.pres.news"; diff --git a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs index b3e07505e1..1e8b434189 100644 --- a/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Catalog/GetMenuHandler.cs @@ -68,7 +68,7 @@ public async Task Handle(GetMenu request, CancellationToken cancellat //top menu pages var now = DateTime.UtcNow; - var pageModel = (await _pageService.GetAllPages(request.Store.Id)) + var pageModel = (await _pageService.GetAllPages(CatalogAccess.For(request.Customer, request.Store.Id))) .PreferStoreOverrides(request.Store.Id) .Where(t => t.Published && t.IncludeInMenu && (!t.StartDateUtc.HasValue || t.StartDateUtc < now) && (!t.EndDateUtc.HasValue || t.EndDateUtc > now)) diff --git a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs index e85959c073..a62c5ae1fa 100644 --- a/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Common/GetSitemapHandler.cs @@ -111,7 +111,7 @@ public async Task Handle(GetSitemap request, CancellationToken can //pages var now = DateTime.UtcNow; - var pages = (await _pageService.GetAllPages(request.Store.Id)) + var pages = (await _pageService.GetAllPages(CatalogAccess.For(request.Customer, request.Store.Id))) .PreferStoreOverrides(request.Store.Id) .Where(t => t.Published && t.IncludeInSitemap && (!t.StartDateUtc.HasValue || t.StartDateUtc < now) && (!t.EndDateUtc.HasValue || t.EndDateUtc > now)) @@ -134,7 +134,8 @@ public async Task Handle(GetSitemap request, CancellationToken can }).ToList(); //knowledge base - var knowledgebasearticles = (await _knowledgebaseService.GetPublicKnowledgebaseArticles()).ToList(); + var knowledgebasearticles = (await _knowledgebaseService.GetPublicKnowledgebaseArticles( + CatalogAccess.For(request.Customer, request.Store.Id))).ToList(); model.KnowledgebaseArticles = knowledgebasearticles.Select(knowledgebasearticle => new KnowledgebaseItemModel { Id = knowledgebasearticle.Id, diff --git a/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs b/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs index b29874e5df..3b520265af 100644 --- a/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/Customers/GetTwoFactorAuthenticationHandler.cs @@ -29,7 +29,7 @@ public GetTwoFactorAuthenticationHandler( { var secretkey = Guid.NewGuid().ToString(); var setupInfo = await _twoFactorAuthenticationService.GenerateCodeSetup(secretkey, request.Customer, - request.Language, _customerSetting.TwoFactorAuthenticationType); + request.Store, request.Language, _customerSetting.TwoFactorAuthenticationType); var model = new CustomerInfoModel.TwoFactorAuthenticationModel { CustomValues = setupInfo.CustomValues, SecretKey = secretkey, diff --git a/src/Web/Grand.Web/Features/Handlers/News/GetHomePageNewsItemsHandler.cs b/src/Web/Grand.Web/Features/Handlers/News/GetHomePageNewsItemsHandler.cs index 7cfde319e5..8485ec446b 100644 --- a/src/Web/Grand.Web/Features/Handlers/News/GetHomePageNewsItemsHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/News/GetHomePageNewsItemsHandler.cs @@ -3,10 +3,13 @@ using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; using Grand.Business.Core.Interfaces.Storage; +using Grand.Domain.Customers; using Grand.Domain.Media; using Grand.Domain.News; using Grand.Infrastructure; using Grand.Infrastructure.Caching; +using Grand.Infrastructure.Configuration; +using Grand.Web.Common.Extensions; using Grand.Web.Events.Cache; using Grand.Web.Features.Models.News; using Grand.Web.Models.Media; @@ -17,6 +20,7 @@ namespace Grand.Web.Features.Handlers.News; public class GetHomePageNewsItemsHandler : IRequestHandler { + private readonly AccessControlConfig _accessControlConfig; private readonly ICacheBase _cacheBase; private readonly IDateTimeService _dateTimeService; private readonly IPictureService _pictureService; @@ -27,11 +31,11 @@ public class GetHomePageNewsItemsHandler : IRequestHandler _contextAccessor.WorkContext.WorkingLanguage.Id; - private string CurrentStoreId => _contextAccessor.StoreContext.CurrentStore.Id; public GetHomePageNewsItemsHandler(ICacheBase cacheBase, IContextAccessor contextAccessor, INewsService newsService, IDateTimeService dateTimeService, IPictureService pictureService, - ITranslationService translationService, NewsSettings newsSettings, MediaSettings mediaSettings) + ITranslationService translationService, NewsSettings newsSettings, MediaSettings mediaSettings, + AccessControlConfig accessControlConfig) { _cacheBase = cacheBase; _contextAccessor = contextAccessor; @@ -41,15 +45,18 @@ public GetHomePageNewsItemsHandler(ICacheBase cacheBase, IContextAccessor contex _translationService = translationService; _newsSettings = newsSettings; _mediaSettings = mediaSettings; + _accessControlConfig = accessControlConfig; } public async Task Handle(GetHomePageNewsItems request, CancellationToken cancellationToken) { - var cacheKey = string.Format(CacheKeyConst.HOMEPAGE_NEWSMODEL_KEY, WorkingLanguageId, CurrentStoreId); + var access = _contextAccessor.CatalogAccess(); + //with the ACL off every customer sees the same news, so the groups stay out of the key + var groupIds = _accessControlConfig.IgnoreAcl ? "" : string.Join(",", access.Customer.GetCustomerGroupIds()); + var cacheKey = string.Format(CacheKeyConst.HOMEPAGE_NEWSMODEL_KEY, WorkingLanguageId, access.StoreId, groupIds); var model = await _cacheBase.GetAsync(cacheKey, async () => { - var newsItems = - await _newsService.GetAllNews(CurrentStoreId, 0, _newsSettings.MainPageNewsCount); + var newsItems = await _newsService.GetAllNews(access, 0, _newsSettings.MainPageNewsCount); var hpnitemodel = new HomePageNewsItemsModel(); foreach (var item in newsItems) { diff --git a/src/Web/Grand.Web/Features/Handlers/News/GetNewsItemListHandler.cs b/src/Web/Grand.Web/Features/Handlers/News/GetNewsItemListHandler.cs index c46145f34e..1b252a3af6 100644 --- a/src/Web/Grand.Web/Features/Handlers/News/GetNewsItemListHandler.cs +++ b/src/Web/Grand.Web/Features/Handlers/News/GetNewsItemListHandler.cs @@ -6,6 +6,7 @@ using Grand.Domain.Media; using Grand.Domain.News; using Grand.Infrastructure; +using Grand.Web.Common.Extensions; using Grand.Web.Features.Models.News; using Grand.Web.Models.Media; using Grand.Web.Models.News; @@ -47,7 +48,7 @@ public async Task Handle(GetNewsItemList request, Cancellatio if (request.Command.PageSize <= 0) request.Command.PageSize = _newsSettings.NewsArchivePageSize; if (request.Command.PageNumber <= 0) request.Command.PageNumber = 1; - var newsItems = await _newsService.GetAllNews(_contextAccessor.StoreContext.CurrentStore.Id, + var newsItems = await _newsService.GetAllNews(_contextAccessor.CatalogAccess(), request.Command.PageNumber - 1, request.Command.PageSize); model.PagingFilteringContext.LoadPagedList(newsItems); foreach (var item in newsItems) diff --git a/src/Web/Grand.Web/Validators/Knowledgebase/KnowledgebaseArticleValidator.cs b/src/Web/Grand.Web/Validators/Knowledgebase/KnowledgebaseArticleValidator.cs index 1d1697e24e..fc2b863ecc 100644 --- a/src/Web/Grand.Web/Validators/Knowledgebase/KnowledgebaseArticleValidator.cs +++ b/src/Web/Grand.Web/Validators/Knowledgebase/KnowledgebaseArticleValidator.cs @@ -8,6 +8,7 @@ using Grand.Infrastructure.Models; using Grand.Infrastructure.Validators; using Grand.SharedKernel.Captcha; +using Grand.Web.Common.Extensions; using Grand.Web.Common.Validators; using Grand.Web.Models.Knowledgebase; @@ -35,7 +36,8 @@ public KnowledgebaseArticleValidator( !knowledgebaseSettings.AllowNotRegisteredUsersToLeaveComments) context.AddFailure( translationService.GetResource("Knowledgebase.Article.Comments.OnlyRegisteredUsersLeaveComments")); - var article = await knowledgebaseService.GetPublicKnowledgebaseArticle(x.ArticleId); + var article = await knowledgebaseService.GetPublicKnowledgebaseArticle(x.ArticleId, + contextAccessor.CatalogAccess()); if (article is not { AllowComments: true }) context.AddFailure(translationService.GetResource("Knowledgebase.Article.Comments.NotAllowed")); });