2021-03-11 13:20:46 +01:00
|
|
|
using System.Text;
|
2022-11-02 15:26:07 +01:00
|
|
|
using Microsoft.AspNetCore.Hosting;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Microsoft.Extensions.FileProviders;
|
2020-08-19 14:14:01 +01:00
|
|
|
using Microsoft.Extensions.Options;
|
2020-05-18 15:19:52 +02:00
|
|
|
using Newtonsoft.Json;
|
2021-03-05 15:36:27 +01:00
|
|
|
using Umbraco.Cms.Core;
|
2021-02-18 11:06:02 +01:00
|
|
|
using Umbraco.Cms.Core.Configuration.Models;
|
|
|
|
|
using Umbraco.Cms.Core.Models;
|
2022-06-20 08:37:17 +02:00
|
|
|
using Umbraco.Cms.Core.Models.Membership;
|
2022-11-02 15:26:07 +01:00
|
|
|
using Umbraco.Cms.Core.Routing;
|
2021-02-18 11:06:02 +01:00
|
|
|
using Umbraco.Cms.Core.Security;
|
|
|
|
|
using Umbraco.Cms.Core.Services;
|
|
|
|
|
using Umbraco.Cms.Core.Tour;
|
|
|
|
|
using Umbraco.Cms.Web.Common.Attributes;
|
2022-11-02 15:26:07 +01:00
|
|
|
using Umbraco.Cms.Web.Common.DependencyInjection;
|
2022-03-31 15:57:23 +02:00
|
|
|
using Umbraco.Extensions;
|
2022-11-02 15:26:07 +01:00
|
|
|
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
namespace Umbraco.Cms.Web.BackOffice.Controllers;
|
|
|
|
|
|
|
|
|
|
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
|
|
|
|
public class TourController : UmbracoAuthorizedJsonController
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
|
|
|
|
|
private readonly IContentTypeService _contentTypeService;
|
|
|
|
|
private readonly TourFilterCollection _filters;
|
2022-11-02 15:26:07 +01:00
|
|
|
private readonly IWebHostEnvironment _webHostEnvironment;
|
2022-06-20 08:37:17 +02:00
|
|
|
private readonly TourSettings _tourSettings;
|
|
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// IHostingEnvironment is still injected as when removing it, the number of
|
|
|
|
|
// parameters matches with the obsolete ctor and the two ctors become ambiguous
|
|
|
|
|
// [ActivatorUtilitiesConstructor] won't solve the problem in this case
|
|
|
|
|
// IHostingEnvironment can be removed when the obsolete ctor is removed
|
|
|
|
|
[ActivatorUtilitiesConstructor]
|
2022-06-20 08:37:17 +02:00
|
|
|
public TourController(
|
|
|
|
|
TourFilterCollection filters,
|
|
|
|
|
IHostingEnvironment hostingEnvironment,
|
|
|
|
|
IOptionsSnapshot<TourSettings> tourSettings,
|
|
|
|
|
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
2022-11-02 15:26:07 +01:00
|
|
|
IContentTypeService contentTypeService,
|
|
|
|
|
IWebHostEnvironment webHostEnvironment)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
_filters = filters;
|
|
|
|
|
_tourSettings = tourSettings.Value;
|
|
|
|
|
_backofficeSecurityAccessor = backofficeSecurityAccessor;
|
|
|
|
|
_contentTypeService = contentTypeService;
|
2022-11-02 15:26:07 +01:00
|
|
|
_webHostEnvironment = webHostEnvironment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Obsolete("Use other ctor - Will be removed in Umbraco 13")]
|
|
|
|
|
public TourController(
|
|
|
|
|
TourFilterCollection filters,
|
|
|
|
|
IHostingEnvironment hostingEnvironment,
|
|
|
|
|
IOptionsSnapshot<TourSettings> tourSettings,
|
|
|
|
|
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
|
|
|
|
IContentTypeService contentTypeService)
|
|
|
|
|
: this(
|
|
|
|
|
filters,
|
|
|
|
|
hostingEnvironment,
|
|
|
|
|
tourSettings,
|
|
|
|
|
backofficeSecurityAccessor,
|
|
|
|
|
contentTypeService,
|
|
|
|
|
StaticServiceProvider.Instance.GetRequiredService<IWebHostEnvironment>())
|
|
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
public async Task<IEnumerable<BackOfficeTourFile>> GetTours()
|
|
|
|
|
{
|
|
|
|
|
var result = new List<BackOfficeTourFile>();
|
|
|
|
|
|
|
|
|
|
if (_tourSettings.EnableTours == false)
|
|
|
|
|
{
|
|
|
|
|
return result;
|
2020-05-18 15:19:52 +02:00
|
|
|
}
|
|
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
|
|
|
|
|
if (user == null)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
return result;
|
|
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Get all filters that will be applied to all tour aliases
|
2022-06-20 08:37:17 +02:00
|
|
|
var aliasOnlyFilters = _filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Don't pass in any filters for core tours that have a plugin name assigned
|
2022-06-20 08:37:17 +02:00
|
|
|
var nonPluginFilters = _filters.Where(x => x.PluginName == null).ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
|
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Get core tour files
|
|
|
|
|
IFileProvider toursProvider = new EmbeddedFileProvider(GetType().Assembly, "Umbraco.Cms.Web.BackOffice.EmbeddedResources.Tours");
|
|
|
|
|
|
|
|
|
|
IEnumerable<IFileInfo> embeddedTourFiles = toursProvider.GetDirectoryContents(string.Empty)
|
|
|
|
|
.Where(x => !x.IsDirectory && x.Name.EndsWith(".json"));
|
|
|
|
|
|
|
|
|
|
foreach (var embeddedTour in embeddedTourFiles)
|
2022-06-20 08:37:17 +02:00
|
|
|
{
|
2022-11-02 15:26:07 +01:00
|
|
|
using Stream stream = embeddedTour.CreateReadStream();
|
|
|
|
|
await TryParseTourFile(embeddedTour.Name, result, nonPluginFilters, aliasOnlyFilters, stream);
|
2022-06-20 08:37:17 +02:00
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Collect all tour files from packages - /App_Plugins physical or virtual locations
|
|
|
|
|
IEnumerable<Tuple<IFileInfo, string>> toursFromPackages = GetTourFiles(_webHostEnvironment.WebRootFileProvider, Constants.SystemDirectories.AppPlugins);
|
2021-03-11 13:20:46 +01:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
foreach (var tuple in toursFromPackages)
|
2022-06-20 08:37:17 +02:00
|
|
|
{
|
2022-11-02 15:26:07 +01:00
|
|
|
string pluginName = tuple.Item2;
|
|
|
|
|
var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Combine matched package filters with filters not specific to a package
|
|
|
|
|
var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
IFileInfo tourFile = tuple.Item1;
|
|
|
|
|
using (Stream stream = tourFile.CreateReadStream())
|
|
|
|
|
{
|
|
|
|
|
await TryParseTourFile(
|
|
|
|
|
tourFile.Name,
|
|
|
|
|
result,
|
|
|
|
|
combinedFilters,
|
|
|
|
|
aliasOnlyFilters,
|
|
|
|
|
stream,
|
|
|
|
|
pluginName);
|
2020-05-18 15:19:52 +02:00
|
|
|
}
|
2022-06-20 08:37:17 +02:00
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Get all allowed sections for the current user
|
2022-06-20 08:37:17 +02:00
|
|
|
var allowedSections = user.AllowedSections.ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
var toursToBeRemoved = new List<BackOfficeTourFile>();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Checking to see if the user has access to the required tour sections, else we remove the tour
|
2022-06-20 08:37:17 +02:00
|
|
|
foreach (BackOfficeTourFile backOfficeTourFile in result)
|
|
|
|
|
{
|
|
|
|
|
if (backOfficeTourFile.Tours != null)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
foreach (BackOfficeTour tour in backOfficeTourFile.Tours)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
if (tour.RequiredSections != null)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
foreach (var toursRequiredSection in tour.RequiredSections)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
if (allowedSections.Contains(toursRequiredSection) == false)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
toursToBeRemoved.Add(backOfficeTourFile);
|
|
|
|
|
break;
|
2020-05-18 15:19:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase);
|
|
|
|
|
}
|
2021-03-11 13:20:46 +01:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
private IEnumerable<Tuple<IFileInfo, string>> GetTourFiles(IFileProvider fileProvider, string folder)
|
2022-06-20 08:37:17 +02:00
|
|
|
{
|
2022-11-02 15:26:07 +01:00
|
|
|
IEnumerable<IFileInfo> pluginFolders = fileProvider.GetDirectoryContents(folder).Where(x => x.IsDirectory);
|
2021-03-11 13:20:46 +01:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
foreach (IFileInfo pluginFolder in pluginFolders)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-11-02 15:26:07 +01:00
|
|
|
var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginFolder.Name)).ToList();
|
|
|
|
|
|
|
|
|
|
// If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely
|
|
|
|
|
var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null);
|
|
|
|
|
if (isPluginFiltered)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// get the full virtual path for the plugin folder
|
|
|
|
|
var pluginFolderPath = WebPath.Combine(folder, pluginFolder.Name);
|
|
|
|
|
|
|
|
|
|
// loop through the folder(s) in order to find tours
|
|
|
|
|
// - there could be multiple on case sensitive file system
|
|
|
|
|
// Hard-coding the "backoffice" directory name to gain a better performance when traversing the App_Plugins directory
|
|
|
|
|
foreach (var subDir in GetToursFolderPaths(fileProvider, pluginFolderPath, "backoffice"))
|
|
|
|
|
{
|
|
|
|
|
IEnumerable<IFileInfo> tourFiles = fileProvider
|
|
|
|
|
.GetDirectoryContents(subDir)
|
|
|
|
|
.Where(x => x.Name.InvariantEndsWith(".json"));
|
|
|
|
|
|
|
|
|
|
foreach (IFileInfo file in tourFiles)
|
|
|
|
|
{
|
|
|
|
|
yield return new Tuple<IFileInfo, string>(file, pluginFolder.Name);
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-20 08:37:17 +02:00
|
|
|
}
|
2022-11-02 15:26:07 +01:00
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
private static IEnumerable<string> GetToursFolderPaths(IFileProvider fileProvider, string path, string subDirName)
|
|
|
|
|
{
|
|
|
|
|
// Hard-coding the "tours" directory name to gain a better performance when traversing the sub directories
|
|
|
|
|
const string toursDirName = "tours";
|
|
|
|
|
|
|
|
|
|
// It is necessary to iterate through the subfolders because on Linux we'll get casing issues when
|
|
|
|
|
// we try to access {path}/{pluginDirectory.Name}/backoffice/tours directly
|
|
|
|
|
foreach (IFileInfo subDir in fileProvider.GetDirectoryContents(path))
|
|
|
|
|
{
|
|
|
|
|
// InvariantEquals({dirName}) is used to gain a better performance when looking for the tours folder
|
|
|
|
|
if (subDir.IsDirectory && subDir.Name.InvariantEquals(subDirName))
|
|
|
|
|
{
|
|
|
|
|
var virtualPath = WebPath.Combine(path, subDir.Name);
|
|
|
|
|
|
|
|
|
|
if (subDir.Name.InvariantEquals(toursDirName))
|
|
|
|
|
{
|
|
|
|
|
yield return virtualPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var nested in GetToursFolderPaths(fileProvider, virtualPath, toursDirName))
|
|
|
|
|
{
|
|
|
|
|
yield return nested;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-06-20 08:37:17 +02:00
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
/// <summary>
|
2022-11-02 15:26:07 +01:00
|
|
|
/// Gets a tours for a specific doctype.
|
2022-06-20 08:37:17 +02:00
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="doctypeAlias">The documenttype alias</param>
|
|
|
|
|
/// <returns>A <see cref="BackOfficeTour" /></returns>
|
|
|
|
|
public async Task<IEnumerable<BackOfficeTour>> GetToursForDoctype(string doctypeAlias)
|
|
|
|
|
{
|
|
|
|
|
IEnumerable<BackOfficeTourFile> tourFiles = await GetTours();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
var doctypeAliasWithCompositions = new List<string> { doctypeAlias };
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
IContentType? contentType = _contentTypeService.Get(doctypeAlias);
|
|
|
|
|
|
|
|
|
|
if (contentType != null)
|
|
|
|
|
{
|
|
|
|
|
doctypeAliasWithCompositions.AddRange(contentType.CompositionAliases());
|
2020-05-18 15:19:52 +02:00
|
|
|
}
|
|
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
return tourFiles.SelectMany(x => x.Tours)
|
|
|
|
|
.Where(x =>
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(x.ContentType))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IEnumerable<string> contentTypes = x.ContentType
|
|
|
|
|
.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim());
|
|
|
|
|
return contentTypes.Intersect(doctypeAliasWithCompositions).Any();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task TryParseTourFile(
|
|
|
|
|
string tourFile,
|
|
|
|
|
ICollection<BackOfficeTourFile> result,
|
|
|
|
|
List<BackOfficeTourFilter> filters,
|
|
|
|
|
List<BackOfficeTourFilter> aliasOnlyFilters,
|
2022-11-02 15:26:07 +01:00
|
|
|
Stream fileStream,
|
2022-06-20 08:37:17 +02:00
|
|
|
string? pluginName = null)
|
|
|
|
|
{
|
|
|
|
|
var fileName = Path.GetFileNameWithoutExtension(tourFile);
|
|
|
|
|
if (fileName == null)
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Get the filters specific to this file
|
2022-06-20 08:37:17 +02:00
|
|
|
var fileFilters = filters.Where(x => x.TourFileName != null && x.TourFileName.IsMatch(fileName)).ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// If there is any filter applied to match the file only (no tour alias) then ignore the file entirely
|
2022-06-20 08:37:17 +02:00
|
|
|
var isFileFiltered = fileFilters.Any(x => x.TourAlias == null);
|
|
|
|
|
if (isFileFiltered)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Now combine all aliases to filter below
|
2022-06-20 08:37:17 +02:00
|
|
|
var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null))
|
|
|
|
|
.Select(x => x.TourAlias)
|
|
|
|
|
.ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
try
|
|
|
|
|
{
|
2022-11-02 15:26:07 +01:00
|
|
|
using var reader = new StreamReader(fileStream, Encoding.UTF8);
|
|
|
|
|
var contents = reader.ReadToEnd();
|
2022-06-20 08:37:17 +02:00
|
|
|
BackOfficeTour[]? tours = JsonConvert.DeserializeObject<BackOfficeTour[]>(contents);
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
IEnumerable<BackOfficeTour>? backOfficeTours = tours?.Where(x =>
|
|
|
|
|
aliasFilters.Count == 0 || aliasFilters.WhereNotNull().All(filter => filter.IsMatch(x.Alias)) == false);
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
var localizedTours = backOfficeTours?.Where(x =>
|
|
|
|
|
string.IsNullOrWhiteSpace(x.Culture) || x.Culture.Equals(user?.Language, StringComparison.InvariantCultureIgnoreCase)).ToList();
|
2020-05-18 15:19:52 +02:00
|
|
|
|
2022-06-20 08:37:17 +02:00
|
|
|
var tour = new BackOfficeTourFile
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
FileName = Path.GetFileNameWithoutExtension(tourFile),
|
|
|
|
|
PluginName = pluginName,
|
|
|
|
|
Tours = localizedTours ?? new List<BackOfficeTour>()
|
|
|
|
|
};
|
|
|
|
|
|
2022-11-02 15:26:07 +01:00
|
|
|
// Don't add if all of the tours are filtered
|
2022-06-20 08:37:17 +02:00
|
|
|
if (tour.Tours.Any())
|
2020-05-18 15:19:52 +02:00
|
|
|
{
|
2022-06-20 08:37:17 +02:00
|
|
|
result.Add(tour);
|
2020-05-18 15:19:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
2022-06-20 08:37:17 +02:00
|
|
|
catch (IOException e)
|
|
|
|
|
{
|
|
|
|
|
throw new IOException("Error while trying to read file: " + tourFile, e);
|
|
|
|
|
}
|
|
|
|
|
catch (JsonReaderException e)
|
|
|
|
|
{
|
|
|
|
|
throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e);
|
|
|
|
|
}
|
2020-05-18 15:19:52 +02:00
|
|
|
}
|
|
|
|
|
}
|