Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

SlugRouteTransformer.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Nop.Core;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.News;
using Nop.Core.Domain.Seo;
using Nop.Core.Domain.Topics;
using Nop.Core.Domain.Vendors;
using Nop.Core.Events;
using Nop.Core.Http;
using Nop.Services.Catalog;
using Nop.Services.Localization;
using Nop.Services.Seo;
using Nop.Web.Framework.Events;

namespace Nop.Web.Framework.Mvc.Routing;

/// 
/// Represents slug route transformer
/// 
public partial class SlugRouteTransformer : DynamicRouteValueTransformer
{
    #region Fields

    protected readonly CatalogSettings _catalogSettings;
    protected readonly ICategoryService _categoryService;
    protected readonly IEventPublisher _eventPublisher;
    protected readonly ILanguageService _languageService;
    protected readonly IManufacturerService _manufacturerService;
    protected readonly IStoreContext _storeContext;
    protected readonly IUrlRecordService _urlRecordService;
    protected readonly LocalizationSettings _localizationSettings;

    #endregion

    #region Ctor

    public SlugRouteTransformer(CatalogSettings catalogSettings,
        ICategoryService categoryService,
        IEventPublisher eventPublisher,
        ILanguageService languageService,
        IManufacturerService manufacturerService,
        IStoreContext storeContext,
        IUrlRecordService urlRecordService,
        LocalizationSettings localizationSettings)
    {
        _catalogSettings = catalogSettings;
        _categoryService = categoryService;
        _eventPublisher = eventPublisher;
        _languageService = languageService;
        _manufacturerService = manufacturerService;
        _storeContext = storeContext;
        _urlRecordService = urlRecordService;
        _localizationSettings = localizationSettings;
    }

    #endregion

    #region Utilities

