Files
Umbraco-CMS/src/Umbraco.Web.BackOffice/Controllers/TourController.cs

316 lines
12 KiB
C#
Raw Normal View History

using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
Merge remote-tracking branch 'origin/v8/dev' into netcore/dev # Conflicts: # build/NuSpecs/UmbracoCms.Core.nuspec # build/NuSpecs/UmbracoCms.Web.nuspec # src/SolutionInfo.cs # src/Umbraco.Core/Cache/CacheKeys.cs # src/Umbraco.Core/Composing/TypeFinder.cs # src/Umbraco.Core/Configuration/GlobalSettings.cs # src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs # src/Umbraco.Core/Configuration/IGlobalSettings.cs # src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs # src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs # src/Umbraco.Core/Constants-AppSettings.cs # src/Umbraco.Core/Editors/UserEditorAuthorizationHelper.cs # src/Umbraco.Core/Extensions/StringExtensions.cs # src/Umbraco.Core/Extensions/UriExtensions.cs # src/Umbraco.Core/IO/IOHelper.cs # src/Umbraco.Core/IO/PhysicalFileSystem.cs # src/Umbraco.Core/Media/Exif/MathEx.cs # src/Umbraco.Core/Media/UploadAutoFillProperties.cs # src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs # src/Umbraco.Core/Models/Membership/User.cs # src/Umbraco.Core/Models/UserExtensions.cs # src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs # src/Umbraco.Core/PropertyEditors/ListViewConfiguration.cs # src/Umbraco.Core/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs # src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs # src/Umbraco.Core/Routing/AliasUrlProvider.cs # src/Umbraco.Core/Routing/DefaultUrlProvider.cs # src/Umbraco.Core/Routing/UriUtility.cs # src/Umbraco.Core/Routing/UrlProviderExtensions.cs # src/Umbraco.Core/Runtime/CoreRuntime.cs # src/Umbraco.Core/RuntimeOptions.cs # src/Umbraco.Core/RuntimeState.cs # src/Umbraco.Core/Security/BackOfficeUserStore.cs # src/Umbraco.Core/Security/ContentPermissions.cs # src/Umbraco.Core/Sync/ApplicationUrlHelper.cs # src/Umbraco.Core/Trees/TreeNode.cs # src/Umbraco.Core/Udi.cs # src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs # src/Umbraco.Examine/Umbraco.Examine.csproj # src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs # src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs # src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs # src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs # src/Umbraco.Infrastructure/Scoping/Scope.cs # src/Umbraco.Infrastructure/Search/ExamineComponent.cs # src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs # src/Umbraco.Infrastructure/Services/Implement/ContentService.cs # src/Umbraco.Infrastructure/Services/Implement/MediaService.cs # src/Umbraco.Infrastructure/Services/Implement/NotificationService.cs # src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs # src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs # src/Umbraco.Tests.UnitTests/Umbraco.Core/Models/UserExtensionsTests.cs # src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs # src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Examine/UmbracoContentValueSetValidatorTests.cs # src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs # src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config # src/Umbraco.Tests/TestHelpers/SettingsForTests.cs # src/Umbraco.Tests/Testing/TestDatabase.cs # src/Umbraco.Tests/Web/Controllers/ContentControllerUnitTests.cs # src/Umbraco.Tests/Web/Controllers/FilterAllowedOutgoingContentAttributeTests.cs # src/Umbraco.Tests/Web/Controllers/MediaControllerUnitTests.cs # src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs # src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs # src/Umbraco.Web.BackOffice/Controllers/ContentController.cs # src/Umbraco.Web.BackOffice/Controllers/EntityController.cs # src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs # src/Umbraco.Web.BackOffice/Controllers/MediaController.cs # src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs # src/Umbraco.Web.BackOffice/Controllers/TourController.cs # src/Umbraco.Web.BackOffice/Controllers/UserGroupEditorAuthorizationHelper.cs # src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs # src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs # src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs # src/Umbraco.Web.BackOffice/Services/IconService.cs # src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs # src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs # src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs # src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs # src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs # src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js # src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html # src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/config/umbracoSettings.Release.config # src/Umbraco.Web/Cache/MemberCacheRefresher.cs # src/Umbraco.Web/Composing/ModuleInjector.cs # src/Umbraco.Web/Editors/BackOfficeController.cs # src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs # src/Umbraco.Web/Editors/ContentTypeController.cs # src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs # src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs # src/Umbraco.Web/Editors/Filters/UserGroupAuthorizationAttribute.cs # src/Umbraco.Web/Editors/TinyMceController.cs # src/Umbraco.Web/Editors/UserGroupsController.cs # src/Umbraco.Web/Editors/UsersController.cs # src/Umbraco.Web/ImageCropperTemplateExtensions.cs # src/Umbraco.Web/Logging/WebProfiler.cs # src/Umbraco.Web/Logging/WebProfilerProvider.cs # src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs # src/Umbraco.Web/Mvc/EnsurePublishedContentRequestAttribute.cs # src/Umbraco.Web/Mvc/JsonNetResult.cs # src/Umbraco.Web/Mvc/MemberAuthorizeAttribute.cs # src/Umbraco.Web/Mvc/RenderRouteHandler.cs # src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs # src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs # src/Umbraco.Web/RoutableDocumentFilter.cs # src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs # src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs # src/Umbraco.Web/Routing/PublishedRouter.cs # src/Umbraco.Web/Runtime/WebInitialComposer.cs # src/Umbraco.Web/Scheduling/KeepAlive.cs # src/Umbraco.Web/Security/AppBuilderExtensions.cs # src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs # src/Umbraco.Web/Security/Providers/UmbracoMembershipProvider.cs # src/Umbraco.Web/Trees/DictionaryTreeController.cs # src/Umbraco.Web/Trees/LanguageTreeController.cs # src/Umbraco.Web/Trees/LogViewerTreeController.cs # src/Umbraco.Web/Trees/PackagesTreeController.cs # src/Umbraco.Web/UmbracoApplication.cs # src/Umbraco.Web/UmbracoApplicationBase.cs # src/Umbraco.Web/UmbracoInjectedModule.cs # src/Umbraco.Web/WebApi/Filters/AdminUsersAuthorizeAttribute.cs # src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs # src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs # src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs # src/Umbraco.Web/WebApi/MemberAuthorizeAttribute.cs
2021-03-05 15:36:27 +01:00
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Tour;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.DependencyInjection;
2022-03-31 15:57:23 +02:00
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class TourController : UmbracoAuthorizedJsonController
{
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IContentTypeService _contentTypeService;
private readonly TourFilterCollection _filters;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly TourSettings _tourSettings;
// 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]
public TourController(
TourFilterCollection filters,
IHostingEnvironment hostingEnvironment,
IOptionsSnapshot<TourSettings> tourSettings,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IContentTypeService contentTypeService,
IWebHostEnvironment webHostEnvironment)
{
_filters = filters;
_tourSettings = tourSettings.Value;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_contentTypeService = contentTypeService;
_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>())
{
}
public async Task<IEnumerable<BackOfficeTourFile>> GetTours()
{
var result = new List<BackOfficeTourFile>();
if (_tourSettings.EnableTours == false)
{
return result;
}
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
if (user == null)
{
return result;
}
// Get all filters that will be applied to all tour aliases
var aliasOnlyFilters = _filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList();
// Don't pass in any filters for core tours that have a plugin name assigned
var nonPluginFilters = _filters.Where(x => x.PluginName == null).ToList();
// 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)
{
using Stream stream = embeddedTour.CreateReadStream();
await TryParseTourFile(embeddedTour.Name, result, nonPluginFilters, aliasOnlyFilters, stream);
}
// Collect all tour files from packages - /App_Plugins physical or virtual locations
IEnumerable<Tuple<IFileInfo, string>> toursFromPackages = GetTourFiles(_webHostEnvironment.WebRootFileProvider, Constants.SystemDirectories.AppPlugins);
foreach (var tuple in toursFromPackages)
{
string pluginName = tuple.Item2;
var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList();
// Combine matched package filters with filters not specific to a package
var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList();
IFileInfo tourFile = tuple.Item1;
using (Stream stream = tourFile.CreateReadStream())
{
await TryParseTourFile(
tourFile.Name,
result,
combinedFilters,
aliasOnlyFilters,
stream,
pluginName);
}
}
// Get all allowed sections for the current user
var allowedSections = user.AllowedSections.ToList();
var toursToBeRemoved = new List<BackOfficeTourFile>();
// Checking to see if the user has access to the required tour sections, else we remove the tour
foreach (BackOfficeTourFile backOfficeTourFile in result)
{
if (backOfficeTourFile.Tours != null)
{
foreach (BackOfficeTour tour in backOfficeTourFile.Tours)
{
if (tour.RequiredSections != null)
{
foreach (var toursRequiredSection in tour.RequiredSections)
{
if (allowedSections.Contains(toursRequiredSection) == false)
{
toursToBeRemoved.Add(backOfficeTourFile);
break;
}
}
}
}
}
}
return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase);
}
private IEnumerable<Tuple<IFileInfo, string>> GetTourFiles(IFileProvider fileProvider, string folder)
{
IEnumerable<IFileInfo> pluginFolders = fileProvider.GetDirectoryContents(folder).Where(x => x.IsDirectory);
foreach (IFileInfo pluginFolder in pluginFolders)
{
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);
}
}
}
}
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;
}
}
}
}
/// <summary>
/// Gets a tours for a specific doctype.
/// </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();
var doctypeAliasWithCompositions = new List<string> { doctypeAlias };
IContentType? contentType = _contentTypeService.Get(doctypeAlias);
if (contentType != null)
{
doctypeAliasWithCompositions.AddRange(contentType.CompositionAliases());
}
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,
Stream fileStream,
string? pluginName = null)
{
var fileName = Path.GetFileNameWithoutExtension(tourFile);
if (fileName == null)
{
return;
}
// Get the filters specific to this file
var fileFilters = filters.Where(x => x.TourFileName != null && x.TourFileName.IsMatch(fileName)).ToList();
// If there is any filter applied to match the file only (no tour alias) then ignore the file entirely
var isFileFiltered = fileFilters.Any(x => x.TourAlias == null);
if (isFileFiltered)
{
return;
}
// Now combine all aliases to filter below
var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null))
.Select(x => x.TourAlias)
.ToList();
try
{
using var reader = new StreamReader(fileStream, Encoding.UTF8);
var contents = reader.ReadToEnd();
BackOfficeTour[]? tours = JsonConvert.DeserializeObject<BackOfficeTour[]>(contents);
IEnumerable<BackOfficeTour>? backOfficeTours = tours?.Where(x =>
aliasFilters.Count == 0 || aliasFilters.WhereNotNull().All(filter => filter.IsMatch(x.Alias)) == false);
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
var localizedTours = backOfficeTours?.Where(x =>
string.IsNullOrWhiteSpace(x.Culture) || x.Culture.Equals(user?.Language, StringComparison.InvariantCultureIgnoreCase)).ToList();
var tour = new BackOfficeTourFile
{
FileName = Path.GetFileNameWithoutExtension(tourFile),
PluginName = pluginName,
Tours = localizedTours ?? new List<BackOfficeTour>()
};
// Don't add if all of the tours are filtered
if (tour.Tours.Any())
{
result.Add(tour);
}
}
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);
}
}
}