V11: Merge dev to contrib (#13363)

* Bump version

* Add IContextCache to deploy connectors (#13287)

* Add IContextCache and implementations

* Update connector interfaces to use IContextCache

* Minor cleanup

* Move DeployContextCache prefix to constant

* Move default implementations to obsolete methods

* Remove DeployContextCache and DictionaryCache

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Add IContextCache to deploy connectors (#13287)

* Add IContextCache and implementations

* Update connector interfaces to use IContextCache

* Minor cleanup

* Move DeployContextCache prefix to constant

* Move default implementations to obsolete methods

* Remove DeployContextCache and DictionaryCache

Co-authored-by: Andy Butland <abutland73@gmail.com>

* Parse lockId as invariant (#13284)

Co-authored-by: Zeegaan <nge@umbraco.dk>

* Fix Sqlite database locking issue (#13246)

* Add locking for creating scope

* Lock the repository instead

* Add scope in action instead of locking in service

* Fix up post-merge

Co-authored-by: Zeegaan <nge@umbraco.dk>

* Bump version to next minor

* Fix for UseExceptionHandler no longer working since v10.3 RC (#13218)

* Fix for UseExceptionHandler no longer working since v10.3 RC

* Update the management api path to match the new one

Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch>

* New backoffice: Cleanup management API routes (#13296)

* Rename ModelsBuilderDashboard folder to ModelsBuilder

* Fix modelsbuilder paths and related naming

* Rename analytics route to telemetry

* Fix controller bases - routes and tags

* Fix items route

* Fix more controllerbase routes

* Fix route

* Fix OpenApi file

* Merging DictionaryItem and Dictionary

* Fix TrackedReferences naming

* Update OpenApi file

* Rename Analytics* related types to Telemetry*

* New Backoffice: Return AnalyticsLevelViewModel from Telemetry/ (#13298)

* Return TelemetryLevelViewModel instead of TelemetryLevel

* Fix schema

* Change telemetry/current to telemetry/level

(cherry picked from commit f2b8494c669cbbf04b623753abbf1be211973aa9)

* Add contants for tree and recycle-bin subpaths

(cherry picked from commit 4449f56bc00832ea6d357a3854b454791c80e0e2)

Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>

* Updated Smidge, Npoco and MailKit (#13310)

* Updated Smidge, Npoco and MailKit

* Added missing command (after breaking interface in npoco)

* OpenId Connect authentication for new management API (#13318)

* First attempt at OpenIddict

* Making headway and more TODOs

* Redo current policies for multiple schemas + clean up auth controller

* Fix bad merge

* Clean up some more test code

* Fix spacing

* Include AddAuthentication() in OpenIddict addition

* A little more clean-up

* Move application creation to its own implementation + prepare for middleware to handle valid callback URL

* Enable refresh token flow

* Fix bad merge from v11/dev

* Support auth for Swagger and Postman in non-production environments + use default login screen for back-office logins

* Add workaround to client side login handling so the OAuth return URL is not corrupted before redirection

* Add temporary configuration handling for new backoffice

* Restructure the code somewhat, move singular responsibility from management API project

* Add recurring task for cleaning up old tokens in the DB

* Fix bad merge + make auth controller align with the new management API structure

* Explicitly handle the new management API path as a backoffice path (NOTE: this is potentially behaviorally breaking!)

* Redo handle the new management API requests as backoffice requests, this time in a non-breaking way

* Add/update TODOs

* Revert duplication of current auth policies for OpenIddict (as it breaks everything for V11 without the new management APIs) and introduce a dedicated PoC policy setup for OpenIddict.

* Fix failing unit tests

* Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Cms.ManagementApi/Security/BackOfficeApplicationManager.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Update src/Umbraco.Core/Routing/UmbracoRequestPaths.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* V11: Using IFileProvider to access assets added from packages (#13141)

* Creating a FileProviderFactory for getting the package.manifest and grid.editors.config.js files through a file provider

* Collecting the package.manifest-s from different sources

* Searching different sources for grid.editors.config.js

* Using an IFileProvider to collect all tours

* Refactoring IconService.cs

* Typo

* Optimizations when looping through the file system

* Moving WebRootFileProviderFactory to Umbraco.Web.Common proj

* Removes double registering

* pluginLangFileSources includes the localPluginFileSources

* Comments

* Remove linq from foreach

* Change workflow for grid.editors.config.js so we check first physical file, then RCL, then Embedded

* Clean up

* Check if config dir exists

* Discover nested package.manifest files

* Fix IFileInfo.PhysicalPath check

* Revert 712810e1fd995720047832ee689f804185ea69d6 as that way files in content root are preferred over those in web root

* Adding comments

* Refactoring

* Remove PhysicalPath check

* Fix registration of WebRootFileProviderFactory

* Use Swashbuckle instead of NSwag (#13350)

* First attempt at OpenIddict

* Making headway and more TODOs

* Redo current policies for multiple schemas + clean up auth controller

* Fix bad merge

* Clean up some more test code

* Fix spacing

* Include AddAuthentication() in OpenIddict addition

* A little more clean-up

* Move application creation to its own implementation + prepare for middleware to handle valid callback URL

* Enable refresh token flow

* Fix bad merge from v11/dev

* Support auth for Swagger and Postman in non-production environments + use default login screen for back-office logins

* Add workaround to client side login handling so the OAuth return URL is not corrupted before redirection

* Add temporary configuration handling for new backoffice

* Restructure the code somewhat, move singular responsibility from management API project

* Add recurring task for cleaning up old tokens in the DB

* Fix bad merge + make auth controller align with the new management API structure

* Explicitly handle the new management API path as a backoffice path (NOTE: this is potentially behaviorally breaking!)

* Redo handle the new management API requests as backoffice requests, this time in a non-breaking way

* Add/update TODOs

* Replace NSwag with Swashbuckle and clean up unnecessary client secret workaround

* Revert duplication of current auth policies for OpenIddict (as it breaks everything for V11 without the new management APIs) and introduce a dedicated PoC policy setup for OpenIddict.

* Fix failing unit tests

* A little niceness + export new OpenApi.json and fix path in contract unit test

* Redo after merge with v11/dev + filter out unwanted mime types

* Remove CreatedResult and NotFoundObjectResult where possible

* Custom schema IDs - no more "ViewModel" postfix and make generic lists look less clunky too

* A little more explanation for generic schema ID generation

* Force Swashbuckle to use enum string names

* Update OpenApi.json to match new enum string values

* Add clarifying comment about weird looking construct

* add workflow to schema (#13349)

* add workflow to schema

* add licenses to CMSDefinition - intentionally only adding to schema, not registered as options

Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch>
Co-authored-by: Ronald Barendse <ronald@barend.se>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Zeegaan <nge@umbraco.dk>
Co-authored-by: Justin Neville <67802060+justin-nevitech@users.noreply.github.com>
Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com>
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
Co-authored-by: Nathan Woulfe <nathan@nathanw.com.au>
This commit is contained in:
Nikolaj Geisle
2022-11-08 08:21:55 +01:00
committed by GitHub
parent 6cb53fd703
commit 64e7933d57
124 changed files with 7952 additions and 5119 deletions

View File

@@ -1,16 +1,21 @@
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
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;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
@@ -20,22 +25,44 @@ public class TourController : UmbracoAuthorizedJsonController
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IContentTypeService _contentTypeService;
private readonly TourFilterCollection _filters;
private readonly IHostingEnvironment _hostingEnvironment;
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>())
{
_filters = filters;
_hostingEnvironment = hostingEnvironment;
_tourSettings = tourSettings.Value;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_contentTypeService = contentTypeService;
}
public async Task<IEnumerable<BackOfficeTourFile>> GetTours()
@@ -53,69 +80,55 @@ public class TourController : UmbracoAuthorizedJsonController
return result;
}
//get all filters that will be applied to all tour aliases
// 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
// Don't pass in any filters for core tours that have a plugin name assigned
var nonPluginFilters = _filters.Where(x => x.PluginName == null).ToList();
//add core tour files
IEnumerable<string> embeddedTourNames = GetType()
.Assembly
.GetManifestResourceNames()
.Where(x => x.StartsWith("Umbraco.Cms.Web.BackOffice.EmbeddedResources.Tours."));
foreach (var embeddedTourName in embeddedTourNames)
// 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)
{
await TryParseTourFile(embeddedTourName, result, nonPluginFilters, aliasOnlyFilters, async x => await GetContentFromEmbeddedResource(x));
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);
//collect all tour files in packages
var appPlugins = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins);
if (Directory.Exists(appPlugins))
foreach (var tuple in toursFromPackages)
{
foreach (var plugin in Directory.EnumerateDirectories(appPlugins))
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())
{
var pluginName = Path.GetFileName(plugin.TrimEnd(Constants.CharArrays.Backslash));
var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName))
.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;
}
//combine matched package filters with filters not specific to a package
var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList();
foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice"))
{
foreach (var tourDir in Directory.EnumerateDirectories(backofficeDir, "tours"))
{
foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json"))
{
await TryParseTourFile(
tourFile,
result,
combinedFilters,
aliasOnlyFilters,
async x => await System.IO.File.ReadAllTextAsync(x),
pluginName);
}
}
}
await TryParseTourFile(
tourFile.Name,
result,
combinedFilters,
aliasOnlyFilters,
stream,
pluginName);
}
}
//Get all allowed sections for the current user
// 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
// 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)
@@ -140,21 +153,70 @@ public class TourController : UmbracoAuthorizedJsonController
return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase);
}
private async Task<string> GetContentFromEmbeddedResource(string fileName)
private IEnumerable<Tuple<IFileInfo, string>> GetTourFiles(IFileProvider fileProvider, string folder)
{
Stream? resourceStream = GetType().Assembly.GetManifestResourceStream(fileName);
IEnumerable<IFileInfo> pluginFolders = fileProvider.GetDirectoryContents(folder).Where(x => x.IsDirectory);
if (resourceStream is null)
foreach (IFileInfo pluginFolder in pluginFolders)
{
return string.Empty;
}
var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginFolder.Name)).ToList();
using var reader = new StreamReader(resourceStream, Encoding.UTF8);
return await reader.ReadToEndAsync();
// 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
/// Gets a tours for a specific doctype.
/// </summary>
/// <param name="doctypeAlias">The documenttype alias</param>
/// <returns>A <see cref="BackOfficeTour" /></returns>
@@ -190,7 +252,7 @@ public class TourController : UmbracoAuthorizedJsonController
ICollection<BackOfficeTourFile> result,
List<BackOfficeTourFilter> filters,
List<BackOfficeTourFilter> aliasOnlyFilters,
Func<string, Task<string>> fileNameToFileContent,
Stream fileStream,
string? pluginName = null)
{
var fileName = Path.GetFileNameWithoutExtension(tourFile);
@@ -199,24 +261,25 @@ public class TourController : UmbracoAuthorizedJsonController
return;
}
//get the filters specific to this file
// 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
// 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
// Now combine all aliases to filter below
var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null))
.Select(x => x.TourAlias)
.ToList();
try
{
var contents = await fileNameToFileContent(tourFile);
using var reader = new StreamReader(fileStream, Encoding.UTF8);
var contents = reader.ReadToEnd();
BackOfficeTour[]? tours = JsonConvert.DeserializeObject<BackOfficeTour[]>(contents);
IEnumerable<BackOfficeTour>? backOfficeTours = tours?.Where(x =>
@@ -234,7 +297,7 @@ public class TourController : UmbracoAuthorizedJsonController
Tours = localizedTours ?? new List<BackOfficeTour>()
};
//don't add if all of the tours are filtered
// Don't add if all of the tours are filtered
if (tour.Tours.Any())
{
result.Add(tour);