SzkoleniaRiskoff/src/Libraries/Nop.Services/Seo/SitemapGenerator.cs

577 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Nop.Core;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Forums;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.News;
using Nop.Core.Domain.Seo;
using Nop.Core.Events;
using Nop.Services.Blogs;
using Nop.Services.Catalog;
using Nop.Services.Localization;
using Nop.Services.News;
using Nop.Services.Topics;
namespace Nop.Services.Seo
{
/// <summary>
/// Represents a sitemap generator
/// </summary>
public partial class SitemapGenerator : ISitemapGenerator
{
#region Fields
private readonly BlogSettings _blogSettings;
private readonly ForumSettings _forumSettings;
private readonly IActionContextAccessor _actionContextAccessor;
private readonly IBlogService _blogService;
private readonly ICategoryService _categoryService;
private readonly IEventPublisher _eventPublisher;
private readonly ILanguageService _languageService;
private readonly IManufacturerService _manufacturerService;
private readonly INewsService _newsService;
private readonly IProductService _productService;
private readonly IProductTagService _productTagService;
private readonly IStoreContext _storeContext;
private readonly ITopicService _topicService;
private readonly IUrlHelperFactory _urlHelperFactory;
private readonly IUrlRecordService _urlRecordService;
private readonly IWebHelper _webHelper;
private readonly LocalizationSettings _localizationSettings;
private readonly NewsSettings _newsSettings;
private readonly SitemapXmlSettings _sitemapXmlSettings;
#endregion
#region Ctor
public SitemapGenerator(BlogSettings blogSettings,
ForumSettings forumSettings,
IActionContextAccessor actionContextAccessor,
IBlogService blogService,
ICategoryService categoryService,
IEventPublisher eventPublisher,
ILanguageService languageService,
IManufacturerService manufacturerService,
INewsService newsService,
IProductService productService,
IProductTagService productTagService,
IStoreContext storeContext,
ITopicService topicService,
IUrlHelperFactory urlHelperFactory,
IUrlRecordService urlRecordService,
IWebHelper webHelper,
LocalizationSettings localizationSettings,
NewsSettings newsSettings,
SitemapXmlSettings sitemapSettings)
{
_blogSettings = blogSettings;
_forumSettings = forumSettings;
_actionContextAccessor = actionContextAccessor;
_blogService = blogService;
_categoryService = categoryService;
_eventPublisher = eventPublisher;
_languageService = languageService;
_manufacturerService = manufacturerService;
_newsService = newsService;
_productService = productService;
_productTagService = productTagService;
_storeContext = storeContext;
_topicService = topicService;
_urlHelperFactory = urlHelperFactory;
_urlRecordService = urlRecordService;
_webHelper = webHelper;
_localizationSettings = localizationSettings;
_newsSettings = newsSettings;
_sitemapXmlSettings = sitemapSettings;
}
#endregion
#region Utilities
/// <summary>
/// Get UrlHelper
/// </summary>
/// <returns>UrlHelper</returns>
protected virtual IUrlHelper GetUrlHelper()
{
return _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
}
/// <summary>
/// Get HTTP protocol
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the protocol name as string
/// </returns>
protected virtual async Task<string> GetHttpProtocolAsync()
{
var store = await _storeContext.GetCurrentStoreAsync();
return store.SslEnabled ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
}
/// <summary>
/// Generate URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the list of sitemap URLs
/// </returns>
protected virtual async Task<IList<SitemapUrl>> GenerateUrlsAsync()
{
var sitemapUrls = new List<SitemapUrl>
{
//home page
await GetLocalizedSitemapUrlAsync("Homepage"),
//search products
await GetLocalizedSitemapUrlAsync("ProductSearch"),
//contact us
await GetLocalizedSitemapUrlAsync("ContactUs")
};
//news
if (_newsSettings.Enabled)
sitemapUrls.Add(await GetLocalizedSitemapUrlAsync("NewsArchive"));
//blog
if (_blogSettings.Enabled)
sitemapUrls.Add(await GetLocalizedSitemapUrlAsync("Blog"));
//forum
if (_forumSettings.ForumsEnabled)
sitemapUrls.Add(await GetLocalizedSitemapUrlAsync("Boards"));
//categories
if (_sitemapXmlSettings.SitemapXmlIncludeCategories)
sitemapUrls.AddRange(await GetCategoryUrlsAsync());
//manufacturers
if (_sitemapXmlSettings.SitemapXmlIncludeManufacturers)
sitemapUrls.AddRange(await GetManufacturerUrlsAsync());
//products
if (_sitemapXmlSettings.SitemapXmlIncludeProducts)
sitemapUrls.AddRange(await GetProductUrlsAsync());
//product tags
if (_sitemapXmlSettings.SitemapXmlIncludeProductTags)
sitemapUrls.AddRange(await GetProductTagUrlsAsync());
//news
if (_sitemapXmlSettings.SitemapXmlIncludeNews && _newsSettings.Enabled)
sitemapUrls.AddRange(await GetNewsItemUrlsAsync());
//blog posts
if (_sitemapXmlSettings.SitemapXmlIncludeBlogPosts && _blogSettings.Enabled)
sitemapUrls.AddRange(await GetBlogPostUrlsAsync());
//topics
if (_sitemapXmlSettings.SitemapXmlIncludeTopics)
sitemapUrls.AddRange(await GetTopicUrlsAsync());
//custom URLs
if (_sitemapXmlSettings.SitemapXmlIncludeCustomUrls)
sitemapUrls.AddRange(GetCustomUrls());
//event notification
await _eventPublisher.PublishAsync(new SitemapCreatedEvent(sitemapUrls));
return sitemapUrls;
}
/// <summary>
/// Get news item URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap URLs
/// </returns>
protected virtual async Task<IEnumerable<SitemapUrl>> GetNewsItemUrlsAsync()
{
var store = await _storeContext.GetCurrentStoreAsync();
return await (await _newsService.GetAllNewsAsync(storeId: store.Id))
.SelectAwait(async news => await GetLocalizedSitemapUrlAsync("NewsItem",
async lang => new { SeName = await _urlRecordService.GetSeNameAsync(news, news.LanguageId, ensureTwoPublishedLanguages: false) },
news.CreatedOnUtc)).ToListAsync();
}
/// <summary>
/// Get category URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap URLs
/// </returns>
protected virtual async Task<IEnumerable<SitemapUrl>> GetCategoryUrlsAsync()
{
var store = await _storeContext.GetCurrentStoreAsync();
return await (await _categoryService.GetAllCategoriesAsync(storeId: store.Id))
.SelectAwait(async category => await GetLocalizedSitemapUrlAsync("Category", GetSeoRouteParamsAwait(category), category.UpdatedOnUtc)).ToListAsync();
}
/// <summary>
/// Get manufacturer URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap URLs
/// </returns>
protected virtual async Task<IEnumerable<SitemapUrl>> GetManufacturerUrlsAsync()
{
var store = await _storeContext.GetCurrentStoreAsync();
return await (await _manufacturerService.GetAllManufacturersAsync(storeId: store.Id))
.SelectAwait(async manufacturer => await GetLocalizedSitemapUrlAsync("Manufacturer", GetSeoRouteParamsAwait(manufacturer), manufacturer.UpdatedOnUtc)).ToListAsync();
}
/// <summary>
/// Get product URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap URLs
/// </returns>
protected virtual async Task<IEnumerable<SitemapUrl>> GetProductUrlsAsync()
{
var store = await _storeContext.GetCurrentStoreAsync();
return await (await _productService.SearchProductsAsync(0, storeId: store.Id,
visibleIndividuallyOnly: true, orderBy: ProductSortingEnum.CreatedOn))
.SelectAwait(async product => await GetLocalizedSitemapUrlAsync("Product", GetSeoRouteParamsAwait(product), product.UpdatedOnUtc)).ToListAsync();
}
/// <summary>
/// Get product tag URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap URLs
/// </returns>
protected virtual async Task<IEnumerable<SitemapUrl>> GetProductTagUrlsAsync()
{
return await (await _productTagService.GetAllProductTagsAsync())
.SelectAwait(async productTag => await GetLocalizedSitemapUrlAsync("ProductsByTag", GetSeoRouteParamsAwait(productTag))).ToListAsync();
}
/// <summary>
/// Get topic URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap URLs
/// </returns>
protected virtual async Task<IEnumerable<SitemapUrl>> GetTopicUrlsAsync()
{
var store = await _storeContext.GetCurrentStoreAsync();
return await (await _topicService.GetAllTopicsAsync(store.Id)).Where(t => t.IncludeInSitemap)
.SelectAwait(async topic => await GetLocalizedSitemapUrlAsync("Topic", GetSeoRouteParamsAwait(topic))).ToListAsync();
}
/// <summary>
/// Get blog post URLs for the sitemap
/// </summary>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap URLs
/// </returns>
protected virtual async Task<IEnumerable<SitemapUrl>> GetBlogPostUrlsAsync()
{
var store = await _storeContext.GetCurrentStoreAsync();
return await (await _blogService.GetAllBlogPostsAsync(store.Id))
.Where(p => p.IncludeInSitemap)
.SelectAwait(async post => await GetLocalizedSitemapUrlAsync("BlogPost",
async lang => new { SeName = await _urlRecordService.GetSeNameAsync(post, post.LanguageId, ensureTwoPublishedLanguages: false) },
post.CreatedOnUtc)).ToListAsync();
}
/// <summary>
/// Get custom URLs for the sitemap
/// </summary>
/// <returns>Sitemap URLs</returns>
protected virtual IEnumerable<SitemapUrl> GetCustomUrls()
{
var storeLocation = _webHelper.GetStoreLocation();
return _sitemapXmlSettings.SitemapCustomUrls.Select(customUrl =>
new SitemapUrl(string.Concat(storeLocation, customUrl), new List<string>(), UpdateFrequency.Weekly, DateTime.UtcNow));
}
/// <summary>
/// Get route params for URL localization
/// </summary>
/// <typeparam name="T">Model type</typeparam>
/// <param name="model">Model</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the lambda for route params
/// </returns>
protected virtual Func<int?, Task<object>> GetSeoRouteParamsAwait<T>(T model)
where T : BaseEntity, ISlugSupported
{
return async lang => new { SeName = await _urlRecordService.GetSeNameAsync(model, lang) };
}
/// <summary>
/// Write sitemap index file into the stream
/// </summary>
/// <param name="stream">Stream</param>
/// <param name="sitemapNumber">The number of sitemaps</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task WriteSitemapIndexAsync(Stream stream, int sitemapNumber)
{
var urlHelper = GetUrlHelper();
await using var writer = new XmlTextWriter(stream, Encoding.UTF8)
{
Formatting = Formatting.Indented
};
writer.WriteStartDocument();
writer.WriteStartElement("sitemapindex");
writer.WriteAttributeString("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
writer.WriteAttributeString("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
writer.WriteAttributeString("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
writer.WriteAttributeString("xsi:schemaLocation", "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd");
//write URLs of all available sitemaps
for (var id = 1; id <= sitemapNumber; id++)
{
var url = urlHelper.RouteUrl("sitemap-indexed.xml", new { Id = id }, await GetHttpProtocolAsync());
var location = await XmlHelper.XmlEncodeAsync(url);
writer.WriteStartElement("sitemap");
writer.WriteElementString("loc", location);
writer.WriteElementString("lastmod", DateTime.UtcNow.ToString(NopSeoDefaults.SitemapDateFormat));
writer.WriteEndElement();
}
writer.WriteEndElement();
}
/// <summary>
/// Write sitemap file into the stream
/// </summary>
/// <param name="stream">Stream</param>
/// <param name="sitemapUrls">List of sitemap URLs</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task WriteSitemapAsync(Stream stream, IList<SitemapUrl> sitemapUrls)
{
await using var writer = new XmlTextWriter(stream, Encoding.UTF8)
{
Formatting = Formatting.Indented
};
writer.WriteStartDocument();
writer.WriteStartElement("urlset");
writer.WriteAttributeString("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
writer.WriteAttributeString("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
writer.WriteAttributeString("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
writer.WriteAttributeString("xsi:schemaLocation", "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd");
//write URLs from list to the sitemap
foreach (var sitemapUrl in sitemapUrls)
{
//write base url
await WriteSitemapUrlAsync(writer, sitemapUrl);
//write all alternate url if exists
foreach (var alternate in sitemapUrl.AlternateLocations
.Where(p => !p.Equals(sitemapUrl.Location, StringComparison.InvariantCultureIgnoreCase)))
{
await WriteSitemapUrlAsync(writer, new SitemapUrl(alternate, sitemapUrl));
}
}
writer.WriteEndElement();
}
/// <summary>
/// Write sitemap
/// </summary>
/// <param name="writer">XML stream writer</param>
/// <param name="sitemapUrl">Sitemap URL</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task WriteSitemapUrlAsync(XmlTextWriter writer, SitemapUrl sitemapUrl)
{
if (string.IsNullOrEmpty(sitemapUrl.Location))
return;
writer.WriteStartElement("url");
var loc = await XmlHelper.XmlEncodeAsync(sitemapUrl.Location);
writer.WriteElementString("loc", loc);
//write all related url
foreach (var alternate in sitemapUrl.AlternateLocations)
{
if (string.IsNullOrEmpty(alternate))
continue;
//extract seo code
var altLoc = await XmlHelper.XmlEncodeAsync(alternate);
var altLocPath = new Uri(altLoc).PathAndQuery;
var (_, lang) = await altLocPath.IsLocalizedUrlAsync(_actionContextAccessor.ActionContext.HttpContext.Request.PathBase, true);
if (string.IsNullOrEmpty(lang?.UniqueSeoCode))
continue;
writer.WriteStartElement("xhtml:link");
writer.WriteAttributeString("rel", "alternate");
writer.WriteAttributeString("hreflang", lang.UniqueSeoCode);
writer.WriteAttributeString("href", altLoc);
writer.WriteEndElement();
}
writer.WriteElementString("changefreq", sitemapUrl.UpdateFrequency.ToString().ToLowerInvariant());
writer.WriteElementString("lastmod", sitemapUrl.UpdatedOn.ToString(NopSeoDefaults.SitemapDateFormat, CultureInfo.InvariantCulture));
writer.WriteEndElement();
}
/// <summary>
/// This will build an XML sitemap for better index with search engines.
/// See http://en.wikipedia.org/wiki/Sitemaps for more information.
/// </summary>
/// <param name="id">Sitemap identifier</param>
/// <param name="stream">Stream of sitemap.</param>
/// <returns>A task that represents the asynchronous operation</returns>
protected virtual async Task GenerateAsync(Stream stream, int? id)
{
//generate all URLs for the sitemap
var sitemapUrls = await GenerateUrlsAsync();
//split URLs into separate lists based on the max size
var sitemaps = sitemapUrls
.Select((url, index) => new { Index = index, Value = url })
.GroupBy(group => group.Index / NopSeoDefaults.SitemapMaxUrlNumber)
.Select(group => group
.Select(url => url.Value)
.ToList()).ToList();
if (!sitemaps.Any())
return;
if (id.HasValue)
{
//requested sitemap does not exist
if (id.Value == 0 || id.Value > sitemaps.Count)
return;
//otherwise write a certain numbered sitemap file into the stream
await WriteSitemapAsync(stream, sitemaps.ElementAt(id.Value - 1));
}
else
{
//URLs more than the maximum allowable, so generate a sitemap index file
if (sitemapUrls.Count >= NopSeoDefaults.SitemapMaxUrlNumber)
{
//write a sitemap index file into the stream
await WriteSitemapIndexAsync(stream, sitemaps.Count);
}
else
{
//otherwise generate a standard sitemap
await WriteSitemapAsync(stream, sitemaps.First());
}
}
}
#endregion
#region Methods
/// <summary>
/// This will build an XML sitemap for better index with search engines.
/// See http://en.wikipedia.org/wiki/Sitemaps for more information.
/// </summary>
/// <param name="id">Sitemap identifier</param>
/// <returns>
/// A task that represents the asynchronous operation
/// The task result contains the sitemap.xml as string
/// </returns>
public virtual async Task<string> GenerateAsync(int? id)
{
await using var stream = new MemoryStream();
await GenerateAsync(stream, id);
return Encoding.UTF8.GetString(stream.ToArray());
}
/// <summary>
/// Return localized urls
/// </summary>
/// <param name="routeName">Route name</param>
/// <param name="getRouteParamsAwait">Lambda for route params object</param>
/// <param name="dateTimeUpdatedOn">A time when URL was updated last time</param>
/// <param name="updateFreq">How often to update url</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task<SitemapUrl> GetLocalizedSitemapUrlAsync(string routeName,
Func<int?, Task<object>> getRouteParamsAwait = null,
DateTime? dateTimeUpdatedOn = null,
UpdateFrequency updateFreq = UpdateFrequency.Weekly)
{
var urlHelper = GetUrlHelper();
//url for current language
var url = urlHelper.RouteUrl(routeName,
getRouteParamsAwait != null ? await getRouteParamsAwait(null) : null,
await GetHttpProtocolAsync());
var updatedOn = dateTimeUpdatedOn ?? DateTime.UtcNow;
var languages = _localizationSettings.SeoFriendlyUrlsForLanguagesEnabled
? await _languageService.GetAllLanguagesAsync()
: null;
if (languages == null)
return new SitemapUrl(url, new List<string>(), updateFreq, updatedOn);
var pathBase = _actionContextAccessor.ActionContext.HttpContext.Request.PathBase;
//return list of localized urls
var localizedUrls = await languages
.SelectAwait(async lang =>
{
var currentUrl = urlHelper.RouteUrl(routeName,
getRouteParamsAwait != null ? await getRouteParamsAwait(lang.Id) : null,
await GetHttpProtocolAsync());
if (string.IsNullOrEmpty(currentUrl))
return null;
//Extract server and path from url
var scheme = new Uri(currentUrl).GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
var path = new Uri(currentUrl).PathAndQuery;
//Replace seo code
var localizedPath = path
.RemoveLanguageSeoCodeFromUrl(pathBase, true)
.AddLanguageSeoCodeToUrl(pathBase, true, lang);
return new Uri(new Uri(scheme), localizedPath).ToString();
})
.Where(value => !string.IsNullOrEmpty(value))
.ToListAsync();
return new SitemapUrl(url, localizedUrls, updateFreq, updatedOn);
}
#endregion
}
}