    /// 
    /// Transform route values according to the passed URL record
    /// 
    /// HTTP context
    /// The route values associated with the current match
    /// Record found by the URL slug
    /// URL catalog path
    /// A task that represents the asynchronous operation
    protected virtual async Task SingleSlugRoutingAsync(HttpContext httpContext, RouteValueDictionary values, UrlRecord urlRecord, string catalogPath)
    {
        //if URL record is not active let's find the latest one
        var slug = urlRecord.IsActive
            ? urlRecord.Slug
            : await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
        if (string.IsNullOrEmpty(slug))
            return;

        if (!urlRecord.IsActive || !string.IsNullOrEmpty(catalogPath))
        {
            //permanent redirect to new URL with active single slug
            InternalRedirect(httpContext, values, $"/{slug}", true);
            return;
        }

        //Ensure that the slug is the same for the current language, 
        //otherwise it can cause some issues when customers choose a new language but a slug stays the same
        if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled && values.TryGetValue(NopRoutingDefaults.RouteValue.Language, out var langValue))
        {
            var store = await _storeContext.GetCurrentStoreAsync();
            var languages = await _languageService.GetAllLanguagesAsync(storeId: store.Id);
            var language = languages
                               .FirstOrDefault(lang => lang.Published && lang.UniqueSeoCode.Equals(langValue?.ToString(), StringComparison.InvariantCultureIgnoreCase))
                           ?? languages.FirstOrDefault();

            var slugLocalized = await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, language.Id);
            if (!string.IsNullOrEmpty(slugLocalized) && !slugLocalized.Equals(slug, StringComparison.InvariantCultureIgnoreCase))
            {
                //we should make validation above because some entities does not have SeName for standard (Id = 0) language (e.g. news, blog posts)

                //redirect to the page for current language
                InternalRedirect(httpContext, values, $"/{language.UniqueSeoCode}/{slugLocalized}", false);
                return;
            }
        }

        //since we are here, all is ok with the slug, so process URL
        switch (urlRecord.EntityName)
        {
            case var name when name.Equals(nameof(Product), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "Product", "ProductDetails", slug, (NopRoutingDefaults.RouteValue.ProductId, urlRecord.EntityId));
                return;

            case var name when name.Equals(nameof(ProductTag), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "Catalog", "ProductsByTag", slug, (NopRoutingDefaults.RouteValue.ProductTagId, urlRecord.EntityId));
                return;

            case var name when name.Equals(nameof(Category), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "Catalog", "Category", slug, (NopRoutingDefaults.RouteValue.CategoryId, urlRecord.EntityId));
                return;

            case var name when name.Equals(nameof(Manufacturer), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "Catalog", "Manufacturer", slug, (NopRoutingDefaults.RouteValue.ManufacturerId, urlRecord.EntityId));
                return;

            case var name when name.Equals(nameof(Vendor), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "Catalog", "Vendor", slug, (NopRoutingDefaults.RouteValue.VendorId, urlRecord.EntityId));
                return;

            case var name when name.Equals(nameof(NewsItem), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "News", "NewsItem", slug, (NopRoutingDefaults.RouteValue.NewsItemId, urlRecord.EntityId));
                return;

            case var name when name.Equals(nameof(BlogPost), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "Blog", "BlogPost", slug, (NopRoutingDefaults.RouteValue.BlogPostId, urlRecord.EntityId));
                return;

            case var name when name.Equals(nameof(Topic), StringComparison.InvariantCultureIgnoreCase):
                RouteToAction(values, "Topic", "TopicDetails", slug, (NopRoutingDefaults.RouteValue.TopicId, urlRecord.EntityId));
                return;
        }
    }

    /// 
    /// Try transforming the route values, assuming the passed URL record is of a product type
    /// 
    /// HTTP context
    /// The route values associated with the current match
    /// Record found by the URL slug
    /// URL catalog path
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains a value whether the route values were processed
    /// 
    protected virtual async Task TryProductCatalogRoutingAsync(HttpContext httpContext, RouteValueDictionary values, UrlRecord urlRecord, string catalogPath)
    {
        //ensure it's a product URL record
        if (!urlRecord.EntityName.Equals(nameof(Product), StringComparison.InvariantCultureIgnoreCase))
            return false;

        //if the product URL structure type is product seName only, it will be processed later by a single slug
        if (_catalogSettings.ProductUrlStructureTypeId == (int)ProductUrlStructureType.Product)
            return false;

        //get active slug for the product
        var slug = urlRecord.IsActive
            ? urlRecord.Slug
            : await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
        if (string.IsNullOrEmpty(slug))
            return false;

        //try to get active catalog (e.g. category or manufacturer) seName for the product
        var catalogSeName = string.Empty;
        var isCategoryProductUrl = _catalogSettings.ProductUrlStructureTypeId == (int)ProductUrlStructureType.CategoryProduct;
        if (isCategoryProductUrl)
        {
            var productCategory = (await _categoryService.GetProductCategoriesByProductIdAsync(urlRecord.EntityId)).FirstOrDefault();
            var category = await _categoryService.GetCategoryByIdAsync(productCategory?.CategoryId ?? 0);
            catalogSeName = category is not null ? await _urlRecordService.GetSeNameAsync(category) : string.Empty;
        }
        var isManufacturerProductUrl = _catalogSettings.ProductUrlStructureTypeId == (int)ProductUrlStructureType.ManufacturerProduct;
        if (isManufacturerProductUrl)
        {
            var productManufacturer = (await _manufacturerService.GetProductManufacturersByProductIdAsync(urlRecord.EntityId)).FirstOrDefault();
            var manufacturer = await _manufacturerService.GetManufacturerByIdAsync(productManufacturer?.ManufacturerId ?? 0);
            catalogSeName = manufacturer is not null ? await _urlRecordService.GetSeNameAsync(manufacturer) : string.Empty;
        }
        if (string.IsNullOrEmpty(catalogSeName))
            return false;

        //get URL record by the specified catalog path
        var catalogUrlRecord = await _urlRecordService.GetBySlugAsync(catalogPath);
        if (catalogUrlRecord is null ||
            (isCategoryProductUrl && !catalogUrlRecord.EntityName.Equals(nameof(Category), StringComparison.InvariantCultureIgnoreCase)) ||
            (isManufacturerProductUrl && !catalogUrlRecord.EntityName.Equals(nameof(Manufacturer), StringComparison.InvariantCultureIgnoreCase)) ||
            !urlRecord.IsActive)
        {
            //permanent redirect to new URL with active catalog seName and active slug
            InternalRedirect(httpContext, values, $"/{catalogSeName}/{slug}", true);
            return true;
        }

        //ensure the catalog seName and slug are the same for the current language
        if (_localizationSettings.SeoFriendlyUrlsForLanguagesEnabled && values.TryGetValue(NopRoutingDefaults.RouteValue.Language, out var langValue))
        {
            var store = await _storeContext.GetCurrentStoreAsync();
            var languages = await _languageService.GetAllLanguagesAsync(storeId: store.Id);
            var language = languages
                               .FirstOrDefault(lang => lang.Published && lang.UniqueSeoCode.Equals(langValue?.ToString(), StringComparison.InvariantCultureIgnoreCase))
                           ?? languages.FirstOrDefault();

            var slugLocalized = await _urlRecordService.GetActiveSlugAsync(urlRecord.EntityId, urlRecord.EntityName, language.Id);
            var catalogSlugLocalized = await _urlRecordService.GetActiveSlugAsync(catalogUrlRecord.EntityId, catalogUrlRecord.EntityName, language.Id);
            if ((!string.IsNullOrEmpty(slugLocalized) && !slugLocalized.Equals(slug, StringComparison.InvariantCultureIgnoreCase)) ||
                (!string.IsNullOrEmpty(catalogSlugLocalized) && !catalogSlugLocalized.Equals(catalogUrlRecord.Slug, StringComparison.InvariantCultureIgnoreCase)))
            {
                //redirect to localized URL for the current language
                var activeSlug = !string.IsNullOrEmpty(slugLocalized) ? slugLocalized : slug;
                var activeCatalogSlug = !string.IsNullOrEmpty(catalogSlugLocalized) ? catalogSlugLocalized : catalogUrlRecord.Slug;
                InternalRedirect(httpContext, values, $"/{language.UniqueSeoCode}/{activeCatalogSlug}/{activeSlug}", false);
                return true;
            }
        }

        //ensure the specified catalog path is equal to the active catalog seName
        //we do it here after localization check to avoid double redirect
        if (!catalogSeName.Equals(catalogUrlRecord.Slug, StringComparison.InvariantCultureIgnoreCase))
        {
            //permanent redirect to new URL with active catalog seName and active slug
            InternalRedirect(httpContext, values, $"/{catalogSeName}/{slug}", true);
            return true;
        }

        //all is ok, so select the appropriate action
        RouteToAction(values, "Product", "ProductDetails", slug,
            (NopRoutingDefaults.RouteValue.ProductId, urlRecord.EntityId), (NopRoutingDefaults.RouteValue.CatalogSeName, catalogSeName));
        return true;
    }

    /// 
    /// Transform route values to redirect the request
    /// 
    /// HTTP context
    /// The route values associated with the current match
    /// Path
    /// Whether the redirect should be permanent
    protected virtual void InternalRedirect(HttpContext httpContext, RouteValueDictionary values, string path, bool permanent)
    {
        values[NopRoutingDefaults.RouteValue.Controller] = "Common";
        values[NopRoutingDefaults.RouteValue.Action] = "InternalRedirect";
        values[NopRoutingDefaults.RouteValue.Url] = $"{httpContext.Request.PathBase}{path}{httpContext.Request.QueryString}";
        values[NopRoutingDefaults.RouteValue.PermanentRedirect] = permanent;
        httpContext.Items[NopHttpDefaults.GenericRouteInternalRedirect] = true;
    }

    /// 
    /// Transform route values to set controller, action and action parameters
    /// 
    /// The route values associated with the current match
    /// Controller name
    /// Action name
    /// URL slug
    /// Action parameters
    protected virtual void RouteToAction(RouteValueDictionary values, string controller, string action, string slug, params (string Key, object Value)[] parameters)
    {
        values[NopRoutingDefaults.RouteValue.Controller] = controller;
        values[NopRoutingDefaults.RouteValue.Action] = action;
        values[NopRoutingDefaults.RouteValue.SeName] = slug;
        foreach (var (key, value) in parameters)
        {
            values[key] = value;
        }
    }

    #endregion

    #region Methods

    /// 
    /// Create a set of transformed route values that will be used to select an action
    /// 
    /// HTTP context
    /// The route values associated with the current match
    /// 
    /// A task that represents the asynchronous operation
    /// The task result contains the set of values
    /// 
    public override async ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary routeValues)
    {
        //get values to transform for action selection
        var values = new RouteValueDictionary(routeValues);
        if (values is null)
            return values;

        if (!values.TryGetValue(NopRoutingDefaults.RouteValue.SeName, out var slug))
            return values;

        //find record by the URL slug
        if (await _urlRecordService.GetBySlugAsync(slug.ToString()) is not UrlRecord urlRecord)
            return values;

        //allow third-party handlers to select an action by the found URL record
        var routingEvent = new GenericRoutingEvent(httpContext, values, urlRecord);
        await _eventPublisher.PublishAsync(routingEvent);
        if (routingEvent.Handled)
            return values;

        //then try to select an action by the found URL record and the catalog path
        var catalogPath = values.TryGetValue(NopRoutingDefaults.RouteValue.CatalogSeName, out var catalogPathValue)
            ? catalogPathValue.ToString()
            : string.Empty;
        if (await TryProductCatalogRoutingAsync(httpContext, values, urlRecord, catalogPath))
            return values;

        //finally, select an action by the URL record only
        await SingleSlugRoutingAsync(httpContext, values, urlRecord, catalogPath);

        return values;
    }

    #endregion
}