diff --git a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs index 2666d81310..e0e5c1ab6a 100644 --- a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; - +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Install.InstallSteps { /// @@ -39,7 +39,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps var currentState = FormatGuidState(_runtimeState.CurrentMigrationState); var newState = FormatGuidState(_runtimeState.FinalMigrationState); - var newVersion = _umbracoVersion.SemanticVersion.ToString(); + var newVersion = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); var oldVersion = new SemVersion(_umbracoVersion.SemanticVersion.Major, 0, 0).ToString(); //TODO can we find the old version somehow? e.g. from current state var reportUrl = $"https://our.umbraco.com/contribute/releases/compare?from={oldVersion}&to={newVersion}¬es=1"; diff --git a/src/Umbraco.Core/Notifications/SendEmailNotification.cs b/src/Umbraco.Core/Notifications/SendEmailNotification.cs index 3c9caabb0e..4ca6dc80c0 100644 --- a/src/Umbraco.Core/Notifications/SendEmailNotification.cs +++ b/src/Umbraco.Core/Notifications/SendEmailNotification.cs @@ -7,5 +7,15 @@ namespace Umbraco.Cms.Core.Notifications public SendEmailNotification(NotificationEmailModel message) => Message = message; public NotificationEmailModel Message { get; } + + /// + /// Call to tell Umbraco that the email sending is handled. + /// + public void HandleEmail() => IsHandled = true; + + /// + /// Returns true if the email sending is handled. + /// + public bool IsHandled { get; private set; } } } diff --git a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs index 2f407df88f..b9c0e99552 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationResource.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationResource.cs @@ -12,17 +12,52 @@ namespace Umbraco.Cms.Core.Packaging { public static class PackageMigrationResource { - private static Stream GetEmbeddedPackageStream(Type planType) + private static Stream GetEmbeddedPackageZipStream(Type planType) { // lookup the embedded resource by convention Assembly currentAssembly = planType.Assembly; var fileName = $"{planType.Namespace}.package.zip"; Stream stream = currentAssembly.GetManifestResourceStream(fileName); + + return stream; + } + + public static XDocument GetEmbeddedPackageDataManifest(Type planType, out ZipArchive zipArchive) + { + XDocument packageXml; + var zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) + { + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return packageXml; + } + + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml; + } + + public static XDocument GetEmbeddedPackageDataManifest(Type planType) + { + return GetEmbeddedPackageDataManifest(planType, out _); + } + + private static XDocument GetEmbeddedPackageXmlDoc(Type planType) + { + // lookup the embedded resource by convention + Assembly currentAssembly = planType.Assembly; + var fileName = $"{planType.Namespace}.package.xml"; + Stream stream = currentAssembly.GetManifestResourceStream(fileName); if (stream == null) { - throw new FileNotFoundException("Cannot find the embedded file.", fileName); + return null; } - return stream; + XDocument xml; + using (stream) + { + xml = XDocument.Load(stream); + } + return xml; } public static string GetEmbeddedPackageDataManifestHash(Type planType) @@ -30,17 +65,46 @@ namespace Umbraco.Cms.Core.Packaging // SEE: HashFromStreams in the benchmarks project for how fast this is. It will run // on every startup for every embedded package.zip. The bigger the zip, the more time it takes. // But it is still very fast ~303ms for a 100MB file. This will only be an issue if there are - // several very large package.zips. + // several very large package.zips. - using Stream stream = GetEmbeddedPackageStream(planType); - return stream.GetStreamHash(); + using Stream stream = GetEmbeddedPackageZipStream(planType); + + if (stream is not null) + { + return stream.GetStreamHash(); + } + + var xml = GetEmbeddedPackageXmlDoc(planType); + + if (xml is not null) + { + return xml.ToString(); + } + + throw new IOException("Missing embedded files for planType: " + planType); } - public static ZipArchive GetEmbeddedPackageDataManifest(Type planType, out XDocument packageXml) - => GetPackageDataManifest(GetEmbeddedPackageStream(planType), out packageXml); + public static bool TryGetEmbeddedPackageDataManifest(Type planType, out XDocument packageXml, out ZipArchive zipArchive) + { + var zipStream = GetEmbeddedPackageZipStream(planType); + if (zipStream is not null) + { + zipArchive = GetPackageDataManifest(zipStream, out packageXml); + return true; + } + + zipArchive = null; + packageXml = GetEmbeddedPackageXmlDoc(planType); + return packageXml is not null; + } public static ZipArchive GetPackageDataManifest(Stream packageZipStream, out XDocument packageXml) { + if (packageZipStream == null) + { + throw new ArgumentNullException(nameof(packageZipStream)); + } + var zip = new ZipArchive(packageZipStream, ZipArchiveMode.Read); ZipArchiveEntry packageXmlEntry = zip.GetEntry("package.xml"); if (packageXmlEntry == null) diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index ffc67663cc..a24890d5f2 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -209,29 +209,46 @@ namespace Umbraco.Cms.Core.Packaging PackageDataTypes(definition, root); Dictionary mediaFiles = PackageMedia(definition, root); - var tempPackagePath = temporaryPath + "/package.zip"; - - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) { - ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); - using (Stream entryStream = packageXmlEntry.Open()) + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) { - compiledPackageXml.Save(entryStream); - } - - foreach (KeyValuePair mediaFile in mediaFiles) - { - var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); - using (Stream entryStream = mediaEntry.Open()) - using (mediaFile.Value) + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) { - mediaFile.Value.Seek(0, SeekOrigin.Begin); - mediaFile.Value.CopyTo(entryStream); + compiledPackageXml.Save(entryStream); + } + + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) + { + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); + } } } } + else + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } + } + + var directoryName = _hostingEnvironment.MapPathWebRoot(Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_'))); @@ -241,7 +258,7 @@ namespace Umbraco.Cms.Core.Packaging Directory.CreateDirectory(directoryName); } - var finalPackagePath = Path.Combine(directoryName, "package.zip"); + var finalPackagePath = Path.Combine(directoryName, fileName); if (File.Exists(finalPackagePath)) { @@ -347,7 +364,7 @@ namespace Umbraco.Cms.Core.Packaging } else if (items.ContainsKey(dictionaryItem.ParentId.Value)) { - // we know the parent exists in the dictionary but + // we know the parent exists in the dictionary but // we haven't processed it yet so we'll leave it for the next loop continue; } diff --git a/src/Umbraco.Infrastructure/EmailSender.cs b/src/Umbraco.Infrastructure/EmailSender.cs index 72daad7de1..20c48a3b04 100644 --- a/src/Umbraco.Infrastructure/EmailSender.cs +++ b/src/Umbraco.Infrastructure/EmailSender.cs @@ -3,6 +3,7 @@ using System.Net.Mail; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -23,12 +24,14 @@ namespace Umbraco.Cms.Infrastructure private readonly IEventAggregator _eventAggregator; private readonly GlobalSettings _globalSettings; private readonly bool _notificationHandlerRegistered; + private readonly ILogger _logger; - public EmailSender(IOptions globalSettings, IEventAggregator eventAggregator) + public EmailSender( + ILogger logger, + IOptions globalSettings, + IEventAggregator eventAggregator) : this(globalSettings, eventAggregator, null) - { - - } + => _logger = logger; public EmailSender(IOptions globalSettings, IEventAggregator eventAggregator, INotificationHandler handler) { @@ -49,39 +52,47 @@ namespace Umbraco.Cms.Infrastructure private async Task SendAsyncInternal(EmailMessage message, bool enableNotification) { + if (enableNotification) + { + var notification = new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From)); + await _eventAggregator.PublishAsync(notification); + + // if a handler handled sending the email then don't continue. + if (notification.IsHandled) + { + _logger.LogDebug("The email sending for {Subject} was handled by a notification handler", notification.Message.Subject); + return; + } + } + if (_globalSettings.IsSmtpServerConfigured == false) { - if (enableNotification) - { - await _eventAggregator.PublishAsync( - new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From))); - } + _logger.LogDebug("Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", message.Subject); return; } - using (var client = new SmtpClient()) + using var client = new SmtpClient(); + + await client.ConnectAsync(_globalSettings.Smtp.Host, + _globalSettings.Smtp.Port, + (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); + + if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null)) { - await client.ConnectAsync(_globalSettings.Smtp.Host, - _globalSettings.Smtp.Port, - (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - - if (!(_globalSettings.Smtp.Username is null && _globalSettings.Smtp.Password is null)) - { - await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); - } - - var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); - if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } - - await client.DisconnectAsync(true); + await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); } + + var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); + if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendAsync(mailMessage); + } + else + { + client.Send(mailMessage); + } + + await client.DisconnectAsync(true); } /// diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 3cc19b364f..2e1a2bcd4d 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Events; @@ -15,17 +14,6 @@ namespace Umbraco.Cms.Core.Logging.Serilog { public global::Serilog.ILogger SerilogLog { get; } - /// - /// Initialize a new instance of the class with a configuration file. - /// - /// - public SerilogLogger(FileInfo logConfigFile) - { - SerilogLog = new LoggerConfiguration() - .ReadFrom.AppSettings(filePath: logConfigFile.FullName) - .CreateLogger(); - } - public SerilogLogger(LoggerConfiguration logConfig) { //Configure Serilog static global logger with config passed in diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index f8d480bc8c..4eb9c5ae38 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -212,7 +212,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade .To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}") .To("{4695D0C9-0729-4976-985B-048D503665D8}") .To("{5C424554-A32D-4852-8ED1-A13508187901}") - // to 9.0.0 + // to 9.0.0 RC .With() .To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}") .To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}") @@ -223,12 +223,16 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade //FINAL .As("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); + // TO 9.0.0 + // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. // - 8.15 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} // - 8.15 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} // - 9.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); + + To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs index ef29207093..f350ed633c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 { + public class ExternalLoginTableIndexes : MigrationBase { public ExternalLoginTableIndexes(IMigrationContext context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs new file mode 100644 index 0000000000..5efb914eb7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs @@ -0,0 +1,59 @@ +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +{ + /// + /// Fixes up the original for post RC release to ensure that + /// the correct indexes are applied. + /// + public class ExternalLoginTableIndexesFixup : MigrationBase + { + public ExternalLoginTableIndexesFixup(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + var indexName1 = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; + var indexName2 = "IX_" + ExternalLoginDto.TableName + "_ProviderKey"; + + if (IndexExists(indexName1)) + { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexName1).OnTable(ExternalLoginDto.TableName).Do(); + } + + if (IndexExists(indexName2)) + { + // drop since it's using a column we're about to modify + Delete.Index(indexName2).OnTable(ExternalLoginDto.TableName).Do(); + } + + // then fixup the length of the loginProvider column + AlterColumn(ExternalLoginDto.TableName, "loginProvider"); + + // create it with the correct definition + Create + .Index(indexName1) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userId").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + + // re-create the original + Create + .Index(indexName2) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs index b16326ea56..8eda0f0b45 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs @@ -19,12 +19,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging { internal class ImportPackageBuilderExpression : MigrationExpressionBase { - private readonly IPackagingService _packagingService; - private readonly IMediaService _mediaService; - private readonly MediaFileManager _mediaFileManager; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IShortStringHelper _shortStringHelper; private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; private bool _executed; public ImportPackageBuilderExpression( @@ -45,7 +45,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging } /// - /// The type of the migration which dictates the namespace of the embedded resource + /// The type of the migration which dictates the namespace of the embedded resource /// public Type EmbeddedResourceMigrationType { get; set; } @@ -63,68 +63,77 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) { - throw new InvalidOperationException($"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); + throw new InvalidOperationException( + $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); } InstallationSummary installationSummary; if (EmbeddedResourceMigrationType != null) { - // get the embedded resource - using (ZipArchive zipPackage = PackageMigrationResource.GetEmbeddedPackageDataManifest( + if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( EmbeddedResourceMigrationType, - out XDocument xml)) + out XDocument xml, out ZipArchive zipPackage)) { // first install the package installationSummary = _packagingService.InstallCompiledPackageData(xml); - // then we need to save each file to the saved media items - var mediaWithFiles = xml.XPathSelectElements( - "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") - .ToDictionary( - x => x.AttributeValue("key"), - x => x.AttributeValue("mediaFilePath")); - - // Any existing media by GUID will not be installed by the package service, it will just be skipped - // so you cannot 'update' media (or content) using a package since those are not schema type items. - // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled - // will be empty for any existing media which means that the files will also not be updated. - foreach (IMedia media in installationSummary.MediaInstalled) + if (zipPackage is not null) { - if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + // get the embedded resource + using (zipPackage) { - // this is a media item that has a file, so find that file in the zip - var entryPath = $"media{mediaFilePath.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = zipPackage.GetEntry(entryPath); - if (mediaEntry == null) - { - throw new InvalidOperationException("No media file found in package zip for path " + entryPath); - } + // then we need to save each file to the saved media items + var mediaWithFiles = xml.XPathSelectElements( + "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") + .ToDictionary( + x => x.AttributeValue("key"), + x => x.AttributeValue("mediaFilePath")); - // read the media file and save it to the media item - // using the current file system provider. - using (Stream mediaStream = mediaEntry.Open()) + // Any existing media by GUID will not be installed by the package service, it will just be skipped + // so you cannot 'update' media (or content) using a package since those are not schema type items. + // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled + // will be empty for any existing media which means that the files will also not be updated. + foreach (IMedia media in installationSummary.MediaInstalled) { - media.SetValue( - _mediaFileManager, - _mediaUrlGenerators, - _shortStringHelper, - _contentTypeBaseServiceProvider, - Constants.Conventions.Media.File, - Path.GetFileName(mediaFilePath), - mediaStream); - } + if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + { + // this is a media item that has a file, so find that file in the zip + var entryPath = $"media{mediaFilePath.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = zipPackage.GetEntry(entryPath); + if (mediaEntry == null) + { + throw new InvalidOperationException( + "No media file found in package zip for path " + + entryPath); + } - _mediaService.Save(media); + // read the media file and save it to the media item + // using the current file system provider. + using (Stream mediaStream = mediaEntry.Open()) + { + media.SetValue( + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Constants.Conventions.Media.File, + Path.GetFileName(mediaFilePath), + mediaStream); + } + + _mediaService.Save(media); + } + } } } } - } - else - { - installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); - } + else + { + installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); + } - Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); + Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); + } } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index fcf4ada7c0..2511aab600 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -222,14 +222,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging importedContentTypes.Add(contentTypeAlias, contentType); } - TContentBase content = CreateContentFromXml(root, importedContentTypes[contentTypeAlias], default, parentId, service); - if (content == null) + if (TryCreateContentFromXml(root, importedContentTypes[contentTypeAlias], default, parentId, service, + out TContentBase content)) { - continue; + contents.Add(content); } - contents.Add(content); - var children = root.Elements().Where(doc => (string)doc.Attribute("isDoc") == string.Empty) .ToList(); @@ -262,8 +260,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging } //Create and add the child to the list - var content = CreateContentFromXml(child, importedContentTypes[contentTypeAlias], parent, default, service); - list.Add(content); + if (TryCreateContentFromXml(child, importedContentTypes[contentTypeAlias], parent, default, service, out var content)) + { + list.Add(content); + } //Recursive call var child1 = child; @@ -278,21 +278,24 @@ namespace Umbraco.Cms.Infrastructure.Packaging return list; } - private T CreateContentFromXml( + private bool TryCreateContentFromXml( XElement element, S contentType, T parent, int parentId, - IContentServiceBase service) + IContentServiceBase service, + out T output) where T : class, IContentBase where S : IContentTypeComposition { Guid key = element.RequiredAttributeValue("key"); // we need to check if the content already exists and if so we ignore the installation for this item - if (service.GetById(key) != null) + var value = service.GetById(key); + if (value != null) { - return null; + output = value; + return false; } var level = element.Attribute("level").Value; @@ -383,7 +386,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging } } - return content; + output = content; + return true; } private T CreateContent(string name, T parent, int parentId, S contentType, Guid key, int level, int sortOrder, int? templateId) @@ -498,7 +502,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging //Iterate the sorted document types and create them as IContentType objects foreach (XElement documentType in documentTypes) { - var alias = documentType.Element("Info").Element("Alias").Value; + var alias = documentType.Element("Info").Element("Alias").Value; if (importedContentTypes.ContainsKey(alias) == false) { @@ -1142,7 +1146,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging IDictionaryItem dictionaryItem; var itemName = dictionaryItemElement.Attribute("Name").Value; Guid key = dictionaryItemElement.RequiredAttributeValue("Key"); - + dictionaryItem = _localizationService.GetDictionaryItemById(key); if (dictionaryItem != null) { @@ -1277,7 +1281,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging throw new InvalidOperationException("No path attribute found"); } var contents = element.Value ?? string.Empty; - + var physicalPath = _hostingEnvironment.MapPathContentRoot(path); // TODO: Do we overwrite? IMO I don't think so since these will be views a user will change. if (!System.IO.File.Exists(physicalPath)) @@ -1419,7 +1423,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (partialView == null) { var content = partialViewXml.Value ?? string.Empty; - + partialView = new PartialView(PartialViewType.PartialView, path) { Content = content }; _fileService.SavePartialView(partialView, userId); result.Add(partialView); diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs new file mode 100644 index 0000000000..00be5c51ab --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +{ + /// + /// Allows for specifying custom DB types that are not natively mapped. + /// + public struct SpecialDbType : IEquatable + { + private readonly string _dbType; + + public SpecialDbType(string dbType) + { + if (string.IsNullOrWhiteSpace(dbType)) + { + throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); + } + + _dbType = dbType; + } + + public SpecialDbType(SpecialDbTypes specialDbTypes) + => _dbType = specialDbTypes.ToString(); + + public static SpecialDbType NTEXT { get; } = new SpecialDbType(SpecialDbTypes.NTEXT); + public static SpecialDbType NCHAR { get; } = new SpecialDbType(SpecialDbTypes.NCHAR); + public static SpecialDbType NVARCHARMAX { get; } = new SpecialDbType(SpecialDbTypes.NVARCHARMAX); + + public override bool Equals(object obj) => obj is SpecialDbType types && Equals(types); + public bool Equals(SpecialDbType other) => _dbType == other._dbType; + public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); + + public override string ToString() => _dbType.ToString(); + + // Make this directly castable to string + public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); + + // direct equality operators with SpecialDbTypes enum + public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; + public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs index 158a7ccb9b..d7fd2ff34f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations { @@ -12,13 +12,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations public class SpecialDbTypeAttribute : Attribute { public SpecialDbTypeAttribute(SpecialDbTypes databaseType) - { - DatabaseType = databaseType; - } + => DatabaseType = new SpecialDbType(databaseType); + + public SpecialDbTypeAttribute(string databaseType) + => DatabaseType = new SpecialDbType(databaseType); /// - /// Gets or sets the for this column + /// Gets or sets the for this column /// - public SpecialDbTypes DatabaseType { get; private set; } + public SpecialDbType DatabaseType { get; private set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs index 9d07395743..d867d6f682 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs @@ -1,13 +1,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations { /// - /// Enum with the two special types that has to be supported because - /// of the current umbraco db schema. + /// Known special DB types required for Umbraco. /// public enum SpecialDbTypes { NTEXT, NCHAR, - NVARCHARMAX + NVARCHARMAX, } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs index 2c22863ae5..dee560a40d 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; @@ -12,9 +12,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions //When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. //This type is typically used as part of an initial table creation public Type PropertyType { get; set; } - //Only used for special cases as part of an initial table creation - public bool HasSpecialDbType { get; set; } - public SpecialDbTypes DbType { get; set; } + + /// + /// Used for column types that cannot be natively mapped. + /// + public SpecialDbType? CustomDbType { get; set; } + public virtual int Seeding { get; set; } public virtual int Size { get; set; } public virtual int Precision { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 407672c995..34ad767b04 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Reflection; using NPoco; @@ -75,8 +75,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions var databaseTypeAttribute = propertyInfo.FirstAttribute(); if (databaseTypeAttribute != null) { - definition.HasSpecialDbType = true; - definition.DbType = databaseTypeAttribute.DatabaseType; + definition.CustomDbType = databaseTypeAttribute.DatabaseType; } else { diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 797400b7cc..942368f5cb 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -24,15 +24,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence IEnumerable providerSpecificMapperFactories) { _getFactory = getFactory; - _embeddedDatabaseCreators = embeddedDatabaseCreators.ToDictionary(x=>x.ProviderName); - _syntaxProviders = syntaxProviders.ToDictionary(x=>x.ProviderName); - _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x=>x.ProviderName); - _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x=>x.ProviderName); + _embeddedDatabaseCreators = embeddedDatabaseCreators.ToDictionary(x => x.ProviderName); + _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); + _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); + _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); } public DbProviderFactory CreateFactory(string providerName) { - if (string.IsNullOrEmpty(providerName)) return null; + if (string.IsNullOrEmpty(providerName)) + return null; return _getFactory(providerName); } @@ -40,7 +41,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) { - if(!_syntaxProviders.TryGetValue(providerName, out var result)) + if (!_syntaxProviders.TryGetValue(providerName, out var result)) { throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); } @@ -51,7 +52,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) { - if(!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) + if (!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) { return new BasicBulkSqlInsertProvider(); } @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public void CreateDatabase(string providerName) { - if(_embeddedDatabaseCreators.TryGetValue(providerName, out var creator)) + if (_embeddedDatabaseCreators.TryGetValue(providerName, out var creator)) { creator.Create(); } @@ -69,7 +70,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public NPocoMapperCollection ProviderSpecificMappers(string providerName) { - if(_providerSpecificMapperFactories.TryGetValue(providerName, out var mapperFactory)) + if (_providerSpecificMapperFactories.TryGetValue(providerName, out var mapperFactory)) { return mapperFactory.Mappers; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index 5c56f642c8..69bf1b837e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -28,9 +28,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos /// Used to store the name of the provider (i.e. Facebook, Google) /// [Column("loginProvider")] - [Length(4000)] // TODO: This value seems WAY too high, this is just a name + [Length(400)] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_LoginProvider")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userId", Name = "IX_" + TableName + "_LoginProvider")] public string LoginProvider { get; set; } /// diff --git a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs index 38e6c23e70..c3875d3770 100644 --- a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -6,6 +6,7 @@ using NPoco; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence { @@ -69,22 +70,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence foreach (var col in _columnDefinitions) { SqlDbType sqlDbType; - if (col.HasSpecialDbType) + if (col.CustomDbType.HasValue) { //get the SqlDbType from the 'special type' - switch (col.DbType) + switch (col.CustomDbType) { - case SpecialDbTypes.NTEXT: + case var x when x == SpecialDbType.NTEXT: sqlDbType = SqlDbType.NText; break; - case SpecialDbTypes.NCHAR: + case var x when x == SpecialDbType.NCHAR: sqlDbType = SqlDbType.NChar; break; - case SpecialDbTypes.NVARCHARMAX: + case var x when x == SpecialDbType.NVARCHARMAX: sqlDbType = SqlDbType.NVarChar; break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException("The custom DB type " + col.CustomDbType + " is not supported for bulk import statements."); } } else if (col.Type.HasValue) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 0dd6e2d43c..71bc5b8d33 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -432,11 +432,12 @@ ORDER BY colName"; { var list = new List { - "DELETE FROM umbracoUser2UserGroup WHERE userId = @id", - "DELETE FROM umbracoUser2NodeNotify WHERE userId = @id", - "DELETE FROM umbracoUserStartNode WHERE userId = @id", - "DELETE FROM umbracoUser WHERE id = @id", - "DELETE FROM umbracoExternalLogin WHERE id = @id" + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" }; return list; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs index 004c4f11f4..18e4791d0b 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; @@ -6,24 +6,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax { public class DbTypes { - public DbType DbType; - public string TextDefinition; - public bool ShouldQuoteValue; - public Dictionary ColumnTypeMap = new Dictionary(); - public Dictionary ColumnDbTypeMap = new Dictionary(); - - public void Set(DbType dbType, string fieldDefinition) + public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) { - DbType = dbType; - TextDefinition = fieldDefinition; - ShouldQuoteValue = fieldDefinition != "INTEGER" - && fieldDefinition != "BIGINT" - && fieldDefinition != "DOUBLE" - && fieldDefinition != "DECIMAL" - && fieldDefinition != "BOOL"; - - ColumnTypeMap[typeof(T)] = fieldDefinition; - ColumnDbTypeMap[typeof(T)] = dbType; + ColumnTypeMap = columnTypeMap; + ColumnDbTypeMap = columnDbTypeMap; } + + public IReadOnlyDictionary ColumnTypeMap { get; } + public IReadOnlyDictionary ColumnDbTypeMap { get; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs new file mode 100644 index 0000000000..bf1e0989f5 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Data; + +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +{ + internal class DbTypesFactory + { + private readonly Dictionary _columnTypeMap = new Dictionary(); + private readonly Dictionary _columnDbTypeMap = new Dictionary(); + + public void Set(DbType dbType, string fieldDefinition) + { + _columnTypeMap[typeof(T)] = fieldDefinition; + _columnDbTypeMap[typeof(T)] = dbType; + } + + public DbTypes Create() => new DbTypes(_columnTypeMap, _columnDbTypeMap); + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 6c551648b7..75d348df1a 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Text.RegularExpressions; @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax string GetQuotedName(string name); bool DoesTableExist(IDatabase db, string tableName); string GetIndexType(IndexTypes indexTypes); - string GetSpecialDbType(SpecialDbTypes dbTypes); + string GetSpecialDbType(SpecialDbType dbType); string CreateTable { get; } string DropTable { get; } string AddColumn { get; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index 4c75128926..0093ee14ce 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -22,8 +22,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax DecimalColumnDefinition = "DECIMAL(38,6)"; TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ BlobColumnDefinition = "VARBINARY(MAX)"; - - InitColumnTypeMap(); } public override string RenameTable => "sp_rename '{0}', '{1}'"; @@ -78,7 +76,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// public virtual SqlDbType GetSqlDbType(Type clrType) { - var dbType = DbTypeMap.ColumnDbTypeMap.First(x => x.Key == clrType).Value; + var dbType = DbTypeMap.ColumnDbTypeMap[clrType]; return GetSqlDbType(dbType); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 24548fd36b..753a372e82 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -24,6 +24,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider where TSyntax : ISqlSyntaxProvider { + private readonly Lazy _dbTypes; + protected SqlSyntaxProviderBase() { ClauseOrder = new List> @@ -42,94 +44,96 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); - InitColumnTypeMap(); - // ReSharper disable VirtualMemberCallInConstructor // ok to call virtual GetQuotedXxxName here - they don't depend on any state var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; // ReSharper restore VirtualMemberCallInConstructor AliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + + _dbTypes = new Lazy(InitColumnTypeMap); } public Regex AliasRegex { get; } - public string GetWildcardPlaceholder() - { - return "%"; - } + public string GetWildcardPlaceholder() => "%"; - public string StringLengthNonUnicodeColumnDefinitionFormat = "VARCHAR({0})"; - public string StringLengthUnicodeColumnDefinitionFormat = "NVARCHAR({0})"; - public string DecimalColumnDefinitionFormat = "DECIMAL({0},{1})"; + public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; + public string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; + public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; - public string DefaultValueFormat = "DEFAULT ({0})"; - public int DefaultStringLength = 255; - public int DefaultDecimalPrecision = 20; - public int DefaultDecimalScale = 9; + public string DefaultValueFormat { get; } = "DEFAULT ({0})"; + public int DefaultStringLength { get; } = 255; + public int DefaultDecimalPrecision { get; } = 20; + public int DefaultDecimalScale { get; } = 9; //Set by Constructor - public string StringColumnDefinition; - public string StringLengthColumnDefinitionFormat; + public string StringColumnDefinition { get; } + public string StringLengthColumnDefinitionFormat { get; } - public string AutoIncrementDefinition = "AUTOINCREMENT"; - public string IntColumnDefinition = "INTEGER"; - public string LongColumnDefinition = "BIGINT"; - public string GuidColumnDefinition = "GUID"; - public string BoolColumnDefinition = "BOOL"; - public string RealColumnDefinition = "DOUBLE"; - public string DecimalColumnDefinition; - public string BlobColumnDefinition = "BLOB"; - public string DateTimeColumnDefinition = "DATETIME"; - public string TimeColumnDefinition = "DATETIME"; + public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; + public string IntColumnDefinition { get; protected set; } = "INTEGER"; + public string LongColumnDefinition { get; protected set; } = "BIGINT"; + public string GuidColumnDefinition { get; protected set; } = "GUID"; + public string BoolColumnDefinition { get; protected set; } = "BOOL"; + public string RealColumnDefinition { get; protected set; } = "DOUBLE"; + public string DecimalColumnDefinition { get; protected set; } + public string BlobColumnDefinition { get; protected set; } = "BLOB"; + public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; + public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; + public string TimeColumnDefinition { get; protected set; } = "DATETIME"; protected IList> ClauseOrder { get; } - protected DbTypes DbTypeMap = new DbTypes(); - protected void InitColumnTypeMap() + protected DbTypes DbTypeMap => _dbTypes.Value; + + private DbTypes InitColumnTypeMap() { - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + var dbTypeMap = new DbTypesFactory(); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + + return dbTypeMap.Create(); } public abstract string ProviderName { get; } @@ -193,17 +197,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax return indexType; } - public virtual string GetSpecialDbType(SpecialDbTypes dbTypes) + public virtual string GetSpecialDbType(SpecialDbType dbType) { - if (dbTypes == SpecialDbTypes.NCHAR) + if (dbType == SpecialDbType.NCHAR) { - return "NCHAR"; + return SpecialDbType.NCHAR; } - else if (dbTypes == SpecialDbTypes.NTEXT) + else if (dbType == SpecialDbType.NTEXT) { - return "NTEXT"; + return SpecialDbType.NTEXT; } - else if (dbTypes == SpecialDbTypes.NVARCHARMAX) + else if (dbType == SpecialDbType.NVARCHARMAX) { return "NVARCHAR(MAX)"; } @@ -470,14 +474,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) return column.CustomType; - if (column.HasSpecialDbType) + if (column.CustomDbType.HasValue) { - if (column.Size != default(int)) + if (column.Size != default) { - return $"{GetSpecialDbType(column.DbType)}({column.Size})"; + return $"{GetSpecialDbType(column.CustomDbType.Value)}({column.Size})"; } - return GetSpecialDbType(column.DbType); + return GetSpecialDbType(column.CustomDbType.Value); } var type = column.Type.HasValue @@ -486,19 +490,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax if (type == typeof(string)) { - var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; + var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); } if (type == typeof(decimal)) { - var precision = column.Size != default(int) ? column.Size : DefaultDecimalPrecision; - var scale = column.Precision != default(int) ? column.Precision : DefaultDecimalScale; + var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; return string.Format(DecimalColumnDefinitionFormat, precision, scale); } - var definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; - var dbTypeDefinition = column.Size != default(int) + var definition = DbTypeMap.ColumnTypeMap[type]; + var dbTypeDefinition = column.Size != default ? $"{definition}({column.Size})" : definition; //NOTE Precision is left out diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 9ddb67d611..e3ddc69e6f 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -18,6 +18,7 @@ namespace Umbraco.Cms.Core.Security private string[] _allowedSections; private int[] _startMediaIds; private int[] _startContentIds; + private DateTime? _inviteDateUtc; private static readonly DelegateEqualityComparer s_startIdsComparer = new DelegateEqualityComparer( (groups, enumerable) => groups.UnsortedSequenceEqual(enumerable), @@ -75,6 +76,15 @@ namespace Umbraco.Cms.Core.Security public int[] CalculatedMediaStartNodeIds { get; set; } public int[] CalculatedContentStartNodeIds { get; set; } + /// + /// Gets or sets invite date + /// + public DateTime? InviteDateUtc + { + get => _inviteDateUtc; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _inviteDateUtc, nameof(InviteDateUtc)); + } + /// /// Gets or sets content start nodes assigned to the User (not ones assigned to the user's groups) /// diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index e6a58efa88..deb85ff496 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -438,6 +438,13 @@ namespace Umbraco.Cms.Core.Security user.LastLoginDate = dt; } + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.InviteDateUtc)) + || (user.InvitedDate?.ToUniversalTime() != identityUser.InviteDateUtc)) + { + anythingChanged = true; + user.InvitedDate = identityUser.InviteDateUtc?.ToLocalTime(); + } + if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc)) || (user.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false) || (identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value)) diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 0d0b9fc156..5addc73a3f 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -74,6 +74,7 @@ namespace Umbraco.Cms.Core.Security target.UserName = source.Username; target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime(); target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime(); + target.InviteDateUtc = source.InvitedDate?.ToUniversalTime(); target.EmailConfirmed = source.EmailConfirmedDate.HasValue; target.Name = source.Name; target.AccessFailedCount = source.FailedPasswordAttempts; @@ -87,7 +88,7 @@ namespace Umbraco.Cms.Core.Security target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; } - // TODO: We need to validate this mapping is OK, we need to get Umbraco.Code working + // Umbraco.Code.MapAll -Id -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled -ConcurrencyStamp -NormalizedEmail -NormalizedUserName -Roles private void Map(IMember source, MemberIdentityUser target) { target.Email = source.Email; diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 7a7bc628b8..2f709d5d27 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -41,12 +41,12 @@ - + diff --git a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs index 00cbdce532..c7e6df8cb2 100644 --- a/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs +++ b/src/Umbraco.Infrastructure/WebAssets/BackOfficeWebAssets.cs @@ -55,15 +55,17 @@ namespace Umbraco.Cms.Infrastructure.WebAssets _runtimeMinifier.CreateCssBundle(UmbracoInitCssBundleName, BundlingOptions.NotOptimizedAndComposite, - FormatPaths("lib/bootstrap-social/bootstrap-social.css", - "assets/css/umbraco.min.css", - "lib/font-awesome/css/font-awesome.min.css")); + FormatPaths( + "assets/css/umbraco.min.css", + "lib/bootstrap-social/bootstrap-social.css", + "lib/font-awesome/css/font-awesome.min.css")); _runtimeMinifier.CreateCssBundle(UmbracoUpgradeCssBundleName, BundlingOptions.NotOptimizedAndComposite, - FormatPaths("assets/css/umbraco.min.css", - "lib/bootstrap-social/bootstrap-social.css", - "lib/font-awesome/css/font-awesome.min.css")); + FormatPaths( + "assets/css/umbraco.min.css", + "lib/bootstrap-social/bootstrap-social.css", + "lib/font-awesome/css/font-awesome.min.css")); _runtimeMinifier.CreateCssBundle(UmbracoPreviewCssBundleName, BundlingOptions.NotOptimizedAndComposite, diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs index e81b6135da..62aa933a04 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -25,10 +25,10 @@ namespace Umbraco.Cms.Persistence.SqlCe { _globalSettings = globalSettings; BlobColumnDefinition = "IMAGE"; - // This is silly to have to do this but the way these inherited classes are structured it's the easiest - // way without an overhaul in type map initialization - DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); - + // NOTE: if this column type is used in sqlce, it will prob result in errors since + // SQLCE cannot support this type correctly without 2x columns and a lot of work arounds. + // We don't use this natively within Umbraco but 3rd parties might with SQL server. + DateTimeOffsetColumnDefinition = "DATETIME"; } public override string ProviderName => Constants.DatabaseProviders.SqlCe; @@ -300,10 +300,14 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() GetQuotedTableName(index.TableName), columns); } - public override string GetSpecialDbType(SpecialDbTypes dbTypes) + public override string GetSpecialDbType(SpecialDbType dbTypes) { - if (dbTypes == SpecialDbTypes.NVARCHARMAX) // SqlCE does not have nvarchar(max) for now + // SqlCE does not have nvarchar(max) for now + if (dbTypes == SpecialDbType.NVARCHARMAX) + { return "NTEXT"; + } + return base.GetSpecialDbType(dbTypes); } public override SqlDbType GetSqlDbType(DbType dbType) diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 59b970ebbc..71b9ac3ef9 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -181,8 +181,8 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest app.UseUmbraco() .WithMiddleware(u => { - u.WithBackOffice(); - u.WithWebsite(); + u.UseBackOffice(); + u.UseWebsite(); }) .WithEndpoints(u => { diff --git a/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs index 3ee9b1375c..74d364256f 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Core/Packaging/CreatedPackagesRepositoryTests.cs @@ -8,6 +8,7 @@ using System.IO.Compression; using System.Linq; using System.Xml.Linq; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; @@ -36,7 +37,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging [TearDown] public void DeleteTestFolder() => Directory.Delete(HostingEnvironment.MapPathContentRoot("~/" + _testBaseFolder), true); - + private IContentService ContentService => GetRequiredService(); private IContentTypeService ContentTypeService => GetRequiredService(); @@ -164,7 +165,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging { var parent = new DictionaryItem("Parent") { - Key = Guid.NewGuid() + Key = Guid.NewGuid() }; LocalizationService.Save(parent); var child1 = new DictionaryItem(parent.Key, "Child1") @@ -204,12 +205,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging }; PackageBuilder.SavePackage(def); - + string packageXmlPath = PackageBuilder.ExportPackage(def); - using (var packageZipStream = File.OpenRead(packageXmlPath)) - using (ZipArchive zipArchive = PackageMigrationResource.GetPackageDataManifest(packageZipStream, out XDocument packageXml)) + using (var packageXmlStream = File.OpenRead(packageXmlPath)) { + var packageXml = XDocument.Load(packageXmlStream); var dictionaryItems = packageXml.Root.Element("DictionaryItems"); Assert.IsNotNull(dictionaryItems); var rootItems = dictionaryItems.Elements("DictionaryItem").ToList(); @@ -226,7 +227,53 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging } [Test] - public void Export() + public void Export_Zip() + { + var mt = MediaTypeBuilder.CreateImageMediaType("testImage"); + MediaTypeService.Save(mt); + var m1 = MediaBuilder.CreateMediaFile(mt, -1); + MediaService.Save(m1); + + //Ensure a file exist + var fullPath = HostingEnvironment.MapPathWebRoot(m1.Properties[Constants.Conventions.Media.File].GetValue().ToString()); + using (StreamWriter file1 = File.CreateText(fullPath)) + { + file1.WriteLine("hello"); + } + + var def = new PackageDefinition + { + Name = "test", + MediaUdis = new List(){m1.GetUdi()} + }; + + bool result = PackageBuilder.SavePackage(def); + Assert.IsTrue(result); + Assert.IsTrue(def.PackagePath.IsNullOrWhiteSpace()); + + string packageXmlPath = PackageBuilder.ExportPackage(def); + + def = PackageBuilder.GetById(def.Id); // re-get + Assert.IsNotNull(def.PackagePath); + + using (FileStream packageZipStream = File.OpenRead(packageXmlPath)) + using (ZipArchive zipArchive = PackageMigrationResource.GetPackageDataManifest(packageZipStream, out XDocument packageXml)) + { + Assert.AreEqual("umbPackage", packageXml.Root.Name.ToString()); + Assert.IsNotNull(zipArchive.GetEntry("media/media/test-file.txt")); + + Assert.AreEqual( + $"", + packageXml.Element("umbPackage").Element("MediaItems").ToString(SaveOptions.DisableFormatting)); + + // TODO: There's a whole lot more assertions to be done + + } + } + + + [Test] + public void Export_Xml() { var template = TemplateBuilder.CreateTextPageTemplate(); @@ -242,19 +289,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Packaging Assert.IsTrue(result); Assert.IsTrue(def.PackagePath.IsNullOrWhiteSpace()); - string packageXmlPath = PackageBuilder.ExportPackage(def); + string packageXmlPath = PackageBuilder.ExportPackage(def); // Get def = PackageBuilder.GetById(def.Id); // re-get Assert.IsNotNull(def.PackagePath); - using (FileStream packageZipStream = File.OpenRead(packageXmlPath)) - using (ZipArchive zipArchive = PackageMigrationResource.GetPackageDataManifest(packageZipStream, out XDocument packageXml)) + using (var packageXmlStream = File.OpenRead(packageXmlPath)) { - Assert.AreEqual("umbPackage", packageXml.Root.Name.ToString()); + var xml = XDocument.Load(packageXmlStream); + Assert.AreEqual("umbPackage", xml.Root.Name.ToString()); - Assert.AreEqual( - $"", - packageXml.Element("umbPackage").Element("Templates").ToString(SaveOptions.DisableFormatting)); + Assert.AreEqual($"", xml.Element("umbPackage").Element("Templates").ToString(SaveOptions.DisableFormatting)); // TODO: There's a whole lot more assertions to be done diff --git a/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index f432787dc4..48e83b9dcf 100644 --- a/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -8,9 +8,11 @@ using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; +using Castle.Core.Logging; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; @@ -137,7 +139,7 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers public static UriUtility UriUtility => s_testHelperInternal.UriUtility; - public static IEmailSender EmailSender { get; } = new EmailSender(Options.Create(new GlobalSettings()), Mock.Of()); + public static IEmailSender EmailSender { get; } = new EmailSender(new NullLogger(), Options.Create(new GlobalSettings()), Mock.Of()); /// /// Some test files are copied to the /bin (/bin/debug) on build, this is a utility to return their physical path based on a virtual path name diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 3cddeb435e..b5d7373dc3 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index ae7776dfaf..cf7a7dd729 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -181,14 +181,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } else { - var opt = _externalAuthenticationOptions.Get(authType.Name); + BackOfficeExternaLoginProviderScheme opt = await _externalAuthenticationOptions.GetAsync(authType.Name); if (opt == null) { return BadRequest($"Could not find external authentication options registered for provider {unlinkLoginModel.LoginProvider}"); } else { - if (!opt.Options.AutoLinkOptions.AllowManualLinking) + if (!opt.ExternalLoginProvider.Options.AutoLinkOptions.AllowManualLinking) { // If AllowManualLinking is disabled for this provider we cannot unlink return BadRequest(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index ec41ddcfaa..a52e018f58 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -33,6 +33,7 @@ using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; +using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -49,6 +50,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // this controller itself doesn't require authz but it's more clear what the intention is. private readonly IBackOfficeUserManager _userManager; + private readonly IRuntimeState _runtimeState; private readonly IRuntimeMinifier _runtimeMinifier; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; @@ -68,6 +70,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public BackOfficeController( IBackOfficeUserManager userManager, + IRuntimeState runtimeState, IRuntimeMinifier runtimeMinifier, IOptions globalSettings, IHostingEnvironment hostingEnvironment, @@ -86,6 +89,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ServerVariablesParser serverVariables) { _userManager = userManager; + _runtimeState = runtimeState; _runtimeMinifier = runtimeMinifier; _globalSettings = globalSettings.Value; _hostingEnvironment = hostingEnvironment; @@ -108,6 +112,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [AllowAnonymous] public async Task Default() { + // TODO: It seems that if you login during an authorize upgrade and the upgrade fails, you can still + // access the back office. This should redirect to the installer in that case? + // force authentication to occur since this is not an authorized endpoint var result = await this.AuthenticateBackOfficeAsync(); @@ -414,7 +421,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ViewData.FromBase64CookieData(_httpContextAccessor.HttpContext, ViewDataExtensions.TokenExternalSignInError, _jsonSerializer) || ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) || ViewData.FromTempData(TempData, ViewDataExtensions.TokenPasswordResetCode)) + { return defaultResponse(); + } //First check if there's external login info, if there's not proceed as normal var loginInfo = await _signInManager.GetExternalLoginInfoAsync(); @@ -444,16 +453,23 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (response == null) throw new ArgumentNullException(nameof(response)); // Sign in the user with this external login provider (which auto links, etc...) - var result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false); + SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false); var errors = new List(); - if (result == Microsoft.AspNetCore.Identity.SignInResult.Success) + if (result == SignInResult.Success) { // Update any authentication tokens if succeeded await _signInManager.UpdateExternalAuthenticationTokensAsync(loginInfo); + + // Check if we are in an upgrade state, if so we need to redirect + if (_runtimeState.Level == Core.RuntimeLevel.Upgrade) + { + // redirect to the the installer + return Redirect("/"); + } } - else if (result == Microsoft.AspNetCore.Identity.SignInResult.TwoFactorRequired) + else if (result == SignInResult.TwoFactorRequired) { var attemptedUser = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); @@ -481,17 +497,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return verifyResponse; } - else if (result == Microsoft.AspNetCore.Identity.SignInResult.LockedOut) + else if (result == SignInResult.LockedOut) { errors.Add($"The local user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out."); } - else if (result == Microsoft.AspNetCore.Identity.SignInResult.NotAllowed) + else if (result == SignInResult.NotAllowed) { // This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails // however since we don't enforce those rules (yet) this shouldn't happen. errors.Add($"The user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in."); } - else if (result == Microsoft.AspNetCore.Identity.SignInResult.Failed) + else if (result == SignInResult.Failed) { // Failed only occurs when the user does not exist errors.Add("The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account, the provider must be linked from the back office."); @@ -508,6 +524,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { errors.AddRange(autoLinkSignInResult.Errors); } + else if (!result.Succeeded) + { + // this shouldn't occur, the above should catch the correct error but we'll be safe just in case + errors.Add($"An unknown error with the requested provider ({loginInfo.LoginProvider}) occurred."); + } if (errors.Count > 0) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index c3fb203ec1..61c1660dd0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -144,7 +144,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// Returns the server variables for authenticated users /// /// - internal Task> GetServerVariablesAsync() + internal async Task> GetServerVariablesAsync() { var globalSettings = _globalSettings; var backOfficeControllerName = ControllerExtensions.GetControllerName(); @@ -432,12 +432,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { // TODO: It would be nicer to not have to manually translate these properties // but then needs to be changed in quite a few places in angular - "providers", _externalLogins.GetBackOfficeProviders() + "providers", (await _externalLogins.GetBackOfficeProvidersAsync()) .Select(p => new { - authType = p.AuthenticationType, - caption = p.Name, - properties = p.Options + authType = p.ExternalLoginProvider.AuthenticationType, + caption = p.AuthenticationScheme.DisplayName, + properties = p.ExternalLoginProvider.Options }) .ToArray() } @@ -456,7 +456,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } }; - return Task.FromResult(defaultVals); + + return defaultVals; } [DataContract] diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 3d08d2dab1..00e486fad5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using MimeKit; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; @@ -551,7 +552,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings), new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail }); - var mailMessage = new EmailMessage(fromEmail, to.Email, emailSubject, emailBody, true); + // This needs to be in the correct mailto format including the name, else + // the name cannot be captured in the email sending notification. + // i.e. "Some Person" + var toMailBoxAddress = new MailboxAddress(to.Name, to.Email); + + var mailMessage = new EmailMessage(fromEmail, toMailBoxAddress.ToString(), emailSubject, emailBody, true); await _emailSender.SendAsync(mailMessage, true); } diff --git a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs index 3340988714..fee75f6eae 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -54,18 +54,18 @@ namespace Umbraco.Extensions /// /// /// - public static Task AngularValueExternalLoginInfoScriptAsync(this IHtmlHelper html, + public static async Task AngularValueExternalLoginInfoScriptAsync(this IHtmlHelper html, IBackOfficeExternalLoginProviders externalLogins, BackOfficeExternalLoginProviderErrors externalLoginErrors) { - var providers = externalLogins.GetBackOfficeProviders(); + var providers = await externalLogins.GetBackOfficeProvidersAsync(); var loginProviders = providers .Select(p => new { - authType = p.AuthenticationType, - caption = p.Name, - properties = p.Options + authType = p.ExternalLoginProvider.AuthenticationType, + caption = p.AuthenticationScheme.DisplayName, + properties = p.ExternalLoginProvider.Options }) .ToArray(); @@ -89,7 +89,7 @@ namespace Umbraco.Extensions sb.AppendLine(JsonConvert.SerializeObject(loginProviders)); sb.AppendLine(@"});"); - return Task.FromResult(html.Raw(sb.ToString())); + return html.Raw(sb.ToString()); } /// diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs index 29216ba980..83c1b4b5f2 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.BackOffice.cs @@ -13,7 +13,7 @@ using Umbraco.Cms.Web.Common.Middleware; namespace Umbraco.Extensions { /// - /// extensions for Umbraco + /// extensions for Umbraco /// public static partial class UmbracoApplicationBuilderExtensions { @@ -22,7 +22,7 @@ namespace Umbraco.Extensions /// /// /// - public static IUmbracoMiddlewareBuilder WithBackOffice(this IUmbracoMiddlewareBuilder builder) + public static IUmbracoApplicationBuilderContext UseBackOffice(this IUmbracoApplicationBuilderContext builder) { KeepAliveSettings keepAliveSettings = builder.ApplicationServices.GetRequiredService>().Value; IHostingEnvironment hostingEnvironment = builder.ApplicationServices.GetRequiredService(); @@ -34,7 +34,7 @@ namespace Umbraco.Extensions return builder; } - public static IUmbracoEndpointBuilder UseBackOfficeEndpoints(this IUmbracoEndpointBuilder app) + public static IUmbracoEndpointBuilderContext UseBackOfficeEndpoints(this IUmbracoEndpointBuilderContext app) { // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization if (app == null) diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Installer.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Installer.cs index 2be8c6bb28..d61b955efc 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Installer.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Installer.cs @@ -13,7 +13,7 @@ namespace Umbraco.Extensions /// /// Enables the Umbraco installer /// - public static IUmbracoEndpointBuilder UseInstallerEndpoints(this IUmbracoEndpointBuilder app) + public static IUmbracoEndpointBuilderContext UseInstallerEndpoints(this IUmbracoEndpointBuilderContext app) { if (!app.RuntimeState.UmbracoCanBoot()) { diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs index 014f81fe8c..012205575a 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoApplicationBuilder.Preview.cs @@ -6,11 +6,11 @@ using Umbraco.Cms.Web.Common.ApplicationBuilder; namespace Umbraco.Extensions { /// - /// extensions for Umbraco + /// extensions for Umbraco /// public static partial class UmbracoApplicationBuilderExtensions { - public static IUmbracoEndpointBuilder UseUmbracoPreviewEndpoints(this IUmbracoEndpointBuilder app) + public static IUmbracoEndpointBuilderContext UseUmbracoPreviewEndpoints(this IUmbracoEndpointBuilderContext app) { PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); previewRoutes.CreateRoutes(app.EndpointRouteBuilder); diff --git a/src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs b/src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs index 3da2553d04..3901e96fbd 100644 --- a/src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs +++ b/src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs @@ -9,12 +9,12 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// public class AutoLinkSignInResult : SignInResult { - public static AutoLinkSignInResult FailedNotLinked => new AutoLinkSignInResult() + public static AutoLinkSignInResult FailedNotLinked { get; } = new AutoLinkSignInResult() { Succeeded = false }; - public static AutoLinkSignInResult FailedNoEmail => new AutoLinkSignInResult() + public static AutoLinkSignInResult FailedNoEmail { get; } = new AutoLinkSignInResult() { Succeeded = false }; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs index 5ccd1c0aa1..bc9f64129f 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -12,18 +13,16 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// public class BackOfficeAuthenticationBuilder : AuthenticationBuilder { - private readonly BackOfficeExternalLoginProviderOptions _loginProviderOptions; + private readonly Action _loginProviderOptions; - public BackOfficeAuthenticationBuilder(IServiceCollection services, BackOfficeExternalLoginProviderOptions loginProviderOptions) + public BackOfficeAuthenticationBuilder( + IServiceCollection services, + Action loginProviderOptions = null) : base(services) - { - _loginProviderOptions = loginProviderOptions; - } + => _loginProviderOptions = loginProviderOptions ?? (x => { }); public string SchemeForBackOffice(string scheme) - { - return Constants.Security.BackOfficeExternalAuthenticationTypePrefix + scheme; - } + => Constants.Security.BackOfficeExternalAuthenticationTypePrefix + scheme; /// /// Overridden to track the final authenticationScheme being registered for the external login @@ -43,7 +42,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security } // add our login provider to the container along with a custom options configuration - Services.AddSingleton(x => new BackOfficeExternalLoginProvider(displayName, authenticationScheme, _loginProviderOptions)); + Services.Configure(authenticationScheme, _loginProviderOptions); + base.Services.AddSingleton(services => + { + return new BackOfficeExternalLoginProvider( + authenticationScheme, + services.GetRequiredService>()); + }); Services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureBackOfficeScheme>()); return base.AddRemoteScheme(authenticationScheme, displayName, configureOptions); diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternaLoginProviderScheme.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternaLoginProviderScheme.cs new file mode 100644 index 0000000000..2732338426 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternaLoginProviderScheme.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.AspNetCore.Authentication; + +namespace Umbraco.Cms.Web.BackOffice.Security +{ + public class BackOfficeExternaLoginProviderScheme + { + public BackOfficeExternaLoginProviderScheme( + BackOfficeExternalLoginProvider externalLoginProvider, + AuthenticationScheme authenticationScheme) + { + ExternalLoginProvider = externalLoginProvider ?? throw new ArgumentNullException(nameof(externalLoginProvider)); + AuthenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme)); + } + + public BackOfficeExternalLoginProvider ExternalLoginProvider { get; } + public AuthenticationScheme AuthenticationScheme { get; } + } + +} diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProvider.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProvider.cs index ff2a64f155..9e78917087 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProvider.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProvider.cs @@ -1,4 +1,5 @@ -using System; +using System; +using Microsoft.Extensions.Options; namespace Umbraco.Cms.Web.BackOffice.Security { @@ -7,33 +8,29 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// public class BackOfficeExternalLoginProvider : IEquatable { - public BackOfficeExternalLoginProvider(string name, string authenticationType, BackOfficeExternalLoginProviderOptions properties) + public BackOfficeExternalLoginProvider( + string authenticationType, + IOptionsMonitor properties) { - Name = name ?? throw new ArgumentNullException(nameof(name)); + if (properties is null) + { + throw new ArgumentNullException(nameof(properties)); + } + AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType)); - Options = properties ?? throw new ArgumentNullException(nameof(properties)); + Options = properties.Get(authenticationType); } - public string Name { get; } + /// + /// The authentication "Scheme" + /// public string AuthenticationType { get; } + public BackOfficeExternalLoginProviderOptions Options { get; } - public override bool Equals(object obj) - { - return Equals(obj as BackOfficeExternalLoginProvider); - } - - public bool Equals(BackOfficeExternalLoginProvider other) - { - return other != null && - Name == other.Name && - AuthenticationType == other.AuthenticationType; - } - - public override int GetHashCode() - { - return HashCode.Combine(Name, AuthenticationType); - } + public override bool Equals(object obj) => Equals(obj as BackOfficeExternalLoginProvider); + public bool Equals(BackOfficeExternalLoginProvider other) => other != null && AuthenticationType == other.AuthenticationType; + public override int GetHashCode() => HashCode.Combine(AuthenticationType); } } diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviderOptions.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviderOptions.cs index fa1c1fe487..d58f1cea17 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviderOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviderOptions.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Web.BackOffice.Security +namespace Umbraco.Cms.Web.BackOffice.Security { - - /// /// Options used to configure back office external login providers /// public class BackOfficeExternalLoginProviderOptions { public BackOfficeExternalLoginProviderOptions( - string buttonStyle, string icon, + string buttonStyle, + string icon, ExternalSignInAutoLinkOptions autoLinkOptions = null, bool denyLocalLogin = false, bool autoRedirectLoginToExternalProvider = false, @@ -22,18 +21,23 @@ CustomBackOfficeView = customBackOfficeView; } - public string ButtonStyle { get; } - public string Icon { get; } + public BackOfficeExternalLoginProviderOptions() + { + } + + public string ButtonStyle { get; set; } = "btn-openid"; + + public string Icon { get; set; } = "fa fa-user"; /// /// Options used to control how users can be auto-linked/created/updated based on the external login provider /// - public ExternalSignInAutoLinkOptions AutoLinkOptions { get; } + public ExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new ExternalSignInAutoLinkOptions(); /// /// When set to true will disable all local user login functionality /// - public bool DenyLocalLogin { get; } + public bool DenyLocalLogin { get; set; } /// /// When specified this will automatically redirect to the OAuth login provider instead of prompting the user to click on the OAuth button first. @@ -42,7 +46,7 @@ /// This is generally used in conjunction with . If more than one OAuth provider specifies this, the last registered /// provider's redirect settings will win. /// - public bool AutoRedirectLoginToExternalProvider { get; } + public bool AutoRedirectLoginToExternalProvider { get; set; } /// /// A virtual path to a custom angular view that is used to replace the entire UI that renders the external login button that the user interacts with @@ -51,6 +55,6 @@ /// If this view is specified it is 100% up to the user to render the html responsible for rendering the link/un-link buttons along with showing any errors /// that occur. This overrides what Umbraco normally does by default. /// - public string CustomBackOfficeView { get; } + public string CustomBackOfficeView { get; set; } } } diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviders.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviders.cs index 7ecb4e2829..4c9799b9a4 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviders.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviders.cs @@ -1,41 +1,71 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; namespace Umbraco.Cms.Web.BackOffice.Security { + /// public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProviders { - public BackOfficeExternalLoginProviders(IEnumerable externalLogins) + private readonly Dictionary _externalLogins; + private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; + + public BackOfficeExternalLoginProviders( + IEnumerable externalLogins, + IAuthenticationSchemeProvider authenticationSchemeProvider) { - _externalLogins = externalLogins; + _externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType); + _authenticationSchemeProvider = authenticationSchemeProvider; } - private readonly IEnumerable _externalLogins; - /// - public BackOfficeExternalLoginProvider Get(string authenticationType) + public async Task GetAsync(string authenticationType) { - return _externalLogins.FirstOrDefault(x => x.AuthenticationType == authenticationType); + if (!_externalLogins.TryGetValue(authenticationType, out BackOfficeExternalLoginProvider provider)) + { + return null; + } + + // get the associated scheme + AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(provider.AuthenticationType); + + if (associatedScheme == null) + { + throw new InvalidOperationException("No authentication scheme registered for " + provider.AuthenticationType); + } + + return new BackOfficeExternaLoginProviderScheme(provider, associatedScheme); } /// public string GetAutoLoginProvider() { - var found = _externalLogins.Where(x => x.Options.AutoRedirectLoginToExternalProvider).ToList(); + var found = _externalLogins.Values.Where(x => x.Options.AutoRedirectLoginToExternalProvider).ToList(); return found.Count > 0 ? found[0].AuthenticationType : null; } /// - public IEnumerable GetBackOfficeProviders() + public async Task> GetBackOfficeProvidersAsync() { - return _externalLogins; + var providersWithSchemes = new List(); + foreach (BackOfficeExternalLoginProvider login in _externalLogins.Values) + { + // get the associated scheme + AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(login.AuthenticationType); + + providersWithSchemes.Add(new BackOfficeExternaLoginProviderScheme(login, associatedScheme)); + } + + return providersWithSchemes; } /// public bool HasDenyLocalLogin() { - var found = _externalLogins.Where(x => x.Options.DenyLocalLogin).ToList(); + var found = _externalLogins.Values.Where(x => x.Options.DenyLocalLogin).ToList(); return found.Count > 0; } } diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginsBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginsBuilder.cs index daea904a49..cab3ea10d1 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginsBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginsBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.Extensions.DependencyInjection; namespace Umbraco.Cms.Web.BackOffice.Security @@ -21,9 +21,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// /// /// - public BackOfficeExternalLoginsBuilder AddBackOfficeLogin( - BackOfficeExternalLoginProviderOptions loginProviderOptions, - Action build) + public BackOfficeExternalLoginsBuilder AddBackOfficeLogin( + Action build, + Action loginProviderOptions = null) { build(new BackOfficeAuthenticationBuilder(_services, loginProviderOptions)); return this; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index 3e921ba0f9..4b970e4b72 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs // to be able to deal with auto-linking and reduce duplicate lookups - var autoLinkOptions = _externalLogins.Get(loginInfo.LoginProvider)?.Options?.AutoLinkOptions; + var autoLinkOptions = (await _externalLogins.GetAsync(loginInfo.LoginProvider))?.ExternalLoginProvider?.Options?.AutoLinkOptions; var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); if (user == null) { diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeExternalLoginProviders.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeExternalLoginProviders.cs index d47873f3cd..2426cfcf4d 100644 --- a/src/Umbraco.Web.BackOffice/Security/IBackOfficeExternalLoginProviders.cs +++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeExternalLoginProviders.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading.Tasks; namespace Umbraco.Cms.Web.BackOffice.Security { @@ -13,13 +14,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// /// /// - BackOfficeExternalLoginProvider Get(string authenticationType); + Task GetAsync(string authenticationType); /// /// Get all registered /// /// - IEnumerable GetBackOfficeProviders(); + Task> GetBackOfficeProvidersAsync(); /// /// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs index 090ef52790..2c817d9eb8 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilder.cs @@ -1,22 +1,21 @@ using System; -using Microsoft.AspNetCore.Builder; -using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { public interface IUmbracoApplicationBuilder { /// - /// Called to include umbraco middleware + /// EXPERT call to replace the middlewares that Umbraco installs by default with a completely custom pipeline. /// - /// + /// /// - IUmbracoApplicationBuilder WithMiddleware(Action configureUmbraco); + IUmbracoEndpointBuilder WithCustomMiddleware(Action configureUmbracoMiddleware); /// - /// Final call during app building to configure endpoints + /// Called to include default middleware to run umbraco. /// - /// - void WithEndpoints(Action configureUmbraco); + /// + /// + IUmbracoEndpointBuilder WithMiddleware(Action configureUmbracoMiddleware); } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderContext.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderContext.cs new file mode 100644 index 0000000000..ecf62af32e --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderContext.cs @@ -0,0 +1,33 @@ +using System; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + /// + /// The context object used during + /// + public interface IUmbracoApplicationBuilderContext : IUmbracoApplicationBuilderServices + { + /// + /// Called to include the core umbraco middleware. + /// + void UseUmbracoCoreMiddleware(); + + /// + /// Manually runs the pre pipeline filters + /// + void RunPrePipeline(); + + /// + /// Manually runs the post pipeline filters + /// + void RunPostPipeline(); + + /// + /// Called to include all of the default umbraco required middleware. + /// + /// + /// If using this method, there is no need to use + /// + void RegisterDefaultRequiredMiddleware(); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoMiddlewareBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderServices.cs similarity index 63% rename from src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoMiddlewareBuilder.cs rename to src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderServices.cs index 78d7f28ab9..5310018969 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoMiddlewareBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoApplicationBuilderServices.cs @@ -1,13 +1,16 @@ -using System; +using System; using Microsoft.AspNetCore.Builder; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { - public interface IUmbracoMiddlewareBuilder + /// + /// Services used during the Umbraco building phase. + /// + public interface IUmbracoApplicationBuilderServices { - IRuntimeState RuntimeState { get; } - IServiceProvider ApplicationServices { get; } IApplicationBuilder AppBuilder { get; } + IServiceProvider ApplicationServices { get; } + IRuntimeState RuntimeState { get; } } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs index 31507477ae..58e0b8fec2 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilder.cs @@ -1,13 +1,13 @@ -using Microsoft.AspNetCore.Routing; +using System; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { - - /// - /// A builder to allow encapsulating the enabled routing features in Umbraco - /// - public interface IUmbracoEndpointBuilder : IUmbracoMiddlewareBuilder - { - IEndpointRouteBuilder EndpointRouteBuilder { get; } + public interface IUmbracoEndpointBuilder + { + /// + /// Final call during app building to configure endpoints + /// + /// + void WithEndpoints(Action configureUmbraco); } } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilderContext.cs b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilderContext.cs new file mode 100644 index 0000000000..6122c9ef2e --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationBuilder/IUmbracoEndpointBuilderContext.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Routing; + +namespace Umbraco.Cms.Web.Common.ApplicationBuilder +{ + + /// + /// A builder to allow encapsulating the enabled routing features in Umbraco + /// + public interface IUmbracoEndpointBuilderContext : IUmbracoApplicationBuilderServices + { + IEndpointRouteBuilder EndpointRouteBuilder { get; } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index b7acc45d22..9d30551071 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -2,61 +2,144 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using SixLabors.ImageSharp.Web.DependencyInjection; using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { /// - /// A builder to allow encapsulating the enabled endpoints in Umbraco + /// A builder used to enable middleware and endpoints required for Umbraco to operate. /// - internal class UmbracoApplicationBuilder : IUmbracoApplicationBuilder, IUmbracoMiddlewareBuilder + /// + /// This helps to ensure that everything is registered in the correct order. + /// + public class UmbracoApplicationBuilder : IUmbracoApplicationBuilder, IUmbracoEndpointBuilder, IUmbracoApplicationBuilderContext { - public UmbracoApplicationBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder) + private readonly IOptions _umbracoPipelineStartupOptions; + + public UmbracoApplicationBuilder(IApplicationBuilder appBuilder) { - ApplicationServices = services; - RuntimeState = runtimeState; - AppBuilder = appBuilder; + AppBuilder = appBuilder ?? throw new ArgumentNullException(nameof(appBuilder)); + ApplicationServices = appBuilder.ApplicationServices; + RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); + _umbracoPipelineStartupOptions = ApplicationServices.GetRequiredService>(); } public IServiceProvider ApplicationServices { get; } public IRuntimeState RuntimeState { get; } public IApplicationBuilder AppBuilder { get; } - public IUmbracoApplicationBuilder WithMiddleware(Action configureUmbraco) + /// + public IUmbracoEndpointBuilder WithCustomMiddleware(Action configureUmbracoMiddleware) { - IOptions startupOptions = ApplicationServices.GetRequiredService>(); - RunPostPipeline(startupOptions.Value); + if (configureUmbracoMiddleware is null) + { + throw new ArgumentNullException(nameof(configureUmbracoMiddleware)); + } - configureUmbraco(this); + configureUmbracoMiddleware(this); return this; } - public void WithEndpoints(Action configureUmbraco) + /// + public IUmbracoEndpointBuilder WithMiddleware(Action configureUmbracoMiddleware) + { + if (configureUmbracoMiddleware is null) + { + throw new ArgumentNullException(nameof(configureUmbracoMiddleware)); + } + + RunPrePipeline(); + + RegisterDefaultRequiredMiddleware(); + + RunPostPipeline(); + + configureUmbracoMiddleware(this); + + return this; + } + + /// + public void WithEndpoints(Action configureUmbraco) { IOptions startupOptions = ApplicationServices.GetRequiredService>(); - RunPreEndpointsPipeline(startupOptions.Value); + RunPreEndpointsPipeline(); AppBuilder.UseEndpoints(endpoints => { - var umbAppBuilder = (IUmbracoEndpointBuilder)ActivatorUtilities.CreateInstance( + var umbAppBuilder = (IUmbracoEndpointBuilderContext)ActivatorUtilities.CreateInstance( ApplicationServices, new object[] { AppBuilder, endpoints }); configureUmbraco(umbAppBuilder); }); } - private void RunPostPipeline(UmbracoPipelineOptions startupOptions) + /// + /// Registers the default required middleware to run Umbraco + /// + /// + public void RegisterDefaultRequiredMiddleware() { - foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + UseUmbracoCoreMiddleware(); + + AppBuilder.UseStatusCodePages(); + + // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. + AppBuilder.UseImageSharp(); + AppBuilder.UseStaticFiles(); + AppBuilder.UseUmbracoPluginsStaticFiles(); + + // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one + // will execute after endpoint routing. The ordering of everything is quite important here, see + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0 + // where we need to have UseAuthentication and UseAuthorization proceeding this call but before + // endpoints are defined. + AppBuilder.UseRouting(); + AppBuilder.UseAuthentication(); + AppBuilder.UseAuthorization(); + + // This must come after auth because the culture is based on the auth'd user + AppBuilder.UseRequestLocalization(); + + // Must be called after UseRouting and before UseEndpoints + AppBuilder.UseSession(); + + // DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline, + // endpoints are terminating middleware. All of our endpoints are declared in ext of IUmbracoApplicationBuilder + } + + public void UseUmbracoCoreMiddleware() + { + AppBuilder.UseUmbracoCore(); + AppBuilder.UseUmbracoRequestLogging(); + + // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed + // before endpoint routing middleware. + AppBuilder.UseUmbracoRouting(); + } + + public void RunPrePipeline() + { + foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) + { + filter.OnPrePipeline(AppBuilder); + } + } + + public void RunPostPipeline() + { + foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) { filter.OnPostPipeline(AppBuilder); } } - private void RunPreEndpointsPipeline(UmbracoPipelineOptions startupOptions) + private void RunPreEndpointsPipeline() { - foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) + foreach (IUmbracoPipelineFilter filter in _umbracoPipelineStartupOptions.Value.PipelineFilters) { filter.OnEndpoints(AppBuilder); } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs index 56d856a22a..86e8f3e957 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoEndpointBuilder.cs @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder /// /// A builder to allow encapsulating the enabled endpoints in Umbraco /// - internal class UmbracoEndpointBuilder : IUmbracoEndpointBuilder + internal class UmbracoEndpointBuilder : IUmbracoEndpointBuilderContext { public UmbracoEndpointBuilder(IServiceProvider services, IRuntimeState runtimeState, IApplicationBuilder appBuilder, IEndpointRouteBuilder endpointRouteBuilder) { diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 64839d2dd3..cc035d116b 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Serilog.Context; -using SixLabors.ImageSharp.Web.DependencyInjection; using StackExchange.Profiling; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -26,63 +25,7 @@ namespace Umbraco.Extensions /// Configures and use services required for using Umbraco /// public static IUmbracoApplicationBuilder UseUmbraco(this IApplicationBuilder app) - { - // TODO: Should we do some checks like this to verify that the corresponding "Add" methods have been called for the - // corresponding "Use" methods? - // https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Mvc/Mvc.Core/src/Builder/MvcApplicationBuilderExtensions.cs#L132 - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - IOptions startupOptions = app.ApplicationServices.GetRequiredService>(); - app.RunPrePipeline(startupOptions.Value); - - app.UseUmbracoCore(); - app.UseUmbracoRequestLogging(); - - // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed - // before endpoint routing middleware. - app.UseUmbracoRouting(); - - app.UseStatusCodePages(); - - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. - // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? - app.UseImageSharp(); - app.UseStaticFiles(); - app.UseUmbracoPlugins(); - - // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one - // will execute after endpoint routing. The ordering of everything is quite important here, see - // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0 - // where we need to have UseAuthentication and UseAuthorization proceeding this call but before - // endpoints are defined. - app.UseRouting(); - app.UseAuthentication(); - app.UseAuthorization(); - - // This must come after auth because the culture is based on the auth'd user - app.UseRequestLocalization(); - - // Must be called after UseRouting and before UseEndpoints - app.UseSession(); - - // DO NOT PUT ANY UseEndpoints declarations here!! Those must all come very last in the pipeline, - // endpoints are terminating middleware. All of our endpoints are declared in ext of IUmbracoApplicationBuilder - - return ActivatorUtilities.CreateInstance( - app.ApplicationServices, - new object[] { app }); - } - - private static void RunPrePipeline(this IApplicationBuilder app, UmbracoPipelineOptions startupOptions) - { - foreach (IUmbracoPipelineFilter filter in startupOptions.PipelineFilters) - { - filter.OnPrePipeline(app); - } - } + => new UmbracoApplicationBuilder(app); /// /// Returns true if Umbraco is greater than @@ -158,7 +101,12 @@ namespace Umbraco.Extensions return app; } - public static IApplicationBuilder UseUmbracoPlugins(this IApplicationBuilder app) + /// + /// Allow static file access for App_Plugins folders + /// + /// + /// + public static IApplicationBuilder UseUmbracoPluginsStaticFiles(this IApplicationBuilder app) { var hostingEnvironment = app.ApplicationServices.GetRequiredService(); var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs index 0d8c7df72b..74b67c36a6 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.RuntimeMinification.cs @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Web.Common.Extensions /// /// Enables runtime minification for Umbraco /// - public static IUmbracoEndpointBuilder UseUmbracoRuntimeMinificationEndpoints(this IUmbracoEndpointBuilder app) + public static IUmbracoEndpointBuilderContext UseUmbracoRuntimeMinificationEndpoints(this IUmbracoEndpointBuilderContext app) { if (app == null) { diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs index dd52b397d3..80729412a3 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -198,6 +198,9 @@ namespace Umbraco.Cms.Web.Common.Security // code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs // we also override to set the current HttpContext principal since this isn't done by default + // we also need to call our handle login to ensure all date/events are set + await HandleSignIn(user, user.UserName, SignInResult.Success); + var userPrincipal = await CreateUserPrincipalAsync(user); foreach (var claim in additionalClaims) { @@ -363,7 +366,7 @@ namespace Umbraco.Cms.Web.Common.Security await Context.SignOutAsync(ExternalAuthenticationType); } if (loginProvider == null) - { + { await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") }); } else diff --git a/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js b/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js index b44f79dd65..10092aaf38 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js @@ -5,63 +5,66 @@ **/ function externalLoginInfoService(externalLoginInfo, umbRequestHelper) { - function getLoginProvider(provider) { - if (provider) { - var found = _.find(externalLoginInfo.providers, x => x.authType == provider); - return found; - } - return null; + function getLoginProvider(provider) { + if (provider) { + var found = _.find(externalLoginInfo.providers, x => x.authType == provider); + return found; } + return null; + } - function getLoginProviderView(provider) { - if (provider && provider.properties && provider.properties.CustomBackOfficeView) { - return umbRequestHelper.convertVirtualToAbsolutePath(provider.properties.CustomBackOfficeView); - } - return null; + function getLoginProviderView(provider) { + if (provider && provider.properties && provider.properties.CustomBackOfficeView) { + return umbRequestHelper.convertVirtualToAbsolutePath(provider.properties.CustomBackOfficeView); } + return null; + } - /** - * Returns true if any provider denies local login if `provider` is null, else whether the passed - * @param {any} provider - */ - function hasDenyLocalLogin(provider) { - if (!provider) { - return _.some(externalLoginInfo.providers, x => x.properties && (x.properties.DenyLocalLogin === true)); - } - else { - return provider && provider.properties && (provider.properties.DenyLocalLogin === true); - } + /** + * Returns true if any provider denies local login if `provider` is null, else whether the passed + * @param {any} provider + */ + function hasDenyLocalLogin(provider) { + if (!provider) { + return _.some(externalLoginInfo.providers, x => x.properties && (x.properties.DenyLocalLogin === true)); } - - /** - * Returns all login providers - */ - function getLoginProviders() { - return externalLoginInfo.providers; + else { + return provider && provider.properties && (provider.properties.DenyLocalLogin === true); } + } - /** Returns all logins providers that have options that the user can interact with */ - function getLoginProvidersWithOptions() { - // only include providers that allow manual linking or ones that provide a custom view - var providers = _.filter(externalLoginInfo.providers, x => { - // transform the data and also include the custom view as a nicer property - x.customView = getLoginProviderView(x); - if (x.customView) { - return true; - } - else { - return x.properties.AutoLinkOptions.AllowManualLinking; - } - }); - return providers; - } + /** + * Returns all login providers + */ + function getLoginProviders() { + return externalLoginInfo.providers; + } - return { - hasDenyLocalLogin: hasDenyLocalLogin, - getLoginProvider: getLoginProvider, - getLoginProviders: getLoginProviders, - getLoginProvidersWithOptions: getLoginProvidersWithOptions, - getLoginProviderView: getLoginProviderView - }; + /** Returns all logins providers that have options that the user can interact with */ + function getLoginProvidersWithOptions() { + // only include providers that allow manual linking or ones that provide a custom view + var providers = _.filter(externalLoginInfo.providers, x => { + // transform the data and also include the custom view as a nicer property + x.customView = getLoginProviderView(x); + if (x.customView) { + return true; + } + else if (x.properties.AutoLinkOptions) { + return x.properties.AutoLinkOptions.AllowManualLinking; + } + else { + return false; + } + }); + return providers; + } + + return { + hasDenyLocalLogin: hasDenyLocalLogin, + getLoginProvider: getLoginProvider, + getLoginProviders: getLoginProviders, + getLoginProvidersWithOptions: getLoginProvidersWithOptions, + getLoginProviderView: getLoginProviderView + }; } angular.module('umbraco.services').factory('externalLoginInfoService', externalLoginInfoService); diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 0419a8c0e4..73eeef864f 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -62,8 +62,8 @@ namespace Umbraco.Cms.Web.UI.NetCore app.UseUmbraco() .WithMiddleware(u => { - u.WithBackOffice(); - u.WithWebsite(); + u.UseBackOffice(); + u.UseWebsite(); }) .WithEndpoints(u => { diff --git a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs index 2b4f0ac491..5ba0ffc3fe 100644 --- a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Text; using System.Text.Encodings.Web; +using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.DataProtection; @@ -18,6 +19,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Core.Web.Mvc; using Umbraco.Cms.Web.Common.Controllers; @@ -85,7 +87,7 @@ namespace Umbraco.Extensions } - public static IHtmlContent CachedPartial( + public static async Task CachedPartialAsync( this IHtmlHelper htmlHelper, string partialViewName, object model, @@ -115,11 +117,9 @@ namespace Umbraco.Extensions if (cacheByMember) { - // TODO reintroduce when members are migrated - throw new NotImplementedException("Reintroduce when members are migrated"); - // var helper = Current.MembershipHelper; - // var currentMember = helper.GetCurrentMember(); - // cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? 0); + var memberManager = htmlHelper.ViewContext.HttpContext.RequestServices.GetRequiredService(); + var currentMember = await memberManager.GetCurrentMemberAsync(); + cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? "0"); } if (contextualKeyBuilder != null) diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index 843d3030ec..c549609397 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -18,7 +18,7 @@ namespace Umbraco.Extensions /// /// /// - public static IUmbracoMiddlewareBuilder WithWebsite(this IUmbracoMiddlewareBuilder builder) + public static IUmbracoApplicationBuilderContext UseWebsite(this IUmbracoApplicationBuilderContext builder) { builder.AppBuilder.UseMiddleware(); builder.AppBuilder.UseMiddleware(); @@ -28,7 +28,7 @@ namespace Umbraco.Extensions /// /// Sets up routes for the front-end umbraco website /// - public static IUmbracoEndpointBuilder UseWebsiteEndpoints(this IUmbracoEndpointBuilder builder) + public static IUmbracoEndpointBuilderContext UseWebsiteEndpoints(this IUmbracoEndpointBuilderContext builder) { if (builder == null) {